12 import aurweb
.exceptions
14 notify_cmd
= aurweb
.config
.get('notifications', 'notify-cmd')
16 repo_path
= aurweb
.config
.get('serve', 'repo-path')
17 repo_regex
= aurweb
.config
.get('serve', 'repo-regex')
18 git_shell_cmd
= aurweb
.config
.get('serve', 'git-shell-cmd')
19 git_update_cmd
= aurweb
.config
.get('serve', 'git-update-cmd')
20 ssh_cmdline
= aurweb
.config
.get('serve', 'ssh-cmdline')
22 enable_maintenance
= aurweb
.config
.getboolean('options', 'enable-maintenance')
23 maintenance_exc
= aurweb
.config
.get('options', 'maintenance-exceptions').split()
26 def pkgbase_from_name(pkgbase
):
27 conn
= aurweb
.db
.Connection()
28 cur
= conn
.execute("SELECT ID FROM PackageBases WHERE Name = ?", [pkgbase
])
31 return row
[0] if row
else None
34 def pkgbase_exists(pkgbase
):
35 return pkgbase_from_name(pkgbase
) is not None
39 conn
= aurweb
.db
.Connection()
41 cur
= conn
.execute("SELECT ID FROM Users WHERE Username = ?", [user
])
42 userid
= cur
.fetchone()[0]
44 raise aurweb
.exceptions
.InvalidUserException(user
)
46 cur
= conn
.execute("SELECT Name, PackagerUID FROM PackageBases " +
47 "WHERE MaintainerUID = ?", [userid
])
49 print((' ' if row
[1] else '*') + row
[0])
53 def create_pkgbase(pkgbase
, user
):
54 if not re
.match(repo_regex
, pkgbase
):
55 raise aurweb
.exceptions
.InvalidRepositoryNameException(pkgbase
)
56 if pkgbase_exists(pkgbase
):
57 raise aurweb
.exceptions
.PackageBaseExistsException(pkgbase
)
59 conn
= aurweb
.db
.Connection()
61 cur
= conn
.execute("SELECT ID FROM Users WHERE Username = ?", [user
])
62 userid
= cur
.fetchone()[0]
64 raise aurweb
.exceptions
.InvalidUserException(user
)
66 now
= int(time
.time())
67 cur
= conn
.execute("INSERT INTO PackageBases (Name, SubmittedTS, " +
68 "ModifiedTS, SubmitterUID, MaintainerUID) VALUES " +
69 "(?, ?, ?, ?, ?)", [pkgbase
, now
, now
, userid
, userid
])
70 pkgbase_id
= cur
.lastrowid
72 cur
= conn
.execute("INSERT INTO PackageNotifications " +
73 "(PackageBaseID, UserID) VALUES (?, ?)",
80 def pkgbase_adopt(pkgbase
, user
, privileged
):
81 pkgbase_id
= pkgbase_from_name(pkgbase
)
83 raise aurweb
.exceptions
.InvalidPackageBaseException(pkgbase
)
85 conn
= aurweb
.db
.Connection()
87 cur
= conn
.execute("SELECT ID FROM PackageBases WHERE ID = ? AND " +
88 "MaintainerUID IS NULL", [pkgbase_id
])
89 if not privileged
and not cur
.fetchone():
90 raise aurweb
.exceptions
.PermissionDeniedException(user
)
92 cur
= conn
.execute("SELECT ID FROM Users WHERE Username = ?", [user
])
93 userid
= cur
.fetchone()[0]
95 raise aurweb
.exceptions
.InvalidUserException(user
)
97 cur
= conn
.execute("UPDATE PackageBases SET MaintainerUID = ? " +
98 "WHERE ID = ?", [userid
, pkgbase_id
])
100 cur
= conn
.execute("SELECT COUNT(*) FROM PackageNotifications WHERE " +
101 "PackageBaseID = ? AND UserID = ?",
102 [pkgbase_id
, userid
])
103 if cur
.fetchone()[0] == 0:
104 cur
= conn
.execute("INSERT INTO PackageNotifications " +
105 "(PackageBaseID, UserID) VALUES (?, ?)",
106 [pkgbase_id
, userid
])
109 subprocess
.Popen((notify_cmd
, 'adopt', str(pkgbase_id
), str(userid
)))
114 def pkgbase_get_comaintainers(pkgbase
):
115 conn
= aurweb
.db
.Connection()
117 cur
= conn
.execute("SELECT UserName FROM PackageComaintainers " +
118 "INNER JOIN Users " +
119 "ON Users.ID = PackageComaintainers.UsersID " +
120 "INNER JOIN PackageBases " +
121 "ON PackageBases.ID = PackageComaintainers.PackageBaseID " +
122 "WHERE PackageBases.Name = ? " +
123 "ORDER BY Priority ASC", [pkgbase
])
125 return [row
[0] for row
in cur
.fetchall()]
128 def pkgbase_set_comaintainers(pkgbase
, userlist
, user
, privileged
):
129 pkgbase_id
= pkgbase_from_name(pkgbase
)
131 raise aurweb
.exceptions
.InvalidPackageBaseException(pkgbase
)
133 if not privileged
and not pkgbase_has_full_access(pkgbase
, user
):
134 raise aurweb
.exceptions
.PermissionDeniedException(user
)
136 conn
= aurweb
.db
.Connection()
138 userlist_old
= set(pkgbase_get_comaintainers(pkgbase
))
141 for olduser
in userlist_old
:
142 cur
= conn
.execute("SELECT ID FROM Users WHERE Username = ?",
144 userid
= cur
.fetchone()[0]
146 raise aurweb
.exceptions
.InvalidUserException(user
)
150 for newuser
in userlist
:
151 cur
= conn
.execute("SELECT ID FROM Users WHERE Username = ?",
153 userid
= cur
.fetchone()[0]
155 raise aurweb
.exceptions
.InvalidUserException(user
)
158 uids_add
= uids_new
- uids_old
159 uids_rem
= uids_old
- uids_new
162 for userid
in uids_new
:
163 if userid
in uids_add
:
164 cur
= conn
.execute("INSERT INTO PackageComaintainers " +
165 "(PackageBaseID, UsersID, Priority) " +
166 "VALUES (?, ?, ?)", [pkgbase_id
, userid
, i
])
167 subprocess
.Popen((notify_cmd
, 'comaintainer-add', str(pkgbase_id
),
170 cur
= conn
.execute("UPDATE PackageComaintainers " +
171 "SET Priority = ? " +
172 "WHERE PackageBaseID = ? AND UsersID = ?",
173 [i
, pkgbase_id
, userid
])
176 for userid
in uids_rem
:
177 cur
= conn
.execute("DELETE FROM PackageComaintainers " +
178 "WHERE PackageBaseID = ? AND UsersID = ?",
179 [pkgbase_id
, userid
])
180 subprocess
.Popen((notify_cmd
, 'comaintainer-remove',
181 str(pkgbase_id
), str(userid
)))
187 def pkgreq_by_pkgbase(pkgbase_id
, reqtype
):
188 conn
= aurweb
.db
.Connection()
190 cur
= conn
.execute("SELECT PackageRequests.ID FROM PackageRequests " +
191 "INNER JOIN RequestTypes ON " +
192 "RequestTypes.ID = PackageRequests.ReqTypeID " +
193 "WHERE PackageRequests.Status = 0 " +
194 "AND PackageRequests.PackageBaseID = ?" +
195 "AND RequestTypes.Name = ?", [pkgbase_id
, reqtype
])
197 return [row
[0] for row
in cur
.fetchall()]
200 def pkgreq_close(reqid
, user
, reason
, comments
, autoclose
=False):
201 statusmap
= {'accepted': 2, 'rejected': 3}
202 if reason
not in statusmap
:
203 raise aurweb
.exceptions
.InvalidReasonException(reason
)
204 status
= statusmap
[reason
]
206 conn
= aurweb
.db
.Connection()
211 cur
= conn
.execute("SELECT ID FROM Users WHERE Username = ?", [user
])
212 userid
= cur
.fetchone()[0]
214 raise aurweb
.exceptions
.InvalidUserException(user
)
216 conn
.execute("UPDATE PackageRequests SET Status = ?, ClosureComment = ? " +
217 "WHERE ID = ?", [status
, comments
, reqid
])
221 subprocess
.Popen((notify_cmd
, 'request-close', str(userid
), str(reqid
),
225 def pkgbase_disown(pkgbase
, user
, privileged
):
226 pkgbase_id
= pkgbase_from_name(pkgbase
)
228 raise aurweb
.exceptions
.InvalidPackageBaseException(pkgbase
)
230 initialized_by_owner
= pkgbase_has_full_access(pkgbase
, user
)
231 if not privileged
and not initialized_by_owner
:
232 raise aurweb
.exceptions
.PermissionDeniedException(user
)
234 # TODO: Support disowning package bases via package request.
236 # Scan through pending orphan requests and close them.
237 comment
= 'The user {:s} disowned the package.'.format(user
)
238 for reqid
in pkgreq_by_pkgbase(pkgbase_id
, 'orphan'):
239 pkgreq_close(reqid
, user
, 'accepted', comment
, True)
242 new_maintainer_userid
= None
244 conn
= aurweb
.db
.Connection()
246 # Make the first co-maintainer the new maintainer, unless the action was
247 # enforced by a Trusted User.
248 if initialized_by_owner
:
249 comaintainers
= pkgbase_get_comaintainers(pkgbase
)
250 if len(comaintainers
) > 0:
251 new_maintainer
= comaintainers
[0]
252 cur
= conn
.execute("SELECT ID FROM Users WHERE Username = ?",
254 new_maintainer_userid
= cur
.fetchone()[0]
255 comaintainers
.remove(new_maintainer
)
257 pkgbase_set_comaintainers(pkgbase
, comaintainers
, user
, privileged
)
258 cur
= conn
.execute("UPDATE PackageBases SET MaintainerUID = ? " +
259 "WHERE ID = ?", [new_maintainer_userid
, pkgbase_id
])
263 cur
= conn
.execute("SELECT ID FROM Users WHERE Username = ?", [user
])
264 userid
= cur
.fetchone()[0]
266 raise aurweb
.exceptions
.InvalidUserException(user
)
268 subprocess
.Popen((notify_cmd
, 'disown', str(pkgbase_id
), str(userid
)))
273 def pkgbase_flag(pkgbase
, user
, comment
):
274 pkgbase_id
= pkgbase_from_name(pkgbase
)
276 raise aurweb
.exceptions
.InvalidPackageBaseException(pkgbase
)
278 raise aurweb
.exceptions
.InvalidCommentException(comment
)
280 conn
= aurweb
.db
.Connection()
282 cur
= conn
.execute("SELECT ID FROM Users WHERE Username = ?", [user
])
283 userid
= cur
.fetchone()[0]
285 raise aurweb
.exceptions
.InvalidUserException(user
)
287 now
= int(time
.time())
288 conn
.execute("UPDATE PackageBases SET " +
289 "OutOfDateTS = ?, FlaggerUID = ?, FlaggerComment = ? " +
290 "WHERE ID = ? AND OutOfDateTS IS NULL",
291 [now
, userid
, comment
, pkgbase_id
])
295 subprocess
.Popen((notify_cmd
, 'flag', str(userid
), str(pkgbase_id
)))
298 def pkgbase_unflag(pkgbase
, user
):
299 pkgbase_id
= pkgbase_from_name(pkgbase
)
301 raise aurweb
.exceptions
.InvalidPackageBaseException(pkgbase
)
303 conn
= aurweb
.db
.Connection()
305 cur
= conn
.execute("SELECT ID FROM Users WHERE Username = ?", [user
])
306 userid
= cur
.fetchone()[0]
308 raise aurweb
.exceptions
.InvalidUserException(user
)
310 if user
in pkgbase_get_comaintainers(pkgbase
):
311 conn
.execute("UPDATE PackageBases SET OutOfDateTS = NULL " +
312 "WHERE ID = ?", [pkgbase_id
])
314 conn
.execute("UPDATE PackageBases SET OutOfDateTS = NULL " +
315 "WHERE ID = ? AND (MaintainerUID = ? OR FlaggerUID = ?)",
316 [pkgbase_id
, userid
, userid
])
321 def pkgbase_vote(pkgbase
, user
):
322 pkgbase_id
= pkgbase_from_name(pkgbase
)
324 raise aurweb
.exceptions
.InvalidPackageBaseException(pkgbase
)
326 conn
= aurweb
.db
.Connection()
328 cur
= conn
.execute("SELECT ID FROM Users WHERE Username = ?", [user
])
329 userid
= cur
.fetchone()[0]
331 raise aurweb
.exceptions
.InvalidUserException(user
)
333 cur
= conn
.execute("SELECT COUNT(*) FROM PackageVotes " +
334 "WHERE UsersID = ? AND PackageBaseID = ?",
335 [userid
, pkgbase_id
])
336 if cur
.fetchone()[0] > 0:
337 raise aurweb
.exceptions
.AlreadyVotedException(pkgbase
)
339 now
= int(time
.time())
340 conn
.execute("INSERT INTO PackageVotes (UsersID, PackageBaseID, VoteTS) " +
341 "VALUES (?, ?, ?)", [userid
, pkgbase_id
, now
])
342 conn
.execute("UPDATE PackageBases SET NumVotes = NumVotes + 1 " +
343 "WHERE ID = ?", [pkgbase_id
])
347 def pkgbase_unvote(pkgbase
, user
):
348 pkgbase_id
= pkgbase_from_name(pkgbase
)
350 raise aurweb
.exceptions
.InvalidPackageBaseException(pkgbase
)
352 conn
= aurweb
.db
.Connection()
354 cur
= conn
.execute("SELECT ID FROM Users WHERE Username = ?", [user
])
355 userid
= cur
.fetchone()[0]
357 raise aurweb
.exceptions
.InvalidUserException(user
)
359 cur
= conn
.execute("SELECT COUNT(*) FROM PackageVotes " +
360 "WHERE UsersID = ? AND PackageBaseID = ?",
361 [userid
, pkgbase_id
])
362 if cur
.fetchone()[0] == 0:
363 raise aurweb
.exceptions
.NotVotedException(pkgbase
)
365 conn
.execute("DELETE FROM PackageVotes WHERE UsersID = ? AND " +
366 "PackageBaseID = ?", [userid
, pkgbase_id
])
367 conn
.execute("UPDATE PackageBases SET NumVotes = NumVotes - 1 " +
368 "WHERE ID = ?", [pkgbase_id
])
372 def pkgbase_set_keywords(pkgbase
, keywords
):
373 pkgbase_id
= pkgbase_from_name(pkgbase
)
375 raise aurweb
.exceptions
.InvalidPackageBaseException(pkgbase
)
377 conn
= aurweb
.db
.Connection()
379 conn
.execute("DELETE FROM PackageKeywords WHERE PackageBaseID = ?",
381 for keyword
in keywords
:
382 conn
.execute("INSERT INTO PackageKeywords (PackageBaseID, Keyword) " +
383 "VALUES (?, ?)", [pkgbase_id
, keyword
])
389 def pkgbase_has_write_access(pkgbase
, user
):
390 conn
= aurweb
.db
.Connection()
392 cur
= conn
.execute("SELECT COUNT(*) FROM PackageBases " +
393 "LEFT JOIN PackageComaintainers " +
394 "ON PackageComaintainers.PackageBaseID = PackageBases.ID " +
395 "INNER JOIN Users " +
396 "ON Users.ID = PackageBases.MaintainerUID " +
397 "OR PackageBases.MaintainerUID IS NULL " +
398 "OR Users.ID = PackageComaintainers.UsersID " +
399 "WHERE Name = ? AND Username = ?", [pkgbase
, user
])
400 return cur
.fetchone()[0] > 0
403 def pkgbase_has_full_access(pkgbase
, user
):
404 conn
= aurweb
.db
.Connection()
406 cur
= conn
.execute("SELECT COUNT(*) FROM PackageBases " +
407 "INNER JOIN Users " +
408 "ON Users.ID = PackageBases.MaintainerUID " +
409 "WHERE Name = ? AND Username = ?", [pkgbase
, user
])
410 return cur
.fetchone()[0] > 0
414 sys
.stderr
.write("{:s}\n".format(msg
))
418 def die_with_help(msg
):
419 die(msg
+ "\nTry `{:s} help` for a list of commands.".format(ssh_cmdline
))
423 sys
.stderr
.write("warning: {:s}\n".format(msg
))
427 sys
.stderr
.write("Commands:\n")
428 colwidth
= max([len(cmd
) for cmd
in cmds
.keys()]) + 4
429 for key
in sorted(cmds
):
430 sys
.stderr
.write(" " + key
.ljust(colwidth
) + cmds
[key
] + "\n")
434 def checkarg_atleast(cmdargv
, *argdesc
):
435 if len(cmdargv
) - 1 < len(argdesc
):
436 msg
= 'missing {:s}'.format(argdesc
[len(cmdargv
) - 1])
437 raise aurweb
.exceptions
.InvalidArgumentsException(msg
)
440 def checkarg_atmost(cmdargv
, *argdesc
):
441 if len(cmdargv
) - 1 > len(argdesc
):
442 raise aurweb
.exceptions
.InvalidArgumentsException('too many arguments')
445 def checkarg(cmdargv
, *argdesc
):
446 checkarg_atleast(cmdargv
, *argdesc
)
447 checkarg_atmost(cmdargv
, *argdesc
)
450 def serve(action
, cmdargv
, user
, privileged
, remote_addr
):
451 if enable_maintenance
:
452 if remote_addr
not in maintenance_exc
:
453 raise aurweb
.exceptions
.MaintenanceException
455 if action
== 'git' and cmdargv
[1] in ('upload-pack', 'receive-pack'):
456 action
= action
+ '-' + cmdargv
[1]
459 if action
== 'git-upload-pack' or action
== 'git-receive-pack':
460 checkarg(cmdargv
, 'path')
462 path
= cmdargv
[1].rstrip('/')
463 if not path
.startswith('/'):
465 if not path
.endswith('.git'):
468 if not re
.match(repo_regex
, pkgbase
):
469 raise aurweb
.exceptions
.InvalidRepositoryNameException(pkgbase
)
471 if action
== 'git-receive-pack' and pkgbase_exists(pkgbase
):
472 if not privileged
and not pkgbase_has_write_access(pkgbase
, user
):
473 raise aurweb
.exceptions
.PermissionDeniedException(user
)
475 os
.environ
["AUR_USER"] = user
476 os
.environ
["AUR_PKGBASE"] = pkgbase
477 os
.environ
["GIT_NAMESPACE"] = pkgbase
478 cmd
= action
+ " '" + repo_path
+ "'"
479 os
.execl(git_shell_cmd
, git_shell_cmd
, '-c', cmd
)
480 elif action
== 'set-keywords':
481 checkarg(cmdargv
, 'repository name')
482 pkgbase_set_keywords(cmdargv
[1], cmdargv
[2:])
483 elif action
== 'list-repos':
486 elif action
== 'setup-repo':
487 checkarg(cmdargv
, 'repository name')
488 warn('{:s} is deprecated. '
489 'Use `git push` to create new repositories.'.format(action
))
490 create_pkgbase(cmdargv
[1], user
)
491 elif action
== 'restore':
492 checkarg(cmdargv
, 'repository name')
495 create_pkgbase(pkgbase
, user
)
497 os
.environ
["AUR_USER"] = user
498 os
.environ
["AUR_PKGBASE"] = pkgbase
499 os
.execl(git_update_cmd
, git_update_cmd
, 'restore')
500 elif action
== 'adopt':
501 checkarg(cmdargv
, 'repository name')
504 pkgbase_adopt(pkgbase
, user
, privileged
)
505 elif action
== 'disown':
506 checkarg(cmdargv
, 'repository name')
509 pkgbase_disown(pkgbase
, user
, privileged
)
510 elif action
== 'flag':
511 checkarg(cmdargv
, 'repository name', 'comment')
515 pkgbase_flag(pkgbase
, user
, comment
)
516 elif action
== 'unflag':
517 checkarg(cmdargv
, 'repository name')
520 pkgbase_unflag(pkgbase
, user
)
521 elif action
== 'vote':
522 checkarg(cmdargv
, 'repository name')
525 pkgbase_vote(pkgbase
, user
)
526 elif action
== 'unvote':
527 checkarg(cmdargv
, 'repository name')
530 pkgbase_unvote(pkgbase
, user
)
531 elif action
== 'set-comaintainers':
532 checkarg_atleast(cmdargv
, 'repository name')
535 userlist
= cmdargv
[2:]
536 pkgbase_set_comaintainers(pkgbase
, userlist
, user
, privileged
)
537 elif action
== 'help':
539 "adopt <name>": "Adopt a package base.",
540 "disown <name>": "Disown a package base.",
541 "flag <name> <comment>": "Flag a package base out-of-date.",
542 "help": "Show this help message and exit.",
543 "list-repos": "List all your repositories.",
544 "restore <name>": "Restore a deleted package base.",
545 "set-comaintainers <name> [...]": "Set package base co-maintainers.",
546 "set-keywords <name> [...]": "Change package base keywords.",
547 "setup-repo <name>": "Create a repository (deprecated).",
548 "unflag <name>": "Remove out-of-date flag from a package base.",
549 "unvote <name>": "Remove vote from a package base.",
550 "vote <name>": "Vote for a package base.",
551 "git-receive-pack": "Internal command used with Git.",
552 "git-upload-pack": "Internal command used with Git.",
556 msg
= 'invalid command: {:s}'.format(action
)
557 raise aurweb
.exceptions
.InvalidArgumentsException(msg
)
561 user
= os
.environ
.get('AUR_USER')
562 privileged
= (os
.environ
.get('AUR_PRIVILEGED', '0') == '1')
563 ssh_cmd
= os
.environ
.get('SSH_ORIGINAL_COMMAND')
564 ssh_client
= os
.environ
.get('SSH_CLIENT')
567 die_with_help("Interactive shell is disabled.")
568 cmdargv
= shlex
.split(ssh_cmd
)
570 remote_addr
= ssh_client
.split(' ')[0] if ssh_client
else None
573 serve(action
, cmdargv
, user
, privileged
, remote_addr
)
574 except aurweb
.exceptions
.MaintenanceException
:
575 die("The AUR is down due to maintenance. We will be back soon.")
576 except aurweb
.exceptions
.InvalidArgumentsException
as e
:
577 die_with_help('{:s}: {}'.format(action
, e
))
578 except aurweb
.exceptions
.AurwebException
as e
:
579 die('{:s}: {}'.format(action
, e
))
582 if __name__
== '__main__':