1/* 2 * Copyright © 2018-2021 A Bunch Tell LLC. 3 * 4 * This file is part of WriteFreely. 5 * 6 * WriteFreely is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU Affero General Public License, included 8 * in the LICENSE file in this source code package. 9 */ 10 11package writefreely 12 13import ( 14 "context" 15 "database/sql" 16 "fmt" 17 "github.com/writeas/web-core/silobridge" 18 wf_db "github.com/writefreely/writefreely/db" 19 "net/http" 20 "strings" 21 "time" 22 23 "github.com/guregu/null" 24 "github.com/guregu/null/zero" 25 uuid "github.com/nu7hatch/gouuid" 26 "github.com/writeas/activityserve" 27 "github.com/writeas/impart" 28 "github.com/writeas/web-core/activitypub" 29 "github.com/writeas/web-core/auth" 30 "github.com/writeas/web-core/data" 31 "github.com/writeas/web-core/id" 32 "github.com/writeas/web-core/log" 33 "github.com/writeas/web-core/query" 34 "github.com/writefreely/writefreely/author" 35 "github.com/writefreely/writefreely/config" 36 "github.com/writefreely/writefreely/key" 37) 38 39const ( 40 mySQLErrDuplicateKey = 1062 41 mySQLErrCollationMix = 1267 42 mySQLErrTooManyConns = 1040 43 mySQLErrMaxUserConns = 1203 44 45 driverMySQL = "mysql" 46 driverSQLite = "sqlite3" 47) 48 49var ( 50 SQLiteEnabled bool 51) 52 53type writestore interface { 54 CreateUser(*config.Config, *User, string, string) error 55 UpdateUserEmail(keys *key.Keychain, userID int64, email string) error 56 UpdateEncryptedUserEmail(int64, []byte) error 57 GetUserByID(int64) (*User, error) 58 GetUserForAuth(string) (*User, error) 59 GetUserForAuthByID(int64) (*User, error) 60 GetUserNameFromToken(string) (string, error) 61 GetUserDataFromToken(string) (int64, string, error) 62 GetAPIUser(header string) (*User, error) 63 GetUserID(accessToken string) int64 64 GetUserIDPrivilege(accessToken string) (userID int64, sudo bool) 65 DeleteToken(accessToken []byte) error 66 FetchLastAccessToken(userID int64) string 67 GetAccessToken(userID int64) (string, error) 68 GetTemporaryAccessToken(userID int64, validSecs int) (string, error) 69 GetTemporaryOneTimeAccessToken(userID int64, validSecs int, oneTime bool) (string, error) 70 DeleteAccount(userID int64) error 71 ChangeSettings(app *App, u *User, s *userSettings) error 72 ChangePassphrase(userID int64, sudo bool, curPass string, hashedPass []byte) error 73 74 GetCollections(u *User, hostName string) (*[]Collection, error) 75 GetPublishableCollections(u *User, hostName string) (*[]Collection, error) 76 GetMeStats(u *User) userMeStats 77 GetTotalCollections() (int64, error) 78 GetTotalPosts() (int64, error) 79 GetTopPosts(u *User, alias string, hostName string) (*[]PublicPost, error) 80 GetAnonymousPosts(u *User, page int) (*[]PublicPost, error) 81 GetUserPosts(u *User) (*[]PublicPost, error) 82 83 CreateOwnedPost(post *SubmittedPost, accessToken, collAlias, hostName string) (*PublicPost, error) 84 CreatePost(userID, collID int64, post *SubmittedPost) (*Post, error) 85 UpdateOwnedPost(post *AuthenticatedPost, userID int64) error 86 GetEditablePost(id, editToken string) (*PublicPost, error) 87 PostIDExists(id string) bool 88 GetPost(id string, collectionID int64) (*PublicPost, error) 89 GetOwnedPost(id string, ownerID int64) (*PublicPost, error) 90 GetPostProperty(id string, collectionID int64, property string) (interface{}, error) 91 92 CreateCollectionFromToken(*config.Config, string, string, string) (*Collection, error) 93 CreateCollection(*config.Config, string, string, int64) (*Collection, error) 94 GetCollectionBy(condition string, value interface{}) (*Collection, error) 95 GetCollection(alias string) (*Collection, error) 96 GetCollectionForPad(alias string) (*Collection, error) 97 GetCollectionByID(id int64) (*Collection, error) 98 UpdateCollection(c *SubmittedCollection, alias string) error 99 DeleteCollection(alias string, userID int64) error 100 101 UpdatePostPinState(pinned bool, postID string, collID, ownerID, pos int64) error 102 GetLastPinnedPostPos(collID int64) int64 103 GetPinnedPosts(coll *CollectionObj, includeFuture bool) (*[]PublicPost, error) 104 RemoveCollectionRedirect(t *sql.Tx, alias string) error 105 GetCollectionRedirect(alias string) (new string) 106 IsCollectionAttributeOn(id int64, attr string) bool 107 CollectionHasAttribute(id int64, attr string) bool 108 109 CanCollect(cpr *ClaimPostRequest, userID int64) bool 110 AttemptClaim(p *ClaimPostRequest, query string, params []interface{}, slugIdx int) (sql.Result, error) 111 DispersePosts(userID int64, postIDs []string) (*[]ClaimPostResult, error) 112 ClaimPosts(cfg *config.Config, userID int64, collAlias string, posts *[]ClaimPostRequest) (*[]ClaimPostResult, error) 113 114 GetPostsCount(c *CollectionObj, includeFuture bool) 115 GetPosts(cfg *config.Config, c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool) (*[]PublicPost, error) 116 GetPostsTagged(cfg *config.Config, c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error) 117 118 GetAPFollowers(c *Collection) (*[]RemoteUser, error) 119 GetAPActorKeys(collectionID int64) ([]byte, []byte) 120 CreateUserInvite(id string, userID int64, maxUses int, expires *time.Time) error 121 GetUserInvites(userID int64) (*[]Invite, error) 122 GetUserInvite(id string) (*Invite, error) 123 GetUsersInvitedCount(id string) int64 124 CreateInvitedUser(inviteID string, userID int64) error 125 126 GetDynamicContent(id string) (*instanceContent, error) 127 UpdateDynamicContent(id, title, content, contentType string) error 128 GetAllUsers(page uint) (*[]User, error) 129 GetAllUsersCount() int64 130 GetUserLastPostTime(id int64) (*time.Time, error) 131 GetCollectionLastPostTime(id int64) (*time.Time, error) 132 133 GetIDForRemoteUser(context.Context, string, string, string) (int64, error) 134 RecordRemoteUserID(context.Context, int64, string, string, string, string) error 135 ValidateOAuthState(context.Context, string) (string, string, int64, string, error) 136 GenerateOAuthState(context.Context, string, string, int64, string) (string, error) 137 GetOauthAccounts(ctx context.Context, userID int64) ([]oauthAccountInfo, error) 138 RemoveOauth(ctx context.Context, userID int64, provider string, clientID string, remoteUserID string) error 139 140 DatabaseInitialized() bool 141} 142 143type datastore struct { 144 *sql.DB 145 driverName string 146} 147 148var _ writestore = &datastore{} 149 150func (db *datastore) now() string { 151 if db.driverName == driverSQLite { 152 return "strftime('%Y-%m-%d %H:%M:%S','now')" 153 } 154 return "NOW()" 155} 156 157func (db *datastore) clip(field string, l int) string { 158 if db.driverName == driverSQLite { 159 return fmt.Sprintf("SUBSTR(%s, 0, %d)", field, l) 160 } 161 return fmt.Sprintf("LEFT(%s, %d)", field, l) 162} 163 164func (db *datastore) upsert(indexedCols ...string) string { 165 if db.driverName == driverSQLite { 166 // NOTE: SQLite UPSERT syntax only works in v3.24.0 (2018-06-04) or later 167 // Leaving this for whenever we can upgrade and include it in our binary 168 cc := strings.Join(indexedCols, ", ") 169 return "ON CONFLICT(" + cc + ") DO UPDATE SET" 170 } 171 return "ON DUPLICATE KEY UPDATE" 172} 173 174func (db *datastore) dateSub(l int, unit string) string { 175 if db.driverName == driverSQLite { 176 return fmt.Sprintf("DATETIME('now', '-%d %s')", l, unit) 177 } 178 return fmt.Sprintf("DATE_SUB(NOW(), INTERVAL %d %s)", l, unit) 179} 180 181// CreateUser creates a new user in the database from the given User, UPDATING it in the process with the user's ID. 182func (db *datastore) CreateUser(cfg *config.Config, u *User, collectionTitle string, collectionDesc string) error { 183 if db.PostIDExists(u.Username) { 184 return impart.HTTPError{http.StatusConflict, "Invalid collection name."} 185 } 186 187 // New users get a `users` and `collections` row. 188 t, err := db.Begin() 189 if err != nil { 190 return err 191 } 192 193 // 1. Add to `users` table 194 // NOTE: Assumes User's Password is already hashed! 195 res, err := t.Exec("INSERT INTO users (username, password, email) VALUES (?, ?, ?)", u.Username, u.HashedPass, u.Email) 196 if err != nil { 197 t.Rollback() 198 if db.isDuplicateKeyErr(err) { 199 return impart.HTTPError{http.StatusConflict, "Username is already taken."} 200 } 201 202 log.Error("Rolling back users INSERT: %v\n", err) 203 return err 204 } 205 u.ID, err = res.LastInsertId() 206 if err != nil { 207 t.Rollback() 208 log.Error("Rolling back after LastInsertId: %v\n", err) 209 return err 210 } 211 212 // 2. Create user's Collection 213 if collectionTitle == "" { 214 collectionTitle = u.Username 215 } 216 res, err = t.Exec("INSERT INTO collections (alias, title, description, privacy, owner_id, view_count) VALUES (?, ?, ?, ?, ?, ?)", u.Username, collectionTitle, collectionDesc, defaultVisibility(cfg), u.ID, 0) 217 if err != nil { 218 t.Rollback() 219 if db.isDuplicateKeyErr(err) { 220 return impart.HTTPError{http.StatusConflict, "Username is already taken."} 221 } 222 log.Error("Rolling back collections INSERT: %v\n", err) 223 return err 224 } 225 226 db.RemoveCollectionRedirect(t, u.Username) 227 228 err = t.Commit() 229 if err != nil { 230 t.Rollback() 231 log.Error("Rolling back after Commit(): %v\n", err) 232 return err 233 } 234 235 return nil 236} 237 238// FIXME: We're returning errors inconsistently in this file. Do we use Errorf 239// for returned value, or impart? 240func (db *datastore) UpdateUserEmail(keys *key.Keychain, userID int64, email string) error { 241 encEmail, err := data.Encrypt(keys.EmailKey, email) 242 if err != nil { 243 return fmt.Errorf("Couldn't encrypt email %s: %s\n", email, err) 244 } 245 246 return db.UpdateEncryptedUserEmail(userID, encEmail) 247} 248 249func (db *datastore) UpdateEncryptedUserEmail(userID int64, encEmail []byte) error { 250 _, err := db.Exec("UPDATE users SET email = ? WHERE id = ?", encEmail, userID) 251 if err != nil { 252 return fmt.Errorf("Unable to update user email: %s", err) 253 } 254 255 return nil 256} 257 258func (db *datastore) CreateCollectionFromToken(cfg *config.Config, alias, title, accessToken string) (*Collection, error) { 259 userID := db.GetUserID(accessToken) 260 if userID == -1 { 261 return nil, ErrBadAccessToken 262 } 263 264 return db.CreateCollection(cfg, alias, title, userID) 265} 266 267func (db *datastore) GetUserCollectionCount(userID int64) (uint64, error) { 268 var collCount uint64 269 err := db.QueryRow("SELECT COUNT(*) FROM collections WHERE owner_id = ?", userID).Scan(&collCount) 270 switch { 271 case err == sql.ErrNoRows: 272 return 0, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user from database."} 273 case err != nil: 274 log.Error("Couldn't get collections count for user %d: %v", userID, err) 275 return 0, err 276 } 277 278 return collCount, nil 279} 280 281func (db *datastore) CreateCollection(cfg *config.Config, alias, title string, userID int64) (*Collection, error) { 282 if db.PostIDExists(alias) { 283 return nil, impart.HTTPError{http.StatusConflict, "Invalid collection name."} 284 } 285 286 // All good, so create new collection 287 res, err := db.Exec("INSERT INTO collections (alias, title, description, privacy, owner_id, view_count) VALUES (?, ?, ?, ?, ?, ?)", alias, title, "", defaultVisibility(cfg), userID, 0) 288 if err != nil { 289 if db.isDuplicateKeyErr(err) { 290 return nil, impart.HTTPError{http.StatusConflict, "Collection already exists."} 291 } 292 log.Error("Couldn't add to collections: %v\n", err) 293 return nil, err 294 } 295 296 c := &Collection{ 297 Alias: alias, 298 Title: title, 299 OwnerID: userID, 300 PublicOwner: false, 301 Public: defaultVisibility(cfg) == CollPublic, 302 } 303 304 c.ID, err = res.LastInsertId() 305 if err != nil { 306 log.Error("Couldn't get collection LastInsertId: %v\n", err) 307 } 308 309 return c, nil 310} 311 312func (db *datastore) GetUserByID(id int64) (*User, error) { 313 u := &User{ID: id} 314 315 err := db.QueryRow("SELECT username, password, email, created, status FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created, &u.Status) 316 switch { 317 case err == sql.ErrNoRows: 318 return nil, ErrUserNotFound 319 case err != nil: 320 log.Error("Couldn't SELECT user password: %v", err) 321 return nil, err 322 } 323 324 return u, nil 325} 326 327// IsUserSilenced returns true if the user account associated with id is 328// currently silenced. 329func (db *datastore) IsUserSilenced(id int64) (bool, error) { 330 u := &User{ID: id} 331 332 err := db.QueryRow("SELECT status FROM users WHERE id = ?", id).Scan(&u.Status) 333 switch { 334 case err == sql.ErrNoRows: 335 return false, fmt.Errorf("is user silenced: %v", ErrUserNotFound) 336 case err != nil: 337 log.Error("Couldn't SELECT user status: %v", err) 338 return false, fmt.Errorf("is user silenced: %v", err) 339 } 340 341 return u.IsSilenced(), nil 342} 343 344// DoesUserNeedAuth returns true if the user hasn't provided any methods for 345// authenticating with the account, such a passphrase or email address. 346// Any errors are reported to admin and silently quashed, returning false as the 347// result. 348func (db *datastore) DoesUserNeedAuth(id int64) bool { 349 var pass, email []byte 350 351 // Find out if user has an email set first 352 err := db.QueryRow("SELECT password, email FROM users WHERE id = ?", id).Scan(&pass, &email) 353 switch { 354 case err == sql.ErrNoRows: 355 // ERROR. Don't give false positives on needing auth methods 356 return false 357 case err != nil: 358 // ERROR. Don't give false positives on needing auth methods 359 log.Error("Couldn't SELECT user %d from users: %v", id, err) 360 return false 361 } 362 // User doesn't need auth if there's an email 363 return len(email) == 0 && len(pass) == 0 364} 365 366func (db *datastore) IsUserPassSet(id int64) (bool, error) { 367 var pass []byte 368 err := db.QueryRow("SELECT password FROM users WHERE id = ?", id).Scan(&pass) 369 switch { 370 case err == sql.ErrNoRows: 371 return false, nil 372 case err != nil: 373 log.Error("Couldn't SELECT user %d from users: %v", id, err) 374 return false, err 375 } 376 377 return len(pass) > 0, nil 378} 379 380func (db *datastore) GetUserForAuth(username string) (*User, error) { 381 u := &User{Username: username} 382 383 err := db.QueryRow("SELECT id, password, email, created, status FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Status) 384 switch { 385 case err == sql.ErrNoRows: 386 // Check if they've entered the wrong, unnormalized username 387 username = getSlug(username, "") 388 if username != u.Username { 389 err = db.QueryRow("SELECT id FROM users WHERE username = ? LIMIT 1", username).Scan(&u.ID) 390 if err == nil { 391 return db.GetUserForAuth(username) 392 } 393 } 394 return nil, ErrUserNotFound 395 case err != nil: 396 log.Error("Couldn't SELECT user password: %v", err) 397 return nil, err 398 } 399 400 return u, nil 401} 402 403func (db *datastore) GetUserForAuthByID(userID int64) (*User, error) { 404 u := &User{ID: userID} 405 406 err := db.QueryRow("SELECT id, password, email, created, status FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Status) 407 switch { 408 case err == sql.ErrNoRows: 409 return nil, ErrUserNotFound 410 case err != nil: 411 log.Error("Couldn't SELECT userForAuthByID: %v", err) 412 return nil, err 413 } 414 415 return u, nil 416} 417 418func (db *datastore) GetUserNameFromToken(accessToken string) (string, error) { 419 t := auth.GetToken(accessToken) 420 if len(t) == 0 { 421 return "", ErrNoAccessToken 422 } 423 424 var oneTime bool 425 var username string 426 err := db.QueryRow("SELECT username, one_time FROM accesstokens LEFT JOIN users ON user_id = id WHERE token LIKE ? AND (expires IS NULL OR expires > "+db.now()+")", t).Scan(&username, &oneTime) 427 switch { 428 case err == sql.ErrNoRows: 429 return "", ErrBadAccessToken 430 case err != nil: 431 return "", ErrInternalGeneral 432 } 433 434 // Delete token if it was one-time 435 if oneTime { 436 db.DeleteToken(t[:]) 437 } 438 439 return username, nil 440} 441 442func (db *datastore) GetUserDataFromToken(accessToken string) (int64, string, error) { 443 t := auth.GetToken(accessToken) 444 if len(t) == 0 { 445 return 0, "", ErrNoAccessToken 446 } 447 448 var userID int64 449 var oneTime bool 450 var username string 451 err := db.QueryRow("SELECT user_id, username, one_time FROM accesstokens LEFT JOIN users ON user_id = id WHERE token LIKE ? AND (expires IS NULL OR expires > "+db.now()+")", t).Scan(&userID, &username, &oneTime) 452 switch { 453 case err == sql.ErrNoRows: 454 return 0, "", ErrBadAccessToken 455 case err != nil: 456 return 0, "", ErrInternalGeneral 457 } 458 459 // Delete token if it was one-time 460 if oneTime { 461 db.DeleteToken(t[:]) 462 } 463 464 return userID, username, nil 465} 466 467func (db *datastore) GetAPIUser(header string) (*User, error) { 468 uID := db.GetUserID(header) 469 if uID == -1 { 470 return nil, fmt.Errorf(ErrUserNotFound.Error()) 471 } 472 return db.GetUserByID(uID) 473} 474 475// GetUserID takes a hexadecimal accessToken, parses it into its binary 476// representation, and gets any user ID associated with the token. If no user 477// is associated, -1 is returned. 478func (db *datastore) GetUserID(accessToken string) int64 { 479 i, _ := db.GetUserIDPrivilege(accessToken) 480 return i 481} 482 483func (db *datastore) GetUserIDPrivilege(accessToken string) (userID int64, sudo bool) { 484 t := auth.GetToken(accessToken) 485 if len(t) == 0 { 486 return -1, false 487 } 488 489 var oneTime bool 490 err := db.QueryRow("SELECT user_id, sudo, one_time FROM accesstokens WHERE token LIKE ? AND (expires IS NULL OR expires > "+db.now()+")", t).Scan(&userID, &sudo, &oneTime) 491 switch { 492 case err == sql.ErrNoRows: 493 return -1, false 494 case err != nil: 495 return -1, false 496 } 497 498 // Delete token if it was one-time 499 if oneTime { 500 db.DeleteToken(t[:]) 501 } 502 503 return 504} 505 506func (db *datastore) DeleteToken(accessToken []byte) error { 507 res, err := db.Exec("DELETE FROM accesstokens WHERE token LIKE ?", accessToken) 508 if err != nil { 509 return err 510 } 511 rowsAffected, _ := res.RowsAffected() 512 if rowsAffected == 0 { 513 return impart.HTTPError{http.StatusNotFound, "Token is invalid or doesn't exist"} 514 } 515 return nil 516} 517 518// FetchLastAccessToken creates a new non-expiring, valid access token for the given 519// userID. 520func (db *datastore) FetchLastAccessToken(userID int64) string { 521 var t []byte 522 err := db.QueryRow("SELECT token FROM accesstokens WHERE user_id = ? AND (expires IS NULL OR expires > "+db.now()+") ORDER BY created DESC LIMIT 1", userID).Scan(&t) 523 switch { 524 case err == sql.ErrNoRows: 525 return "" 526 case err != nil: 527 log.Error("Failed selecting from accesstoken: %v", err) 528 return "" 529 } 530 531 u, err := uuid.Parse(t) 532 if err != nil { 533 return "" 534 } 535 return u.String() 536} 537 538// GetAccessToken creates a new non-expiring, valid access token for the given 539// userID. 540func (db *datastore) GetAccessToken(userID int64) (string, error) { 541 return db.GetTemporaryOneTimeAccessToken(userID, 0, false) 542} 543 544// GetTemporaryAccessToken creates a new valid access token for the given 545// userID that remains valid for the given time in seconds. If validSecs is 0, 546// the access token doesn't automatically expire. 547func (db *datastore) GetTemporaryAccessToken(userID int64, validSecs int) (string, error) { 548 return db.GetTemporaryOneTimeAccessToken(userID, validSecs, false) 549} 550 551// GetTemporaryOneTimeAccessToken creates a new valid access token for the given 552// userID that remains valid for the given time in seconds and can only be used 553// once if oneTime is true. If validSecs is 0, the access token doesn't 554// automatically expire. 555func (db *datastore) GetTemporaryOneTimeAccessToken(userID int64, validSecs int, oneTime bool) (string, error) { 556 u, err := uuid.NewV4() 557 if err != nil { 558 log.Error("Unable to generate token: %v", err) 559 return "", err 560 } 561 562 // Insert UUID to `accesstokens` 563 binTok := u[:] 564 565 expirationVal := "NULL" 566 if validSecs > 0 { 567 expirationVal = fmt.Sprintf("DATE_ADD("+db.now()+", INTERVAL %d SECOND)", validSecs) 568 } 569 570 _, err = db.Exec("INSERT INTO accesstokens (token, user_id, one_time, expires) VALUES (?, ?, ?, "+expirationVal+")", string(binTok), userID, oneTime) 571 if err != nil { 572 log.Error("Couldn't INSERT accesstoken: %v", err) 573 return "", err 574 } 575 576 return u.String(), nil 577} 578 579func (db *datastore) CreateOwnedPost(post *SubmittedPost, accessToken, collAlias, hostName string) (*PublicPost, error) { 580 var userID, collID int64 = -1, -1 581 var coll *Collection 582 var err error 583 if accessToken != "" { 584 userID = db.GetUserID(accessToken) 585 if userID == -1 { 586 return nil, ErrBadAccessToken 587 } 588 if collAlias != "" { 589 coll, err = db.GetCollection(collAlias) 590 if err != nil { 591 return nil, err 592 } 593 coll.hostName = hostName 594 if coll.OwnerID != userID { 595 return nil, ErrForbiddenCollection 596 } 597 collID = coll.ID 598 } 599 } 600 601 rp := &PublicPost{} 602 rp.Post, err = db.CreatePost(userID, collID, post) 603 if err != nil { 604 return rp, err 605 } 606 if coll != nil { 607 coll.ForPublic() 608 rp.Collection = &CollectionObj{Collection: *coll} 609 } 610 return rp, nil 611} 612 613func (db *datastore) CreatePost(userID, collID int64, post *SubmittedPost) (*Post, error) { 614 idLen := postIDLen 615 friendlyID := id.GenerateFriendlyRandomString(idLen) 616 617 // Handle appearance / font face 618 appearance := post.Font 619 if !post.isFontValid() { 620 appearance = "norm" 621 } 622 623 var err error 624 ownerID := sql.NullInt64{ 625 Valid: false, 626 } 627 ownerCollID := sql.NullInt64{ 628 Valid: false, 629 } 630 slug := sql.NullString{"", false} 631 632 // If an alias was supplied, we'll add this to the collection as well. 633 if userID > 0 { 634 ownerID.Int64 = userID 635 ownerID.Valid = true 636 if collID > 0 { 637 ownerCollID.Int64 = collID 638 ownerCollID.Valid = true 639 var slugVal string 640 if post.Slug != nil && *post.Slug != "" { 641 slugVal = *post.Slug 642 } else { 643 if post.Title != nil && *post.Title != "" { 644 slugVal = getSlug(*post.Title, post.Language.String) 645 if slugVal == "" { 646 slugVal = getSlug(*post.Content, post.Language.String) 647 } 648 } else { 649 slugVal = getSlug(*post.Content, post.Language.String) 650 } 651 } 652 if slugVal == "" { 653 slugVal = friendlyID 654 } 655 slug = sql.NullString{slugVal, true} 656 } 657 } 658 659 created := time.Now() 660 if db.driverName == driverSQLite { 661 // SQLite stores datetimes in UTC, so convert time.Now() to it here 662 created = created.UTC() 663 } 664 if post.Created != nil { 665 created, err = time.Parse("2006-01-02T15:04:05Z", *post.Created) 666 if err != nil { 667 log.Error("Unable to parse Created time '%s': %v", *post.Created, err) 668 created = time.Now() 669 if db.driverName == driverSQLite { 670 // SQLite stores datetimes in UTC, so convert time.Now() to it here 671 created = created.UTC() 672 } 673 } 674 } 675 676 stmt, err := db.Prepare("INSERT INTO posts (id, slug, title, content, text_appearance, language, rtl, privacy, owner_id, collection_id, created, updated, view_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " + db.now() + ", ?)") 677 if err != nil { 678 return nil, err 679 } 680 defer stmt.Close() 681 _, err = stmt.Exec(friendlyID, slug, post.Title, post.Content, appearance, post.Language, post.IsRTL, 0, ownerID, ownerCollID, created, 0) 682 if err != nil { 683 if db.isDuplicateKeyErr(err) { 684 // Duplicate entry error; try a new slug 685 // TODO: make this a little more robust 686 slug = sql.NullString{id.GenSafeUniqueSlug(slug.String), true} 687 _, err = stmt.Exec(friendlyID, slug, post.Title, post.Content, appearance, post.Language, post.IsRTL, 0, ownerID, ownerCollID, created, 0) 688 if err != nil { 689 return nil, handleFailedPostInsert(fmt.Errorf("Retried slug generation, still failed: %v", err)) 690 } 691 } else { 692 return nil, handleFailedPostInsert(err) 693 } 694 } 695 696 // TODO: return Created field in proper format 697 return &Post{ 698 ID: friendlyID, 699 Slug: null.NewString(slug.String, slug.Valid), 700 Font: appearance, 701 Language: zero.NewString(post.Language.String, post.Language.Valid), 702 RTL: zero.NewBool(post.IsRTL.Bool, post.IsRTL.Valid), 703 OwnerID: null.NewInt(userID, true), 704 CollectionID: null.NewInt(userID, true), 705 Created: created.Truncate(time.Second).UTC(), 706 Updated: time.Now().Truncate(time.Second).UTC(), 707 Title: zero.NewString(*(post.Title), true), 708 Content: *(post.Content), 709 }, nil 710} 711 712// UpdateOwnedPost updates an existing post with only the given fields in the 713// supplied AuthenticatedPost. 714func (db *datastore) UpdateOwnedPost(post *AuthenticatedPost, userID int64) error { 715 params := []interface{}{} 716 var queryUpdates, sep, authCondition string 717 if post.Slug != nil && *post.Slug != "" { 718 queryUpdates += sep + "slug = ?" 719 sep = ", " 720 params = append(params, getSlug(*post.Slug, "")) 721 } 722 if post.Content != nil { 723 queryUpdates += sep + "content = ?" 724 sep = ", " 725 params = append(params, post.Content) 726 } 727 if post.Title != nil { 728 queryUpdates += sep + "title = ?" 729 sep = ", " 730 params = append(params, post.Title) 731 } 732 if post.Language.Valid { 733 queryUpdates += sep + "language = ?" 734 sep = ", " 735 params = append(params, post.Language.String) 736 } 737 if post.IsRTL.Valid { 738 queryUpdates += sep + "rtl = ?" 739 sep = ", " 740 params = append(params, post.IsRTL.Bool) 741 } 742 if post.Font != "" { 743 queryUpdates += sep + "text_appearance = ?" 744 sep = ", " 745 params = append(params, post.Font) 746 } 747 if post.Created != nil { 748 createTime, err := time.Parse(postMetaDateFormat, *post.Created) 749 if err != nil { 750 log.Error("Unable to parse Created date: %v", err) 751 return fmt.Errorf("That's the incorrect format for Created date.") 752 } 753 queryUpdates += sep + "created = ?" 754 sep = ", " 755 params = append(params, createTime) 756 } 757 758 // WHERE parameters... 759 // id = ? 760 params = append(params, post.ID) 761 // AND owner_id = ? 762 authCondition = "(owner_id = ?)" 763 params = append(params, userID) 764 765 if queryUpdates == "" { 766 return ErrPostNoUpdatableVals 767 } 768 769 queryUpdates += sep + "updated = " + db.now() 770 771 res, err := db.Exec("UPDATE posts SET "+queryUpdates+" WHERE id = ? AND "+authCondition, params...) 772 if err != nil { 773 log.Error("Unable to update owned post: %v", err) 774 return err 775 } 776 777 rowsAffected, _ := res.RowsAffected() 778 if rowsAffected == 0 { 779 // Show the correct error message if nothing was updated 780 var dummy int 781 err := db.QueryRow("SELECT 1 FROM posts WHERE id = ? AND "+authCondition, post.ID, params[len(params)-1]).Scan(&dummy) 782 switch { 783 case err == sql.ErrNoRows: 784 return ErrUnauthorizedEditPost 785 case err != nil: 786 log.Error("Failed selecting from posts: %v", err) 787 } 788 return nil 789 } 790 791 return nil 792} 793 794func (db *datastore) GetCollectionBy(condition string, value interface{}) (*Collection, error) { 795 c := &Collection{} 796 797 // FIXME: change Collection to reflect database values. Add helper functions to get actual values 798 var styleSheet, script, signature, format zero.String 799 row := db.QueryRow("SELECT id, alias, title, description, style_sheet, script, post_signature, format, owner_id, privacy, view_count FROM collections WHERE "+condition, value) 800 801 err := row.Scan(&c.ID, &c.Alias, &c.Title, &c.Description, &styleSheet, &script, &signature, &format, &c.OwnerID, &c.Visibility, &c.Views) 802 switch { 803 case err == sql.ErrNoRows: 804 return nil, impart.HTTPError{http.StatusNotFound, "Collection doesn't exist."} 805 case db.isHighLoadError(err): 806 return nil, ErrUnavailable 807 case err != nil: 808 log.Error("Failed selecting from collections: %v", err) 809 return nil, err 810 } 811 c.StyleSheet = styleSheet.String 812 c.Script = script.String 813 c.Signature = signature.String 814 c.Format = format.String 815 c.Public = c.IsPublic() 816 c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer") 817 818 c.db = db 819 820 return c, nil 821} 822 823func (db *datastore) GetCollection(alias string) (*Collection, error) { 824 return db.GetCollectionBy("alias = ?", alias) 825} 826 827func (db *datastore) GetCollectionForPad(alias string) (*Collection, error) { 828 c := &Collection{Alias: alias} 829 830 row := db.QueryRow("SELECT id, alias, title, description, privacy FROM collections WHERE alias = ?", alias) 831 832 err := row.Scan(&c.ID, &c.Alias, &c.Title, &c.Description, &c.Visibility) 833 switch { 834 case err == sql.ErrNoRows: 835 return c, impart.HTTPError{http.StatusNotFound, "Collection doesn't exist."} 836 case err != nil: 837 log.Error("Failed selecting from collections: %v", err) 838 return c, ErrInternalGeneral 839 } 840 c.Public = c.IsPublic() 841 842 return c, nil 843} 844 845func (db *datastore) GetCollectionByID(id int64) (*Collection, error) { 846 return db.GetCollectionBy("id = ?", id) 847} 848 849func (db *datastore) GetCollectionFromDomain(host string) (*Collection, error) { 850 return db.GetCollectionBy("host = ?", host) 851} 852 853func (db *datastore) UpdateCollection(c *SubmittedCollection, alias string) error { 854 q := query.NewUpdate(). 855 SetStringPtr(c.Title, "title"). 856 SetStringPtr(c.Description, "description"). 857 SetNullString(c.StyleSheet, "style_sheet"). 858 SetNullString(c.Script, "script"). 859 SetNullString(c.Signature, "post_signature") 860 861 if c.Format != nil { 862 cf := &CollectionFormat{Format: c.Format.String} 863 if cf.Valid() { 864 q.SetNullString(c.Format, "format") 865 } 866 } 867 868 var updatePass bool 869 if c.Visibility != nil && (collVisibility(*c.Visibility)&CollProtected == 0 || c.Pass != "") { 870 q.SetIntPtr(c.Visibility, "privacy") 871 if c.Pass != "" { 872 updatePass = true 873 } 874 } 875 876 // WHERE values 877 q.Where("alias = ? AND owner_id = ?", alias, c.OwnerID) 878 879 if q.Updates == "" && c.Monetization == nil { 880 return ErrPostNoUpdatableVals 881 } 882 883 // Find any current domain 884 var collID int64 885 var rowsAffected int64 886 var changed bool 887 var res sql.Result 888 err := db.QueryRow("SELECT id FROM collections WHERE alias = ?", alias).Scan(&collID) 889 if err != nil { 890 log.Error("Failed selecting from collections: %v. Some things won't work.", err) 891 } 892 893 // Update MathJax value 894 if c.MathJax { 895 if db.driverName == driverSQLite { 896 _, err = db.Exec("INSERT OR REPLACE INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?)", collID, "render_mathjax", "1") 897 } else { 898 _, err = db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?) "+db.upsert("collection_id", "attribute")+" value = ?", collID, "render_mathjax", "1", "1") 899 } 900 if err != nil { 901 log.Error("Unable to insert render_mathjax value: %v", err) 902 return err 903 } 904 } else { 905 _, err = db.Exec("DELETE FROM collectionattributes WHERE collection_id = ? AND attribute = ?", collID, "render_mathjax") 906 if err != nil { 907 log.Error("Unable to delete render_mathjax value: %v", err) 908 return err 909 } 910 } 911 912 // Update Monetization value 913 if c.Monetization != nil { 914 skipUpdate := false 915 if *c.Monetization != "" { 916 // Strip away any excess spaces 917 trimmed := strings.TrimSpace(*c.Monetization) 918 // Only update value when it starts with "$", per spec: https://paymentpointers.org 919 if strings.HasPrefix(trimmed, "$") { 920 c.Monetization = &trimmed 921 } else { 922 // Value appears invalid, so don't update 923 skipUpdate = true 924 } 925 } 926 if !skipUpdate { 927 _, err = db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE value = ?", collID, "monetization_pointer", *c.Monetization, *c.Monetization) 928 if err != nil { 929 log.Error("Unable to insert monetization_pointer value: %v", err) 930 return err 931 } 932 } 933 } 934 935 // Update rest of the collection data 936 if q.Updates != "" { 937 res, err = db.Exec("UPDATE collections SET "+q.Updates+" WHERE "+q.Conditions, q.Params...) 938 if err != nil { 939 log.Error("Unable to update collection: %v", err) 940 return err 941 } 942 } 943 944 rowsAffected, _ = res.RowsAffected() 945 if !changed || rowsAffected == 0 { 946 // Show the correct error message if nothing was updated 947 var dummy int 948 err := db.QueryRow("SELECT 1 FROM collections WHERE alias = ? AND owner_id = ?", alias, c.OwnerID).Scan(&dummy) 949 switch { 950 case err == sql.ErrNoRows: 951 return ErrUnauthorizedEditPost 952 case err != nil: 953 log.Error("Failed selecting from collections: %v", err) 954 } 955 if !updatePass { 956 return nil 957 } 958 } 959 960 if updatePass { 961 hashedPass, err := auth.HashPass([]byte(c.Pass)) 962 if err != nil { 963 log.Error("Unable to create hash: %s", err) 964 return impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."} 965 } 966 if db.driverName == driverSQLite { 967 _, err = db.Exec("INSERT OR REPLACE INTO collectionpasswords (collection_id, password) VALUES ((SELECT id FROM collections WHERE alias = ?), ?)", alias, hashedPass) 968 } else { 969 _, err = db.Exec("INSERT INTO collectionpasswords (collection_id, password) VALUES ((SELECT id FROM collections WHERE alias = ?), ?) "+db.upsert("collection_id")+" password = ?", alias, hashedPass, hashedPass) 970 } 971 if err != nil { 972 return err 973 } 974 } 975 976 return nil 977} 978 979const postCols = "id, slug, text_appearance, language, rtl, privacy, owner_id, collection_id, pinned_position, created, updated, view_count, title, content" 980 981// getEditablePost returns a PublicPost with the given ID only if the given 982// edit token is valid for the post. 983func (db *datastore) GetEditablePost(id, editToken string) (*PublicPost, error) { 984 // FIXME: code duplicated from getPost() 985 // TODO: add slight logic difference to getPost / one func 986 var ownerName sql.NullString 987 p := &Post{} 988 989 row := db.QueryRow("SELECT "+postCols+", (SELECT username FROM users WHERE users.id = posts.owner_id) AS username FROM posts WHERE id = ? LIMIT 1", id) 990 err := row.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content, &ownerName) 991 switch { 992 case err == sql.ErrNoRows: 993 return nil, ErrPostNotFound 994 case err != nil: 995 log.Error("Failed selecting from collections: %v", err) 996 return nil, err 997 } 998 999 if p.Content == "" && p.Title.String == "" { 1000 return nil, ErrPostUnpublished 1001 } 1002 1003 res := p.processPost() 1004 if ownerName.Valid { 1005 res.Owner = &PublicUser{Username: ownerName.String} 1006 } 1007 1008 return &res, nil 1009} 1010 1011func (db *datastore) PostIDExists(id string) bool { 1012 var dummy bool 1013 err := db.QueryRow("SELECT 1 FROM posts WHERE id = ?", id).Scan(&dummy) 1014 return err == nil && dummy 1015} 1016 1017// GetPost gets a public-facing post object from the database. If collectionID 1018// is > 0, the post will be retrieved by slug and collection ID, rather than 1019// post ID. 1020// TODO: break this into two functions: 1021// - GetPost(id string) 1022// - GetCollectionPost(slug string, collectionID int64) 1023func (db *datastore) GetPost(id string, collectionID int64) (*PublicPost, error) { 1024 var ownerName sql.NullString 1025 p := &Post{} 1026 1027 var row *sql.Row 1028 var where string 1029 params := []interface{}{id} 1030 if collectionID > 0 { 1031 where = "slug = ? AND collection_id = ?" 1032 params = append(params, collectionID) 1033 } else { 1034 where = "id = ?" 1035 } 1036 row = db.QueryRow("SELECT "+postCols+", (SELECT username FROM users WHERE users.id = posts.owner_id) AS username FROM posts WHERE "+where+" LIMIT 1", params...) 1037 err := row.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content, &ownerName) 1038 switch { 1039 case err == sql.ErrNoRows: 1040 if collectionID > 0 { 1041 return nil, ErrCollectionPageNotFound 1042 } 1043 return nil, ErrPostNotFound 1044 case err != nil: 1045 log.Error("Failed selecting from collections: %v", err) 1046 return nil, err 1047 } 1048 1049 if p.Content == "" && p.Title.String == "" { 1050 return nil, ErrPostUnpublished 1051 } 1052 1053 res := p.processPost() 1054 if ownerName.Valid { 1055 res.Owner = &PublicUser{Username: ownerName.String} 1056 } 1057 1058 return &res, nil 1059} 1060 1061// TODO: don't duplicate getPost() functionality 1062func (db *datastore) GetOwnedPost(id string, ownerID int64) (*PublicPost, error) { 1063 p := &Post{} 1064 1065 var row *sql.Row 1066 where := "id = ? AND owner_id = ?" 1067 params := []interface{}{id, ownerID} 1068 row = db.QueryRow("SELECT "+postCols+" FROM posts WHERE "+where+" LIMIT 1", params...) 1069 err := row.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content) 1070 switch { 1071 case err == sql.ErrNoRows: 1072 return nil, ErrPostNotFound 1073 case err != nil: 1074 log.Error("Failed selecting from collections: %v", err) 1075 return nil, err 1076 } 1077 1078 if p.Content == "" && p.Title.String == "" { 1079 return nil, ErrPostUnpublished 1080 } 1081 1082 res := p.processPost() 1083 1084 return &res, nil 1085} 1086 1087func (db *datastore) GetPostProperty(id string, collectionID int64, property string) (interface{}, error) { 1088 propSelects := map[string]string{ 1089 "views": "view_count AS views", 1090 } 1091 selectQuery, ok := propSelects[property] 1092 if !ok { 1093 return nil, impart.HTTPError{http.StatusBadRequest, fmt.Sprintf("Invalid property: %s.", property)} 1094 } 1095 1096 var res interface{} 1097 var row *sql.Row 1098 if collectionID != 0 { 1099 row = db.QueryRow("SELECT "+selectQuery+" FROM posts WHERE slug = ? AND collection_id = ? LIMIT 1", id, collectionID) 1100 } else { 1101 row = db.QueryRow("SELECT "+selectQuery+" FROM posts WHERE id = ? LIMIT 1", id) 1102 } 1103 err := row.Scan(&res) 1104 switch { 1105 case err == sql.ErrNoRows: 1106 return nil, impart.HTTPError{http.StatusNotFound, "Post not found."} 1107 case err != nil: 1108 log.Error("Failed selecting post: %v", err) 1109 return nil, err 1110 } 1111 1112 return res, nil 1113} 1114 1115// GetPostsCount modifies the CollectionObj to include the correct number of 1116// standard (non-pinned) posts. It will return future posts if `includeFuture` 1117// is true. 1118func (db *datastore) GetPostsCount(c *CollectionObj, includeFuture bool) { 1119 var count int64 1120 timeCondition := "" 1121 if !includeFuture { 1122 timeCondition = "AND created <= " + db.now() 1123 } 1124 err := db.QueryRow("SELECT COUNT(*) FROM posts WHERE collection_id = ? AND pinned_position IS NULL "+timeCondition, c.ID).Scan(&count) 1125 switch { 1126 case err == sql.ErrNoRows: 1127 c.TotalPosts = 0 1128 case err != nil: 1129 log.Error("Failed selecting from collections: %v", err) 1130 c.TotalPosts = 0 1131 } 1132 1133 c.TotalPosts = int(count) 1134} 1135 1136// GetPosts retrieves all posts for the given Collection. 1137// It will return future posts if `includeFuture` is true. 1138// It will include only standard (non-pinned) posts unless `includePinned` is true. 1139// TODO: change includeFuture to isOwner, since that's how it's used 1140func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool) (*[]PublicPost, error) { 1141 collID := c.ID 1142 1143 cf := c.NewFormat() 1144 order := "DESC" 1145 if cf.Ascending() && !forceRecentFirst { 1146 order = "ASC" 1147 } 1148 1149 pagePosts := cf.PostsPerPage() 1150 start := page*pagePosts - pagePosts 1151 if page == 0 { 1152 start = 0 1153 pagePosts = 1000 1154 } 1155 1156 limitStr := "" 1157 if page > 0 { 1158 limitStr = fmt.Sprintf(" LIMIT %d, %d", start, pagePosts) 1159 } 1160 timeCondition := "" 1161 if !includeFuture { 1162 timeCondition = "AND created <= " + db.now() 1163 } 1164 pinnedCondition := "" 1165 if !includePinned { 1166 pinnedCondition = "AND pinned_position IS NULL" 1167 } 1168 rows, err := db.Query("SELECT "+postCols+" FROM posts WHERE collection_id = ? "+pinnedCondition+" "+timeCondition+" ORDER BY created "+order+limitStr, collID) 1169 if err != nil { 1170 log.Error("Failed selecting from posts: %v", err) 1171 return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve collection posts."} 1172 } 1173 defer rows.Close() 1174 1175 // TODO: extract this common row scanning logic for queries using `postCols` 1176 posts := []PublicPost{} 1177 for rows.Next() { 1178 p := &Post{} 1179 err = rows.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content) 1180 if err != nil { 1181 log.Error("Failed scanning row: %v", err) 1182 break 1183 } 1184 p.extractData() 1185 p.augmentContent(c) 1186 p.formatContent(cfg, c, includeFuture, false) 1187 1188 posts = append(posts, p.processPost()) 1189 } 1190 err = rows.Err() 1191 if err != nil { 1192 log.Error("Error after Next() on rows: %v", err) 1193 } 1194 1195 return &posts, nil 1196} 1197 1198// GetPostsTagged retrieves all posts on the given Collection that contain the 1199// given tag. 1200// It will return future posts if `includeFuture` is true. 1201// TODO: change includeFuture to isOwner, since that's how it's used 1202func (db *datastore) GetPostsTagged(cfg *config.Config, c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error) { 1203 collID := c.ID 1204 1205 cf := c.NewFormat() 1206 order := "DESC" 1207 if cf.Ascending() { 1208 order = "ASC" 1209 } 1210 1211 pagePosts := cf.PostsPerPage() 1212 start := page*pagePosts - pagePosts 1213 if page == 0 { 1214 start = 0 1215 pagePosts = 1000 1216 } 1217 1218 limitStr := "" 1219 if page > 0 { 1220 limitStr = fmt.Sprintf(" LIMIT %d, %d", start, pagePosts) 1221 } 1222 timeCondition := "" 1223 if !includeFuture { 1224 timeCondition = "AND created <= " + db.now() 1225 } 1226 1227 var rows *sql.Rows 1228 var err error 1229 if db.driverName == driverSQLite { 1230 rows, err = db.Query("SELECT "+postCols+" FROM posts WHERE collection_id = ? AND LOWER(content) regexp ? "+timeCondition+" ORDER BY created "+order+limitStr, collID, `.*#`+strings.ToLower(tag)+`\b.*`) 1231 } else { 1232 rows, err = db.Query("SELECT "+postCols+" FROM posts WHERE collection_id = ? AND LOWER(content) RLIKE ? "+timeCondition+" ORDER BY created "+order+limitStr, collID, "#"+strings.ToLower(tag)+"[[:>:]]") 1233 } 1234 if err != nil { 1235 log.Error("Failed selecting from posts: %v", err) 1236 return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve collection posts."} 1237 } 1238 defer rows.Close() 1239 1240 // TODO: extract this common row scanning logic for queries using `postCols` 1241 posts := []PublicPost{} 1242 for rows.Next() { 1243 p := &Post{} 1244 err = rows.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content) 1245 if err != nil { 1246 log.Error("Failed scanning row: %v", err) 1247 break 1248 } 1249 p.extractData() 1250 p.augmentContent(c) 1251 p.formatContent(cfg, c, includeFuture, false) 1252 1253 posts = append(posts, p.processPost()) 1254 } 1255 err = rows.Err() 1256 if err != nil { 1257 log.Error("Error after Next() on rows: %v", err) 1258 } 1259 1260 return &posts, nil 1261} 1262 1263func (db *datastore) GetAPFollowers(c *Collection) (*[]RemoteUser, error) { 1264 rows, err := db.Query("SELECT actor_id, inbox, shared_inbox FROM remotefollows f INNER JOIN remoteusers u ON f.remote_user_id = u.id WHERE collection_id = ?", c.ID) 1265 if err != nil { 1266 log.Error("Failed selecting from followers: %v", err) 1267 return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve followers."} 1268 } 1269 defer rows.Close() 1270 1271 followers := []RemoteUser{} 1272 for rows.Next() { 1273 f := RemoteUser{} 1274 err = rows.Scan(&f.ActorID, &f.Inbox, &f.SharedInbox) 1275 followers = append(followers, f) 1276 } 1277 return &followers, nil 1278} 1279 1280// CanCollect returns whether or not the given user can add the given post to a 1281// collection. This is true when a post is already owned by the user. 1282// NOTE: this is currently only used to potentially add owned posts to a 1283// collection. This has the SIDE EFFECT of also generating a slug for the post. 1284// FIXME: make this side effect more explicit (or extract it) 1285func (db *datastore) CanCollect(cpr *ClaimPostRequest, userID int64) bool { 1286 var title, content string 1287 var lang sql.NullString 1288 err := db.QueryRow("SELECT title, content, language FROM posts WHERE id = ? AND owner_id = ?", cpr.ID, userID).Scan(&title, &content, &lang) 1289 switch { 1290 case err == sql.ErrNoRows: 1291 return false 1292 case err != nil: 1293 log.Error("Failed on post CanCollect(%s, %d): %v", cpr.ID, userID, err) 1294 return false 1295 } 1296 1297 // Since we have the post content and the post is collectable, generate the 1298 // post's slug now. 1299 cpr.Slug = getSlugFromPost(title, content, lang.String) 1300 1301 return true 1302} 1303 1304func (db *datastore) AttemptClaim(p *ClaimPostRequest, query string, params []interface{}, slugIdx int) (sql.Result, error) { 1305 qRes, err := db.Exec(query, params...) 1306 if err != nil { 1307 if db.isDuplicateKeyErr(err) && slugIdx > -1 { 1308 s := id.GenSafeUniqueSlug(p.Slug) 1309 if s == p.Slug { 1310 // Sanity check to prevent infinite recursion 1311 return qRes, fmt.Errorf("GenSafeUniqueSlug generated nothing unique: %s", s) 1312 } 1313 p.Slug = s 1314 params[slugIdx] = p.Slug 1315 return db.AttemptClaim(p, query, params, slugIdx) 1316 } 1317 return qRes, fmt.Errorf("attemptClaim: %s", err) 1318 } 1319 return qRes, nil 1320} 1321 1322func (db *datastore) DispersePosts(userID int64, postIDs []string) (*[]ClaimPostResult, error) { 1323 postClaimReqs := map[string]bool{} 1324 res := []ClaimPostResult{} 1325 for i := range postIDs { 1326 postID := postIDs[i] 1327 1328 r := ClaimPostResult{Code: 0, ErrorMessage: ""} 1329 1330 // Perform post validation 1331 if postID == "" { 1332 r.ErrorMessage = "Missing post ID. " 1333 } 1334 if _, ok := postClaimReqs[postID]; ok { 1335 r.Code = 429 1336 r.ErrorMessage = "You've already tried anonymizing this post." 1337 r.ID = postID 1338 res = append(res, r) 1339 continue 1340 } 1341 postClaimReqs[postID] = true 1342 1343 var err error 1344 // Get full post information to return 1345 var fullPost *PublicPost 1346 fullPost, err = db.GetPost(postID, 0) 1347 if err != nil { 1348 if err, ok := err.(impart.HTTPError); ok { 1349 r.Code = err.Status 1350 r.ErrorMessage = err.Message 1351 r.ID = postID 1352 res = append(res, r) 1353 continue 1354 } else { 1355 log.Error("Error getting post in dispersePosts: %v", err) 1356 } 1357 } 1358 if fullPost.OwnerID.Int64 != userID { 1359 r.Code = http.StatusConflict 1360 r.ErrorMessage = "Post is already owned by someone else." 1361 r.ID = postID 1362 res = append(res, r) 1363 continue 1364 } 1365 1366 var qRes sql.Result 1367 var query string 1368 var params []interface{} 1369 // Do AND owner_id = ? for sanity. 1370 // This should've been caught and returned with a good error message 1371 // just above. 1372 query = "UPDATE posts SET collection_id = NULL WHERE id = ? AND owner_id = ?" 1373 params = []interface{}{postID, userID} 1374 qRes, err = db.Exec(query, params...) 1375 if err != nil { 1376 r.Code = http.StatusInternalServerError 1377 r.ErrorMessage = "A glitch happened on our end." 1378 r.ID = postID 1379 res = append(res, r) 1380 log.Error("dispersePosts (post %s): %v", postID, err) 1381 continue 1382 } 1383 1384 // Post was successfully dispersed 1385 r.Code = http.StatusOK 1386 r.Post = fullPost 1387 1388 rowsAffected, _ := qRes.RowsAffected() 1389 if rowsAffected == 0 { 1390 // This was already claimed, but return 200 1391 r.Code = http.StatusOK 1392 } 1393 res = append(res, r) 1394 } 1395 1396 return &res, nil 1397} 1398 1399func (db *datastore) ClaimPosts(cfg *config.Config, userID int64, collAlias string, posts *[]ClaimPostRequest) (*[]ClaimPostResult, error) { 1400 postClaimReqs := map[string]bool{} 1401 res := []ClaimPostResult{} 1402 postCollAlias := collAlias 1403 for i := range *posts { 1404 p := (*posts)[i] 1405 if &p == nil { 1406 continue 1407 } 1408 1409 r := ClaimPostResult{Code: 0, ErrorMessage: ""} 1410 1411 // Perform post validation 1412 if p.ID == "" { 1413 r.ErrorMessage = "Missing post ID `id`. " 1414 } 1415 if _, ok := postClaimReqs[p.ID]; ok { 1416 r.Code = 429 1417 r.ErrorMessage = "You've already tried claiming this post." 1418 r.ID = p.ID 1419 res = append(res, r) 1420 continue 1421 } 1422 postClaimReqs[p.ID] = true 1423 1424 canCollect := db.CanCollect(&p, userID) 1425 if !canCollect && p.Token == "" { 1426 // TODO: ensure post isn't owned by anyone else when a valid modify 1427 // token is given. 1428 r.ErrorMessage += "Missing post Edit Token `token`." 1429 } 1430 if r.ErrorMessage != "" { 1431 // Post validate failed 1432 r.Code = http.StatusBadRequest 1433 r.ID = p.ID 1434 res = append(res, r) 1435 continue 1436 } 1437 1438 var err error 1439 var qRes sql.Result 1440 var query string 1441 var params []interface{} 1442 var slugIdx int = -1 1443 var coll *Collection 1444 if collAlias == "" { 1445 // Posts are being claimed at /posts/claim, not 1446 // /collections/{alias}/collect, so use given individual collection 1447 // to associate post with. 1448 postCollAlias = p.CollectionAlias 1449 } 1450 if postCollAlias != "" { 1451 // Associate this post with a collection 1452 if p.CreateCollection { 1453 // This is a new collection 1454 // TODO: consider removing this. This seriously complicates this 1455 // method and adds another (unnecessary?) logic path. 1456 coll, err = db.CreateCollection(cfg, postCollAlias, "", userID) 1457 if err != nil { 1458 if err, ok := err.(impart.HTTPError); ok { 1459 r.Code = err.Status 1460 r.ErrorMessage = err.Message 1461 } else { 1462 r.Code = http.StatusInternalServerError 1463 r.ErrorMessage = "Unknown error occurred creating collection" 1464 } 1465 r.ID = p.ID 1466 res = append(res, r) 1467 continue 1468 } 1469 } else { 1470 // Attempt to add to existing collection 1471 coll, err = db.GetCollection(postCollAlias) 1472 if err != nil { 1473 if err, ok := err.(impart.HTTPError); ok { 1474 if err.Status == http.StatusNotFound { 1475 // Show obfuscated "forbidden" response, as if attempting to add to an 1476 // unowned blog. 1477 r.Code = ErrForbiddenCollection.Status 1478 r.ErrorMessage = ErrForbiddenCollection.Message 1479 } else { 1480 r.Code = err.Status 1481 r.ErrorMessage = err.Message 1482 } 1483 } else { 1484 r.Code = http.StatusInternalServerError 1485 r.ErrorMessage = "Unknown error occurred claiming post with collection" 1486 } 1487 r.ID = p.ID 1488 res = append(res, r) 1489 continue 1490 } 1491 if coll.OwnerID != userID { 1492 r.Code = ErrForbiddenCollection.Status 1493 r.ErrorMessage = ErrForbiddenCollection.Message 1494 r.ID = p.ID 1495 res = append(res, r) 1496 continue 1497 } 1498 } 1499 if p.Slug == "" { 1500 p.Slug = p.ID 1501 } 1502 if canCollect { 1503 // User already owns this post, so just add it to the given 1504 // collection. 1505 query = "UPDATE posts SET collection_id = ?, slug = ? WHERE id = ? AND owner_id = ?" 1506 params = []interface{}{coll.ID, p.Slug, p.ID, userID} 1507 slugIdx = 1 1508 } else { 1509 query = "UPDATE posts SET owner_id = ?, collection_id = ?, slug = ? WHERE id = ? AND modify_token = ? AND owner_id IS NULL" 1510 params = []interface{}{userID, coll.ID, p.Slug, p.ID, p.Token} 1511 slugIdx = 2 1512 } 1513 } else { 1514 query = "UPDATE posts SET owner_id = ? WHERE id = ? AND modify_token = ? AND owner_id IS NULL" 1515 params = []interface{}{userID, p.ID, p.Token} 1516 } 1517 qRes, err = db.AttemptClaim(&p, query, params, slugIdx) 1518 if err != nil { 1519 r.Code = http.StatusInternalServerError 1520 r.ErrorMessage = "An unknown error occurred." 1521 r.ID = p.ID 1522 res = append(res, r) 1523 log.Error("claimPosts (post %s): %v", p.ID, err) 1524 continue 1525 } 1526 1527 // Get full post information to return 1528 var fullPost *PublicPost 1529 if p.Token != "" { 1530 fullPost, err = db.GetEditablePost(p.ID, p.Token) 1531 } else { 1532 fullPost, err = db.GetPost(p.ID, 0) 1533 } 1534 if err != nil { 1535 if err, ok := err.(impart.HTTPError); ok { 1536 r.Code = err.Status 1537 r.ErrorMessage = err.Message 1538 r.ID = p.ID 1539 res = append(res, r) 1540 continue 1541 } 1542 } 1543 if fullPost.OwnerID.Int64 != userID { 1544 r.Code = http.StatusConflict 1545 r.ErrorMessage = "Post is already owned by someone else." 1546 r.ID = p.ID 1547 res = append(res, r) 1548 continue 1549 } 1550 1551 // Post was successfully claimed 1552 r.Code = http.StatusOK 1553 r.Post = fullPost 1554 if coll != nil { 1555 r.Post.Collection = &CollectionObj{Collection: *coll} 1556 } 1557 1558 rowsAffected, _ := qRes.RowsAffected() 1559 if rowsAffected == 0 { 1560 // This was already claimed, but return 200 1561 r.Code = http.StatusOK 1562 } 1563 res = append(res, r) 1564 } 1565 1566 return &res, nil 1567} 1568 1569func (db *datastore) UpdatePostPinState(pinned bool, postID string, collID, ownerID, pos int64) error { 1570 if pos <= 0 || pos > 20 { 1571 pos = db.GetLastPinnedPostPos(collID) + 1 1572 if pos == -1 { 1573 pos = 1 1574 } 1575 } 1576 var err error 1577 if pinned { 1578 _, err = db.Exec("UPDATE posts SET pinned_position = ? WHERE id = ?", pos, postID) 1579 } else { 1580 _, err = db.Exec("UPDATE posts SET pinned_position = NULL WHERE id = ?", postID) 1581 } 1582 if err != nil { 1583 log.Error("Unable to update pinned post: %v", err) 1584 return err 1585 } 1586 return nil 1587} 1588 1589func (db *datastore) GetLastPinnedPostPos(collID int64) int64 { 1590 var lastPos sql.NullInt64 1591 err := db.QueryRow("SELECT MAX(pinned_position) FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL", collID).Scan(&lastPos) 1592 switch { 1593 case err == sql.ErrNoRows: 1594 return -1 1595 case err != nil: 1596 log.Error("Failed selecting from posts: %v", err) 1597 return -1 1598 } 1599 if !lastPos.Valid { 1600 return -1 1601 } 1602 return lastPos.Int64 1603} 1604 1605func (db *datastore) GetPinnedPosts(coll *CollectionObj, includeFuture bool) (*[]PublicPost, error) { 1606 // FIXME: sqlite-backed instances don't include ellipsis on truncated titles 1607 timeCondition := "" 1608 if !includeFuture { 1609 timeCondition = "AND created <= " + db.now() 1610 } 1611 rows, err := db.Query("SELECT id, slug, title, "+db.clip("content", 80)+", pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL "+timeCondition+" ORDER BY pinned_position ASC", coll.ID) 1612 if err != nil { 1613 log.Error("Failed selecting pinned posts: %v", err) 1614 return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve pinned posts."} 1615 } 1616 defer rows.Close() 1617 1618 posts := []PublicPost{} 1619 for rows.Next() { 1620 p := &Post{} 1621 err = rows.Scan(&p.ID, &p.Slug, &p.Title, &p.Content, &p.PinnedPosition) 1622 if err != nil { 1623 log.Error("Failed scanning row: %v", err) 1624 break 1625 } 1626 p.extractData() 1627 p.augmentContent(&coll.Collection) 1628 1629 pp := p.processPost() 1630 pp.Collection = coll 1631 posts = append(posts, pp) 1632 } 1633 return &posts, nil 1634} 1635 1636func (db *datastore) GetCollections(u *User, hostName string) (*[]Collection, error) { 1637 rows, err := db.Query("SELECT id, alias, title, description, privacy, view_count FROM collections WHERE owner_id = ? ORDER BY id ASC", u.ID) 1638 if err != nil { 1639 log.Error("Failed selecting from collections: %v", err) 1640 return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user collections."} 1641 } 1642 defer rows.Close() 1643 1644 colls := []Collection{} 1645 for rows.Next() { 1646 c := Collection{} 1647 err = rows.Scan(&c.ID, &c.Alias, &c.Title, &c.Description, &c.Visibility, &c.Views) 1648 if err != nil { 1649 log.Error("Failed scanning row: %v", err) 1650 break 1651 } 1652 c.hostName = hostName 1653 c.URL = c.CanonicalURL() 1654 c.Public = c.IsPublic() 1655 1656 /* 1657 // NOTE: future functionality 1658 if visibility != nil { // TODO: && visibility == CollPublic { 1659 // Add Monetization info when retrieving all public collections 1660 c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer") 1661 } 1662 */ 1663 1664 colls = append(colls, c) 1665 } 1666 err = rows.Err() 1667 if err != nil { 1668 log.Error("Error after Next() on rows: %v", err) 1669 } 1670 1671 return &colls, nil 1672} 1673 1674func (db *datastore) GetPublishableCollections(u *User, hostName string) (*[]Collection, error) { 1675 c, err := db.GetCollections(u, hostName) 1676 if err != nil { 1677 return nil, err 1678 } 1679 1680 if len(*c) == 0 { 1681 return nil, impart.HTTPError{http.StatusInternalServerError, "You don't seem to have any blogs; they might've moved to another account. Try logging out and logging into your other account."} 1682 } 1683 return c, nil 1684} 1685 1686func (db *datastore) GetPublicCollections(hostName string) (*[]Collection, error) { 1687 rows, err := db.Query(`SELECT c.id, alias, title, description, privacy, view_count 1688 FROM collections c 1689 LEFT JOIN users u ON u.id = c.owner_id 1690 WHERE c.privacy = 1 AND u.status = 0 1691 ORDER BY id ASC`) 1692 if err != nil { 1693 log.Error("Failed selecting public collections: %v", err) 1694 return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve public collections."} 1695 } 1696 defer rows.Close() 1697 1698 colls := []Collection{} 1699 for rows.Next() { 1700 c := Collection{} 1701 err = rows.Scan(&c.ID, &c.Alias, &c.Title, &c.Description, &c.Visibility, &c.Views) 1702 if err != nil { 1703 log.Error("Failed scanning row: %v", err) 1704 break 1705 } 1706 c.hostName = hostName 1707 c.URL = c.CanonicalURL() 1708 c.Public = c.IsPublic() 1709 1710 // Add Monetization information 1711 c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer") 1712 1713 colls = append(colls, c) 1714 } 1715 err = rows.Err() 1716 if err != nil { 1717 log.Error("Error after Next() on rows: %v", err) 1718 } 1719 1720 return &colls, nil 1721} 1722 1723func (db *datastore) GetMeStats(u *User) userMeStats { 1724 s := userMeStats{} 1725 1726 // User counts 1727 colls, _ := db.GetUserCollectionCount(u.ID) 1728 s.TotalCollections = colls 1729 1730 var articles, collPosts uint64 1731 err := db.QueryRow("SELECT COUNT(*) FROM posts WHERE owner_id = ? AND collection_id IS NULL", u.ID).Scan(&articles) 1732 if err != nil && err != sql.ErrNoRows { 1733 log.Error("Couldn't get articles count for user %d: %v", u.ID, err) 1734 } 1735 s.TotalArticles = articles 1736 1737 err = db.QueryRow("SELECT COUNT(*) FROM posts WHERE owner_id = ? AND collection_id IS NOT NULL", u.ID).Scan(&collPosts) 1738 if err != nil && err != sql.ErrNoRows { 1739 log.Error("Couldn't get coll posts count for user %d: %v", u.ID, err) 1740 } 1741 s.CollectionPosts = collPosts 1742 1743 return s 1744} 1745 1746func (db *datastore) GetTotalCollections() (collCount int64, err error) { 1747 err = db.QueryRow(` 1748 SELECT COUNT(*) 1749 FROM collections c 1750 LEFT JOIN users u ON u.id = c.owner_id 1751 WHERE u.status = 0`).Scan(&collCount) 1752 if err != nil { 1753 log.Error("Unable to fetch collections count: %v", err) 1754 } 1755 return 1756} 1757 1758func (db *datastore) GetTotalPosts() (postCount int64, err error) { 1759 err = db.QueryRow(` 1760 SELECT COUNT(*) 1761 FROM posts p 1762 LEFT JOIN users u ON u.id = p.owner_id 1763 WHERE u.status = 0`).Scan(&postCount) 1764 if err != nil { 1765 log.Error("Unable to fetch posts count: %v", err) 1766 } 1767 return 1768} 1769 1770func (db *datastore) GetTopPosts(u *User, alias string, hostName string) (*[]PublicPost, error) { 1771 params := []interface{}{u.ID} 1772 where := "" 1773 if alias != "" { 1774 where = " AND alias = ?" 1775 params = append(params, alias) 1776 } 1777 rows, err := db.Query("SELECT p.id, p.slug, p.view_count, p.title, c.alias, c.title, c.description, c.view_count FROM posts p LEFT JOIN collections c ON p.collection_id = c.id WHERE p.owner_id = ?"+where+" ORDER BY p.view_count DESC, created DESC LIMIT 25", params...) 1778 if err != nil { 1779 log.Error("Failed selecting from posts: %v", err) 1780 return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user top posts."} 1781 } 1782 defer rows.Close() 1783 1784 posts := []PublicPost{} 1785 var gotErr bool 1786 for rows.Next() { 1787 p := Post{} 1788 c := Collection{} 1789 var alias, title, description sql.NullString 1790 var views sql.NullInt64 1791 err = rows.Scan(&p.ID, &p.Slug, &p.ViewCount, &p.Title, &alias, &title, &description, &views) 1792 if err != nil { 1793 log.Error("Failed scanning User.getPosts() row: %v", err) 1794 gotErr = true 1795 break 1796 } 1797 p.extractData() 1798 pubPost := p.processPost() 1799 1800 if alias.Valid && alias.String != "" { 1801 c.Alias = alias.String 1802 c.Title = title.String 1803 c.Description = description.String 1804 c.Views = views.Int64 1805 c.hostName = hostName 1806 pubPost.Collection = &CollectionObj{Collection: c} 1807 } 1808 1809 posts = append(posts, pubPost) 1810 } 1811 err = rows.Err() 1812 if err != nil { 1813 log.Error("Error after Next() on rows: %v", err) 1814 } 1815 1816 if gotErr && len(posts) == 0 { 1817 // There were a lot of errors 1818 return nil, impart.HTTPError{http.StatusInternalServerError, "Unable to get data."} 1819 } 1820 1821 return &posts, nil 1822} 1823 1824func (db *datastore) GetAnonymousPosts(u *User, page int) (*[]PublicPost, error) { 1825 pagePosts := 10 1826 start := page*pagePosts - pagePosts 1827 if page == 0 { 1828 start = 0 1829 pagePosts = 1000 1830 } 1831 1832 limitStr := "" 1833 if page > 0 { 1834 limitStr = fmt.Sprintf(" LIMIT %d, %d", start, pagePosts) 1835 } 1836 rows, err := db.Query("SELECT id, view_count, title, created, updated, content FROM posts WHERE owner_id = ? AND collection_id IS NULL ORDER BY created DESC"+limitStr, u.ID) 1837 if err != nil { 1838 log.Error("Failed selecting from posts: %v", err) 1839 return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user anonymous posts."} 1840 } 1841 defer rows.Close() 1842 1843 posts := []PublicPost{} 1844 for rows.Next() { 1845 p := Post{} 1846 err = rows.Scan(&p.ID, &p.ViewCount, &p.Title, &p.Created, &p.Updated, &p.Content) 1847 if err != nil { 1848 log.Error("Failed scanning row: %v", err) 1849 break 1850 } 1851 p.extractData() 1852 1853 posts = append(posts, p.processPost()) 1854 } 1855 err = rows.Err() 1856 if err != nil { 1857 log.Error("Error after Next() on rows: %v", err) 1858 } 1859 1860 return &posts, nil 1861} 1862 1863func (db *datastore) GetUserPosts(u *User) (*[]PublicPost, error) { 1864 rows, err := db.Query("SELECT p.id, p.slug, p.view_count, p.title, p.created, p.updated, p.content, p.text_appearance, p.language, p.rtl, c.alias, c.title, c.description, c.view_count FROM posts p LEFT JOIN collections c ON collection_id = c.id WHERE p.owner_id = ? ORDER BY created ASC", u.ID) 1865 if err != nil { 1866 log.Error("Failed selecting from posts: %v", err) 1867 return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user posts."} 1868 } 1869 defer rows.Close() 1870 1871 posts := []PublicPost{} 1872 var gotErr bool 1873 for rows.Next() { 1874 p := Post{} 1875 c := Collection{} 1876 var alias, title, description sql.NullString 1877 var views sql.NullInt64 1878 err = rows.Scan(&p.ID, &p.Slug, &p.ViewCount, &p.Title, &p.Created, &p.Updated, &p.Content, &p.Font, &p.Language, &p.RTL, &alias, &title, &description, &views) 1879 if err != nil { 1880 log.Error("Failed scanning User.getPosts() row: %v", err) 1881 gotErr = true 1882 break 1883 } 1884 p.extractData() 1885 pubPost := p.processPost() 1886 1887 if alias.Valid && alias.String != "" { 1888 c.Alias = alias.String 1889 c.Title = title.String 1890 c.Description = description.String 1891 c.Views = views.Int64 1892 pubPost.Collection = &CollectionObj{Collection: c} 1893 } 1894 1895 posts = append(posts, pubPost) 1896 } 1897 err = rows.Err() 1898 if err != nil { 1899 log.Error("Error after Next() on rows: %v", err) 1900 } 1901 1902 if gotErr && len(posts) == 0 { 1903 // There were a lot of errors 1904 return nil, impart.HTTPError{http.StatusInternalServerError, "Unable to get data."} 1905 } 1906 1907 return &posts, nil 1908} 1909 1910func (db *datastore) GetUserPostsCount(userID int64) int64 { 1911 var count int64 1912 err := db.QueryRow("SELECT COUNT(*) FROM posts WHERE owner_id = ?", userID).Scan(&count) 1913 switch { 1914 case err == sql.ErrNoRows: 1915 return 0 1916 case err != nil: 1917 log.Error("Failed selecting posts count for user %d: %v", userID, err) 1918 return 0 1919 } 1920 1921 return count 1922} 1923 1924// ChangeSettings takes a User and applies the changes in the given 1925// userSettings, MODIFYING THE USER with successful changes. 1926func (db *datastore) ChangeSettings(app *App, u *User, s *userSettings) error { 1927 var errPass error 1928 q := query.NewUpdate() 1929 1930 // Update email if given 1931 if s.Email != "" { 1932 encEmail, err := data.Encrypt(app.keys.EmailKey, s.Email) 1933 if err != nil { 1934 log.Error("Couldn't encrypt email %s: %s\n", s.Email, err) 1935 return impart.HTTPError{http.StatusInternalServerError, "Unable to encrypt email address."} 1936 } 1937 q.SetBytes(encEmail, "email") 1938 1939 // Update the email if something goes awry updating the password 1940 defer func() { 1941 if errPass != nil { 1942 db.UpdateEncryptedUserEmail(u.ID, encEmail) 1943 } 1944 }() 1945 u.Email = zero.StringFrom(s.Email) 1946 } 1947 1948 // Update username if given 1949 var newUsername string 1950 if s.Username != "" { 1951 var ie *impart.HTTPError 1952 newUsername, ie = getValidUsername(app, s.Username, u.Username) 1953 if ie != nil { 1954 // Username is invalid 1955 return *ie 1956 } 1957 if !author.IsValidUsername(app.cfg, newUsername) { 1958 // Ensure the username is syntactically correct. 1959 return impart.HTTPError{http.StatusPreconditionFailed, "Username isn't valid."} 1960 } 1961 1962 t, err := db.Begin() 1963 if err != nil { 1964 log.Error("Couldn't start username change transaction: %v", err) 1965 return err 1966 } 1967 1968 _, err = t.Exec("UPDATE users SET username = ? WHERE id = ?", newUsername, u.ID) 1969 if err != nil { 1970 t.Rollback() 1971 if db.isDuplicateKeyErr(err) { 1972 return impart.HTTPError{http.StatusConflict, "Username is already taken."} 1973 } 1974 log.Error("Unable to update users table: %v", err) 1975 return ErrInternalGeneral 1976 } 1977 1978 _, err = t.Exec("UPDATE collections SET alias = ? WHERE alias = ? AND owner_id = ?", newUsername, u.Username, u.ID) 1979 if err != nil { 1980 t.Rollback() 1981 if db.isDuplicateKeyErr(err) { 1982 return impart.HTTPError{http.StatusConflict, "Username is already taken."} 1983 } 1984 log.Error("Unable to update collection: %v", err) 1985 return ErrInternalGeneral 1986 } 1987 1988 // Keep track of name changes for redirection 1989 db.RemoveCollectionRedirect(t, newUsername) 1990 _, err = t.Exec("UPDATE collectionredirects SET new_alias = ? WHERE new_alias = ?", newUsername, u.Username) 1991 if err != nil { 1992 log.Error("Unable to update collectionredirects: %v", err) 1993 } 1994 _, err = t.Exec("INSERT INTO collectionredirects (prev_alias, new_alias) VALUES (?, ?)", u.Username, newUsername) 1995 if err != nil { 1996 log.Error("Unable to add new collectionredirect: %v", err) 1997 } 1998 1999 err = t.Commit() 2000 if err != nil { 2001 t.Rollback() 2002 log.Error("Rolling back after Commit(): %v\n", err) 2003 return err 2004 } 2005 2006 u.Username = newUsername 2007 } 2008 2009 // Update passphrase if given 2010 if s.NewPass != "" { 2011 // Check if user has already set a password 2012 var err error 2013 u.HasPass, err = db.IsUserPassSet(u.ID) 2014 if err != nil { 2015 errPass = impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data."} 2016 return errPass 2017 } 2018 2019 if u.HasPass { 2020 // Check if currently-set password is correct 2021 hashedPass := u.HashedPass 2022 if len(hashedPass) == 0 { 2023 authUser, err := db.GetUserForAuthByID(u.ID) 2024 if err != nil { 2025 errPass = err 2026 return errPass 2027 } 2028 hashedPass = authUser.HashedPass 2029 } 2030 if !auth.Authenticated(hashedPass, []byte(s.OldPass)) { 2031 errPass = impart.HTTPError{http.StatusUnauthorized, "Incorrect password."} 2032 return errPass 2033 } 2034 } 2035 hashedPass, err := auth.HashPass([]byte(s.NewPass)) 2036 if err != nil { 2037 errPass = impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."} 2038 return errPass 2039 } 2040 q.SetBytes(hashedPass, "password") 2041 } 2042 2043 // WHERE values 2044 q.Append(u.ID) 2045 2046 if q.Updates == "" { 2047 if s.Username == "" { 2048 return ErrPostNoUpdatableVals 2049 } 2050 2051 // Nothing to update except username. That was successful, so return now. 2052 return nil 2053 } 2054 2055 res, err := db.Exec("UPDATE users SET "+q.Updates+" WHERE id = ?", q.Params...) 2056 if err != nil { 2057 log.Error("Unable to update collection: %v", err) 2058 return err 2059 } 2060 2061 rowsAffected, _ := res.RowsAffected() 2062 if rowsAffected == 0 { 2063 // Show the correct error message if nothing was updated 2064 var dummy int 2065 err := db.QueryRow("SELECT 1 FROM users WHERE id = ?", u.ID).Scan(&dummy) 2066 switch { 2067 case err == sql.ErrNoRows: 2068 return ErrUnauthorizedGeneral 2069 case err != nil: 2070 log.Error("Failed selecting from users: %v", err) 2071 } 2072 return nil 2073 } 2074 2075 if s.NewPass != "" && !u.HasPass { 2076 u.HasPass = true 2077 } 2078 2079 return nil 2080} 2081 2082func (db *datastore) ChangePassphrase(userID int64, sudo bool, curPass string, hashedPass []byte) error { 2083 var dbPass []byte 2084 err := db.QueryRow("SELECT password FROM users WHERE id = ?", userID).Scan(&dbPass) 2085 switch { 2086 case err == sql.ErrNoRows: 2087 return ErrUserNotFound 2088 case err != nil: 2089 log.Error("Couldn't SELECT user password for change: %v", err) 2090 return err 2091 } 2092 2093 if !sudo && !auth.Authenticated(dbPass, []byte(curPass)) { 2094 return impart.HTTPError{http.StatusUnauthorized, "Incorrect password."} 2095 } 2096 2097 _, err = db.Exec("UPDATE users SET password = ? WHERE id = ?", hashedPass, userID) 2098 if err != nil { 2099 log.Error("Could not update passphrase: %v", err) 2100 return err 2101 } 2102 2103 return nil 2104} 2105 2106func (db *datastore) RemoveCollectionRedirect(t *sql.Tx, alias string) error { 2107 _, err := t.Exec("DELETE FROM collectionredirects WHERE prev_alias = ?", alias) 2108 if err != nil { 2109 log.Error("Unable to delete from collectionredirects: %v", err) 2110 return err 2111 } 2112 return nil 2113} 2114 2115func (db *datastore) GetCollectionRedirect(alias string) (new string) { 2116 row := db.QueryRow("SELECT new_alias FROM collectionredirects WHERE prev_alias = ?", alias) 2117 err := row.Scan(&new) 2118 if err != nil && err != sql.ErrNoRows && !db.isIgnorableError(err) { 2119 log.Error("Failed selecting from collectionredirects: %v", err) 2120 } 2121 return 2122} 2123 2124func (db *datastore) DeleteCollection(alias string, userID int64) error { 2125 c := &Collection{Alias: alias} 2126 var username string 2127 2128 row := db.QueryRow("SELECT username FROM users WHERE id = ?", userID) 2129 err := row.Scan(&username) 2130 if err != nil { 2131 return err 2132 } 2133 2134 // Ensure user isn't deleting their main blog 2135 if alias == username { 2136 return impart.HTTPError{http.StatusForbidden, "You cannot currently delete your primary blog."} 2137 } 2138 2139 row = db.QueryRow("SELECT id FROM collections WHERE alias = ? AND owner_id = ?", alias, userID) 2140 err = row.Scan(&c.ID) 2141 switch { 2142 case err == sql.ErrNoRows: 2143 return impart.HTTPError{http.StatusNotFound, "Collection doesn't exist or you're not allowed to delete it."} 2144 case err != nil: 2145 log.Error("Failed selecting from collections: %v", err) 2146 return ErrInternalGeneral 2147 } 2148 2149 t, err := db.Begin() 2150 if err != nil { 2151 return err 2152 } 2153 2154 // Float all collection's posts 2155 _, err = t.Exec("UPDATE posts SET collection_id = NULL WHERE collection_id = ? AND owner_id = ?", c.ID, userID) 2156 if err != nil { 2157 t.Rollback() 2158 return err 2159 } 2160 2161 // Remove redirects to or from this collection 2162 _, err = t.Exec("DELETE FROM collectionredirects WHERE prev_alias = ? OR new_alias = ?", alias, alias) 2163 if err != nil { 2164 t.Rollback() 2165 return err 2166 } 2167 2168 // Remove any optional collection password 2169 _, err = t.Exec("DELETE FROM collectionpasswords WHERE collection_id = ?", c.ID) 2170 if err != nil { 2171 t.Rollback() 2172 return err 2173 } 2174 2175 // Finally, delete collection itself 2176 _, err = t.Exec("DELETE FROM collections WHERE id = ?", c.ID) 2177 if err != nil { 2178 t.Rollback() 2179 return err 2180 } 2181 2182 err = t.Commit() 2183 if err != nil { 2184 t.Rollback() 2185 return err 2186 } 2187 2188 return nil 2189} 2190 2191func (db *datastore) IsCollectionAttributeOn(id int64, attr string) bool { 2192 var v string 2193 err := db.QueryRow("SELECT value FROM collectionattributes WHERE collection_id = ? AND attribute = ?", id, attr).Scan(&v) 2194 switch { 2195 case err == sql.ErrNoRows: 2196 return false 2197 case err != nil: 2198 log.Error("Couldn't SELECT value in isCollectionAttributeOn for attribute '%s': %v", attr, err) 2199 return false 2200 } 2201 return v == "1" 2202} 2203 2204func (db *datastore) CollectionHasAttribute(id int64, attr string) bool { 2205 var dummy string 2206 err := db.QueryRow("SELECT value FROM collectionattributes WHERE collection_id = ? AND attribute = ?", id, attr).Scan(&dummy) 2207 switch { 2208 case err == sql.ErrNoRows: 2209 return false 2210 case err != nil: 2211 log.Error("Couldn't SELECT value in collectionHasAttribute for attribute '%s': %v", attr, err) 2212 return false 2213 } 2214 return true 2215} 2216 2217func (db *datastore) GetCollectionAttribute(id int64, attr string) string { 2218 var v string 2219 err := db.QueryRow("SELECT value FROM collectionattributes WHERE collection_id = ? AND attribute = ?", id, attr).Scan(&v) 2220 switch { 2221 case err == sql.ErrNoRows: 2222 return "" 2223 case err != nil: 2224 log.Error("Couldn't SELECT value in getCollectionAttribute for attribute '%s': %v", attr, err) 2225 return "" 2226 } 2227 return v 2228} 2229 2230func (db *datastore) SetCollectionAttribute(id int64, attr, v string) error { 2231 _, err := db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?)", id, attr, v) 2232 if err != nil { 2233 log.Error("Unable to INSERT into collectionattributes: %v", err) 2234 return err 2235 } 2236 return nil 2237} 2238 2239// DeleteAccount will delete the entire account for userID 2240func (db *datastore) DeleteAccount(userID int64) error { 2241 // Get all collections 2242 rows, err := db.Query("SELECT id, alias FROM collections WHERE owner_id = ?", userID) 2243 if err != nil { 2244 log.Error("Unable to get collections: %v", err) 2245 return err 2246 } 2247 defer rows.Close() 2248 colls := []Collection{} 2249 var c Collection 2250 for rows.Next() { 2251 err = rows.Scan(&c.ID, &c.Alias) 2252 if err != nil { 2253 log.Error("Unable to scan collection cols: %v", err) 2254 return err 2255 } 2256 colls = append(colls, c) 2257 } 2258 2259 // Start transaction 2260 t, err := db.Begin() 2261 if err != nil { 2262 log.Error("Unable to begin: %v", err) 2263 return err 2264 } 2265 2266 // Clean up all collection related information 2267 var res sql.Result 2268 for _, c := range colls { 2269 // Delete tokens 2270 res, err = t.Exec("DELETE FROM collectionattributes WHERE collection_id = ?", c.ID) 2271 if err != nil { 2272 t.Rollback() 2273 log.Error("Unable to delete attributes on %s: %v", c.Alias, err) 2274 return err 2275 } 2276 rs, _ := res.RowsAffected() 2277 log.Info("Deleted %d for %s from collectionattributes", rs, c.Alias) 2278 2279 // Remove any optional collection password 2280 res, err = t.Exec("DELETE FROM collectionpasswords WHERE collection_id = ?", c.ID) 2281 if err != nil { 2282 t.Rollback() 2283 log.Error("Unable to delete passwords on %s: %v", c.Alias, err) 2284 return err 2285 } 2286 rs, _ = res.RowsAffected() 2287 log.Info("Deleted %d for %s from collectionpasswords", rs, c.Alias) 2288 2289 // Remove redirects to this collection 2290 res, err = t.Exec("DELETE FROM collectionredirects WHERE new_alias = ?", c.Alias) 2291 if err != nil { 2292 t.Rollback() 2293 log.Error("Unable to delete redirects on %s: %v", c.Alias, err) 2294 return err 2295 } 2296 rs, _ = res.RowsAffected() 2297 log.Info("Deleted %d for %s from collectionredirects", rs, c.Alias) 2298 2299 // Remove any collection keys 2300 res, err = t.Exec("DELETE FROM collectionkeys WHERE collection_id = ?", c.ID) 2301 if err != nil { 2302 t.Rollback() 2303 log.Error("Unable to delete keys on %s: %v", c.Alias, err) 2304 return err 2305 } 2306 rs, _ = res.RowsAffected() 2307 log.Info("Deleted %d for %s from collectionkeys", rs, c.Alias) 2308 2309 // TODO: federate delete collection 2310 2311 // Remove remote follows 2312 res, err = t.Exec("DELETE FROM remotefollows WHERE collection_id = ?", c.ID) 2313 if err != nil { 2314 t.Rollback() 2315 log.Error("Unable to delete remote follows on %s: %v", c.Alias, err) 2316 return err 2317 } 2318 rs, _ = res.RowsAffected() 2319 log.Info("Deleted %d for %s from remotefollows", rs, c.Alias) 2320 } 2321 2322 // Delete collections 2323 res, err = t.Exec("DELETE FROM collections WHERE owner_id = ?", userID) 2324 if err != nil { 2325 t.Rollback() 2326 log.Error("Unable to delete collections: %v", err) 2327 return err 2328 } 2329 rs, _ := res.RowsAffected() 2330 log.Info("Deleted %d from collections", rs) 2331 2332 // Delete tokens 2333 res, err = t.Exec("DELETE FROM accesstokens WHERE user_id = ?", userID) 2334 if err != nil { 2335 t.Rollback() 2336 log.Error("Unable to delete access tokens: %v", err) 2337 return err 2338 } 2339 rs, _ = res.RowsAffected() 2340 log.Info("Deleted %d from accesstokens", rs) 2341 2342 // Delete user attributes 2343 res, err = t.Exec("DELETE FROM oauth_users WHERE user_id = ?", userID) 2344 if err != nil { 2345 t.Rollback() 2346 log.Error("Unable to delete oauth_users: %v", err) 2347 return err 2348 } 2349 rs, _ = res.RowsAffected() 2350 log.Info("Deleted %d from oauth_users", rs) 2351 2352 // Delete posts 2353 // TODO: should maybe get each row so we can federate a delete 2354 // if so needs to be outside of transaction like collections 2355 res, err = t.Exec("DELETE FROM posts WHERE owner_id = ?", userID) 2356 if err != nil { 2357 t.Rollback() 2358 log.Error("Unable to delete posts: %v", err) 2359 return err 2360 } 2361 rs, _ = res.RowsAffected() 2362 log.Info("Deleted %d from posts", rs) 2363 2364 // Delete user attributes 2365 res, err = t.Exec("DELETE FROM userattributes WHERE user_id = ?", userID) 2366 if err != nil { 2367 t.Rollback() 2368 log.Error("Unable to delete attributes: %v", err) 2369 return err 2370 } 2371 rs, _ = res.RowsAffected() 2372 log.Info("Deleted %d from userattributes", rs) 2373 2374 // Delete user invites 2375 res, err = t.Exec("DELETE FROM userinvites WHERE owner_id = ?", userID) 2376 if err != nil { 2377 t.Rollback() 2378 log.Error("Unable to delete invites: %v", err) 2379 return err 2380 } 2381 rs, _ = res.RowsAffected() 2382 log.Info("Deleted %d from userinvites", rs) 2383 2384 // Delete the user 2385 res, err = t.Exec("DELETE FROM users WHERE id = ?", userID) 2386 if err != nil { 2387 t.Rollback() 2388 log.Error("Unable to delete user: %v", err) 2389 return err 2390 } 2391 rs, _ = res.RowsAffected() 2392 log.Info("Deleted %d from users", rs) 2393 2394 // Commit all changes to the database 2395 err = t.Commit() 2396 if err != nil { 2397 t.Rollback() 2398 log.Error("Unable to commit: %v", err) 2399 return err 2400 } 2401 2402 // TODO: federate delete actor 2403 2404 return nil 2405} 2406 2407func (db *datastore) GetAPActorKeys(collectionID int64) ([]byte, []byte) { 2408 var pub, priv []byte 2409 err := db.QueryRow("SELECT public_key, private_key FROM collectionkeys WHERE collection_id = ?", collectionID).Scan(&pub, &priv) 2410 switch { 2411 case err == sql.ErrNoRows: 2412 // Generate keys 2413 pub, priv = activitypub.GenerateKeys() 2414 _, err = db.Exec("INSERT INTO collectionkeys (collection_id, public_key, private_key) VALUES (?, ?, ?)", collectionID, pub, priv) 2415 if err != nil { 2416 log.Error("Unable to INSERT new activitypub keypair: %v", err) 2417 return nil, nil 2418 } 2419 case err != nil: 2420 log.Error("Couldn't SELECT collectionkeys: %v", err) 2421 return nil, nil 2422 } 2423 2424 return pub, priv 2425} 2426 2427func (db *datastore) CreateUserInvite(id string, userID int64, maxUses int, expires *time.Time) error { 2428 _, err := db.Exec("INSERT INTO userinvites (id, owner_id, max_uses, created, expires, inactive) VALUES (?, ?, ?, "+db.now()+", ?, 0)", id, userID, maxUses, expires) 2429 return err 2430} 2431 2432func (db *datastore) GetUserInvites(userID int64) (*[]Invite, error) { 2433 rows, err := db.Query("SELECT id, max_uses, created, expires, inactive FROM userinvites WHERE owner_id = ? ORDER BY created DESC", userID) 2434 if err != nil { 2435 log.Error("Failed selecting from userinvites: %v", err) 2436 return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user invites."} 2437 } 2438 defer rows.Close() 2439 2440 is := []Invite{} 2441 for rows.Next() { 2442 i := Invite{} 2443 err = rows.Scan(&i.ID, &i.MaxUses, &i.Created, &i.Expires, &i.Inactive) 2444 is = append(is, i) 2445 } 2446 return &is, nil 2447} 2448 2449func (db *datastore) GetUserInvite(id string) (*Invite, error) { 2450 var i Invite 2451 err := db.QueryRow("SELECT id, max_uses, created, expires, inactive FROM userinvites WHERE id = ?", id).Scan(&i.ID, &i.MaxUses, &i.Created, &i.Expires, &i.Inactive) 2452 switch { 2453 case err == sql.ErrNoRows, db.isIgnorableError(err): 2454 return nil, impart.HTTPError{http.StatusNotFound, "Invite doesn't exist."} 2455 case err != nil: 2456 log.Error("Failed selecting invite: %v", err) 2457 return nil, err 2458 } 2459 2460 return &i, nil 2461} 2462 2463// IsUsersInvite returns true if the user with ID created the invite with code 2464// and an error other than sql no rows, if any. Will return false in the event 2465// of an error. 2466func (db *datastore) IsUsersInvite(code string, userID int64) (bool, error) { 2467 var id string 2468 err := db.QueryRow("SELECT id FROM userinvites WHERE id = ? AND owner_id = ?", code, userID).Scan(&id) 2469 if err != nil && err != sql.ErrNoRows { 2470 log.Error("Failed selecting invite: %v", err) 2471 return false, err 2472 } 2473 return id != "", nil 2474} 2475 2476func (db *datastore) GetUsersInvitedCount(id string) int64 { 2477 var count int64 2478 err := db.QueryRow("SELECT COUNT(*) FROM usersinvited WHERE invite_id = ?", id).Scan(&count) 2479 switch { 2480 case err == sql.ErrNoRows: 2481 return 0 2482 case err != nil: 2483 log.Error("Failed selecting users invited count: %v", err) 2484 return 0 2485 } 2486 2487 return count 2488} 2489 2490func (db *datastore) CreateInvitedUser(inviteID string, userID int64) error { 2491 _, err := db.Exec("INSERT INTO usersinvited (invite_id, user_id) VALUES (?, ?)", inviteID, userID) 2492 return err 2493} 2494 2495func (db *datastore) GetInstancePages() ([]*instanceContent, error) { 2496 return db.GetAllDynamicContent("page") 2497} 2498 2499func (db *datastore) GetAllDynamicContent(t string) ([]*instanceContent, error) { 2500 where := "" 2501 params := []interface{}{} 2502 if t != "" { 2503 where = " WHERE content_type = ?" 2504 params = append(params, t) 2505 } 2506 rows, err := db.Query("SELECT id, title, content, updated, content_type FROM appcontent"+where, params...) 2507 if err != nil { 2508 log.Error("Failed selecting from appcontent: %v", err) 2509 return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve instance pages."} 2510 } 2511 defer rows.Close() 2512 2513 pages := []*instanceContent{} 2514 for rows.Next() { 2515 c := &instanceContent{} 2516 err = rows.Scan(&c.ID, &c.Title, &c.Content, &c.Updated, &c.Type) 2517 if err != nil { 2518 log.Error("Failed scanning row: %v", err) 2519 break 2520 } 2521 pages = append(pages, c) 2522 } 2523 err = rows.Err() 2524 if err != nil { 2525 log.Error("Error after Next() on rows: %v", err) 2526 } 2527 2528 return pages, nil 2529} 2530 2531func (db *datastore) GetDynamicContent(id string) (*instanceContent, error) { 2532 c := &instanceContent{ 2533 ID: id, 2534 } 2535 err := db.QueryRow("SELECT title, content, updated, content_type FROM appcontent WHERE id = ?", id).Scan(&c.Title, &c.Content, &c.Updated, &c.Type) 2536 switch { 2537 case err == sql.ErrNoRows: 2538 return nil, nil 2539 case err != nil: 2540 log.Error("Couldn't SELECT FROM appcontent for id '%s': %v", id, err) 2541 return nil, err 2542 } 2543 return c, nil 2544} 2545 2546func (db *datastore) UpdateDynamicContent(id, title, content, contentType string) error { 2547 var err error 2548 if db.driverName == driverSQLite { 2549 _, err = db.Exec("INSERT OR REPLACE INTO appcontent (id, title, content, updated, content_type) VALUES (?, ?, ?, "+db.now()+", ?)", id, title, content, contentType) 2550 } else { 2551 _, err = db.Exec("INSERT INTO appcontent (id, title, content, updated, content_type) VALUES (?, ?, ?, "+db.now()+", ?) "+db.upsert("id")+" title = ?, content = ?, updated = "+db.now(), id, title, content, contentType, title, content) 2552 } 2553 if err != nil { 2554 log.Error("Unable to INSERT appcontent for '%s': %v", id, err) 2555 } 2556 return err 2557} 2558 2559func (db *datastore) GetAllUsers(page uint) (*[]User, error) { 2560 limitStr := fmt.Sprintf("0, %d", adminUsersPerPage) 2561 if page > 1 { 2562 limitStr = fmt.Sprintf("%d, %d", (page-1)*adminUsersPerPage, adminUsersPerPage) 2563 } 2564 2565 rows, err := db.Query("SELECT id, username, created, status FROM users ORDER BY created DESC LIMIT " + limitStr) 2566 if err != nil { 2567 log.Error("Failed selecting from users: %v", err) 2568 return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve all users."} 2569 } 2570 defer rows.Close() 2571 2572 users := []User{} 2573 for rows.Next() { 2574 u := User{} 2575 err = rows.Scan(&u.ID, &u.Username, &u.Created, &u.Status) 2576 if err != nil { 2577 log.Error("Failed scanning GetAllUsers() row: %v", err) 2578 break 2579 } 2580 users = append(users, u) 2581 } 2582 return &users, nil 2583} 2584 2585func (db *datastore) GetAllUsersCount() int64 { 2586 var count int64 2587 err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count) 2588 switch { 2589 case err == sql.ErrNoRows: 2590 return 0 2591 case err != nil: 2592 log.Error("Failed selecting all users count: %v", err) 2593 return 0 2594 } 2595 2596 return count 2597} 2598 2599func (db *datastore) GetUserLastPostTime(id int64) (*time.Time, error) { 2600 var t time.Time 2601 err := db.QueryRow("SELECT created FROM posts WHERE owner_id = ? ORDER BY created DESC LIMIT 1", id).Scan(&t) 2602 switch { 2603 case err == sql.ErrNoRows: 2604 return nil, nil 2605 case err != nil: 2606 log.Error("Failed selecting last post time from posts: %v", err) 2607 return nil, err 2608 } 2609 return &t, nil 2610} 2611 2612// SetUserStatus changes a user's status in the database. see Users.UserStatus 2613func (db *datastore) SetUserStatus(id int64, status UserStatus) error { 2614 _, err := db.Exec("UPDATE users SET status = ? WHERE id = ?", status, id) 2615 if err != nil { 2616 return fmt.Errorf("failed to update user status: %v", err) 2617 } 2618 return nil 2619} 2620 2621func (db *datastore) GetCollectionLastPostTime(id int64) (*time.Time, error) { 2622 var t time.Time 2623 err := db.QueryRow("SELECT created FROM posts WHERE collection_id = ? ORDER BY created DESC LIMIT 1", id).Scan(&t) 2624 switch { 2625 case err == sql.ErrNoRows: 2626 return nil, nil 2627 case err != nil: 2628 log.Error("Failed selecting last post time from posts: %v", err) 2629 return nil, err 2630 } 2631 return &t, nil 2632} 2633 2634func (db *datastore) GenerateOAuthState(ctx context.Context, provider string, clientID string, attachUser int64, inviteCode string) (string, error) { 2635 state := id.Generate62RandomString(24) 2636 attachUserVal := sql.NullInt64{Valid: attachUser > 0, Int64: attachUser} 2637 inviteCodeVal := sql.NullString{Valid: inviteCode != "", String: inviteCode} 2638 _, err := db.ExecContext(ctx, "INSERT INTO oauth_client_states (state, provider, client_id, used, created_at, attach_user_id, invite_code) VALUES (?, ?, ?, FALSE, "+db.now()+", ?, ?)", state, provider, clientID, attachUserVal, inviteCodeVal) 2639 if err != nil { 2640 return "", fmt.Errorf("unable to record oauth client state: %w", err) 2641 } 2642 return state, nil 2643} 2644 2645func (db *datastore) ValidateOAuthState(ctx context.Context, state string) (string, string, int64, string, error) { 2646 var provider string 2647 var clientID string 2648 var attachUserID sql.NullInt64 2649 var inviteCode sql.NullString 2650 err := wf_db.RunTransactionWithOptions(ctx, db.DB, &sql.TxOptions{}, func(ctx context.Context, tx *sql.Tx) error { 2651 err := tx. 2652 QueryRowContext(ctx, "SELECT provider, client_id, attach_user_id, invite_code FROM oauth_client_states WHERE state = ? AND used = FALSE", state). 2653 Scan(&provider, &clientID, &attachUserID, &inviteCode) 2654 if err != nil { 2655 return err 2656 } 2657 2658 res, err := tx.ExecContext(ctx, "UPDATE oauth_client_states SET used = TRUE WHERE state = ?", state) 2659 if err != nil { 2660 return err 2661 } 2662 rowsAffected, err := res.RowsAffected() 2663 if err != nil { 2664 return err 2665 } 2666 if rowsAffected != 1 { 2667 return fmt.Errorf("state not found") 2668 } 2669 return nil 2670 }) 2671 if err != nil { 2672 return "", "", 0, "", nil 2673 } 2674 return provider, clientID, attachUserID.Int64, inviteCode.String, nil 2675} 2676 2677func (db *datastore) RecordRemoteUserID(ctx context.Context, localUserID int64, remoteUserID, provider, clientID, accessToken string) error { 2678 var err error 2679 if db.driverName == driverSQLite { 2680 _, err = db.ExecContext(ctx, "INSERT OR REPLACE INTO oauth_users (user_id, remote_user_id, provider, client_id, access_token) VALUES (?, ?, ?, ?, ?)", localUserID, remoteUserID, provider, clientID, accessToken) 2681 } else { 2682 _, err = db.ExecContext(ctx, "INSERT INTO oauth_users (user_id, remote_user_id, provider, client_id, access_token) VALUES (?, ?, ?, ?, ?) "+db.upsert("user")+" access_token = ?", localUserID, remoteUserID, provider, clientID, accessToken, accessToken) 2683 } 2684 if err != nil { 2685 log.Error("Unable to INSERT oauth_users for '%d': %v", localUserID, err) 2686 } 2687 return err 2688} 2689 2690// GetIDForRemoteUser returns a user ID associated with a remote user ID. 2691func (db *datastore) GetIDForRemoteUser(ctx context.Context, remoteUserID, provider, clientID string) (int64, error) { 2692 var userID int64 = -1 2693 err := db. 2694 QueryRowContext(ctx, "SELECT user_id FROM oauth_users WHERE remote_user_id = ? AND provider = ? AND client_id = ?", remoteUserID, provider, clientID). 2695 Scan(&userID) 2696 // Not finding a record is OK. 2697 if err != nil && err != sql.ErrNoRows { 2698 return -1, err 2699 } 2700 return userID, nil 2701} 2702 2703type oauthAccountInfo struct { 2704 Provider string 2705 ClientID string 2706 RemoteUserID string 2707 DisplayName string 2708 AllowDisconnect bool 2709} 2710 2711func (db *datastore) GetOauthAccounts(ctx context.Context, userID int64) ([]oauthAccountInfo, error) { 2712 rows, err := db.QueryContext(ctx, "SELECT provider, client_id, remote_user_id FROM oauth_users WHERE user_id = ? ", userID) 2713 if err != nil { 2714 log.Error("Failed selecting from oauth_users: %v", err) 2715 return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user oauth accounts."} 2716 } 2717 defer rows.Close() 2718 2719 var records []oauthAccountInfo 2720 for rows.Next() { 2721 info := oauthAccountInfo{} 2722 err = rows.Scan(&info.Provider, &info.ClientID, &info.RemoteUserID) 2723 if err != nil { 2724 log.Error("Failed scanning GetAllUsers() row: %v", err) 2725 break 2726 } 2727 records = append(records, info) 2728 } 2729 return records, nil 2730} 2731 2732// DatabaseInitialized returns whether or not the current datastore has been 2733// initialized with the correct schema. 2734// Currently, it checks to see if the `users` table exists. 2735func (db *datastore) DatabaseInitialized() bool { 2736 var dummy string 2737 var err error 2738 if db.driverName == driverSQLite { 2739 err = db.QueryRow("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'users'").Scan(&dummy) 2740 } else { 2741 err = db.QueryRow("SHOW TABLES LIKE 'users'").Scan(&dummy) 2742 } 2743 switch { 2744 case err == sql.ErrNoRows: 2745 return false 2746 case err != nil: 2747 log.Error("Couldn't SHOW TABLES: %v", err) 2748 return false 2749 } 2750 2751 return true 2752} 2753 2754func (db *datastore) RemoveOauth(ctx context.Context, userID int64, provider string, clientID string, remoteUserID string) error { 2755 _, err := db.ExecContext(ctx, `DELETE FROM oauth_users WHERE user_id = ? AND provider = ? AND client_id = ? AND remote_user_id = ?`, userID, provider, clientID, remoteUserID) 2756 return err 2757} 2758 2759func stringLogln(log *string, s string, v ...interface{}) { 2760 *log += fmt.Sprintf(s+"\n", v...) 2761} 2762 2763func handleFailedPostInsert(err error) error { 2764 log.Error("Couldn't insert into posts: %v", err) 2765 return err 2766} 2767 2768func (db *datastore) GetProfilePageFromHandle(app *App, handle string) (string, error) { 2769 handle = strings.TrimLeft(handle, "@") 2770 actorIRI := "" 2771 parts := strings.Split(handle, "@") 2772 if len(parts) != 2 { 2773 return "", fmt.Errorf("invalid handle format") 2774 } 2775 domain := parts[1] 2776 2777 // Check non-AP instances 2778 if siloProfileURL := silobridge.Profile(parts[0], domain); siloProfileURL != "" { 2779 return siloProfileURL, nil 2780 } 2781 2782 remoteUser, err := getRemoteUserFromHandle(app, handle) 2783 if err != nil { 2784 // can't find using handle in the table but the table may already have this user without 2785 // handle from a previous version 2786 // TODO: Make this determination. We should know whether a user exists without a handle, or doesn't exist at all 2787 actorIRI = RemoteLookup(handle) 2788 _, errRemoteUser := getRemoteUser(app, actorIRI) 2789 // if it exists then we need to update the handle 2790 if errRemoteUser == nil { 2791 _, err := app.db.Exec("UPDATE remoteusers SET handle = ? WHERE actor_id = ?", handle, actorIRI) 2792 if err != nil { 2793 log.Error("Couldn't update handle '%s' for user %s", handle, actorIRI) 2794 } 2795 } else { 2796 // this probably means we don't have the user in the table so let's try to insert it 2797 // here we need to ask the server for the inboxes 2798 remoteActor, err := activityserve.NewRemoteActor(actorIRI) 2799 if err != nil { 2800 log.Error("Couldn't fetch remote actor: %v", err) 2801 } 2802 if debugging { 2803 log.Info("%s %s %s %s", actorIRI, remoteActor.GetInbox(), remoteActor.GetSharedInbox(), handle) 2804 } 2805 _, err = app.db.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox, handle) VALUES(?, ?, ?, ?)", actorIRI, remoteActor.GetInbox(), remoteActor.GetSharedInbox(), handle) 2806 if err != nil { 2807 log.Error("Couldn't insert remote user: %v", err) 2808 return "", err 2809 } 2810 } 2811 } else { 2812 actorIRI = remoteUser.ActorID 2813 } 2814 return actorIRI, nil 2815} 2816