github.com/projectdiscovery/nuclei/v2@v2.9.15/pkg/protocols/headless/engine/page_actions.go (about) 1 package engine 2 3 import ( 4 "context" 5 "os" 6 "path/filepath" 7 "strconv" 8 "strings" 9 "sync" 10 "time" 11 12 "github.com/go-rod/rod" 13 "github.com/go-rod/rod/lib/input" 14 "github.com/go-rod/rod/lib/proto" 15 "github.com/go-rod/rod/lib/utils" 16 "github.com/pkg/errors" 17 "github.com/projectdiscovery/gologger" 18 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs" 19 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/expressions" 20 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/generators" 21 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/utils/vardump" 22 protocolutils "github.com/projectdiscovery/nuclei/v2/pkg/protocols/utils" 23 httputil "github.com/projectdiscovery/nuclei/v2/pkg/protocols/utils/http" 24 errorutil "github.com/projectdiscovery/utils/errors" 25 fileutil "github.com/projectdiscovery/utils/file" 26 folderutil "github.com/projectdiscovery/utils/folder" 27 stringsutil "github.com/projectdiscovery/utils/strings" 28 urlutil "github.com/projectdiscovery/utils/url" 29 "github.com/segmentio/ksuid" 30 ) 31 32 var ( 33 errinvalidArguments = errorutil.New("invalid arguments provided") 34 ErrLFAccessDenied = errorutil.New("Use -allow-local-file-access flag to enable local file access") 35 ) 36 37 const ( 38 errCouldNotGetElement = "could not get element" 39 errCouldNotScroll = "could not scroll into view" 40 errElementDidNotAppear = "Element did not appear in the given amount of time" 41 ) 42 43 // ExecuteActions executes a list of actions on a page. 44 func (p *Page) ExecuteActions(input *contextargs.Context, actions []*Action, variables map[string]interface{}) (map[string]string, error) { 45 outData := make(map[string]string) 46 var err error 47 for _, act := range actions { 48 switch act.ActionType.ActionType { 49 case ActionNavigate: 50 err = p.NavigateURL(act, outData, variables) 51 case ActionScript: 52 err = p.RunScript(act, outData) 53 case ActionClick: 54 err = p.ClickElement(act, outData) 55 case ActionRightClick: 56 err = p.RightClickElement(act, outData) 57 case ActionTextInput: 58 err = p.InputElement(act, outData) 59 case ActionScreenshot: 60 err = p.Screenshot(act, outData) 61 case ActionTimeInput: 62 err = p.TimeInputElement(act, outData) 63 case ActionSelectInput: 64 err = p.SelectInputElement(act, outData) 65 case ActionWaitLoad: 66 err = p.WaitLoad(act, outData) 67 case ActionGetResource: 68 err = p.GetResource(act, outData) 69 case ActionExtract: 70 err = p.ExtractElement(act, outData) 71 case ActionWaitEvent: 72 err = p.WaitEvent(act, outData) 73 case ActionFilesInput: 74 if p.options.Options.AllowLocalFileAccess { 75 err = p.FilesInput(act, outData) 76 } else { 77 err = ErrLFAccessDenied 78 } 79 case ActionAddHeader: 80 err = p.ActionAddHeader(act, outData) 81 case ActionSetHeader: 82 err = p.ActionSetHeader(act, outData) 83 case ActionDeleteHeader: 84 err = p.ActionDeleteHeader(act, outData) 85 case ActionSetBody: 86 err = p.ActionSetBody(act, outData) 87 case ActionSetMethod: 88 err = p.ActionSetMethod(act, outData) 89 case ActionKeyboard: 90 err = p.KeyboardAction(act, outData) 91 case ActionDebug: 92 err = p.DebugAction(act, outData) 93 case ActionSleep: 94 err = p.SleepAction(act, outData) 95 case ActionWaitVisible: 96 err = p.WaitVisible(act, outData) 97 default: 98 continue 99 } 100 if err != nil { 101 return nil, errors.Wrap(err, "error occurred executing action") 102 } 103 } 104 return outData, nil 105 } 106 107 type rule struct { 108 *sync.Once 109 Action ActionType 110 Part string 111 Args map[string]string 112 } 113 114 // WaitVisible waits until an element appears. 115 func (p *Page) WaitVisible(act *Action, out map[string]string) error { 116 timeout, err := getTimeout(p, act) 117 if err != nil { 118 return errors.Wrap(err, "Wrong timeout given") 119 } 120 121 pollTime, err := getPollTime(p, act) 122 if err != nil { 123 return errors.Wrap(err, "Wrong polling time given") 124 } 125 126 element, _ := p.Sleeper(pollTime, timeout). 127 Timeout(timeout). 128 pageElementBy(act.Data) 129 130 if element != nil { 131 if err := element.WaitVisible(); err != nil { 132 return errors.Wrap(err, errElementDidNotAppear) 133 } 134 } else { 135 return errors.New(errElementDidNotAppear) 136 } 137 138 return nil 139 } 140 141 func (p *Page) Sleeper(pollTimeout, timeout time.Duration) *Page { 142 page := *p 143 page.page = page.Page().Sleeper(func() utils.Sleeper { 144 return createBackOffSleeper(pollTimeout, timeout) 145 }) 146 return &page 147 } 148 149 func (p *Page) Timeout(timeout time.Duration) *Page { 150 page := *p 151 page.page = page.Page().Timeout(timeout) 152 return &page 153 } 154 155 func createBackOffSleeper(pollTimeout, timeout time.Duration) utils.Sleeper { 156 backoffSleeper := utils.BackoffSleeper(pollTimeout, timeout, func(duration time.Duration) time.Duration { 157 return duration 158 }) 159 160 return func(ctx context.Context) error { 161 if ctx.Err() != nil { 162 return ctx.Err() 163 } 164 165 return backoffSleeper(ctx) 166 } 167 } 168 169 func getTimeout(p *Page, act *Action) (time.Duration, error) { 170 return geTimeParameter(p, act, "timeout", 3, time.Second) 171 } 172 173 func getPollTime(p *Page, act *Action) (time.Duration, error) { 174 return geTimeParameter(p, act, "pollTime", 100, time.Millisecond) 175 } 176 177 func geTimeParameter(p *Page, act *Action, parameterName string, defaultValue time.Duration, duration time.Duration) (time.Duration, error) { 178 pollTimeString := p.getActionArgWithDefaultValues(act, parameterName) 179 if pollTimeString == "" { 180 return defaultValue * duration, nil 181 } 182 timeout, err := strconv.Atoi(pollTimeString) 183 if err != nil { 184 return time.Duration(0), err 185 } 186 return time.Duration(timeout) * duration, nil 187 } 188 189 // ActionAddHeader executes a AddHeader action. 190 func (p *Page) ActionAddHeader(act *Action, out map[string]string /*TODO review unused parameter*/) error { 191 in := p.getActionArgWithDefaultValues(act, "part") 192 193 args := make(map[string]string) 194 args["key"] = p.getActionArgWithDefaultValues(act, "key") 195 args["value"] = p.getActionArgWithDefaultValues(act, "value") 196 p.rules = append(p.rules, rule{Action: ActionAddHeader, Part: in, Args: args}) 197 return nil 198 } 199 200 // ActionSetHeader executes a SetHeader action. 201 func (p *Page) ActionSetHeader(act *Action, out map[string]string /*TODO review unused parameter*/) error { 202 in := p.getActionArgWithDefaultValues(act, "part") 203 204 args := make(map[string]string) 205 args["key"] = p.getActionArgWithDefaultValues(act, "key") 206 args["value"] = p.getActionArgWithDefaultValues(act, "value") 207 p.rules = append(p.rules, rule{Action: ActionSetHeader, Part: in, Args: args}) 208 return nil 209 } 210 211 // ActionDeleteHeader executes a DeleteHeader action. 212 func (p *Page) ActionDeleteHeader(act *Action, out map[string]string /*TODO review unused parameter*/) error { 213 in := p.getActionArgWithDefaultValues(act, "part") 214 215 args := make(map[string]string) 216 args["key"] = p.getActionArgWithDefaultValues(act, "key") 217 p.rules = append(p.rules, rule{Action: ActionDeleteHeader, Part: in, Args: args}) 218 return nil 219 } 220 221 // ActionSetBody executes a SetBody action. 222 func (p *Page) ActionSetBody(act *Action, out map[string]string) error { 223 in := p.getActionArgWithDefaultValues(act, "part") 224 225 args := make(map[string]string) 226 args["body"] = p.getActionArgWithDefaultValues(act, "body") 227 p.rules = append(p.rules, rule{Action: ActionSetBody, Part: in, Args: args}) 228 return nil 229 } 230 231 // ActionSetMethod executes an SetMethod action. 232 func (p *Page) ActionSetMethod(act *Action, out map[string]string) error { 233 in := p.getActionArgWithDefaultValues(act, "part") 234 235 args := make(map[string]string) 236 args["method"] = p.getActionArgWithDefaultValues(act, "method") 237 p.rules = append(p.rules, rule{Action: ActionSetMethod, Part: in, Args: args, Once: &sync.Once{}}) 238 return nil 239 } 240 241 // NavigateURL executes an ActionLoadURL actions loading a URL for the page. 242 func (p *Page) NavigateURL(action *Action, out map[string]string, allvars map[string]interface{}) error { 243 // input <- is input url from cli 244 // target <- is the url from template (ex: {{BaseURL}}/test) 245 input, err := urlutil.Parse(p.input.MetaInput.Input) 246 if err != nil { 247 return errorutil.NewWithErr(err).Msgf("could not parse url %s", p.input.MetaInput.Input) 248 } 249 target := p.getActionArgWithDefaultValues(action, "url") 250 if target == "" { 251 return errinvalidArguments 252 } 253 254 // if target contains port ex: {{BaseURL}}:8080 use port specified in input 255 input, target = httputil.UpdateURLPortFromPayload(input, target) 256 hasTrailingSlash := httputil.HasTrailingSlash(target) 257 258 // create vars from input url 259 defaultReqVars := protocolutils.GenerateVariables(input, hasTrailingSlash, contextargs.GenerateVariables(p.input)) 260 // merge all variables 261 // Note: ideally we should evaluate all available variables with reqvars 262 // but due to cyclic dependency between packages `engine` and `protocols` 263 // allvars are evaluated,merged and passed from headless package itself 264 // TODO: remove cyclic dependency between packages `engine` and `protocols` 265 allvars = generators.MergeMaps(allvars, defaultReqVars) 266 267 if vardump.EnableVarDump { 268 gologger.Debug().Msgf("Final Protocol request variables: \n%s\n", vardump.DumpVariables(allvars)) 269 } 270 271 // Evaluate the target url with all variables 272 target, err = expressions.Evaluate(target, allvars) 273 if err != nil { 274 return errorutil.NewWithErr(err).Msgf("could not evaluate url %s", target) 275 } 276 277 reqURL, err := urlutil.ParseURL(target, true) 278 if err != nil { 279 return errorutil.NewWithTag("http", "failed to parse url %v while creating http request", target) 280 } 281 282 // ===== parameter automerge ===== 283 // while merging parameters first preference is given to target params 284 finalparams := input.Params.Clone() 285 finalparams.Merge(reqURL.Params.Encode()) 286 reqURL.Params = finalparams 287 288 // log all navigated requests 289 p.instance.requestLog[action.GetArg("url")] = reqURL.String() 290 291 if err := p.page.Navigate(reqURL.String()); err != nil { 292 return errorutil.NewWithErr(err).Msgf("could not navigate to url %s", reqURL.String()) 293 } 294 return nil 295 } 296 297 // RunScript runs a script on the loaded page 298 func (p *Page) RunScript(action *Action, out map[string]string) error { 299 code := p.getActionArgWithDefaultValues(action, "code") 300 if code == "" { 301 return errinvalidArguments 302 } 303 if p.getActionArgWithDefaultValues(action, "hook") == "true" { 304 if _, err := p.page.EvalOnNewDocument(code); err != nil { 305 return err 306 } 307 } 308 data, err := p.page.Eval(code) 309 if err != nil { 310 return err 311 } 312 if data != nil && action.Name != "" { 313 out[action.Name] = data.Value.String() 314 } 315 return nil 316 } 317 318 // ClickElement executes click actions for an element. 319 func (p *Page) ClickElement(act *Action, out map[string]string /*TODO review unused parameter*/) error { 320 element, err := p.pageElementBy(act.Data) 321 if err != nil { 322 return errors.Wrap(err, errCouldNotGetElement) 323 } 324 if err = element.ScrollIntoView(); err != nil { 325 return errors.Wrap(err, errCouldNotScroll) 326 } 327 if err = element.Click(proto.InputMouseButtonLeft, 1); err != nil { 328 return errors.Wrap(err, "could not click element") 329 } 330 return nil 331 } 332 333 // KeyboardAction executes a keyboard action on the page. 334 func (p *Page) KeyboardAction(act *Action, out map[string]string /*TODO review unused parameter*/) error { 335 return p.page.Keyboard.Type([]input.Key(p.getActionArgWithDefaultValues(act, "keys"))...) 336 } 337 338 // RightClickElement executes right click actions for an element. 339 func (p *Page) RightClickElement(act *Action, out map[string]string /*TODO review unused parameter*/) error { 340 element, err := p.pageElementBy(act.Data) 341 if err != nil { 342 return errors.Wrap(err, errCouldNotGetElement) 343 } 344 if err = element.ScrollIntoView(); err != nil { 345 return errors.Wrap(err, errCouldNotScroll) 346 } 347 if err = element.Click(proto.InputMouseButtonRight, 1); err != nil { 348 return errors.Wrap(err, "could not right click element") 349 } 350 return nil 351 } 352 353 // Screenshot executes screenshot action on a page 354 func (p *Page) Screenshot(act *Action, out map[string]string) error { 355 to := p.getActionArgWithDefaultValues(act, "to") 356 if to == "" { 357 to = ksuid.New().String() 358 if act.Name != "" { 359 out[act.Name] = to 360 } 361 } 362 var data []byte 363 var err error 364 if p.getActionArgWithDefaultValues(act, "fullpage") == "true" { 365 data, err = p.page.Screenshot(true, &proto.PageCaptureScreenshot{}) 366 } else { 367 data, err = p.page.Screenshot(false, &proto.PageCaptureScreenshot{}) 368 } 369 if err != nil { 370 return errors.Wrap(err, "could not take screenshot") 371 } 372 if p.getActionArgWithDefaultValues(act, "mkdir") == "true" && stringsutil.ContainsAny(to, folderutil.UnixPathSeparator, folderutil.WindowsPathSeparator) { 373 // creates new directory if needed based on path `to` 374 // TODO: replace all permission bits with fileutil constants (https://github.com/projectdiscovery/utils/issues/113) 375 if err := os.MkdirAll(filepath.Dir(to), 0700); err != nil { 376 return errorutil.NewWithErr(err).Msgf("failed to create directory while writing screenshot") 377 } 378 } 379 filePath := to 380 if !strings.HasSuffix(to, ".png") { 381 filePath += ".png" 382 } 383 384 if fileutil.FileExists(filePath) { 385 // return custom error as overwriting files is not supported 386 return errorutil.NewWithTag("screenshot", "failed to write screenshot, file %v already exists", filePath) 387 } 388 err = os.WriteFile(filePath, data, 0540) 389 if err != nil { 390 return errors.Wrap(err, "could not write screenshot") 391 } 392 gologger.Info().Msgf("Screenshot successfully saved at %v\n", filePath) 393 return nil 394 } 395 396 // InputElement executes input element actions for an element. 397 func (p *Page) InputElement(act *Action, out map[string]string /*TODO review unused parameter*/) error { 398 value := p.getActionArgWithDefaultValues(act, "value") 399 if value == "" { 400 return errinvalidArguments 401 } 402 element, err := p.pageElementBy(act.Data) 403 if err != nil { 404 return errors.Wrap(err, errCouldNotGetElement) 405 } 406 if err = element.ScrollIntoView(); err != nil { 407 return errors.Wrap(err, errCouldNotScroll) 408 } 409 if err = element.Input(value); err != nil { 410 return errors.Wrap(err, "could not input element") 411 } 412 return nil 413 } 414 415 // TimeInputElement executes time input on an element 416 func (p *Page) TimeInputElement(act *Action, out map[string]string /*TODO review unused parameter*/) error { 417 value := p.getActionArgWithDefaultValues(act, "value") 418 if value == "" { 419 return errinvalidArguments 420 } 421 element, err := p.pageElementBy(act.Data) 422 if err != nil { 423 return errors.Wrap(err, errCouldNotGetElement) 424 } 425 if err = element.ScrollIntoView(); err != nil { 426 return errors.Wrap(err, errCouldNotScroll) 427 } 428 t, err := time.Parse(time.RFC3339, value) 429 if err != nil { 430 return errors.Wrap(err, "could not parse time") 431 } 432 if err := element.InputTime(t); err != nil { 433 return errors.Wrap(err, "could not input element") 434 } 435 return nil 436 } 437 438 // SelectInputElement executes select input statement action on a element 439 func (p *Page) SelectInputElement(act *Action, out map[string]string /*TODO review unused parameter*/) error { 440 value := p.getActionArgWithDefaultValues(act, "value") 441 if value == "" { 442 return errinvalidArguments 443 } 444 element, err := p.pageElementBy(act.Data) 445 if err != nil { 446 return errors.Wrap(err, errCouldNotGetElement) 447 } 448 if err = element.ScrollIntoView(); err != nil { 449 return errors.Wrap(err, errCouldNotScroll) 450 } 451 452 selectedBool := false 453 if p.getActionArgWithDefaultValues(act, "selected") == "true" { 454 selectedBool = true 455 } 456 by := p.getActionArgWithDefaultValues(act, "selector") 457 if err := element.Select([]string{value}, selectedBool, selectorBy(by)); err != nil { 458 return errors.Wrap(err, "could not select input") 459 } 460 return nil 461 } 462 463 // WaitLoad waits for the page to load 464 func (p *Page) WaitLoad(act *Action, out map[string]string /*TODO review unused parameter*/) error { 465 p.page.Timeout(2 * time.Second).WaitNavigation(proto.PageLifecycleEventNameFirstMeaningfulPaint)() 466 467 // Wait for the window.onload event and also wait for the network requests 468 // to become idle for a maximum duration of 3 seconds. If the requests 469 // do not finish, 470 if err := p.page.WaitLoad(); err != nil { 471 return errors.Wrap(err, "could not wait load event") 472 } 473 _ = p.page.WaitIdle(1 * time.Second) 474 return nil 475 } 476 477 // GetResource gets a resource from an element from page. 478 func (p *Page) GetResource(act *Action, out map[string]string) error { 479 element, err := p.pageElementBy(act.Data) 480 if err != nil { 481 return errors.Wrap(err, errCouldNotGetElement) 482 } 483 resource, err := element.Resource() 484 if err != nil { 485 return errors.Wrap(err, "could not get src for element") 486 } 487 if act.Name != "" { 488 out[act.Name] = string(resource) 489 } 490 return nil 491 } 492 493 // FilesInput acts with a file input element on page 494 func (p *Page) FilesInput(act *Action, out map[string]string /*TODO review unused parameter*/) error { 495 element, err := p.pageElementBy(act.Data) 496 if err != nil { 497 return errors.Wrap(err, errCouldNotGetElement) 498 } 499 if err = element.ScrollIntoView(); err != nil { 500 return errors.Wrap(err, errCouldNotScroll) 501 } 502 value := p.getActionArgWithDefaultValues(act, "value") 503 filesPaths := strings.Split(value, ",") 504 if err := element.SetFiles(filesPaths); err != nil { 505 return errors.Wrap(err, "could not set files") 506 } 507 return nil 508 } 509 510 // ExtractElement extracts from an element on the page. 511 func (p *Page) ExtractElement(act *Action, out map[string]string) error { 512 element, err := p.pageElementBy(act.Data) 513 if err != nil { 514 return errors.Wrap(err, errCouldNotGetElement) 515 } 516 if err = element.ScrollIntoView(); err != nil { 517 return errors.Wrap(err, errCouldNotScroll) 518 } 519 switch p.getActionArgWithDefaultValues(act, "target") { 520 case "attribute": 521 attrName := p.getActionArgWithDefaultValues(act, "attribute") 522 if attrName == "" { 523 return errors.New("attribute can't be empty") 524 } 525 attrValue, err := element.Attribute(attrName) 526 if err != nil { 527 return errors.Wrap(err, "could not get attribute") 528 } 529 if act.Name != "" { 530 out[act.Name] = *attrValue 531 } 532 default: 533 text, err := element.Text() 534 if err != nil { 535 return errors.Wrap(err, "could not get element text node") 536 } 537 if act.Name != "" { 538 out[act.Name] = text 539 } 540 } 541 return nil 542 } 543 544 type protoEvent struct { 545 event string 546 } 547 548 // ProtoEvent returns the cdp.Event.Method 549 func (p *protoEvent) ProtoEvent() string { 550 return p.event 551 } 552 553 // WaitEvent waits for an event to happen on the page. 554 func (p *Page) WaitEvent(act *Action, out map[string]string /*TODO review unused parameter*/) error { 555 event := p.getActionArgWithDefaultValues(act, "event") 556 if event == "" { 557 return errors.New("event not recognized") 558 } 559 protoEvent := &protoEvent{event: event} 560 561 // Uses another instance in order to be able to chain the timeout only to the wait operation 562 pageCopy := p.page 563 timeout := p.getActionArg(act, "timeout") 564 if timeout != "" { 565 ts, err := strconv.Atoi(timeout) 566 if err != nil { 567 return errors.Wrap(err, "could not get timeout") 568 } 569 if ts > 0 { 570 pageCopy = p.page.Timeout(time.Duration(ts) * time.Second) 571 } 572 } 573 // Just wait the event to happen 574 pageCopy.WaitEvent(protoEvent)() 575 return nil 576 } 577 578 // pageElementBy returns a page element from a variety of inputs. 579 // 580 // Supported values for by: r -> selector & regex, x -> xpath, js -> eval js, 581 // search => query, default ("") => selector. 582 func (p *Page) pageElementBy(data map[string]string) (*rod.Element, error) { 583 by, ok := data["by"] 584 if !ok { 585 by = "" 586 } 587 page := p.page 588 589 switch by { 590 case "r", "regex": 591 return page.ElementR(data["selector"], data["regex"]) 592 case "x", "xpath": 593 return page.ElementX(data["xpath"]) 594 case "js": 595 return page.ElementByJS(&rod.EvalOptions{JS: data["js"]}) 596 case "search": 597 elms, err := page.Search(data["query"]) 598 if err != nil { 599 return nil, err 600 } 601 602 if elms.First != nil { 603 return elms.First, nil 604 } 605 return nil, errors.New("no such element") 606 default: 607 return page.Element(data["selector"]) 608 } 609 } 610 611 // DebugAction enables debug action on a page. 612 func (p *Page) DebugAction(act *Action, out map[string]string /*TODO review unused parameter*/) error { 613 p.instance.browser.engine.SlowMotion(5 * time.Second) 614 p.instance.browser.engine.Trace(true) 615 return nil 616 } 617 618 // SleepAction sleeps on the page for a specified duration 619 func (p *Page) SleepAction(act *Action, out map[string]string /*TODO review unused parameter*/) error { 620 seconds := act.Data["duration"] 621 if seconds == "" { 622 seconds = "5" 623 } 624 parsed, err := strconv.Atoi(seconds) 625 if err != nil { 626 return err 627 } 628 time.Sleep(time.Duration(parsed) * time.Second) 629 return nil 630 } 631 632 // selectorBy returns a selector from a representation. 633 func selectorBy(selector string) rod.SelectorType { 634 switch selector { 635 case "r": 636 return rod.SelectorTypeRegex 637 case "css": 638 return rod.SelectorTypeCSSSector 639 case "regex": 640 return rod.SelectorTypeRegex 641 default: 642 return rod.SelectorTypeText 643 } 644 } 645 646 func (p *Page) getActionArg(action *Action, arg string) string { 647 return p.getActionArgWithValues(action, arg, nil) 648 } 649 650 func (p *Page) getActionArgWithDefaultValues(action *Action, arg string) string { 651 return p.getActionArgWithValues(action, arg, generators.MergeMaps( 652 generators.BuildPayloadFromOptions(p.instance.browser.options), 653 p.payloads, 654 )) 655 } 656 657 func (p *Page) getActionArgWithValues(action *Action, arg string, values map[string]interface{}) string { 658 argValue := action.GetArg(arg) 659 argValue = replaceWithValues(argValue, values) 660 if p.instance.interactsh != nil { 661 var interactshURLs []string 662 argValue, interactshURLs = p.instance.interactsh.Replace(argValue, p.InteractshURLs) 663 p.addInteractshURL(interactshURLs...) 664 } 665 return argValue 666 }