Skip to main content

mago_analyzer/plugin/libraries/stdlib/cookie/
setcookie.rs

1use mago_reporting::Annotation;
2use mago_reporting::Issue;
3use mago_span::HasSpan;
4use mago_syntax::cst::Argument;
5use mago_syntax::cst::Expression;
6use mago_syntax::cst::FunctionCall;
7
8use crate::code::IssueCode;
9use crate::plugin::context::HookContext;
10use crate::plugin::hook::FunctionCallHook;
11use crate::plugin::hook::HookResult;
12use crate::plugin::provider::Provider;
13use crate::plugin::provider::ProviderMeta;
14
15/// Hook for `setcookie()` and `setrawcookie()`.
16///
17/// These functions have two signatures:
18///   1. setcookie(string $name, string $value = "", int $expires_or_options = 0,
19///      string $path = "", string $domain = "", bool $secure = false, bool $httponly = false): bool
20///   2. setcookie(string $name, string $value = "", array $options = []): bool
21///
22/// When the 3rd argument is an array, only 3 arguments are allowed.
23/// PHP raises a fatal error if additional arguments are passed with the array form.
24#[derive(Default)]
25pub struct SetCookieHook;
26
27impl Provider for SetCookieHook {
28    fn meta() -> &'static ProviderMeta {
29        static META: ProviderMeta = ProviderMeta::new(
30            "php::cookie::setcookie",
31            "setcookie",
32            "Validates setcookie/setrawcookie argument combinations.",
33        );
34
35        &META
36    }
37}
38
39impl FunctionCallHook for SetCookieHook {
40    fn after_function_call(&self, call: &FunctionCall<'_>, context: &mut HookContext<'_, '_>) -> HookResult<()> {
41        let Expression::Identifier(identifier) = call.function else {
42            return Ok(());
43        };
44
45        let name_bytes = identifier.value();
46        let name = mago_bytes::BytesDisplay(name_bytes);
47        if !name_bytes.eq_ignore_ascii_case(b"setcookie") && !name_bytes.eq_ignore_ascii_case(b"setrawcookie") {
48            return Ok(());
49        }
50
51        let arguments = &call.argument_list.arguments;
52        if arguments.len() <= 3 {
53            return Ok(());
54        }
55
56        // Check if the 3rd argument (index 2) is an array type.
57        let Some(third_arg) = arguments.get(2) else {
58            return Ok(());
59        };
60
61        let third_arg_expr = match third_arg {
62            Argument::Positional(arg) => arg.value,
63            Argument::Named(arg) => {
64                let param_name = arg.name.value;
65                if param_name != b"expires_or_options" && param_name != b"options" {
66                    return Ok(());
67                }
68                arg.value
69            }
70        };
71
72        let Some(third_arg_type) = context.get_expression_type(third_arg_expr) else {
73            return Ok(());
74        };
75
76        if !third_arg_type.has_array() {
77            return Ok(());
78        }
79
80        // The 3rd argument is an array, only 3 arguments are allowed.
81        // Report the 4th argument as unexpected.
82        let Some(fourth_arg) = arguments.get(3) else {
83            return Ok(());
84        };
85        let fourth_arg_span = match fourth_arg {
86            Argument::Positional(arg) => arg.value.span(),
87            Argument::Named(arg) => arg.span(),
88        };
89
90        context.report(
91            IssueCode::TooManyArguments,
92            Issue::error(format!("Too many arguments provided for function `{name}`."))
93                .with_annotation(
94                    Annotation::primary(fourth_arg_span).with_message("Unexpected argument provided here"),
95                )
96                .with_annotation(
97                    Annotation::secondary(call.function.span()).with_message("For this function call"),
98                )
99                .with_note(format!(
100                    "When argument #3 (`$expires_or_options`) is an array, `{name}()` expects exactly 3 arguments, but received {}.",
101                    arguments.len()
102                ))
103                .with_help("Remove the extra arguments and pass options in the array instead."),
104        );
105
106        Ok(())
107    }
108}