github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/daemon/api_snaps.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2015-2020 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package daemon 21 22 import ( 23 "context" 24 "encoding/json" 25 "errors" 26 "fmt" 27 "mime" 28 "net/http" 29 "strings" 30 31 "github.com/snapcore/snapd/client" 32 "github.com/snapcore/snapd/i18n" 33 "github.com/snapcore/snapd/logger" 34 "github.com/snapcore/snapd/overlord/auth" 35 "github.com/snapcore/snapd/overlord/servicestate" 36 "github.com/snapcore/snapd/overlord/snapstate" 37 "github.com/snapcore/snapd/overlord/state" 38 "github.com/snapcore/snapd/progress" 39 "github.com/snapcore/snapd/sandbox" 40 "github.com/snapcore/snapd/snap" 41 "github.com/snapcore/snapd/snap/channel" 42 "github.com/snapcore/snapd/strutil" 43 ) 44 45 var ( 46 // see daemon.go:canAccess for details how the access is controlled 47 snapCmd = &Command{ 48 Path: "/v2/snaps/{name}", 49 GET: getSnapInfo, 50 POST: postSnap, 51 ReadAccess: openAccess{}, 52 WriteAccess: authenticatedAccess{Polkit: polkitActionManage}, 53 } 54 55 snapsCmd = &Command{ 56 Path: "/v2/snaps", 57 GET: getSnapsInfo, 58 POST: postSnaps, 59 ReadAccess: openAccess{}, 60 WriteAccess: authenticatedAccess{Polkit: polkitActionManage}, 61 } 62 ) 63 64 func getSnapInfo(c *Command, r *http.Request, user *auth.UserState) Response { 65 vars := muxVars(r) 66 name := vars["name"] 67 68 about, err := localSnapInfo(c.d.overlord.State(), name) 69 if err != nil { 70 if err == errNoSnap { 71 return SnapNotFound(name, err) 72 } 73 74 return InternalError("%v", err) 75 } 76 77 route := c.d.router.Get(c.Path) 78 if route == nil { 79 return InternalError("cannot find route for %q snap", name) 80 } 81 82 url, err := route.URL("name", name) 83 if err != nil { 84 return InternalError("cannot build URL for %q snap: %v", name, err) 85 } 86 87 sd := servicestate.NewStatusDecorator(progress.Null) 88 89 result := webify(mapLocal(about, sd), url.String()) 90 91 return SyncResponse(result) 92 } 93 94 func webify(result *client.Snap, resource string) *client.Snap { 95 if result.Icon == "" || strings.HasPrefix(result.Icon, "http") { 96 return result 97 } 98 result.Icon = "" 99 100 route := appIconCmd.d.router.Get(appIconCmd.Path) 101 if route != nil { 102 url, err := route.URL("name", result.Name) 103 if err == nil { 104 result.Icon = url.String() 105 } 106 } 107 108 return result 109 } 110 111 func postSnap(c *Command, r *http.Request, user *auth.UserState) Response { 112 route := c.d.router.Get(stateChangeCmd.Path) 113 if route == nil { 114 return InternalError("cannot find route for change") 115 } 116 117 decoder := json.NewDecoder(r.Body) 118 var inst snapInstruction 119 if err := decoder.Decode(&inst); err != nil { 120 return BadRequest("cannot decode request body into snap instruction: %v", err) 121 } 122 inst.ctx = r.Context() 123 124 state := c.d.overlord.State() 125 state.Lock() 126 defer state.Unlock() 127 128 if user != nil { 129 inst.userID = user.ID 130 } 131 132 vars := muxVars(r) 133 inst.Snaps = []string{vars["name"]} 134 135 if err := inst.validate(); err != nil { 136 return BadRequest("%s", err) 137 } 138 139 impl := inst.dispatch() 140 if impl == nil { 141 return BadRequest("unknown action %s", inst.Action) 142 } 143 144 msg, tsets, err := impl(&inst, state) 145 if err != nil { 146 return inst.errToResponse(err) 147 } 148 149 chg := newChange(state, inst.Action+"-snap", msg, tsets, inst.Snaps) 150 151 ensureStateSoon(state) 152 153 return AsyncResponse(nil, chg.ID()) 154 } 155 156 type snapRevisionOptions struct { 157 Channel string `json:"channel"` 158 Revision snap.Revision `json:"revision"` 159 160 CohortKey string `json:"cohort-key"` 161 LeaveCohort bool `json:"leave-cohort"` 162 } 163 164 func (ropt *snapRevisionOptions) validate() error { 165 if ropt.CohortKey != "" { 166 if ropt.LeaveCohort { 167 return fmt.Errorf("cannot specify both cohort-key and leave-cohort") 168 } 169 if !ropt.Revision.Unset() { 170 return fmt.Errorf("cannot specify both cohort-key and revision") 171 } 172 } 173 174 if ropt.Channel != "" { 175 _, err := channel.Parse(ropt.Channel, "-") 176 if err != nil { 177 return err 178 } 179 } 180 return nil 181 } 182 183 type snapInstruction struct { 184 progress.NullMeter 185 186 Action string `json:"action"` 187 Amend bool `json:"amend"` 188 snapRevisionOptions 189 DevMode bool `json:"devmode"` 190 JailMode bool `json:"jailmode"` 191 Classic bool `json:"classic"` 192 IgnoreValidation bool `json:"ignore-validation"` 193 IgnoreRunning bool `json:"ignore-running"` 194 Unaliased bool `json:"unaliased"` 195 Purge bool `json:"purge,omitempty"` 196 Snaps []string `json:"snaps"` 197 Users []string `json:"users"` 198 199 // The fields below should not be unmarshalled into. Do not export them. 200 userID int 201 ctx context.Context 202 } 203 204 func (inst *snapInstruction) revnoOpts() *snapstate.RevisionOptions { 205 return &snapstate.RevisionOptions{ 206 Channel: inst.Channel, 207 Revision: inst.Revision, 208 CohortKey: inst.CohortKey, 209 LeaveCohort: inst.LeaveCohort, 210 } 211 } 212 213 func (inst *snapInstruction) modeFlags() (snapstate.Flags, error) { 214 return modeFlags(inst.DevMode, inst.JailMode, inst.Classic) 215 } 216 217 func (inst *snapInstruction) installFlags() (snapstate.Flags, error) { 218 flags, err := inst.modeFlags() 219 if err != nil { 220 return snapstate.Flags{}, err 221 } 222 if inst.Unaliased { 223 flags.Unaliased = true 224 } 225 if inst.IgnoreRunning { 226 flags.IgnoreRunning = true 227 } 228 if inst.IgnoreValidation { 229 flags.IgnoreValidation = true 230 } 231 232 return flags, nil 233 } 234 235 func (inst *snapInstruction) validate() error { 236 if inst.CohortKey != "" { 237 if inst.Action != "install" && inst.Action != "refresh" && inst.Action != "switch" { 238 return fmt.Errorf("cohort-key can only be specified for install, refresh, or switch") 239 } 240 } 241 if inst.LeaveCohort { 242 if inst.Action != "refresh" && inst.Action != "switch" { 243 return fmt.Errorf("leave-cohort can only be specified for refresh or switch") 244 } 245 } 246 if inst.Action == "install" { 247 for _, snapName := range inst.Snaps { 248 // FIXME: alternatively we could simply mutate *inst 249 // and s/ubuntu-core/core/ ? 250 if snapName == "ubuntu-core" { 251 return fmt.Errorf(`cannot install "ubuntu-core", please use "core" instead`) 252 } 253 } 254 } 255 256 return inst.snapRevisionOptions.validate() 257 } 258 259 type snapInstructionResult struct { 260 Summary string 261 Affected []string 262 Tasksets []*state.TaskSet 263 Result map[string]interface{} 264 } 265 266 var errDevJailModeConflict = errors.New("cannot use devmode and jailmode flags together") 267 var errClassicDevmodeConflict = errors.New("cannot use classic and devmode flags together") 268 var errNoJailMode = errors.New("this system cannot honour the jailmode flag") 269 270 func modeFlags(devMode, jailMode, classic bool) (snapstate.Flags, error) { 271 flags := snapstate.Flags{} 272 devModeOS := sandbox.ForceDevMode() 273 switch { 274 case jailMode && devModeOS: 275 return flags, errNoJailMode 276 case jailMode && devMode: 277 return flags, errDevJailModeConflict 278 case devMode && classic: 279 return flags, errClassicDevmodeConflict 280 } 281 // NOTE: jailmode and classic are allowed together. In that setting, 282 // jailmode overrides classic and the app gets regular (non-classic) 283 // confinement. 284 flags.JailMode = jailMode 285 flags.Classic = classic 286 flags.DevMode = devMode 287 return flags, nil 288 } 289 290 func snapInstall(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 291 if len(inst.Snaps[0]) == 0 { 292 return "", nil, fmt.Errorf(i18n.G("cannot install snap with empty name")) 293 } 294 295 flags, err := inst.installFlags() 296 if err != nil { 297 return "", nil, err 298 } 299 300 var ckey string 301 if inst.CohortKey == "" { 302 logger.Noticef("Installing snap %q revision %s", inst.Snaps[0], inst.Revision) 303 } else { 304 ckey = strutil.ElliptLeft(inst.CohortKey, 10) 305 logger.Noticef("Installing snap %q from cohort %q", inst.Snaps[0], ckey) 306 } 307 tset, err := snapstateInstall(inst.ctx, st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags) 308 if err != nil { 309 return "", nil, err 310 } 311 312 msg := fmt.Sprintf(i18n.G("Install %q snap"), inst.Snaps[0]) 313 if inst.Channel != "stable" && inst.Channel != "" { 314 msg += fmt.Sprintf(" from %q channel", inst.Channel) 315 } 316 if inst.CohortKey != "" { 317 msg += fmt.Sprintf(" from %q cohort", ckey) 318 } 319 return msg, []*state.TaskSet{tset}, nil 320 } 321 322 func snapUpdate(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 323 // TODO: bail if revision is given (and != current?), *or* behave as with install --revision? 324 flags, err := inst.modeFlags() 325 if err != nil { 326 return "", nil, err 327 } 328 if inst.IgnoreValidation { 329 flags.IgnoreValidation = true 330 } 331 if inst.IgnoreRunning { 332 flags.IgnoreRunning = true 333 } 334 if inst.Amend { 335 flags.Amend = true 336 } 337 338 // we need refreshed snap-declarations to enforce refresh-control as best as we can 339 if err = assertstateRefreshSnapDeclarations(st, inst.userID); err != nil { 340 return "", nil, err 341 } 342 343 ts, err := snapstateUpdate(st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags) 344 if err != nil { 345 return "", nil, err 346 } 347 348 msg := fmt.Sprintf(i18n.G("Refresh %q snap"), inst.Snaps[0]) 349 if inst.Channel != "stable" && inst.Channel != "" { 350 msg = fmt.Sprintf(i18n.G("Refresh %q snap from %q channel"), inst.Snaps[0], inst.Channel) 351 } 352 353 return msg, []*state.TaskSet{ts}, nil 354 } 355 356 func snapRemove(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 357 ts, err := snapstate.Remove(st, inst.Snaps[0], inst.Revision, &snapstate.RemoveFlags{Purge: inst.Purge}) 358 if err != nil { 359 return "", nil, err 360 } 361 362 msg := fmt.Sprintf(i18n.G("Remove %q snap"), inst.Snaps[0]) 363 return msg, []*state.TaskSet{ts}, nil 364 } 365 366 func snapRevert(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 367 var ts *state.TaskSet 368 369 flags, err := inst.modeFlags() 370 if err != nil { 371 return "", nil, err 372 } 373 374 if inst.Revision.Unset() { 375 ts, err = snapstateRevert(st, inst.Snaps[0], flags) 376 } else { 377 ts, err = snapstateRevertToRevision(st, inst.Snaps[0], inst.Revision, flags) 378 } 379 if err != nil { 380 return "", nil, err 381 } 382 383 msg := fmt.Sprintf(i18n.G("Revert %q snap"), inst.Snaps[0]) 384 return msg, []*state.TaskSet{ts}, nil 385 } 386 387 func snapEnable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 388 if !inst.Revision.Unset() { 389 return "", nil, errors.New("enable takes no revision") 390 } 391 ts, err := snapstate.Enable(st, inst.Snaps[0]) 392 if err != nil { 393 return "", nil, err 394 } 395 396 msg := fmt.Sprintf(i18n.G("Enable %q snap"), inst.Snaps[0]) 397 return msg, []*state.TaskSet{ts}, nil 398 } 399 400 func snapDisable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 401 if !inst.Revision.Unset() { 402 return "", nil, errors.New("disable takes no revision") 403 } 404 ts, err := snapstate.Disable(st, inst.Snaps[0]) 405 if err != nil { 406 return "", nil, err 407 } 408 409 msg := fmt.Sprintf(i18n.G("Disable %q snap"), inst.Snaps[0]) 410 return msg, []*state.TaskSet{ts}, nil 411 } 412 413 func snapSwitch(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 414 if !inst.Revision.Unset() { 415 return "", nil, errors.New("switch takes no revision") 416 } 417 ts, err := snapstateSwitch(st, inst.Snaps[0], inst.revnoOpts()) 418 if err != nil { 419 return "", nil, err 420 } 421 422 var msg string 423 switch { 424 case inst.LeaveCohort && inst.Channel != "": 425 msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q and away from cohort"), inst.Snaps[0], inst.Channel) 426 case inst.LeaveCohort: 427 msg = fmt.Sprintf(i18n.G("Switch %q snap away from cohort"), inst.Snaps[0]) 428 case inst.CohortKey == "" && inst.Channel != "": 429 msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q"), inst.Snaps[0], inst.Channel) 430 case inst.CohortKey != "" && inst.Channel == "": 431 msg = fmt.Sprintf(i18n.G("Switch %q snap to cohort %q"), inst.Snaps[0], strutil.ElliptLeft(inst.CohortKey, 10)) 432 default: 433 msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q and cohort %q"), inst.Snaps[0], inst.Channel, strutil.ElliptLeft(inst.CohortKey, 10)) 434 } 435 return msg, []*state.TaskSet{ts}, nil 436 } 437 438 type snapActionFunc func(*snapInstruction, *state.State) (string, []*state.TaskSet, error) 439 440 var snapInstructionDispTable = map[string]snapActionFunc{ 441 "install": snapInstall, 442 "refresh": snapUpdate, 443 "remove": snapRemove, 444 "revert": snapRevert, 445 "enable": snapEnable, 446 "disable": snapDisable, 447 "switch": snapSwitch, 448 } 449 450 func (inst *snapInstruction) dispatch() snapActionFunc { 451 if len(inst.Snaps) != 1 { 452 logger.Panicf("dispatch only handles single-snap ops; got %d", len(inst.Snaps)) 453 } 454 return snapInstructionDispTable[inst.Action] 455 } 456 457 func (inst *snapInstruction) errToResponse(err error) *apiError { 458 if len(inst.Snaps) == 0 { 459 return errToResponse(err, nil, BadRequest, "cannot %s: %v", inst.Action) 460 } 461 462 return errToResponse(err, inst.Snaps, BadRequest, "cannot %s %s: %v", inst.Action, strutil.Quoted(inst.Snaps)) 463 } 464 465 func postSnaps(c *Command, r *http.Request, user *auth.UserState) Response { 466 contentType := r.Header.Get("Content-Type") 467 468 mediaType, params, err := mime.ParseMediaType(contentType) 469 if err != nil { 470 return BadRequest("cannot parse content type: %v", err) 471 } 472 473 if mediaType == "application/json" { 474 charset := strings.ToUpper(params["charset"]) 475 if charset != "" && charset != "UTF-8" { 476 return BadRequest("unknown charset in content type: %s", contentType) 477 } 478 return snapOpMany(c, r, user) 479 } 480 481 if !strings.HasPrefix(contentType, "multipart/") { 482 return BadRequest("unknown content type: %s", contentType) 483 } 484 485 return sideloadOrTrySnap(c, r.Body, params["boundary"], user) 486 } 487 488 func snapOpMany(c *Command, r *http.Request, user *auth.UserState) Response { 489 route := c.d.router.Get(stateChangeCmd.Path) 490 if route == nil { 491 return InternalError("cannot find route for change") 492 } 493 494 decoder := json.NewDecoder(r.Body) 495 var inst snapInstruction 496 if err := decoder.Decode(&inst); err != nil { 497 return BadRequest("cannot decode request body into snap instruction: %v", err) 498 } 499 500 // TODO: inst.Amend, etc? 501 if inst.Channel != "" || !inst.Revision.Unset() || inst.DevMode || inst.JailMode || inst.CohortKey != "" || inst.LeaveCohort || inst.Purge { 502 return BadRequest("unsupported option provided for multi-snap operation") 503 } 504 if err := inst.validate(); err != nil { 505 return BadRequest("%v", err) 506 } 507 508 st := c.d.overlord.State() 509 st.Lock() 510 defer st.Unlock() 511 512 if user != nil { 513 inst.userID = user.ID 514 } 515 516 op := inst.dispatchForMany() 517 if op == nil { 518 return BadRequest("unsupported multi-snap operation %q", inst.Action) 519 } 520 res, err := op(&inst, st) 521 if err != nil { 522 return inst.errToResponse(err) 523 } 524 525 var chg *state.Change 526 if len(res.Tasksets) == 0 { 527 chg = st.NewChange(inst.Action+"-snap", res.Summary) 528 chg.SetStatus(state.DoneStatus) 529 } else { 530 chg = newChange(st, inst.Action+"-snap", res.Summary, res.Tasksets, res.Affected) 531 ensureStateSoon(st) 532 } 533 534 chg.Set("api-data", map[string]interface{}{"snap-names": res.Affected}) 535 536 return AsyncResponse(res.Result, chg.ID()) 537 } 538 539 type snapManyActionFunc func(*snapInstruction, *state.State) (*snapInstructionResult, error) 540 541 func (inst *snapInstruction) dispatchForMany() (op snapManyActionFunc) { 542 switch inst.Action { 543 case "refresh": 544 op = snapUpdateMany 545 case "install": 546 op = snapInstallMany 547 case "remove": 548 op = snapRemoveMany 549 case "snapshot": 550 // see api_snapshots.go 551 op = snapshotMany 552 } 553 return op 554 } 555 556 func snapInstallMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { 557 for _, name := range inst.Snaps { 558 if len(name) == 0 { 559 return nil, fmt.Errorf(i18n.G("cannot install snap with empty name")) 560 } 561 } 562 installed, tasksets, err := snapstateInstallMany(st, inst.Snaps, inst.userID) 563 if err != nil { 564 return nil, err 565 } 566 567 var msg string 568 switch len(inst.Snaps) { 569 case 0: 570 return nil, fmt.Errorf("cannot install zero snaps") 571 case 1: 572 msg = fmt.Sprintf(i18n.G("Install snap %q"), inst.Snaps[0]) 573 default: 574 quoted := strutil.Quoted(inst.Snaps) 575 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 576 msg = fmt.Sprintf(i18n.G("Install snaps %s"), quoted) 577 } 578 579 return &snapInstructionResult{ 580 Summary: msg, 581 Affected: installed, 582 Tasksets: tasksets, 583 }, nil 584 } 585 586 func snapUpdateMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { 587 // we need refreshed snap-declarations to enforce refresh-control as best as we can, this also ensures that snap-declarations and their prerequisite assertions are updated regularly 588 if err := assertstateRefreshSnapDeclarations(st, inst.userID); err != nil { 589 return nil, err 590 } 591 592 // TODO: use a per-request context 593 updated, tasksets, err := snapstateUpdateMany(context.TODO(), st, inst.Snaps, inst.userID, nil) 594 if err != nil { 595 return nil, err 596 } 597 598 var msg string 599 switch len(updated) { 600 case 0: 601 if len(inst.Snaps) != 0 { 602 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 603 msg = fmt.Sprintf(i18n.G("Refresh snaps %s: no updates"), strutil.Quoted(inst.Snaps)) 604 } else { 605 msg = i18n.G("Refresh all snaps: no updates") 606 } 607 case 1: 608 msg = fmt.Sprintf(i18n.G("Refresh snap %q"), updated[0]) 609 default: 610 quoted := strutil.Quoted(updated) 611 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 612 msg = fmt.Sprintf(i18n.G("Refresh snaps %s"), quoted) 613 } 614 615 return &snapInstructionResult{ 616 Summary: msg, 617 Affected: updated, 618 Tasksets: tasksets, 619 }, nil 620 } 621 622 func snapRemoveMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { 623 removed, tasksets, err := snapstateRemoveMany(st, inst.Snaps) 624 if err != nil { 625 return nil, err 626 } 627 628 var msg string 629 switch len(inst.Snaps) { 630 case 0: 631 return nil, fmt.Errorf("cannot remove zero snaps") 632 case 1: 633 msg = fmt.Sprintf(i18n.G("Remove snap %q"), inst.Snaps[0]) 634 default: 635 quoted := strutil.Quoted(inst.Snaps) 636 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 637 msg = fmt.Sprintf(i18n.G("Remove snaps %s"), quoted) 638 } 639 640 return &snapInstructionResult{ 641 Summary: msg, 642 Affected: removed, 643 Tasksets: tasksets, 644 }, nil 645 } 646 647 // query many snaps 648 func getSnapsInfo(c *Command, r *http.Request, user *auth.UserState) Response { 649 650 if shouldSearchStore(r) { 651 logger.Noticef("Jumping to \"find\" to better support legacy request %q", r.URL) 652 return searchStore(c, r, user) 653 } 654 655 route := c.d.router.Get(snapCmd.Path) 656 if route == nil { 657 return InternalError("cannot find route for snaps") 658 } 659 660 query := r.URL.Query() 661 var all bool 662 sel := query.Get("select") 663 switch sel { 664 case "all": 665 all = true 666 case "enabled", "": 667 all = false 668 default: 669 return BadRequest("invalid select parameter: %q", sel) 670 } 671 var wanted map[string]bool 672 if ns := query.Get("snaps"); len(ns) > 0 { 673 nsl := strutil.CommaSeparatedList(ns) 674 wanted = make(map[string]bool, len(nsl)) 675 for _, name := range nsl { 676 wanted[name] = true 677 } 678 } 679 680 found, err := allLocalSnapInfos(c.d.overlord.State(), all, wanted) 681 if err != nil { 682 return InternalError("cannot list local snaps! %v", err) 683 } 684 685 results := make([]*json.RawMessage, len(found)) 686 687 sd := servicestate.NewStatusDecorator(progress.Null) 688 for i, x := range found { 689 name := x.info.InstanceName() 690 rev := x.info.Revision 691 692 url, err := route.URL("name", name) 693 if err != nil { 694 logger.Noticef("Cannot build URL for snap %q revision %s: %v", name, rev, err) 695 continue 696 } 697 698 data, err := json.Marshal(webify(mapLocal(x, sd), url.String())) 699 if err != nil { 700 return InternalError("cannot serialize snap %q revision %s: %v", name, rev, err) 701 } 702 raw := json.RawMessage(data) 703 results[i] = &raw 704 } 705 706 return &findResponse{ 707 Results: results, 708 Sources: []string{"local"}, 709 } 710 } 711 712 func shouldSearchStore(r *http.Request) bool { 713 // we should jump to the old behaviour iff q is given, or if 714 // sources is given and either empty or contains the word 715 // 'store'. Otherwise, local results only. 716 717 query := r.URL.Query() 718 719 if _, ok := query["q"]; ok { 720 logger.Debugf("use of obsolete \"q\" parameter: %q", r.URL) 721 return true 722 } 723 724 if src, ok := query["sources"]; ok { 725 logger.Debugf("use of obsolete \"sources\" parameter: %q", r.URL) 726 if len(src) == 0 || strings.Contains(src[0], "store") { 727 return true 728 } 729 } 730 731 return false 732 }