diff --git a/Dockerfile b/Dockerfile index 25061e7295732..fc9a64048c04d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -173,6 +173,9 @@ COPY --from=downloader --chmod=755 /deno /usr/bin/deno COPY --from=oven/bun:1.1.7 /usr/local/bin/bun /usr/bin/bun +COPY --from=php:8.3.7-cli /usr/local/bin/php /usr/bin/php +COPY --from=composer:2.7.6 /usr/bin/composer /usr/bin/composer + # add the docker client to call docker from a worker if enabled COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/ diff --git a/backend/.sqlx/query-0a686ca61444d7ad7484071727aa039a6ea6697e5a49a633b767c052aa3e0a18.json b/backend/.sqlx/query-0a686ca61444d7ad7484071727aa039a6ea6697e5a49a633b767c052aa3e0a18.json index 053857a0a0b98..3737e1cf1e7b2 100644 --- a/backend/.sqlx/query-0a686ca61444d7ad7484071727aa039a6ea6697e5a49a633b767c052aa3e0a18.json +++ b/backend/.sqlx/query-0a686ca61444d7ad7484071727aa039a6ea6697e5a49a633b767c052aa3e0a18.json @@ -54,7 +54,8 @@ "snowflake", "graphql", "powershell", - "mssql" + "mssql", + "php" ] } } diff --git a/backend/.sqlx/query-2f42460fdd8aa125c8fd46b3cd02e47f57de0f073d3ce3bc7d21a7e404a83b5c.json b/backend/.sqlx/query-2f42460fdd8aa125c8fd46b3cd02e47f57de0f073d3ce3bc7d21a7e404a83b5c.json index 030a85c00040f..6996508ba696a 100644 --- a/backend/.sqlx/query-2f42460fdd8aa125c8fd46b3cd02e47f57de0f073d3ce3bc7d21a7e404a83b5c.json +++ b/backend/.sqlx/query-2f42460fdd8aa125c8fd46b3cd02e47f57de0f073d3ce3bc7d21a7e404a83b5c.json @@ -48,7 +48,8 @@ "snowflake", "graphql", "powershell", - "mssql" + "mssql", + "php" ] } } diff --git a/backend/.sqlx/query-620ddf29c5e867079df4c2aa6e80bccb19beeb9ddfa308ca97f254cd5ba8157e.json b/backend/.sqlx/query-620ddf29c5e867079df4c2aa6e80bccb19beeb9ddfa308ca97f254cd5ba8157e.json index 4598ac08df958..9045babf7c20d 100644 --- a/backend/.sqlx/query-620ddf29c5e867079df4c2aa6e80bccb19beeb9ddfa308ca97f254cd5ba8157e.json +++ b/backend/.sqlx/query-620ddf29c5e867079df4c2aa6e80bccb19beeb9ddfa308ca97f254cd5ba8157e.json @@ -68,7 +68,8 @@ "snowflake", "graphql", "powershell", - "mssql" + "mssql", + "php" ] } } diff --git a/backend/.sqlx/query-6b313cc9a57ae3c943bda4a3213f7f6231a44b6ef5a52754074d136007f4f72a.json b/backend/.sqlx/query-6b313cc9a57ae3c943bda4a3213f7f6231a44b6ef5a52754074d136007f4f72a.json index 72c175aff7f5a..2b79c631bc49f 100644 --- a/backend/.sqlx/query-6b313cc9a57ae3c943bda4a3213f7f6231a44b6ef5a52754074d136007f4f72a.json +++ b/backend/.sqlx/query-6b313cc9a57ae3c943bda4a3213f7f6231a44b6ef5a52754074d136007f4f72a.json @@ -43,7 +43,8 @@ "snowflake", "graphql", "powershell", - "mssql" + "mssql", + "php" ] } } diff --git a/backend/.sqlx/query-8e7ff45c5378c3a3406ba94dc0653afa5d28c072203617c598caefaaf1bafcfb.json b/backend/.sqlx/query-8e7ff45c5378c3a3406ba94dc0653afa5d28c072203617c598caefaaf1bafcfb.json index e1cdc4e0c7ff0..ecc0f8baccfd6 100644 --- a/backend/.sqlx/query-8e7ff45c5378c3a3406ba94dc0653afa5d28c072203617c598caefaaf1bafcfb.json +++ b/backend/.sqlx/query-8e7ff45c5378c3a3406ba94dc0653afa5d28c072203617c598caefaaf1bafcfb.json @@ -34,7 +34,8 @@ "snowflake", "graphql", "powershell", - "mssql" + "mssql", + "php" ] } } diff --git a/backend/.sqlx/query-9d3556319411a27a875bf6cf0e5eda837cc63e4d8be912c0b5bfeea4a0c8db2e.json b/backend/.sqlx/query-9d3556319411a27a875bf6cf0e5eda837cc63e4d8be912c0b5bfeea4a0c8db2e.json index f296c4afc69e4..a8cde35c5c731 100644 --- a/backend/.sqlx/query-9d3556319411a27a875bf6cf0e5eda837cc63e4d8be912c0b5bfeea4a0c8db2e.json +++ b/backend/.sqlx/query-9d3556319411a27a875bf6cf0e5eda837cc63e4d8be912c0b5bfeea4a0c8db2e.json @@ -48,7 +48,8 @@ "snowflake", "graphql", "powershell", - "mssql" + "mssql", + "php" ] } } diff --git a/backend/.sqlx/query-b69891c25dd029b1a54e97ace292433e1485324ff7dc802fe75d21c8c6db1d42.json b/backend/.sqlx/query-b69891c25dd029b1a54e97ace292433e1485324ff7dc802fe75d21c8c6db1d42.json index f95d9d95b264d..aed0e14f2ef04 100644 --- a/backend/.sqlx/query-b69891c25dd029b1a54e97ace292433e1485324ff7dc802fe75d21c8c6db1d42.json +++ b/backend/.sqlx/query-b69891c25dd029b1a54e97ace292433e1485324ff7dc802fe75d21c8c6db1d42.json @@ -48,7 +48,8 @@ "snowflake", "graphql", "powershell", - "mssql" + "mssql", + "php" ] } } diff --git a/backend/.sqlx/query-d5a8614286c170e0d175903cd1b53ff66b37ed8110a0b67aedb9f25e6a7383e1.json b/backend/.sqlx/query-d5a8614286c170e0d175903cd1b53ff66b37ed8110a0b67aedb9f25e6a7383e1.json index a9e46342cc6f1..e83fb44087d2b 100644 --- a/backend/.sqlx/query-d5a8614286c170e0d175903cd1b53ff66b37ed8110a0b67aedb9f25e6a7383e1.json +++ b/backend/.sqlx/query-d5a8614286c170e0d175903cd1b53ff66b37ed8110a0b67aedb9f25e6a7383e1.json @@ -74,7 +74,8 @@ "snowflake", "graphql", "powershell", - "mssql" + "mssql", + "php" ] } } diff --git a/backend/.sqlx/query-ef132ac8d79579b08d7359789b6f22991f51e1c945efc2924df6253d62b83bba.json b/backend/.sqlx/query-ef132ac8d79579b08d7359789b6f22991f51e1c945efc2924df6253d62b83bba.json index 73af3f369c0f3..f191057047e7e 100644 --- a/backend/.sqlx/query-ef132ac8d79579b08d7359789b6f22991f51e1c945efc2924df6253d62b83bba.json +++ b/backend/.sqlx/query-ef132ac8d79579b08d7359789b6f22991f51e1c945efc2924df6253d62b83bba.json @@ -48,7 +48,8 @@ "snowflake", "graphql", "powershell", - "mssql" + "mssql", + "php" ] } } diff --git a/backend/Cargo.lock b/backend/Cargo.lock index c4e9a248e1898..7f8d2f924f95b 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -208,6 +208,15 @@ dependencies = [ "password-hash 0.5.0", ] +[[package]] +name = "ariadne" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1cb2a2046bea8ce5e875551f5772024882de0b540c7f93dfc5d6cf1ca8b030c" +dependencies = [ + "yansi", +] + [[package]] name = "array-init" version = "2.1.0" @@ -5097,6 +5106,18 @@ dependencies = [ "siphasher", ] +[[package]] +name = "php-parser-rs" +version = "0.1.3" +source = "git+https://github.com/php-rust-tools/parser?rev=ec4cb411dec09450946ef57920b7ffced7f6495d#ec4cb411dec09450946ef57920b7ffced7f6495d" +dependencies = [ + "ariadne", + "clap", + "schemars", + "serde", + "serde_json", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -9876,6 +9897,20 @@ dependencies = [ "windmill-parser", ] +[[package]] +name = "windmill-parser-php" +version = "1.327.0" +dependencies = [ + "anyhow", + "convert_case 0.6.0", + "itertools 0.10.5", + "lazy_static", + "php-parser-rs", + "regex", + "serde_json", + "windmill-parser", +] + [[package]] name = "windmill-parser-py" version = "1.328.0" @@ -9946,6 +9981,7 @@ dependencies = [ "windmill-parser-bash", "windmill-parser-go", "windmill-parser-graphql", + "windmill-parser-php", "windmill-parser-py", "windmill-parser-sql", "windmill-parser-ts", @@ -10006,6 +10042,7 @@ dependencies = [ "bytes", "chrono", "const_format", + "convert_case 0.6.0", "deno_ast", "deno_console", "deno_core", @@ -10056,6 +10093,7 @@ dependencies = [ "windmill-parser-bash", "windmill-parser-go", "windmill-parser-graphql", + "windmill-parser-php", "windmill-parser-py", "windmill-parser-py-imports", "windmill-parser-sql", @@ -10300,6 +10338,12 @@ dependencies = [ "lzma-sys", ] +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "yoke" version = "0.7.3" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 5eef472fe10c4..23ed332d74a7b 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -107,6 +107,7 @@ windmill-parser-go = { path = "./parsers/windmill-parser-go" } windmill-parser-bash = { path = "./parsers/windmill-parser-bash" } windmill-parser-sql = { path = "./parsers/windmill-parser-sql" } windmill-parser-graphql = { path = "./parsers/windmill-parser-graphql" } +windmill-parser-php = { path = "./parsers/windmill-parser-php" } windmill-api-client = { path = "./windmill-api-client" } axum = { version = "^0.7", features = ["multipart"] } @@ -139,6 +140,7 @@ rand_core = { version = "^0", features = ["std"] } magic-crypt = "^3" git-version = "^0" rustpython-parser = { git = "https://github.com/RustPython/Parser", rev = "9ce55aefdeb35e2f706ce0b02d5a2dfe6295fc57" } +php-parser-rs = { git = "https://github.com/php-rust-tools/parser", rev = "ec4cb411dec09450946ef57920b7ffced7f6495d" } cron = "^0" mail-send = { version = "0.4.0", features = ["builder"], default-features=false } urlencoding = "^2" diff --git a/backend/migrations/20240513125035_add_php_lang.down.sql b/backend/migrations/20240513125035_add_php_lang.down.sql new file mode 100644 index 0000000000000..d2f607c5b8bd6 --- /dev/null +++ b/backend/migrations/20240513125035_add_php_lang.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/backend/migrations/20240513125035_add_php_lang.up.sql b/backend/migrations/20240513125035_add_php_lang.up.sql new file mode 100644 index 0000000000000..4e56329d467b0 --- /dev/null +++ b/backend/migrations/20240513125035_add_php_lang.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +ALTER TYPE SCRIPT_LANG ADD VALUE IF NOT EXISTS 'php'; \ No newline at end of file diff --git a/backend/migrations/20240514092225_add_php_tag_and_default_lang.down.sql b/backend/migrations/20240514092225_add_php_tag_and_default_lang.down.sql new file mode 100644 index 0000000000000..d2f607c5b8bd6 --- /dev/null +++ b/backend/migrations/20240514092225_add_php_tag_and_default_lang.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/backend/migrations/20240514092225_add_php_tag_and_default_lang.up.sql b/backend/migrations/20240514092225_add_php_tag_and_default_lang.up.sql new file mode 100644 index 0000000000000..bfd521d946727 --- /dev/null +++ b/backend/migrations/20240514092225_add_php_tag_and_default_lang.up.sql @@ -0,0 +1,4 @@ +-- Add up migration script here +UPDATE config set config = '{"worker_tags": ["deno", "python3", "go", "bash", "powershell", "dependency", "flow", "hub", "other", "bun", "php"]}'::jsonb where name = 'worker__default' and config @> '{"worker_tags": ["deno", "python3", "go", "bash", "powershell", "dependency", "flow", "hub", "other", "bun"]}'::jsonb; +UPDATE workspace_settings SET default_scripts = jsonb_set(default_scripts, '{order}', default_scripts->'order' || '["php"]'::jsonb) WHERE default_scripts IS NOT NULL AND default_scripts->'order' IS NOT NULL AND NOT default_scripts->'order' @> '["php"]'::jsonb; + diff --git a/backend/parsers/windmill-parser-php/Cargo.toml b/backend/parsers/windmill-parser-php/Cargo.toml new file mode 100644 index 0000000000000..05d86a6c67d57 --- /dev/null +++ b/backend/parsers/windmill-parser-php/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "windmill-parser-php" +version.workspace = true +edition.workspace = true +authors.workspace = true + +[lib] +name = "windmill_parser_php" +path = "./src/lib.rs" + +[dependencies] +windmill-parser.workspace = true +itertools.workspace = true +serde_json.workspace = true +anyhow.workspace = true +php-parser-rs.workspace = true +convert_case.workspace = true +lazy_static.workspace = true +regex.workspace = true \ No newline at end of file diff --git a/backend/parsers/windmill-parser-php/src/lib.rs b/backend/parsers/windmill-parser-php/src/lib.rs new file mode 100644 index 0000000000000..adf2c94f0cecc --- /dev/null +++ b/backend/parsers/windmill-parser-php/src/lib.rs @@ -0,0 +1,166 @@ +use convert_case::{Case, Casing}; +use regex::Regex; +use serde_json::Value; +use windmill_parser::{Arg, MainArgSignature, Typ}; + +use php_parser_rs::parser::{ + self, + ast::{ + data_type::Type, + functions::{FunctionParameterList, FunctionStatement}, + literals::Literal, + Expression, Statement, + }, +}; + +lazy_static::lazy_static! { + static ref RE_SNK_CASE: Regex = Regex::new(r"_(\d)").unwrap(); +} + +fn to_snake_case(s: String) -> String { + let r = s.to_case(Case::Snake); + + // s_3 => s3 + RE_SNK_CASE.replace_all(&r, "$1").to_string() +} + +fn parse_php_type(e: Type) -> Typ { + match e { + Type::Float(_) => Typ::Float, + Type::Boolean(_) => Typ::Bool, + Type::Integer(_) => Typ::Int, + Type::String(_) => Typ::Str(None), + Type::Array(_) => Typ::List(Box::new(Typ::Str(None))), + Type::Object(_) => Typ::Object(vec![]), + Type::Named(_, name) => Typ::Resource(to_snake_case(name.to_string())), + _ => Typ::Unknown, + } +} + +fn parse_default_expr(e: Expression) -> Option { + match e { + Expression::Literal(l) => match l { + Literal::String(s) => Some(Value::String(s.value.to_string())), + Literal::Integer(i) => match i.value.to_string().parse() { + Ok(i) => Some(Value::Number(i)), + Err(_) => None, + }, + Literal::Float(f) => match f.value.to_string().parse() { + Ok(i) => Some(Value::Number(i)), + Err(_) => None, + }, + }, + Expression::Bool(b) => Some(Value::Bool(b.value)), + _ => None, + } +} + +pub fn parse_php_signature( + code: &str, + override_main: Option, +) -> anyhow::Result { + let main_name = override_main.unwrap_or("main".to_string()); + + let ast = parser::parse(code) + .map_err(|e| anyhow::anyhow!("Error parsing code: {}", e.to_string()))?; + + let params = ast.into_iter().find_map(|x| match x { + Statement::Function(FunctionStatement { + name, + parameters: FunctionParameterList { parameters, .. }, + .. + }) if name.to_string() == main_name => Some(parameters), + _ => None, + }); + + if let Some(params) = params { + let args = params + .into_iter() + .map(|x| { + let typ = x.data_type.map_or(Typ::Unknown, |e| parse_php_type(e)); + let default = x.default.map_or(None, |e| parse_default_expr(e)); + Arg { + otyp: None, + name: x.name.to_string().trim_start_matches('$').to_string(), + typ, + has_default: default.is_some(), + default, + } + }) + .collect(); + + Ok(MainArgSignature { star_args: false, star_kwargs: false, args }) + } else { + Err(anyhow::anyhow!( + "main function was not findable".to_string(), + )) + } +} + +#[cfg(test)] +mod tests { + + use serde_json::Number; + + use super::*; + + #[test] + fn test_parse_php_sig() -> anyhow::Result<()> { + let code = " +" ], - "version": "1.318.0", + "version": "1.327.0", "files": [ "windmill_parser_wasm_bg.wasm", "windmill_parser_wasm.js", diff --git a/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm.d.ts b/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm.d.ts index 21500d4888ffd..30e4bc2f54788 100644 --- a/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm.d.ts +++ b/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm.d.ts @@ -70,6 +70,11 @@ export function parse_db_resource(code: string): string | undefined; * @returns {string} */ export function parse_graphql(code: string): string; +/** +* @param {string} code +* @returns {string} +*/ +export function parse_php(code: string): string; export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; @@ -89,6 +94,7 @@ export interface InitOutput { readonly parse_mssql: (a: number, b: number, c: number) => void; readonly parse_db_resource: (a: number, b: number, c: number) => void; readonly parse_graphql: (a: number, b: number, c: number) => void; + readonly parse_php: (a: number, b: number, c: number) => void; readonly __wbindgen_malloc: (a: number, b: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; readonly __wbindgen_add_to_stack_pointer: (a: number) => number; diff --git a/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm.js b/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm.js index b69351023e6ca..bb061da15cadf 100644 --- a/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm.js +++ b/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm.js @@ -97,15 +97,6 @@ function getInt32Memory0() { return cachedInt32Memory0; } -const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); - -if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; - -function getStringFromWasm0(ptr, len) { - ptr = ptr >>> 0; - return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); -} - function addHeapObject(obj) { if (heap_next === heap.length) heap.push(heap.length + 1); const idx = heap_next; @@ -115,6 +106,15 @@ function addHeapObject(obj) { return idx; } +const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); + +if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + let cachedFloat64Memory0 = null; function getFloat64Memory0() { @@ -519,6 +519,29 @@ export function parse_graphql(code) { } } +/** +* @param {string} code +* @returns {string} +*/ +export function parse_php(code) { + let deferred2_0; + let deferred2_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + wasm.parse_php(retptr, ptr0, len0); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + deferred2_0 = r0; + deferred2_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_free(deferred2_0, deferred2_1, 1); + } +} + function handleError(f, args) { try { return f.apply(this, args); @@ -564,6 +587,10 @@ function __wbg_get_imports() { imports.wbg.__wbindgen_object_drop_ref = function(arg0) { takeObject(arg0); }; + imports.wbg.__wbg_eval_f2b8ae7add53626d = function(arg0, arg1) { + const ret = eval(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }; imports.wbg.__wbindgen_string_get = function(arg0, arg1) { const obj = getObject(arg1); const ret = typeof(obj) === 'string' ? obj : undefined; @@ -572,10 +599,6 @@ function __wbg_get_imports() { getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; }; - imports.wbg.__wbindgen_error_new = function(arg0, arg1) { - const ret = new Error(getStringFromWasm0(arg0, arg1)); - return addHeapObject(ret); - }; imports.wbg.__wbindgen_boolean_get = function(arg0) { const v = getObject(arg0); const ret = typeof(v) === 'boolean' ? (v ? 1 : 0) : 2; @@ -597,6 +620,10 @@ function __wbg_get_imports() { const ret = BigInt.asUintN(64, arg0); return addHeapObject(ret); }; + imports.wbg.__wbindgen_error_new = function(arg0, arg1) { + const ret = new Error(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }; imports.wbg.__wbindgen_number_get = function(arg0, arg1) { const obj = getObject(arg1); const ret = typeof(obj) === 'number' ? obj : undefined; @@ -612,10 +639,6 @@ function __wbg_get_imports() { const ret = getObject(arg0) in getObject(arg1); return ret; }; - imports.wbg.__wbg_eval_33c4985197d1feaf = function(arg0, arg1) { - const ret = eval(getStringFromWasm0(arg0, arg1)); - return addHeapObject(ret); - }; imports.wbg.__wbindgen_jsval_loose_eq = function(arg0, arg1) { const ret = getObject(arg0) == getObject(arg1); return ret; diff --git a/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm_bg.wasm b/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm_bg.wasm index 90d4cb855edf4..727d8a9ae855d 100644 Binary files a/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm_bg.wasm and b/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm_bg.wasm differ diff --git a/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm_bg.wasm.d.ts b/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm_bg.wasm.d.ts index 884e4bea8874f..1fdcb8941c20d 100644 --- a/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm_bg.wasm.d.ts +++ b/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm_bg.wasm.d.ts @@ -15,6 +15,7 @@ export function parse_snowflake(a: number, b: number, c: number): void; export function parse_mssql(a: number, b: number, c: number): void; export function parse_db_resource(a: number, b: number, c: number): void; export function parse_graphql(a: number, b: number, c: number): void; +export function parse_php(a: number, b: number, c: number): void; export function __wbindgen_malloc(a: number, b: number): number; export function __wbindgen_realloc(a: number, b: number, c: number, d: number): number; export function __wbindgen_add_to_stack_pointer(a: number): number; diff --git a/backend/parsers/windmill-parser-wasm/src/lib.rs b/backend/parsers/windmill-parser-wasm/src/lib.rs index 5619e0142d4e7..5218357656b68 100644 --- a/backend/parsers/windmill-parser-wasm/src/lib.rs +++ b/backend/parsers/windmill-parser-wasm/src/lib.rs @@ -92,3 +92,8 @@ pub fn parse_db_resource(code: &str) -> Option { pub fn parse_graphql(code: &str) -> String { wrap_sig(windmill_parser_graphql::parse_graphql_sig(code)) } + +#[wasm_bindgen] +pub fn parse_php(code: &str) -> String { + wrap_sig(windmill_parser_php::parse_php_signature(code, None)) +} diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 63d64726ceabc..f5ca07d5aa4c1 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -8406,6 +8406,7 @@ components: graphql, nativets, bun, + php, ] kind: type: string @@ -8501,6 +8502,7 @@ components: graphql, nativets, bun, + php, ] kind: type: string @@ -8719,6 +8721,7 @@ components: graphql, nativets, bun, + php, ] email: type: string @@ -8829,6 +8832,7 @@ components: graphql, nativets, bun, + php, ] is_skipped: type: boolean @@ -9343,6 +9347,7 @@ components: graphql, nativets, bun, + php ] tag: type: string @@ -10380,6 +10385,7 @@ components: graphql, nativets, bun, + php ] required: - raw_code diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index 0073d0a5f3b80..150ec4adba1b4 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -557,7 +557,8 @@ async fn create_script_internal<'c>( let lock = if !(ns.language == ScriptLang::Python3 || ns.language == ScriptLang::Go || ns.language == ScriptLang::Bun - || ns.language == ScriptLang::Deno) + || ns.language == ScriptLang::Deno + || ns.language == ScriptLang::Php) { Some(String::new()) } else { diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index b242e7ef841d7..f5ec2865784e9 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -2473,6 +2473,7 @@ async fn tarball_workspace( "bun.ts" } } + ScriptLang::Php => "php", }; archive .write_to_archive(&script.content, &format!("{}.{}", script.path, ext)) diff --git a/backend/windmill-common/src/scripts.rs b/backend/windmill-common/src/scripts.rs index b7ab659b5bb6d..99d77e9f81607 100644 --- a/backend/windmill-common/src/scripts.rs +++ b/backend/windmill-common/src/scripts.rs @@ -39,6 +39,7 @@ pub enum ScriptLang { Snowflake, Graphql, Mssql, + Php, } impl ScriptLang { @@ -57,6 +58,7 @@ impl ScriptLang { ScriptLang::Snowflake => "snowflake", ScriptLang::Mssql => "mssql", ScriptLang::Graphql => "graphql", + ScriptLang::Php => "php", } } } diff --git a/backend/windmill-worker/Cargo.toml b/backend/windmill-worker/Cargo.toml index d0b080fe2e7b3..1328ca9da9852 100644 --- a/backend/windmill-worker/Cargo.toml +++ b/backend/windmill-worker/Cargo.toml @@ -29,6 +29,7 @@ windmill-parser-py-imports.workspace = true windmill-parser-bash.workspace = true windmill-parser-sql.workspace = true windmill-parser-graphql.workspace = true +windmill-parser-php.workspace = true windmill-git-sync.workspace = true sqlx.workspace = true uuid.workspace = true @@ -80,6 +81,7 @@ tokio-util = { workspace = true, optional = true } openidconnect = { workspace = true, optional = true} tar = { workspace = true, optional = true} object_store = { workspace = true, optional = true} +convert_case.workspace = true [build-dependencies] deno_fetch.workspace = true diff --git a/backend/windmill-worker/nsjail/run.php.config.proto b/backend/windmill-worker/nsjail/run.php.config.proto new file mode 100644 index 0000000000000..f2e33be8e1110 --- /dev/null +++ b/backend/windmill-worker/nsjail/run.php.config.proto @@ -0,0 +1,136 @@ +name: "php run script" + +mode: ONCE +hostname: "php" +log_level: ERROR + +disable_rl: true + +cwd: "/tmp" + +clone_newnet: false +clone_newuser: {CLONE_NEWUSER} + +keep_caps: false +keep_env: true +mount_proc: true + +mount { + src: "/bin" + dst: "/bin" + is_bind: true +} + +mount { + src: "/lib" + dst: "/lib" + is_bind: true +} + + +mount { + src: "/lib64" + dst: "/lib64" + is_bind: true +} + + +mount { + src: "/usr" + dst: "/usr" + is_bind: true +} + +mount { + src: "/dev/null" + dst: "/dev/null" + is_bind: true + rw: true +} + +mount { + dst: "/tmp" + fstype: "tmpfs" + rw: true + options: "size=800000000" +} + +mount { + src: "{JOB_DIR}/main.php" + dst: "/tmp/main.php" + is_bind: true + mandatory: false +} + +mount { + src: "{JOB_DIR}/wrapper.php" + dst: "/tmp/wrapper.php" + is_bind: true + mandatory: false +} + +mount { + src: "/etc" + dst: "/etc" + is_bind: true +} + +mount { + src: "/dev/random" + dst: "/dev/random" + is_bind: true +} + +mount { + src: "/dev/urandom" + dst: "/dev/urandom" + is_bind: true +} + +mount { + src: "{JOB_DIR}/args.json" + dst: "/tmp/args.json" + is_bind: true +} + +mount { + src: "{JOB_DIR}/result.json" + dst: "/tmp/result.json" + rw: true + is_bind: true +} + +mount { + src: "{JOB_DIR}/composer.json" + dst: "/tmp/composer.json" + is_bind: true + mandatory: false +} + +mount { + src: "{JOB_DIR}/composer.lock" + dst: "/tmp/composer.lock" + is_bind: true + mandatory: false +} + +mount { + src: "{JOB_DIR}/vendor" + dst: "/tmp/vendor" + is_bind: true + mandatory: false +} + +iface_no_lo: true + +mount { + src: "{CACHE_DIR}" + dst: "/tmp/windmill/cache/php" + is_bind: true + rw: true + mandatory: false +} + +{SHARED_MOUNT} + +envar: "HOME=/tmp" diff --git a/backend/windmill-worker/src/lib.rs b/backend/windmill-worker/src/lib.rs index dcbbe5fe679a2..7ee01679dc17a 100644 --- a/backend/windmill-worker/src/lib.rs +++ b/backend/windmill-worker/src/lib.rs @@ -18,6 +18,7 @@ mod graphql_executor; mod js_eval; mod mysql_executor; mod pg_executor; +mod php_executor; mod python_executor; mod worker; mod worker_flow; diff --git a/backend/windmill-worker/src/php_executor.rs b/backend/windmill-worker/src/php_executor.rs new file mode 100644 index 0000000000000..730ebacf29ab5 --- /dev/null +++ b/backend/windmill-worker/src/php_executor.rs @@ -0,0 +1,330 @@ +use convert_case::{Case, Casing}; +use itertools::Itertools; +use regex::Regex; +use serde_json::value::RawValue; +use std::{collections::HashMap, process::Stdio}; +use tokio::{fs::File, io::AsyncReadExt, process::Command}; +use uuid::Uuid; +use windmill_common::{ + error::{self, to_anyhow, Result}, + jobs::QueuedJob, +}; +use windmill_parser::Typ; +use windmill_queue::{append_logs, CanceledBy}; + +use crate::{ + common::{ + create_args_and_out_file, get_main_override, get_reserved_variables, handle_child, + read_result, start_child_process, write_file, + }, + AuthedClientBackgroundTask, COMPOSER_CACHE_DIR, COMPOSER_PATH, DISABLE_NSJAIL, DISABLE_NUSER, + NSJAIL_PATH, PHP_PATH, +}; + +const NSJAIL_CONFIG_RUN_PHP_CONTENT: &str = include_str!("../nsjail/run.php.config.proto"); + +lazy_static::lazy_static! { + static ref RE: Regex = Regex::new(r"^//\s?(\S+)\s*$").unwrap(); +} + +const COMPOSER_LOCK_SPLIT: &str = "\nLOCK\n"; + +pub fn parse_php_imports(code: &str) -> anyhow::Result> { + let find_requirements = code + .lines() + .find_position(|x| x.starts_with("//require:") || x.starts_with("// require:")); + + if let Some((pos, _)) = find_requirements { + let requirements = code + .lines() + .skip(pos + 1) + .map_while(|x| { + RE.captures(x).map(|x| { + match x.get(1).unwrap().as_str().split("@").collect_vec()[..] { + [path, version] => (path.to_string(), version.to_string()), + [path] | [path, ..] => (path.to_string(), "*".to_string()), + [] => unreachable!(), + } + }) + }) + .collect::>(); + + let composer_json = + serde_json::to_string_pretty(&HashMap::from([("require", requirements)])) + .map_err(to_anyhow)?; + + Ok(Some(composer_json)) + } else { + Ok(None) + } +} + +pub async fn composer_install( + mem_peak: &mut i32, + canceled_by: &mut Option, + job_id: &Uuid, + w_id: &str, + db: &sqlx::Pool, + job_dir: &str, + worker_name: &str, + requirements: String, + lock: Option, +) -> Result { + write_file(job_dir, "composer.json", &requirements).await?; + + if let Some(lock) = lock.as_ref() { + write_file(job_dir, "composer.lock", lock).await?; + } + + let mut child_cmd = Command::new(&*COMPOSER_PATH); + let args = vec!["install", "--no-dev", "--no-progress"]; + child_cmd + .current_dir(job_dir) + .env("COMPOSER_HOME", &*COMPOSER_CACHE_DIR) + .args(args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let child_process = start_child_process(child_cmd, &*COMPOSER_PATH).await?; + + handle_child( + job_id, + db, + mem_peak, + canceled_by, + child_process, + false, + worker_name, + w_id, + "composer install", + None, + false, + ) + .await?; + + match lock { + Some(lock) => Ok(format!("{requirements}{COMPOSER_LOCK_SPLIT}{lock}")), + None => { + let mut lock_content = "".to_string(); + let mut lock_file = File::open(format!("{job_dir}/composer.lock")).await?; + lock_file.read_to_string(&mut lock_content).await?; + Ok(format!("{requirements}{COMPOSER_LOCK_SPLIT}{lock_content}")) + } + } +} + +fn generate_resource_class(rt_name: &str, arg_name: &str) -> String { + let rt_name = rt_name.to_case(Case::Pascal); + format!( + "#[AllowDynamicProperties] +class {rt_name} {{ + public function __construct($data) {{ + foreach ($data AS $key => $value) $this->{{$key}} = $value; + }} +}} +$args->{arg_name} = new {rt_name}($args->{arg_name});" + ) +} + +#[tracing::instrument(level = "trace", skip_all)] +pub async fn handle_php_job( + requirements_o: Option, + mem_peak: &mut i32, + canceled_by: &mut Option, + job: &QueuedJob, + db: &sqlx::Pool, + client: &AuthedClientBackgroundTask, + job_dir: &str, + inner_content: &String, + base_internal_url: &str, + worker_name: &str, + envs: HashMap, + shared_mount: &str, +) -> error::Result> { + let (composer_json, composer_lock) = match requirements_o { + Some(reqs_and_lock) if !reqs_and_lock.is_empty() => { + let splitted = reqs_and_lock.split(COMPOSER_LOCK_SPLIT).collect_vec(); + if splitted.len() != 2 { + return Err(error::Error::ExecutionErr( + format!("Invalid requirements, expected to find LOCK split pattern in reqs. Found: |{reqs_and_lock}|") + )); + } + (Some(splitted[0].to_string()), Some(splitted[1].to_string())) + } + _ => (parse_php_imports(inner_content)?, None), + }; + + let autoload_line = if let Some(composer_json) = composer_json { + let logs1 = "\n\n--- COMPOSER INSTALL ---\n".to_string(); + append_logs(job.id, job.workspace_id.clone(), logs1, db).await; + + composer_install( + mem_peak, + canceled_by, + &job.id, + &job.workspace_id, + db, + job_dir, + worker_name, + composer_json, + composer_lock, + ) + .await?; + "require './vendor/autoload.php';" + } else { + "" + }; + + let init_logs = "\n\n--- PHP CODE EXECUTION ---\n".to_string(); + + append_logs(job.id.clone(), job.workspace_id.to_string(), init_logs, db).await; + + let _ = write_file(job_dir, "main.php", inner_content).await?; + + let main_override = get_main_override(job.args.as_ref()); + + let write_wrapper_f = async { + let args = + windmill_parser_php::parse_php_signature(inner_content, main_override.clone())?.args; + + let args_to_include = args + .iter() + .filter(|x| { + !x.has_default || job.args.as_ref().is_some_and(|a| a.contains_key(&x.name)) + }) + .collect::>(); + + let func_args_str = args_to_include + .iter() + .map(|x| format!("$args->{}", x.name)) + .collect::>() + .join(","); + + let resource_classes = args_to_include + .iter() + .filter_map(|x| match &x.typ { + Typ::Resource(name) => Some((name, &x.name)), + _ => None, + }) + .unique() + .map(|(rt_name, arg_name)| generate_resource_class(rt_name, arg_name)) + .collect::>() + .join("\n\n"); + + let main_name = main_override.unwrap_or("main".to_string()); + + let wrapper_content: String = format!( + r#" + $e->getMessage(), + "name" => $e->getName(), + "stack" => $e->getTraceAsString() + ]; + $step_id = getenv('WM_FLOW_STEP_ID'); + if ($step_id) {{ + $err["step_id"] = $step_id; + }} + file_put_contents("result.json", json_encode($err)); + exit(1); +}} + "#, + ); + write_file(job_dir, "wrapper.php", &wrapper_content).await?; + Ok(()) as error::Result<()> + }; + + let reserved_variables_args_out_f = async { + let args_and_out_f = async { + create_args_and_out_file(&client, job, job_dir, db).await?; + Ok(()) as Result<()> + }; + let reserved_variables_f = async { + let client = client.get_authed().await; + let vars = get_reserved_variables(job, &client.token, db).await?; + Ok(vars) as Result> + }; + let (_, reserved_variables) = tokio::try_join!(args_and_out_f, reserved_variables_f)?; + Ok(reserved_variables) as error::Result> + }; + + let (reserved_variables, _) = tokio::try_join!(reserved_variables_args_out_f, write_wrapper_f)?; + + let child = if !*DISABLE_NSJAIL { + let _ = write_file( + job_dir, + "run.config.proto", + &NSJAIL_CONFIG_RUN_PHP_CONTENT + .replace("{JOB_DIR}", job_dir) + .replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()) + .replace("{SHARED_MOUNT}", shared_mount), + ) + .await?; + + let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str()); + let args = vec![ + "--config", + "run.config.proto", + "--", + &PHP_PATH, + "/tmp/wrapper.php", + ]; + nsjail_cmd + .current_dir(job_dir) + .env_clear() + .envs(envs) + .envs(reserved_variables) + .env("COMPOSER_HOME", &*COMPOSER_CACHE_DIR) + .env("BASE_INTERNAL_URL", base_internal_url) + .args(args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + start_child_process(nsjail_cmd, NSJAIL_PATH.as_str()).await? + } else { + let cmd = { + let script_path = format!("{job_dir}/wrapper.php"); + + let mut php_cmd = Command::new(&*PHP_PATH); + let args = vec![&script_path]; + php_cmd + .current_dir(job_dir) + .env_clear() + .envs(envs) + .envs(reserved_variables) + .env("COMPOSER_HOME", &*COMPOSER_CACHE_DIR) + .env("BASE_INTERNAL_URL", base_internal_url) + .args(args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + php_cmd + }; + start_child_process(cmd, &*PHP_PATH).await? + }; + + handle_child( + &job.id, + db, + mem_peak, + canceled_by, + child, + false, + worker_name, + &job.workspace_id, + "php run", + job.timeout, + false, + ) + .await?; + read_result(job_dir).await +} diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index ab6a4989b8c9e..00813a13b097f 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -100,6 +100,7 @@ use crate::{ js_eval::{eval_fetch_timeout, transpile_ts}, mysql_executor::do_mysql, pg_executor::do_postgresql, + php_executor::{composer_install, handle_php_job, parse_php_imports}, python_executor::{ create_dependencies_dir, handle_python_job, handle_python_reqs, pip_compile, }, @@ -203,6 +204,7 @@ pub const BUN_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "bun"); pub const HUB_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "hub"); pub const GO_BIN_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "gobin"); pub const POWERSHELL_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "powershell"); +pub const COMPOSER_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "composer"); const NUM_SECS_PING: u64 = 5; @@ -273,6 +275,8 @@ lazy_static::lazy_static! { pub static ref NPM_PATH: String = std::env::var("NPM_PATH").unwrap_or_else(|_| "/usr/bin/npm".to_string()); pub static ref NODE_PATH: String = std::env::var("NODE_PATH").unwrap_or_else(|_| "/usr/bin/node".to_string()); pub static ref POWERSHELL_PATH: String = std::env::var("POWERSHELL_PATH").unwrap_or_else(|_| "/usr/bin/pwsh".to_string()); + pub static ref PHP_PATH: String = std::env::var("PHP_PATH").unwrap_or_else(|_| "/usr/bin/php".to_string()); + pub static ref COMPOSER_PATH: String = std::env::var("COMPOSER_PATH").unwrap_or_else(|_| "/usr/bin/composer".to_string()); pub static ref NSJAIL_PATH: String = std::env::var("NSJAIL_PATH").unwrap_or_else(|_| "nsjail".to_string()); pub static ref PATH_ENV: String = std::env::var("PATH").unwrap_or_else(|_| String::new()); pub static ref HOME_ENV: String = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); @@ -3431,6 +3435,23 @@ mount {{ ) .await } + Some(ScriptLang::Php) => { + handle_php_job( + requirements_o, + mem_peak, + canceled_by, + job, + db, + client, + job_dir, + &inner_content, + base_internal_url, + worker_name, + envs, + &shared_mount, + ) + .await + } _ => panic!("unreachable, language is not supported: {language:#?}"), }; tracing::info!( @@ -4357,6 +4378,34 @@ async fn capture_dependency_job( .await?; Ok(req.unwrap_or_else(String::new)) } + ScriptLang::Php => { + let reqs = if raw_deps { + if job_raw_code.is_empty() { + return Ok("".to_string()); + } + job_raw_code.to_string() + } else { + match parse_php_imports(job_raw_code)? { + Some(reqs) => reqs, + None => { + return Ok("".to_string()); + } + } + }; + + composer_install( + mem_peak, + canceled_by, + job_id, + w_id, + db, + job_dir, + worker_name, + reqs, + None, + ) + .await + } ScriptLang::Postgresql => Ok("".to_owned()), ScriptLang::Mysql => Ok("".to_owned()), ScriptLang::Bigquery => Ok("".to_owned()), diff --git a/cli/metadata.ts b/cli/metadata.ts index b448dd0559ada..e3cecc23525b6 100644 --- a/cli/metadata.ts +++ b/cli/metadata.ts @@ -30,7 +30,7 @@ import { generateHash } from "./utils.ts"; export async function generateAllMetadata() {} function findClosestRawReqs( - lang: "bun" | "python3" | undefined, + lang: "bun" | "python3" | "php" | undefined, remotePath: string, globalDeps: GlobalDeps ): string | undefined { @@ -53,6 +53,15 @@ function findClosestRawReqs( bestCandidate = { k, v }; } }); + } else if (lang == "php") { + Object.entries(globalDeps.composers).forEach(([k, v]) => { + if ( + remotePath.startsWith(k) && + k.length >= (bestCandidate?.k ?? "").length + ) { + bestCandidate = { k, v }; + } + }); } // @ts-ignore return bestCandidate?.v; @@ -77,14 +86,14 @@ export async function generateMetadataInternal( const language = inferContentTypeFromFilePath(scriptPath, opts.defaultTs); const rawReqs = findClosestRawReqs( - language as "bun" | "python3" | undefined, + language as "bun" | "python3" | "php" | undefined, scriptPath, globalDeps ); if (rawReqs) { log.info( colors.blue( - `Found raw requirements (package.json/requirements.txt) for ${scriptPath}, using it` + `Found raw requirements (package.json/requirements.txt/composer.json) for ${scriptPath}, using it` ) ); } @@ -184,7 +193,8 @@ async function updateScriptLock( language == "bun" || language == "python3" || language == "go" || - language == "deno" + language == "deno" || + language == "php" ) ) { return; diff --git a/cli/script.ts b/cli/script.ts index ff68ba19f6683..b6234cbb6ddaa 100644 --- a/cli/script.ts +++ b/cli/script.ts @@ -671,17 +671,24 @@ async function bootstrap( export type GlobalDeps = { pkgs: Record; reqs: Record; + composers: Record; }; export async function findGlobalDeps( codebases: SyncCodebase[] ): Promise { const pkgs: { [key: string]: string } = {}; const reqs: { [key: string]: string } = {}; + const composers: { [key: string]: string } = {}; const els = await FSFSElement(Deno.cwd(), codebases); for await (const entry of readDirRecursiveWithIgnore((p, isDir) => { p = "/" + p; return ( - !isDir && !(p.endsWith("/package.json") || p.endsWith("requirements.txt")) + !isDir && + !( + p.endsWith("/package.json") || + p.endsWith("requirements.txt") || + p.endsWith("composer.json") + ) ); }, els)) { if (entry.isDirectory || entry.ignored) continue; @@ -690,9 +697,11 @@ export async function findGlobalDeps( pkgs[entry.path.substring(0, entry.path.length - 12)] = content; } else if (entry.path.endsWith("requirements.txt")) { reqs[entry.path.substring(0, entry.path.length - 16)] = content; + } else if (entry.path.endsWith("composer.json")) { + composers[entry.path.substring(0, entry.path.length - 13)] = content; } } - return { pkgs, reqs }; + return { pkgs, reqs, composers }; } async function generateMetadata( opts: GlobalOptions & { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5392d05f16ffb..612ecfffedd15 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -53,7 +53,7 @@ "vscode-languageclient": "~9.0.1", "vscode-uri": "~3.0.8", "vscode-ws-jsonrpc": "~3.1.0", - "windmill-parser-wasm": "^1.318.0", + "windmill-parser-wasm": "^1.327.0", "windmill-sql-datatype-parser-wasm": "^1.318.0", "y-monaco": "^0.1.4", "y-websocket": "^1.5.0", @@ -10310,9 +10310,9 @@ } }, "node_modules/windmill-parser-wasm": { - "version": "1.318.0", - "resolved": "https://registry.npmjs.org/windmill-parser-wasm/-/windmill-parser-wasm-1.318.0.tgz", - "integrity": "sha512-uxiluG0/EIwqwmR4FTppxa2eHZ0hrTijjWmf5L1vJYAgdCotI+glQhjdD2EMNtQRwX9+Pjj8QoVYckrNwJ/2yg==" + "version": "1.327.0", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm/-/windmill-parser-wasm-1.327.0.tgz", + "integrity": "sha512-lliouG5syBSwaPDN3C5fd2whqZmD5kW7S+gXBtunnMwIydPfXiHqWTL4F2zvbHhcHhiIgN04ATxL7gpxcMP1wg==" }, "node_modules/windmill-sql-datatype-parser-wasm": { "version": "1.318.0", diff --git a/frontend/package.json b/frontend/package.json index 2f43fd9c047ea..3ace904839c00 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -133,7 +133,7 @@ "vscode-languageclient": "~9.0.1", "vscode-uri": "~3.0.8", "vscode-ws-jsonrpc": "~3.1.0", - "windmill-parser-wasm": "^1.318.0", + "windmill-parser-wasm": "^1.327.0", "windmill-sql-datatype-parser-wasm": "^1.318.0", "y-monaco": "^0.1.4", "y-websocket": "^1.5.0", diff --git a/frontend/src/lib/components/Editor.svelte b/frontend/src/lib/components/Editor.svelte index 04bb6d9836748..31f8f4692b251 100644 --- a/frontend/src/lib/components/Editor.svelte +++ b/frontend/src/lib/components/Editor.svelte @@ -22,6 +22,7 @@ import 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution' import 'monaco-editor/esm/vs/basic-languages/graphql/graphql.contribution' import 'monaco-editor/esm/vs/basic-languages/powershell/powershell.contribution' + import 'monaco-editor/esm/vs/basic-languages/php/php.contribution' import 'monaco-editor/esm/vs/language/typescript/monaco.contribution' import 'monaco-editor/esm/vs/basic-languages/css/css.contribution' @@ -84,6 +85,7 @@ | 'sql' | 'graphql' | 'powershell' + | 'php' | 'css' | 'javascript' export let code: string = '' @@ -166,6 +168,12 @@ } } + export function backspace(): void { + if (editor) { + editor.trigger('keyboard', 'deleteLeft', {}) + } + } + export function insertAtBeginning(code: string): void { if (editor) { const range = { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 } diff --git a/frontend/src/lib/components/EditorBar.svelte b/frontend/src/lib/components/EditorBar.svelte index 3187fb6b9e26a..33e4f1b2667f5 100644 --- a/frontend/src/lib/components/EditorBar.svelte +++ b/frontend/src/lib/components/EditorBar.svelte @@ -86,11 +86,19 @@ 'go', 'deno', 'bun', - 'nativets' + 'nativets', + 'php' + ].includes(lang ?? '') + $: showVarPicker = [ + 'python3', + 'bash', + 'powershell', + 'go', + 'deno', + 'bun', + 'nativets', + 'php' ].includes(lang ?? '') - $: showVarPicker = ['python3', 'bash', 'powershell', 'go', 'deno', 'bun', 'nativets'].includes( - lang ?? '' - ) $: showResourcePicker = [ 'python3', 'bash', @@ -98,10 +106,13 @@ 'go', 'deno', 'bun', - 'nativets' + 'nativets', + 'php' ].includes(lang ?? '') $: showResourceTypePicker = - ['typescript', 'javascript'].includes(scriptLangToEditorLang(lang)) || lang === 'python3' + ['typescript', 'javascript'].includes(scriptLangToEditorLang(lang)) || + lang === 'python3' || + lang === 'php' let codeViewer: Drawer let codeObj: { language: SupportedLanguage; content: string } | undefined = undefined @@ -187,12 +198,47 @@ if (!code.includes('from typing import TypedDict')) { editor.insertAtBeginning('from typing import TypedDict\n') } + } else if (lang === 'php') { + const phpSchema = phpCompile(resourceType.schema as any) + const rtName = toCamel(capitalize(name)) + editor.insertAtCursor(`if (!class_exists('${rtName}')) {\nclass ${rtName} {\n${phpSchema}\n`) + editor.backspace() + editor.insertAtCursor('}') } else { const tsSchema = compile(resourceType.schema as any) - editor.insertAtCursor(`type ${toCamel(capitalize(name))} = ${tsSchema}\n`) + editor.insertAtCursor(`type ${toCamel(capitalize(name))} = ${tsSchema}`) } sendUserToast(`${name} inserted at cursor`) } + + function phpCompile(schema: Schema) { + let res = ' ' + const entries = Object.entries(schema.properties) + if (entries.length === 0) { + return 'array' + } + let i = 0 + for (let [name, prop] of entries) { + let typ = 'array' + if (prop.type === 'array') { + typ = 'array' + } else if (prop.type === 'string') { + typ = 'string' + } else if (prop.type === 'number') { + typ = 'float' + } else if (prop.type === 'integer') { + typ = 'int' + } else if (prop.type === 'boolean') { + typ = 'bool' + } + res += `public ${typ} $${name};` + i++ + if (i < entries.length) { + res += '\n' + } + } + return res + } function pythonCompile(schema: Schema) { let res = '' const entries = Object.entries(schema.properties) @@ -285,6 +331,8 @@ editor.insertAtCursor(`$${name}`) } else if (lang == 'powershell') { editor.insertAtCursor(`$Env:${name}`) + } else if (lang == 'php') { + editor.insertAtCursor(`getenv('${name}');`) } sendUserToast(`${name} inserted at cursor`) }} @@ -336,6 +384,11 @@ editor.insertAtBeginning(`import * as wmill from "./windmill.ts"\n`) } editor.insertAtCursor(`(await wmill.getVariable('${path}'))`) + } else if (lang == 'php') { + editor.insertAtCursor(`$ch = curl_init(getenv('BASE_INTERNAL_URL') . '/api/w/' . getenv('WM_WORKSPACE') . '/variables/get_value/${path}'); +curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . getenv('WM_TOKEN'))); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +$var = json_decode(curl_exec($ch));`) } sendUserToast(`${name} inserted at cursor`) }} @@ -401,6 +454,11 @@ editor.insertAtBeginning(`import * as wmill from "./windmill.ts"\n`) } editor.insertAtCursor(`(await wmill.getResource('${path}'))`) + } else if (lang == 'php') { + editor.insertAtCursor(`$ch = curl_init(getenv('BASE_INTERNAL_URL') . '/api/w/' . getenv('WM_WORKSPACE') . '/resources/get_value_interpolated/${path}'); +curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . getenv('WM_TOKEN'))); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +$res = json_decode(curl_exec($ch));`) } sendUserToast(`${path} inserted at cursor`) }} diff --git a/frontend/src/lib/components/HighlightCode.svelte b/frontend/src/lib/components/HighlightCode.svelte index d7354cd4a4ff1..88079450a23b1 100644 --- a/frontend/src/lib/components/HighlightCode.svelte +++ b/frontend/src/lib/components/HighlightCode.svelte @@ -8,6 +8,7 @@ import javascript from 'svelte-highlight/languages/javascript' import sql from 'svelte-highlight/languages/sql' import powershell from 'svelte-highlight/languages/powershell' + import php from 'svelte-highlight/languages/php' import type { Script } from '$lib/gen' import { Button } from './common' import { copyToClipboard } from '$lib/utils' @@ -45,6 +46,8 @@ return sql case 'powershell': return powershell + case 'php': + return php default: return typescript diff --git a/frontend/src/lib/components/WorkspaceGroup.svelte b/frontend/src/lib/components/WorkspaceGroup.svelte index ec55bae516c98..99c4f90c21f97 100644 --- a/frontend/src/lib/components/WorkspaceGroup.svelte +++ b/frontend/src/lib/components/WorkspaceGroup.svelte @@ -93,7 +93,8 @@ 'flow', 'hub', 'other', - 'bun' + 'bun', + 'php' ] const nativeTags = [ 'nativets', diff --git a/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte b/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte index 69222cc1c96c6..62dc74daacb79 100644 --- a/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte +++ b/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte @@ -15,6 +15,7 @@ import BunIcon from '$lib/components/icons/BunIcon.svelte' import DenoIcon from '$lib/components/icons/DenoIcon.svelte' import type { Script } from '$lib/gen' + import PHPIcon from '$lib/components/icons/PHPIcon.svelte' export let lang: | SupportedLanguage @@ -42,7 +43,8 @@ snowflake: 'Snowflake', mysql: 'MySQL', mssql: 'MS SQL Server', - bun: 'TypeScript' + bun: 'TypeScript', + php: 'PHP' } const langToComponent: Record< @@ -66,7 +68,8 @@ powershell: PowershellIcon, postgresql: PostgresIcon, nativets: RestIcon, - graphql: GraphqlIcon + graphql: GraphqlIcon, + php: PHPIcon } let subIconScale = width === 30 ? 0.6 : 0.8 diff --git a/frontend/src/lib/components/icons/PHPIcon.svelte b/frontend/src/lib/components/icons/PHPIcon.svelte new file mode 100644 index 0000000000000..c95f04589e3ea --- /dev/null +++ b/frontend/src/lib/components/icons/PHPIcon.svelte @@ -0,0 +1,118 @@ + + + + Official PHP Logo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/lib/editorUtils.ts b/frontend/src/lib/editorUtils.ts index e9e5dd5e782d8..632815014dd6b 100644 --- a/frontend/src/lib/editorUtils.ts +++ b/frontend/src/lib/editorUtils.ts @@ -58,6 +58,8 @@ export function langToExt(lang: string): string { return 'sh' case 'powershell': return 'ps1' + case 'php': + return 'php' case 'deno': return 'ts' case 'nativets': diff --git a/frontend/src/lib/infer.ts b/frontend/src/lib/infer.ts index 1aff662b5509c..3c72dd7c7a569 100644 --- a/frontend/src/lib/infer.ts +++ b/frontend/src/lib/infer.ts @@ -17,7 +17,8 @@ import init, { parse_outputs, parse_mssql, parse_ts_imports, - parse_db_resource + parse_db_resource, + parse_php } from 'windmill-parser-wasm' import wasmUrl from 'windmill-parser-wasm/windmill_parser_wasm_bg.wasm?url' import { workspaceStore } from './stores.js' @@ -120,6 +121,8 @@ export async function inferArgs( inferedSchema = JSON.parse(parse_bash(code)) } else if (language == 'powershell') { inferedSchema = JSON.parse(parse_powershell(code)) + } else if (language == 'php') { + inferedSchema = JSON.parse(parse_php(code)) } else { return } diff --git a/frontend/src/lib/script_helpers.ts b/frontend/src/lib/script_helpers.ts index fbc8af26f2673..218e2c7521ebd 100644 --- a/frontend/src/lib/script_helpers.ts +++ b/frontend/src/lib/script_helpers.ts @@ -190,6 +190,26 @@ export const GRAPHQL_INIT_CODE = `query($name4: String, $name2: Int, $name3: [St } ` +export const PHP_INIT_CODE = `priority: {job.priority} {/if} - {#if job.tag && !['deno', 'python3', 'flow', 'other', 'go', 'postgresql', 'mysql', 'bigquery', 'snowflake', 'mssql', 'graphql', 'nativets', 'bash', 'powershell', 'other', 'dependency'].includes(job.tag)} + {#if job.tag && !['deno', 'python3', 'flow', 'other', 'go', 'postgresql', 'mysql', 'bigquery', 'snowflake', 'mssql', 'graphql', 'nativets', 'bash', 'powershell', 'php', 'other', 'dependency'].includes(job.tag)}
Tag: {job.tag}