summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.bzrignore32
-rw-r--r--.gitignore20
-rw-r--r--.htaccess57
-rw-r--r--.perltidyrc16
-rw-r--r--.travis.yml48
-rw-r--r--Bugzilla.pm286
-rw-r--r--Bugzilla/Attachment.pm237
-rw-r--r--Bugzilla/Attachment/PatchReader.pm82
-rw-r--r--Bugzilla/Auth.pm27
-rw-r--r--Bugzilla/Auth/Login.pm2
-rw-r--r--Bugzilla/Auth/Login/Cookie.pm79
-rw-r--r--Bugzilla/Auth/Persist/Cookie.pm33
-rw-r--r--Bugzilla/Auth/Verify.pm8
-rw-r--r--Bugzilla/Auth/Verify/DB.pm5
-rw-r--r--Bugzilla/Bug.pm674
-rw-r--r--Bugzilla/BugMail.pm265
-rw-r--r--Bugzilla/BugUrl.pm2
-rw-r--r--Bugzilla/BugUrl/GitHub.pm36
-rw-r--r--Bugzilla/BugUrl/MozSupport.pm36
-rw-r--r--Bugzilla/BugUserLastVisit.pm92
-rw-r--r--Bugzilla/CGI.pm92
-rw-r--r--Bugzilla/Classification.pm17
-rw-r--r--Bugzilla/Comment.pm208
-rw-r--r--Bugzilla/Comment/TagWeights.pm77
-rw-r--r--Bugzilla/Component.pm20
-rw-r--r--Bugzilla/Config.pm6
-rw-r--r--Bugzilla/Config/Admin.pm7
-rw-r--r--Bugzilla/Config/Advanced.pm43
-rw-r--r--Bugzilla/Config/Auth.pm6
-rw-r--r--Bugzilla/Config/BugFields.pm8
-rw-r--r--Bugzilla/Config/Common.pm14
-rw-r--r--Bugzilla/Config/GroupSecurity.pm11
-rw-r--r--Bugzilla/Config/Memcached.pm32
-rw-r--r--Bugzilla/Constants.pm75
-rw-r--r--Bugzilla/DB.pm27
-rw-r--r--Bugzilla/DB/Mysql.pm2
-rw-r--r--Bugzilla/DB/Schema.pm112
-rw-r--r--Bugzilla/DB/Schema/Mysql.pm2
-rw-r--r--Bugzilla/DB/Schema/Oracle.pm2
-rw-r--r--Bugzilla/DB/Schema/Pg.pm2
-rw-r--r--Bugzilla/DB/Schema/Sqlite.pm1
-rw-r--r--Bugzilla/Error.pm111
-rw-r--r--Bugzilla/Field.pm204
-rw-r--r--Bugzilla/Field/Choice.pm2
-rw-r--r--Bugzilla/Flag.pm167
-rw-r--r--Bugzilla/FlagType.pm72
-rw-r--r--Bugzilla/Group.pm9
-rw-r--r--Bugzilla/Hook.pm132
-rw-r--r--Bugzilla/Install.pm6
-rw-r--r--Bugzilla/Install/DB.pm126
-rw-r--r--Bugzilla/Install/Filesystem.pm68
-rw-r--r--Bugzilla/Install/Requirements.pm27
-rw-r--r--Bugzilla/Install/Util.pm6
-rw-r--r--Bugzilla/Job/BugMail.pm31
-rw-r--r--Bugzilla/JobQueue.pm75
-rw-r--r--Bugzilla/JobQueue/Runner.pm25
-rw-r--r--Bugzilla/Keyword.pm2
-rw-r--r--Bugzilla/Mailer.pm60
-rw-r--r--Bugzilla/Memcached.pm490
-rw-r--r--Bugzilla/Metrics/Collector.pm171
-rw-r--r--Bugzilla/Metrics/Memcached.pm23
-rw-r--r--Bugzilla/Metrics/Mysql.pm148
-rw-r--r--Bugzilla/Metrics/Reporter.pm102
-rw-r--r--Bugzilla/Metrics/Reporter/ElasticSearch.pm104
-rw-r--r--Bugzilla/Metrics/Reporter/STDERR.pm151
-rw-r--r--Bugzilla/Metrics/Template.pm23
-rw-r--r--Bugzilla/Metrics/Template/Context.pm29
-rw-r--r--Bugzilla/Migrate.pm9
-rw-r--r--Bugzilla/Milestone.pm13
-rw-r--r--Bugzilla/Object.pm355
-rw-r--r--Bugzilla/PatchReader.pm117
-rw-r--r--Bugzilla/PatchReader/AddCVSContext.pm226
-rw-r--r--Bugzilla/PatchReader/Base.pm23
-rw-r--r--Bugzilla/PatchReader/CVSClient.pm48
-rw-r--r--Bugzilla/PatchReader/DiffPrinter/raw.pm61
-rw-r--r--Bugzilla/PatchReader/DiffPrinter/template.pm119
-rw-r--r--Bugzilla/PatchReader/FilterPatch.pm43
-rw-r--r--Bugzilla/PatchReader/FixPatchRoot.pm130
-rw-r--r--Bugzilla/PatchReader/NarrowPatch.pm44
-rw-r--r--Bugzilla/PatchReader/PatchInfoGrabber.pm45
-rw-r--r--Bugzilla/PatchReader/Raw.pm268
-rw-r--r--Bugzilla/Product.pm18
-rw-r--r--Bugzilla/Search.pm968
-rw-r--r--Bugzilla/Search/Clause.pm7
-rw-r--r--Bugzilla/Search/ClauseGroup.pm96
-rw-r--r--Bugzilla/Search/Quicksearch.pm103
-rw-r--r--Bugzilla/Search/Recent.pm16
-rw-r--r--Bugzilla/Search/Saved.pm6
-rw-r--r--Bugzilla/Send/Sendmail.pm95
-rw-r--r--Bugzilla/Sentry.pm322
-rw-r--r--Bugzilla/Status.pm20
-rw-r--r--Bugzilla/Template.pm290
-rw-r--r--Bugzilla/Template/Context.pm8
-rw-r--r--Bugzilla/Token.pm2
-rw-r--r--Bugzilla/User.pm419
-rw-r--r--Bugzilla/User/Setting.pm60
-rw-r--r--Bugzilla/UserAgent.pm259
-rw-r--r--Bugzilla/Util.pm102
-rw-r--r--Bugzilla/WebService.pm80
-rw-r--r--Bugzilla/WebService/Bug.pm1650
-rw-r--r--Bugzilla/WebService/BugUserLastVisit.pm208
-rw-r--r--Bugzilla/WebService/Bugzilla.pm48
-rw-r--r--Bugzilla/WebService/Classification.pm208
-rw-r--r--Bugzilla/WebService/Constants.pm91
-rw-r--r--Bugzilla/WebService/Group.pm29
-rw-r--r--Bugzilla/WebService/Product.pm323
-rw-r--r--Bugzilla/WebService/Server.pm81
-rw-r--r--Bugzilla/WebService/Server/JSONRPC.pm39
-rw-r--r--Bugzilla/WebService/Server/REST.pm659
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Bug.pm190
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm52
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm69
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Classification.pm49
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Group.pm56
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Product.pm82
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/User.pm80
-rw-r--r--Bugzilla/WebService/Server/XMLRPC.pm34
-rw-r--r--Bugzilla/WebService/User.pm292
-rw-r--r--Bugzilla/WebService/Util.pm238
-rw-r--r--Build.PL61
-rw-r--r--MANIFEST.SKIP53
-rw-r--r--README1
-rwxr-xr-xattachment.cgi66
-rwxr-xr-xbuglist.cgi182
-rw-r--r--bzr-update.sh9
-rwxr-xr-xchart.cgi3
-rwxr-xr-xchecksetup.pl5
-rwxr-xr-xclean-bug-user-last-visit.pl38
-rwxr-xr-xcollectstats.pl3
-rwxr-xr-xconfig.cgi41
-rwxr-xr-xcontrib/addcustomfield.pl63
-rwxr-xr-xcontrib/clear-memcached.pl27
-rwxr-xr-xcontrib/clear-templates.pl40
-rwxr-xr-xcontrib/convert-workflow.pl1
-rwxr-xr-xcontrib/fix_comment_text.pl75
-rwxr-xr-xcontrib/merge-users.pl31
-rwxr-xr-xcontrib/moco-ldap-check.pl542
-rwxr-xr-xcontrib/nagios_blocker_checker.pl201
-rwxr-xr-xcontrib/new-yui3.pl80
-rwxr-xr-xcontrib/nuke-bugs.pl69
-rwxr-xr-xcontrib/recode.pl2
-rw-r--r--contrib/reorg-tools/README9
-rwxr-xr-xcontrib/reorg-tools/convert_date_time_date.pl62
-rwxr-xr-xcontrib/reorg-tools/fix_all_open_status_queries.pl144
-rwxr-xr-xcontrib/reorg-tools/fixgroupqueries.pl123
-rwxr-xr-xcontrib/reorg-tools/fixqueries.pl136
-rwxr-xr-xcontrib/reorg-tools/migrate_crash_signatures.pl132
-rwxr-xr-xcontrib/reorg-tools/migrate_orange_bugs.pl158
-rwxr-xr-xcontrib/reorg-tools/move_dupes_to_invalid.pl101
-rwxr-xr-xcontrib/reorg-tools/move_flag_types.pl172
-rwxr-xr-xcontrib/reorg-tools/move_os.pl81
-rwxr-xr-xcontrib/reorg-tools/movebugs.pl182
-rwxr-xr-xcontrib/reorg-tools/movecomponent.pl145
-rwxr-xr-xcontrib/reorg-tools/reassign_open_bugs.pl87
-rwxr-xr-xcontrib/reorg-tools/reset_default_user.pl145
-rwxr-xr-xcontrib/reorg-tools/syncflags.pl88
-rwxr-xr-xcontrib/reorg-tools/syncmsandversions.pl122
-rwxr-xr-xcontrib/sanitizeme.pl214
-rwxr-xr-xcontrib/sendbugmail.pl31
-rwxr-xr-xcontrib/sendunsentbugmail.pl29
-rwxr-xr-xcontrib/verify-user.pl129
-rwxr-xr-xdescribecomponents.cgi7
-rwxr-xr-xdescribekeywords.cgi12
-rw-r--r--docs/en/xml/using.xml9
-rwxr-xr-xeditclassifications.cgi12
-rwxr-xr-xeditsettings.cgi1
-rwxr-xr-xeditusers.cgi36
-rwxr-xr-xenter_bug.cgi261
-rw-r--r--errors/401.html40
-rw-r--r--errors/403.html37
-rw-r--r--errors/404.html37
-rw-r--r--errors/500.html37
-rw-r--r--extensions/AntiSpam/Config.pm21
-rw-r--r--extensions/AntiSpam/Extension.pm349
-rw-r--r--extensions/AntiSpam/lib/Config.pm75
-rw-r--r--extensions/AntiSpam/template/en/default/admin/params/antispam.html.tmpl37
-rw-r--r--extensions/AntiSpam/template/en/default/hook/admin/admin-end_links_right.html.tmpl16
-rw-r--r--extensions/AntiSpam/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl16
-rw-r--r--extensions/AntiSpam/template/en/default/hook/global/user-error-errors.html.tmpl13
-rw-r--r--extensions/BMO/Config.pm48
-rw-r--r--extensions/BMO/Extension.pm1561
-rwxr-xr-xextensions/BMO/bin/bug_1022707.pl50
-rwxr-xr-xextensions/BMO/bin/migrate-github-pull-requests.pl90
-rw-r--r--extensions/BMO/lib/Constants.pm33
-rw-r--r--extensions/BMO/lib/Data.pm242
-rw-r--r--extensions/BMO/lib/FakeBug.pm42
-rw-r--r--extensions/BMO/lib/Reports/EmailQueue.pm84
-rw-r--r--extensions/BMO/lib/Reports/Groups.pm243
-rw-r--r--extensions/BMO/lib/Reports/ProductSecurity.pm67
-rw-r--r--extensions/BMO/lib/Reports/ReleaseTracking.pm409
-rw-r--r--extensions/BMO/lib/Reports/Triage.pm217
-rw-r--r--extensions/BMO/lib/Reports/UserActivity.pm327
-rw-r--r--extensions/BMO/lib/Util.pm90
-rw-r--r--extensions/BMO/lib/WebService.pm200
-rw-r--r--extensions/BMO/t/bug_format_comment.t84
-rw-r--r--extensions/BMO/template/en/default/account/create.html.tmpl178
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-automative.txt.tmpl52
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-creative.txt.tmpl39
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-dev-engagement-event.txt.tmpl84
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-doc.txt.tmpl20
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-employee-incident.txt.tmpl57
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-finance.txt.tmpl35
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-fxos-betaprogram.txt.tmpl24
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-fxos-feature.txt.tmpl24
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-fxos-mcts-waiver.txt.tmpl36
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-fxos-partner.txt.tmpl23
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-fxos-preload-app.txt.tmpl28
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-ipp.txt.tmpl30
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-legal.txt.tmpl39
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-mdn.txt.tmpl66
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-mobile-compat.txt.tmpl33
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-mozlist.txt.tmpl44
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-mozpr.txt.tmpl130
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-privacy-data.txt.tmpl30
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-recoverykey.txt.tmpl28
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-swag.txt.tmpl50
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-user-engagement.txt.tmpl36
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-automative.html.tmpl276
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-creative.html.tmpl259
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-dev-engagement-event.html.tmpl537
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-doc.html.tmpl222
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-employee-incident.html.tmpl11
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-finance.html.tmpl257
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-fxos-betaprogram.html.tmpl180
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-fxos-feature.html.tmpl181
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-fxos-mcts-waiver.html.tmpl208
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-fxos-partner.html.tmpl239
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-fxos-preload-app.html.tmpl185
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-ipp.html.tmpl183
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-itrequest.html.tmpl238
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-legal.html.tmpl226
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-mdn.html.tmpl279
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-mobile-compat.html.tmpl201
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-mozlist.html.tmpl177
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-mozpr.html.tmpl683
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-poweredby.html.tmpl87
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-presentation.html.tmpl11
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-privacy-data.html.tmpl219
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-recoverykey.html.tmpl70
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-swag.html.tmpl903
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-trademark.html.tmpl87
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-user-engagement.html.tmpl219
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-web-bounty.html.tmpl142
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-winqual.html.tmpl831
-rw-r--r--extensions/BMO/template/en/default/bug/create/created-fxos-betaprogram.html.tmpl30
-rw-r--r--extensions/BMO/template/en/default/bug/create/custom_forms.none.tmpl173
-rw-r--r--extensions/BMO/template/en/default/bug/create/user-message.html.tmpl49
-rw-r--r--extensions/BMO/template/en/default/email/bugmail.html.tmpl204
-rw-r--r--extensions/BMO/template/en/default/email/bugmail.txt.tmpl92
-rw-r--r--extensions/BMO/template/en/default/global/choose-product.html.tmpl228
-rw-r--r--extensions/BMO/template/en/default/global/prod-comp-search.html.tmpl43
-rw-r--r--extensions/BMO/template/en/default/global/redirect.html.tmpl25
-rw-r--r--extensions/BMO/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl13
-rw-r--r--extensions/BMO/template/en/default/hook/attachment/createformcontents-mimetypes.html.tmpl2
-rw-r--r--extensions/BMO/template/en/default/hook/attachment/createformcontents-patch_notes.html.tmpl1
-rw-r--r--extensions/BMO/template/en/default/hook/bug/comments-a_comment-end.html.tmpl19
-rw-r--r--extensions/BMO/template/en/default/hook/bug/comments-aftercomments.html.tmpl69
-rw-r--r--extensions/BMO/template/en/default/hook/bug/comments-comment_banner.html.tmpl13
-rw-r--r--extensions/BMO/template/en/default/hook/bug/create/create-after_custom_fields.html.tmpl30
-rw-r--r--extensions/BMO/template/en/default/hook/bug/create/create-custom_field.html.tmpl13
-rw-r--r--extensions/BMO/template/en/default/hook/bug/create/create-end.html.tmpl33
-rw-r--r--extensions/BMO/template/en/default/hook/bug/create/create-form.html.tmpl47
-rw-r--r--extensions/BMO/template/en/default/hook/bug/edit-after_importance.html.tmpl76
-rw-r--r--extensions/BMO/template/en/default/hook/bug/edit-before_restrict_visibility.html.tmpl25
-rw-r--r--extensions/BMO/template/en/default/hook/bug/field-help-end.none.tmpl96
-rw-r--r--extensions/BMO/template/en/default/hook/bug/process/header-title.html.tmpl9
-rw-r--r--extensions/BMO/template/en/default/hook/bug/show-header-end.html.tmpl18
-rw-r--r--extensions/BMO/template/en/default/hook/global/field-descs-end.none.tmpl12
-rw-r--r--extensions/BMO/template/en/default/hook/global/footer-end.html.tmpl11
-rw-r--r--extensions/BMO/template/en/default/hook/global/header-additional_header.html.tmpl54
-rw-r--r--extensions/BMO/template/en/default/hook/global/header-start.html.tmpl41
-rw-r--r--extensions/BMO/template/en/default/hook/global/messages-messages.html.tmpl5
-rw-r--r--extensions/BMO/template/en/default/hook/global/setting-descs-settings.none.tmpl14
-rw-r--r--extensions/BMO/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl17
-rw-r--r--extensions/BMO/template/en/default/hook/global/user-error-error_message.html.tmpl26
-rw-r--r--extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl40
-rw-r--r--extensions/BMO/template/en/default/hook/global/user-error.html.tmpl/auth_failure/permissions.html.tmpl29
-rw-r--r--extensions/BMO/template/en/default/hook/global/variables-end.none.tmpl3
-rw-r--r--extensions/BMO/template/en/default/hook/index-additional_links.html.tmpl18
-rw-r--r--extensions/BMO/template/en/default/hook/index-intro.html.tmpl2
-rw-r--r--extensions/BMO/template/en/default/hook/list/list-links.html.tmpl15
-rw-r--r--extensions/BMO/template/en/default/hook/list/table-before_table.html.tmpl9
-rw-r--r--extensions/BMO/template/en/default/hook/pages/fields-resolution.html.tmpl13
-rw-r--r--extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl59
-rw-r--r--extensions/BMO/template/en/default/list/list.microsummary.tmpl (renamed from template/en/default/global/help.html.tmpl)20
-rw-r--r--extensions/BMO/template/en/default/list/server-push.html.tmpl52
-rw-r--r--extensions/BMO/template/en/default/pages/bug-writing.html.tmpl11
-rw-r--r--extensions/BMO/template/en/default/pages/custom_forms.html.tmpl40
-rw-r--r--extensions/BMO/template/en/default/pages/email_queue.html.tmpl67
-rw-r--r--extensions/BMO/template/en/default/pages/etiquette.html.tmpl146
-rw-r--r--extensions/BMO/template/en/default/pages/get_help.html.tmpl42
-rw-r--r--extensions/BMO/template/en/default/pages/get_permissions.html.tmpl44
-rw-r--r--extensions/BMO/template/en/default/pages/group_admins.html.tmpl54
-rw-r--r--extensions/BMO/template/en/default/pages/group_members.html.tmpl97
-rw-r--r--extensions/BMO/template/en/default/pages/group_members.json.tmpl32
-rw-r--r--extensions/BMO/template/en/default/pages/group_membership.html.tmpl75
-rw-r--r--extensions/BMO/template/en/default/pages/group_membership.txt.tmpl16
-rw-r--r--extensions/BMO/template/en/default/pages/product_security_report.html.tmpl60
-rw-r--r--extensions/BMO/template/en/default/pages/query_database.html.tmpl47
-rw-r--r--extensions/BMO/template/en/default/pages/release_tracking_report.html.tmpl103
-rw-r--r--extensions/BMO/template/en/default/pages/triage_reports.html.tmpl199
-rw-r--r--extensions/BMO/template/en/default/pages/upgrade-3.6.html.tmpl304
-rw-r--r--extensions/BMO/template/en/default/pages/user_activity.html.tmpl230
-rw-r--r--extensions/BMO/template/en/default/search/search-plugin.xml.tmpl17
-rw-r--r--extensions/BMO/web/core.pngbin0 -> 7497 bytes
-rw-r--r--extensions/BMO/web/images/advanced.pngbin0 -> 720 bytes
-rw-r--r--extensions/BMO/web/images/background.pngbin0 -> 1695 bytes
-rw-r--r--extensions/BMO/web/images/bugzilla.pngbin0 -> 1242 bytes
-rw-r--r--extensions/BMO/web/images/creative.pngbin0 -> 245022 bytes
-rw-r--r--extensions/BMO/web/images/favicon.icobin0 -> 1150 bytes
-rw-r--r--extensions/BMO/web/images/groups/bugzilla-approvers.pngbin0 -> 829 bytes
-rw-r--r--extensions/BMO/web/images/groups/calendar-drivers.pngbin0 -> 744 bytes
-rw-r--r--extensions/BMO/web/images/guided.pngbin0 -> 1045 bytes
-rw-r--r--extensions/BMO/web/images/mozchomp.gifbin0 -> 89485 bytes
-rw-r--r--extensions/BMO/web/images/mozilla-tab.pngbin0 -> 7535 bytes
-rw-r--r--extensions/BMO/web/images/notice.pngbin0 -> 6654 bytes
-rw-r--r--extensions/BMO/web/images/presshat.pngbin0 -> 23450 bytes
-rw-r--r--extensions/BMO/web/images/sign_warning.pngbin0 -> 1776 bytes
-rw-r--r--extensions/BMO/web/images/stop-sign.gifbin0 -> 3227 bytes
-rw-r--r--extensions/BMO/web/images/throbber.gifbin0 -> 723 bytes
-rw-r--r--extensions/BMO/web/images/user-engagement.pngbin0 -> 25818 bytes
-rw-r--r--extensions/BMO/web/js/edit_bug.js41
-rw-r--r--extensions/BMO/web/js/edituser_menu.js33
-rw-r--r--extensions/BMO/web/js/form_validate.js41
-rw-r--r--extensions/BMO/web/js/release_tracking_report.js203
-rw-r--r--extensions/BMO/web/js/sorttable.js709
-rw-r--r--extensions/BMO/web/js/swag.js60
-rw-r--r--extensions/BMO/web/js/triage_reports.js83
-rw-r--r--extensions/BMO/web/js/webtrends.js213
-rw-r--r--extensions/BMO/web/producticons/component.pngbin0 -> 7497 bytes
-rw-r--r--extensions/BMO/web/producticons/dino.pngbin0 -> 3375 bytes
-rw-r--r--extensions/BMO/web/producticons/firefox.pngbin0 -> 7720 bytes
-rw-r--r--extensions/BMO/web/producticons/firefox_android.pngbin0 -> 15997 bytes
-rw-r--r--extensions/BMO/web/producticons/firefox_os.pngbin0 -> 18928 bytes
-rw-r--r--extensions/BMO/web/producticons/input.pngbin0 -> 8333 bytes
-rw-r--r--extensions/BMO/web/producticons/localization.pngbin0 -> 6914 bytes
-rw-r--r--extensions/BMO/web/producticons/marketplace.pngbin0 -> 7412 bytes
-rw-r--r--extensions/BMO/web/producticons/other.pngbin0 -> 6654 bytes
-rw-r--r--extensions/BMO/web/producticons/seamonkey.pngbin0 -> 5255 bytes
-rw-r--r--extensions/BMO/web/producticons/sync.pngbin0 -> 8896 bytes
-rw-r--r--extensions/BMO/web/producticons/thunderbird.pngbin0 -> 9939 bytes
-rw-r--r--extensions/BMO/web/producticons/webmaker.pngbin0 -> 59095 bytes
-rw-r--r--extensions/BMO/web/styles/choose_product.css16
-rw-r--r--extensions/BMO/web/styles/create_account.css62
-rw-r--r--extensions/BMO/web/styles/edit_bug.css49
-rw-r--r--extensions/BMO/web/styles/reports.css75
-rw-r--r--extensions/BMO/web/styles/triage_reports.css23
-rw-r--r--extensions/BMO/web/yui-history-iframe.txt (renamed from extensions/BmpConvert/disabled)0
-rw-r--r--extensions/Bitly/Config.pm45
-rw-r--r--extensions/Bitly/Extension.pm31
-rw-r--r--extensions/Bitly/lib/WebService.pm141
-rw-r--r--extensions/Bitly/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl13
-rw-r--r--extensions/Bitly/template/en/default/hook/global/header-start.html.tmpl13
-rw-r--r--extensions/Bitly/template/en/default/hook/global/user-error-errors.html.tmpl17
-rw-r--r--extensions/Bitly/template/en/default/hook/list/list-links.html.tmpl24
-rw-r--r--extensions/Bitly/web/js/bitly.js100
-rw-r--r--extensions/Bitly/web/styles/bitly.css23
-rw-r--r--extensions/BugmailFilter/Config.pm15
-rw-r--r--extensions/BugmailFilter/Extension.pm478
-rw-r--r--extensions/BugmailFilter/lib/Constants.pm120
-rw-r--r--extensions/BugmailFilter/lib/FakeField.pm57
-rw-r--r--extensions/BugmailFilter/lib/Filter.pm212
-rw-r--r--extensions/BugmailFilter/template/en/default/account/prefs/bugmail_filter.html.tmpl392
-rw-r--r--extensions/BugmailFilter/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl14
-rw-r--r--extensions/BugmailFilter/template/en/default/hook/global/user-error-errors.html.tmpl22
-rw-r--r--extensions/BugmailFilter/web/js/bugmail-filter.js60
-rw-r--r--extensions/BugmailFilter/web/style/bugmail-filter.css44
-rw-r--r--extensions/BzAPI/Config.pm16
-rw-r--r--extensions/BzAPI/Extension.pm267
-rwxr-xr-xextensions/BzAPI/bin/rest.cgi34
-rw-r--r--extensions/BzAPI/lib/Constants.pm155
-rw-r--r--extensions/BzAPI/lib/Resources/Bug.pm867
-rw-r--r--extensions/BzAPI/lib/Resources/Bugzilla.pm138
-rw-r--r--extensions/BzAPI/lib/Resources/User.pm79
-rw-r--r--extensions/BzAPI/lib/Util.pm459
-rw-r--r--extensions/BzAPI/template/en/default/config.json.tmpl302
-rw-r--r--extensions/BzAPI/template/en/default/hook/global/user-error-errors.html.tmpl11
-rw-r--r--extensions/ComponentWatching/Config.pm12
-rw-r--r--extensions/ComponentWatching/Extension.pm626
-rw-r--r--extensions/ComponentWatching/template/en/default/account/prefs/component_watch.html.tmpl261
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/account/prefs/email-relationships.html.tmpl10
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl14
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/admin/components/edit-common-rows.html.tmpl20
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/admin/components/list-before_table.html.tmpl17
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl23
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/global/messages-component_updated_fields.html.tmpl15
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/global/reason-descs-end.none.tmpl10
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/global/user-error-errors.html.tmpl17
-rw-r--r--extensions/ContributorEngagement/Config.pm19
-rw-r--r--extensions/ContributorEngagement/Extension.pm123
-rw-r--r--extensions/ContributorEngagement/lib/Constants.pm31
-rw-r--r--extensions/ContributorEngagement/template/en/default/contributor/email.txt.tmpl49
-rw-r--r--extensions/EditComments/Config.pm19
-rw-r--r--extensions/EditComments/Extension.pm270
-rw-r--r--extensions/EditComments/lib/WebService.pm170
-rw-r--r--extensions/EditComments/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl13
-rw-r--r--extensions/EditComments/template/en/default/hook/bug/comments-a_comment-end.html.tmpl49
-rw-r--r--extensions/EditComments/template/en/default/hook/bug/show-header-end.html.tmpl12
-rw-r--r--extensions/EditComments/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl11
-rw-r--r--extensions/EditComments/template/en/default/hook/global/user-error-errors.html.tmpl12
-rw-r--r--extensions/EditComments/template/en/default/pages/editcomments.html.tmpl122
-rw-r--r--extensions/EditComments/web/js/editcomments.js90
-rw-r--r--extensions/EditComments/web/styles/editcomments.css10
-rw-r--r--extensions/EditTable/Config.pm15
-rw-r--r--extensions/EditTable/Extension.pm180
-rw-r--r--extensions/EditTable/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl11
-rw-r--r--extensions/EditTable/template/en/default/hook/global/user-error-errors.html.tmpl17
-rw-r--r--extensions/EditTable/template/en/default/pages/edit_table.html.tmpl43
-rw-r--r--extensions/EditTable/web/js/edit_table.js131
-rw-r--r--extensions/EditTable/web/styles/edit_table.css39
-rw-r--r--extensions/Ember/Config.pm19
-rw-r--r--extensions/Ember/Extension.pm22
-rw-r--r--extensions/Ember/disabled (renamed from extensions/Voting/disabled)0
-rw-r--r--extensions/Ember/lib/FakeBug.pm78
-rw-r--r--extensions/Ember/lib/WebService.pm995
-rw-r--r--extensions/Ember/template/en/default/hook/global/user-error-errors.html.tmpl4
-rw-r--r--extensions/Example/Extension.pm217
-rw-r--r--extensions/FlagDefaultRequestee/Config.pm17
-rw-r--r--extensions/FlagDefaultRequestee/Extension.pm173
-rw-r--r--extensions/FlagDefaultRequestee/lib/Constants.pm25
-rw-r--r--extensions/FlagDefaultRequestee/template/en/default/flag/default_requestees.html.tmpl105
-rw-r--r--extensions/FlagDefaultRequestee/template/en/default/hook/admin/flag-type/edit-rows.html.tmpl21
-rw-r--r--extensions/FlagDefaultRequestee/template/en/default/hook/attachment/create-end.html.tmpl9
-rw-r--r--extensions/FlagDefaultRequestee/template/en/default/hook/attachment/edit-end.html.tmpl9
-rw-r--r--extensions/FlagDefaultRequestee/template/en/default/hook/bug/create/create-form.html.tmpl9
-rw-r--r--extensions/FlagDefaultRequestee/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl9
-rw-r--r--extensions/FlagDefaultRequestee/template/en/default/hook/global/messages-flag_type_updated_fields.html.tmpl15
-rw-r--r--extensions/FlagDefaultRequestee/template/en/default/hook/global/user-error-errors.html.tmpl13
-rw-r--r--extensions/FlagTypeComment/Config.pm29
-rw-r--r--extensions/FlagTypeComment/Extension.pm200
-rw-r--r--extensions/FlagTypeComment/lib/Constants.pm50
-rw-r--r--extensions/FlagTypeComment/template/en/default/flag/type_comment.html.tmpl54
-rw-r--r--extensions/FlagTypeComment/template/en/default/hook/admin/flag-type/edit-rows.html.tmpl45
-rw-r--r--extensions/FlagTypeComment/template/en/default/hook/attachment/create-end.html.tmpl23
-rw-r--r--extensions/FlagTypeComment/template/en/default/hook/attachment/edit-end.html.tmpl23
-rw-r--r--extensions/FlagTypeComment/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl23
-rw-r--r--extensions/Gravatar/Config.pm16
-rw-r--r--extensions/Gravatar/Extension.pm43
-rw-r--r--extensions/Gravatar/template/en/default/hook/bug/comments-user-image.html.tmpl19
-rw-r--r--extensions/Gravatar/template/en/default/hook/bug/show-header-end.html.tmpl11
-rw-r--r--extensions/Gravatar/template/en/default/hook/global/setting-descs-settings.none.tmpl12
-rw-r--r--extensions/Gravatar/web/default.jpgbin0 -> 1163 bytes
-rw-r--r--extensions/GuidedBugEntry/Config.pm19
-rw-r--r--extensions/GuidedBugEntry/Extension.pm118
-rw-r--r--extensions/GuidedBugEntry/template/en/default/bug/create/comment-guided.txt.tmpl25
-rw-r--r--extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl529
-rw-r--r--extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl50
-rw-r--r--extensions/GuidedBugEntry/template/en/default/pages/guided_products.js.tmpl26
-rw-r--r--extensions/GuidedBugEntry/web/images/advanced.pngbin0 -> 720 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/help.pngbin0 -> 786 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/input.pngbin0 -> 5545 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/message.pngbin0 -> 1497 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/sumo.pngbin0 -> 3517 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/support.pngbin0 -> 2409 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/throbber.gifbin0 -> 723 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/warning.pngbin0 -> 1428 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/webbug.pngbin0 -> 2053 bytes
-rw-r--r--extensions/GuidedBugEntry/web/js/guided.js927
-rw-r--r--extensions/GuidedBugEntry/web/js/products.js118
-rw-r--r--extensions/GuidedBugEntry/web/style/guided.css237
-rw-r--r--extensions/GuidedBugEntry/web/yui-history-iframe.txt0
-rw-r--r--extensions/InlineHistory/Config.pm13
-rw-r--r--extensions/InlineHistory/Extension.pm215
-rw-r--r--extensions/InlineHistory/README10
-rw-r--r--extensions/InlineHistory/template/en/default/hook/bug/comments-aftercomments.html.tmpl160
-rw-r--r--extensions/InlineHistory/template/en/default/hook/bug/comments-comment_banner.html.tmpl13
-rw-r--r--extensions/InlineHistory/template/en/default/hook/bug/show-header-end.html.tmpl12
-rw-r--r--extensions/InlineHistory/template/en/default/hook/global/setting-descs-settings.none.tmpl11
-rw-r--r--extensions/InlineHistory/web/inline-history.js417
-rw-r--r--extensions/InlineHistory/web/style.css35
-rw-r--r--extensions/LastResolved/Config.pm20
-rw-r--r--extensions/LastResolved/Extension.pm113
-rw-r--r--extensions/LastResolved/template/en/default/hook/bug/edit-custom_field.html.tmpl12
-rw-r--r--extensions/LastResolved/template/en/default/hook/global/field-descs-end.none.tmpl11
-rw-r--r--extensions/LastResolved/template/en/default/hook/list/edit-multiple-custom_field.html.tmpl11
-rw-r--r--extensions/LimitedEmail/Config.pm22
-rw-r--r--extensions/LimitedEmail/Extension.pm62
-rw-r--r--extensions/LimitedEmail/disabled0
-rw-r--r--extensions/MozProjectReview/Config.pm19
-rw-r--r--extensions/MozProjectReview/Extension.pm284
-rw-r--r--extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-finance.txt.tmpl30
-rw-r--r--extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-legal.txt.tmpl51
-rw-r--r--extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-policy.txt.tmpl17
-rw-r--r--extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-tech.txt.tmpl12
-rw-r--r--extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-vendor.txt.tmpl16
-rw-r--r--extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-sec-review.txt.tmpl20
-rw-r--r--extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review.txt.tmpl33
-rw-r--r--extensions/MozProjectReview/template/en/default/bug/create/create-moz-project-review.html.tmpl711
-rw-r--r--extensions/MozProjectReview/template/en/default/hook/global/messages-messages.html.tmpl13
-rw-r--r--extensions/MozProjectReview/web/js/moz_project_review.js267
-rw-r--r--extensions/MozProjectReview/web/style/moz_project_review.css48
-rw-r--r--extensions/MyDashboard/Config.pm14
-rw-r--r--extensions/MyDashboard/Extension.pm126
-rw-r--r--extensions/MyDashboard/lib/Queries.pm313
-rw-r--r--extensions/MyDashboard/lib/TimeAgo.pm179
-rw-r--r--extensions/MyDashboard/lib/Util.pm50
-rw-r--r--extensions/MyDashboard/lib/WebService.pm145
-rw-r--r--extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-header.html.tmpl11
-rw-r--r--extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-row.html.tmpl15
-rw-r--r--extensions/MyDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl12
-rw-r--r--extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl156
-rw-r--r--extensions/MyDashboard/web/js/flags.js228
-rw-r--r--extensions/MyDashboard/web/js/query.js265
-rw-r--r--extensions/MyDashboard/web/styles/mydashboard.css74
-rw-r--r--extensions/Needinfo/Config.pm18
-rw-r--r--extensions/Needinfo/Extension.pm180
-rw-r--r--extensions/Needinfo/template/en/default/bug/needinfo.html.tmpl199
-rw-r--r--extensions/Needinfo/template/en/default/hook/attachment/create-form_before_submit.html.tmpl16
-rw-r--r--extensions/Needinfo/template/en/default/hook/attachment/edit-after_comment_textarea.html.tmpl25
-rw-r--r--extensions/Needinfo/template/en/default/hook/bug/edit-after_comment_commit_button.html.tmpl11
-rw-r--r--extensions/Needinfo/template/en/default/hook/global/header-start.html.tmpl12
-rw-r--r--extensions/Needinfo/template/en/default/hook/global/user-error-errors.html.tmpl13
-rw-r--r--extensions/Needinfo/template/en/default/hook/request/email-after_summary.txt.tmpl13
-rw-r--r--extensions/Needinfo/web/styles/needinfo.css10
-rw-r--r--extensions/OpenGraph/Config.pm16
-rw-r--r--extensions/OpenGraph/Extension.pm16
-rw-r--r--extensions/OpenGraph/template/en/default/hook/global/header-start.html.tmpl13
-rw-r--r--extensions/OpenGraph/web/bugzilla.pngbin0 -> 17036 bytes
-rw-r--r--extensions/OrangeFactor/Config.pm13
-rw-r--r--extensions/OrangeFactor/Extension.pm43
-rw-r--r--extensions/OrangeFactor/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl26
-rw-r--r--extensions/OrangeFactor/template/en/default/hook/bug/show-header-end.html.tmpl17
-rw-r--r--extensions/OrangeFactor/template/en/default/hook/global/setting-descs-settings.none.tmpl11
-rw-r--r--extensions/OrangeFactor/web/js/AUTHORS.processing.js35
-rw-r--r--extensions/OrangeFactor/web/js/LICENSE.processing.js22
-rw-r--r--extensions/OrangeFactor/web/js/LICENSE.sparklines.js20
-rw-r--r--extensions/OrangeFactor/web/js/orange_factor.js91
-rw-r--r--extensions/OrangeFactor/web/js/sparklines.min.js133
-rw-r--r--extensions/OrangeFactor/web/style/orangefactor.css13
-rw-r--r--extensions/Persona/Config.pm29
-rw-r--r--extensions/Persona/Extension.pm73
-rw-r--r--extensions/Persona/TODO19
-rw-r--r--extensions/Persona/lib/Config.pm41
-rw-r--r--extensions/Persona/lib/Login.pm127
-rw-r--r--extensions/Persona/template/en/default/admin/params/browserid.html.tmpl22
-rw-r--r--extensions/Persona/template/en/default/admin/params/persona.html.tmpl24
-rw-r--r--extensions/Persona/template/en/default/hook/account/auth/login-additional_methods.html.tmpl6
-rw-r--r--extensions/Persona/template/en/default/hook/account/auth/login-small-additional_methods.html.tmpl17
-rw-r--r--extensions/Persona/template/en/default/hook/account/create-additional_methods.html.tmpl13
-rw-r--r--extensions/Persona/template/en/default/hook/global/header-additional_header.html.tmpl87
-rw-r--r--extensions/Persona/template/en/default/hook/global/user-error-errors.html.tmpl12
-rw-r--r--extensions/Persona/web/images/persona_sign_in.pngbin0 -> 3684 bytes
-rw-r--r--extensions/Persona/web/images/sign_in.pngbin0 -> 1993 bytes
-rw-r--r--extensions/ProdCompSearch/Config.pm15
-rw-r--r--extensions/ProdCompSearch/Extension.pm21
-rw-r--r--extensions/ProdCompSearch/lib/WebService.pm121
-rw-r--r--extensions/ProdCompSearch/template/en/default/pages/prodcompsearch.html.tmpl25
-rw-r--r--extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl40
-rw-r--r--extensions/ProdCompSearch/web/images/throbber.gifbin0 -> 723 bytes
-rw-r--r--extensions/ProdCompSearch/web/js/prod_comp_search.js144
-rw-r--r--extensions/ProdCompSearch/web/styles/prod_comp_search.css27
-rw-r--r--extensions/ProductDashboard/Config.pm14
-rw-r--r--extensions/ProductDashboard/Extension.pm200
-rw-r--r--extensions/ProductDashboard/lib/Queries.pm476
-rw-r--r--extensions/ProductDashboard/lib/Util.pm95
-rw-r--r--extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl9
-rw-r--r--extensions/ProductDashboard/template/en/default/hook/global/user-error-errors.html.tmpl12
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl237
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl146
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl34
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl38
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl87
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl27
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl122
-rw-r--r--extensions/ProductDashboard/web/images/spacer.gifbin0 -> 43 bytes
-rw-r--r--extensions/ProductDashboard/web/js/components.js90
-rw-r--r--extensions/ProductDashboard/web/js/duplicates.js28
-rw-r--r--extensions/ProductDashboard/web/js/popularity.js28
-rw-r--r--extensions/ProductDashboard/web/js/recents.js32
-rw-r--r--extensions/ProductDashboard/web/js/roadmap.js24
-rw-r--r--extensions/ProductDashboard/web/js/summary.js45
-rw-r--r--extensions/ProductDashboard/web/styles/productdashboard.css45
-rw-r--r--extensions/Profanivore/Config.pm40
-rw-r--r--extensions/Profanivore/Extension.pm169
-rw-r--r--extensions/Profanivore/README14
-rw-r--r--extensions/Push/Config.pm56
-rw-r--r--extensions/Push/Extension.pm658
-rwxr-xr-xextensions/Push/bin/bugzilla-pushd.pl54
-rwxr-xr-xextensions/Push/bin/nagios_push_checker.pl54
-rw-r--r--extensions/Push/lib/Admin.pm122
-rw-r--r--extensions/Push/lib/BacklogMessage.pm150
-rw-r--r--extensions/Push/lib/BacklogQueue.pm127
-rw-r--r--extensions/Push/lib/Backoff.pm110
-rw-r--r--extensions/Push/lib/Config.pm215
-rw-r--r--extensions/Push/lib/Connector.disabled/AMQP.pm230
-rw-r--r--extensions/Push/lib/Connector.disabled/ServiceNow.pm434
-rw-r--r--extensions/Push/lib/Connector/Base.pm106
-rw-r--r--extensions/Push/lib/Connector/File.pm68
-rw-r--r--extensions/Push/lib/Connector/ReviewBoard.pm187
-rw-r--r--extensions/Push/lib/Connector/ReviewBoard/Client.pm67
-rw-r--r--extensions/Push/lib/Connector/ReviewBoard/Resource.pm38
-rw-r--r--extensions/Push/lib/Connector/ReviewBoard/ReviewRequest.pm28
-rw-r--r--extensions/Push/lib/Connector/TCL.pm352
-rw-r--r--extensions/Push/lib/Connectors.pm116
-rw-r--r--extensions/Push/lib/Constants.pm41
-rw-r--r--extensions/Push/lib/Daemon.pm96
-rw-r--r--extensions/Push/lib/Log.pm45
-rw-r--r--extensions/Push/lib/LogEntry.pm71
-rw-r--r--extensions/Push/lib/Logger.pm70
-rw-r--r--extensions/Push/lib/Message.pm104
-rw-r--r--extensions/Push/lib/Option.pm66
-rw-r--r--extensions/Push/lib/Push.pm264
-rw-r--r--extensions/Push/lib/Queue.pm72
-rw-r--r--extensions/Push/lib/Serialise.pm318
-rw-r--r--extensions/Push/lib/Util.pm162
-rw-r--r--extensions/Push/t/ReviewBoard.t224
-rw-r--r--extensions/Push/template/en/default/hook/admin/admin-end_links_right.html.tmpl18
-rw-r--r--extensions/Push/template/en/default/hook/global/code-error-errors.html.tmpl25
-rw-r--r--extensions/Push/template/en/default/hook/global/messages-messages.html.tmpl16
-rw-r--r--extensions/Push/template/en/default/hook/global/user-error-errors.html.tmpl11
-rw-r--r--extensions/Push/template/en/default/pages/push_config.html.tmpl134
-rw-r--r--extensions/Push/template/en/default/pages/push_log.html.tmpl45
-rw-r--r--extensions/Push/template/en/default/pages/push_queues.html.tmpl102
-rw-r--r--extensions/Push/template/en/default/pages/push_queues_view.html.tmpl80
-rw-r--r--extensions/Push/template/en/default/setup/strings.txt.pl11
-rw-r--r--extensions/Push/web/admin.css71
-rw-r--r--extensions/Push/web/admin.js37
-rw-r--r--extensions/REMO/Config.pm34
-rw-r--r--extensions/REMO/Extension.pm309
-rw-r--r--extensions/REMO/template/en/default/bug/create/comment-mozreps.txt.tmpl95
-rw-r--r--extensions/REMO/template/en/default/bug/create/comment-remo-budget.txt.tmpl57
-rw-r--r--extensions/REMO/template/en/default/bug/create/comment-remo-it.txt.tmpl79
-rw-r--r--extensions/REMO/template/en/default/bug/create/comment-remo-swag.txt.tmpl73
-rw-r--r--extensions/REMO/template/en/default/bug/create/create-mozreps.html.tmpl247
-rw-r--r--extensions/REMO/template/en/default/bug/create/create-remo-budget.html.tmpl295
-rw-r--r--extensions/REMO/template/en/default/bug/create/create-remo-it.html.tmpl294
-rw-r--r--extensions/REMO/template/en/default/bug/create/create-remo-swag.html.tmpl326
-rw-r--r--extensions/REMO/template/en/default/bug/create/create-remo-swag.xml.tmpl104
-rw-r--r--extensions/REMO/template/en/default/bug/create/created-mozreps.html.tmpl38
-rw-r--r--extensions/REMO/template/en/default/bug/create/created-remo-budget.html.tmpl27
-rw-r--r--extensions/REMO/template/en/default/hook/global/user-error-errors.html.tmpl40
-rw-r--r--extensions/REMO/template/en/default/pages/comment-remo-form-payment.txt.tmpl37
-rw-r--r--extensions/REMO/template/en/default/pages/remo-form-payment.html.tmpl243
-rw-r--r--extensions/REMO/web/js/form_validate.js21
-rw-r--r--extensions/REMO/web/js/swag.js60
-rw-r--r--extensions/REMO/web/styles/moz_reps.css53
-rw-r--r--extensions/RequestNagger/Config.pm15
-rw-r--r--extensions/RequestNagger/Extension.pm349
-rwxr-xr-xextensions/RequestNagger/bin/send-request-nags.pl209
-rw-r--r--extensions/RequestNagger/lib/Bug.pm30
-rw-r--r--extensions/RequestNagger/lib/Constants.pm116
-rw-r--r--extensions/RequestNagger/lib/TimeAgo.pm186
-rw-r--r--extensions/RequestNagger/template/en/default/account/prefs/request_nagging.html.tmpl56
-rw-r--r--extensions/RequestNagger/template/en/default/email/request_nagging-requestee-header.txt.tmpl19
-rw-r--r--extensions/RequestNagger/template/en/default/email/request_nagging-requestee.html.tmpl90
-rw-r--r--extensions/RequestNagger/template/en/default/email/request_nagging-requestee.txt.tmpl45
-rw-r--r--extensions/RequestNagger/template/en/default/email/request_nagging-watching-header.txt.tmpl15
-rw-r--r--extensions/RequestNagger/template/en/default/email/request_nagging-watching.html.tmpl104
-rw-r--r--extensions/RequestNagger/template/en/default/email/request_nagging-watching.txt.tmpl47
-rw-r--r--extensions/RequestNagger/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl14
-rw-r--r--extensions/RequestNagger/template/en/default/hook/admin/products/edit-common-rows.html.tmpl16
-rw-r--r--extensions/RequestNagger/template/en/default/hook/admin/products/updated-changes.html.tmpl14
-rw-r--r--extensions/RequestNagger/template/en/default/hook/global/setting-descs-settings.none.tmpl11
-rw-r--r--extensions/RequestNagger/template/en/default/hook/global/user-error-errors.html.tmpl25
-rw-r--r--extensions/RequestNagger/template/en/default/pages/request_defer.html.tmpl101
-rw-r--r--extensions/RequestNagger/web/js/requestnagger.js13
-rw-r--r--extensions/RequestNagger/web/style/requestnagger.css42
-rw-r--r--extensions/RestrictComments/Config.pm16
-rw-r--r--extensions/RestrictComments/Extension.pm95
-rw-r--r--extensions/RestrictComments/lib/Config.pm47
-rw-r--r--extensions/RestrictComments/template/en/default/admin/params/restrictcomments.html.tmpl23
-rw-r--r--extensions/RestrictComments/template/en/default/hook/bug/edit-after_comment_commit_button.html.tmpl26
-rw-r--r--extensions/RestrictComments/template/en/default/pages/restrict_comments_guidelines.html.tmpl62
-rw-r--r--extensions/Review/Config.pm15
-rw-r--r--extensions/Review/Extension.pm939
-rwxr-xr-xextensions/Review/bin/migrate_mentor_from_whiteboard.pl229
-rwxr-xr-xextensions/Review/bin/review_requests_rebuild.pl29
-rw-r--r--extensions/Review/lib/FlagStateActivity.pm122
-rw-r--r--extensions/Review/lib/Util.pm84
-rw-r--r--extensions/Review/lib/WebService.pm488
-rw-r--r--extensions/Review/template/en/default/hook/admin/components/edit-common-rows.html.tmpl23
-rw-r--r--extensions/Review/template/en/default/hook/admin/products/edit-common-rows.html.tmpl28
-rw-r--r--extensions/Review/template/en/default/hook/admin/products/updated-changes.html.tmpl19
-rw-r--r--extensions/Review/template/en/default/hook/attachment/create-end.html.tmpl20
-rw-r--r--extensions/Review/template/en/default/hook/attachment/edit-end.html.tmpl15
-rw-r--r--extensions/Review/template/en/default/hook/bug/create/create-after_custom_fields.html.tmpl20
-rw-r--r--extensions/Review/template/en/default/hook/bug/create/create-end.html.tmpl16
-rw-r--r--extensions/Review/template/en/default/hook/bug/edit-after_people.html.tmpl53
-rw-r--r--extensions/Review/template/en/default/hook/bug/show-bug_end.xml.tmpl12
-rw-r--r--extensions/Review/template/en/default/hook/flag/list-requestee.html.tmpl17
-rw-r--r--extensions/Review/template/en/default/hook/global/header-message.html.tmpl23
-rw-r--r--extensions/Review/template/en/default/hook/global/header-start.html.tmpl91
-rw-r--r--extensions/Review/template/en/default/hook/global/messages-component_updated_fields.html.tmpl11
-rw-r--r--extensions/Review/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl11
-rw-r--r--extensions/Review/template/en/default/hook/global/user-error-errors.html.tmpl26
-rw-r--r--extensions/Review/template/en/default/hook/reports/menu-end.html.tmpl16
-rw-r--r--extensions/Review/template/en/default/pages/review_history.html.tmpl62
-rw-r--r--extensions/Review/template/en/default/pages/review_requests_rebuild.html.tmpl23
-rw-r--r--extensions/Review/template/en/default/pages/review_suggestions.html.tmpl76
-rw-r--r--extensions/Review/web/js/moment.min.js6
-rw-r--r--extensions/Review/web/js/review.js210
-rw-r--r--extensions/Review/web/js/review_history.js384
-rw-r--r--extensions/Review/web/styles/badge.css16
-rw-r--r--extensions/Review/web/styles/reports.css41
-rw-r--r--extensions/Review/web/styles/review.css10
-rw-r--r--extensions/Review/web/styles/review_history.css10
-rw-r--r--extensions/SecureMail/Config.pm49
-rw-r--r--extensions/SecureMail/Extension.pm659
-rw-r--r--extensions/SecureMail/README8
-rw-r--r--extensions/SecureMail/template/en/default/account/email/encryption-required.txt.tmpl15
-rw-r--r--extensions/SecureMail/template/en/default/account/email/securemail-test.txt.tmpl23
-rw-r--r--extensions/SecureMail/template/en/default/account/prefs/securemail.html.tmpl40
-rw-r--r--extensions/SecureMail/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl28
-rw-r--r--extensions/SecureMail/template/en/default/hook/admin/groups/create-field.html.tmpl25
-rw-r--r--extensions/SecureMail/template/en/default/hook/admin/groups/edit-field.html.tmpl27
-rw-r--r--extensions/SecureMail/template/en/default/hook/global/user-error-errors.html.tmpl27
-rw-r--r--extensions/SecureMail/template/en/default/pages/securemail/help.html.tmpl130
-rw-r--r--extensions/ShadowBugs/Config.pm15
-rw-r--r--extensions/ShadowBugs/Extension.pm99
-rw-r--r--extensions/ShadowBugs/disabled0
-rw-r--r--extensions/ShadowBugs/template/en/default/hook/bug/comments-aftercomments.html.tmpl70
-rw-r--r--extensions/ShadowBugs/template/en/default/hook/bug/edit-after_comment_textarea.html.tmpl13
-rw-r--r--extensions/ShadowBugs/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl27
-rw-r--r--extensions/ShadowBugs/template/en/default/hook/bug/edit-custom_field.html.tmpl9
-rw-r--r--extensions/ShadowBugs/template/en/default/hook/bug/show-header-end.html.tmpl12
-rw-r--r--extensions/ShadowBugs/template/en/default/hook/global/user-error-errors.html.tmpl14
-rw-r--r--extensions/ShadowBugs/web/shadow-bugs.js51
-rw-r--r--extensions/ShadowBugs/web/style.css10
-rw-r--r--extensions/SiteMapIndex/Config.pm36
-rw-r--r--extensions/SiteMapIndex/Extension.pm157
-rw-r--r--extensions/SiteMapIndex/lib/Constants.pm47
-rw-r--r--extensions/SiteMapIndex/lib/Util.pm205
-rw-r--r--extensions/SiteMapIndex/robots.txt10
-rw-r--r--extensions/SiteMapIndex/template/en/default/hook/global/header-additional_header.html.tmpl23
-rw-r--r--extensions/SiteMapIndex/template/en/default/hook/global/messages-messages.html.tmpl37
-rw-r--r--extensions/Splinter/Config.pm5
-rw-r--r--extensions/Splinter/Extension.pm148
-rw-r--r--extensions/Splinter/lib/Config.pm46
-rw-r--r--extensions/Splinter/lib/Util.pm163
-rw-r--r--extensions/Splinter/template/en/default/admin/params/splinter.html.tmpl38
-rw-r--r--extensions/Splinter/template/en/default/hook/attachment/edit-action.html.tmpl25
-rw-r--r--extensions/Splinter/template/en/default/hook/attachment/list-action.html.tmpl25
-rw-r--r--extensions/Splinter/template/en/default/hook/global/user-error-errors.html.tmpl5
-rw-r--r--extensions/Splinter/template/en/default/hook/request/email-after_summary.txt.tmpl9
-rw-r--r--extensions/Splinter/template/en/default/hook/request/queue-after_column.html.tmpl4
-rw-r--r--extensions/Splinter/template/en/default/pages/splinter.html.tmpl282
-rw-r--r--extensions/Splinter/template/en/default/pages/splinter/help.html.tmpl153
-rw-r--r--extensions/Splinter/web/splinter.css428
-rw-r--r--extensions/Splinter/web/splinter.js2700
-rw-r--r--extensions/TagNewUsers/Config.pm15
-rw-r--r--extensions/TagNewUsers/Extension.pm263
-rw-r--r--extensions/TagNewUsers/template/en/default/hook/bug/comments-user.html.tmpl26
-rw-r--r--extensions/TagNewUsers/template/en/default/hook/bug/show-header-end.html.tmpl9
-rw-r--r--extensions/TagNewUsers/web/style.css10
-rw-r--r--extensions/TrackingFlags/Config.pm24
-rw-r--r--extensions/TrackingFlags/Extension.pm789
-rwxr-xr-xextensions/TrackingFlags/bin/bulk_flag_clear.pl137
-rwxr-xr-xextensions/TrackingFlags/bin/migrate_tracking_flags.pl316
-rw-r--r--extensions/TrackingFlags/lib/Admin.pm446
-rw-r--r--extensions/TrackingFlags/lib/Constants.pm41
-rw-r--r--extensions/TrackingFlags/lib/Flag.pm467
-rw-r--r--extensions/TrackingFlags/lib/Flag/Bug.pm187
-rw-r--r--extensions/TrackingFlags/lib/Flag/Value.pm142
-rw-r--r--extensions/TrackingFlags/lib/Flag/Visibility.pm172
-rw-r--r--extensions/TrackingFlags/template/en/default/bug/tracking_flags.html.tmpl62
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/admin/admin-end_links_right.html.tmpl18
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl23
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/create/create-bug_flags.html.tmpl29
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/create/create-form.html.tmpl63
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-bug_flags_end.html.tmpl33
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-project_flags_end.html.tmpl18
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-tracking_flags_end.html.tmpl18
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl46
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/field-editable.html.tmpl38
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/field-non_editable.html.tmpl9
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/show-header-end.html.tmpl10
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/global/code-error-errors.html.tmpl27
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/global/header-start.html.tmpl11
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/global/messages-messages.html.tmpl18
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/global/user-error-errors.html.tmpl58
-rw-r--r--extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_edit.html.tmpl197
-rw-r--r--extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_list.html.tmpl73
-rw-r--r--extensions/TrackingFlags/web/js/admin.js440
-rw-r--r--extensions/TrackingFlags/web/js/tracking_flags.js95
-rw-r--r--extensions/TrackingFlags/web/styles/admin.css111
-rw-r--r--extensions/TrackingFlags/web/styles/edit_bug.css18
-rw-r--r--extensions/TryAutoLand/Config.pm19
-rw-r--r--extensions/TryAutoLand/Extension.pm323
-rwxr-xr-xextensions/TryAutoLand/bin/TryAutoLand.getBugs.pl60
-rwxr-xr-xextensions/TryAutoLand/bin/TryAutoLand.updateStatus.pl65
-rwxr-xr-xextensions/TryAutoLand/bin/TryAutoLand.updateStatus_json.pl65
-rw-r--r--extensions/TryAutoLand/lib/Constants.pm31
-rw-r--r--extensions/TryAutoLand/lib/WebService.pm189
-rw-r--r--extensions/TryAutoLand/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl101
-rw-r--r--extensions/TryAutoLand/template/en/default/hook/bug/field-help-end.none.tmpl15
-rw-r--r--extensions/TryAutoLand/template/en/default/hook/bug/show-header-end.html.tmpl11
-rw-r--r--extensions/TryAutoLand/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl11
-rw-r--r--extensions/TryAutoLand/template/en/default/hook/global/user-error-errors.html.tmpl33
-rw-r--r--extensions/TryAutoLand/web/style.css23
-rw-r--r--extensions/TypeSniffer/Config.pm40
-rw-r--r--extensions/TypeSniffer/Extension.pm100
-rw-r--r--extensions/UserProfile/Config.pm15
-rw-r--r--extensions/UserProfile/Extension.pm554
-rwxr-xr-xextensions/UserProfile/bin/migrate.pl43
-rwxr-xr-xextensions/UserProfile/bin/update.pl81
-rw-r--r--extensions/UserProfile/lib/TimeAgo.pm179
-rw-r--r--extensions/UserProfile/lib/Util.pm387
-rw-r--r--extensions/UserProfile/template/en/default/hook/account/prefs/account-field.html.tmpl11
-rw-r--r--extensions/UserProfile/template/en/default/pages/user_profile.html.tmpl300
-rw-r--r--extensions/UserProfile/web/styles/user_profile.css48
-rw-r--r--extensions/UserStory/Config.pm21
-rw-r--r--extensions/UserStory/Extension.pm102
-rw-r--r--extensions/UserStory/lib/Constants.pm29
-rw-r--r--extensions/UserStory/template/en/default/hook/bug/comments-comment_banner.html.tmpl73
-rw-r--r--extensions/UserStory/template/en/default/hook/bug/create/create-after_custom_fields.html.tmpl84
-rw-r--r--extensions/UserStory/template/en/default/hook/bug/create/create-custom_field.html.tmpl12
-rw-r--r--extensions/UserStory/template/en/default/hook/bug/edit-custom_field.html.tmpl11
-rw-r--r--extensions/UserStory/template/en/default/hook/bug/show-header-end.html.tmpl9
-rw-r--r--extensions/UserStory/web/style/user_story.css41
-rw-r--r--extensions/Voting/Extension.pm177
-rw-r--r--extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl3
-rw-r--r--extensions/Voting/template/en/default/pages/voting/user.html.tmpl19
-rw-r--r--extensions/ZPushNotify/Config.pm15
-rw-r--r--extensions/ZPushNotify/Extension.pm110
-rw-r--r--extensions/ZPushNotify/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl13
-rw-r--r--images/buggie.pngbin0 -> 17002 bytes
-rwxr-xr-ximportxml.pl11
-rwxr-xr-xjobqueue.pl3
-rw-r--r--js/TUI.js10
-rw-r--r--js/attachment.js10
-rw-r--r--js/bug.js46
-rw-r--r--js/comment-tagging.js389
-rw-r--r--js/comments.js65
-rw-r--r--js/create_bug.js116
-rw-r--r--js/custom-search.js174
-rw-r--r--js/field.js178
-rw-r--r--js/global.js35
-rw-r--r--js/instant-search.js201
-rw-r--r--js/util.js81
-rw-r--r--js/yui3/align-plugin/align-plugin-min.js8
-rw-r--r--js/yui3/anim-base/anim-base-min.js8
-rw-r--r--js/yui3/anim-color/anim-color-min.js8
-rw-r--r--js/yui3/anim-curve/anim-curve-min.js8
-rw-r--r--js/yui3/anim-easing/anim-easing-min.js8
-rw-r--r--js/yui3/anim-node-plugin/anim-node-plugin-min.js8
-rw-r--r--js/yui3/anim-scroll/anim-scroll-min.js8
-rw-r--r--js/yui3/anim-shape/anim-shape-min.js8
-rw-r--r--js/yui3/anim-xy/anim-xy-min.js8
-rw-r--r--js/yui3/app-base/app-base-min.js8
-rw-r--r--js/yui3/app-content/app-content-min.js8
-rw-r--r--js/yui3/app-transitions-css/app-transitions-css-min.css8
-rw-r--r--js/yui3/app-transitions-css/app-transitions-css.css40
-rw-r--r--js/yui3/app-transitions-native/app-transitions-native-min.js8
-rw-r--r--js/yui3/app-transitions/app-transitions-min.js8
-rw-r--r--js/yui3/array-extras/array-extras-min.js8
-rw-r--r--js/yui3/array-invoke/array-invoke-min.js8
-rw-r--r--js/yui3/arraylist-add/arraylist-add-min.js8
-rw-r--r--js/yui3/arraylist-filter/arraylist-filter-min.js8
-rw-r--r--js/yui3/arraylist/arraylist-min.js8
-rw-r--r--js/yui3/arraysort/arraysort-min.js8
-rw-r--r--js/yui3/assets/skin/audio-light/skin.css197
-rw-r--r--js/yui3/assets/skin/audio/skin.css197
-rw-r--r--js/yui3/assets/skin/capsule-dark/skin.css197
-rw-r--r--js/yui3/assets/skin/capsule/skin.css201
-rw-r--r--js/yui3/assets/skin/night/skin.css1815
-rw-r--r--js/yui3/assets/skin/round-dark/skin.css193
-rw-r--r--js/yui3/assets/skin/round/skin.css193
-rw-r--r--js/yui3/assets/skin/sam-dark/skin.css189
-rw-r--r--js/yui3/assets/skin/sam/skin.css1727
-rw-r--r--js/yui3/assets/skins/audio-light/rail-x.png0
-rw-r--r--js/yui3/assets/skins/audio-light/rail-y.png0
-rw-r--r--js/yui3/assets/skins/audio-light/slider-base.css8
-rw-r--r--js/yui3/assets/skins/audio-light/thumb-x.png0
-rw-r--r--js/yui3/assets/skins/audio-light/thumb-y.png0
-rw-r--r--js/yui3/assets/skins/audio/rail-x.png0
-rw-r--r--js/yui3/assets/skins/audio/rail-y.png0
-rw-r--r--js/yui3/assets/skins/audio/slider-base.css8
-rw-r--r--js/yui3/assets/skins/audio/thumb-x.png0
-rw-r--r--js/yui3/assets/skins/audio/thumb-y.png0
-rw-r--r--js/yui3/assets/skins/capsule-dark/rail-x-dots.png0
-rw-r--r--js/yui3/assets/skins/capsule-dark/rail-x-lines.png0
-rw-r--r--js/yui3/assets/skins/capsule-dark/rail-x.png0
-rw-r--r--js/yui3/assets/skins/capsule-dark/rail-y-dots.png0
-rw-r--r--js/yui3/assets/skins/capsule-dark/rail-y-lines.png0
-rw-r--r--js/yui3/assets/skins/capsule-dark/rail-y.png0
-rw-r--r--js/yui3/assets/skins/capsule-dark/slider-base.css8
-rw-r--r--js/yui3/assets/skins/capsule-dark/thumb-x-line.png0
-rw-r--r--js/yui3/assets/skins/capsule-dark/thumb-x.png0
-rw-r--r--js/yui3/assets/skins/capsule-dark/thumb-y-line.png0
-rw-r--r--js/yui3/assets/skins/capsule-dark/thumb-y.png0
-rw-r--r--js/yui3/assets/skins/capsule/rail-x-dots.png0
-rw-r--r--js/yui3/assets/skins/capsule/rail-x-lines.png0
-rw-r--r--js/yui3/assets/skins/capsule/rail-x.png0
-rw-r--r--js/yui3/assets/skins/capsule/rail-y-dots.png0
-rw-r--r--js/yui3/assets/skins/capsule/rail-y-lines.png0
-rw-r--r--js/yui3/assets/skins/capsule/rail-y.png0
-rw-r--r--js/yui3/assets/skins/capsule/slider-base.css8
-rw-r--r--js/yui3/assets/skins/capsule/thumb-x-line.png0
-rw-r--r--js/yui3/assets/skins/capsule/thumb-x.png0
-rw-r--r--js/yui3/assets/skins/capsule/thumb-y-line.png0
-rw-r--r--js/yui3/assets/skins/capsule/thumb-y-lines.png0
-rw-r--r--js/yui3/assets/skins/capsule/thumb-y.png0
-rw-r--r--js/yui3/assets/skins/night/arrows.png0
-rw-r--r--js/yui3/assets/skins/night/autocomplete-list.css8
-rw-r--r--js/yui3/assets/skins/night/calendar-base.css8
-rw-r--r--js/yui3/assets/skins/night/calendar.css8
-rw-r--r--js/yui3/assets/skins/night/calendarnavigator.css8
-rw-r--r--js/yui3/assets/skins/night/datatable-base.css8
-rw-r--r--js/yui3/assets/skins/night/datatable-highlight.css8
-rw-r--r--js/yui3/assets/skins/night/datatable-message.css8
-rw-r--r--js/yui3/assets/skins/night/datatable-paginator.css8
-rw-r--r--js/yui3/assets/skins/night/datatable-scroll.css8
-rw-r--r--js/yui3/assets/skins/night/datatable-sort.css8
-rw-r--r--js/yui3/assets/skins/night/dial.css8
-rw-r--r--js/yui3/assets/skins/night/horizontal-menu-submenu-indicator.png0
-rw-r--r--js/yui3/assets/skins/night/node-menunav.css8
-rw-r--r--js/yui3/assets/skins/night/overlay.css8
-rw-r--r--js/yui3/assets/skins/night/panel.css8
-rw-r--r--js/yui3/assets/skins/night/rail-x-lines.png0
-rw-r--r--js/yui3/assets/skins/night/rail-x.png0
-rw-r--r--js/yui3/assets/skins/night/rail-y-lines.png0
-rw-r--r--js/yui3/assets/skins/night/rail-y.png0
-rw-r--r--js/yui3/assets/skins/night/resize-base.css8
-rw-r--r--js/yui3/assets/skins/night/scrollview-base.css8
-rw-r--r--js/yui3/assets/skins/night/scrollview-list.css8
-rw-r--r--js/yui3/assets/skins/night/scrollview-scrollbars.css8
-rw-r--r--js/yui3/assets/skins/night/slider-base.css8
-rw-r--r--js/yui3/assets/skins/night/sort-arrow-sprite-ie.png0
-rw-r--r--js/yui3/assets/skins/night/sort-arrow-sprite.png0
-rw-r--r--js/yui3/assets/skins/night/sprite_icons.png0
-rw-r--r--js/yui3/assets/skins/night/tabview.css8
-rw-r--r--js/yui3/assets/skins/night/thumb-x.png0
-rw-r--r--js/yui3/assets/skins/night/thumb-y.png0
-rw-r--r--js/yui3/assets/skins/night/vertical-menu-submenu-indicator.png0
-rw-r--r--js/yui3/assets/skins/night/widget-base.css8
-rw-r--r--js/yui3/assets/skins/night/widget-modality.css8
-rw-r--r--js/yui3/assets/skins/night/widget-stack.css8
-rw-r--r--js/yui3/assets/skins/round-dark/rail-x.png0
-rw-r--r--js/yui3/assets/skins/round-dark/rail-y.png0
-rw-r--r--js/yui3/assets/skins/round-dark/slider-base.css8
-rw-r--r--js/yui3/assets/skins/round-dark/thumb-x-grip.png0
-rw-r--r--js/yui3/assets/skins/round-dark/thumb-x.png0
-rw-r--r--js/yui3/assets/skins/round-dark/thumb-y-grip.png0
-rw-r--r--js/yui3/assets/skins/round-dark/thumb-y.png0
-rw-r--r--js/yui3/assets/skins/round/rail-x.png0
-rw-r--r--js/yui3/assets/skins/round/rail-y.png0
-rw-r--r--js/yui3/assets/skins/round/slider-base.css8
-rw-r--r--js/yui3/assets/skins/round/thumb-x-grip.png0
-rw-r--r--js/yui3/assets/skins/round/thumb-x.png0
-rw-r--r--js/yui3/assets/skins/round/thumb-y-grip.png0
-rw-r--r--js/yui3/assets/skins/round/thumb-y.png0
-rw-r--r--js/yui3/assets/skins/sam-dark/rail-x-lines.png0
-rw-r--r--js/yui3/assets/skins/sam-dark/rail-x.png0
-rw-r--r--js/yui3/assets/skins/sam-dark/rail-y-lines.png0
-rw-r--r--js/yui3/assets/skins/sam-dark/rail-y.png0
-rw-r--r--js/yui3/assets/skins/sam-dark/slider-base.css8
-rw-r--r--js/yui3/assets/skins/sam-dark/thumb-x.png0
-rw-r--r--js/yui3/assets/skins/sam-dark/thumb-y.png0
-rw-r--r--js/yui3/assets/skins/sam/arrows.pngbin0 -> 258 bytes
-rw-r--r--js/yui3/assets/skins/sam/autocomplete-list.css8
-rw-r--r--js/yui3/assets/skins/sam/bg.pngbin0 -> 121 bytes
-rw-r--r--js/yui3/assets/skins/sam/calendar-base.css8
-rw-r--r--js/yui3/assets/skins/sam/calendar.css8
-rw-r--r--js/yui3/assets/skins/sam/calendarnavigator.css8
-rw-r--r--js/yui3/assets/skins/sam/console-filters.css8
-rw-r--r--js/yui3/assets/skins/sam/console.css8
-rw-r--r--js/yui3/assets/skins/sam/datatable-base.css8
-rw-r--r--js/yui3/assets/skins/sam/datatable-highlight.css8
-rw-r--r--js/yui3/assets/skins/sam/datatable-message.css8
-rw-r--r--js/yui3/assets/skins/sam/datatable-paginator.css8
-rw-r--r--js/yui3/assets/skins/sam/datatable-scroll.css8
-rw-r--r--js/yui3/assets/skins/sam/datatable-sort.css8
-rw-r--r--js/yui3/assets/skins/sam/dial.css8
-rw-r--r--js/yui3/assets/skins/sam/dt-arrow-dn.pngbin0 -> 101 bytes
-rw-r--r--js/yui3/assets/skins/sam/dt-arrow-up.pngbin0 -> 99 bytes
-rw-r--r--js/yui3/assets/skins/sam/horizontal-menu-submenu-indicator.pngbin0 -> 157 bytes
-rw-r--r--js/yui3/assets/skins/sam/horizontal-menu-submenu-toggle.pngbin0 -> 183 bytes
-rw-r--r--js/yui3/assets/skins/sam/node-flick.css8
-rw-r--r--js/yui3/assets/skins/sam/node-menunav.css8
-rw-r--r--js/yui3/assets/skins/sam/overlay.css8
-rw-r--r--js/yui3/assets/skins/sam/panel.css8
-rw-r--r--js/yui3/assets/skins/sam/rail-x-lines.pngbin0 -> 3656 bytes
-rw-r--r--js/yui3/assets/skins/sam/rail-x.pngbin0 -> 3639 bytes
-rw-r--r--js/yui3/assets/skins/sam/rail-y-lines.pngbin0 -> 3642 bytes
-rw-r--r--js/yui3/assets/skins/sam/rail-y.pngbin0 -> 3629 bytes
-rw-r--r--js/yui3/assets/skins/sam/resize-base.css8
-rw-r--r--js/yui3/assets/skins/sam/scrollview-base.css8
-rw-r--r--js/yui3/assets/skins/sam/scrollview-list.css8
-rw-r--r--js/yui3/assets/skins/sam/scrollview-scrollbars.css8
-rw-r--r--js/yui3/assets/skins/sam/slider-base.css8
-rw-r--r--js/yui3/assets/skins/sam/sort-arrow-sprite-ie.pngbin0 -> 3628 bytes
-rw-r--r--js/yui3/assets/skins/sam/sort-arrow-sprite.pngbin0 -> 2884 bytes
-rw-r--r--js/yui3/assets/skins/sam/sprite.pngbin0 -> 2913 bytes
-rw-r--r--js/yui3/assets/skins/sam/sprite_icons.gifbin0 -> 142 bytes
-rw-r--r--js/yui3/assets/skins/sam/sprite_icons.pngbin0 -> 176 bytes
-rw-r--r--js/yui3/assets/skins/sam/tabview.css8
-rw-r--r--js/yui3/assets/skins/sam/test-console.css8
-rw-r--r--js/yui3/assets/skins/sam/thumb-x.pngbin0 -> 3873 bytes
-rw-r--r--js/yui3/assets/skins/sam/thumb-y.pngbin0 -> 3860 bytes
-rw-r--r--js/yui3/assets/skins/sam/vertical-menu-submenu-indicator.pngbin0 -> 156 bytes
-rw-r--r--js/yui3/assets/skins/sam/warn_error.pngbin0 -> 571 bytes
-rw-r--r--js/yui3/assets/skins/sam/widget-base.css8
-rw-r--r--js/yui3/assets/skins/sam/widget-modality.css8
-rw-r--r--js/yui3/assets/skins/sam/widget-stack.css8
-rw-r--r--js/yui3/async-queue/async-queue-min.js8
-rw-r--r--js/yui3/attribute-base/attribute-base-min.js8
-rw-r--r--js/yui3/attribute-complex/attribute-complex-min.js8
-rw-r--r--js/yui3/attribute-core/attribute-core-min.js8
-rw-r--r--js/yui3/attribute-extras/attribute-extras-min.js8
-rw-r--r--js/yui3/attribute-observable/attribute-observable-min.js8
-rw-r--r--js/yui3/autocomplete-base/autocomplete-base-min.js9
-rw-r--r--js/yui3/autocomplete-filters-accentfold/autocomplete-filters-accentfold-min.js8
-rw-r--r--js/yui3/autocomplete-filters/autocomplete-filters-min.js8
-rw-r--r--js/yui3/autocomplete-highlighters-accentfold/autocomplete-highlighters-accentfold-min.js8
-rw-r--r--js/yui3/autocomplete-highlighters/autocomplete-highlighters-min.js8
-rw-r--r--js/yui3/autocomplete-list-keys/autocomplete-list-keys-min.js8
-rw-r--r--js/yui3/autocomplete-list/assets/autocomplete-list-core.css34
-rw-r--r--js/yui3/autocomplete-list/assets/skins/night/autocomplete-list.css8
-rw-r--r--js/yui3/autocomplete-list/assets/skins/sam/autocomplete-list.css8
-rw-r--r--js/yui3/autocomplete-list/autocomplete-list-min.js9
-rw-r--r--js/yui3/autocomplete-list/lang/autocomplete-list.js8
-rw-r--r--js/yui3/autocomplete-list/lang/autocomplete-list_en.js8
-rw-r--r--js/yui3/autocomplete-list/lang/autocomplete-list_es.js8
-rw-r--r--js/yui3/autocomplete-list/lang/autocomplete-list_hu.js8
-rw-r--r--js/yui3/autocomplete-list/lang/autocomplete-list_it.js8
-rw-r--r--js/yui3/autocomplete-plugin/autocomplete-plugin-min.js8
-rw-r--r--js/yui3/autocomplete-sources/autocomplete-sources-min.js8
-rw-r--r--js/yui3/axis-base/axis-base-min.js8
-rw-r--r--js/yui3/axis-category-base/axis-category-base-min.js8
-rw-r--r--js/yui3/axis-category/axis-category-min.js8
-rw-r--r--js/yui3/axis-numeric-base/axis-numeric-base-min.js8
-rw-r--r--js/yui3/axis-numeric/axis-numeric-min.js8
-rw-r--r--js/yui3/axis-stacked-base/axis-stacked-base-min.js8
-rw-r--r--js/yui3/axis-stacked/axis-stacked-min.js8
-rw-r--r--js/yui3/axis-time-base/axis-time-base-min.js8
-rw-r--r--js/yui3/axis-time/axis-time-min.js8
-rw-r--r--js/yui3/axis/axis-min.js12
-rw-r--r--js/yui3/base-base/base-base-min.js8
-rw-r--r--js/yui3/base-build/base-build-min.js8
-rw-r--r--js/yui3/base-core/base-core-min.js8
-rw-r--r--js/yui3/base-observable/base-observable-min.js8
-rw-r--r--js/yui3/base-pluginhost/base-pluginhost-min.js8
-rw-r--r--js/yui3/button-core/button-core-min.js8
-rw-r--r--js/yui3/button-group/button-group-min.js8
-rw-r--r--js/yui3/button-plugin/button-plugin-min.js8
-rw-r--r--js/yui3/button/button-min.js8
-rw-r--r--js/yui3/cache-base/cache-base-min.js8
-rw-r--r--js/yui3/cache-offline/cache-offline-min.js8
-rw-r--r--js/yui3/cache-plugin/cache-plugin-min.js8
-rw-r--r--js/yui3/calendar-base/assets/calendar-base-core.css28
-rw-r--r--js/yui3/calendar-base/assets/skins/night/calendar-base.css8
-rw-r--r--js/yui3/calendar-base/assets/skins/sam/calendar-base.css8
-rw-r--r--js/yui3/calendar-base/calendar-base-min.js10
-rw-r--r--js/yui3/calendar-base/lang/calendar-base.js8
-rw-r--r--js/yui3/calendar-base/lang/calendar-base_de.js8
-rw-r--r--js/yui3/calendar-base/lang/calendar-base_en.js8
-rw-r--r--js/yui3/calendar-base/lang/calendar-base_es-AR.js8
-rw-r--r--js/yui3/calendar-base/lang/calendar-base_es.js8
-rw-r--r--js/yui3/calendar-base/lang/calendar-base_fr.js8
-rw-r--r--js/yui3/calendar-base/lang/calendar-base_hu.js8
-rw-r--r--js/yui3/calendar-base/lang/calendar-base_it.js8
-rw-r--r--js/yui3/calendar-base/lang/calendar-base_ja.js8
-rw-r--r--js/yui3/calendar-base/lang/calendar-base_nb-NO.js8
-rw-r--r--js/yui3/calendar-base/lang/calendar-base_nl.js8
-rw-r--r--js/yui3/calendar-base/lang/calendar-base_pt-BR.js8
-rw-r--r--js/yui3/calendar-base/lang/calendar-base_ru.js8
-rw-r--r--js/yui3/calendar-base/lang/calendar-base_zh-HANT-TW.js8
-rw-r--r--js/yui3/calendar-base/lang/calendar-base_zh-Hans-CN.js8
-rw-r--r--js/yui3/calendar-base/lang/calendar-base_zh-Hans.js8
-rw-r--r--js/yui3/calendar-base/lang/calendar-base_zh-Hant-HK.js8
-rw-r--r--js/yui3/calendar-base/lang/calendar-base_zh-Hant.js8
-rw-r--r--js/yui3/calendar/assets/calendar-core.css38
-rw-r--r--js/yui3/calendar/assets/skins/night/calendar.css8
-rw-r--r--js/yui3/calendar/assets/skins/sam/calendar.css8
-rw-r--r--js/yui3/calendar/calendar-min.js9
-rw-r--r--js/yui3/calendarnavigator/assets/calendarnavigator-core.css25
-rw-r--r--js/yui3/calendarnavigator/assets/skins/night/calendarnavigator.css8
-rw-r--r--js/yui3/calendarnavigator/assets/skins/sam/calendarnavigator.css8
-rw-r--r--js/yui3/calendarnavigator/calendarnavigator-min.js8
-rw-r--r--js/yui3/charts-base/charts-base-min.js15
-rw-r--r--js/yui3/charts-legend/charts-legend-min.js10
-rw-r--r--js/yui3/classnamemanager/classnamemanager-min.js8
-rw-r--r--js/yui3/clickable-rail/assets/thumb-x-oblong-dark.pngbin0 -> 4042 bytes
-rw-r--r--js/yui3/clickable-rail/assets/thumb-x-oblong.pngbin0 -> 961 bytes
-rw-r--r--js/yui3/clickable-rail/assets/thumb-x-oblong2-dark.pngbin0 -> 4045 bytes
-rw-r--r--js/yui3/clickable-rail/assets/thumb-x-oblong2.pngbin0 -> 706 bytes
-rw-r--r--js/yui3/clickable-rail/assets/thumb-y-oblong-dark.pngbin0 -> 519 bytes
-rw-r--r--js/yui3/clickable-rail/assets/thumb-y-oblong.pngbin0 -> 1023 bytes
-rw-r--r--js/yui3/clickable-rail/assets/thumb-y-oblong2-dark.pngbin0 -> 706 bytes
-rw-r--r--js/yui3/clickable-rail/assets/thumb-y-oblong2.pngbin0 -> 746 bytes
-rw-r--r--js/yui3/clickable-rail/clickable-rail-min.js8
-rw-r--r--js/yui3/color-base/color-base-min.js8
-rw-r--r--js/yui3/color-harmony/color-harmony-min.js8
-rw-r--r--js/yui3/color-hsl/color-hsl-min.js8
-rw-r--r--js/yui3/color-hsv/color-hsv-min.js8
-rw-r--r--js/yui3/console-filters/assets/console-filters-core.css7
-rw-r--r--js/yui3/console-filters/assets/skins/sam/console-filters.css8
-rw-r--r--js/yui3/console-filters/console-filters-min.js8
-rw-r--r--js/yui3/console/assets/console-core.css7
-rw-r--r--js/yui3/console/assets/skins/sam/bg.pngbin0 -> 121 bytes
-rw-r--r--js/yui3/console/assets/skins/sam/console.css8
-rw-r--r--js/yui3/console/assets/skins/sam/warn_error.pngbin0 -> 571 bytes
-rw-r--r--js/yui3/console/assets/warn_error.pngbin0 -> 571 bytes
-rw-r--r--js/yui3/console/console-min.js9
-rw-r--r--js/yui3/console/lang/console.js8
-rw-r--r--js/yui3/console/lang/console_en.js8
-rw-r--r--js/yui3/console/lang/console_es.js8
-rw-r--r--js/yui3/console/lang/console_hu.js8
-rw-r--r--js/yui3/console/lang/console_it.js8
-rw-r--r--js/yui3/console/lang/console_ja.js8
-rw-r--r--js/yui3/content-editable/content-editable-min.js8
-rw-r--r--js/yui3/cookie/cookie-min.js8
-rw-r--r--js/yui3/createlink-base/createlink-base-min.js8
-rw-r--r--js/yui3/cssbase-context/cssbase-context-min.css8
-rw-r--r--js/yui3/cssbase-context/cssbase-context.css83
-rw-r--r--js/yui3/cssbase/cssbase-min.css8
-rw-r--r--js/yui3/cssbase/cssbase.css84
-rw-r--r--js/yui3/cssbutton/cssbutton-min.css8
-rw-r--r--js/yui3/cssbutton/cssbutton.css154
-rw-r--r--js/yui3/cssfonts-context/cssfonts-context-min.css8
-rw-r--r--js/yui3/cssfonts-context/cssfonts-context.css49
-rw-r--r--js/yui3/cssfonts/cssfonts-min.css8
-rw-r--r--js/yui3/cssfonts/cssfonts.css49
-rw-r--r--js/yui3/cssgrids-base/cssgrids-base-min.css13
-rw-r--r--js/yui3/cssgrids-base/cssgrids-base.css86
-rw-r--r--js/yui3/cssgrids-responsive/cssgrids-responsive-min.css13
-rw-r--r--js/yui3/cssgrids-responsive/cssgrids-responsive.css404
-rw-r--r--js/yui3/cssgrids-units/cssgrids-units-min.css13
-rw-r--r--js/yui3/cssgrids-units/cssgrids-units.css227
-rw-r--r--js/yui3/cssgrids/cssgrids-min.css18
-rw-r--r--js/yui3/cssgrids/cssgrids.css303
-rw-r--r--js/yui3/cssnormalize-context/cssnormalize-context-min.css16
-rw-r--r--js/yui3/cssnormalize-context/cssnormalize-context.css590
-rw-r--r--js/yui3/cssnormalize/cssnormalize-min.css16
-rw-r--r--js/yui3/cssnormalize/cssnormalize.css590
-rw-r--r--js/yui3/cssreset-context/cssreset-context-min.css8
-rw-r--r--js/yui3/cssreset-context/cssreset-context.css128
-rw-r--r--js/yui3/cssreset/cssreset-min.css8
-rw-r--r--js/yui3/cssreset/cssreset.css123
-rw-r--r--js/yui3/dataschema-array/dataschema-array-min.js8
-rw-r--r--js/yui3/dataschema-base/dataschema-base-min.js8
-rw-r--r--js/yui3/dataschema-json/dataschema-json-min.js8
-rw-r--r--js/yui3/dataschema-text/dataschema-text-min.js8
-rw-r--r--js/yui3/dataschema-xml/dataschema-xml-min.js8
-rw-r--r--js/yui3/datasource-arrayschema/datasource-arrayschema-min.js8
-rw-r--r--js/yui3/datasource-cache/datasource-cache-min.js8
-rw-r--r--js/yui3/datasource-function/datasource-function-min.js8
-rw-r--r--js/yui3/datasource-get/datasource-get-min.js8
-rw-r--r--js/yui3/datasource-io/datasource-io-min.js8
-rw-r--r--js/yui3/datasource-jsonschema/datasource-jsonschema-min.js8
-rw-r--r--js/yui3/datasource-local/datasource-local-min.js8
-rw-r--r--js/yui3/datasource-polling/datasource-polling-min.js8
-rw-r--r--js/yui3/datasource-textschema/datasource-textschema-min.js8
-rw-r--r--js/yui3/datasource-xmlschema/datasource-xmlschema-min.js8
-rw-r--r--js/yui3/datatable-base-deprecated/assets/skins/sam/dt-arrow-dn.pngbin0 -> 101 bytes
-rw-r--r--js/yui3/datatable-base-deprecated/assets/skins/sam/dt-arrow-up.pngbin0 -> 99 bytes
-rw-r--r--js/yui3/datatable-base/assets/datatable-base-core.css11
-rw-r--r--js/yui3/datatable-base/assets/skins/night/datatable-base.css8
-rw-r--r--js/yui3/datatable-base/assets/skins/sam/datatable-base.css8
-rw-r--r--js/yui3/datatable-base/datatable-base-min.js8
-rw-r--r--js/yui3/datatable-body/datatable-body-min.js9
-rw-r--r--js/yui3/datatable-column-widths/datatable-column-widths-min.js8
-rw-r--r--js/yui3/datatable-core/datatable-core-min.js8
-rw-r--r--js/yui3/datatable-datasource/datatable-datasource-min.js8
-rw-r--r--js/yui3/datatable-foot/datatable-foot-min.js8
-rw-r--r--js/yui3/datatable-formatters/datatable-formatters-min.js8
-rw-r--r--js/yui3/datatable-head/datatable-head-min.js8
-rw-r--r--js/yui3/datatable-highlight/assets/datatable-highlight-core.css33
-rw-r--r--js/yui3/datatable-highlight/assets/skins/night/datatable-highlight.css8
-rw-r--r--js/yui3/datatable-highlight/assets/skins/sam/datatable-highlight.css8
-rw-r--r--js/yui3/datatable-highlight/datatable-highlight-min.js8
-rw-r--r--js/yui3/datatable-keynav/datatable-keynav-min.js9
-rw-r--r--js/yui3/datatable-message/assets/datatable-message-core.css15
-rw-r--r--js/yui3/datatable-message/assets/skins/night/datatable-message.css8
-rw-r--r--js/yui3/datatable-message/assets/skins/sam/datatable-message.css8
-rw-r--r--js/yui3/datatable-message/datatable-message-min.js8
-rw-r--r--js/yui3/datatable-message/lang/datatable-message.js8
-rw-r--r--js/yui3/datatable-message/lang/datatable-message_en.js8
-rw-r--r--js/yui3/datatable-message/lang/datatable-message_es.js8
-rw-r--r--js/yui3/datatable-message/lang/datatable-message_fr.js8
-rw-r--r--js/yui3/datatable-message/lang/datatable-message_hu.js8
-rw-r--r--js/yui3/datatable-message/lang/datatable-message_it.js8
-rw-r--r--js/yui3/datatable-mutable/datatable-mutable-min.js8
-rw-r--r--js/yui3/datatable-paginator-templates/datatable-paginator-templates-min.js8
-rw-r--r--js/yui3/datatable-paginator/assets/datatable-paginator-core.css65
-rw-r--r--js/yui3/datatable-paginator/assets/skins/night/datatable-paginator.css8
-rw-r--r--js/yui3/datatable-paginator/assets/skins/sam/datatable-paginator.css8
-rw-r--r--js/yui3/datatable-paginator/datatable-paginator-min.js9
-rw-r--r--js/yui3/datatable-paginator/lang/datatable-paginator.js8
-rw-r--r--js/yui3/datatable-paginator/lang/datatable-paginator_en.js8
-rw-r--r--js/yui3/datatable-paginator/lang/datatable-paginator_fr.js8
-rw-r--r--js/yui3/datatable-scroll/assets/datatable-scroll-core.css78
-rw-r--r--js/yui3/datatable-scroll/assets/skins/night/datatable-scroll.css8
-rw-r--r--js/yui3/datatable-scroll/assets/skins/sam/datatable-scroll.css8
-rw-r--r--js/yui3/datatable-scroll/datatable-scroll-min.js9
-rw-r--r--js/yui3/datatable-sort/assets/datatable-sort-core.css24
-rw-r--r--js/yui3/datatable-sort/assets/skins/night/datatable-sort.css8
-rw-r--r--js/yui3/datatable-sort/assets/skins/night/sort-arrow-sprite-ie.pngbin0 -> 3629 bytes
-rw-r--r--js/yui3/datatable-sort/assets/skins/night/sort-arrow-sprite.pngbin0 -> 2885 bytes
-rw-r--r--js/yui3/datatable-sort/assets/skins/sam/datatable-sort.css8
-rw-r--r--js/yui3/datatable-sort/assets/skins/sam/sort-arrow-sprite-ie.pngbin0 -> 3628 bytes
-rw-r--r--js/yui3/datatable-sort/assets/skins/sam/sort-arrow-sprite.pngbin0 -> 2884 bytes
-rw-r--r--js/yui3/datatable-sort/datatable-sort-min.js8
-rw-r--r--js/yui3/datatable-sort/lang/datatable-sort.js8
-rw-r--r--js/yui3/datatable-sort/lang/datatable-sort_en.js8
-rw-r--r--js/yui3/datatable-sort/lang/datatable-sort_es.js8
-rw-r--r--js/yui3/datatable-sort/lang/datatable-sort_fr.js8
-rw-r--r--js/yui3/datatable-sort/lang/datatable-sort_hu.js8
-rw-r--r--js/yui3/datatable-table/datatable-table-min.js8
-rw-r--r--js/yui3/datatype-date-format/datatype-date-format-min.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_ar-JO.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_ar.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_ca-ES.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_ca.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_da-DK.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_da.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_de-AT.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_de-DE.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_de.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_el-GR.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_el.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_en-AU.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_en-CA.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_en-GB.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_en-IE.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_en-IN.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_en-JO.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_en-MY.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_en-NZ.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_en-PH.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_en-SG.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_en-US.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_en.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_es-AR.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_es-BO.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_es-CL.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_es-CO.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_es-EC.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_es-ES.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_es-MX.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_es-PE.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_es-PY.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_es-US.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_es-UY.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_es-VE.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_es.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_fi-FI.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_fi.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_fr-BE.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_fr-CA.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_fr-FR.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_fr.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_hi-IN.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_hi.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_hu.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_id-ID.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_id.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_it-IT.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_it.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_ja-JP.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_ja.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_ko-KR.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_ko.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_ms-MY.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_ms.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_nb-NO.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_nb.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_nl-BE.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_nl-NL.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_nl.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_pl-PL.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_pl.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_pt-BR.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_pt.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_ro-RO.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_ro.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_ru-RU.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_ru.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_sv-SE.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_sv.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_th-TH.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_th.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_tr-TR.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_tr.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_vi-VN.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_vi.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hans-CN.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hans.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hant-HK.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hant-TW.js8
-rw-r--r--js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hant.js8
-rw-r--r--js/yui3/datatype-date-math/datatype-date-math-min.js8
-rw-r--r--js/yui3/datatype-date-parse/datatype-date-parse-min.js8
-rw-r--r--js/yui3/datatype-number-format/datatype-number-format-min.js8
-rw-r--r--js/yui3/datatype-number-parse/datatype-number-parse-min.js8
-rw-r--r--js/yui3/datatype-xml-format/datatype-xml-format-min.js8
-rw-r--r--js/yui3/datatype-xml-parse/datatype-xml-parse-min.js8
-rw-r--r--js/yui3/dd-constrain/dd-constrain-min.js8
-rw-r--r--js/yui3/dd-ddm-base/dd-ddm-base-min.js8
-rw-r--r--js/yui3/dd-ddm-drop/dd-ddm-drop-min.js8
-rw-r--r--js/yui3/dd-ddm/dd-ddm-min.js8
-rw-r--r--js/yui3/dd-delegate/dd-delegate-min.js8
-rw-r--r--js/yui3/dd-drag/dd-drag-min.js9
-rw-r--r--js/yui3/dd-drop-plugin/dd-drop-plugin-min.js8
-rw-r--r--js/yui3/dd-drop/dd-drop-min.js8
-rw-r--r--js/yui3/dd-gestures/dd-gestures-min.js8
-rw-r--r--js/yui3/dd-plugin/dd-plugin-min.js8
-rw-r--r--js/yui3/dd-proxy/dd-proxy-min.js8
-rw-r--r--js/yui3/dd-scroll/dd-scroll-min.js8
-rw-r--r--js/yui3/dial/assets/dial-core.css49
-rw-r--r--js/yui3/dial/assets/skins/night/dial.css8
-rw-r--r--js/yui3/dial/assets/skins/sam/dial.css8
-rw-r--r--js/yui3/dial/dial-min.js10
-rw-r--r--js/yui3/dial/lang/dial.js8
-rw-r--r--js/yui3/dial/lang/dial_en.js8
-rw-r--r--js/yui3/dial/lang/dial_es.js8
-rw-r--r--js/yui3/dial/lang/dial_hu.js8
-rw-r--r--js/yui3/dom-base/dom-base-min.js9
-rw-r--r--js/yui3/dom-core/dom-core-min.js8
-rw-r--r--js/yui3/dom-screen/dom-screen-min.js8
-rw-r--r--js/yui3/dom-style-ie/dom-style-ie-min.js8
-rw-r--r--js/yui3/dom-style/dom-style-min.js8
-rw-r--r--js/yui3/dump/dump-min.js8
-rw-r--r--js/yui3/editor-base/editor-base-min.js9
-rw-r--r--js/yui3/editor-bidi/editor-bidi-min.js8
-rw-r--r--js/yui3/editor-br/editor-br-min.js8
-rw-r--r--js/yui3/editor-inline/editor-inline-min.js8
-rw-r--r--js/yui3/editor-lists/editor-lists-min.js8
-rw-r--r--js/yui3/editor-para-base/editor-para-base-min.js8
-rw-r--r--js/yui3/editor-para-ie/editor-para-ie-min.js8
-rw-r--r--js/yui3/editor-para/editor-para-min.js8
-rw-r--r--js/yui3/editor-selection/editor-selection-min.js9
-rw-r--r--js/yui3/editor-tab/editor-tab-min.js8
-rw-r--r--js/yui3/escape/escape-min.js8
-rw-r--r--js/yui3/event-base-ie/event-base-ie-min.js10
-rw-r--r--js/yui3/event-base/event-base-min.js9
-rw-r--r--js/yui3/event-contextmenu/event-contextmenu-min.js8
-rw-r--r--js/yui3/event-custom-base/event-custom-base-min.js10
-rw-r--r--js/yui3/event-custom-complex/event-custom-complex-min.js8
-rw-r--r--js/yui3/event-delegate/event-delegate-min.js8
-rw-r--r--js/yui3/event-flick/event-flick-min.js8
-rw-r--r--js/yui3/event-focus/event-focus-min.js8
-rw-r--r--js/yui3/event-hover/event-hover-min.js8
-rw-r--r--js/yui3/event-key/event-key-min.js8
-rw-r--r--js/yui3/event-mouseenter/event-mouseenter-min.js8
-rw-r--r--js/yui3/event-mousewheel/event-mousewheel-min.js8
-rw-r--r--js/yui3/event-move/event-move-min.js8
-rw-r--r--js/yui3/event-outside/event-outside-min.js8
-rw-r--r--js/yui3/event-resize/event-resize-min.js8
-rw-r--r--js/yui3/event-simulate/event-simulate-min.js9
-rw-r--r--js/yui3/event-synthetic/event-synthetic-min.js8
-rw-r--r--js/yui3/event-tap/event-tap-min.js8
-rw-r--r--js/yui3/event-touch/event-touch-min.js8
-rw-r--r--js/yui3/event-valuechange/event-valuechange-min.js8
-rw-r--r--js/yui3/exec-command/exec-command-min.js9
-rw-r--r--js/yui3/features/features-min.js8
-rw-r--r--js/yui3/file-flash/file-flash-min.js8
-rw-r--r--js/yui3/file-html5/file-html5-min.js8
-rw-r--r--js/yui3/file/file-min.js8
-rw-r--r--js/yui3/frame/frame-min.js9
-rw-r--r--js/yui3/gallery-datatable-paginator/gallery-datatable-paginator-min.js2
-rw-r--r--js/yui3/gallery-datatable-row-expansion-bmo/assets/skins/sam/closed.pngbin0 -> 218 bytes
-rw-r--r--js/yui3/gallery-datatable-row-expansion-bmo/assets/skins/sam/gallery-datatable-row-expansion-bmo.css1
-rw-r--r--js/yui3/gallery-datatable-row-expansion-bmo/assets/skins/sam/open.pngbin0 -> 203 bytes
-rw-r--r--js/yui3/gallery-datatable-row-expansion-bmo/gallery-datatable-row-expansion-bmo-min.js383
-rw-r--r--js/yui3/gallery-funcprog/gallery-funcprog-min.js1
-rw-r--r--js/yui3/gallery-math/gallery-math-min.js1
-rw-r--r--js/yui3/gallery-node-optimizations/gallery-node-optimizations-min.js1
-rw-r--r--js/yui3/gallery-object-extras/gallery-object-extras-min.js1
-rw-r--r--js/yui3/gallery-paginator-view/assets/skins/sam/gallery-paginator-view.css1
-rw-r--r--js/yui3/gesture-simulate/gesture-simulate-min.js9
-rw-r--r--js/yui3/get-nodejs/get-nodejs-min.js8
-rw-r--r--js/yui3/get/get-min.js9
-rw-r--r--js/yui3/graphics-canvas-default/graphics-canvas-default-min.js8
-rw-r--r--js/yui3/graphics-canvas/graphics-canvas-min.js12
-rw-r--r--js/yui3/graphics-group/graphics-group-min.js8
-rw-r--r--js/yui3/graphics-svg-default/graphics-svg-default-min.js8
-rw-r--r--js/yui3/graphics-svg/graphics-svg-min.js12
-rw-r--r--js/yui3/graphics-vml-default/graphics-vml-default-min.js8
-rw-r--r--js/yui3/graphics-vml/graphics-vml-min.js12
-rw-r--r--js/yui3/graphics/graphics-min.js8
-rw-r--r--js/yui3/handlebars-base/handlebars-base-min.js13
-rw-r--r--js/yui3/handlebars-compiler/handlebars-compiler-min.js18
-rw-r--r--js/yui3/highlight-accentfold/highlight-accentfold-min.js8
-rw-r--r--js/yui3/highlight-base/highlight-base-min.js8
-rw-r--r--js/yui3/history-base/history-base-min.js8
-rw-r--r--js/yui3/history-hash-ie/history-hash-ie-min.js8
-rw-r--r--js/yui3/history-hash/history-hash-min.js8
-rw-r--r--js/yui3/history-html5/history-html5-min.js8
-rw-r--r--js/yui3/imageloader/imageloader-min.js8
-rw-r--r--js/yui3/intl-base/intl-base-min.js8
-rw-r--r--js/yui3/intl/intl-min.js8
-rw-r--r--js/yui3/io-base/io-base-min.js8
-rw-r--r--js/yui3/io-form/io-form-min.js8
-rw-r--r--js/yui3/io-nodejs/io-nodejs-min.js8
-rw-r--r--js/yui3/io-queue/io-queue-min.js8
-rw-r--r--js/yui3/io-upload-iframe/io-upload-iframe-min.js8
-rw-r--r--js/yui3/io-xdr/io-xdr-min.js8
-rw-r--r--js/yui3/io-xdr/io.swfbin0 -> 2247 bytes
-rw-r--r--js/yui3/json-parse-shim/json-parse-shim-min.js8
-rw-r--r--js/yui3/json-parse/json-parse-min.js8
-rw-r--r--js/yui3/json-stringify-shim/json-stringify-shim-min.js8
-rw-r--r--js/yui3/json-stringify/json-stringify-min.js8
-rw-r--r--js/yui3/jsonp-url/jsonp-url-min.js8
-rw-r--r--js/yui3/jsonp/jsonp-min.js8
-rw-r--r--js/yui3/lazy-model-list/lazy-model-list-min.js8
-rw-r--r--js/yui3/loader-base/loader-base-min.js11
-rw-r--r--js/yui3/loader-rollup/loader-rollup-min.js8
-rw-r--r--js/yui3/loader-yui3/loader-yui3-min.js13
-rw-r--r--js/yui3/loader/loader-min.js16
-rw-r--r--js/yui3/matrix/matrix-min.js9
-rw-r--r--js/yui3/model-list/model-list-min.js8
-rw-r--r--js/yui3/model-sync-local/model-sync-local-min.js8
-rw-r--r--js/yui3/model-sync-rest/model-sync-rest-min.js8
-rw-r--r--js/yui3/model/model-min.js8
-rw-r--r--js/yui3/node-base/node-base-min.js8
-rw-r--r--js/yui3/node-core/node-core-min.js9
-rw-r--r--js/yui3/node-event-delegate/node-event-delegate-min.js8
-rw-r--r--js/yui3/node-event-html5/node-event-html5-min.js8
-rw-r--r--js/yui3/node-event-simulate/node-event-simulate-min.js8
-rw-r--r--js/yui3/node-flick/assets/node-flick-core.css15
-rw-r--r--js/yui3/node-flick/assets/skins/sam/node-flick.css8
-rw-r--r--js/yui3/node-flick/node-flick-min.js8
-rw-r--r--js/yui3/node-focusmanager/node-focusmanager-min.js8
-rw-r--r--js/yui3/node-load/node-load-min.js8
-rw-r--r--js/yui3/node-menunav/assets/node-menunav-core.css176
-rw-r--r--js/yui3/node-menunav/assets/skins/night/horizontal-menu-submenu-indicator.pngbin0 -> 157 bytes
-rw-r--r--js/yui3/node-menunav/assets/skins/night/node-menunav.css8
-rw-r--r--js/yui3/node-menunav/assets/skins/night/vertical-menu-submenu-indicator.pngbin0 -> 156 bytes
-rw-r--r--js/yui3/node-menunav/assets/skins/sam/horizontal-menu-submenu-indicator.pngbin0 -> 157 bytes
-rw-r--r--js/yui3/node-menunav/assets/skins/sam/horizontal-menu-submenu-toggle.pngbin0 -> 183 bytes
-rw-r--r--js/yui3/node-menunav/assets/skins/sam/node-menunav.css8
-rw-r--r--js/yui3/node-menunav/assets/skins/sam/vertical-menu-submenu-indicator.pngbin0 -> 156 bytes
-rw-r--r--js/yui3/node-menunav/node-menunav-min.js9
-rw-r--r--js/yui3/node-pluginhost/node-pluginhost-min.js8
-rw-r--r--js/yui3/node-screen/node-screen-min.js8
-rw-r--r--js/yui3/node-scroll-info/node-scroll-info-min.js8
-rw-r--r--js/yui3/node-style/node-style-min.js8
-rw-r--r--js/yui3/oop/oop-min.js8
-rw-r--r--js/yui3/overlay/assets/overlay-core.css18
-rw-r--r--js/yui3/overlay/assets/skins/night/overlay.css8
-rw-r--r--js/yui3/overlay/assets/skins/sam/overlay.css8
-rw-r--r--js/yui3/overlay/overlay-min.js8
-rw-r--r--js/yui3/paginator-core/paginator-core-min.js8
-rw-r--r--js/yui3/paginator-url/paginator-url-min.js8
-rw-r--r--js/yui3/paginator/paginator-min.js8
-rw-r--r--js/yui3/panel/assets/panel-core.css29
-rw-r--r--js/yui3/panel/assets/skins/night/panel.css8
-rw-r--r--js/yui3/panel/assets/skins/night/sprite_icons.pngbin0 -> 176 bytes
-rw-r--r--js/yui3/panel/assets/skins/sam/panel.css8
-rw-r--r--js/yui3/panel/assets/skins/sam/sprite_icons.pngbin0 -> 176 bytes
-rw-r--r--js/yui3/panel/panel-min.js8
-rw-r--r--js/yui3/parallel/parallel-min.js8
-rw-r--r--js/yui3/pjax-base/pjax-base-min.js8
-rw-r--r--js/yui3/pjax-content/pjax-content-min.js8
-rw-r--r--js/yui3/pjax-plugin/pjax-plugin-min.js8
-rw-r--r--js/yui3/pjax/pjax-min.js8
-rw-r--r--js/yui3/plugin/plugin-min.js8
-rw-r--r--js/yui3/pluginhost-base/pluginhost-base-min.js8
-rw-r--r--js/yui3/pluginhost-config/pluginhost-config-min.js8
-rw-r--r--js/yui3/promise/promise-min.js8
-rw-r--r--js/yui3/querystring-parse-simple/querystring-parse-simple-min.js8
-rw-r--r--js/yui3/querystring-parse/querystring-parse-min.js8
-rw-r--r--js/yui3/querystring-stringify-simple/querystring-stringify-simple-min.js8
-rw-r--r--js/yui3/querystring-stringify/querystring-stringify-min.js8
-rw-r--r--js/yui3/queue-promote/queue-promote-min.js8
-rw-r--r--js/yui3/range-slider/assets/thumb-x-oblong-dark.pngbin0 -> 4042 bytes
-rw-r--r--js/yui3/range-slider/assets/thumb-x-oblong.pngbin0 -> 961 bytes
-rw-r--r--js/yui3/range-slider/assets/thumb-x-oblong2-dark.pngbin0 -> 4045 bytes
-rw-r--r--js/yui3/range-slider/assets/thumb-x-oblong2.pngbin0 -> 706 bytes
-rw-r--r--js/yui3/range-slider/assets/thumb-y-oblong-dark.pngbin0 -> 519 bytes
-rw-r--r--js/yui3/range-slider/assets/thumb-y-oblong.pngbin0 -> 1023 bytes
-rw-r--r--js/yui3/range-slider/assets/thumb-y-oblong2-dark.pngbin0 -> 706 bytes
-rw-r--r--js/yui3/range-slider/assets/thumb-y-oblong2.pngbin0 -> 746 bytes
-rw-r--r--js/yui3/range-slider/range-slider-min.js8
-rw-r--r--js/yui3/recordset-base/recordset-base-min.js8
-rw-r--r--js/yui3/recordset-filter/recordset-filter-min.js8
-rw-r--r--js/yui3/recordset-indexer/recordset-indexer-min.js8
-rw-r--r--js/yui3/recordset-sort/recordset-sort-min.js8
-rw-r--r--js/yui3/resize-base/assets/resize-base-core.css248
-rw-r--r--js/yui3/resize-base/assets/skins/night/arrows.pngbin0 -> 258 bytes
-rw-r--r--js/yui3/resize-base/assets/skins/night/resize-base.css8
-rw-r--r--js/yui3/resize-base/assets/skins/sam/arrows.pngbin0 -> 258 bytes
-rw-r--r--js/yui3/resize-base/assets/skins/sam/resize-base.css8
-rw-r--r--js/yui3/resize-base/resize-base-min.js9
-rw-r--r--js/yui3/resize-constrain/assets/skins/night/arrows.pngbin0 -> 258 bytes
-rw-r--r--js/yui3/resize-constrain/assets/skins/sam/arrows.pngbin0 -> 258 bytes
-rw-r--r--js/yui3/resize-constrain/resize-constrain-min.js8
-rw-r--r--js/yui3/resize-plugin/assets/skins/night/arrows.pngbin0 -> 258 bytes
-rw-r--r--js/yui3/resize-plugin/assets/skins/sam/arrows.pngbin0 -> 258 bytes
-rw-r--r--js/yui3/resize-plugin/resize-plugin-min.js8
-rw-r--r--js/yui3/resize-proxy/assets/skins/night/arrows.pngbin0 -> 258 bytes
-rw-r--r--js/yui3/resize-proxy/assets/skins/sam/arrows.pngbin0 -> 258 bytes
-rw-r--r--js/yui3/resize-proxy/resize-proxy-min.js8
-rw-r--r--js/yui3/router/router-min.js9
-rw-r--r--js/yui3/scrollview-base-ie/scrollview-base-ie-min.js8
-rw-r--r--js/yui3/scrollview-base/assets/scrollview-base-core.css21
-rw-r--r--js/yui3/scrollview-base/assets/skins/night/scrollview-base.css8
-rw-r--r--js/yui3/scrollview-base/assets/skins/sam/scrollview-base.css8
-rw-r--r--js/yui3/scrollview-base/scrollview-base-min.js9
-rw-r--r--js/yui3/scrollview-list/assets/scrollview-list-core.css7
-rw-r--r--js/yui3/scrollview-list/assets/skins/night/scrollview-list.css8
-rw-r--r--js/yui3/scrollview-list/assets/skins/sam/scrollview-list.css8
-rw-r--r--js/yui3/scrollview-list/scrollview-list-min.js8
-rw-r--r--js/yui3/scrollview-paginator/scrollview-paginator-min.js8
-rw-r--r--js/yui3/scrollview-scrollbars/assets/scrollview-scrollbars-core.css102
-rw-r--r--js/yui3/scrollview-scrollbars/assets/skins/night/scrollview-scrollbars.css8
-rw-r--r--js/yui3/scrollview-scrollbars/assets/skins/sam/scrollview-scrollbars.css8
-rw-r--r--js/yui3/scrollview-scrollbars/scrollview-scrollbars-min.js8
-rw-r--r--js/yui3/scrollview/scrollview-min.js8
-rw-r--r--js/yui3/selector-css2/selector-css2-min.js8
-rw-r--r--js/yui3/selector-css3/selector-css3-min.js8
-rw-r--r--js/yui3/selector-native/selector-native-min.js8
-rw-r--r--js/yui3/selector/selector-min.js8
-rw-r--r--js/yui3/series-area-stacked/series-area-stacked-min.js8
-rw-r--r--js/yui3/series-area/series-area-min.js8
-rw-r--r--js/yui3/series-areaspline-stacked/series-areaspline-stacked-min.js8
-rw-r--r--js/yui3/series-areaspline/series-areaspline-min.js8
-rw-r--r--js/yui3/series-bar-stacked/series-bar-stacked-min.js8
-rw-r--r--js/yui3/series-bar/series-bar-min.js8
-rw-r--r--js/yui3/series-base/series-base-min.js8
-rw-r--r--js/yui3/series-candlestick/series-candlestick-min.js8
-rw-r--r--js/yui3/series-cartesian/series-cartesian-min.js9
-rw-r--r--js/yui3/series-column-stacked/series-column-stacked-min.js8
-rw-r--r--js/yui3/series-column/series-column-min.js8
-rw-r--r--js/yui3/series-combo-stacked/series-combo-stacked-min.js8
-rw-r--r--js/yui3/series-combo/series-combo-min.js8
-rw-r--r--js/yui3/series-combospline-stacked/series-combospline-stacked-min.js8
-rw-r--r--js/yui3/series-combospline/series-combospline-min.js8
-rw-r--r--js/yui3/series-curve-util/series-curve-util-min.js8
-rw-r--r--js/yui3/series-fill-util/series-fill-util-min.js8
-rw-r--r--js/yui3/series-histogram-base/series-histogram-base-min.js8
-rw-r--r--js/yui3/series-line-stacked/series-line-stacked-min.js8
-rw-r--r--js/yui3/series-line-util/series-line-util-min.js8
-rw-r--r--js/yui3/series-line/series-line-min.js8
-rw-r--r--js/yui3/series-marker-stacked/series-marker-stacked-min.js8
-rw-r--r--js/yui3/series-marker/series-marker-min.js8
-rw-r--r--js/yui3/series-ohlc/series-ohlc-min.js8
-rw-r--r--js/yui3/series-pie/series-pie-min.js9
-rw-r--r--js/yui3/series-plot-util/series-plot-util-min.js8
-rw-r--r--js/yui3/series-range/series-range-min.js8
-rw-r--r--js/yui3/series-spline-stacked/series-spline-stacked-min.js8
-rw-r--r--js/yui3/series-spline/series-spline-min.js8
-rw-r--r--js/yui3/series-stacked/series-stacked-min.js8
-rw-r--r--js/yui3/shim-plugin/shim-plugin-min.js8
-rw-r--r--js/yui3/slider-base/assets/skins/audio-light/rail-x.pngbin0 -> 3668 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/audio-light/rail-y.pngbin0 -> 3664 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/audio-light/slider-base.css8
-rw-r--r--js/yui3/slider-base/assets/skins/audio-light/slider-skin.css98
-rw-r--r--js/yui3/slider-base/assets/skins/audio-light/thumb-x.pngbin0 -> 400 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/audio-light/thumb-y.pngbin0 -> 503 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/audio/rail-x.pngbin0 -> 3662 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/audio/rail-y.pngbin0 -> 3660 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/audio/slider-base.css8
-rw-r--r--js/yui3/slider-base/assets/skins/audio/slider-skin.css98
-rw-r--r--js/yui3/slider-base/assets/skins/audio/thumb-x.pngbin0 -> 3742 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/audio/thumb-y.pngbin0 -> 414 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule-dark/rail-x-dots.pngbin0 -> 3651 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule-dark/rail-x-lines.pngbin0 -> 3673 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule-dark/rail-x.pngbin0 -> 3710 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule-dark/rail-y-dots.pngbin0 -> 3642 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule-dark/rail-y-lines.pngbin0 -> 3667 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule-dark/rail-y.pngbin0 -> 3673 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule-dark/slider-base.css8
-rw-r--r--js/yui3/slider-base/assets/skins/capsule-dark/slider-skin.css98
-rw-r--r--js/yui3/slider-base/assets/skins/capsule-dark/thumb-x-line.pngbin0 -> 3845 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule-dark/thumb-x.pngbin0 -> 4166 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule-dark/thumb-y-line.pngbin0 -> 518 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule-dark/thumb-y.pngbin0 -> 685 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule/rail-x-dots.pngbin0 -> 3655 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule/rail-x-lines.pngbin0 -> 3669 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule/rail-x.pngbin0 -> 3701 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule/rail-y-dots.pngbin0 -> 3643 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule/rail-y-lines.pngbin0 -> 3643 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule/rail-y.pngbin0 -> 3689 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule/slider-base.css8
-rw-r--r--js/yui3/slider-base/assets/skins/capsule/slider-skin.css100
-rw-r--r--js/yui3/slider-base/assets/skins/capsule/thumb-x-line.pngbin0 -> 828 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule/thumb-x.pngbin0 -> 768 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule/thumb-y-line.pngbin0 -> 669 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule/thumb-y-lines.pngbin0 -> 3665 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/capsule/thumb-y.pngbin0 -> 541 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/night/rail-x-lines.pngbin0 -> 301 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/night/rail-x.pngbin0 -> 273 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/night/rail-y-lines.pngbin0 -> 262 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/night/rail-y.pngbin0 -> 321 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/night/slider-base.css8
-rw-r--r--js/yui3/slider-base/assets/skins/night/slider-skin.css94
-rw-r--r--js/yui3/slider-base/assets/skins/night/thumb-x.pngbin0 -> 467 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/night/thumb-y.pngbin0 -> 566 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/round-dark/rail-x.pngbin0 -> 3637 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/round-dark/rail-y.pngbin0 -> 3635 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/round-dark/slider-base.css8
-rw-r--r--js/yui3/slider-base/assets/skins/round-dark/slider-skin.css96
-rw-r--r--js/yui3/slider-base/assets/skins/round-dark/thumb-x-grip.pngbin0 -> 4365 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/round-dark/thumb-x.pngbin0 -> 4333 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/round-dark/thumb-y-grip.pngbin0 -> 648 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/round-dark/thumb-y.pngbin0 -> 835 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/round/rail-x.pngbin0 -> 3642 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/round/rail-y.pngbin0 -> 3637 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/round/slider-base.css8
-rw-r--r--js/yui3/slider-base/assets/skins/round/slider-skin.css96
-rw-r--r--js/yui3/slider-base/assets/skins/round/thumb-x-grip.pngbin0 -> 692 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/round/thumb-x.pngbin0 -> 4143 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/round/thumb-y-grip.pngbin0 -> 731 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/round/thumb-y.pngbin0 -> 890 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/sam-dark/rail-x-lines.pngbin0 -> 3647 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/sam-dark/rail-x.pngbin0 -> 3635 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/sam-dark/rail-y-lines.pngbin0 -> 3640 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/sam-dark/rail-y.pngbin0 -> 3628 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/sam-dark/slider-base.css8
-rw-r--r--js/yui3/slider-base/assets/skins/sam-dark/slider-skin.css94
-rw-r--r--js/yui3/slider-base/assets/skins/sam-dark/thumb-x.pngbin0 -> 3853 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/sam-dark/thumb-y.pngbin0 -> 602 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/sam/rail-x-lines.pngbin0 -> 3656 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/sam/rail-x.pngbin0 -> 3639 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/sam/rail-y-lines.pngbin0 -> 3642 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/sam/rail-y.pngbin0 -> 3629 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/sam/slider-base.css8
-rw-r--r--js/yui3/slider-base/assets/skins/sam/slider-skin.css94
-rw-r--r--js/yui3/slider-base/assets/skins/sam/thumb-x.pngbin0 -> 3873 bytes
-rw-r--r--js/yui3/slider-base/assets/skins/sam/thumb-y.pngbin0 -> 3860 bytes
-rw-r--r--js/yui3/slider-base/assets/slider-base-core.css38
-rw-r--r--js/yui3/slider-base/assets/slider-core.css38
-rw-r--r--js/yui3/slider-base/assets/thumb-x-oblong-dark.pngbin0 -> 4042 bytes
-rw-r--r--js/yui3/slider-base/assets/thumb-x-oblong.pngbin0 -> 961 bytes
-rw-r--r--js/yui3/slider-base/assets/thumb-x-oblong2-dark.pngbin0 -> 4045 bytes
-rw-r--r--js/yui3/slider-base/assets/thumb-x-oblong2.pngbin0 -> 706 bytes
-rw-r--r--js/yui3/slider-base/assets/thumb-y-oblong-dark.pngbin0 -> 519 bytes
-rw-r--r--js/yui3/slider-base/assets/thumb-y-oblong.pngbin0 -> 1023 bytes
-rw-r--r--js/yui3/slider-base/assets/thumb-y-oblong2-dark.pngbin0 -> 706 bytes
-rw-r--r--js/yui3/slider-base/assets/thumb-y-oblong2.pngbin0 -> 746 bytes
-rw-r--r--js/yui3/slider-base/slider-base-min.js8
-rw-r--r--js/yui3/slider-value-range/assets/thumb-x-oblong-dark.pngbin0 -> 4042 bytes
-rw-r--r--js/yui3/slider-value-range/assets/thumb-x-oblong.pngbin0 -> 961 bytes
-rw-r--r--js/yui3/slider-value-range/assets/thumb-x-oblong2-dark.pngbin0 -> 4045 bytes
-rw-r--r--js/yui3/slider-value-range/assets/thumb-x-oblong2.pngbin0 -> 706 bytes
-rw-r--r--js/yui3/slider-value-range/assets/thumb-y-oblong-dark.pngbin0 -> 519 bytes
-rw-r--r--js/yui3/slider-value-range/assets/thumb-y-oblong.pngbin0 -> 1023 bytes
-rw-r--r--js/yui3/slider-value-range/assets/thumb-y-oblong2-dark.pngbin0 -> 706 bytes
-rw-r--r--js/yui3/slider-value-range/assets/thumb-y-oblong2.pngbin0 -> 746 bytes
-rw-r--r--js/yui3/slider-value-range/slider-value-range-min.js8
-rw-r--r--js/yui3/sortable-scroll/sortable-scroll-min.js8
-rw-r--r--js/yui3/sortable/sortable-min.js8
-rw-r--r--js/yui3/stylesheet/stylesheet-min.js8
-rw-r--r--js/yui3/substitute/substitute-min.js8
-rw-r--r--js/yui3/swf/swf-min.js8
-rw-r--r--js/yui3/swfdetect/swfdetect-min.js8
-rw-r--r--js/yui3/tabview-base/tabview-base-min.js8
-rw-r--r--js/yui3/tabview-plugin/tabview-plugin-min.js8
-rw-r--r--js/yui3/tabview/assets/skins/night/tabview.css8
-rw-r--r--js/yui3/tabview/assets/skins/sam/tabview.css8
-rw-r--r--js/yui3/tabview/assets/tabview-core.css49
-rw-r--r--js/yui3/tabview/tabview-min.js8
-rw-r--r--js/yui3/template-base/template-base-min.js8
-rw-r--r--js/yui3/template-micro/template-micro-min.js8
-rw-r--r--js/yui3/test-console/assets/skins/sam/test-console.css8
-rw-r--r--js/yui3/test-console/assets/test-console-core.css15
-rw-r--r--js/yui3/test-console/test-console-min.js8
-rw-r--r--js/yui3/test/test-min.js13
-rw-r--r--js/yui3/text-accentfold/text-accentfold-min.js8
-rw-r--r--js/yui3/text-data-accentfold/text-data-accentfold-min.js8
-rw-r--r--js/yui3/text-data-wordbreak/text-data-wordbreak-min.js9
-rw-r--r--js/yui3/text-wordbreak/text-wordbreak-min.js8
-rw-r--r--js/yui3/timers/timers-min.js8
-rw-r--r--js/yui3/transition-timer/transition-timer-min.js8
-rw-r--r--js/yui3/transition/transition-min.js9
-rw-r--r--js/yui3/tree-labelable/tree-labelable-min.js8
-rw-r--r--js/yui3/tree-lazy/tree-lazy-min.js8
-rw-r--r--js/yui3/tree-node/tree-node-min.js8
-rw-r--r--js/yui3/tree-openable/tree-openable-min.js8
-rw-r--r--js/yui3/tree-selectable/tree-selectable-min.js8
-rw-r--r--js/yui3/tree-sortable/tree-sortable-min.js8
-rw-r--r--js/yui3/tree/tree-min.js8
-rw-r--r--js/yui3/uploader-deprecated/assets/uploader.swfbin0 -> 6611 bytes
-rw-r--r--js/yui3/uploader-flash/assets/uploader-flash-core.css11
-rw-r--r--js/yui3/uploader-flash/uploader-flash-min.js10
-rw-r--r--js/yui3/uploader-html5/assets/uploader-flash-core.css11
-rw-r--r--js/yui3/uploader-html5/uploader-html5-min.js9
-rw-r--r--js/yui3/uploader-queue/assets/uploader-flash-core.css11
-rw-r--r--js/yui3/uploader-queue/uploader-queue-min.js8
-rw-r--r--js/yui3/uploader/assets/flashuploader.swfbin0 -> 5080 bytes
-rw-r--r--js/yui3/uploader/assets/uploader-flash-core.css11
-rw-r--r--js/yui3/uploader/uploader-min.js8
-rw-r--r--js/yui3/view-node-map/view-node-map-min.js8
-rw-r--r--js/yui3/view/view-min.js8
-rw-r--r--js/yui3/widget-anim/widget-anim-min.js8
-rw-r--r--js/yui3/widget-autohide/widget-autohide-min.js8
-rw-r--r--js/yui3/widget-base-ie/widget-base-ie-min.js8
-rw-r--r--js/yui3/widget-base/assets/skins/night/widget-base.css8
-rw-r--r--js/yui3/widget-base/assets/skins/sam/widget-base.css8
-rw-r--r--js/yui3/widget-base/assets/widget-base-core.css27
-rw-r--r--js/yui3/widget-base/widget-base-min.js9
-rw-r--r--js/yui3/widget-buttons/assets/skins/night/sprite_icons.gifbin0 -> 142 bytes
-rw-r--r--js/yui3/widget-buttons/assets/skins/sam/sprite_icons.gifbin0 -> 142 bytes
-rw-r--r--js/yui3/widget-buttons/widget-buttons-min.js9
-rw-r--r--js/yui3/widget-child/widget-child-min.js8
-rw-r--r--js/yui3/widget-htmlparser/widget-htmlparser-min.js8
-rw-r--r--js/yui3/widget-modality/assets/skins/night/widget-modality.css8
-rw-r--r--js/yui3/widget-modality/assets/skins/sam/widget-modality.css8
-rw-r--r--js/yui3/widget-modality/assets/widget-modality-core.css8
-rw-r--r--js/yui3/widget-modality/widget-modality-min.js10
-rw-r--r--js/yui3/widget-parent/widget-parent-min.js8
-rw-r--r--js/yui3/widget-position-align/widget-position-align-min.js8
-rw-r--r--js/yui3/widget-position-constrain/widget-position-constrain-min.js8
-rw-r--r--js/yui3/widget-position/widget-position-min.js8
-rw-r--r--js/yui3/widget-skin/widget-skin-min.js8
-rw-r--r--js/yui3/widget-stack/assets/skins/night/widget-stack.css8
-rw-r--r--js/yui3/widget-stack/assets/skins/sam/widget-stack.css8
-rw-r--r--js/yui3/widget-stack/assets/widget-stack-core.css26
-rw-r--r--js/yui3/widget-stack/widget-stack-min.js8
-rw-r--r--js/yui3/widget-stdmod/widget-stdmod-min.js8
-rw-r--r--js/yui3/widget-uievents/widget-uievents-min.js8
-rw-r--r--js/yui3/yql-jsonp/yql-jsonp-min.js8
-rw-r--r--js/yui3/yql-nodejs/yql-nodejs-min.js8
-rw-r--r--js/yui3/yql-winjs/yql-winjs-min.js8
-rw-r--r--js/yui3/yql/yql-min.js8
-rw-r--r--js/yui3/yui-base/yui-base-min.js14
-rw-r--r--js/yui3/yui-core/yui-core-min.js11
-rw-r--r--js/yui3/yui-later/yui-later-min.js8
-rw-r--r--js/yui3/yui-log-nodejs/yui-log-nodejs-min.js8
-rw-r--r--js/yui3/yui-log/yui-log-min.js8
-rw-r--r--js/yui3/yui-nodejs/yui-nodejs-min.js22
-rw-r--r--js/yui3/yui-throttle/yui-throttle-min.js10
-rw-r--r--js/yui3/yui/yui-min.js23
-rwxr-xr-xlong_list.cgi36
-rwxr-xr-xmetrics.pl46
-rw-r--r--mod_perl.pl24
-rwxr-xr-xpage.cgi5
-rwxr-xr-xpost_bug.cgi40
-rwxr-xr-xprocess_bug.cgi98
-rwxr-xr-xquery.cgi9
-rwxr-xr-xquips.cgi1
-rwxr-xr-xrelogin.cgi13
-rwxr-xr-xreport.cgi15
-rwxr-xr-xrequest.cgi102
-rwxr-xr-xrest.cgi29
-rw-r--r--robots.txt18
-rwxr-xr-xsanitycheck.cgi26
-rwxr-xr-xsearch_plugin.cgi8
-rwxr-xr-xsentry.pl93
-rwxr-xr-xshow_activity.cgi4
-rwxr-xr-xshow_bug.cgi6
-rwxr-xr-xshowattachment.cgi40
-rwxr-xr-xshowdependencygraph.cgi16
-rwxr-xr-xshowdependencytree.cgi107
-rw-r--r--skins/README30
-rw-r--r--skins/contrib/Dusk-Helvetica/buglist.css24
-rw-r--r--skins/contrib/Dusk-Helvetica/global.css267
-rw-r--r--skins/contrib/Dusk-Helvetica/index.css9
-rw-r--r--skins/contrib/Dusk-Segoe/buglist.css24
-rw-r--r--skins/contrib/Dusk-Segoe/global.css267
-rw-r--r--skins/contrib/Dusk-Segoe/index.css9
-rw-r--r--skins/contrib/Dusk-Segoe/show_bug.css3
-rw-r--r--skins/contrib/Dusk/global.css10
-rw-r--r--skins/contrib/Mozilla-OpenSans/bugzilla-magnifier.pngbin0 -> 7491 bytes
-rw-r--r--skins/contrib/Mozilla-OpenSans/bugzilla-papericon.pngbin0 -> 3800 bytes
-rw-r--r--skins/contrib/Mozilla-OpenSans/bugzilla-person-alternate.pngbin0 -> 8359 bytes
-rw-r--r--skins/contrib/Mozilla-OpenSans/bugzilla-person.pngbin0 -> 6648 bytes
-rw-r--r--skins/contrib/Mozilla-OpenSans/bugzilla-questionmark2.pngbin0 -> 11552 bytes
-rw-r--r--skins/contrib/Mozilla-OpenSans/dropdown.pngbin0 -> 130 bytes
-rw-r--r--skins/contrib/Mozilla-OpenSans/footer-mozilla.pngbin0 -> 528 bytes
-rw-r--r--skins/contrib/Mozilla-OpenSans/global.css878
-rw-r--r--skins/contrib/Mozilla-OpenSans/grain.pngbin0 -> 47497 bytes
-rw-r--r--skins/contrib/Mozilla-OpenSans/index.css20
-rw-r--r--skins/contrib/Mozilla-OpenSans/noise.pngbin0 -> 3077 bytes
-rw-r--r--skins/contrib/Mozilla-OpenSans/opensans-bold.woffbin0 -> 22748 bytes
-rw-r--r--skins/contrib/Mozilla-OpenSans/opensans-semibold.woffbin0 -> 22604 bytes
-rw-r--r--skins/contrib/Mozilla-OpenSans/opensans.woffbin0 -> 21956 bytes
-rw-r--r--skins/contrib/Mozilla-OpenSans/search.pngbin0 -> 199 bytes
-rw-r--r--skins/contrib/Mozilla-OpenSans/tabzilla.pngbin0 -> 3518 bytes
-rw-r--r--skins/contrib/Mozilla/bugzilla-magnifier.pngbin0 -> 7491 bytes
-rw-r--r--skins/contrib/Mozilla/bugzilla-papericon.pngbin0 -> 3800 bytes
-rw-r--r--skins/contrib/Mozilla/bugzilla-person-alternate.pngbin0 -> 8359 bytes
-rw-r--r--skins/contrib/Mozilla/bugzilla-person.pngbin0 -> 6648 bytes
-rw-r--r--skins/contrib/Mozilla/bugzilla-questionmark2.pngbin0 -> 11552 bytes
-rw-r--r--skins/contrib/Mozilla/dropdown.pngbin0 -> 130 bytes
-rw-r--r--skins/contrib/Mozilla/fira/FiraMono-Bold.woffbin0 -> 70896 bytes
-rw-r--r--skins/contrib/Mozilla/fira/FiraMono-Regular.woffbin0 -> 66176 bytes
-rw-r--r--skins/contrib/Mozilla/fira/FiraSans-Bold.woffbin0 -> 88524 bytes
-rw-r--r--skins/contrib/Mozilla/fira/FiraSans-BoldItalic.woffbin0 -> 93844 bytes
-rw-r--r--skins/contrib/Mozilla/fira/FiraSans-Italic.woffbin0 -> 88544 bytes
-rw-r--r--skins/contrib/Mozilla/fira/FiraSans-Regular.woffbin0 -> 83492 bytes
-rw-r--r--skins/contrib/Mozilla/fira/FiraSans-SemiBold.woffbin0 -> 88208 bytes
-rw-r--r--skins/contrib/Mozilla/fira/FiraSans-SemiBoldItalic.woffbin0 -> 93324 bytes
-rw-r--r--skins/contrib/Mozilla/footer-mozilla.pngbin0 -> 528 bytes
-rw-r--r--skins/contrib/Mozilla/global.css921
-rw-r--r--skins/contrib/Mozilla/grain.pngbin0 -> 47497 bytes
-rw-r--r--skins/contrib/Mozilla/index.css20
-rw-r--r--skins/contrib/Mozilla/noise.pngbin0 -> 3077 bytes
-rw-r--r--skins/contrib/Mozilla/search.pngbin0 -> 199 bytes
-rw-r--r--skins/contrib/Mozilla/tabzilla.pngbin0 -> 3518 bytes
-rw-r--r--skins/custom/IE-fixes.css4
-rw-r--r--skins/custom/bug_groups.css31
-rw-r--r--skins/custom/buglist.css41
-rw-r--r--skins/custom/create_bug.css71
-rw-r--r--skins/custom/global.css77
-rw-r--r--skins/custom/index.css31
-rw-r--r--skins/custom/search_form.css6
-rw-r--r--skins/custom/show_bug.css85
-rw-r--r--skins/standard/attachment.css4
-rw-r--r--skins/standard/buglist.css25
-rw-r--r--skins/standard/enter_bug.css2
-rw-r--r--skins/standard/global.css97
-rw-r--r--skins/standard/guided.css4
-rw-r--r--skins/standard/reports.css5
-rw-r--r--skins/standard/show_bug.css39
-rw-r--r--t/001compile.t5
-rw-r--r--t/004template.t29
-rw-r--r--t/008filter.t5
-rw-r--r--t/012throwables.t4
-rw-r--r--t/Support/Files.pm13
-rw-r--r--template/en/default/account/auth/login-small.html.tmpl44
-rw-r--r--template/en/default/account/auth/login.html.tmpl8
-rw-r--r--template/en/default/account/create.html.tmpl2
-rw-r--r--template/en/default/account/prefs/email.html.tmpl47
-rw-r--r--template/en/default/account/prefs/permissions.html.tmpl4
-rw-r--r--template/en/default/account/prefs/saved-searches.html.tmpl2
-rw-r--r--template/en/default/account/prefs/settings.html.tmpl9
-rw-r--r--template/en/default/account/profile-activity.html.tmpl2
-rw-r--r--template/en/default/admin/flag-type/edit.html.tmpl2
-rw-r--r--template/en/default/admin/params/admin.html.tmpl7
-rw-r--r--template/en/default/admin/params/advanced.html.tmpl22
-rw-r--r--template/en/default/admin/params/attachment.html.tmpl4
-rw-r--r--template/en/default/admin/params/auth.html.tmpl6
-rw-r--r--template/en/default/admin/params/bugfields.html.tmpl6
-rw-r--r--template/en/default/admin/params/groupsecurity.html.tmpl3
-rw-r--r--template/en/default/admin/params/memcached.html.tmpl22
-rw-r--r--template/en/default/admin/users/edit.html.tmpl23
-rw-r--r--template/en/default/admin/users/list.html.tmpl14
-rw-r--r--template/en/default/attachment/create.html.tmpl12
-rw-r--r--template/en/default/attachment/createformcontents.html.tmpl2
-rw-r--r--template/en/default/attachment/delete_reason.txt.tmpl11
-rw-r--r--template/en/default/attachment/diff-footer.html.tmpl6
-rw-r--r--template/en/default/attachment/diff-header.html.tmpl21
-rw-r--r--template/en/default/attachment/edit.html.tmpl34
-rw-r--r--template/en/default/attachment/list.html.tmpl14
-rw-r--r--template/en/default/bug/activity/table.html.tmpl2
-rw-r--r--template/en/default/bug/comment.html.tmpl37
-rw-r--r--template/en/default/bug/comments.html.tmpl188
-rw-r--r--template/en/default/bug/create/comment-guided.txt.tmpl2
-rw-r--r--template/en/default/bug/create/create-guided.html.tmpl48
-rw-r--r--template/en/default/bug/create/create.html.tmpl425
-rw-r--r--template/en/default/bug/edit.html.tmpl499
-rw-r--r--template/en/default/bug/field-help.none.tmpl4
-rw-r--r--template/en/default/bug/field.html.tmpl100
-rw-r--r--template/en/default/bug/navigate.html.tmpl32
-rw-r--r--template/en/default/bug/process/bugmail.html.tmpl58
-rw-r--r--template/en/default/bug/process/updates-disabled.html.tmpl73
-rw-r--r--template/en/default/bug/process/verify-new-product.html.tmpl9
-rw-r--r--template/en/default/bug/show-header.html.tmpl43
-rw-r--r--template/en/default/bug/show-multiple.html.tmpl2
-rw-r--r--template/en/default/bug/show.xml.tmpl4
-rw-r--r--template/en/default/bug/time.html.tmpl8
-rw-r--r--template/en/default/config.rdf.tmpl4
-rw-r--r--template/en/default/email/bugmail-header.txt.tmpl20
-rw-r--r--template/en/default/email/bugmail.html.tmpl21
-rw-r--r--template/en/default/email/bugmail.txt.tmpl9
-rw-r--r--template/en/default/email/header-common.txt.tmpl24
-rw-r--r--template/en/default/email/lockout.txt.tmpl4
-rw-r--r--template/en/default/extensions/config.pm.tmpl27
-rw-r--r--template/en/default/extensions/extension.pm.tmpl29
-rw-r--r--template/en/default/extensions/hook-readme.txt.tmpl24
-rw-r--r--template/en/default/extensions/license.txt.tmpl49
-rw-r--r--template/en/default/extensions/name-readme.txt.tmpl24
-rw-r--r--template/en/default/extensions/util.pm.tmpl29
-rw-r--r--template/en/default/extensions/web-readme.txt.tmpl24
-rw-r--r--template/en/default/filterexceptions.pl5
-rw-r--r--template/en/default/flag/list.html.tmpl212
-rw-r--r--template/en/default/global/code-error.html.tmpl42
-rw-r--r--template/en/default/global/common-links.html.tmpl11
-rw-r--r--template/en/default/global/field-descs.none.tmpl8
-rw-r--r--template/en/default/global/footer.html.tmpl2
-rw-r--r--template/en/default/global/header.html.tmpl191
-rw-r--r--template/en/default/global/messages.html.tmpl1
-rw-r--r--template/en/default/global/setting-descs.none.tmpl5
-rw-r--r--template/en/default/global/user-error.html.tmpl124
-rw-r--r--template/en/default/global/user.html.tmpl17
-rw-r--r--template/en/default/global/userselect.html.tmpl2
-rw-r--r--template/en/default/index.html.tmpl77
-rw-r--r--template/en/default/list/change-columns.html.tmpl2
-rw-r--r--template/en/default/list/edit-multiple.html.tmpl17
-rw-r--r--template/en/default/list/list.html.tmpl41
-rw-r--r--template/en/default/list/table.html.tmpl81
-rw-r--r--template/en/default/pages/bugzilla.dtd.tmpl179
-rw-r--r--template/en/default/pages/fields.html.tmpl67
-rw-r--r--template/en/default/pages/quicksearch.html.tmpl54
-rw-r--r--template/en/default/reports/components.html.tmpl11
-rw-r--r--template/en/default/reports/report.html.tmpl4
-rw-r--r--template/en/default/request/email.txt.tmpl10
-rw-r--r--template/en/default/request/queue.csv.tmpl46
-rw-r--r--template/en/default/request/queue.html.tmpl25
-rw-r--r--template/en/default/rest.html.tmpl19
-rw-r--r--template/en/default/search/boolean-charts.html.tmpl15
-rw-r--r--template/en/default/search/field.html.tmpl4
-rw-r--r--template/en/default/search/form.html.tmpl7
-rw-r--r--template/en/default/search/search-google.html.tmpl45
-rw-r--r--template/en/default/search/search-instant.html.tmpl85
-rw-r--r--template/en/default/search/search-specific.html.tmpl11
-rw-r--r--template/en/default/search/tabs.html.tmpl8
-rw-r--r--template/en/default/setup/strings.txt.pl2
-rwxr-xr-xtoken.cgi3
-rwxr-xr-xuserprefs.cgi71
-rwxr-xr-xwhine.pl9
-rwxr-xr-xxml.cgi41
-rw-r--r--xt/lib/Bugzilla/Test/Search/FieldTest.pm11
1897 files changed, 101836 insertions, 3720 deletions
diff --git a/.bzrignore b/.bzrignore
deleted file mode 100644
index 7ab83e7ad..000000000
--- a/.bzrignore
+++ /dev/null
@@ -1,32 +0,0 @@
-.htaccess
-/lib/*
-/template/en/custom
-/docs/bugzilla.ent
-/docs/en/xml/bugzilla.ent
-/docs/en/txt
-/docs/en/html
-/docs/en/pdf
-/skins/custom
-/graphs
-/data
-/localconfig
-/index.html
-
-/skins/contrib/Dusk/IE-fixes.css
-/skins/contrib/Dusk/admin.css
-/skins/contrib/Dusk/attachment.css
-/skins/contrib/Dusk/create_attachment.css
-/skins/contrib/Dusk/dependency-tree.css
-/skins/contrib/Dusk/duplicates.css
-/skins/contrib/Dusk/editusers.css
-/skins/contrib/Dusk/enter_bug.css
-/skins/contrib/Dusk/help.css
-/skins/contrib/Dusk/panel.css
-/skins/contrib/Dusk/page.css
-/skins/contrib/Dusk/params.css
-/skins/contrib/Dusk/reports.css
-/skins/contrib/Dusk/show_bug.css
-/skins/contrib/Dusk/search_form.css
-/skins/contrib/Dusk/show_multiple.css
-/skins/contrib/Dusk/summarize-time.css
-.DS_Store
diff --git a/.gitignore b/.gitignore
index 7ab83e7ad..0370a07e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,27 +6,9 @@
/docs/en/txt
/docs/en/html
/docs/en/pdf
-/skins/custom
/graphs
/data
/localconfig
/index.html
-
-/skins/contrib/Dusk/IE-fixes.css
-/skins/contrib/Dusk/admin.css
-/skins/contrib/Dusk/attachment.css
-/skins/contrib/Dusk/create_attachment.css
-/skins/contrib/Dusk/dependency-tree.css
-/skins/contrib/Dusk/duplicates.css
-/skins/contrib/Dusk/editusers.css
-/skins/contrib/Dusk/enter_bug.css
-/skins/contrib/Dusk/help.css
-/skins/contrib/Dusk/panel.css
-/skins/contrib/Dusk/page.css
-/skins/contrib/Dusk/params.css
-/skins/contrib/Dusk/reports.css
-/skins/contrib/Dusk/show_bug.css
-/skins/contrib/Dusk/search_form.css
-/skins/contrib/Dusk/show_multiple.css
-/skins/contrib/Dusk/summarize-time.css
+/template_cache
.DS_Store
diff --git a/.htaccess b/.htaccess
index c16ee19af..c392c4653 100644
--- a/.htaccess
+++ b/.htaccess
@@ -1,5 +1,5 @@
# Don't allow people to retrieve non-cgi executable files or our private data
-<FilesMatch (\.pm|\.pl|\.tmpl|localconfig.*)$>
+<FilesMatch (\.pm|\.pl|\.tmpl|\.swf|localconfig.*)$>
deny from all
</FilesMatch>
<IfModule mod_expires.c>
@@ -23,3 +23,58 @@
</IfModule>
</IfModule>
</IfModule>
+
+<IfModule mod_deflate.c>
+ AddOutputFilterByType DEFLATE text/javascript application/json application/javascript text/css
+</IfModule>
+
+AddType image/x-icon .ico
+AddType application/font-woff .woff
+
+ErrorDocument 401 /errors/401.html
+ErrorDocument 403 /errors/403.html
+ErrorDocument 404 /errors/404.html
+ErrorDocument 500 /errors/500.html
+
+Redirect permanent /queryhelp.cgi https://bugzilla.mozilla.org/query.cgi?format=advanced&help=1
+Redirect permanent /bug_status.html https://bugzilla.mozilla.org/page.cgi?id=fields.html
+Redirect permanent /bugwritinghelp.html https://bugzilla.mozilla.org/page.cgi?id=bug-writing.html
+Redirect permanent /etiquette.html https://bugzilla.mozilla.org/page.cgi?id=etiquette.html
+Redirect permanent /duplicates.html https://bugzilla.mozilla.org/duplicates.cgi
+
+RewriteEngine On
+RewriteRule ^template_cache/ - [F,L,NC]
+RewriteRule ^template_cache.deleteme/ - [F,L,NC]
+RewriteRule ^review(.*) page.cgi?id=splinter.html$1 [QSA]
+RewriteRule ^user_?profile(.*) page.cgi?id=user_profile.html$1 [QSA]
+RewriteRule ^request_defer(.*) page.cgi?id=request_defer.html$1 [QSA]
+RewriteRule ^favicon\.ico$ extensions/BMO/web/images/favicon.ico
+RewriteRule ^form[\.:]itrequest$ enter_bug.cgi?product=Infrastructure+\%26+Operations&format=itrequest
+RewriteRule ^form[\.:](mozlist|poweredby|presentation|trademark|recoverykey)$ enter_bug.cgi?product=mozilla.org&format=$1
+RewriteRule ^form[\.:]legal$ enter_bug.cgi?product=Legal&format=legal
+RewriteRule ^form[\.:]mozpr$ enter_bug.cgi?product=Mozilla+PR&format=mozpr
+RewriteRule ^form[\.:]reps[\.:]mentorship$ enter_bug.cgi?product=Mozilla+Reps&format=mozreps
+RewriteRule ^form[\.:]reps[\.:]budget$ enter_bug.cgi?product=Mozilla+Reps&format=remo-budget
+RewriteRule ^form[\.:]reps[\.:]swag$ enter_bug.cgi?product=Mozilla+Reps&format=remo-swag
+RewriteRule ^form[\.:]reps[\.:]it$ enter_bug.cgi?product=Mozilla+Reps&format=remo-it
+RewriteRule ^form[\.:]reps[\.:]payment$ page.cgi?id=remo-form-payment.html
+RewriteRule ^form[\.:]employee[\.\-:]incident$ enter_bug.cgi?product=mozilla.org&format=employee-incident
+RewriteRule ^form[\.:]brownbag$ https://air.mozilla.org/requests
+RewriteRule ^form[\.:]finance$ enter_bug.cgi?product=Finance&format=finance
+RewriteRule ^form[\.:]privacy[\.\-:]data$ enter_bug.cgi?product=Privacy&format=privacy-data
+RewriteRule ^form[\.:]moz[\.\-:]project[\.\-:]review$ enter_bug.cgi?product=mozilla.org&format=moz-project-review
+RewriteRule ^form[\.:]docs?$ enter_bug.cgi?product=Developer+Documentation&format=doc [QSA]
+RewriteRule ^form[\.:]mdn?$ enter_bug.cgi?product=Mozilla+Developer+Network&format=mdn
+RewriteRule ^form[\.:](swag|gear)$ enter_bug.cgi?product=mozilla.org&format=swag
+RewriteRule ^form[\.:](b2g|fxos)[\.\-:](partner|betaprogram|feature) enter_bug.cgi?product=Firefox+OS&format=fxos-$2 [QSA]
+RewriteRule ^form[\.:]ipp$ enter_bug.cgi?product=Internet+Public+Policy&format=ipp
+RewriteRule ^form[\.:]creative$ enter_bug.cgi?product=Marketing&format=creative
+RewriteRule ^form[\.:]user[\.\-:]engagement$ enter_bug.cgi?product=Marketing&format=user-engagement
+RewriteRule ^form[\.:]dev[\.\-:]engagement[\.\-\:]event$ enter_bug.cgi?product=Developer+Engagement&format=dev-engagement-event
+RewriteRule ^form[\.:]mobile[\.\-:]compat$ enter_bug.cgi?product=Tech+Evangelism&format=mobile-compat
+RewriteRule ^form[\.:]web[\.:]bounty$ enter_bug.cgi?product=mozilla.org&format=web-bounty
+RewriteRule ^form[\.:]automative$ enter_bug.cgi?product=Testing&format=automative
+RewriteRule ^form[\.:]fxos[\.\-:]preload[\.\-:]app$ enter_bug.cgi?product=Marketplace&format=fxos-preload-app
+RewriteRule ^form[\.:]fxos[\.\-:]mcts[\.\-:]waiver$ enter_bug.cgi?product=Firefox+OS&format=fxos-mcts-waiver
+RewriteRule ^rest/(.*)$ rest.cgi/$1 [NE]
+RewriteRule ^bzapi/(.*)$ extensions/BzAPI/bin/rest.cgi/$1 [NE]
diff --git a/.perltidyrc b/.perltidyrc
new file mode 100644
index 000000000..288712461
--- /dev/null
+++ b/.perltidyrc
@@ -0,0 +1,16 @@
+--block-brace-tightness=0
+--brace-tightness=1
+--continuation-indentation=2
+--indent-columns=4
+--line-up-parentheses
+--maximum-line-length=120
+--noblanks-before-blocks
+--opening-brace-always-on-right
+--opening-token-right
+--paren-tightness=2
+--square-bracket-tightness=2
+--stack-closing-tokens
+--stack-opening-tokens
+--vertical-tightness-closing=2
+--vertical-tightness=2
+--want-break-after="."
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 68e2b47e2..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,48 +0,0 @@
-language: perl
-perl:
- - 5.10
- - 5.12
-
-env:
- - TEST_SUITE=sanity
- - TEST_SUITE=docs
- - TEST_SUITE=webservices DB=mysql
- - TEST_SUITE=selenium DB=mysql
- - TEST_SUITE=webservices DB=pg
- - TEST_SUITE=selenium DB=pg
-
-matrix:
- exclude:
- - perl: 5.12
- env: TEST_SUITE=docs
- - perl: 5.10
- env: TEST_SUITE=webservices DB=mysql
- - perl: 5.12
- env: TEST_SUITE=selenium DB=mysql
- - perl: 5.10
- env: TEST_SUITE=webservices DB=pg
- - perl: 5.12
- env: TEST_SUITE=selenium DB=pg
-
-before_install:
- - git clone https://github.com/bugzilla/qa.git -b 4.2 qa
-
-install: true
-
-script: ./qa/travis.sh
-
-after_failure:
- - sudo cat /var/log/apache2/error.log
-
-notifications:
- irc:
- channels:
- - "irc.mozilla.org#qa-bugzilla"
- - "irc.mozilla.org#bugzilla"
- template:
- - "Bugzilla %{branch} : %{author} : %{message}"
- - "Commit Message : %{commit_message}"
- - "Commit Link : %{compare_url}"
- - "Build Link : %{build_url}"
- on_success: change
- on_failure: always
diff --git a/Bugzilla.pm b/Bugzilla.pm
index 5b39e4c81..52a44e375 100644
--- a/Bugzilla.pm
+++ b/Bugzilla.pm
@@ -35,23 +35,29 @@ BEGIN {
}
}
-use Bugzilla::Config;
-use Bugzilla::Constants;
use Bugzilla::Auth;
use Bugzilla::Auth::Persist::Cookie;
use Bugzilla::CGI;
-use Bugzilla::Extension;
+use Bugzilla::Config;
+use Bugzilla::Constants;
use Bugzilla::DB;
+use Bugzilla::Error;
+use Bugzilla::Extension;
+use Bugzilla::Field;
+use Bugzilla::Flag;
+use Bugzilla::Hook;
use Bugzilla::Install::Localconfig qw(read_localconfig);
use Bugzilla::Install::Requirements qw(OPTIONAL_MODULES);
-use Bugzilla::Install::Util qw(init_console);
+use Bugzilla::Install::Util qw(init_console include_languages);
+use Bugzilla::Memcached;
use Bugzilla::Template;
+use Bugzilla::Token;
use Bugzilla::User;
-use Bugzilla::Error;
use Bugzilla::Util;
-use Bugzilla::Field;
-use Bugzilla::Flag;
-use Bugzilla::Token;
+
+use Bugzilla::Metrics::Collector;
+use Bugzilla::Metrics::Template;
+use Bugzilla::Metrics::Memcached;
use File::Basename;
use File::Spec::Functions;
@@ -84,7 +90,7 @@ use constant SHUTDOWNHTML_RETRY_AFTER => 3600;
# Global Code
#####################################################################
-# $::SIG{__DIE__} = i_am_cgi() ? \&CGI::Carp::confess : \&Carp::confess;
+#$::SIG{__DIE__} = i_am_cgi() ? \&CGI::Carp::confess : \&Carp::confess;
# Note that this is a raw subroutine, not a method, so $class isn't available.
sub init_page {
@@ -125,6 +131,30 @@ sub init_page {
my $script = basename($0);
+ # BMO - init metrics collection if required
+ if (i_am_cgi() && $script eq 'show_bug.cgi') {
+ # we need to measure loading the params, so default to on
+ Bugzilla->metrics_enabled(1);
+ Bugzilla->metrics($script);
+ # we can now hit params to check if we really should be enabled.
+ # note - we can't use anything which uses templates or the database, as
+ # that would initialise those modules with metrics enabled.
+ if (!Bugzilla->params->{metrics_enabled}) {
+ Bugzilla->metrics_enabled(0);
+ }
+ else {
+ # to avoid generating massive amounts of data, we're only interested in
+ # a small subset of users
+ my $user_id = Bugzilla->cgi->cookie('Bugzilla_login');
+ if (!$user_id
+ || !grep { $user_id == $_ }
+ split(/\s*,\s*/, Bugzilla->params->{metrics_user_ids}))
+ {
+ Bugzilla->metrics_enabled(0);
+ }
+ }
+ }
+
# Because of attachment_base, attachment.cgi handles this itself.
if ($script ne 'attachment.cgi') {
do_ssl_redirect_if_required();
@@ -193,9 +223,12 @@ sub init_page {
#####################################################################
sub template {
- my $class = shift;
- $class->request_cache->{template} ||= Bugzilla::Template->create();
- return $class->request_cache->{template};
+ # BMO - use metrics subclass if required
+ if (Bugzilla->metrics_enabled) {
+ return $_[0]->request_cache->{template} ||= Bugzilla::Metrics::Template->create();
+ } else {
+ return $_[0]->request_cache->{template} ||= Bugzilla::Template->create();
+ }
}
sub template_inner {
@@ -203,9 +236,7 @@ sub template_inner {
my $cache = $class->request_cache;
my $current_lang = $cache->{template_current_lang}->[0];
$lang ||= $current_lang || '';
- $class->request_cache->{"template_inner_$lang"}
- ||= Bugzilla::Template->create(language => $lang);
- return $class->request_cache->{"template_inner_$lang"};
+ return $cache->{"template_inner_$lang"} ||= Bugzilla::Template->create(language => $lang);
}
our $extension_packages;
@@ -264,9 +295,7 @@ sub feature {
}
sub cgi {
- my $class = shift;
- $class->request_cache->{cgi} ||= new Bugzilla::CGI();
- return $class->request_cache->{cgi};
+ return $_[0]->request_cache->{cgi} ||= new Bugzilla::CGI();
}
sub input_params {
@@ -286,21 +315,15 @@ sub input_params {
}
sub localconfig {
- my $class = shift;
- $class->request_cache->{localconfig} ||= read_localconfig();
- return $class->request_cache->{localconfig};
+ return $_[0]->process_cache->{localconfig} ||= read_localconfig();
}
sub params {
- my $class = shift;
- $class->request_cache->{params} ||= Bugzilla::Config::read_param_file();
- return $class->request_cache->{params};
+ return $_[0]->request_cache->{params} ||= Bugzilla::Config::read_param_file();
}
sub user {
- my $class = shift;
- $class->request_cache->{user} ||= new Bugzilla::User;
- return $class->request_cache->{user};
+ return $_[0]->request_cache->{user} ||= new Bugzilla::User;
}
sub set_user {
@@ -309,8 +332,7 @@ sub set_user {
}
sub sudoer {
- my $class = shift;
- return $class->request_cache->{sudoer};
+ return $_[0]->request_cache->{sudoer};
}
sub sudo_request {
@@ -388,6 +410,12 @@ sub login {
$class->set_user($authenticated_user);
}
+ if (Bugzilla->sudoer) {
+ Bugzilla->sudoer->update_last_seen_date();
+ } else {
+ $class->user->update_last_seen_date();
+ }
+
return $class->user;
}
@@ -426,31 +454,27 @@ sub logout_request {
}
sub job_queue {
- my $class = shift;
require Bugzilla::JobQueue;
- $class->request_cache->{job_queue} ||= Bugzilla::JobQueue->new();
- return $class->request_cache->{job_queue};
+ return $_[0]->request_cache->{job_queue} ||= Bugzilla::JobQueue->new();
}
sub dbh {
- my $class = shift;
# If we're not connected, then we must want the main db
- $class->request_cache->{dbh} ||= $class->dbh_main;
-
- return $class->request_cache->{dbh};
+ return $_[0]->request_cache->{dbh} ||= $_[0]->dbh_main;
}
sub dbh_main {
- my $class = shift;
- $class->request_cache->{dbh_main} ||= Bugzilla::DB::connect_main();
- return $class->request_cache->{dbh_main};
+ return $_[0]->request_cache->{dbh_main} ||= Bugzilla::DB::connect_main();
}
sub languages {
- my $class = shift;
return Bugzilla::Install::Util::supported_languages();
}
+sub current_language {
+ return $_[0]->request_cache->{current_language} ||= (include_languages())[0];
+}
+
sub error_mode {
my ($class, $newval) = @_;
if (defined $newval) {
@@ -490,6 +514,9 @@ sub usage_mode {
elsif ($newval == USAGE_MODE_TEST) {
$class->error_mode(ERROR_MODE_TEST);
}
+ elsif ($newval == USAGE_MODE_REST) {
+ $class->error_mode(ERROR_MODE_REST);
+ }
else {
ThrowCodeError('usage_mode_invalid',
{'invalid_usage_mode', $newval});
@@ -597,12 +624,20 @@ sub fields {
}
sub active_custom_fields {
- my $class = shift;
- if (!exists $class->request_cache->{active_custom_fields}) {
- $class->request_cache->{active_custom_fields} =
- Bugzilla::Field->match({ custom => 1, obsolete => 0 });
+ my ($class, $params) = @_;
+ my $cache_id = 'active_custom_fields';
+ if ($params) {
+ $cache_id .= ($params->{product} ? '_p' . $params->{product}->id : '') .
+ ($params->{component} ? '_c' . $params->{component}->id : '');
}
- return @{$class->request_cache->{active_custom_fields}};
+ if (!exists $class->request_cache->{$cache_id}) {
+ my $fields = Bugzilla::Field->match({ custom => 1, obsolete => 0});
+ @$fields = grep($_->type ne FIELD_TYPE_EXTENSION, @$fields);
+ Bugzilla::Hook::process('active_custom_fields',
+ { fields => \$fields, params => $params });
+ $class->request_cache->{$cache_id} = $fields;
+ }
+ return @{$class->request_cache->{$cache_id}};
}
sub has_flags {
@@ -615,13 +650,8 @@ sub has_flags {
}
sub local_timezone {
- my $class = shift;
-
- if (!defined $class->request_cache->{local_timezone}) {
- $class->request_cache->{local_timezone} =
- DateTime::TimeZone->new(name => 'local');
- }
- return $class->request_cache->{local_timezone};
+ return $_[0]->process_cache->{local_timezone}
+ ||= DateTime::TimeZone->new(name => 'local');
}
# This creates the request cache for non-mod_perl installations.
@@ -642,11 +672,70 @@ sub request_cache {
return $_request_cache;
}
+sub clear_request_cache {
+ $_request_cache = {};
+ if ($ENV{MOD_PERL}) {
+ require Apache2::RequestUtil;
+ my $request = eval { Apache2::RequestUtil->request };
+ if ($request) {
+ my $pnotes = $request->pnotes;
+ delete @$pnotes{(keys %$pnotes)};
+ }
+ }
+}
+
+# This is a per-process cache. Under mod_cgi it's identical to the
+# request_cache. When using mod_perl, items in this cache live until the
+# worker process is terminated.
+our $_process_cache = {};
+
+sub process_cache {
+ return $_process_cache;
+}
+
+# BMO - Instrumentation
+
+sub metrics_enabled {
+ if (defined $_[1]) {
+ if (!$_[1]
+ && $_[0]->request_cache->{metrics_enabled}
+ && $_[0]->request_cache->{metrics})
+ {
+ $_[0]->request_cache->{metrics}->cancel();
+ delete $_[0]->request_cache->{metrics};
+ }
+ $_[0]->request_cache->{metrics_enabled} = $_[1];
+ }
+ else {
+ return $_[0]->request_cache->{metrics_enabled};
+ }
+}
+
+sub metrics {
+ return $_[0]->request_cache->{metrics} ||= Bugzilla::Metrics::Collector->new($_[1]);
+}
+
+# This is a memcached wrapper, which provides cross-process and cross-system
+# caching.
+sub memcached {
+ # BMO - use metrics subclass if required
+ if (Bugzilla->metrics_enabled) {
+ return $_[0]->request_cache->{memcached} ||= Bugzilla::Metrics::Memcached->_new();
+ } else {
+ return $_[0]->request_cache->{memcached} ||= Bugzilla::Memcached->_new();
+ }
+}
+
# Private methods
# Per-process cleanup. Note that this is a plain subroutine, not a method,
# so we don't have $class available.
sub _cleanup {
+ # BMO - finalise and report on metrics
+ if (Bugzilla->metrics_enabled) {
+ Bugzilla->metrics->finish();
+ }
+
my $main = Bugzilla->request_cache->{dbh_main};
my $shadow = Bugzilla->request_cache->{dbh_shadow};
foreach my $dbh ($main, $shadow) {
@@ -654,7 +743,7 @@ sub _cleanup {
$dbh->bz_rollback_transaction() if $dbh->bz_in_transaction;
$dbh->disconnect;
}
- undef $_request_cache;
+ clear_request_cache();
# These are both set by CGI.pm but need to be undone so that
# Apache can actually shut down its children if it needs to.
@@ -775,10 +864,10 @@ not an arrayref.
=item C<user>
-C<undef> if there is no currently logged in user or if the login code has not
-yet been run. If an sudo session is in progress, the C<Bugzilla::User>
-corresponding to the person who is being impersonated. If no session is in
-progress, the current C<Bugzilla::User>.
+Default C<Bugzilla::User> object if there is no currently logged in user or
+if the login code has not yet been run. If an sudo session is in progress,
+the C<Bugzilla::User> corresponding to the person who is being impersonated.
+If no session is in progress, the current C<Bugzilla::User>.
=item C<set_user>
@@ -915,6 +1004,10 @@ The main database handle. See L<DBI>.
Currently installed languages.
Returns a reference to a list of RFC 1766 language tags of installed languages.
+=item C<current_language>
+
+The currently active language.
+
=item C<switch_to_shadow_db>
Switch from using the main database to using the shadow database.
@@ -947,3 +1040,82 @@ Tells you whether or not a specific feature is enabled. For names
of features, see C<OPTIONAL_MODULES> in C<Bugzilla::Install::Requirements>.
=back
+
+=head1 B<CACHING>
+
+Bugzilla has several different caches available which provide different
+capabilities and lifetimes.
+
+The keys of all caches are unregulated; use of prefixes is suggested to avoid
+collisions.
+
+=over
+
+=item B<Request Cache>
+
+The request cache is a hashref which supports caching any perl variable for the
+duration of the current request. At the end of the current request the contents
+of this cache are cleared.
+
+Examples of its use include caching objects to avoid re-fetching the same data
+from the database, and passing data between otherwise unconnected parts of
+Bugzilla.
+
+=over
+
+=item C<request_cache>
+
+Returns a hashref which can be checked and modified to store any perl variable
+for the duration of the current request.
+
+=item C<clear_request_cache>
+
+Removes all entries from the C<request_cache>.
+
+=back
+
+=item B<Process Cache>
+
+The process cache is a hashref which support caching of any perl variable. If
+Bugzilla is configured to run using Apache mod_perl, the contents of this cache
+are persisted across requests for the lifetime of the Apache worker process
+(which varies depending on the SizeLimit configuration in mod_perl.pl).
+
+If Bugzilla isn't running under mod_perl, the process cache's contents are
+cleared at the end of the request.
+
+The process cache is only suitable for items which never change while Bugzilla
+is running (for example the path where Bugzilla is installed).
+
+=over
+
+=item C<process_cache>
+
+Returns a hashref which can be checked and modified to store any perl variable
+for the duration of the current process (mod_perl) or request (mod_cgi).
+
+=back
+
+=item B<Memcached>
+
+If Memcached is installed and configured, Bugzilla can use it to cache data
+across requests and between webheads. Unlike the request and process caches,
+only scalars, hashrefs, and arrayrefs can be stored in Memcached.
+
+Memcached integration is only required for large installations of Bugzilla -- if
+you have multiple webheads then configuring Memcached is recommended.
+
+=over
+
+=item C<memcached>
+
+Returns a C<Bugzilla::Memcached> object. An object is always returned even if
+Memcached is not available.
+
+See the documentation for the C<Bugzilla::Memcached> module for more
+information.
+
+=back
+
+=back
+
diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm
index fa8845358..1302fc716 100644
--- a/Bugzilla/Attachment.pm
+++ b/Bugzilla/Attachment.pm
@@ -22,10 +22,11 @@
# Marc Schumann <wurblzap@gmail.com>
# Frédéric Buclin <LpSolit@gmail.com>
-use strict;
-
package Bugzilla::Attachment;
+use 5.10.0;
+use strict;
+
=head1 NAME
Bugzilla::Attachment - Bugzilla attachment class.
@@ -61,6 +62,7 @@ use Bugzilla::Hook;
use File::Copy;
use List::Util qw(max);
+use Storable qw(dclone);
use base qw(Bugzilla::Object);
@@ -75,22 +77,19 @@ use constant LIST_ORDER => ID_FIELD;
use constant AUDIT_CREATES => 0;
use constant AUDIT_UPDATES => 0;
-sub DB_COLUMNS {
- my $dbh = Bugzilla->dbh;
-
- return qw(
- attach_id
- bug_id
- description
- filename
- isobsolete
- ispatch
- isprivate
- mimetype
- modification_time
- submitter_id),
- $dbh->sql_date_format('attachments.creation_ts', '%Y.%m.%d %H:%i') . ' AS creation_ts';
-}
+use constant DB_COLUMNS => qw(
+ attach_id
+ bug_id
+ creation_ts
+ description
+ filename
+ isobsolete
+ ispatch
+ isprivate
+ mimetype
+ modification_time
+ submitter_id
+);
use constant REQUIRED_FIELD_MAP => {
bug_id => 'bug',
@@ -116,7 +115,8 @@ use constant VALIDATORS => {
};
use constant VALIDATOR_DEPENDENCIES => {
- mimetype => ['ispatch'],
+ content_type => ['ispatch'],
+ mimetype => ['ispatch'],
};
use constant UPDATE_VALIDATORS => {
@@ -142,8 +142,7 @@ the ID of the bug to which the attachment is attached
=cut
sub bug_id {
- my $self = shift;
- return $self->{bug_id};
+ return $_[0]->{bug_id};
}
=over
@@ -157,11 +156,8 @@ the bug object to which the attachment is attached
=cut
sub bug {
- my $self = shift;
-
require Bugzilla::Bug;
- $self->{bug} ||= Bugzilla::Bug->new($self->bug_id);
- return $self->{bug};
+ return $_[0]->{bug} //= Bugzilla::Bug->new({ id => $_[0]->bug_id, cache => 1 });
}
=over
@@ -175,8 +171,7 @@ user-provided text describing the attachment
=cut
sub description {
- my $self = shift;
- return $self->{description};
+ return $_[0]->{description};
}
=over
@@ -190,8 +185,7 @@ the attachment's MIME media type
=cut
sub contenttype {
- my $self = shift;
- return $self->{mimetype};
+ return $_[0]->{mimetype};
}
=over
@@ -205,10 +199,8 @@ the user who attached the attachment
=cut
sub attacher {
- my $self = shift;
- return $self->{attacher} if exists $self->{attacher};
- $self->{attacher} = new Bugzilla::User($self->{submitter_id});
- return $self->{attacher};
+ return $_[0]->{attacher}
+ //= new Bugzilla::User({ id => $_[0]->{submitter_id}, cache => 1 });
}
=over
@@ -222,8 +214,7 @@ the date and time on which the attacher attached the attachment
=cut
sub attached {
- my $self = shift;
- return $self->{creation_ts};
+ return $_[0]->{creation_ts};
}
=over
@@ -237,8 +228,7 @@ the date and time on which the attachment was last modified.
=cut
sub modification_time {
- my $self = shift;
- return $self->{modification_time};
+ return $_[0]->{modification_time};
}
=over
@@ -252,8 +242,7 @@ the name of the file the attacher attached
=cut
sub filename {
- my $self = shift;
- return $self->{filename};
+ return $_[0]->{filename};
}
=over
@@ -267,8 +256,7 @@ whether or not the attachment is a patch
=cut
sub ispatch {
- my $self = shift;
- return $self->{ispatch};
+ return $_[0]->{ispatch};
}
=over
@@ -282,8 +270,7 @@ whether or not the attachment is obsolete
=cut
sub isobsolete {
- my $self = shift;
- return $self->{isobsolete};
+ return $_[0]->{isobsolete};
}
=over
@@ -297,8 +284,7 @@ whether or not the attachment is private
=cut
sub isprivate {
- my $self = shift;
- return $self->{isprivate};
+ return $_[0]->{isprivate};
}
=over
@@ -315,8 +301,7 @@ matches, because this will return a value even if it's matched by the generic
=cut
sub is_viewable {
- my $self = shift;
- my $contenttype = $self->contenttype;
+ my $contenttype = $_[0]->contenttype;
my $cgi = Bugzilla->cgi;
# We assume we can view all text and image types.
@@ -390,7 +375,7 @@ the length (in characters) of the attachment content
sub datasize {
my $self = shift;
- return $self->{datasize} if exists $self->{datasize};
+ return $self->{datasize} if defined $self->{datasize};
# If we have already retrieved the data, return its size.
return length($self->{data}) if exists $self->{data};
@@ -415,6 +400,53 @@ sub datasize {
return $self->{datasize};
}
+=over
+
+=item C<linecount>
+
+the number of lines of the attachment content
+
+=back
+
+=cut
+
+# linecount allows for getting the number of lines of an attachment
+# from the database directly if the data has not yet been loaded for
+# performance reasons.
+
+sub linecount {
+ my ($self) = @_;
+
+ return $self->{linecount} if exists $self->{linecount};
+
+ # Limit this to just text/* attachments as this could
+ # cause strange results for binary attachments.
+ return if $self->contenttype !~ /^text\//;
+
+ # If the data has already been loaded, we can just determine
+ # line count from the data directly.
+ if ($self->{data}) {
+ $self->{linecount} = $self->{data} =~ tr/\n/\n/;
+ }
+ else {
+ $self->{linecount} =
+ int(Bugzilla->dbh->selectrow_array("
+ SELECT LENGTH(attach_data.thedata)-LENGTH(REPLACE(attach_data.thedata,'\n',''))/LENGTH('\n')
+ FROM attach_data WHERE id = ?", undef, $self->id));
+
+ }
+
+ # If we still do not have a linecount either the attachment
+ # is stored in a local file or has been deleted. If the former,
+ # we call self->data to force a load from the filesystem and
+ # then do a split on newlines and count again.
+ unless ($self->{linecount}) {
+ $self->{linecount} = $self->data =~ tr/\n/\n/;
+ }
+
+ return $self->{linecount};
+}
+
sub _get_local_filename {
my $self = shift;
my $hash = ($self->id % 100) + 100;
@@ -433,11 +465,8 @@ flags that have been set on the attachment
=cut
sub flags {
- my $self = shift;
-
# Don't cache it as it must be in sync with ->flag_types.
- $self->{flags} = [map { @{$_->{flags}} } @{$self->flag_types}];
- return $self->{flags};
+ return $_[0]->{flags} = [map { @{$_->{flags}} } @{$_[0]->flag_types}];
}
=over
@@ -458,10 +487,10 @@ sub flag_types {
my $vars = { target_type => 'attachment',
product_id => $self->bug->product_id,
component_id => $self->bug->component_id,
- attach_id => $self->id };
+ attach_id => $self->id,
+ active_or_has_flags => $self->bug_id };
- $self->{flag_types} = Bugzilla::Flag->_flag_types($vars);
- return $self->{flag_types};
+ return $self->{flag_types} = Bugzilla::Flag->_flag_types($vars);
}
###############################
@@ -573,7 +602,7 @@ sub _check_filename {
else {
ThrowUserError('file_not_specified');
}
- }
+ }
# Remove path info (if any) from the file name. The browser should do this
# for us, but some are buggy. This may not work on Mac file names and could
@@ -608,12 +637,12 @@ sub _check_is_private {
=over
-=item C<get_attachments_by_bug($bug_id)>
+=item C<get_attachments_by_bug($bug)>
Description: retrieves and returns the attachments the currently logged in
user can view for the given bug.
-Params: C<$bug_id> - integer - the ID of the bug for which
+Params: C<$bug> - Bugzilla::Bug object - the bug for which
to retrieve and return attachments.
Returns: a reference to an array of attachment objects.
@@ -621,14 +650,14 @@ Returns: a reference to an array of attachment objects.
=cut
sub get_attachments_by_bug {
- my ($class, $bug_id, $vars) = @_;
+ my ($class, $bug, $vars) = @_;
my $user = Bugzilla->user;
my $dbh = Bugzilla->dbh;
# By default, private attachments are not accessible, unless the user
# is in the insider group or submitted the attachment.
my $and_restriction = '';
- my @values = ($bug_id);
+ my @values = ($bug->id);
unless ($user->is_insider) {
$and_restriction = 'AND (isprivate = 0 OR submitter_id = ?)';
@@ -645,45 +674,67 @@ sub get_attachments_by_bug {
# attachment listed here, we collect all the data at once and
# populate $attachment->{flags} ourselves.
if ($vars->{preload}) {
- $_->{flags} = [] foreach @$attachments;
- my %att = map { $_->id => $_ } @$attachments;
+ # Preload flag types and flags
+ my $vars = { target_type => 'attachment',
+ product_id => $bug->product_id,
+ component_id => $bug->component_id,
+ attach_id => $attach_ids };
+ my $flag_types = Bugzilla::Flag->_flag_types($vars);
+
+ foreach my $attachment (@$attachments) {
+ $attachment->{flag_types} = [];
+ my $new_types = dclone($flag_types);
+ foreach my $new_type (@$new_types) {
+ $new_type->{flags} = [ grep($_->attach_id == $attachment->id,
+ @{ $new_type->{flags} }) ];
+ push(@{ $attachment->{flag_types} }, $new_type);
+ }
+ }
- my $flags = Bugzilla::Flag->match({ bug_id => $bug_id,
- target_type => 'attachment' });
+ # Preload attachers.
+ my %user_ids = map { $_->{submitter_id} => 1 } @$attachments;
+ my $users = Bugzilla::User->new_from_list([keys %user_ids]);
+ my %user_map = map { $_->id => $_ } @$users;
+ foreach my $attachment (@$attachments) {
+ $attachment->{attacher} = $user_map{$attachment->{submitter_id}};
+ }
- # Exclude flags for private attachments you cannot see.
- @$flags = grep {exists $att{$_->attach_id}} @$flags;
+ # Preload datasizes.
+ my $sizes =
+ $dbh->selectall_hashref('SELECT attach_id, LENGTH(thedata) AS datasize
+ FROM attachments LEFT JOIN attach_data ON attach_id = id
+ WHERE bug_id = ?',
+ 'attach_id', undef, $bug->id);
- push(@{$att{$_->attach_id}->{flags}}, $_) foreach @$flags;
- $attachments = [sort {$a->id <=> $b->id} values %att];
+ # Force the size of attachments not in the DB to be recalculated.
+ $_->{datasize} = $sizes->{$_->id}->{datasize} || undef foreach @$attachments;
}
return $attachments;
}
=pod
-=item C<validate_can_edit($attachment, $product_id)>
+=item C<validate_can_edit>
Description: validates if the user is allowed to view and edit the attachment.
Only the submitter or someone with editbugs privs can edit it.
Only the submitter and users in the insider group can view
private attachments.
-Params: $attachment - the attachment object being edited.
- $product_id - the product ID the attachment belongs to.
+Params: none
Returns: 1 on success, 0 otherwise.
=cut
sub validate_can_edit {
- my ($attachment, $product_id) = @_;
+ my $self = shift;
my $user = Bugzilla->user;
# The submitter can edit their attachments.
- return ($attachment->attacher->id == $user->id
- || ((!$attachment->isprivate || $user->is_insider)
- && $user->in_group('editbugs', $product_id))) ? 1 : 0;
+ return ($self->attacher->id == $user->id
+ || ((!$self->isprivate || $user->is_insider)
+ && $user->in_group('editbugs', $self->bug->product_id))) ? 1 : 0;
}
=item C<validate_obsolete($bug, $attach_ids)>
@@ -720,7 +771,7 @@ sub validate_obsolete {
|| ThrowUserError('invalid_attach_id', $vars);
# Check that the user can view and edit this attachment.
- $attachment->validate_can_edit($bug->product_id)
+ $attachment->validate_can_edit
|| ThrowUserError('illegal_attachment_edit', { attach_id => $attachment->id });
if ($attachment->bug_id != $bug->bug_id) {
@@ -852,23 +903,23 @@ sub update {
}
# Record changes in the activity table.
- my $sth = $dbh->prepare('INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
- fieldid, removed, added)
- VALUES (?, ?, ?, ?, ?, ?, ?)');
-
+ require Bugzilla::Bug;
foreach my $field (keys %$changes) {
my $change = $changes->{$field};
$field = "attachments.$field" unless $field eq "flagtypes.name";
- my $fieldid = get_field_id($field);
- $sth->execute($self->bug_id, $self->id, $user->id, $timestamp,
- $fieldid, $change->[0], $change->[1]);
+ Bugzilla::Bug::LogActivityEntry($self->bug_id, $field, $change->[0],
+ $change->[1], $user->id, $timestamp, undef, $self->id);
}
if (scalar(keys %$changes)) {
- $dbh->do('UPDATE attachments SET modification_time = ? WHERE attach_id = ?',
- undef, ($timestamp, $self->id));
- $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
- undef, ($timestamp, $self->bug_id));
+ $dbh->do('UPDATE attachments SET modification_time = ? WHERE attach_id = ?',
+ undef, ($timestamp, $self->id));
+ $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
+ undef, ($timestamp, $self->bug_id));
+ $self->{modification_time} = $timestamp;
+ # because we updated the attachments table after SUPER::update(), we
+ # need to ensure the cache is flushed.
+ Bugzilla->memcached->clear({ table => 'attachments', id => $self->id });
}
return $changes;
@@ -893,11 +944,21 @@ sub remove_from_db {
my $dbh = Bugzilla->dbh;
$dbh->bz_start_transaction();
- $dbh->do('DELETE FROM flags WHERE attach_id = ?', undef, $self->id);
+ my $flag_ids = $dbh->selectcol_arrayref(
+ 'SELECT id FROM flags WHERE attach_id = ?', undef, $self->id);
+ $dbh->do('DELETE FROM flags WHERE ' . $dbh->sql_in('id', $flag_ids))
+ if @$flag_ids;
$dbh->do('DELETE FROM attach_data WHERE id = ?', undef, $self->id);
$dbh->do('UPDATE attachments SET mimetype = ?, ispatch = ?, isobsolete = ?
WHERE attach_id = ?', undef, ('text/plain', 0, 1, $self->id));
$dbh->bz_commit_transaction();
+
+ # As we don't call SUPER->remove_from_db we need to manually clear
+ # memcached here.
+ Bugzilla->memcached->clear({ table => 'attachments', id => $self->id });
+ foreach my $flag_id (@$flag_ids) {
+ Bugzilla->memcached->clear({ table => 'flags', id => $flag_id });
+ }
}
###############################
diff --git a/Bugzilla/Attachment/PatchReader.pm b/Bugzilla/Attachment/PatchReader.pm
index cfc7610f4..1ab14f386 100644
--- a/Bugzilla/Attachment/PatchReader.pm
+++ b/Bugzilla/Attachment/PatchReader.pm
@@ -19,6 +19,9 @@ use strict;
package Bugzilla::Attachment::PatchReader;
+use IPC::Open3;
+use Symbol 'gensym';
+
use Bugzilla::Error;
use Bugzilla::Attachment;
use Bugzilla::Util;
@@ -33,8 +36,8 @@ sub process_diff {
my ($reader, $last_reader) = setup_patch_readers(undef, $context);
if ($format eq 'raw') {
- require PatchReader::DiffPrinter::raw;
- $last_reader->sends_data_to(new PatchReader::DiffPrinter::raw());
+ require Bugzilla::PatchReader::DiffPrinter::raw;
+ $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::raw());
# Actually print out the patch.
print $cgi->header(-type => 'text/plain',
-expires => '+3M');
@@ -46,7 +49,7 @@ sub process_diff {
if ($lc->{interdiffbin} && $lc->{diffpath}) {
# Get the list of attachments that the user can view in this bug.
my @attachments =
- @{Bugzilla::Attachment->get_attachments_by_bug($attachment->bug_id)};
+ @{Bugzilla::Attachment->get_attachments_by_bug($attachment->bug)};
# Extract patches only.
@attachments = grep {$_->ispatch == 1} @attachments;
# We want them sorted from newer to older.
@@ -109,13 +112,37 @@ sub process_interdiff {
# Send through interdiff, send output directly to template.
# Must hack path so that interdiff will work.
$ENV{'PATH'} = $lc->{diffpath};
- open my $interdiff_fh, "$lc->{interdiffbin} $old_filename $new_filename|";
- binmode $interdiff_fh;
+
+ my ($pid, $interdiff_stdout, $interdiff_stderr);
+ if ($ENV{MOD_PERL}) {
+ require Apache2::RequestUtil;
+ require Apache2::SubProcess;
+ my $request = Apache2::RequestUtil->request;
+ (undef, $interdiff_stdout, $interdiff_stderr) = $request->spawn_proc_prog(
+ $lc->{interdiffbin}, [$old_filename, $new_filename]
+ );
+ } else {
+ $interdiff_stderr = gensym;
+ my $pid = open3(gensym, $interdiff_stdout, $interdiff_stderr,
+ $lc->{interdiffbin}, $old_filename, $new_filename);
+ }
+ binmode $interdiff_stdout;
+
+ # Check for errors
+ {
+ local $/ = undef;
+ my $error = <$interdiff_stderr>;
+ if ($error) {
+ warn($error);
+ $warning = 'interdiff3';
+ }
+ }
+
my ($reader, $last_reader) = setup_patch_readers("", $context);
if ($format eq 'raw') {
- require PatchReader::DiffPrinter::raw;
- $last_reader->sends_data_to(new PatchReader::DiffPrinter::raw());
+ require Bugzilla::PatchReader::DiffPrinter::raw;
+ $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::raw());
# Actually print out the patch.
print $cgi->header(-type => 'text/plain',
-expires => '+3M');
@@ -123,7 +150,7 @@ sub process_interdiff {
}
else {
# In case the HTML page is displayed with the UTF-8 encoding.
- binmode $interdiff_fh, ':utf8' if Bugzilla->params->{'utf8'};
+ binmode $interdiff_stdout, ':utf8' if Bugzilla->params->{'utf8'};
$vars->{'warning'} = $warning if $warning;
$vars->{'bugid'} = $new_attachment->bug_id;
@@ -134,9 +161,9 @@ sub process_interdiff {
setup_template_patch_reader($last_reader, $format, $context, $vars);
}
- $reader->iterate_fh($interdiff_fh, 'interdiff #' . $old_attachment->id .
+ $reader->iterate_fh($interdiff_stdout, 'interdiff #' . $old_attachment->id .
' #' . $new_attachment->id);
- close $interdiff_fh;
+ waitpid($pid, 0) if $pid;
$ENV{'PATH'} = '';
# Delete temporary files.
@@ -152,29 +179,29 @@ sub get_unified_diff {
my ($attachment, $format) = @_;
# Bring in the modules we need.
- require PatchReader::Raw;
- require PatchReader::FixPatchRoot;
- require PatchReader::DiffPrinter::raw;
- require PatchReader::PatchInfoGrabber;
+ require Bugzilla::PatchReader::Raw;
+ require Bugzilla::PatchReader::FixPatchRoot;
+ require Bugzilla::PatchReader::DiffPrinter::raw;
+ require Bugzilla::PatchReader::PatchInfoGrabber;
require File::Temp;
$attachment->ispatch
|| ThrowCodeError('must_be_patch', { 'attach_id' => $attachment->id });
# Reads in the patch, converting to unified diff in a temp file.
- my $reader = new PatchReader::Raw;
+ my $reader = new Bugzilla::PatchReader::Raw;
my $last_reader = $reader;
# Fixes patch root (makes canonical if possible).
if (Bugzilla->params->{'cvsroot'}) {
my $fix_patch_root =
- new PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'});
+ new Bugzilla::PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'});
$last_reader->sends_data_to($fix_patch_root);
$last_reader = $fix_patch_root;
}
# Grabs the patch file info.
- my $patch_info_grabber = new PatchReader::PatchInfoGrabber();
+ my $patch_info_grabber = new Bugzilla::PatchReader::PatchInfoGrabber();
$last_reader->sends_data_to($patch_info_grabber);
$last_reader = $patch_info_grabber;
@@ -184,7 +211,7 @@ sub get_unified_diff {
# The HTML page will be displayed with the UTF-8 encoding.
binmode $fh, ':utf8';
}
- my $raw_printer = new PatchReader::DiffPrinter::raw($fh);
+ my $raw_printer = new Bugzilla::PatchReader::DiffPrinter::raw($fh);
$last_reader->sends_data_to($raw_printer);
$last_reader = $raw_printer;
@@ -228,13 +255,13 @@ sub setup_patch_readers {
# Define the patch readers.
# The reader that reads the patch in (whatever its format).
- require PatchReader::Raw;
- my $reader = new PatchReader::Raw;
+ require Bugzilla::PatchReader::Raw;
+ my $reader = new Bugzilla::PatchReader::Raw;
my $last_reader = $reader;
# Fix the patch root if we have a cvs root.
if (Bugzilla->params->{'cvsroot'}) {
- require PatchReader::FixPatchRoot;
- $last_reader->sends_data_to(new PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'}));
+ require Bugzilla::PatchReader::FixPatchRoot;
+ $last_reader->sends_data_to(new Bugzilla::PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'}));
$last_reader->sends_data_to->diff_root($diff_root) if defined($diff_root);
$last_reader = $last_reader->sends_data_to;
}
@@ -243,12 +270,12 @@ sub setup_patch_readers {
if ($context ne 'patch' && Bugzilla->localconfig->{cvsbin}
&& Bugzilla->params->{'cvsroot_get'})
{
- require PatchReader::AddCVSContext;
+ require Bugzilla::PatchReader::AddCVSContext;
# We need to set $cvsbin as global, because PatchReader::CVSClient
# needs it in order to find 'cvs'.
$main::cvsbin = Bugzilla->localconfig->{cvsbin};
$last_reader->sends_data_to(
- new PatchReader::AddCVSContext($context, Bugzilla->params->{'cvsroot_get'}));
+ new Bugzilla::PatchReader::AddCVSContext($context, Bugzilla->params->{'cvsroot_get'}));
$last_reader = $last_reader->sends_data_to;
}
@@ -260,7 +287,7 @@ sub setup_template_patch_reader {
my $cgi = Bugzilla->cgi;
my $template = Bugzilla->template;
- require PatchReader::DiffPrinter::template;
+ require Bugzilla::PatchReader::DiffPrinter::template;
# Define the vars for templates.
if (defined $cgi->param('headers')) {
@@ -276,10 +303,9 @@ sub setup_template_patch_reader {
&& Bugzilla->params->{'cvsroot_get'} && !$vars->{'newid'};
# Print everything out.
- print $cgi->header(-type => 'text/html',
- -expires => '+3M');
+ print $cgi->header(-type => 'text/html');
- $last_reader->sends_data_to(new PatchReader::DiffPrinter::template($template,
+ $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::template($template,
"attachment/diff-header.$format.tmpl",
"attachment/diff-file.$format.tmpl",
"attachment/diff-footer.$format.tmpl",
diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm
index 45034e166..2c58b52a8 100644
--- a/Bugzilla/Auth.pm
+++ b/Bugzilla/Auth.pm
@@ -38,6 +38,7 @@ use Bugzilla::User::Setting ();
use Bugzilla::Auth::Login::Stack;
use Bugzilla::Auth::Verify::Stack;
use Bugzilla::Auth::Persist::Cookie;
+use Socket;
sub new {
my ($class, $params) = @_;
@@ -123,6 +124,15 @@ sub can_logout {
return $getter->can_logout;
}
+sub login_token {
+ my ($self) = @_;
+ my $getter = $self->{_info_getter}->{successful};
+ if ($getter && $getter->isa('Bugzilla::Auth::Login::Cookie')) {
+ return $getter->login_token;
+ }
+ return undef;
+}
+
sub user_can_create_account {
my ($self) = @_;
my $verifier = $self->{_verifier}->{successful};
@@ -215,10 +225,19 @@ sub _handle_login_result {
my $default_settings = Bugzilla::User::Setting::get_defaults();
my $template = Bugzilla->template_inner(
$default_settings->{lang}->{default_value});
+ my $address = $attempts->[0]->{ip_addr};
+ # Note: inet_aton will only resolve IPv4 addresses.
+ # For IPv6 we'll need to use inet_pton which requires Perl 5.12.
+ my $n = inet_aton($address);
+ if ($n) {
+ my $host = gethostbyaddr($n, AF_INET);
+ $address = "$host ($address)" if $host;
+ }
my $vars = {
locked_user => $user,
attempts => $attempts,
unlock_at => $unlock_at,
+ address => $address,
};
my $message;
$template->process('email/lockout.txt.tmpl', $vars, \$message)
@@ -416,6 +435,14 @@ Params: None
Returns: C<true> if users can change their own email address,
C<false> otherwise.
+=item C<login_token>
+
+Description: If a login token was used instead of a cookie then this
+ will return the current login token data such as user id
+ and the token itself.
+Params: None
+Returns: A hash containing C<login_token> and C<user_id>.
+
=back
=head1 STRUCTURE
diff --git a/Bugzilla/Auth/Login.pm b/Bugzilla/Auth/Login.pm
index 42ce51c62..7e03778b3 100644
--- a/Bugzilla/Auth/Login.pm
+++ b/Bugzilla/Auth/Login.pm
@@ -17,7 +17,7 @@
package Bugzilla::Auth::Login;
use strict;
-use fields qw();
+use fields qw(_login_token);
# Determines whether or not a user can logout. It's really a subroutine,
# but we implement it here as a constant. Override it in subclasses if
diff --git a/Bugzilla/Auth/Login/Cookie.pm b/Bugzilla/Auth/Login/Cookie.pm
index de9188c64..62a6c58a9 100644
--- a/Bugzilla/Auth/Login/Cookie.pm
+++ b/Bugzilla/Auth/Login/Cookie.pm
@@ -21,13 +21,15 @@ use base qw(Bugzilla::Auth::Login);
use Bugzilla::Constants;
use Bugzilla::Util;
+use Bugzilla::Error;
use List::Util qw(first);
use constant requires_persistence => 0;
use constant requires_verification => 0;
use constant can_login => 0;
-use constant is_automatic => 1;
+
+sub is_automatic { return $_[0]->login_token ? 0 : 1; }
# Note that Cookie never consults the Verifier, it always assumes
# it has a valid DB account or it fails.
@@ -35,24 +37,35 @@ sub get_login_info {
my ($self) = @_;
my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh;
+ my ($user_id, $login_cookie);
- my $ip_addr = remote_ip();
- my $login_cookie = $cgi->cookie("Bugzilla_logincookie");
- my $user_id = $cgi->cookie("Bugzilla_login");
+ if (!Bugzilla->request_cache->{auth_no_automatic_login}) {
+ $login_cookie = $cgi->cookie("Bugzilla_logincookie");
+ $user_id = $cgi->cookie("Bugzilla_login");
- # If cookies cannot be found, this could mean that they haven't
- # been made available yet. In this case, look at Bugzilla_cookie_list.
- unless ($login_cookie) {
- my $cookie = first {$_->name eq 'Bugzilla_logincookie'}
- @{$cgi->{'Bugzilla_cookie_list'}};
- $login_cookie = $cookie->value if $cookie;
+ # If cookies cannot be found, this could mean that they haven't
+ # been made available yet. In this case, look at Bugzilla_cookie_list.
+ unless ($login_cookie) {
+ my $cookie = first {$_->name eq 'Bugzilla_logincookie'}
+ @{$cgi->{'Bugzilla_cookie_list'}};
+ $login_cookie = $cookie->value if $cookie;
+ }
+ unless ($user_id) {
+ my $cookie = first {$_->name eq 'Bugzilla_login'}
+ @{$cgi->{'Bugzilla_cookie_list'}};
+ $user_id = $cookie->value if $cookie;
+ }
}
- unless ($user_id) {
- my $cookie = first {$_->name eq 'Bugzilla_login'}
- @{$cgi->{'Bugzilla_cookie_list'}};
- $user_id = $cookie->value if $cookie;
+
+ # If no cookies were provided, we also look for a login token
+ # passed in the parameters of a webservice
+ my $token = $self->login_token;
+ if ($token && (!$login_cookie || !$user_id)) {
+ ($user_id, $login_cookie) = ($token->{'user_id'}, $token->{'login_token'});
}
+ my $ip_addr = remote_ip();
+
if ($login_cookie && $user_id) {
# Anything goes for these params - they're just strings which
# we're going to verify against the db
@@ -76,13 +89,43 @@ sub get_login_info {
WHERE cookie = ?", undef, $login_cookie);
return { user_id => $user_id };
}
+ elsif (i_am_webservice()) {
+ ThrowUserError('invalid_cookies_or_token');
+ }
}
- # Either the he cookie is invalid, or we got no cookie. We don't want
- # to ever return AUTH_LOGINFAILED, because we don't want Bugzilla to
- # actually throw an error when it gets a bad cookie. It should just
- # look like there was no cookie to begin with.
+ # Either the cookie or token is invalid and we are not authenticating
+ # via a webservice, or we did not receive a cookie or token. We don't
+ # want to ever return AUTH_LOGINFAILED, because we don't want Bugzilla to
+ # actually throw an error when it gets a bad cookie or token. It should just
+ # look like there was no cookie or token to begin with.
return { failure => AUTH_NODATA };
}
+sub login_token {
+ my ($self) = @_;
+ my $input = Bugzilla->input_params;
+ my $usage_mode = Bugzilla->usage_mode;
+
+ return $self->{'_login_token'} if exists $self->{'_login_token'};
+
+ if (!i_am_webservice()) {
+ return $self->{'_login_token'} = undef;
+ }
+
+ # Check if a token was passed in via requests for WebServices
+ my $token = trim(delete $input->{'Bugzilla_token'});
+ return $self->{'_login_token'} = undef if !$token;
+
+ my ($user_id, $login_token) = split('-', $token, 2);
+ if (!detaint_natural($user_id) || !$login_token) {
+ return $self->{'_login_token'} = undef;
+ }
+
+ return $self->{'_login_token'} = {
+ user_id => $user_id,
+ login_token => $login_token
+ };
+}
+
1;
diff --git a/Bugzilla/Auth/Persist/Cookie.pm b/Bugzilla/Auth/Persist/Cookie.pm
index ace474635..c1d133772 100644
--- a/Bugzilla/Auth/Persist/Cookie.pm
+++ b/Bugzilla/Auth/Persist/Cookie.pm
@@ -36,6 +36,8 @@ use Bugzilla::Constants;
use Bugzilla::Util;
use Bugzilla::Token;
+use Bugzilla::Auth::Login::Cookie qw(login_token);
+
use List::Util qw(first);
sub new {
@@ -107,6 +109,7 @@ sub logout {
my $dbh = Bugzilla->dbh;
my $cgi = Bugzilla->cgi;
+ my $input = Bugzilla->input_params;
$param = {} unless $param;
my $user = $param->{user} || Bugzilla->user;
my $type = $param->{type} || LOGOUT_ALL;
@@ -120,16 +123,24 @@ sub logout {
# The LOGOUT_*_CURRENT options require the current login cookie.
# If a new cookie has been issued during this run, that's the current one.
# If not, it's the one we've received.
+ my @login_cookies;
my $cookie = first {$_->name eq 'Bugzilla_logincookie'}
@{$cgi->{'Bugzilla_cookie_list'}};
- my $login_cookie;
if ($cookie) {
- $login_cookie = $cookie->value;
+ push(@login_cookies, $cookie->value);
}
else {
- $login_cookie = $cgi->cookie("Bugzilla_logincookie") || '';
+ push(@login_cookies, $cgi->cookie("Bugzilla_logincookie"));
+ }
+
+ # If we are a webservice using a token instead of cookie
+ # then add that as well to the login cookies to delete
+ if (my $login_token = $user->authorizer->login_token) {
+ push(@login_cookies, $login_token->{'login_token'});
}
- trick_taint($login_cookie);
+
+ # Make sure that @login_cookies is not empty to not break SQL statements.
+ push(@login_cookies, '') unless @login_cookies;
# These queries use both the cookie ID and the user ID as keys. Even
# though we know the userid must match, we still check it in the SQL
@@ -138,12 +149,18 @@ sub logout {
# logged in and got the same cookie, we could be logging the other
# user out here. Yes, this is very very very unlikely, but why take
# chances? - bbaetz
+ map { trick_taint($_) } @login_cookies;
+ @login_cookies = map { $dbh->quote($_) } @login_cookies;
if ($type == LOGOUT_KEEP_CURRENT) {
- $dbh->do("DELETE FROM logincookies WHERE cookie != ? AND userid = ?",
- undef, $login_cookie, $user->id);
+ $dbh->do("DELETE FROM logincookies WHERE " .
+ $dbh->sql_in('cookie', \@login_cookies, 1) .
+ " AND userid = ?",
+ undef, $user->id);
} elsif ($type == LOGOUT_CURRENT) {
- $dbh->do("DELETE FROM logincookies WHERE cookie = ? AND userid = ?",
- undef, $login_cookie, $user->id);
+ $dbh->do("DELETE FROM logincookies WHERE " .
+ $dbh->sql_in('cookie', \@login_cookies) .
+ " AND userid = ?",
+ undef, $user->id);
} else {
die("Invalid type $type supplied to logout()");
}
diff --git a/Bugzilla/Auth/Verify.pm b/Bugzilla/Auth/Verify.pm
index a8cd0af2c..3578631f1 100644
--- a/Bugzilla/Auth/Verify.pm
+++ b/Bugzilla/Auth/Verify.pm
@@ -97,6 +97,7 @@ sub create_or_update_user {
if ($extern_id && $username_user_id && !$extern_user_id) {
$dbh->do('UPDATE profiles SET extern_id = ? WHERE userid = ?',
undef, $extern_id, $username_user_id);
+ Bugzilla->memcached->clear({ table => 'profiles', id => $username_user_id });
}
# Finally, at this point, one of these will give us a valid user id.
@@ -109,23 +110,26 @@ sub create_or_update_user {
'Bugzilla::Auth::Verify::create_or_update_user'})
unless $user_id;
- my $user = new Bugzilla::User($user_id);
+ my $user = new Bugzilla::User({ id => $user_id, cache => 1 });
# Now that we have a valid User, we need to see if any data has to be
# updated.
+ my $user_updated = 0;
if ($username && lc($user->login) ne lc($username)) {
validate_email_syntax($username)
|| return { failure => AUTH_ERROR, error => 'auth_invalid_email',
details => {addr => $username} };
$user->set_login($username);
+ $user_updated = 1;
}
if ($real_name && $user->name ne $real_name) {
# $real_name is more than likely tainted, but we only use it
# in a placeholder and we never use it after this.
trick_taint($real_name);
$user->set_name($real_name);
+ $user_updated = 1;
}
- $user->update();
+ $user->update() if $user_updated;
return { user => $user };
}
diff --git a/Bugzilla/Auth/Verify/DB.pm b/Bugzilla/Auth/Verify/DB.pm
index 2fcfd4017..2840b4ab8 100644
--- a/Bugzilla/Auth/Verify/DB.pm
+++ b/Bugzilla/Auth/Verify/DB.pm
@@ -90,7 +90,9 @@ sub check_credentials {
# whatever hashing system we're using now.
my $current_algorithm = PASSWORD_DIGEST_ALGORITHM;
if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/) {
- $user->set_password($password);
+ # We can't call $user->set_password because we don't want the password
+ # complexity rules to apply here.
+ $user->{cryptpassword} = bz_crypt($password);
$user->update();
}
@@ -103,6 +105,7 @@ sub change_password {
my $cryptpassword = bz_crypt($password);
$dbh->do("UPDATE profiles SET cryptpassword = ? WHERE userid = ?",
undef, $cryptpassword, $user->id);
+ Bugzilla->memcached->clear({ table => 'profiles', id => $user->id });
}
1;
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index 90bd8b66d..39a0f2596 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -50,6 +50,7 @@ use Bugzilla::Group;
use Bugzilla::Status;
use Bugzilla::Comment;
use Bugzilla::BugUrl;
+use Bugzilla::BugUserLastVisit;
use List::MoreUtils qw(firstidx uniq part);
use List::Util qw(min max first);
@@ -76,11 +77,14 @@ use constant LIST_ORDER => ID_FIELD;
# Bugs have their own auditing table, bugs_activity.
use constant AUDIT_CREATES => 0;
use constant AUDIT_UPDATES => 0;
+# This will be enabled later
+use constant USE_MEMCACHED => 0;
# This is a sub because it needs to call other subroutines.
sub DB_COLUMNS {
my $dbh = Bugzilla->dbh;
- my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT}
+ my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT
+ && $_->type != FIELD_TYPE_EXTENSION}
Bugzilla->active_custom_fields;
my @custom_names = map {$_->name} @custom;
@@ -93,6 +97,7 @@ sub DB_COLUMNS {
bug_status
cclist_accessible
component_id
+ creation_ts
delta_ts
estimated_time
everconfirmed
@@ -111,12 +116,11 @@ sub DB_COLUMNS {
version
),
'reporter AS reporter_id',
- $dbh->sql_date_format('creation_ts', '%Y.%m.%d %H:%i') . ' AS creation_ts',
$dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline',
@custom_names);
-
+
Bugzilla::Hook::process("bug_columns", { columns => \@columns });
-
+
return @columns;
}
@@ -166,6 +170,9 @@ sub VALIDATORS {
elsif ($field->type == FIELD_TYPE_DATETIME) {
$validator = \&_check_datetime_field;
}
+ elsif ($field->type == FIELD_TYPE_DATE) {
+ $validator = \&_check_date_field;
+ }
elsif ($field->type == FIELD_TYPE_FREETEXT) {
$validator = \&_check_freetext_field;
}
@@ -211,7 +218,8 @@ sub VALIDATOR_DEPENDENCIES {
};
sub UPDATE_COLUMNS {
- my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT}
+ my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT
+ && $_->type != FIELD_TYPE_EXTENSION}
Bugzilla->active_custom_fields;
my @custom_names = map {$_->name} @custom;
my @columns = qw(
@@ -248,7 +256,8 @@ use constant NUMERIC_COLUMNS => qw(
);
sub DATE_COLUMNS {
- my @fields = @{ Bugzilla->fields({ type => FIELD_TYPE_DATETIME }) };
+ my @fields = (@{ Bugzilla->fields({ type => FIELD_TYPE_DATETIME }) },
+ @{ Bugzilla->fields({ type => FIELD_TYPE_DATE }) });
return map { $_->name } @fields;
}
@@ -280,10 +289,6 @@ use constant FIELD_MAP => {
summary => 'short_desc',
url => 'bug_file_loc',
whiteboard => 'status_whiteboard',
-
- # These are special values for the WebService Bug.search method.
- limit => 'LIMIT',
- offset => 'OFFSET',
};
use constant REQUIRED_FIELD_MAP => {
@@ -311,21 +316,11 @@ use constant EXTRA_REQUIRED_FIELDS => qw(creation_ts target_milestone cc qa_cont
#####################################################################
-# This and "new" catch every single way of creating a bug, so that we
-# can call _create_cf_accessors.
-sub _do_list_select {
- my $invocant = shift;
- $invocant->_create_cf_accessors();
- return $invocant->SUPER::_do_list_select(@_);
-}
-
sub new {
my $invocant = shift;
my $class = ref($invocant) || $invocant;
my $param = shift;
- $class->_create_cf_accessors();
-
# Remove leading "#" mark if we've just been passed an id.
if (!ref $param && $param =~ /^#(\d+)$/) {
$param = $1;
@@ -333,10 +328,14 @@ sub new {
# If we get something that looks like a word (not a number),
# make it the "name" param.
- if (!defined $param || (!ref($param) && $param !~ /^\d+$/)) {
+ if (!defined $param
+ || (!ref($param) && (!$param || $param =~ /\D/))
+ || (ref($param) && (!$param->{id} || $param->{id} =~ /\D/)))
+ {
# But only if aliases are enabled.
if (Bugzilla->params->{'usebugaliases'} && $param) {
- $param = { name => $param };
+ $param = { name => ref($param) ? $param->{id} : $param,
+ cache => ref($param) ? $param->{cache} : 0 };
}
else {
# Aliases are off, and we got something that's not a number.
@@ -370,20 +369,35 @@ sub new {
return $self;
}
-sub check {
+sub initialize {
+ $_[0]->_create_cf_accessors();
+}
+
+sub object_cache_key {
my $class = shift;
- my ($id, $field) = @_;
+ my $key = $class->SUPER::object_cache_key(@_)
+ || return;
+ return $key . ',' . Bugzilla->user->id;
+}
- ThrowUserError('improper_bug_id_field_value', { field => $field }) unless defined $id;
+sub check {
+ my $class = shift;
+ my ($param, $field) = @_;
# Bugzilla::Bug throws lots of special errors, so we don't call
# SUPER::check, we just call our new and do our own checks.
- my $self = $class->new(trim($id));
- # For error messages, use the id that was returned by new(), because
- # it's cleaned up.
- $id = $self->id;
+ my $id = ref($param)
+ ? ($param->{id} = trim($param->{id}))
+ : ($param = trim($param));
+ ThrowUserError('improper_bug_id_field_value', { field => $field }) unless defined $id;
+
+ my $self = $class->new($param);
if ($self->{error}) {
+ # For error messages, use the id that was returned by new(), because
+ # it's cleaned up.
+ $id = $self->id;
+
if ($self->{error} eq 'NotFound') {
ThrowUserError("bug_id_does_not_exist", { bug_id => $id });
}
@@ -493,6 +507,59 @@ sub preload {
# If we don't do this, can_see_bug will do one call per bug in
# the dependency lists, during get_bug_link in Bugzilla::Template.
$user->visible_bugs(\@all_dep_ids);
+
+ # We preload comments here in order to allow us to compare the time it
+ # takes to load comments from the database with the template rendering
+ # time.
+ foreach my $bug (@$bugs) {
+ $bug->comments();
+ }
+
+ foreach my $bug (@$bugs) {
+ $bug->_preload_referenced_bugs();
+ }
+}
+
+# Helps load up bugs referenced in comments by retrieving them with a single
+# query from the database and injecting bug objects into the object-cache.
+sub _preload_referenced_bugs {
+ my $self = shift;
+ my @referenced_bug_ids;
+
+ # inject current duplicates into the object-cache first
+ foreach my $bug (@{ $self->duplicates }) {
+ $bug->object_cache_set()
+ unless Bugzilla::Bug->object_cache_get($bug->id);
+ }
+
+ # preload bugs from comments
+ require Bugzilla::Template;
+ foreach my $comment (@{ $self->comments }) {
+ if ($comment->type == CMT_HAS_DUPE || $comment->type == CMT_DUPE_OF) {
+ # duplicate bugs that aren't currently in $self->duplicates
+ push @referenced_bug_ids, $comment->extra_data
+ unless Bugzilla::Bug->object_cache_get($comment->extra_data);
+ }
+ else {
+ # bugs referenced in comments
+ Bugzilla::Template::quoteUrls($comment->body, undef, undef, undef,
+ sub {
+ my $bug_id = $_[0];
+ push @referenced_bug_ids, $bug_id
+ unless Bugzilla::Bug->object_cache_get($bug_id);
+ });
+ }
+ }
+
+ # inject into object-cache
+ my $referenced_bugs = Bugzilla::Bug->new_from_list(
+ [ uniq @referenced_bug_ids ]);
+ foreach my $bug (@$referenced_bugs) {
+ $bug->object_cache_set();
+ }
+
+ # preload bug visibility
+ Bugzilla->user->visible_bugs(\@referenced_bug_ids);
}
sub possible_duplicates {
@@ -512,8 +579,10 @@ sub possible_duplicates {
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
my @words = split(/[\b\s]+/, $short_desc || '');
- # Exclude punctuation from the array.
- @words = map { /(\w+)/; $1 } @words;
+ # Remove leading/trailing punctuation from words
+ foreach my $word (@words) {
+ $word =~ s/(?:^\W+|\W+$)//g;
+ }
# And make sure that each word is longer than 2 characters.
@words = grep { defined $_ and length($_) > 2 } @words;
@@ -615,6 +684,14 @@ sub create {
my ($class, $params) = @_;
my $dbh = Bugzilla->dbh;
+ # BMO - allow parameter alteration before creation. also add support for
+ # fields which are not bug columns (eg bug_mentors). extensions should move
+ # fields from $params to $stash, then use the bug_end_of_create hook to
+ # update the database
+ my $stash = {};
+ Bugzilla::Hook::process('bug_before_create', { params => $params,
+ stash => $stash });
+
$dbh->bz_start_transaction();
# These fields have default values which we can use if they are undefined.
@@ -640,6 +717,7 @@ sub create {
my $blocked = delete $params->{blocked};
my $keywords = delete $params->{keywords};
my $creation_comment = delete $params->{comment};
+ my $see_also = delete $params->{see_also};
# We don't want the bug to appear in the system until it's correctly
# protected by groups.
@@ -676,21 +754,20 @@ sub create {
# Set up dependencies (blocked/dependson)
my $sth_deps = $dbh->prepare(
'INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)');
- my $sth_bug_time = $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?');
foreach my $depends_on_id (@$depends_on) {
$sth_deps->execute($bug->bug_id, $depends_on_id);
# Log the reverse action on the other bug.
LogActivityEntry($depends_on_id, 'blocked', '', $bug->bug_id,
$bug->{reporter_id}, $timestamp);
- $sth_bug_time->execute($timestamp, $depends_on_id);
+ _update_delta_ts($depends_on_id, $timestamp);
}
foreach my $blocked_id (@$blocked) {
$sth_deps->execute($blocked_id, $bug->bug_id);
# Log the reverse action on the other bug.
LogActivityEntry($blocked_id, 'dependson', '', $bug->bug_id,
$bug->{reporter_id}, $timestamp);
- $sth_bug_time->execute($timestamp, $blocked_id);
+ _update_delta_ts($blocked_id, $timestamp);
}
# Insert the values into the multiselect value tables
@@ -703,6 +780,25 @@ sub create {
}
}
+ # Insert any see_also values
+ if ($see_also) {
+ my $see_also_array = $see_also;
+ if (!ref $see_also_array) {
+ $see_also = trim($see_also);
+ $see_also_array = [ split(/[\s,]+/, $see_also) ];
+ }
+ foreach my $value (@$see_also_array) {
+ $bug->add_see_also($value);
+ }
+ foreach my $see_also (@{ $bug->see_also }) {
+ $see_also->insert_create_data($see_also);
+ }
+ foreach my $ref_bug (@{ $bug->{_update_ref_bugs} || [] }) {
+ $ref_bug->update();
+ }
+ delete $bug->{_update_ref_bugs};
+ }
+
# Comment #0 handling...
# We now have a bug id so we can fill this out
@@ -712,8 +808,10 @@ sub create {
# but sometimes it's blank.
Bugzilla::Comment->insert_create_data($creation_comment);
+ # BMO - add the stash param from bug_start_of_create
Bugzilla::Hook::process('bug_end_of_create', { bug => $bug,
timestamp => $timestamp,
+ stash => $stash,
});
$dbh->bz_commit_transaction();
@@ -721,7 +819,10 @@ sub create {
# Because MySQL doesn't support transactions on the fulltext table,
# we do this after we've committed the transaction. That way we're
# sure we're inserting a good Bug ID.
- $bug->_sync_fulltext('new bug');
+ $bug->_sync_fulltext( new_bug => 1 );
+
+ # BMO - some work should happen outside of the transaction block
+ Bugzilla::Hook::process('bug_after_create', { bug => $bug, timestamp => $timestamp });
return $bug;
}
@@ -775,8 +876,9 @@ sub run_create_validators {
sub update {
my $self = shift;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
- my $dbh = Bugzilla->dbh;
# XXX This is just a temporary hack until all updating happens
# inside this function.
my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
@@ -785,6 +887,10 @@ sub update {
my ($changes, $old_bug) = $self->SUPER::update(@_);
+ Bugzilla::Hook::process('bug_start_of_update',
+ { timestamp => $delta_ts, bug => $self,
+ old_bug => $old_bug, changes => $changes });
+
# Certain items in $changes have to be fixed so that they hold
# a name instead of an ID.
foreach my $field (qw(product_id component_id)) {
@@ -863,10 +969,9 @@ sub update {
# Add an activity entry for the other bug.
LogActivityEntry($removed_id, $other, $self->id, '',
- Bugzilla->user->id, $delta_ts);
+ $user->id, $delta_ts);
# Update delta_ts on the other bug so that we trigger mid-airs.
- $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
- undef, $delta_ts, $removed_id);
+ _update_delta_ts($removed_id, $delta_ts);
}
foreach my $added_id (@$added) {
$dbh->do("INSERT INTO dependencies ($type, $other) VALUES (?,?)",
@@ -874,10 +979,9 @@ sub update {
# Add an activity entry for the other bug.
LogActivityEntry($added_id, $other, '', $self->id,
- Bugzilla->user->id, $delta_ts);
+ $user->id, $delta_ts);
# Update delta_ts on the other bug so that we trigger mid-airs.
- $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
- undef, $delta_ts, $added_id);
+ _update_delta_ts($added_id, $delta_ts);
}
if (scalar(@$removed) || scalar(@$added)) {
@@ -916,7 +1020,7 @@ sub update {
$comment = Bugzilla::Comment->insert_create_data($comment);
if ($comment->work_time) {
LogActivityEntry($self->id, "work_time", "", $comment->work_time,
- Bugzilla->user->id, $delta_ts);
+ $user->id, $delta_ts);
}
}
@@ -927,7 +1031,7 @@ sub update {
my ($from, $to)
= $comment->is_private ? (0, 1) : (1, 0);
LogActivityEntry($self->id, "longdescs.isprivate", $from, $to,
- Bugzilla->user->id, $delta_ts, $comment->id);
+ $user->id, $delta_ts, $comment->id);
}
# Clear the cache of comments
@@ -974,6 +1078,10 @@ sub update {
$changes->{'flagtypes.name'} = [$removed, $added];
}
+ # BMO - allow extensions to alter what is logged into bugs_activity
+ Bugzilla::Hook::process('bug_update_before_logging',
+ { bug => $self, timestamp => $delta_ts, changes => $changes, old_bug => $old_bug });
+
# Log bugs_activity items
# XXX Eventually, when bugs_activity is able to track the dupe_id,
# this code should go below the duplicates-table-updating code below.
@@ -981,8 +1089,8 @@ sub update {
my $change = $changes->{$field};
my $from = defined $change->[0] ? $change->[0] : '';
my $to = defined $change->[1] ? $change->[1] : '';
- LogActivityEntry($self->id, $field, $from, $to, Bugzilla->user->id,
- $delta_ts);
+ LogActivityEntry($self->id, $field, $from, $to,
+ $user->id, $delta_ts);
}
# Check if we have to update the duplicates table and the other bug.
@@ -996,7 +1104,7 @@ sub update {
$update_dup->update();
}
}
-
+
$changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef];
}
@@ -1008,20 +1116,50 @@ sub update {
if (scalar(keys %$changes) || $self->{added_comments}
|| $self->{comment_isprivate})
{
- $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
- undef, ($delta_ts, $self->id));
+ _update_delta_ts($self->id, $delta_ts);
$self->{delta_ts} = $delta_ts;
}
+ # Update last-visited
+ if ($user->is_involved_in_bug($self)) {
+ $self->update_user_last_visit($user, $delta_ts);
+ }
+
+ # If a user is no longer involved, remove their last visit entry
+ my $last_visits = Bugzilla::BugUserLastVisit->match({bug_id => $self->id});
+ foreach my $lv (@$last_visits) {
+ $lv->remove_from_db() unless $lv->user->is_involved_in_bug($self);
+ }
+
+ # Update bug ignore data if user wants to ignore mail for this bug
+ if (exists $self->{'bug_ignored'}) {
+ my $bug_ignored_changed;
+ if ($self->{'bug_ignored'} && !$user->is_bug_ignored($self->id)) {
+ $dbh->do('INSERT INTO email_bug_ignore
+ (user_id, bug_id) VALUES (?, ?)',
+ undef, $user->id, $self->id);
+ $bug_ignored_changed = 1;
+
+ }
+ elsif (!$self->{'bug_ignored'} && $user->is_bug_ignored($self->id)) {
+ $dbh->do('DELETE FROM email_bug_ignore
+ WHERE user_id = ? AND bug_id = ?',
+ undef, $user->id, $self->id);
+ $bug_ignored_changed = 1;
+ }
+ delete $user->{bugs_ignored} if $bug_ignored_changed;
+ }
+
$dbh->bz_commit_transaction();
# The only problem with this here is that update() is often called
# in the middle of a transaction, and if that transaction is rolled
# back, this change will *not* be rolled back. As we expect rollbacks
# to be extremely rare, that is OK for us.
- $self->_sync_fulltext()
- if $self->{added_comments} || $changes->{short_desc}
- || $self->{comment_isprivate};
+ $self->_sync_fulltext(
+ update_short_desc => $changes->{short_desc},
+ update_comments => $self->{added_comments} || $self->{comment_isprivate}
+ );
# Remove obsolete internal variables.
delete $self->{'_old_assigned_to'};
@@ -1029,7 +1167,11 @@ sub update {
# Also flush the visible_bugs cache for this bug as the user's
# relationship with this bug may have changed.
- delete Bugzilla->user->{_visible_bugs_cache}->{$self->id};
+ delete $user->{_visible_bugs_cache}->{$self->id};
+
+ # BMO - some work should happen outside of the transaction block
+ Bugzilla::Hook::process('bug_after_update',
+ { bug => $self, timestamp => $delta_ts, changes => $changes, old_bug => $old_bug });
return $changes;
}
@@ -1055,27 +1197,56 @@ sub _extract_multi_selects {
# Should be called any time you update short_desc or change a comment.
sub _sync_fulltext {
- my ($self, $new_bug) = @_;
+ my ($self, %options) = @_;
my $dbh = Bugzilla->dbh;
- if ($new_bug) {
- $dbh->do('INSERT INTO bugs_fulltext (bug_id, short_desc)
- SELECT bug_id, short_desc FROM bugs WHERE bug_id = ?',
- undef, $self->id);
+
+ my($all_comments, $public_comments);
+ if ($options{new_bug} || $options{update_comments}) {
+ my $comments = $dbh->selectall_arrayref(
+ 'SELECT thetext, isprivate FROM longdescs WHERE bug_id = ?',
+ undef, $self->id);
+ $all_comments = join("\n", map { $_->[0] } @$comments);
+ my @no_private = grep { !$_->[1] } @$comments;
+ $public_comments = join("\n", map { $_->[0] } @no_private);
}
- else {
- $dbh->do('UPDATE bugs_fulltext SET short_desc = ? WHERE bug_id = ?',
- undef, $self->short_desc, $self->id);
+
+ if ($options{new_bug}) {
+ $dbh->do('INSERT INTO bugs_fulltext (bug_id, short_desc, comments,
+ comments_noprivate)
+ VALUES (?, ?, ?, ?)',
+ undef,
+ $self->id, $self->short_desc, $all_comments, $public_comments);
+ } else {
+ my(@names, @values);
+ if ($options{update_short_desc}) {
+ push @names, 'short_desc';
+ push @values, $self->short_desc;
+ }
+ if ($options{update_comments}) {
+ push @names, ('comments', 'comments_noprivate');
+ push @values, ($all_comments, $public_comments);
+ }
+ if (@names) {
+ $dbh->do('UPDATE bugs_fulltext SET ' .
+ join(', ', map { "$_ = ?" } @names) .
+ ' WHERE bug_id = ?',
+ undef,
+ @values, $self->id);
+ }
}
- my $comments = $dbh->selectall_arrayref(
- 'SELECT thetext, isprivate FROM longdescs WHERE bug_id = ?',
- undef, $self->id);
- my $all = join("\n", map { $_->[0] } @$comments);
- my @no_private = grep { !$_->[1] } @$comments;
- my $nopriv_string = join("\n", map { $_->[0] } @no_private);
- $dbh->do('UPDATE bugs_fulltext SET comments = ?, comments_noprivate = ?
- WHERE bug_id = ?', undef, $all, $nopriv_string, $self->id);
}
+# Update a bug's delta_ts without requiring the full object to be loaded.
+sub _update_delta_ts {
+ my ($bug_id, $timestamp) = @_;
+ Bugzilla->dbh->do(
+ "UPDATE bugs SET delta_ts = ? WHERE bug_id = ?",
+ undef,
+ $timestamp, $bug_id
+ );
+ Bugzilla::Hook::process('bug_end_of_update_delta_ts',
+ { bug_id => $bug_id, timestamp => $timestamp });
+}
# This is the correct way to delete bugs from the DB.
# No bug should be deleted from anywhere else except from here.
@@ -1164,15 +1335,15 @@ sub send_changes {
changer => $user,
);
- _send_bugmail({ id => $self->id, type => 'bug', forced => \%forced },
- $vars);
+ my $recipient_count = _send_bugmail(
+ { id => $self->id, type => 'bug', forced => \%forced }, $vars);
# If the bug was marked as a duplicate, we need to notify users on the
# other bug of any changes to that bug.
my $new_dup_id = $changes->{'dup_id'} ? $changes->{'dup_id'}->[1] : undef;
if ($new_dup_id) {
- _send_bugmail({ forced => { changer => $user }, type => "dupe",
- id => $new_dup_id }, $vars);
+ $recipient_count += _send_bugmail(
+ { forced => { changer => $user }, type => "dupe", id => $new_dup_id }, $vars);
}
# If there were changes in dependencies, we need to notify those
@@ -1191,7 +1362,7 @@ sub send_changes {
foreach my $id (@{ $self->blocked }) {
$params->{id} = $id;
- _send_bugmail($params, $vars);
+ $recipient_count += _send_bugmail($params, $vars);
}
}
}
@@ -1209,15 +1380,17 @@ sub send_changes {
delete $changed_deps{''};
foreach my $id (sort { $a <=> $b } (keys %changed_deps)) {
- _send_bugmail({ forced => { changer => $user }, type => "dep",
- id => $id }, $vars);
+ $recipient_count += _send_bugmail(
+ { forced => { changer => $user }, type => "dep", id => $id }, $vars);
}
# Sending emails for the referenced bugs.
foreach my $ref_bug_id (uniq @{ $self->{see_also_changes} || [] }) {
- _send_bugmail({ forced => { changer => $user },
- id => $ref_bug_id }, $vars);
+ $recipient_count += _send_bugmail(
+ { forced => { changer => $user }, id => $ref_bug_id }, $vars);
}
+
+ return $recipient_count;
}
sub _send_bugmail {
@@ -1225,7 +1398,7 @@ sub _send_bugmail {
require Bugzilla::BugMail;
- my $results =
+ my $results =
Bugzilla::BugMail::Send($params->{'id'}, $params->{'forced'}, $params);
if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) {
@@ -1236,6 +1409,8 @@ sub _send_bugmail {
|| ThrowTemplateError($template->error());
$vars->{'header_done'} = 1;
}
+
+ return scalar @{ $results->{sent} };
}
#####################################################################
@@ -1658,7 +1833,9 @@ sub _check_groups {
foreach my $name (@$group_names) {
my $group = Bugzilla::Group->check_no_disclose({ %args, name => $name });
- if (!$product->group_is_settable($group)) {
+ # BMO : allow bugs to be always placed into some groups
+ if (!$product->group_always_settable($group)
+ && !$product->group_is_settable($group)) {
ThrowUserError('group_restriction_not_allowed', { %args, name => $name });
}
$add_groups{$group->id} = $group;
@@ -1667,7 +1844,7 @@ sub _check_groups {
# Now enforce mandatory groups.
$add_groups{$_->id} = $_ foreach @{ $product->groups_mandatory };
-
+
my @add_groups = values %add_groups;
return \@add_groups;
}
@@ -1989,8 +2166,12 @@ sub _check_field_is_mandatory {
}
}
+sub _check_date_field {
+ my ($invocant, $date) = @_;
+ return $invocant->_check_datetime_field($date, undef, {date_only => 1});
+}
sub _check_datetime_field {
- my ($invocant, $date_time) = @_;
+ my ($invocant, $date_time, $field, $params) = @_;
# Empty datetimes are empty strings or strings only containing
# 0's, whitespace, and punctuation.
@@ -2004,6 +2185,10 @@ sub _check_datetime_field {
ThrowUserError('illegal_date', { date => $date,
format => 'YYYY-MM-DD' });
}
+ if ($time && $params->{date_only}) {
+ ThrowUserError('illegal_date', { date => $date_time,
+ format => 'YYYY-MM-DD' });
+ }
if ($time && !validate_time($time)) {
ThrowUserError('illegal_time', { 'time' => $time,
format => 'HH:MM:SS' });
@@ -2254,7 +2439,8 @@ sub set_all {
$self->_add_remove($params, 'see_also');
# And set custom fields.
- my @custom_fields = Bugzilla->active_custom_fields;
+ my @custom_fields = grep { $_->type != FIELD_TYPE_EXTENSION }
+ Bugzilla->active_custom_fields;
foreach my $field (@custom_fields) {
my $fname = $field->name;
if (exists $params->{$fname}) {
@@ -2269,7 +2455,7 @@ sub set_all {
# we have to check that the current assignee, qa, and CCs are still
# valid if we've switched products, under strict_isolation. We can only
# do that here, because if they *did* change the assignee, qa, or CC,
- # then we don't want to check the original ones, only the new ones.
+ # then we don't want to check the original ones, only the new ones.
$self->_check_strict_isolation() if $product_changed;
}
@@ -2299,6 +2485,7 @@ sub reset_assigned_to {
my $comp = $self->component_obj;
$self->set_assigned_to($comp->default_assignee);
}
+sub set_bug_ignored { $_[0]->set('bug_ignored', $_[1]); }
sub set_cclist_accessible { $_[0]->set('cclist_accessible', $_[1]); }
sub set_comment_is_private {
my ($self, $comment_id, $isprivate) = @_;
@@ -2796,20 +2983,26 @@ sub add_group {
# If the bug is already in this group, then there is nothing to do.
return if $self->in_group($group);
-
- # Make sure that bugs in this product can actually be restricted
- # to this group by the current user.
- $self->product_obj->group_is_settable($group)
- || ThrowUserError('group_restriction_not_allowed', $args);
-
- # OtherControl people can add groups only during a product change,
- # and only when the group is not NA for them.
- if (!Bugzilla->user->in_group($group->name)) {
- my $controls = $self->product_obj->group_controls->{$group->id};
- if (!$self->{_old_product_name}
- || $controls->{othercontrol} == CONTROLMAPNA)
- {
- ThrowUserError('group_restriction_not_allowed', $args);
+ # BMO : allow bugs to be always placed into some groups by the bug's
+ # reporter, or by users with editbugs
+ my $user = Bugzilla->user;
+ if (!$self->product_obj->group_always_settable($group)
+ || ($self->{reporter_id} != $user->id && !$user->in_group('editbugs')))
+ {
+ # Make sure that bugs in this product can actually be restricted
+ # to this group by the current user.
+ $self->product_obj->group_is_settable($group)
+ || ThrowUserError('group_restriction_not_allowed', $args);
+
+ # OtherControl people can add groups only during a product change,
+ # and only when the group is not NA for them.
+ if (!$user->in_group($group->name)) {
+ my $controls = $self->product_obj->group_controls->{$group->id};
+ if (!$self->{_old_product_name}
+ || $controls->{othercontrol} == CONTROLMAPNA)
+ {
+ ThrowUserError('group_restriction_not_allowed', $args);
+ }
}
}
@@ -3156,7 +3349,7 @@ sub attachments {
return [] if $self->{'error'};
$self->{'attachments'} =
- Bugzilla::Attachment->get_attachments_by_bug($self->bug_id, {preload => 1});
+ Bugzilla::Attachment->get_attachments_by_bug($self, {preload => 1});
return $self->{'attachments'};
}
@@ -3164,8 +3357,8 @@ sub assigned_to {
my ($self) = @_;
return $self->{'assigned_to_obj'} if exists $self->{'assigned_to_obj'};
$self->{'assigned_to'} = 0 if $self->{'error'};
- $self->{'assigned_to_obj'} ||= new Bugzilla::User($self->{'assigned_to'});
- return $self->{'assigned_to_obj'};
+ return $self->{'assigned_to_obj'}
+ = new Bugzilla::User({ id => $self->{'assigned_to'}, cache => 1 });
}
sub blocked {
@@ -3229,11 +3422,8 @@ sub cc_users {
sub component {
my ($self) = @_;
- return $self->{component} if exists $self->{component};
return '' if $self->{error};
- ($self->{component}) = Bugzilla->dbh->selectrow_array(
- 'SELECT name FROM components WHERE id = ?',
- undef, $self->{component_id});
+ ($self->{component}) //= $self->component_obj->name;
return $self->{component};
}
@@ -3242,7 +3432,8 @@ sub component_obj {
my ($self) = @_;
return $self->{component_obj} if defined $self->{component_obj};
return {} if $self->{error};
- $self->{component_obj} = new Bugzilla::Component($self->{component_id});
+ $self->{component_obj} =
+ new Bugzilla::Component({ id => $self->{component_id}, cache => 1 });
return $self->{component_obj};
}
@@ -3281,6 +3472,26 @@ sub depends_on_obj {
return $self->{depends_on_obj};
}
+sub duplicates {
+ my $self = shift;
+ return $self->{duplicates} if exists $self->{duplicates};
+ return [] if $self->{error};
+ $self->{duplicates} = Bugzilla::Bug->new_from_list($self->duplicate_ids);
+ return $self->{duplicates};
+}
+
+sub duplicate_ids {
+ my $self = shift;
+ return $self->{duplicate_ids} if exists $self->{duplicate_ids};
+ return [] if $self->{error};
+
+ my $dbh = Bugzilla->dbh;
+ $self->{duplicate_ids} =
+ $dbh->selectcol_arrayref('SELECT dupe FROM duplicates WHERE dupe_of = ?',
+ undef, $self->id);
+ return $self->{duplicate_ids};
+}
+
sub flag_types {
my ($self) = @_;
return $self->{'flag_types'} if exists $self->{'flag_types'};
@@ -3289,7 +3500,8 @@ sub flag_types {
my $vars = { target_type => 'bug',
product_id => $self->{product_id},
component_id => $self->{component_id},
- bug_id => $self->bug_id };
+ bug_id => $self->bug_id,
+ active_or_has_flags => $self->bug_id };
$self->{'flag_types'} = Bugzilla::Flag->_flag_types($vars);
return $self->{'flag_types'};
@@ -3338,8 +3550,14 @@ sub comments {
$comment->{count} = $count++;
$comment->{bug} = $self;
}
- Bugzilla::Comment->preload($self->{'comments'});
+ # Some bugs may have no comments when upgrading old installations.
+ Bugzilla::Comment->preload($self->{'comments'}) if @{ $self->{'comments'} };
+ # BMO - for comment deletion support
+ Bugzilla::Hook::process('bug_comments',
+ { bug => $self, comments => $self->{'comments'} });
}
+ return unless defined wantarray;
+
my @comments = @{ $self->{'comments'} };
my $order = $params->{order}
@@ -3378,11 +3596,8 @@ sub percentage_complete {
sub product {
my ($self) = @_;
- return $self->{product} if exists $self->{product};
return '' if $self->{error};
- ($self->{product}) = Bugzilla->dbh->selectrow_array(
- 'SELECT name FROM products WHERE id = ?',
- undef, $self->{product_id});
+ ($self->{product}) //= $self->product_obj->name;
return $self->{product};
}
@@ -3390,7 +3605,8 @@ sub product {
sub product_obj {
my $self = shift;
return {} if $self->{error};
- $self->{product_obj} ||= new Bugzilla::Product($self->{product_id});
+ $self->{product_obj} ||=
+ new Bugzilla::Product({ id => $self->{product_id}, cache => 1 });
return $self->{product_obj};
}
@@ -3400,7 +3616,8 @@ sub qa_contact {
return undef if $self->{'error'};
if (Bugzilla->params->{'useqacontact'} && $self->{'qa_contact'}) {
- $self->{'qa_contact_obj'} = new Bugzilla::User($self->{'qa_contact'});
+ $self->{'qa_contact_obj'}
+ = new Bugzilla::User({ id => $self->{'qa_contact'}, cache => 1 });
} else {
# XXX - This is somewhat inconsistent with the assignee/reporter
# methods, which will return an empty User if they get a 0.
@@ -3414,8 +3631,8 @@ sub reporter {
my ($self) = @_;
return $self->{'reporter'} if exists $self->{'reporter'};
$self->{'reporter_id'} = 0 if $self->{'error'};
- $self->{'reporter'} = new Bugzilla::User($self->{'reporter_id'});
- return $self->{'reporter'};
+ return $self->{'reporter'}
+ = new Bugzilla::User({ id => $self->{'reporter_id'}, cache => 1 });
}
sub see_also {
@@ -3556,6 +3773,49 @@ sub groups {
}
}
+ # BMO: if required, hack in groups exposed by -visible membership
+ # (eg mozilla-employee-confidential-visible), so reporters can add the
+ # bug to a group on show_bug.
+ # if the bug is already in the group, the user will not be able to remove
+ # it unless they are a true group member.
+ my $user = Bugzilla->user;
+ if ($self->{'reporter_id'} == $user->id) {
+ foreach my $group (@{ $user->groups }) {
+ # map from -visible group to the real one
+ my $group_name = $group->name;
+ next unless $group_name =~ s/-visible$//;
+ next if $user->in_group($group_name);
+ $group = Bugzilla::Group->new({ name => $group_name, cache => 1 });
+
+ # only show the group if it's visible to normal members
+ my ($member_control) = $dbh->selectrow_array(
+ "SELECT membercontrol
+ FROM groups
+ LEFT JOIN group_control_map
+ ON group_control_map.group_id = groups.id
+ AND group_control_map.product_id = ?
+ WHERE groups.id = ?",
+ undef,
+ $self->{product_id}, $group->id
+ );
+
+ if (
+ $member_control
+ && $member_control == CONTROLMAPSHOWN
+ && !grep { $_->{bit} == $group->id } @groups)
+ {
+ push(@groups, {
+ bit => $group->id,
+ name => $group->name,
+ ison => 0,
+ ingroup => 1,
+ mandatory => 0,
+ description => $group->description,
+ });
+ }
+ }
+ }
+
$self->{'groups'} = \@groups;
return $self->{'groups'};
@@ -3674,6 +3934,9 @@ sub editable_bug_fields {
# Ensure field exists before attempting to remove it.
splice(@fields, $location, 1) if ($location > -1);
}
+
+ Bugzilla::Hook::process('bug_editable_bug_fields', { fields => \@fields });
+
# Sorted because the old @::log_columns variable, which this replaces,
# was sorted.
return sort(@fields);
@@ -3684,32 +3947,52 @@ sub editable_bug_fields {
# Join with bug_status and bugs tables to show bugs with open statuses first,
# and then the others
sub EmitDependList {
- my ($myfield, $targetfield, $bug_id) = (@_);
+ my ($my_field, $target_field, $bug_id, $exclude_resolved) = @_;
+ my $cache = Bugzilla->request_cache->{bug_dependency_list} ||= {};
+
my $dbh = Bugzilla->dbh;
- my $list_ref = $dbh->selectcol_arrayref(
- "SELECT $targetfield
+ $exclude_resolved = $exclude_resolved ? 1 : 0;
+ my $is_open_clause = $exclude_resolved ? 'AND is_open = 1' : '';
+
+ $cache->{"${target_field}_sth_$exclude_resolved"} ||= $dbh->prepare(
+ "SELECT $target_field
FROM dependencies
- INNER JOIN bugs ON dependencies.$targetfield = bugs.bug_id
+ INNER JOIN bugs ON dependencies.$target_field = bugs.bug_id
INNER JOIN bug_status ON bugs.bug_status = bug_status.value
- WHERE $myfield = ?
- ORDER BY is_open DESC, $targetfield",
- undef, $bug_id);
- return $list_ref;
+ WHERE $my_field = ? $is_open_clause
+ ORDER BY is_open DESC, $target_field");
+
+ return $dbh->selectcol_arrayref(
+ $cache->{"${target_field}_sth_$exclude_resolved"},
+ undef, $bug_id);
}
# Creates a lot of bug objects in the same order as the input array.
sub _bugs_in_order {
my ($self, $bug_ids) = @_;
- my $bugs = $self->new_from_list($bug_ids);
- my %bug_map = map { $_->id => $_ } @$bugs;
- my @result = map { $bug_map{$_} } @$bug_ids;
- return \@result;
+ my %bug_map;
+ # there's no need to load bugs from the database if they are already in the
+ # object-cache
+ my @missing_ids;
+ foreach my $bug_id (@$bug_ids) {
+ if (my $bug = Bugzilla::Bug->object_cache_get($bug_id)) {
+ $bug_map{$bug_id} = $bug;
+ }
+ else {
+ push @missing_ids, $bug_id;
+ }
+ }
+ my $bugs = $self->new_from_list(\@missing_ids);
+ foreach my $bug (@$bugs) {
+ $bug_map{$bug->id} = $bug;
+ }
+ return [ map { $bug_map{$_} } @$bug_ids ];
}
# Get the activity of a bug, starting from $starttime (if given).
# This routine assumes Bugzilla::Bug->check has been previously called.
sub GetBugActivity {
- my ($bug_id, $attach_id, $starttime) = @_;
+ my ($bug_id, $attach_id, $starttime, $include_comment_tags) = @_;
my $dbh = Bugzilla->dbh;
# Arguments passed to the SQL query.
@@ -3720,7 +4003,7 @@ sub GetBugActivity {
if (defined $starttime) {
trick_taint($starttime);
push (@args, $starttime);
- $datepart = "AND bugs_activity.bug_when > ?";
+ $datepart = "AND bug_when > ?";
}
my $attachpart = "";
@@ -3741,7 +4024,7 @@ sub GetBugActivity {
my $query = "SELECT fielddefs.name, bugs_activity.attach_id, " .
$dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') .
- ", bugs_activity.removed, bugs_activity.added, profiles.login_name,
+ " AS bug_when, bugs_activity.removed, bugs_activity.added, profiles.login_name,
bugs_activity.comment_id
FROM bugs_activity
$suppjoins
@@ -3752,8 +4035,42 @@ sub GetBugActivity {
WHERE bugs_activity.bug_id = ?
$datepart
$attachpart
- $suppwhere
- ORDER BY bugs_activity.bug_when";
+ $suppwhere ";
+
+ if (Bugzilla->params->{'comment_taggers_group'}
+ && $include_comment_tags
+ && !$attach_id)
+ {
+ # Only includes comment tag activity for comments the user is allowed to see.
+ $suppjoins = "";
+ $suppwhere = "";
+ if (!Bugzilla->user->is_insider) {
+ $suppjoins = "INNER JOIN longdescs
+ ON longdescs.comment_id = longdescs_tags_activity.comment_id";
+ $suppwhere = "AND longdescs.isprivate = 0";
+ }
+
+ $query .= "
+ UNION ALL
+ SELECT 'comment_tag' AS name,
+ NULL AS attach_id," .
+ $dbh->sql_date_format('longdescs_tags_activity.bug_when', '%Y.%m.%d %H:%i:%s') . " AS bug_when,
+ longdescs_tags_activity.removed,
+ longdescs_tags_activity.added,
+ profiles.login_name,
+ longdescs_tags_activity.comment_id as comment_id
+ FROM longdescs_tags_activity
+ INNER JOIN profiles ON profiles.userid = longdescs_tags_activity.who
+ $suppjoins
+ WHERE longdescs_tags_activity.bug_id = ?
+ $datepart
+ $suppwhere
+ ";
+ push @args, $bug_id;
+ push @args, $starttime if defined $starttime;
+ }
+
+ $query .= "ORDER BY bug_when, comment_id";
my $list = $dbh->selectall_arrayref($query, undef, @args);
@@ -3808,16 +4125,29 @@ sub GetBugActivity {
$changes = [];
}
+ # If this is the same field as the previoius item, then concatenate
+ # the data into the same change.
+ if ($operation->{'who'} && $who eq $operation->{'who'}
+ && $when eq $operation->{'when'}
+ && $fieldname eq $operation->{'fieldname'}
+ && ($comment_id || 0) == ($operation->{'comment_id'} || 0)
+ && ($attachid || 0) == ($operation->{'attachid'} || 0))
+ {
+ my $old_change = pop @$changes;
+ $removed = _join_activity_entries($fieldname, $old_change->{'removed'}, $removed);
+ $added = _join_activity_entries($fieldname, $old_change->{'added'}, $added);
+ }
+
$operation->{'who'} = $who;
$operation->{'when'} = $when;
+ $operation->{'fieldname'} = $change{'fieldname'} = $fieldname;
+ $operation->{'attachid'} = $change{'attachid'} = $attachid;
- $change{'fieldname'} = $fieldname;
- $change{'attachid'} = $attachid;
$change{'removed'} = $removed;
$change{'added'} = $added;
-
+
if ($comment_id) {
- $change{'comment'} = Bugzilla::Comment->new($comment_id);
+ $operation->{comment_id} = $change{'comment'} = Bugzilla::Comment->new($comment_id);
}
push (@$changes, \%change);
@@ -3832,9 +4162,40 @@ sub GetBugActivity {
return(\@operations, $incomplete_data);
}
+sub _join_activity_entries {
+ my ($field, $current_change, $new_change) = @_;
+ # We need to insert characters as these were removed by old
+ # LogActivityEntry code.
+
+ return $new_change if $current_change eq '';
+
+ # Buglists and see_also need the comma restored
+ if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') {
+ if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') {
+ return $current_change . $new_change;
+ } else {
+ return $current_change . ', ' . $new_change;
+ }
+ }
+
+ # Assume bug_file_loc contain a single url, don't insert a delimiter
+ if ($field eq 'bug_file_loc') {
+ return $current_change . $new_change;
+ }
+
+ # All other fields get a space unless the first character of the second
+ # string is a comma or space
+ if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') {
+ return $current_change . $new_change;
+ } else {
+ return $current_change . ' ' . $new_change;
+ }
+}
+
# Update the bugs_activity table to reflect changes made in bugs.
sub LogActivityEntry {
- my ($i, $col, $removed, $added, $whoid, $timestamp, $comment_id) = @_;
+ my ($i, $col, $removed, $added, $whoid, $timestamp, $comment_id,
+ $attach_id) = @_;
my $dbh = Bugzilla->dbh;
# in the case of CCs, deps, and keywords, there's a possibility that someone
# might try to add or remove a lot of them at once, which might take more
@@ -3846,7 +4207,6 @@ sub LogActivityEntry {
my $commaposition = find_wrap_point($removed, MAX_LINE_LENGTH);
$removestr = substr($removed, 0, $commaposition);
$removed = substr($removed, $commaposition);
- $removed =~ s/^[,\s]+//; # remove any comma or space
} else {
$removed = ""; # no more entries
}
@@ -3854,17 +4214,36 @@ sub LogActivityEntry {
my $commaposition = find_wrap_point($added, MAX_LINE_LENGTH);
$addstr = substr($added, 0, $commaposition);
$added = substr($added, $commaposition);
- $added =~ s/^[,\s]+//; # remove any comma or space
} else {
$added = ""; # no more entries
}
trick_taint($addstr);
trick_taint($removestr);
my $fieldid = get_field_id($col);
- $dbh->do("INSERT INTO bugs_activity
- (bug_id, who, bug_when, fieldid, removed, added, comment_id)
- VALUES (?, ?, ?, ?, ?, ?, ?)",
- undef, ($i, $whoid, $timestamp, $fieldid, $removestr, $addstr, $comment_id));
+ $dbh->do(
+ "INSERT INTO bugs_activity
+ (bug_id, who, bug_when, fieldid, removed, added, comment_id, attach_id)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+ undef,
+ ($i, $whoid, $timestamp, $fieldid, $removestr, $addstr, $comment_id,
+ $attach_id));
+ }
+}
+
+# Update bug_user_last_visit table
+sub update_user_last_visit {
+ my ($self, $user, $last_visit_ts) = @_;
+ my $lv = Bugzilla::BugUserLastVisit->match({ bug_id => $self->id,
+ user_id => $user->id })->[0];
+
+ if ($lv) {
+ $lv->set(last_visit_ts => $last_visit_ts);
+ $lv->update;
+ }
+ else {
+ Bugzilla::BugUserLastVisit->create({ bug_id => $self->id,
+ user_id => $user->id,
+ last_visit_ts => $last_visit_ts });
}
}
@@ -3941,8 +4320,8 @@ sub check_can_change_field {
return 1;
}
- # Allow anyone to change comments.
- if ($field =~ /^longdesc/) {
+ # Allow anyone to change comments, or set flags
+ if ($field =~ /^longdesc/ || $field eq 'flagtypes.name') {
return 1;
}
@@ -4148,6 +4527,7 @@ sub _create_cf_accessors {
my $fields = Bugzilla->fields({ custom => 1 });
foreach my $field (@$fields) {
+ next if $field->type == FIELD_TYPE_EXTENSION;
my $accessor = $class->_accessor_for($field);
my $name = "${class}::" . $field->name;
{
@@ -4157,6 +4537,8 @@ sub _create_cf_accessors {
}
}
+ Bugzilla::Hook::process('bug_create_cf_accessors');
+
Bugzilla->request_cache->{"${class}_cf_accessors_created"} = 1;
}
diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm
index 55eeeab25..9980535ce 100644
--- a/Bugzilla/BugMail.pm
+++ b/Bugzilla/BugMail.pm
@@ -47,7 +47,9 @@ use Bugzilla::Hook;
use Date::Parse;
use Date::Format;
use Scalar::Util qw(blessed);
-use List::MoreUtils qw(uniq);
+use List::MoreUtils qw(uniq firstidx);
+use Sys::Hostname;
+use Storable qw(dclone);
use constant BIT_DIRECT => 1;
use constant BIT_WATCHING => 2;
@@ -107,30 +109,53 @@ sub Send {
my %user_cache = map { $_->id => $_ } (@assignees, @qa_contacts, @ccs);
my @diffs;
+ my @referenced_bugs;
if (!$start) {
@diffs = _get_new_bugmail_fields($bug);
}
if ($params->{dep_only}) {
+ my $fields = Bugzilla->fields({ by_name => 1 });
push(@diffs, { field_name => 'bug_status',
+ field_desc => $fields->{bug_status}->description,
old => $params->{changes}->{bug_status}->[0],
new => $params->{changes}->{bug_status}->[1],
login_name => $changer->login,
blocker => $params->{blocker} },
{ field_name => 'resolution',
+ field_desc => $fields->{resolution}->description,
old => $params->{changes}->{resolution}->[0],
new => $params->{changes}->{resolution}->[1],
login_name => $changer->login,
blocker => $params->{blocker} });
+ push(@referenced_bugs, $params->{blocker}->id);
}
else {
- push(@diffs, _get_diffs($bug, $end, \%user_cache));
+ my ($diffs, $referenced) = _get_diffs($bug, $end, \%user_cache);
+ push(@diffs, @$diffs);
+ push(@referenced_bugs, @$referenced);
}
my $comments = $bug->comments({ after => $start, to => $end });
# Skip empty comments.
@$comments = grep { $_->type || $_->body =~ /\S/ } @$comments;
+ # Add duplicate bug to referenced bug list
+ foreach my $comment (@$comments) {
+ if ($comment->type == CMT_DUPE_OF || $comment->type == CMT_HAS_DUPE) {
+ push(@referenced_bugs, $comment->extra_data);
+ }
+ }
+
+ # Add dependencies to referenced bug list on new bugs
+ if (!$start) {
+ push @referenced_bugs, @{ $bug->dependson };
+ push @referenced_bugs, @{ $bug->blocked };
+ }
+
+ # If no changes have been made, there is no need to process further.
+ return {'sent' => []} unless scalar(@diffs) || scalar(@$comments);
+
###########################################################################
# Start of email filtering code
###########################################################################
@@ -186,28 +211,30 @@ sub Send {
# Make sure %user_cache has every user in it so far referenced
foreach my $user_id (keys %recipients) {
- $user_cache{$user_id} ||= new Bugzilla::User($user_id);
+ $user_cache{$user_id} ||= new Bugzilla::User({ id => $user_id, cache => 1 });
}
Bugzilla::Hook::process('bugmail_recipients',
{ bug => $bug, recipients => \%recipients,
users => \%user_cache, diffs => \@diffs });
- # Find all those user-watching anyone on the current list, who is not
- # on it already themselves.
- my $involved = join(",", keys %recipients);
+ if (scalar keys %recipients) {
+ # Find all those user-watching anyone on the current list, who is not
+ # on it already themselves.
+ my $involved = join(",", keys %recipients);
- my $userwatchers =
- $dbh->selectall_arrayref("SELECT watcher, watched FROM watch
- WHERE watched IN ($involved)");
+ my $userwatchers =
+ $dbh->selectall_arrayref("SELECT watcher, watched FROM watch
+ WHERE watched IN ($involved)");
- # Mark these people as having the role of the person they are watching
- foreach my $watch (@$userwatchers) {
- while (my ($role, $bits) = each %{$recipients{$watch->[1]}}) {
- $recipients{$watch->[0]}->{$role} |= BIT_WATCHING
- if $bits & BIT_DIRECT;
+ # Mark these people as having the role of the person they are watching
+ foreach my $watch (@$userwatchers) {
+ while (my ($role, $bits) = each %{$recipients{$watch->[1]}}) {
+ $recipients{$watch->[0]}->{$role} |= BIT_WATCHING
+ if $bits & BIT_DIRECT;
+ }
+ push(@{$watching{$watch->[0]}}, $watch->[1]);
}
- push(@{$watching{$watch->[0]}}, $watch->[1]);
}
# Global watcher
@@ -222,21 +249,25 @@ sub Send {
# the bug in question. However, we are not necessarily going to mail them
# all - there are preferences, permissions checks and all sorts to do yet.
my @sent;
- my @excluded;
# The email client will display the Date: header in the desired timezone,
# so we can always use UTC here.
my $date = $params->{dep_only} ? $end : $bug->delta_ts;
$date = format_time($date, '%a, %d %b %Y %T %z', 'UTC');
+ # Remove duplicate references, and convert to bug objects
+ @referenced_bugs = @{ Bugzilla::Bug->new_from_list([uniq @referenced_bugs]) };
+
foreach my $user_id (keys %recipients) {
my %rels_which_want;
- my $sent_mail = 0;
- $user_cache{$user_id} ||= new Bugzilla::User($user_id);
- my $user = $user_cache{$user_id};
+ my $user = $user_cache{$user_id} ||= new Bugzilla::User({ id => $user_id, cache => 1 });
# Deleted users must be excluded.
next unless $user;
+ # If email notifications are disabled for this account, or the bug
+ # is ignored, there is no need to do additional checks.
+ next if ($user->email_disabled || $user->is_bug_ignored($id));
+
if ($user->can_see_bug($id)) {
# Go through each role the user has and see if they want mail in
# that role.
@@ -253,7 +284,7 @@ sub Send {
}
}
}
-
+
if (scalar(%rels_which_want)) {
# So the user exists, can see the bug, and wants mail in at least
# one role. But do we want to send it to them?
@@ -267,10 +298,32 @@ sub Send {
}
# Make sure the user isn't in the nomail list, and the dep check passed.
- if ($user->email_enabled && $dep_ok) {
- # OK, OK, if we must. Email the user.
- $sent_mail = sendMail(
- { to => $user,
+ # BMO: never send emails to bugs or .tld addresses. this check needs to
+ # happen after the bugmail_recipients hook.
+ if ($user->email_enabled && $dep_ok &&
+ ($user->login !~ /bugs$/) && ($user->login !~ /\.tld$/))
+ {
+ # Don't show summaries for bugs the user can't access, and
+ # provide a hook for extensions such as SecureMail to filter
+ # this list.
+ #
+ # We build an array with the short_desc as a separate item to
+ # allow extensions to modify the summary without touching the
+ # bug object.
+ my $referenced_bugs = [];
+ foreach my $ref (@{ $user->visible_bugs(\@referenced_bugs) }) {
+ push @$referenced_bugs, {
+ bug => $ref,
+ id => $ref->id,
+ short_desc => $ref->short_desc,
+ };
+ }
+ Bugzilla::Hook::process('bugmail_referenced_bugs',
+ { updated_bug => $bug,
+ referenced_bugs => $referenced_bugs });
+
+ my $sent_mail = sendMail(
+ { to => $user,
bug => $bug,
comments => $comments,
date => $date,
@@ -279,16 +332,12 @@ sub Send {
$watching{$user_id} : undef,
diffs => \@diffs,
rels_which_want => \%rels_which_want,
+ referenced_bugs => $referenced_bugs,
+ dep_only => $params->{dep_only}
});
+ push(@sent, $user->login) if $sent_mail;
}
}
-
- if ($sent_mail) {
- push(@sent, $user->login);
- }
- else {
- push(@excluded, $user->login);
- }
}
# When sending bugmail about a blocker being reopened or resolved,
@@ -300,12 +349,12 @@ sub Send {
$bug->{lastdiffed} = $end;
}
- return {'sent' => \@sent, 'excluded' => \@excluded};
+ return {'sent' => \@sent};
}
sub sendMail {
my $params = shift;
-
+
my $user = $params->{to};
my $bug = $params->{bug};
my @send_comments = @{ $params->{comments} };
@@ -314,6 +363,8 @@ sub sendMail {
my $watchingRef = $params->{watchers};
my @diffs = @{ $params->{diffs} };
my $relRef = $params->{rels_which_want};
+ my $referenced_bugs = $params->{referenced_bugs};
+ my $dep_only = $params->{dep_only};
# Only display changes the user is allowed see.
my @display_diffs;
@@ -352,33 +403,113 @@ sub sendMail {
push(@watchingrel, 'None') unless @watchingrel;
push @watchingrel, map { user_id_to_login($_) } @$watchingRef;
+ # BMO: Use field descriptions instead of field names in header
+ my @changedfields = uniq map { $_->{field_desc} } @display_diffs;
+ my @changedfieldnames = uniq map { $_->{field_name} } @display_diffs;
+
+ # BMO: Add a field to indicate when a comment was added
+ if (grep($_->type != CMT_ATTACHMENT_CREATED, @send_comments)) {
+ push(@changedfields, 'Comment Created');
+ push(@changedfieldnames, 'comment');
+ }
+
+ # Add attachments.created to changedfields if one or more
+ # comments contain information about a new attachment
+ if (grep($_->type == CMT_ATTACHMENT_CREATED, @send_comments)) {
+ push(@changedfields, 'Attachment Created');
+ push(@changedfieldnames, 'attachment.created');
+ }
+
+ my $bugmailtype = "changed";
+ $bugmailtype = "new" if !$bug->lastdiffed;
+ $bugmailtype = "dep_changed" if $dep_only;
+
my $vars = {
- date => $date,
- to_user => $user,
- bug => $bug,
- reasons => \@reasons,
- reasons_watch => \@reasons_watch,
- reasonsheader => join(" ", @headerrel),
+ date => $date,
+ to_user => $user,
+ bug => $bug,
+ reasons => \@reasons,
+ reasons_watch => \@reasons_watch,
+ reasonsheader => join(" ", @headerrel),
reasonswatchheader => join(" ", @watchingrel),
- changer => $changer,
- diffs => \@display_diffs,
- changedfields => [uniq map { $_->{field_name} } @display_diffs],
- new_comments => \@send_comments,
- threadingmarker => build_thread_marker($bug->id, $user->id, !$bug->lastdiffed),
+ changer => $changer,
+ diffs => \@display_diffs,
+ changedfields => \@changedfields,
+ changedfieldnames => \@changedfieldnames,
+ new_comments => \@send_comments,
+ threadingmarker => build_thread_marker($bug->id, $user->id, !$bug->lastdiffed),
+ referenced_bugs => $referenced_bugs,
+ bugmailtype => $bugmailtype,
};
- my $msg = _generate_bugmail($user, $vars);
- MessageToMTA($msg);
+
+ if (Bugzilla->params->{'use_mailer_queue'}) {
+ enqueue($vars);
+ } else {
+ MessageToMTA(_generate_bugmail($vars));
+ }
return 1;
}
+sub enqueue {
+ my ($vars) = @_;
+ # we need to flatten all objects to a hash before pushing to the job queue.
+ # the hashes need to be inflated in the dequeue method.
+ $vars->{bug} = _flatten_object($vars->{bug});
+ $vars->{to_user} = _flatten_object($vars->{to_user});
+ $vars->{changer} = _flatten_object($vars->{changer});
+ $vars->{new_comments} = [ map { _flatten_object($_) } @{ $vars->{new_comments} } ];
+ foreach my $diff (@{ $vars->{diffs} }) {
+ $diff->{who} = _flatten_object($diff->{who});
+ if (exists $diff->{blocker}) {
+ $diff->{blocker} = _flatten_object($diff->{blocker});
+ }
+ }
+ foreach my $reference (@{ $vars->{referenced_bugs} }) {
+ $reference->{bug} = _flatten_object($reference->{bug});
+ }
+ Bugzilla->job_queue->insert('bug_mail', { vars => $vars });
+}
+
+sub dequeue {
+ my ($payload) = @_;
+ # clone the payload so we can modify it without impacting TheSchwartz's
+ # ability to process the job when we've finished
+ my $vars = dclone($payload);
+ # inflate objects
+ $vars->{bug} = Bugzilla::Bug->new_from_hash($vars->{bug});
+ $vars->{to_user} = Bugzilla::User->new_from_hash($vars->{to_user});
+ $vars->{changer} = Bugzilla::User->new_from_hash($vars->{changer});
+ $vars->{new_comments} = [ map { Bugzilla::Comment->new_from_hash($_) } @{ $vars->{new_comments} } ];
+ foreach my $diff (@{ $vars->{diffs} }) {
+ $diff->{who} = Bugzilla::User->new_from_hash($diff->{who});
+ if (exists $diff->{blocker}) {
+ $diff->{blocker} = Bugzilla::Bug->new_from_hash($diff->{blocker});
+ }
+ }
+ # generate bugmail and send
+ MessageToMTA(_generate_bugmail($vars), 1);
+}
+
+sub _flatten_object {
+ my ($object) = @_;
+ # nothing to do if it's already flattened
+ return $object unless blessed($object);
+ # the same objects are used for each recipient, so cache the flattened hash
+ my $cache = Bugzilla->request_cache->{bugmail_flat_objects} ||= {};
+ my $key = blessed($object) . '-' . $object->id;
+ return $cache->{$key} ||= $object->flatten_to_hash;
+}
+
sub _generate_bugmail {
- my ($user, $vars) = @_;
+ my ($vars) = @_;
+ my $user = $vars->{to_user};
my $template = Bugzilla->template_inner($user->setting('lang'));
my ($msg_text, $msg_html, $msg_header);
-
+
$template->process("email/bugmail-header.txt.tmpl", $vars, \$msg_header)
|| ThrowTemplateError($template->error());
+
$template->process("email/bugmail.txt.tmpl", $vars, \$msg_text)
|| ThrowTemplateError($template->error());
@@ -395,7 +526,7 @@ sub _generate_bugmail {
|| ThrowTemplateError($template->error());
push @parts, Email::MIME->create(
attributes => {
- content_type => "text/html",
+ content_type => "text/html",
},
body => $msg_html,
);
@@ -403,6 +534,10 @@ sub _generate_bugmail {
# TT trims the trailing newline, and threadingmarker may be ignored.
my $email = new Email::MIME("$msg_header\n");
+
+ # For tracking/diagnostic purposes, add our hostname
+ $email->header_set('X-Generated-By' => hostname());
+
if (scalar(@parts) == 1) {
$email->content_type_set($parts[0]->content_type);
} else {
@@ -426,6 +561,7 @@ sub _get_diffs {
my $diffs = $dbh->selectall_arrayref(
"SELECT fielddefs.name AS field_name,
+ fielddefs.description AS field_desc,
bugs_activity.bug_when, bugs_activity.removed AS old,
bugs_activity.added AS new, bugs_activity.attach_id,
bugs_activity.comment_id, bugs_activity.who
@@ -434,11 +570,12 @@ sub _get_diffs {
ON fielddefs.id = bugs_activity.fieldid
WHERE bugs_activity.bug_id = ?
$when_restriction
- ORDER BY bugs_activity.bug_when", {Slice=>{}}, @args);
+ ORDER BY bugs_activity.bug_when, fielddefs.description", {Slice=>{}}, @args);
+ my $referenced_bugs = [];
foreach my $diff (@$diffs) {
- $user_cache->{$diff->{who}} ||= new Bugzilla::User($diff->{who});
- $diff->{who} = $user_cache->{$diff->{who}};
+ $user_cache->{$diff->{who}} ||= new Bugzilla::User({ id => $diff->{who}, cache => 1 });
+ $diff->{who} = $user_cache->{$diff->{who}};
if ($diff->{attach_id}) {
$diff->{isprivate} = $dbh->selectrow_array(
'SELECT isprivate FROM attachments WHERE attach_id = ?',
@@ -449,9 +586,13 @@ sub _get_diffs {
$diff->{num} = $comment->count;
$diff->{isprivate} = $diff->{new};
}
+ elsif ($diff->{field_name} eq 'dependson' || $diff->{field_name} eq 'blocked') {
+ push @$referenced_bugs, grep { /^\d+$/ } split(/[\s,]+/, $diff->{old});
+ push @$referenced_bugs, grep { /^\d+$/ } split(/[\s,]+/, $diff->{new});
+ }
}
- return @$diffs;
+ return ($diffs, $referenced_bugs);
}
sub _get_new_bugmail_fields {
@@ -459,6 +600,20 @@ sub _get_new_bugmail_fields {
my @fields = @{ Bugzilla->fields({obsolete => 0, in_new_bugmail => 1}) };
my @diffs;
+ # Show fields in the same order as the DEFAULT_FIELDS list, which mirrors
+ # 4.0's behavour and provides sane grouping of similar fields.
+ # Any additional fields are sorted by descrsiption
+ my @prepend;
+ foreach my $name (map { $_->{name} } Bugzilla::Field::DEFAULT_FIELDS) {
+ my $idx = firstidx { $_->name eq $name } @fields;
+ if ($idx != -1) {
+ push(@prepend, $fields[$idx]);
+ splice(@fields, $idx, 1);
+ }
+ }
+ @fields = sort { $a->description cmp $b->description } @fields;
+ @fields = (@prepend, @fields);
+
foreach my $field (@fields) {
my $name = $field->name;
my $value = $bug->$name;
@@ -484,7 +639,9 @@ sub _get_new_bugmail_fields {
# If there isn't anything to show, don't include this header.
next unless $value;
- push(@diffs, {field_name => $name, new => $value});
+ push(@diffs, {field_name => $name,
+ field_desc => $field->description,
+ new => $value});
}
return @diffs;
diff --git a/Bugzilla/BugUrl.pm b/Bugzilla/BugUrl.pm
index 837c0d4fe..f59ec3601 100644
--- a/Bugzilla/BugUrl.pm
+++ b/Bugzilla/BugUrl.pm
@@ -69,6 +69,8 @@ use constant SUB_CLASSES => qw(
Bugzilla::BugUrl::Trac
Bugzilla::BugUrl::MantisBT
Bugzilla::BugUrl::SourceForge
+ Bugzilla::BugUrl::GitHub
+ Bugzilla::BugUrl::MozSupport
);
###############################
diff --git a/Bugzilla/BugUrl/GitHub.pm b/Bugzilla/BugUrl/GitHub.pm
new file mode 100644
index 000000000..63be65bed
--- /dev/null
+++ b/Bugzilla/BugUrl/GitHub.pm
@@ -0,0 +1,36 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::BugUrl::GitHub;
+use strict;
+use base qw(Bugzilla::BugUrl);
+
+###############################
+#### Methods ####
+###############################
+
+sub should_handle {
+ my ($class, $uri) = @_;
+
+ # GitHub issue URLs have only one form:
+ # https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/issues/111
+ return ($uri->authority =~ /^github.com$/i
+ and $uri->path =~ m|^/[^/]+/[^/]+/issues/\d+$|) ? 1 : 0;
+}
+
+sub _check_value {
+ my ($class, $uri) = @_;
+
+ $uri = $class->SUPER::_check_value($uri);
+
+ # GitHub HTTP URLs redirect to HTTPS, so just use the HTTPS scheme.
+ $uri->scheme('https');
+
+ return $uri;
+}
+
+1;
diff --git a/Bugzilla/BugUrl/MozSupport.pm b/Bugzilla/BugUrl/MozSupport.pm
new file mode 100644
index 000000000..f78bec799
--- /dev/null
+++ b/Bugzilla/BugUrl/MozSupport.pm
@@ -0,0 +1,36 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::BugUrl::MozSupport;
+use strict;
+use base qw(Bugzilla::BugUrl);
+
+###############################
+#### Methods ####
+###############################
+
+sub should_handle {
+ my ($class, $uri) = @_;
+
+ # Mozilla support questions normally have the form:
+ # https://support.mozilla.org/<language>/questions/<id>
+ return ($uri->authority =~ /^support.mozilla.org$/i
+ and $uri->path =~ m|^(/[^/]+)?/questions/\d+$|) ? 1 : 0;
+}
+
+sub _check_value {
+ my ($class, $uri) = @_;
+
+ $uri = $class->SUPER::_check_value($uri);
+
+ # Support.mozilla.org redirects to https automatically
+ $uri->scheme('https');
+
+ return $uri;
+}
+
+1;
diff --git a/Bugzilla/BugUserLastVisit.pm b/Bugzilla/BugUserLastVisit.pm
new file mode 100644
index 000000000..6a7b7f3ef
--- /dev/null
+++ b/Bugzilla/BugUserLastVisit.pm
@@ -0,0 +1,92 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::BugUserLastVisit;
+
+use 5.10.1;
+use strict;
+
+use parent qw(Bugzilla::Object);
+
+#####################################################################
+# Overriden Constants that are used as methods
+#####################################################################
+
+use constant DB_TABLE => 'bug_user_last_visit';
+use constant DB_COLUMNS => qw( id user_id bug_id last_visit_ts );
+use constant UPDATE_COLUMNS => qw( last_visit_ts );
+use constant VALIDATORS => {};
+use constant LIST_ORDER => 'id';
+use constant NAME_FIELD => 'id';
+
+# turn off auditing and exclude these objects from memcached
+use constant { AUDIT_CREATES => 0,
+ AUDIT_UPDATES => 0,
+ AUDIT_REMOVES => 0,
+ USE_MEMCACHED => 0 };
+
+#####################################################################
+# Provide accessors for our columns
+#####################################################################
+
+sub id { return $_[0]->{id} }
+sub bug_id { return $_[0]->{bug_id} }
+sub user_id { return $_[0]->{user_id} }
+sub last_visit_ts { return $_[0]->{last_visit_ts} }
+
+sub user {
+ my $self = shift;
+
+ $self->{user} //= Bugzilla::User->new({id => $self->user_id, cache => 1});
+ return $self->{user};
+}
+
+1;
+__END__
+
+=head1 NAME
+
+Bugzilla::BugUserLastVisit - Model for BugUserLastVisit bug search data
+
+=head1 SYNOPSIS
+
+ use Bugzilla::BugUserLastVisit;
+
+ my $lv = Bugzilla::BugUserLastVisit->new($id);
+
+ # Class Functions
+ $user = Bugzilla::BugUserLastVisit->create({
+ bug_id => $bug_id,
+ user_id => $user_id,
+ last_visit_ts => $last_visit_ts
+ });
+
+=head1 DESCRIPTION
+
+This package handles Bugzilla BugUserLastVisit.
+
+C<Bugzilla::BugUserLastVisit> is an implementation of L<Bugzilla::Object>, and
+thus provides all the methods of L<Bugzilla::Object> in addition to the methods
+listed below.
+
+=head1 METHODS
+
+=head2 Accessor Methods
+
+=over
+
+=item C<id>
+
+=item C<bug_id>
+
+=item C<user_id>
+
+=item C<last_visit_ts>
+
+=item C<user>
+
+=back
diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm
index 4dd223a31..a12fb284b 100644
--- a/Bugzilla/CGI.pm
+++ b/Bugzilla/CGI.pm
@@ -73,11 +73,22 @@ sub new {
# Make sure our outgoing cookie list is empty on each invocation
$self->{Bugzilla_cookie_list} = [];
+ # Path-Info is of no use for Bugzilla and interacts badly with IIS.
+ # Moreover, it causes unexpected behaviors, such as totally breaking
+ # the rendering of pages.
+ my $script = basename($0);
+ if ($self->path_info) {
+ my @whitelist = ("rest.cgi");
+ Bugzilla::Hook::process('path_info_whitelist', { whitelist => \@whitelist });
+ if (!grep($_ eq $script, @whitelist)) {
+ print $self->redirect($self->url(-path => 0, -query => 1));
+ }
+ }
+
# Send appropriate charset
$self->charset(Bugzilla->params->{'utf8'} ? 'UTF-8' : '');
# Redirect to urlbase/sslbase if we are not viewing an attachment.
- my $script = basename($0);
if ($self->url_is_attachment_base and $script ne 'attachment.cgi') {
$self->redirect_to_urlbase();
}
@@ -127,7 +138,8 @@ sub canonicalise_query {
my $esc_key = url_quote($key);
foreach my $value ($self->param($key)) {
- if (defined($value)) {
+ # Omit params with an empty value
+ if (defined($value) && $value ne '') {
my $esc_value = url_quote($value);
push(@parameters, "$esc_key=$esc_value");
@@ -224,6 +236,26 @@ sub clean_search_url {
}
}
+sub check_etag {
+ my ($self, $valid_etag) = @_;
+
+ # ETag support.
+ my $if_none_match = $self->http('If-None-Match');
+ return if !$if_none_match;
+
+ my @if_none = split(/[\s,]+/, $if_none_match);
+ foreach my $possible_etag (@if_none) {
+ # remove quotes from begin and end of the string
+ $possible_etag =~ s/^\"//g;
+ $possible_etag =~ s/\"$//g;
+ if ($possible_etag eq $valid_etag or $possible_etag eq '*') {
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
# Overwrite to ensure nph doesn't get set, and unset HEADERS_ONCE
sub multipart_init {
my $self = shift;
@@ -278,9 +310,24 @@ sub multipart_start {
$headers .= "Set-Cookie: ${cookie}${CGI::CRLF}";
}
$headers .= $CGI::CRLF;
+ $self->{_multipart_in_progress} = 1;
return $headers;
}
+sub close_standby_message {
+ my ($self, $contenttype, $disposition) = @_;
+
+ if ($self->{_multipart_in_progress}) {
+ print $self->multipart_end();
+ print $self->multipart_start(-type => $contenttype,
+ -content_disposition => $disposition);
+ }
+ else {
+ print $self->header(-type => $contenttype,
+ -content_disposition => $disposition);
+ }
+}
+
# Override header so we can add the cookies in
sub header {
my $self = shift;
@@ -316,6 +363,10 @@ sub header {
unshift(@_, '-x_frame_options' => 'SAMEORIGIN');
}
+ if ($self->{'_content_disp'}) {
+ unshift(@_, '-content_disposition' => $self->{'_content_disp'});
+ }
+
# Add X-XSS-Protection header to prevent simple XSS attacks
# and enforce the blocking (rather than the rewriting) mode.
unshift(@_, '-x_xss_protection' => '1; mode=block');
@@ -432,6 +483,10 @@ sub remove_cookie {
# URLs that get POSTed to buglist.cgi.
sub redirect_search_url {
my $self = shift;
+
+ # If there is no parameter, there is nothing to do.
+ return unless $self->param;
+
# If we're retreiving an old list, we never need to redirect or
# do anything related to Bugzilla::Search::Recent.
return if $self->param('regetlastlist');
@@ -470,9 +525,9 @@ sub redirect_search_url {
# GET requests that lacked a list_id are always redirected. POST requests
# are only redirected if they're under the CGI_URI_LIMIT though.
- my $uri_length = length($self->self_url());
- if ($self->request_method() ne 'POST' or $uri_length < CGI_URI_LIMIT) {
- print $self->redirect(-url => $self->self_url());
+ my $self_url = $self->self_url();
+ if ($self->request_method() ne 'POST' or length($self_url) < CGI_URI_LIMIT) {
+ print $self->redirect(-url => $self_url);
exit;
}
}
@@ -526,7 +581,23 @@ sub url_is_attachment_base {
$regex =~ s/\\\%bugid\\\%/\\d+/;
}
$regex = "^$regex";
- return ($self->self_url =~ $regex) ? 1 : 0;
+ return ($self->url =~ $regex) ? 1 : 0;
+}
+
+sub set_dated_content_disp {
+ my ($self, $type, $prefix, $ext) = @_;
+
+ my @time = localtime(time());
+ my $date = sprintf "%04d-%02d-%02d", 1900+$time[5], $time[4]+1, $time[3];
+ my $filename = "$prefix-$date.$ext";
+
+ $filename =~ s/\s/_/g; # Remove whitespace to avoid HTTP header tampering
+ $filename =~ s/\\/_/g; # Remove backslashes as well
+ $filename =~ s/"/\\"/g; # escape quotes
+
+ my $disposition = "$type; filename=\"$filename\"";
+
+ $self->{'_content_disp'} = $disposition;
}
##########################
@@ -603,7 +674,9 @@ I<Bugzilla::CGI> also includes additional functions.
=item C<canonicalise_query(@exclude)>
-This returns a sorted string of the parameters, suitable for use in a url.
+This returns a sorted string of the parameters whose values are non-empty,
+suitable for use in a url.
+
Values in C<@exclude> are not included in the result.
=item C<send_cookie>
@@ -636,6 +709,11 @@ instead of calling this directly.
Redirects from the current URL to one prefixed by the urlbase parameter.
+=item C<set_dated_content_disp>
+
+Sets an appropriate date-dependent value for the Content Disposition header
+for a downloadable resource.
+
=back
=head1 SEE ALSO
diff --git a/Bugzilla/Classification.pm b/Bugzilla/Classification.pm
index 88ec4eb3b..a989a40f2 100644
--- a/Bugzilla/Classification.pm
+++ b/Bugzilla/Classification.pm
@@ -31,6 +31,8 @@ use base qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object);
#### Initialization ####
###############################
+use constant IS_CONFIG => 1;
+
use constant DB_TABLE => 'classifications';
use constant LIST_ORDER => 'sortkey, name';
@@ -56,6 +58,7 @@ use constant VALIDATORS => {
###############################
#### Constructors #####
###############################
+
sub remove_from_db {
my $self = shift;
my $dbh = Bugzilla->dbh;
@@ -63,9 +66,19 @@ sub remove_from_db {
ThrowUserError("classification_not_deletable") if ($self->id == 1);
$dbh->bz_start_transaction();
+
# Reclassify products to the default classification, if needed.
- $dbh->do("UPDATE products SET classification_id = 1
- WHERE classification_id = ?", undef, $self->id);
+ my $product_ids = $dbh->selectcol_arrayref(
+ 'SELECT id FROM products WHERE classification_id = ?', undef, $self->id);
+
+ if (@$product_ids) {
+ $dbh->do('UPDATE products SET classification_id = 1 WHERE '
+ . $dbh->sql_in('id', $product_ids));
+ foreach my $id (@$product_ids) {
+ Bugzilla->memcached->clear({ table => 'products', id => $id });
+ }
+ Bugzilla->memcached->clear_config();
+ }
$self->SUPER::remove_from_db();
diff --git a/Bugzilla/Comment.pm b/Bugzilla/Comment.pm
index ee342fb2d..979019541 100644
--- a/Bugzilla/Comment.pm
+++ b/Bugzilla/Comment.pm
@@ -26,11 +26,14 @@ package Bugzilla::Comment;
use base qw(Bugzilla::Object);
use Bugzilla::Attachment;
+use Bugzilla::Comment::TagWeights;
use Bugzilla::Constants;
use Bugzilla::Error;
+use Bugzilla::Hook;
use Bugzilla::User;
use Bugzilla::Util;
+use List::Util qw(first);
use Scalar::Util qw(blessed);
###############################
@@ -92,21 +95,91 @@ use constant VALIDATOR_DEPENDENCIES => {
sub update {
my $self = shift;
- my $changes = $self->SUPER::update(@_);
- $self->bug->_sync_fulltext();
+ my ($changes, $old_comment) = $self->SUPER::update(@_);
+
+ if (exists $changes->{'thetext'} || exists $changes->{'isprivate'}) {
+ $self->bug->_sync_fulltext( update_comments => 1);
+ }
+
+ my @old_tags = @{ $old_comment->tags };
+ my @new_tags = @{ $self->tags };
+ my ($removed_tags, $added_tags) = diff_arrays(\@old_tags, \@new_tags);
+
+ if (@$removed_tags || @$added_tags) {
+ my $dbh = Bugzilla->dbh;
+ my $when = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)");
+ my $sth_delete = $dbh->prepare(
+ "DELETE FROM longdescs_tags WHERE comment_id = ? AND tag = ?"
+ );
+ my $sth_insert = $dbh->prepare(
+ "INSERT INTO longdescs_tags(comment_id, tag) VALUES (?, ?)"
+ );
+ my $sth_activity = $dbh->prepare(
+ "INSERT INTO longdescs_tags_activity
+ (bug_id, comment_id, who, bug_when, added, removed)
+ VALUES (?, ?, ?, ?, ?, ?)"
+ );
+
+ foreach my $tag (@$removed_tags) {
+ my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag });
+ if ($weighted) {
+ if ($weighted->weight == 1) {
+ $weighted->remove_from_db();
+ } else {
+ $weighted->set_weight($weighted->weight - 1);
+ $weighted->update();
+ }
+ }
+ trick_taint($tag);
+ $sth_delete->execute($self->id, $tag);
+ $sth_activity->execute(
+ $self->bug_id, $self->id, Bugzilla->user->id, $when, '', $tag);
+ }
+
+ foreach my $tag (@$added_tags) {
+ my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag });
+ if ($weighted) {
+ $weighted->set_weight($weighted->weight + 1);
+ $weighted->update();
+ } else {
+ Bugzilla::Comment::TagWeights->create({ tag => $tag, weight => 1 });
+ }
+ trick_taint($tag);
+ $sth_insert->execute($self->id, $tag);
+ $sth_activity->execute(
+ $self->bug_id, $self->id, Bugzilla->user->id, $when, $tag, '');
+ }
+ }
+
return $changes;
}
-# Speeds up displays of comment lists by loading all ->author objects
-# at once for a whole list.
+# Speeds up displays of comment lists by loading all author objects and tags at
+# once for a whole list.
sub preload {
my ($class, $comments) = @_;
+ # Author
my %user_ids = map { $_->{who} => 1 } @$comments;
my $users = Bugzilla::User->new_from_list([keys %user_ids]);
my %user_map = map { $_->id => $_ } @$users;
foreach my $comment (@$comments) {
$comment->{author} = $user_map{$comment->{who}};
}
+ # Tags
+ my $dbh = Bugzilla->dbh;
+ my @comment_ids = map { $_->id } @$comments;
+ my %comment_map = map { $_->id => $_ } @$comments;
+ my $rows = $dbh->selectall_arrayref(
+ "SELECT comment_id, " . $dbh->sql_group_concat('tag', "','") . "
+ FROM longdescs_tags
+ WHERE " . $dbh->sql_in('comment_id', \@comment_ids) . "
+ GROUP BY comment_id");
+ foreach my $row (@$rows) {
+ $comment_map{$row->[0]}->{tags} = [ split(/,/, $row->[1]) ];
+ }
+ foreach my $comment (@$comments) {
+ $comment->{tags} //= [];
+ }
}
###############################
@@ -126,6 +199,39 @@ sub work_time {
sub type { return $_[0]->{'type'}; }
sub extra_data { return $_[0]->{'extra_data'} }
+sub tags {
+ my ($self) = @_;
+ return [] unless Bugzilla->params->{'comment_taggers_group'};
+ $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref(
+ "SELECT tag
+ FROM longdescs_tags
+ WHERE comment_id = ?
+ ORDER BY tag",
+ undef, $self->id);
+ return $self->{'tags'};
+}
+
+sub collapsed {
+ my ($self) = @_;
+ return 0 unless Bugzilla->params->{'comment_taggers_group'};
+ return $self->{collapsed} if exists $self->{collapsed};
+ $self->{collapsed} = 0;
+ Bugzilla->request_cache->{comment_tags_collapsed}
+ ||= [ split(/\s*,\s*/, Bugzilla->params->{'collapsed_comment_tags'}) ];
+ my @collapsed_tags = @{ Bugzilla->request_cache->{comment_tags_collapsed} };
+ foreach my $my_tag (@{ $self->tags }) {
+ $my_tag = lc($my_tag);
+ foreach my $collapsed_tag (@collapsed_tags) {
+ if ($my_tag eq lc($collapsed_tag)) {
+ $self->{collapsed} = 1;
+ last;
+ }
+ }
+ last if $self->{collapsed};
+ }
+ return $self->{collapsed};
+}
+
sub bug {
my $self = shift;
require Bugzilla::Bug;
@@ -143,14 +249,15 @@ sub is_about_attachment {
sub attachment {
my ($self) = @_;
return undef if not $self->is_about_attachment;
- $self->{attachment} ||= new Bugzilla::Attachment($self->extra_data);
+ $self->{attachment} ||=
+ new Bugzilla::Attachment({ id => $self->extra_data, cache => 1 });
return $self->{attachment};
}
sub author {
my $self = shift;
- $self->{'author'} ||= new Bugzilla::User($self->{'who'});
- return $self->{'author'};
+ return $self->{'author'}
+ ||= new Bugzilla::User({ id => $self->{'who'}, cache => 1 });
}
sub body_full {
@@ -181,6 +288,30 @@ sub set_is_private { $_[0]->set('isprivate', $_[1]); }
sub set_type { $_[0]->set('type', $_[1]); }
sub set_extra_data { $_[0]->set('extra_data', $_[1]); }
+sub add_tag {
+ my ($self, $tag) = @_;
+ $tag = $self->_check_tag($tag);
+
+ my $tags = $self->tags;
+ return if grep { lc($tag) eq lc($_) } @$tags;
+ push @$tags, $tag;
+ $self->{'tags'} = [ sort @$tags ];
+ Bugzilla::Hook::process("comment_after_add_tag",
+ { comment => $self, tag => $tag });
+}
+
+sub remove_tag {
+ my ($self, $tag) = @_;
+ $tag = $self->_check_tag($tag);
+
+ my $tags = $self->tags;
+ my $index = first { lc($tags->[$_]) eq lc($tag) } 0..scalar(@$tags) - 1;
+ return unless defined $index;
+ splice(@$tags, $index, 1);
+ Bugzilla::Hook::process("comment_after_remove_tag",
+ { comment => $self, tag => $tag });
+}
+
##############
# Validators #
##############
@@ -323,6 +454,17 @@ sub _check_isprivate {
return $isprivate ? 1 : 0;
}
+sub _check_tag {
+ my ($invocant, $tag) = @_;
+ length($tag) < MIN_COMMENT_TAG_LENGTH
+ and ThrowUserError('comment_tag_too_short', { tag => $tag });
+ length($tag) > MAX_COMMENT_TAG_LENGTH
+ and ThrowUserError('comment_tag_too_long', { tag => $tag });
+ $tag =~ /^[\w\d\._-]+$/
+ or ThrowUserError('comment_tag_invalid', { tag => $tag });
+ return $tag;
+}
+
sub count {
my ($self) = @_;
@@ -337,7 +479,7 @@ sub count {
undef, $self->bug_id, $self->creation_ts);
return --$self->{'count'};
-}
+}
1;
@@ -383,7 +525,7 @@ C<string> Time spent as related to this comment.
=item C<is_private>
-C<boolean> Comment is marked as private
+C<boolean> Comment is marked as private.
=item C<already_wrapped>
@@ -398,6 +540,54 @@ L<Bugzilla::User> who created the comment.
C<int> The position this comment is located in the full list of comments for a bug starting from 0.
+=item C<collapsed>
+
+C<boolean> Comment should be displayed as collapsed by default.
+
+=item C<tags>
+
+C<array of strings> The tags attached to the comment.
+
+=item C<add_tag>
+
+=over
+
+=item B<Description>
+
+Attaches the specified tag to the comment.
+
+=item B<Params>
+
+=over
+
+=item C<tag>
+
+C<string> The tag to attach.
+
+=back
+
+=back
+
+=item C<remove_tag>
+
+=over
+
+=item B<Description>
+
+Detaches the specified tag from the comment.
+
+=item B<Params>
+
+=over
+
+=item C<tag>
+
+C<string> The tag to detach.
+
+=back
+
+=back
+
=item C<body_full>
=over
diff --git a/Bugzilla/Comment/TagWeights.pm b/Bugzilla/Comment/TagWeights.pm
new file mode 100644
index 000000000..f1a220a47
--- /dev/null
+++ b/Bugzilla/Comment/TagWeights.pm
@@ -0,0 +1,77 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Comment::TagWeights;
+
+use 5.10.1;
+use strict;
+
+use parent qw(Bugzilla::Object);
+
+use Bugzilla::Constants;
+
+# No auditing required
+use constant AUDIT_CREATES => 0;
+use constant AUDIT_UPDATES => 0;
+use constant AUDIT_REMOVES => 0;
+
+use constant DB_COLUMNS => qw(
+ id
+ tag
+ weight
+);
+
+use constant UPDATE_COLUMNS => qw(
+ weight
+);
+
+use constant DB_TABLE => 'longdescs_tags_weights';
+use constant ID_FIELD => 'id';
+use constant NAME_FIELD => 'tag';
+use constant LIST_ORDER => 'weight DESC';
+use constant VALIDATORS => { };
+
+# There's no gain to caching these objects
+use constant USE_MEMCACHED => 0;
+
+sub tag { return $_[0]->{'tag'} }
+sub weight { return $_[0]->{'weight'} }
+
+sub set_weight { $_[0]->set('weight', $_[1]); }
+
+1;
+
+=head1 NAME
+
+Comment::TagWeights - Bugzilla comment weighting class.
+
+=head1 DESCRIPTION
+
+TagWeights.pm represents a Comment::TagWeight object. It is an implementation
+of L<Bugzilla::Object>, and thus provides all methods that L<Bugzilla::Object>
+provides.
+
+TagWeights is used to quickly find tags and order by their usage count.
+
+=head1 PROPERTIES
+
+=over
+
+=item C<tag>
+
+C<getter string> The tag
+
+=item C<weight>
+
+C<getter int> The tag's weight. The value returned corresponds to the number of
+comments with this tag attached.
+
+=item C<set_weight>
+
+C<setter int> Set the tag's weight.
+
+=back
diff --git a/Bugzilla/Component.pm b/Bugzilla/Component.pm
index dc3cc1b9e..0fe6fb25a 100644
--- a/Bugzilla/Component.pm
+++ b/Bugzilla/Component.pm
@@ -352,30 +352,30 @@ sub bug_ids {
sub default_assignee {
my $self = shift;
-
- if (!defined $self->{'default_assignee'}) {
- $self->{'default_assignee'} =
- new Bugzilla::User($self->{'initialowner'});
- }
- return $self->{'default_assignee'};
+ return $self->{'default_assignee'}
+ ||= new Bugzilla::User({ id => $self->{'initialowner'}, cache => 1 });
}
sub default_qa_contact {
my $self = shift;
if (!defined $self->{'default_qa_contact'}) {
- $self->{'default_qa_contact'} =
- new Bugzilla::User($self->{'initialqacontact'});
+ my $params = $self->{'initialqacontact'}
+ ? { id => $self->{'initialqacontact'}, cache => 1 }
+ : $self->{'initialqacontact'};
+ $self->{'default_qa_contact'} = new Bugzilla::User($params);
}
return $self->{'default_qa_contact'};
}
sub flag_types {
- my $self = shift;
+ my ($self, $params) = @_;
+ $params ||= {};
if (!defined $self->{'flag_types'}) {
my $flagtypes = Bugzilla::FlagType::match({ product_id => $self->product_id,
- component_id => $self->id });
+ component_id => $self->id,
+ %$params });
$self->{'flag_types'} = {};
$self->{'flag_types'}->{'bug'} =
diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm
index 990fd8dd2..3e9b793a6 100644
--- a/Bugzilla/Config.pm
+++ b/Bugzilla/Config.pm
@@ -35,7 +35,6 @@ use strict;
use base qw(Exporter);
use Bugzilla::Constants;
use Bugzilla::Hook;
-use Bugzilla::Install::Filesystem qw(fix_file_permissions);
use Data::Dumper;
use File::Temp;
@@ -301,7 +300,10 @@ sub write_params {
rename $tmpname, $param_file
or die "Can't rename $tmpname to $param_file: $!";
- fix_file_permissions($param_file);
+ # It's not common to edit parameters and loading
+ # Bugzilla::Install::Filesystem is slow.
+ require Bugzilla::Install::Filesystem;
+ Bugzilla::Install::Filesystem::fix_file_permissions($param_file);
# And now we have to reset the params cache so that Bugzilla will re-read
# them.
diff --git a/Bugzilla/Config/Admin.pm b/Bugzilla/Config/Admin.pm
index e6141cf9e..769e3170b 100644
--- a/Bugzilla/Config/Admin.pm
+++ b/Bugzilla/Config/Admin.pm
@@ -56,6 +56,13 @@ sub get_param_list {
name => 'allowuserdeletion',
type => 'b',
default => 0
+ },
+
+ {
+ name => 'last_visit_keep_days',
+ type => 't',
+ default => 10,
+ checker => \&check_numeric
});
return @param_list;
}
diff --git a/Bugzilla/Config/Advanced.pm b/Bugzilla/Config/Advanced.pm
index 941cefc4f..4b57df24d 100644
--- a/Bugzilla/Config/Advanced.pm
+++ b/Bugzilla/Config/Advanced.pm
@@ -63,6 +63,49 @@ use constant get_param_list => (
default => 'off',
checker => \&check_multi
},
+
+ {
+ name => 'disable_bug_updates',
+ type => 'b',
+ default => 0
+ },
+
+ {
+ name => 'sentry_uri',
+ type => 't',
+ default => '',
+ },
+
+ {
+ name => 'metrics_enabled',
+ type => 'b',
+ default => 0
+ },
+ {
+ name => 'metrics_user_ids',
+ type => 't',
+ default => '3881,5038,5898,13647,20209,251051,373476,409787'
+ },
+ {
+ name => 'metrics_elasticsearch_server',
+ type => 't',
+ default => '127.0.0.1:9200'
+ },
+ {
+ name => 'metrics_elasticsearch_index',
+ type => 't',
+ default => 'bmo-metrics'
+ },
+ {
+ name => 'metrics_elasticsearch_type',
+ type => 't',
+ default => 'timings'
+ },
+ {
+ name => 'metrics_elasticsearch_ttl',
+ type => 't',
+ default => '1210000000' # 14 days
+ },
);
1;
diff --git a/Bugzilla/Config/Auth.pm b/Bugzilla/Config/Auth.pm
index a61cab5a2..d70c1f81e 100644
--- a/Bugzilla/Config/Auth.pm
+++ b/Bugzilla/Config/Auth.pm
@@ -97,6 +97,12 @@ sub get_param_list {
},
{
+ name => 'webservice_email_filter',
+ type => 'b',
+ default => 0
+ },
+
+ {
name => 'emailregexp',
type => 't',
default => q:^[\\w\\.\\+\\-=]+@[\\w\\.\\-]+\\.[\\w\\-]+$:,
diff --git a/Bugzilla/Config/BugFields.pm b/Bugzilla/Config/BugFields.pm
index d0de9dac6..490d945ea 100644
--- a/Bugzilla/Config/BugFields.pm
+++ b/Bugzilla/Config/BugFields.pm
@@ -113,7 +113,13 @@ sub get_param_list {
choices => ['', @legal_OS],
default => '',
checker => \&check_opsys
- } );
+ },
+
+ {
+ name => 'collapsed_comment_tags',
+ type => 't',
+ default => 'obsolete, spam',
+ });
return @param_list;
}
diff --git a/Bugzilla/Config/Common.pm b/Bugzilla/Config/Common.pm
index 00c699217..edd5872e1 100644
--- a/Bugzilla/Config/Common.pm
+++ b/Bugzilla/Config/Common.pm
@@ -52,6 +52,7 @@ use base qw(Exporter);
check_mail_delivery_method check_notification check_utf8
check_bug_status check_smtp_auth check_theschwartz_available
check_maxattachmentsize check_email
+ check_comment_taggers_group
);
# Checking functions for the various values
@@ -369,6 +370,14 @@ sub check_theschwartz_available {
return "";
}
+sub check_comment_taggers_group {
+ my $group_name = shift;
+ if ($group_name && !Bugzilla->feature('jsonrpc')) {
+ return "Comment tagging requires installation of the JSONRPC feature";
+ }
+ return check_group($group_name);
+}
+
# OK, here are the parameter definitions themselves.
#
# Each definition is a hash with keys:
@@ -467,4 +476,9 @@ Checks that the value is a valid number
Checks that the value is a valid regexp
+=item C<check_comment_taggers_group>
+
+Checks that the required modules for comment tagging are installed, and that a
+valid group is provided.
+
=back
diff --git a/Bugzilla/Config/GroupSecurity.pm b/Bugzilla/Config/GroupSecurity.pm
index 6296583d9..24eddaf6b 100644
--- a/Bugzilla/Config/GroupSecurity.pm
+++ b/Bugzilla/Config/GroupSecurity.pm
@@ -79,7 +79,15 @@ sub get_param_list {
default => 'editbugs',
checker => \&check_group
},
-
+
+ {
+ name => 'comment_taggers_group',
+ type => 's',
+ choices => \&_get_all_group_names,
+ default => 'editbugs',
+ checker => \&check_comment_taggers_group
+ },
+
{
name => 'debug_group',
type => 's',
@@ -107,4 +115,5 @@ sub _get_all_group_names {
unshift(@group_names, '');
return \@group_names;
}
+
1;
diff --git a/Bugzilla/Config/Memcached.pm b/Bugzilla/Config/Memcached.pm
new file mode 100644
index 000000000..08d8ce0e7
--- /dev/null
+++ b/Bugzilla/Config/Memcached.pm
@@ -0,0 +1,32 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Config::Memcached;
+
+use 5.10.1;
+use strict;
+
+use Bugzilla::Config::Common;
+
+our $sortkey = 1550;
+
+sub get_param_list {
+ return (
+ {
+ name => 'memcached_servers',
+ type => 't',
+ default => ''
+ },
+ {
+ name => 'memcached_namespace',
+ type => 't',
+ default => 'bugzilla:',
+ },
+ );
+}
+
+1;
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index 862c18917..1383ca7fe 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -39,12 +39,15 @@ use Memoize;
@Bugzilla::Constants::EXPORT = qw(
BUGZILLA_VERSION
+ REST_DOC
REMOTE_FILE
LOCAL_FILE
bz_locations
+ CONCATENATE_ASSETS
+
IS_NULL
NOT_NULL
@@ -90,6 +93,9 @@ use Memoize;
COMMENT_COLS
MAX_COMMENT_LENGTH
+ MIN_COMMENT_TAG_LENGTH
+ MAX_COMMENT_TAG_LENGTH
+
CMT_NORMAL
CMT_DUPE_OF
CMT_HAS_DUPE
@@ -105,7 +111,7 @@ use Memoize;
POS_EVENTS
EVT_OTHER EVT_ADDED_REMOVED EVT_COMMENT EVT_ATTACHMENT EVT_ATTACHMENT_DATA
EVT_PROJ_MANAGEMENT EVT_OPENED_CLOSED EVT_KEYWORD EVT_CC EVT_DEPEND_BLOCK
- EVT_BUG_CREATED
+ EVT_BUG_CREATED EVT_COMPONENT
NEG_EVENTS
EVT_UNCONFIRMED EVT_CHANGED_BY_ME
@@ -125,10 +131,14 @@ use Memoize;
FIELD_TYPE_MULTI_SELECT
FIELD_TYPE_TEXTAREA
FIELD_TYPE_DATETIME
+ FIELD_TYPE_DATE
FIELD_TYPE_BUG_ID
FIELD_TYPE_BUG_URLS
FIELD_TYPE_KEYWORDS
+ FIELD_TYPE_EXTENSION
+ FIELD_TYPE_HIGHEST_PLUS_ONE
+
EMPTY_DATETIME_REGEX
ABNORMAL_SELECTS
@@ -141,12 +151,14 @@ use Memoize;
USAGE_MODE_EMAIL
USAGE_MODE_JSON
USAGE_MODE_TEST
+ USAGE_MODE_REST
ERROR_MODE_WEBPAGE
ERROR_MODE_DIE
ERROR_MODE_DIE_SOAP_FAULT
ERROR_MODE_JSON_RPC
ERROR_MODE_TEST
+ ERROR_MODE_REST
COLOR_ERROR
COLOR_SUCCESS
@@ -182,6 +194,7 @@ use Memoize;
MAX_FREETEXT_LENGTH
MAX_BUG_URL_LENGTH
MAX_POSSIBLE_DUPLICATES
+ MAX_WEBDOT_BUGS
PASSWORD_DIGEST_ALGORITHM
PASSWORD_SALT_LENGTH
@@ -204,10 +217,19 @@ use Memoize;
# Bugzilla version
use constant BUGZILLA_VERSION => "4.2.11";
+# A base link to the current REST Documentation. We place it here
+# as it will need to be updated to whatever the current release is.
+use constant REST_DOC => "http://www.bugzilla.org/docs/tip/en/html/api/";
+
# Location of the remote and local XML files to track new releases.
use constant REMOTE_FILE => 'http://updates.bugzilla.org/bugzilla-update.xml';
use constant LOCAL_FILE => 'bugzilla-update.xml'; # Relative to datadir.
+# When true CSS and JavaScript assets will be concatanted and minified at
+# run-time, to reduce the number of requests required to render a page.
+# Setting this to a false value can help debugging.
+use constant CONCATENATE_ASSETS => 1;
+
# These are unique values that are unlikely to match a string or a number,
# to be used in criteria for match() functions and other things. They start
# and end with spaces because most Bugzilla stuff has trim() called on it,
@@ -262,7 +284,8 @@ use constant AUTH_NO_SUCH_USER => 5;
use constant AUTH_LOCKOUT => 6;
# The minimum length a password must have.
-use constant USER_PASSWORD_MIN_LENGTH => 6;
+# BMO uses 8 characters.
+use constant USER_PASSWORD_MIN_LENGTH => 8;
use constant LOGIN_OPTIONAL => 0;
use constant LOGIN_NORMAL => 1;
@@ -303,6 +326,10 @@ use constant COMMENT_COLS => 80;
# Used in _check_comment(). Gives the max length allowed for a comment.
use constant MAX_COMMENT_LENGTH => 65535;
+# The minimum and maximum length of comment tags.
+use constant MIN_COMMENT_TAG_LENGTH => 3;
+use constant MAX_COMMENT_TAG_LENGTH => 24;
+
# The type of bug comments.
use constant CMT_NORMAL => 0;
use constant CMT_DUPE_OF => 1;
@@ -355,11 +382,13 @@ use constant EVT_KEYWORD => 7;
use constant EVT_CC => 8;
use constant EVT_DEPEND_BLOCK => 9;
use constant EVT_BUG_CREATED => 10;
+use constant EVT_COMPONENT => 11;
use constant POS_EVENTS => EVT_OTHER, EVT_ADDED_REMOVED, EVT_COMMENT,
EVT_ATTACHMENT, EVT_ATTACHMENT_DATA,
EVT_PROJ_MANAGEMENT, EVT_OPENED_CLOSED, EVT_KEYWORD,
- EVT_CC, EVT_DEPEND_BLOCK, EVT_BUG_CREATED;
+ EVT_CC, EVT_DEPEND_BLOCK, EVT_BUG_CREATED,
+ EVT_COMPONENT;
use constant EVT_UNCONFIRMED => 50;
use constant EVT_CHANGED_BY_ME => 51;
@@ -389,7 +418,8 @@ use constant SENDMAIL_PATH => '/usr/lib:/usr/sbin:/usr/ucblib';
# only storage but also logic. For example, we might add a "user" field type
# whose values are stored in an integer column in the database but for which
# we do more than we would do for a standard integer type (f.e. we might
-# display a user picker).
+# display a user picker). Fields of type FIELD_TYPE_EXTENSION should generally
+# be ignored by the core code and is used primary by extensions.
use constant FIELD_TYPE_UNKNOWN => 0;
use constant FIELD_TYPE_FREETEXT => 1;
@@ -400,6 +430,12 @@ use constant FIELD_TYPE_DATETIME => 5;
use constant FIELD_TYPE_BUG_ID => 6;
use constant FIELD_TYPE_BUG_URLS => 7;
use constant FIELD_TYPE_KEYWORDS => 8;
+use constant FIELD_TYPE_DATE => 9;
+use constant FIELD_TYPE_EXTENSION => 99;
+
+# Add new field types above this line, and change the below value in the
+# obvious fashion
+use constant FIELD_TYPE_HIGHEST_PLUS_ONE => 100;
use constant EMPTY_DATETIME_REGEX => qr/^[0\-:\sA-Za-z]+$/;
@@ -431,8 +467,8 @@ use constant MAX_LOGIN_ATTEMPTS => 5;
use constant LOGIN_LOCKOUT_INTERVAL => 30;
# The maximum number of seconds the Strict-Transport-Security header
-# will remain valid. Default is one week.
-use constant MAX_STS_AGE => 604800;
+# will remain valid. BMO uses one year.
+use constant MAX_STS_AGE => 31536000;
# Protocols which are considered as safe.
use constant SAFE_PROTOCOLS => ('afs', 'cid', 'ftp', 'gopher', 'http', 'https',
@@ -445,15 +481,16 @@ use constant LEGAL_CONTENT_TYPES => ('application', 'audio', 'image', 'message',
use constant contenttypes =>
{
- "html"=> "text/html" ,
- "rdf" => "application/rdf+xml" ,
- "atom"=> "application/atom+xml" ,
- "xml" => "application/xml" ,
- "js" => "application/x-javascript" ,
- "json"=> "application/json" ,
- "csv" => "text/csv" ,
- "png" => "image/png" ,
- "ics" => "text/calendar" ,
+ "html" => "text/html" ,
+ "rdf" => "application/rdf+xml" ,
+ "atom" => "application/atom+xml" ,
+ "xml" => "application/xml" ,
+ "dtd" => "application/xml-dtd" ,
+ "js" => "application/x-javascript" ,
+ "json" => "application/json" ,
+ "csv" => "text/csv" ,
+ "png" => "image/png" ,
+ "ics" => "text/calendar" ,
};
# Usage modes. Default USAGE_MODE_BROWSER. Use with Bugzilla->usage_mode.
@@ -463,6 +500,7 @@ use constant USAGE_MODE_XMLRPC => 2;
use constant USAGE_MODE_EMAIL => 3;
use constant USAGE_MODE_JSON => 4;
use constant USAGE_MODE_TEST => 5;
+use constant USAGE_MODE_REST => 6;
# Error modes. Default set by Bugzilla->usage_mode (so ERROR_MODE_WEBPAGE
# usually). Use with Bugzilla->error_mode.
@@ -471,6 +509,7 @@ use constant ERROR_MODE_DIE => 1;
use constant ERROR_MODE_DIE_SOAP_FAULT => 2;
use constant ERROR_MODE_JSON_RPC => 3;
use constant ERROR_MODE_TEST => 4;
+use constant ERROR_MODE_REST => 5;
# The ANSI colors of messages that command-line scripts use
use constant COLOR_ERROR => 'red';
@@ -562,6 +601,9 @@ use constant MAX_BUG_URL_LENGTH => 255;
# will return.
use constant MAX_POSSIBLE_DUPLICATES => 25;
+# Maximum number of bugs to display in a dependency graph
+use constant MAX_WEBDOT_BUGS => 2000;
+
# This is the name of the algorithm used to hash passwords before storing
# them in the database. This can be any string that is valid to pass to
# Perl's "Digest" module. Note that if you change this, it won't take
@@ -635,7 +677,7 @@ sub _bz_locations {
# make sure this still points to the CGIs.
'cgi_path' => $libpath,
'templatedir' => "$libpath/template",
- 'template_cache' => "$datadir/template",
+ 'template_cache' => "$libpath/template_cache",
'project' => $project,
'localconfig' => "$libpath/$localconfig",
'datadir' => $datadir,
@@ -650,6 +692,7 @@ sub _bz_locations {
# The script should really generate these graphs directly...
'webdotdir' => "$datadir/webdot",
'extensionsdir' => "$libpath/extensions",
+ 'assetsdir' => "$datadir/assets",
};
}
diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm
index 1f9c31518..183f619a5 100644
--- a/Bugzilla/DB.pm
+++ b/Bugzilla/DB.pm
@@ -43,6 +43,8 @@ use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::DB::Schema;
+use Bugzilla::Metrics::Mysql;
+
use List::Util qw(max);
use Storable qw(dclone);
@@ -148,6 +150,12 @@ sub _connect {
. " localconfig: " . $@);
# instantiate the correct DB specific module
+
+ # BMO - enable instrumentation of db calls
+ if (Bugzilla->metrics_enabled) {
+ $pkg_module = 'Bugzilla::Metrics::Mysql';
+ }
+
my $dbh = $pkg_module->new($params);
return $dbh;
@@ -159,7 +167,7 @@ sub _handle_error {
# Cut down the error string to a reasonable size
$_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000)
if length($_[0]) > 4000;
- $_[0] = Carp::longmess($_[0]);
+ # BMO: stracktrace disabled: $_[0] = Carp::longmess($_[0]);
return 0; # Now let DBI handle raising the error
}
@@ -1383,14 +1391,19 @@ sub _bz_real_schema {
my ($self) = @_;
return $self->{private_real_schema} if exists $self->{private_real_schema};
- my ($data, $version) = $self->selectrow_array(
- "SELECT schema_data, version FROM bz_schema");
+ my $bz_schema;
+ unless ($bz_schema = Bugzilla->memcached->get({ key => 'bz_schema' })) {
+ $bz_schema = $self->selectrow_arrayref(
+ "SELECT schema_data, version FROM bz_schema"
+ );
+ Bugzilla->memcached->set({ key => 'bz_schema', value => $bz_schema });
+ }
(die "_bz_real_schema tried to read the bz_schema table but it's empty!")
- if !$data;
+ if !$bz_schema;
- $self->{private_real_schema} =
- $self->_bz_schema->deserialize_abstract($data, $version);
+ $self->{private_real_schema} =
+ $self->_bz_schema->deserialize_abstract($bz_schema->[0], $bz_schema->[1]);
return $self->{private_real_schema};
}
@@ -1432,6 +1445,8 @@ sub _bz_store_real_schema {
$sth->bind_param(1, $store_me, $self->BLOB_TYPE);
$sth->bind_param(2, $schema_version);
$sth->execute();
+
+ Bugzilla->memcached->clear({ key => 'bz_schema' });
}
# For bz_populate_enum_tables
diff --git a/Bugzilla/DB/Mysql.pm b/Bugzilla/DB/Mysql.pm
index 9ddb46622..c430725ef 100644
--- a/Bugzilla/DB/Mysql.pm
+++ b/Bugzilla/DB/Mysql.pm
@@ -181,7 +181,7 @@ sub sql_fulltext_search {
$mode = 'IN BOOLEAN MODE';
# quote un-quoted compound words
- my @words = quotewords('[\s()]+', 'delimiters', $text);
+ my @words = grep { defined } quotewords('[\s()]+', 'delimiters', $text);
foreach my $word (@words) {
# match words that have non-word chars in the middle of them
if ($word =~ /\w\W+\w/ && $word !~ m/"/) {
diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm
index 1e598c61e..be392c105 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -342,6 +342,8 @@ use constant ABSTRACT_SCHEMA => {
bugs_activity => {
FIELDS => [
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1,
+ PRIMARYKEY => 1},
bug_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'bugs',
COLUMN => 'bug_id',
@@ -358,8 +360,8 @@ use constant ABSTRACT_SCHEMA => {
REFERENCES => {TABLE => 'fielddefs',
COLUMN => 'id'}},
added => {TYPE => 'varchar(255)'},
- removed => {TYPE => 'TINYTEXT'},
- comment_id => {TYPE => 'INT3',
+ removed => {TYPE => 'varchar(255)'},
+ comment_id => {TYPE => 'INT4',
REFERENCES => { TABLE => 'longdescs',
COLUMN => 'comment_id',
DELETE => 'CASCADE'}},
@@ -370,6 +372,7 @@ use constant ABSTRACT_SCHEMA => {
bugs_activity_bug_when_idx => ['bug_when'],
bugs_activity_fieldid_idx => ['fieldid'],
bugs_activity_added_idx => ['added'],
+ bugs_activity_removed_idx => ['removed'],
],
},
@@ -393,7 +396,7 @@ use constant ABSTRACT_SCHEMA => {
longdescs => {
FIELDS => [
- comment_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
+ comment_id => {TYPE => 'INTSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
bug_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'bugs',
@@ -421,6 +424,54 @@ use constant ABSTRACT_SCHEMA => {
],
},
+ longdescs_tags => {
+ FIELDS => [
+ id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 },
+ comment_id => { TYPE => 'INT4',
+ REFERENCES => { TABLE => 'longdescs',
+ COLUMN => 'comment_id',
+ DELETE => 'CASCADE' }},
+ tag => { TYPE => 'varchar(24)', NOTNULL => 1 },
+ ],
+ INDEXES => [
+ longdescs_tags_idx => { FIELDS => ['comment_id', 'tag'], TYPE => 'UNIQUE' },
+ ],
+ },
+
+ longdescs_tags_weights => {
+ FIELDS => [
+ id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 },
+ tag => { TYPE => 'varchar(24)', NOTNULL => 1 },
+ weight => { TYPE => 'INT3', NOTNULL => 1 },
+ ],
+ INDEXES => [
+ longdescs_tags_weights_tag_idx => { FIELDS => ['tag'], TYPE => 'UNIQUE' },
+ ],
+ },
+
+ longdescs_tags_activity => {
+ FIELDS => [
+ id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 },
+ bug_id => { TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => { TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE' }},
+ comment_id => { TYPE => 'INT4',
+ REFERENCES => { TABLE => 'longdescs',
+ COLUMN => 'comment_id',
+ DELETE => 'CASCADE' }},
+ who => { TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => { TABLE => 'profiles',
+ COLUMN => 'userid' }},
+ bug_when => { TYPE => 'DATETIME', NOTNULL => 1 },
+ added => { TYPE => 'varchar(24)' },
+ removed => { TYPE => 'varchar(24)' },
+ ],
+ INDEXES => [
+ longdescs_tags_activity_bug_id_idx => ['bug_id'],
+ ],
+ },
+
dependencies => {
FIELDS => [
blocked => {TYPE => 'INT3', NOTNULL => 1,
@@ -433,7 +484,8 @@ use constant ABSTRACT_SCHEMA => {
DELETE => 'CASCADE'}},
],
INDEXES => [
- dependencies_blocked_idx => ['blocked'],
+ dependencies_blocked_idx => {FIELDS => [qw(blocked dependson)],
+ TYPE => 'UNIQUE'},
dependencies_dependson_idx => ['dependson'],
],
},
@@ -466,6 +518,7 @@ use constant ABSTRACT_SCHEMA => {
attachments_creation_ts_idx => ['creation_ts'],
attachments_modification_time_idx => ['modification_time'],
attachments_submitter_id_idx => ['submitter_id', 'bug_id'],
+ attachments_ispatch_idx => ['ispatch'],
],
},
attach_data => {
@@ -526,6 +579,9 @@ use constant ABSTRACT_SCHEMA => {
added => {TYPE => 'MEDIUMTEXT'},
at_time => {TYPE => 'DATETIME', NOTNULL => 1},
],
+ INDEXES => [
+ audit_log_class_idx => ['class', 'at_time'],
+ ],
},
# Keywords
@@ -651,8 +707,8 @@ use constant ABSTRACT_SCHEMA => {
DELETE => 'CASCADE'}},
],
INDEXES => [
- flaginclusions_type_id_idx =>
- [qw(type_id product_id component_id)],
+ flaginclusions_type_id_idx => { FIELDS => [qw(type_id product_id component_id)],
+ TYPE => 'UNIQUE' },
],
},
@@ -672,8 +728,8 @@ use constant ABSTRACT_SCHEMA => {
DELETE => 'CASCADE'}},
],
INDEXES => [
- flagexclusions_type_id_idx =>
- [qw(type_id product_id component_id)],
+ flagexclusions_type_id_idx => { FIELDS => [qw(type_id product_id component_id)],
+ TYPE => 'UNIQUE' },
],
},
@@ -889,6 +945,7 @@ use constant ABSTRACT_SCHEMA => {
extern_id => {TYPE => 'varchar(64)'},
is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'TRUE'},
+ last_seen_date => {TYPE => 'DATETIME'},
],
INDEXES => [
profiles_login_name_idx => {FIELDS => ['login_name'],
@@ -915,6 +972,8 @@ use constant ABSTRACT_SCHEMA => {
profiles_activity => {
FIELDS => [
+ id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
+ PRIMARYKEY => 1},
userid => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'profiles',
COLUMN => 'userid',
@@ -952,6 +1011,23 @@ use constant ABSTRACT_SCHEMA => {
],
},
+ email_bug_ignore => {
+ FIELDS => [
+ user_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
+ bug_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE'}},
+ ],
+ INDEXES => [
+ email_bug_ignore_user_id_idx => {FIELDS => [qw(user_id bug_id)],
+ TYPE => 'UNIQUE'},
+ ],
+ },
+
watch => {
FIELDS => [
watcher => {TYPE => 'INT3', NOTNULL => 1,
@@ -1638,6 +1714,26 @@ use constant ABSTRACT_SCHEMA => {
],
},
+ bug_user_last_visit => {
+ FIELDS => [
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1,
+ PRIMARYKEY => 1},
+ user_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
+ bug_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE'}},
+ last_visit_ts => {TYPE => 'DATETIME', NOTNULL => 1},
+ ],
+ INDEXES => [
+ bug_user_last_visit_idx => {FIELDS => ['user_id', 'bug_id'],
+ TYPE => 'UNIQUE'},
+ bug_user_last_visit_last_visit_ts_idx => ['last_visit_ts'],
+ ],
+ },
};
# Foreign Keys are added in Bugzilla::DB::bz_add_field_tables
diff --git a/Bugzilla/DB/Schema/Mysql.pm b/Bugzilla/DB/Schema/Mysql.pm
index 8c9ea2dda..5fc50a986 100644
--- a/Bugzilla/DB/Schema/Mysql.pm
+++ b/Bugzilla/DB/Schema/Mysql.pm
@@ -120,7 +120,7 @@ sub _initialize {
LONGBLOB => 'longblob',
DATETIME => 'datetime',
-
+ DATE => 'date',
};
$self->_adjust_schema;
diff --git a/Bugzilla/DB/Schema/Oracle.pm b/Bugzilla/DB/Schema/Oracle.pm
index a61b1e323..37b406671 100644
--- a/Bugzilla/DB/Schema/Oracle.pm
+++ b/Bugzilla/DB/Schema/Oracle.pm
@@ -70,7 +70,7 @@ sub _initialize {
LONGBLOB => 'blob',
DATETIME => 'date',
-
+ DATE => 'date',
};
$self->_adjust_schema;
diff --git a/Bugzilla/DB/Schema/Pg.pm b/Bugzilla/DB/Schema/Pg.pm
index d21f5099c..662120c03 100644
--- a/Bugzilla/DB/Schema/Pg.pm
+++ b/Bugzilla/DB/Schema/Pg.pm
@@ -80,7 +80,7 @@ sub _initialize {
LONGBLOB => 'bytea',
DATETIME => 'timestamp(0) without time zone',
-
+ DATE => 'date',
};
$self->_adjust_schema;
diff --git a/Bugzilla/DB/Schema/Sqlite.pm b/Bugzilla/DB/Schema/Sqlite.pm
index aad1f17bc..7ed9def3e 100644
--- a/Bugzilla/DB/Schema/Sqlite.pm
+++ b/Bugzilla/DB/Schema/Sqlite.pm
@@ -57,6 +57,7 @@ sub _initialize {
LONGBLOB => 'blob',
DATETIME => 'DATETIME',
+ DATE => 'DATETIME',
};
$self->_adjust_schema;
diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm
index 178f6f90c..6e3309778 100644
--- a/Bugzilla/Error.pm
+++ b/Bugzilla/Error.pm
@@ -26,8 +26,9 @@ package Bugzilla::Error;
use strict;
use base qw(Exporter);
-@Bugzilla::Error::EXPORT = qw(ThrowCodeError ThrowTemplateError ThrowUserError);
+@Bugzilla::Error::EXPORT = qw(ThrowCodeError ThrowTemplateError ThrowUserError ThrowErrorPage);
+use Bugzilla::Sentry;
use Bugzilla::Constants;
use Bugzilla::WebService::Constants;
use Bugzilla::Util;
@@ -93,6 +94,7 @@ sub _throw_error {
my $template = Bugzilla->template;
my $message;
+
# There are some tests that throw and catch a lot of errors,
# and calling $template->process over and over for those errors
# is too slow. So instead, we just "die" with a dump of the arguments.
@@ -108,8 +110,22 @@ sub _throw_error {
message => \$message });
if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) {
- print Bugzilla->cgi->header();
- print $message;
+ if (sentry_should_notify($vars->{error})) {
+ $vars->{maintainers_notified} = 1;
+ $vars->{processed} = {};
+ } else {
+ $vars->{maintainers_notified} = 0;
+ }
+
+ my $cgi = Bugzilla->cgi;
+ $cgi->close_standby_message('text/html', 'inline');
+ $template->process($name, $vars)
+ || ThrowTemplateError($template->error());
+ print $cgi->multipart_final() if $cgi->{_multipart_in_progress};
+
+ if ($vars->{maintainers_notified}) {
+ sentry_handle_error($vars->{error}, $vars->{processed}->{error_message});
+ }
}
elsif (Bugzilla->error_mode == ERROR_MODE_TEST) {
die Dumper($vars);
@@ -118,7 +134,8 @@ sub _throw_error {
die("$message\n");
}
elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT
- || Bugzilla->error_mode == ERROR_MODE_JSON_RPC)
+ || Bugzilla->error_mode == ERROR_MODE_JSON_RPC
+ || Bugzilla->error_mode == ERROR_MODE_REST)
{
# Clone the hash so we aren't modifying the constant.
my %error_map = %{ WS_ERROR_CODE() };
@@ -135,13 +152,20 @@ sub _throw_error {
}
else {
my $server = Bugzilla->_json_server;
+
+ my $status_code = 0;
+ if (Bugzilla->error_mode == ERROR_MODE_REST) {
+ my %status_code_map = %{ REST_STATUS_CODE_MAP() };
+ $status_code = $status_code_map{$code} || $status_code_map{'_default'};
+ }
# Technically JSON-RPC isn't allowed to have error numbers
# higher than 999, but we do this to avoid conflicts with
# the internal JSON::RPC error codes.
- $server->raise_error(code => 100000 + $code,
- message => $message,
- id => $server->{_bz_request_id},
- version => $server->version);
+ $server->raise_error(code => 100000 + $code,
+ status_code => $status_code,
+ message => $message,
+ id => $server->{_bz_request_id},
+ version => $server->version);
# Most JSON-RPC Throw*Error calls happen within an eval inside
# of JSON::RPC. So, in that circumstance, instead of exiting,
# we die with no message. JSON::RPC checks raise_error before
@@ -183,40 +207,83 @@ sub ThrowTemplateError {
die("error: template error: $template_err");
}
+ # mod_perl overrides exit to call die with this string
+ # we never want to display this to the user
+ exit if $template_err =~ /\bModPerl::Util::exit\b/;
+
$vars->{'template_error_msg'} = $template_err;
$vars->{'error'} = "template_error";
+ sentry_handle_error('error', $template_err);
+ $vars->{'template_error_msg'} =~ s/ at \S+ line \d+\.\s*$//;
+
my $template = Bugzilla->template;
# Try a template first; but if this one fails too, fall back
# on plain old print statements.
if (!$template->process("global/code-error.html.tmpl", $vars)) {
- my $maintainer = Bugzilla->params->{'maintainer'};
+ my $maintainer = html_quote(Bugzilla->params->{'maintainer'});
my $error = html_quote($vars->{'template_error_msg'});
my $error2 = html_quote($template->error());
print <<END;
<tt>
<p>
- Bugzilla has suffered an internal error. Please save this page and
- send it to $maintainer with details of what you were doing at the
- time this message appeared.
+ Bugzilla has suffered an internal error:
+ </p>
+ <p>
+ $error
+ </p>
+ <!-- template error, no real need to show this to the user
+ $error2
+ -->
+ <p>
+ The <a href="mailto:$maintainer">Bugzilla maintainers</a> have
+ been notified of this error.
</p>
- <script type="text/javascript"> <!--
- document.write("<p>URL: " +
- document.location.href.replace(/&/g,"&amp;")
- .replace(/</g,"&lt;")
- .replace(/>/g,"&gt;") + "</p>");
- // -->
- </script>
- <p>Template->process() failed twice.<br>
- First error: $error<br>
- Second error: $error2</p>
</tt>
END
}
exit;
}
+sub ThrowErrorPage {
+ # BMO customisation for bug 659231
+ my ($template_name, $message) = @_;
+
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction();
+
+ if (Bugzilla->error_mode == ERROR_MODE_DIE) {
+ die("error: $message");
+ }
+
+ if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT
+ || Bugzilla->error_mode == ERROR_MODE_JSON_RPC)
+ {
+ my $code = ERROR_UNKNOWN_TRANSIENT;
+ if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
+ die SOAP::Fault->faultcode($code)->faultstring($message);
+ } else {
+ my $server = Bugzilla->_json_server;
+ $server->raise_error(code => 100000 + $code,
+ message => $message,
+ id => $server->{_bz_request_id},
+ version => $server->version);
+ die if _in_eval();
+ $server->response($server->error_response_header);
+ }
+ } else {
+ my $cgi = Bugzilla->cgi;
+ my $template = Bugzilla->template;
+ my $vars = {};
+ $vars->{message} = $message;
+ print $cgi->header();
+ $template->process($template_name, $vars)
+ || ThrowTemplateError($template->error());
+ exit;
+ }
+}
+
1;
__END__
diff --git a/Bugzilla/Field.pm b/Bugzilla/Field.pm
index 81677c7ea..d5f8e33f9 100644
--- a/Bugzilla/Field.pm
+++ b/Bugzilla/Field.pm
@@ -78,6 +78,8 @@ use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Util;
use List::MoreUtils qw(any);
+use Bugzilla::Config qw(SetParam write_params);
+use Bugzilla::Hook;
use Scalar::Util qw(blessed);
@@ -85,6 +87,8 @@ use Scalar::Util qw(blessed);
#### Initialization ####
###############################
+use constant IS_CONFIG => 1;
+
use constant DB_TABLE => 'fielddefs';
use constant LIST_ORDER => 'sortkey, name';
@@ -159,6 +163,7 @@ use constant SQL_DEFINITIONS => {
FIELD_TYPE_TEXTAREA, { TYPE => 'MEDIUMTEXT',
NOTNULL => 1, DEFAULT => "''"},
FIELD_TYPE_DATETIME, { TYPE => 'DATETIME' },
+ FIELD_TYPE_DATE, { TYPE => 'DATE' },
FIELD_TYPE_BUG_ID, { TYPE => 'INT3' },
};
@@ -176,6 +181,9 @@ use constant DEFAULT_FIELDS => (
{name => 'product', desc => 'Product', in_new_bugmail => 1,
is_mandatory => 1,
type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
+ {name => 'component', desc => 'Component', in_new_bugmail => 1,
+ is_mandatory => 1,
+ type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
{name => 'version', desc => 'Version', in_new_bugmail => 1,
is_mandatory => 1, buglist => 1},
{name => 'rep_platform', desc => 'Platform', in_new_bugmail => 1,
@@ -196,9 +204,6 @@ use constant DEFAULT_FIELDS => (
type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
{name => 'priority', desc => 'Priority', in_new_bugmail => 1,
type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
- {name => 'component', desc => 'Component', in_new_bugmail => 1,
- is_mandatory => 1,
- type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
{name => 'assigned_to', desc => 'AssignedTo', in_new_bugmail => 1,
buglist => 1},
{name => 'reporter', desc => 'ReportedBy', in_new_bugmail => 1,
@@ -207,9 +212,11 @@ use constant DEFAULT_FIELDS => (
buglist => 1},
{name => 'cc', desc => 'CC', in_new_bugmail => 1},
{name => 'dependson', desc => 'Depends on', in_new_bugmail => 1,
- is_numeric => 1},
+ is_numeric => 1, buglist => 1},
{name => 'blocked', desc => 'Blocks', in_new_bugmail => 1,
- is_numeric => 1},
+ is_numeric => 1, buglist => 1},
+
+ {name => 'assignee_last_login', desc => 'Assignee Last Login Date', buglist => 1},
{name => 'attachments.description', desc => 'Attachment description'},
{name => 'attachments.filename', desc => 'Attachment filename'},
@@ -261,6 +268,9 @@ use constant DEFAULT_FIELDS => (
{name => 'see_also', desc => "See Also",
type => FIELD_TYPE_BUG_URLS},
{name => 'tag', desc => 'Tags'},
+ {name => 'last_visit_ts', desc => 'Last Visit', buglist => 1,
+ type => FIELD_TYPE_DATETIME},
+ {name => 'comment_tag', desc => 'Comment Tag'},
);
################
@@ -347,9 +357,7 @@ sub _check_sortkey {
sub _check_type {
my ($invocant, $type, undef, $params) = @_;
my $saved_type = $type;
- # The constant here should be updated every time a new,
- # higher field type is added.
- (detaint_natural($type) && $type <= FIELD_TYPE_KEYWORDS)
+ (detaint_natural($type) && $type < FIELD_TYPE_HIGHEST_PLUS_ONE)
|| ThrowCodeError('invalid_customfield_type', { type => $saved_type });
my $custom = blessed($invocant) ? $invocant->custom : $params->{custom};
@@ -918,53 +926,67 @@ sub remove_from_db {
ThrowUserError('customfield_not_obsolete', {'name' => $self->name });
}
- $dbh->bz_start_transaction();
+ # BMO: disable bug updates during field creation
+ # using an eval as try/finally
+ eval {
+ SetParam('disable_bug_updates', 1);
+ write_params();
- # Check to see if bug activity table has records (should be fast with index)
- my $has_activity = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs_activity
- WHERE fieldid = ?", undef, $self->id);
- if ($has_activity) {
- ThrowUserError('customfield_has_activity', {'name' => $name });
- }
+ $dbh->bz_start_transaction();
- # Check to see if bugs table has records (slow)
- my $bugs_query = "";
+ # Check to see if bug activity table has records (should be fast with index)
+ my $has_activity = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs_activity
+ WHERE fieldid = ?", undef, $self->id);
+ if ($has_activity) {
+ ThrowUserError('customfield_has_activity', {'name' => $name });
+ }
- if ($self->type == FIELD_TYPE_MULTI_SELECT) {
- $bugs_query = "SELECT COUNT(*) FROM bug_$name";
- }
- else {
- $bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL";
- if ($self->type != FIELD_TYPE_BUG_ID && $self->type != FIELD_TYPE_DATETIME) {
- $bugs_query .= " AND $name != ''";
+ # Check to see if bugs table has records (slow)
+ my $bugs_query = "";
+
+ if ($self->type == FIELD_TYPE_MULTI_SELECT) {
+ $bugs_query = "SELECT COUNT(*) FROM bug_$name";
}
- # Ignore the default single select value
- if ($self->type == FIELD_TYPE_SINGLE_SELECT) {
- $bugs_query .= " AND $name != '---'";
+ else {
+ $bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL";
+ if ($self->type != FIELD_TYPE_BUG_ID
+ && $self->type != FIELD_TYPE_DATE
+ && $self->type != FIELD_TYPE_DATETIME)
+ {
+ $bugs_query .= " AND $name != ''";
+ }
+ # Ignore the default single select value
+ if ($self->type == FIELD_TYPE_SINGLE_SELECT) {
+ $bugs_query .= " AND $name != '---'";
+ }
}
- }
- my $has_bugs = $dbh->selectrow_array($bugs_query);
- if ($has_bugs) {
- ThrowUserError('customfield_has_contents', {'name' => $name });
- }
+ my $has_bugs = $dbh->selectrow_array($bugs_query);
+ if ($has_bugs) {
+ ThrowUserError('customfield_has_contents', {'name' => $name });
+ }
- # Once we reach here, we should be OK to delete.
- $dbh->do('DELETE FROM fielddefs WHERE id = ?', undef, $self->id);
+ # Once we reach here, we should be OK to delete.
+ $dbh->do('DELETE FROM fielddefs WHERE id = ?', undef, $self->id);
- my $type = $self->type;
+ my $type = $self->type;
- # the values for multi-select are stored in a seperate table
- if ($type != FIELD_TYPE_MULTI_SELECT) {
- $dbh->bz_drop_column('bugs', $name);
- }
+ # the values for multi-select are stored in a seperate table
+ if ($type != FIELD_TYPE_MULTI_SELECT) {
+ $dbh->bz_drop_column('bugs', $name);
+ }
- if ($self->is_select) {
- # Delete the table that holds the legal values for this field.
- $dbh->bz_drop_field_tables($self);
- }
+ if ($self->is_select) {
+ # Delete the table that holds the legal values for this field.
+ $dbh->bz_drop_field_tables($self);
+ }
- $dbh->bz_commit_transaction()
+ $dbh->bz_commit_transaction();
+ };
+ my $error = "$@";
+ SetParam('disable_bug_updates', 0);
+ write_params();
+ die $error if $error;
}
=pod
@@ -1012,48 +1034,71 @@ sub create {
my ($params) = @_;
my $dbh = Bugzilla->dbh;
- # This makes sure the "sortkey" validator runs, even if
- # the parameter isn't sent to create().
- $params->{sortkey} = undef if !exists $params->{sortkey};
- $params->{type} ||= 0;
- # We mark the custom field as obsolete till it has been fully created,
- # to avoid race conditions when viewing bugs at the same time.
- my $is_obsolete = $params->{obsolete};
- $params->{obsolete} = 1 if $params->{custom};
-
- $dbh->bz_start_transaction();
- $class->check_required_create_fields(@_);
- my $field_values = $class->run_create_validators($params);
- my $visibility_values = delete $field_values->{visibility_values};
- my $field = $class->insert_create_data($field_values);
-
- $field->set_visibility_values($visibility_values);
- $field->_update_visibility_values();
+ # BMO: disable bug updates during field creation
+ # using an eval as try/finally
+ my $field;
+ eval {
+ if ($params->{'custom'}) {
+ SetParam('disable_bug_updates', 1);
+ write_params();
+ }
- $dbh->bz_commit_transaction();
+ # This makes sure the "sortkey" validator runs, even if
+ # the parameter isn't sent to create().
+ $params->{sortkey} = undef if !exists $params->{sortkey};
+ $params->{type} ||= 0;
+ # We mark the custom field as obsolete till it has been fully created,
+ # to avoid race conditions when viewing bugs at the same time.
+ my $is_obsolete = $params->{obsolete};
+ $params->{obsolete} = 1 if $params->{custom};
+
+ $dbh->bz_start_transaction();
+ $class->check_required_create_fields(@_);
+ my $field_values = $class->run_create_validators($params);
+ my $visibility_values = delete $field_values->{visibility_values};
+ $field = $class->insert_create_data($field_values);
+
+ $field->set_visibility_values($visibility_values);
+ $field->_update_visibility_values();
+
+ $dbh->bz_commit_transaction();
+ Bugzilla->memcached->clear_config();
+
+ if ($field->custom) {
+ my $name = $field->name;
+ my $type = $field->type;
+ if (SQL_DEFINITIONS->{$type}) {
+ # Create the database column that stores the data for this field.
+ $dbh->bz_add_column('bugs', $name, SQL_DEFINITIONS->{$type});
+ }
- if ($field->custom) {
- my $name = $field->name;
- my $type = $field->type;
- if (SQL_DEFINITIONS->{$type}) {
- # Create the database column that stores the data for this field.
- $dbh->bz_add_column('bugs', $name, SQL_DEFINITIONS->{$type});
- }
+ if ($field->is_select) {
+ # Create the table that holds the legal values for this field.
+ $dbh->bz_add_field_tables($field);
+ }
- if ($field->is_select) {
- # Create the table that holds the legal values for this field.
- $dbh->bz_add_field_tables($field);
- }
+ if ($type == FIELD_TYPE_SINGLE_SELECT) {
+ # Insert a default value of "---" into the legal values table.
+ $dbh->do("INSERT INTO $name (value) VALUES ('---')");
+ }
- if ($type == FIELD_TYPE_SINGLE_SELECT) {
- # Insert a default value of "---" into the legal values table.
- $dbh->do("INSERT INTO $name (value) VALUES ('---')");
+ # Restore the original obsolete state of the custom field.
+ $dbh->do('UPDATE fielddefs SET obsolete = 0 WHERE id = ?', undef, $field->id)
+ unless $is_obsolete;
+
+ Bugzilla->memcached->clear({ table => 'fielddefs', id => $field->id });
+ Bugzilla->memcached->clear_config();
}
+ };
- # Restore the original obsolete state of the custom field.
- $dbh->do('UPDATE fielddefs SET obsolete = 0 WHERE id = ?', undef, $field->id)
- unless $is_obsolete;
+ my $error = "$@";
+ if ($params->{'custom'}) {
+ SetParam('disable_bug_updates', 0);
+ write_params();
}
+ die $error if $error;
+
+ Bugzilla::Hook::process("field_end_of_create", { field => $field });
return $field;
}
@@ -1066,6 +1111,7 @@ sub update {
$dbh->do("UPDATE " . $self->name . " SET visibility_value_id = NULL");
}
$self->_update_visibility_values();
+ Bugzilla->memcached->clear_config();
return $changes;
}
diff --git a/Bugzilla/Field/Choice.pm b/Bugzilla/Field/Choice.pm
index 773dbd4ce..8d7d61def 100644
--- a/Bugzilla/Field/Choice.pm
+++ b/Bugzilla/Field/Choice.pm
@@ -37,6 +37,8 @@ use Scalar::Util qw(blessed);
# Initialization #
##################
+use constant IS_CONFIG => 1;
+
use constant DB_COLUMNS => qw(
id
value
diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm
index b687532c0..e87f1f674 100644
--- a/Bugzilla/Flag.pm
+++ b/Bugzilla/Flag.pm
@@ -81,15 +81,21 @@ use constant AUDIT_REMOVES => 0;
use constant SKIP_REQUESTEE_ON_ERROR => 1;
-use constant DB_COLUMNS => qw(
- id
- type_id
- bug_id
- attach_id
- requestee_id
- setter_id
- status
-);
+sub DB_COLUMNS {
+ my $dbh = Bugzilla->dbh;
+ return qw(
+ id
+ type_id
+ bug_id
+ attach_id
+ requestee_id
+ setter_id
+ status),
+ $dbh->sql_date_format('creation_date', '%Y.%m.%d %H:%i:%s') .
+ ' AS creation_date',
+ $dbh->sql_date_format('modification_date', '%Y.%m.%d %H:%i:%s') .
+ ' AS modification_date';
+}
use constant UPDATE_COLUMNS => qw(
requestee_id
@@ -134,6 +140,14 @@ Returns the ID of the attachment this flag belongs to, if any.
Returns the status '+', '-', '?' of the flag.
+=item C<creation_date>
+
+Returns the timestamp when the flag was created.
+
+=item C<modification_date>
+
+Returns the timestamp when the flag was last modified.
+
=back
=cut
@@ -146,6 +160,8 @@ sub attach_id { return $_[0]->{'attach_id'}; }
sub status { return $_[0]->{'status'}; }
sub setter_id { return $_[0]->{'setter_id'}; }
sub requestee_id { return $_[0]->{'requestee_id'}; }
+sub creation_date { return $_[0]->{'creation_date'}; }
+sub modification_date { return $_[0]->{'modification_date'}; }
###############################
#### Methods ####
@@ -180,22 +196,23 @@ is an attachment flag, else undefined.
sub type {
my $self = shift;
- $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'});
- return $self->{'type'};
+ return $self->{'type'}
+ ||= new Bugzilla::FlagType($self->{'type_id'}, cache => 1 );
}
sub setter {
my $self = shift;
- $self->{'setter'} ||= new Bugzilla::User($self->{'setter_id'});
- return $self->{'setter'};
+ return $self->{'setter' }
+ ||= new Bugzilla::User({ id => $self->{'setter_id'}, cache => 1 });
}
sub requestee {
my $self = shift;
if (!defined $self->{'requestee'} && $self->{'requestee_id'}) {
- $self->{'requestee'} = new Bugzilla::User($self->{'requestee_id'});
+ $self->{'requestee'}
+ = new Bugzilla::User({ id => $self->{'requestee_id'}, cache => 1 });
}
return $self->{'requestee'};
}
@@ -205,16 +222,16 @@ sub attachment {
return undef unless $self->attach_id;
require Bugzilla::Attachment;
- $self->{'attachment'} ||= new Bugzilla::Attachment($self->attach_id);
- return $self->{'attachment'};
+ return $self->{'attachment'}
+ ||= new Bugzilla::Attachment({ id => $self->attach_id, cache => 1 });
}
sub bug {
my $self = shift;
require Bugzilla::Bug;
- $self->{'bug'} ||= new Bugzilla::Bug($self->bug_id);
- return $self->{'bug'};
+ return $self->{'bug'}
+ ||= new Bugzilla::Bug({ id => $self->bug_id, cache => 1 });
}
################################
@@ -284,7 +301,7 @@ sub count {
sub set_flag {
my ($class, $obj, $params) = @_;
- my ($bug, $attachment);
+ my ($bug, $attachment, $obj_flag, $requestee_changed);
if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) {
$attachment = $obj;
$bug = $attachment->bug;
@@ -296,6 +313,12 @@ sub set_flag {
ThrowCodeError('flag_unexpected_object', { 'caller' => ref $obj });
}
+ # Make sure the user can change flags
+ my $privs;
+ $bug->check_can_change_field('flagtypes.name', 0, 1, \$privs)
+ || ThrowUserError('illegal_change',
+ { field => 'flagtypes.name', privs => $privs });
+
# Update (or delete) an existing flag.
if ($params->{id}) {
my $flag = $class->check({ id => $params->{id} });
@@ -322,13 +345,14 @@ sub set_flag {
($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types};
push(@{$obj_flagtype->{flags}}, $flag);
}
- my ($obj_flag) = grep { $_->id == $flag->id } @{$obj_flagtype->{flags}};
+ ($obj_flag) = grep { $_->id == $flag->id } @{$obj_flagtype->{flags}};
# If the flag has the correct type but cannot be found above, this means
# the flag is going to be removed (e.g. because this is a pending request
# and the attachment is being marked as obsolete).
return unless $obj_flag;
- $class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment);
+ ($obj_flag, $requestee_changed) =
+ $class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment);
}
# Create a new flag.
elsif ($params->{type_id}) {
@@ -360,12 +384,21 @@ sub set_flag {
}
}
- $class->_validate(undef, $obj_flagtype, $params, $bug, $attachment);
+ ($obj_flag, $requestee_changed) =
+ $class->_validate(undef, $obj_flagtype, $params, $bug, $attachment);
}
else {
ThrowCodeError('param_required', { function => $class . '->set_flag',
param => 'id/type_id' });
}
+
+ if ($obj_flag
+ && $requestee_changed
+ && $obj_flag->requestee_id
+ && $obj_flag->requestee->setting('requestee_cc') eq 'on')
+ {
+ $bug->add_cc($obj_flag->requestee);
+ }
}
sub _validate {
@@ -383,25 +416,27 @@ sub _validate {
my $old_requestee_id = $obj_flag->requestee_id;
$obj_flag->_set_status($params->{status});
- $obj_flag->_set_requestee($params->{requestee}, $attachment, $params->{skip_roe});
+ $obj_flag->_set_requestee($params->{requestee}, $bug, $attachment, $params->{skip_roe});
+
+ # The requestee ID can be undefined.
+ my $requestee_changed = ($obj_flag->requestee_id || 0) != ($old_requestee_id || 0);
# The setter field MUST NOT be updated if neither the status
# nor the requestee fields changed.
- if (($obj_flag->status ne $old_status)
- # The requestee ID can be undefined.
- || (($obj_flag->requestee_id || 0) != ($old_requestee_id || 0)))
- {
+ if (($obj_flag->status ne $old_status) || $requestee_changed) {
$obj_flag->_set_setter($params->{setter});
}
# If the flag is deleted, remove it from the list.
if ($obj_flag->status eq 'X') {
@{$flag_type->{flags}} = grep { $_->id != $obj_flag->id } @{$flag_type->{flags}};
+ return;
}
# Add the newly created flag to the list.
elsif (!$obj_flag->id) {
push(@{$flag_type->{flags}}, $obj_flag);
}
+ return wantarray ? ($obj_flag, $requestee_changed) : $obj_flag;
}
=pod
@@ -418,10 +453,14 @@ Creates a flag record in the database.
sub create {
my ($class, $flag, $timestamp) = @_;
- $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT NOW()');
+ $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
my $params = {};
my @columns = grep { $_ ne 'id' } $class->_get_db_columns;
+
+ # Some columns use date formatting so use alias instead
+ @columns = map { /\s+AS\s+(.*)$/ ? $1 : $_ } @columns;
+
$params->{$_} = $flag->{$_} foreach @columns;
$params->{creation_date} = $params->{modification_date} = $timestamp;
@@ -433,14 +472,21 @@ sub create {
sub update {
my $self = shift;
my $dbh = Bugzilla->dbh;
- my $timestamp = shift || $dbh->selectrow_array('SELECT NOW()');
+ my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
my $changes = $self->SUPER::update(@_);
if (scalar(keys %$changes)) {
$dbh->do('UPDATE flags SET modification_date = ? WHERE id = ?',
undef, ($timestamp, $self->id));
+ $self->{'modification_date'} =
+ format_time($timestamp, '%Y.%m.%d %T', Bugzilla->local_timezone);
+ Bugzilla->memcached->clear({ table => 'flags', id => $self->id });
}
+
+ # BMO - provide a hook which passes the flag object
+ Bugzilla::Hook::process('flag_updated', {flag => $self, changes => $changes, timestamp => $timestamp});
+
return $changes;
}
@@ -495,6 +541,10 @@ sub update_flags {
# These flags have been deleted.
foreach my $old_flag (values %old_flags) {
$class->notify(undef, $old_flag, $self, $timestamp);
+
+ # BMO - provide a hook which passes the timestamp,
+ # because that isn't passed to remove_from_db().
+ Bugzilla::Hook::process('flag_deleted', {flag => $old_flag, timestamp => $timestamp});
$old_flag->remove_from_db();
}
@@ -586,11 +636,17 @@ sub force_retarget {
if ($is_retargetted) {
$dbh->do('UPDATE flags SET type_id = ? WHERE id = ?',
undef, ($flag->type_id, $flag->id));
+ Bugzilla->memcached->clear({ table => 'flags', id => $flag->id });
}
else {
# Track deleted attachment flags.
push(@removed, $class->snapshot([$flag])) if $flag->attach_id;
$class->notify(undef, $flag, $bug || $flag->bug);
+
+ # BMO - provide a hook which passes the timestamp,
+ # because that isn't passed to remove_from_db().
+ my ($timestamp) = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ Bugzilla::Hook::process('flag_deleted', {flag => $flag, timestamp => $timestamp});
$flag->remove_from_db();
}
}
@@ -602,10 +658,10 @@ sub force_retarget {
###############################
sub _set_requestee {
- my ($self, $requestee, $attachment, $skip_requestee_on_error) = @_;
+ my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_;
$self->{requestee} =
- $self->_check_requestee($requestee, $attachment, $skip_requestee_on_error);
+ $self->_check_requestee($requestee, $bug, $attachment, $skip_requestee_on_error);
$self->{requestee_id} =
$self->{requestee} ? $self->{requestee}->id : undef;
@@ -627,7 +683,7 @@ sub _set_status {
}
sub _check_requestee {
- my ($self, $requestee, $attachment, $skip_requestee_on_error) = @_;
+ my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_;
# If the flag status is not "?", then no requestee can be defined.
return undef if ($self->status ne '?');
@@ -647,15 +703,29 @@ sub _check_requestee {
# is specifically requestable. For existing flags, if the requestee
# was set before the flag became specifically unrequestable, the
# user can either remove him or leave him alone.
- ThrowCodeError('flag_requestee_disabled', { type => $self->type })
+ ThrowCodeError('flag_type_requestee_disabled', { type => $self->type })
if !$self->type->is_requesteeble;
+ # BMO customisation:
+ # You can't ask a disabled account, as they don't have the ability to
+ # set the flag.
+ ThrowUserError('flag_requestee_disabled', { requestee => $requestee })
+ if !$requestee->is_enabled;
+
# Make sure the requestee can see the bug.
# Note that can_see_bug() will query the DB, so if the bug
# is being added/removed from some groups and these changes
# haven't been committed to the DB yet, they won't be taken
- # into account here. In this case, old restrictions matters.
- if (!$requestee->can_see_bug($self->bug_id)) {
+ # into account here. In this case, old group restrictions matter.
+ # However, if the user has just been changed to the assignee,
+ # qa_contact, or added to the cc list of the bug and the bug
+ # is cclist_accessible, the requestee is allowed.
+ if (!$requestee->can_see_bug($self->bug_id)
+ && (!$bug->cclist_accessible
+ || !grep($_->id == $requestee->id, @{ $bug->cc_users })
+ && $requestee->id != $bug->assigned_to->id
+ && (!$bug->qa_contact || $requestee->id != $bug->qa_contact->id)))
+ {
if ($skip_requestee_on_error) {
undef $requestee;
}
@@ -789,7 +859,7 @@ sub extract_flags_from_cgi {
# Extract a list of existing flag IDs.
my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
- return () if (!scalar(@flagtype_ids) && !scalar(@flag_ids));
+ return ([], []) unless (scalar(@flagtype_ids) || scalar(@flag_ids));
my (@new_flags, @flags);
foreach my $flag_id (@flag_ids) {
@@ -955,7 +1025,7 @@ sub notify {
my %recipients;
foreach my $cc (split(/[, ]+/, $cc_list)) {
- my $ccuser = new Bugzilla::User({ name => $cc });
+ my $ccuser = new Bugzilla::User({ name => $cc, cache => 1 });
next if (scalar(@bug_in_groups) && (!$ccuser || !$ccuser->can_see_bug($bug->bug_id)));
next if $attachment_is_private && (!$ccuser || !$ccuser->is_insider);
# Prevent duplicated entries due to case sensitivity.
@@ -1054,29 +1124,30 @@ sub _flag_types {
return $flag_types;
}
-=head1 SEE ALSO
+=head1 B<Methods in need of POD>
=over
-=item B<Bugzilla::FlagType>
+=item update_activity
-=back
+=item setter_id
+=item bug
-=head1 CONTRIBUTORS
+=item requestee_id
-=over
+=item DB_COLUMNS
-=item Myk Melez <myk@mozilla.org>
+=item set_flag
-=item Jouni Heikniemi <jouni@heikniemi.net>
+=item type_id
-=item Kevin Benton <kevin.benton@amd.com>
+=item snapshot
-=item Frédéric Buclin <LpSolit@gmail.com>
+=item update_flags
-=back
+=item update
-=cut
+=back
1;
diff --git a/Bugzilla/FlagType.pm b/Bugzilla/FlagType.pm
index 811530c42..8c958f5ce 100644
--- a/Bugzilla/FlagType.pm
+++ b/Bugzilla/FlagType.pm
@@ -52,6 +52,7 @@ use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Util;
use Bugzilla::Group;
+use Bugzilla::Hook;
use base qw(Bugzilla::Object);
@@ -133,6 +134,8 @@ sub create {
exclusions => $exclusions });
$flagtype->update();
+ Bugzilla::Hook::process('flagtype_end_of_create', { type => $flagtype });
+
$dbh->bz_commit_transaction();
return $flagtype;
}
@@ -197,10 +200,21 @@ sub update {
# Silently remove requestees from flags which are no longer
# specifically requestable.
if (!$self->is_requesteeble) {
- $dbh->do('UPDATE flags SET requestee_id = NULL WHERE type_id = ?',
- undef, $self->id);
+ my $ids = $dbh->selectcol_arrayref(
+ 'SELECT id FROM flags WHERE type_id = ? AND requestee_id IS NOT NULL',
+ undef, $self->id);
+
+ if (@$ids) {
+ $dbh->do('UPDATE flags SET requestee_id = NULL WHERE ' . $dbh->sql_in('id', $ids));
+ foreach my $id (@$ids) {
+ Bugzilla->memcached->clear({ table => 'flags', id => $id });
+ }
+ }
}
+ Bugzilla::Hook::process('flagtype_end_of_update',
+ { type => $self, changed => $changes });
+
$dbh->bz_commit_transaction();
return $changes;
}
@@ -462,7 +476,8 @@ sub grant_list {
my @custusers;
my @allusers = @{Bugzilla->user->get_userlist};
foreach my $user (@allusers) {
- my $user_obj = new Bugzilla::User({name => $user->{login}});
+ my $user_obj
+ = new Bugzilla::User({ name => $user->{login}, cache => 1 });
push(@custusers, $user) if $user_obj->can_set_flag($self);
}
return \@custusers;
@@ -601,7 +616,7 @@ sub match {
$tables = join(' ', @$tables);
$criteria = join(' AND ', @criteria);
- my $flagtype_ids = $dbh->selectcol_arrayref("SELECT id FROM $tables WHERE $criteria");
+ my $flagtype_ids = $dbh->selectcol_arrayref("SELECT flagtypes.id FROM $tables WHERE $criteria");
return Bugzilla::FlagType->new_from_list($flagtype_ids);
}
@@ -652,6 +667,8 @@ by the query.
=back
+=end private
+
=cut
sub sqlify_criteria {
@@ -679,6 +696,11 @@ sub sqlify_criteria {
my $is_active = $criteria->{is_active} ? "1" : "0";
push(@criteria, "flagtypes.is_active = $is_active");
}
+ if (exists($criteria->{active_or_has_flags}) && $criteria->{active_or_has_flags} =~ /^\d+$/) {
+ push(@$tables, "LEFT JOIN flags AS f ON flagtypes.id = f.type_id " .
+ "AND f.bug_id = " . $criteria->{active_or_has_flags});
+ push(@criteria, "(flagtypes.is_active = 1 OR f.id IS NOT NULL)");
+ }
if ($criteria->{product_id}) {
my $product_id = $criteria->{product_id};
detaint_natural($product_id)
@@ -725,32 +747,48 @@ sub sqlify_criteria {
push(@criteria, "(flagtypes.grant_group_id = $gid " .
" OR flagtypes.request_group_id = $gid)");
}
-
+
return @criteria;
}
1;
-=end private
-
-=head1 SEE ALSO
+=head1 B<Methods in need of POD>
=over
-=item B<Bugzilla::Flags>
+=item exclusions_as_hash
-=back
+=item request_group_id
-=head1 CONTRIBUTORS
+=item set_is_active
-=over
+=item set_is_multiplicable
-=item Myk Melez <myk@mozilla.org>
+=item inclusions_as_hash
-=item Kevin Benton <kevin.benton@amd.com>
+=item set_sortkey
-=item Frédéric Buclin <LpSolit@gmail.com>
+=item grant_group_id
-=back
+=item set_cc_list
-=cut
+=item set_request_group
+
+=item set_name
+
+=item set_is_specifically_requestable
+
+=item set_grant_group
+
+=item create
+
+=item set_clusions
+
+=item set_description
+
+=item set_is_requestable
+
+=item update
+
+=back
diff --git a/Bugzilla/Group.pm b/Bugzilla/Group.pm
index 382407748..732de348f 100644
--- a/Bugzilla/Group.pm
+++ b/Bugzilla/Group.pm
@@ -37,6 +37,8 @@ use Bugzilla::Config qw(:admin);
##### Module Initialization ###
###############################
+use constant IS_CONFIG => 1;
+
use constant DB_COLUMNS => qw(
groups.id
groups.name
@@ -119,9 +121,10 @@ sub _get_members {
}
sub flag_types {
- my $self = shift;
+ my ($self, $params) = @_;
+ $params ||= {};
require Bugzilla::FlagType;
- $self->{flag_types} ||= Bugzilla::FlagType::match({ group => $self->id });
+ $self->{flag_types} ||= Bugzilla::FlagType::match({ group => $self->id, %$params });
return $self->{flag_types};
}
@@ -230,6 +233,7 @@ sub update {
Bugzilla::Hook::process('group_end_of_update',
{ group => $self, changes => $changes });
$dbh->bz_commit_transaction();
+ Bugzilla->memcached->clear_config();
return $changes;
}
@@ -419,6 +423,7 @@ sub create {
Bugzilla::Hook::process('group_end_of_create', { group => $group });
$dbh->bz_commit_transaction();
+ Bugzilla->memcached->clear_config();
return $group;
}
diff --git a/Bugzilla/Hook.pm b/Bugzilla/Hook.pm
index c658989a0..5e3dca655 100644
--- a/Bugzilla/Hook.pm
+++ b/Bugzilla/Hook.pm
@@ -434,6 +434,39 @@ to the user.
=back
+=head2 bug_start_of_update
+
+This happens near the beginning of L<Bugzilla::Bug/update>, after L<Bugzilla::Object/update>
+is called, but before all other special changes are made to the database. Once use case is
+this allows for adding your own entries to the C<changes> hash which gets added to the
+bugs_activity table later keeping you from having to do it yourself. Also this is also helpful
+if your extension needs to add CC members, flags, keywords, groups, etc. This generally
+occurs inside a database transaction.
+
+Params:
+
+=over
+
+=item C<bug>
+
+The changed bug object, with all fields set to their updated values.
+
+=item C<old_bug>
+
+A bug object pulled from the database before the fields were set to
+their updated values (so it has the old values available for each field).
+
+=item C<timestamp>
+
+The timestamp used for all updates in this transaction, as a SQL date
+string.
+
+=item C<changes>
+
+The hash of changed fields. C<< $changes->{field} = [old, new] >>
+
+=back
+
=head2 buglist_columns
This happens in L<Bugzilla::Search/COLUMNS>, which determines legal bug
@@ -1289,6 +1322,22 @@ your template.
=back
+=head2 path_info_whitelist
+
+By default, Bugzilla removes the Path-Info information from URLs before
+passing data to CGI scripts. If this information is needed for your
+customizations, you can enumerate the pages you want to whitelist here.
+
+Params:
+
+=over
+
+=item C<whitelist>
+
+An array of script names that will not have their Path-Info automatically
+removed.
+
+=back
=head2 post_bug_after_creation
@@ -1545,6 +1594,89 @@ See L<Bugzilla::WebService::Constants/WS_ERROR_CODE> for an example.
=back
+=head2 webservice_fix_credentials
+
+This hook allows for altering the credential parameters provided by the client
+before authentication actually occurs. For example, this can be used to allow mapping
+of custom parameters to the standard Bugzilla_login and Bugzilla_password parameters.
+
+Params:
+
+=over
+
+=item C<params>
+
+A hash ref containing the parameters passed into the webservice after
+they have been obtained from the URL or body of the request.
+
+=back
+
+=head2 webservice_rest_request
+
+This hook allows for altering any of the parameters provided by the client
+after authentication has occured. You are able to change things like renaming
+of keys, removing values, or adding additional information.
+
+Params:
+
+=over
+
+=item C<params>
+
+A hash ref containing the parameters passed into the webservice after
+they have been obtained from the URL or body of the request.
+
+=item C<rpc>
+
+The current JSONRPC, XMLRPC, or REST object.
+
+=back
+
+=head2 webservice_rest_resources
+
+This hook allows for altering of the REST resources data allowing you to
+add additional paths to perform additional operations or to override the
+resources already provided by the webservice modules.
+
+Params:
+
+=over
+
+=item C<resources>
+
+A hash returned from each module loaded that is used to determine
+which code handler to use based on a regex match of the CGI path.
+
+=item C<rpc>
+
+The current JSONRPC, XMLRPC, or REST object.
+
+=back
+
+=head2 webservice_rest_response
+
+This hook allows for altering the result data or response object
+that is being returned by the current REST webservice call.
+
+Params:
+
+=over
+
+=item C<response>
+
+The HTTP response object generated by JSON-RPC library. You can use this
+to add headers, etc.
+
+=item C<result>
+
+A reference to a hash that contains the result data.
+
+=item C<rpc>
+
+The current JSONRPC, XMLRPC, or REST object.
+
+=back
+
=head1 SEE ALSO
L<Bugzilla::Extension>
diff --git a/Bugzilla/Install.pm b/Bugzilla/Install.pm
index ce8fe6bad..d5f4f04cd 100644
--- a/Bugzilla/Install.pm
+++ b/Bugzilla/Install.pm
@@ -93,6 +93,12 @@ sub SETTINGS {
# 2011-06-21 glob@mozilla.com -- Bug 589128
email_format => { options => ['html', 'text_only'],
default => 'html' },
+ # 2011-06-16 glob@mozilla.com -- Bug 663747
+ bugmail_new_prefix => { options => ['on', 'off'], default => 'on' },
+ # 2013-07-26 joshi_sunil@in.com -- Bug 669535
+ possible_duplicates => { options => ['on', 'off'], default => 'on' },
+ # 2011-10-11 glob@mozilla.com -- Bug 301656
+ requestee_cc => { options => ['on', 'off'], default => 'on' },
}
};
diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm
index 3ac83775a..4fb1f1c83 100644
--- a/Bugzilla/Install/DB.pm
+++ b/Bugzilla/Install/DB.pm
@@ -261,7 +261,7 @@ sub update_table_definitions {
$dbh->bz_add_column("bugs", "cclist_accessible",
{TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
- $dbh->bz_add_column("bugs_activity", "attach_id", {TYPE => 'INT3'});
+ $dbh->bz_add_column("bugs_activity", "attach_id", {TYPE => 'INT5'});
_delete_logincookies_cryptpassword_and_handle_invalid_cookies();
@@ -398,7 +398,7 @@ sub update_table_definitions {
"WHERE initialqacontact = 0");
_migrate_email_prefs_to_new_table();
- _initialize_dependency_tree_changes_email_pref();
+ _initialize_new_email_prefs();
_change_all_mysql_booleans_to_tinyint();
# make classification_id field type be consistent with DB:Schema
@@ -455,7 +455,7 @@ sub update_table_definitions {
# 2005-12-07 altlst@sonic.net -- Bug 225221
$dbh->bz_add_column('longdescs', 'comment_id',
- {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+ {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
_stop_storing_inactive_flags();
_change_short_desc_from_mediumtext_to_varchar();
@@ -607,7 +607,7 @@ sub update_table_definitions {
_fix_series_creator_fk();
# 2009-11-14 dkl@redhat.com - Bug 310450
- $dbh->bz_add_column('bugs_activity', 'comment_id', {TYPE => 'INT3'});
+ $dbh->bz_add_column('bugs_activity', 'comment_id', {TYPE => 'INT4'});
# 2010-04-07 LpSolit@gmail.com - Bug 69621
$dbh->bz_drop_column('bugs', 'keywords');
@@ -660,6 +660,9 @@ sub update_table_definitions {
# 2011-10-11 miketosh - Bug 690173
_on_delete_set_null_for_audit_log_userid();
+ # 2011-11-01 glob@mozilla.com - Bug 240437
+ $dbh->bz_add_column('profiles', 'last_seen_date', {TYPE => 'DATETIME'});
+
# 2011-11-28 dkl@mozilla.com - Bug 685611
_fix_notnull_defaults();
@@ -669,6 +672,45 @@ sub update_table_definitions {
$dbh->bz_add_index('profile_search', 'profile_search_user_id_idx', [qw(user_id)]);
}
+ # 2012-06-06 dkl@mozilla.com - Bug 762288
+ $dbh->bz_alter_column('bugs_activity', 'removed',
+ { TYPE => 'varchar(255)' });
+ $dbh->bz_add_index('bugs_activity', 'bugs_activity_removed_idx', ['removed']);
+
+ # 2012-06-13 dkl@mozilla.com - Bug 764457
+ $dbh->bz_add_column('bugs_activity', 'id',
+ {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+
+ # 2012-06-13 dkl@mozilla.com - Bug 764466
+ $dbh->bz_add_column('profiles_activity', 'id',
+ {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+
+ # 2012-07-24 dkl@mozilla.com - Bug 776972
+ # BMO - we change this to BIGSERIAL further down
+ #$dbh->bz_alter_column('bugs_activity', 'id',
+ # {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+
+
+ # 2012-07-24 dkl@mozilla.com - Bug 776982
+ _fix_longdescs_primary_key();
+
+ # 2012-08-02 dkl@mozilla.com - Bug 756953
+ _fix_dependencies_dupes();
+
+ # 2013-02-04 dkl@mozilla.com - Bug 824346
+ _fix_flagclusions_indexes();
+
+ # 2012-04-15 Frank@Frank-Becker.de - Bug 740536
+ $dbh->bz_add_index('audit_log', 'audit_log_class_idx', ['class', 'at_time']);
+
+ # 2013-08-16 glob@mozilla.com - Bug 905925
+ $dbh->bz_add_index('attachments', 'attachments_ispatch_idx', ['ispatch']);
+
+ # 2014-06-09 dylan@mozilla.com - Bug 1022923
+ $dbh->bz_add_index('bug_user_last_visit',
+ 'bug_user_last_visit_last_visit_ts_idx',
+ ['last_visit_ts']);
+
################################################################
# New --TABLE-- changes should go *** A B O V E *** this point #
################################################################
@@ -2396,13 +2438,16 @@ sub _migrate_email_prefs_to_new_table {
}
}
-sub _initialize_dependency_tree_changes_email_pref {
+sub _initialize_new_email_prefs {
my $dbh = Bugzilla->dbh;
# Check for any "new" email settings that wouldn't have been ported over
# during the block above. Since these settings would have otherwise
# fallen under EVT_OTHER, we'll just clone those settings. That way if
# folks have already disabled all of that mail, there won't be any change.
- my %events = ("Dependency Tree Changes" => EVT_DEPEND_BLOCK);
+ my %events = (
+ "Dependency Tree Changes" => EVT_DEPEND_BLOCK,
+ "Product/Component Changes" => EVT_COMPONENT,
+ );
foreach my $desc (keys %events) {
my $event = $events{$desc};
@@ -3220,6 +3265,11 @@ sub _populate_bugs_fulltext {
print "Populating bugs_fulltext with $num_bugs entries...";
print " (this can take a long time.)\n";
}
+
+ # As recommended by Monty Widenius for GNOME's upgrade.
+ # mkanat and justdave concur it'll be helpful for bmo, too.
+ $dbh->do('SET SESSION myisam_sort_buffer_size = 3221225472');
+
my $newline = $dbh->quote("\n");
$dbh->do(
qq{$command INTO bugs_fulltext (bug_id, short_desc, comments,
@@ -3682,6 +3732,70 @@ sub _fix_notnull_defaults {
}
}
+sub _fix_longdescs_primary_key {
+ my $dbh = Bugzilla->dbh;
+ if ($dbh->bz_column_info('longdescs', 'comment_id')->{TYPE} ne 'INTSERIAL') {
+ $dbh->bz_drop_related_fks('longdescs', 'comment_id');
+ $dbh->bz_alter_column('bugs_activity', 'comment_id', {TYPE => 'INT4'});
+ $dbh->bz_alter_column('longdescs', 'comment_id',
+ {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+ }
+}
+
+sub _fix_dependencies_dupes {
+ my $dbh = Bugzilla->dbh;
+ my $blocked_idx = $dbh->bz_index_info('dependencies', 'dependencies_blocked_idx');
+ if ($blocked_idx && scalar @{$blocked_idx->{'FIELDS'}} < 2) {
+ # Remove duplicated entries
+ my $dupes = $dbh->selectall_arrayref("
+ SELECT blocked, dependson, COUNT(*) AS count
+ FROM dependencies " .
+ $dbh->sql_group_by('blocked, dependson') . "
+ HAVING COUNT(*) > 1",
+ { Slice => {} });
+ print "Removing duplicated entries from the 'dependencies' table...\n" if @$dupes;
+ foreach my $dupe (@$dupes) {
+ $dbh->do("DELETE FROM dependencies
+ WHERE blocked = ? AND dependson = ?",
+ undef, $dupe->{blocked}, $dupe->{dependson});
+ $dbh->do("INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)",
+ undef, $dupe->{blocked}, $dupe->{dependson});
+ }
+ $dbh->bz_drop_index('dependencies', 'dependencies_blocked_idx');
+ $dbh->bz_add_index('dependencies', 'dependencies_blocked_idx',
+ { FIELDS => [qw(blocked dependson)], TYPE => 'UNIQUE' });
+ }
+}
+
+sub _fix_flagclusions_indexes {
+ my $dbh = Bugzilla->dbh;
+ foreach my $table ('flaginclusions', 'flagexclusions') {
+ my $index = $table . '_type_id_idx';
+ my $idx_info = $dbh->bz_index_info($table, $index);
+ if ($idx_info && $idx_info->{'TYPE'} ne 'UNIQUE') {
+ # Remove duplicated entries
+ my $dupes = $dbh->selectall_arrayref("
+ SELECT type_id, product_id, component_id, COUNT(*) AS count
+ FROM $table " .
+ $dbh->sql_group_by('type_id, product_id, component_id') . "
+ HAVING COUNT(*) > 1",
+ { Slice => {} });
+ print "Removing duplicated entries from the '$table' table...\n" if @$dupes;
+ foreach my $dupe (@$dupes) {
+ $dbh->do("DELETE FROM $table
+ WHERE type_id = ? AND product_id = ? AND component_id = ?",
+ undef, $dupe->{type_id}, $dupe->{product_id}, $dupe->{component_id});
+ $dbh->do("INSERT INTO $table (type_id, product_id, component_id) VALUES (?, ?, ?)",
+ undef, $dupe->{type_id}, $dupe->{product_id}, $dupe->{component_id});
+ }
+ $dbh->bz_drop_index($table, $index);
+ $dbh->bz_add_index($table, $index,
+ { FIELDS => [qw(type_id product_id component_id)],
+ TYPE => 'UNIQUE' });
+ }
+ }
+}
+
1;
__END__
diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm
index c5215ecfa..621ece803 100644
--- a/Bugzilla/Install/Filesystem.pm
+++ b/Bugzilla/Install/Filesystem.pm
@@ -38,6 +38,7 @@ use File::Find;
use File::Path;
use File::Basename;
use File::Copy qw(move);
+use File::Slurp;
use IO::File;
use POSIX ();
@@ -125,6 +126,7 @@ sub FILESYSTEM {
my $localconfig = bz_locations()->{'localconfig'};
my $template_cache = bz_locations()->{'template_cache'};
my $graphsdir = bz_locations()->{'graphsdir'};
+ my $assetsdir = bz_locations()->{'assetsdir'};
# We want to set the permissions the same for all localconfig files
# across all PROJECTs, so we do something special with $localconfig,
@@ -159,7 +161,10 @@ sub FILESYSTEM {
'runtests.pl' => { perms => OWNER_EXECUTE },
'jobqueue.pl' => { perms => OWNER_EXECUTE },
'migrate.pl' => { perms => OWNER_EXECUTE },
+ 'sentry.pl' => { perms => WS_EXECUTE },
+ 'metrics.pl' => { perms => WS_EXECUTE },
'install-module.pl' => { perms => OWNER_EXECUTE },
+ 'clean-bug-user-last-visit.pl' => { perms => WS_EXECUTE },
'Bugzilla.pm' => { perms => CGI_READ },
"$localconfig*" => { perms => CGI_READ },
@@ -170,6 +175,7 @@ sub FILESYSTEM {
'contrib/README' => { perms => OWNER_WRITE },
'contrib/*/README' => { perms => OWNER_WRITE },
+ 'contrib/sendunsentbugmail.pl' => { perms => WS_EXECUTE },
'docs/bugzilla.ent' => { perms => OWNER_WRITE },
'docs/makedocs.pl' => { perms => OWNER_EXECUTE },
'docs/style.css' => { perms => WS_SERVE },
@@ -179,13 +185,16 @@ sub FILESYSTEM {
"$datadir/old-params.txt" => { perms => OWNER_WRITE },
"$extensionsdir/create.pl" => { perms => OWNER_EXECUTE },
"$extensionsdir/*/*.pl" => { perms => WS_EXECUTE },
+ "$extensionsdir/*/bin/*" => { perms => WS_EXECUTE },
);
# Directories that we want to set the perms on, but not
# recurse through. These are directories we didn't create
# in checkesetup.pl.
+ #
+ # Purpose of BMO change: unknown.
my %non_recurse_dirs = (
- '.' => DIR_WS_SERVE,
+ '.' => 0755,
docs => DIR_WS_SERVE,
);
@@ -205,6 +214,8 @@ sub FILESYSTEM {
dirs => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE },
"$datadir/db" => { files => CGI_WRITE,
dirs => DIR_CGI_WRITE },
+ $assetsdir => { files => WS_SERVE,
+ dirs => DIR_CGI_OVERWRITE | DIR_ALSO_WS_SERVE },
# Readable directories
"$datadir/mining" => { files => CGI_READ,
@@ -243,10 +254,13 @@ sub FILESYSTEM {
dirs => DIR_WS_SERVE },
"$extensionsdir/*/web" => { files => WS_SERVE,
dirs => DIR_WS_SERVE },
-
+
+ # Purpose: allow webserver to read .bzr so we execute bzr commands
+ # in backticks and look at the result over the web. Used to show
+ # bzr history.
+ '.bzr' => { files => WS_SERVE,
+ dirs => DIR_WS_SERVE },
# Directories only for the owner, not for the webserver.
- '.bzr' => { files => OWNER_WRITE,
- dirs => DIR_OWNER_WRITE },
t => { files => OWNER_WRITE,
dirs => DIR_OWNER_WRITE },
xt => { files => OWNER_WRITE,
@@ -264,7 +278,8 @@ sub FILESYSTEM {
# The name of each directory that we should actually *create*,
# pointing at its default permissions.
my %create_dirs = (
- # This is DIR_ALSO_WS_SERVE because it contains $webdotdir.
+ # This is DIR_ALSO_WS_SERVE because it contains $webdotdir and
+ # $assetsdir.
$datadir => DIR_CGI_OVERWRITE | DIR_ALSO_WS_SERVE,
# Directories that are read-only for cgi scripts
"$datadir/mining" => DIR_CGI_READ,
@@ -275,6 +290,7 @@ sub FILESYSTEM {
$attachdir => DIR_CGI_WRITE,
$graphsdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE,
$webdotdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE,
+ $assetsdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE,
# Directories that contain content served directly by the web server.
"$skinsdir/custom" => DIR_WS_SERVE,
"$skinsdir/contrib" => DIR_WS_SERVE,
@@ -364,6 +380,18 @@ EOT
Deny from all
EOT
},
+
+ "$assetsdir/.htaccess" => { perms => WS_SERVE, contents => <<EOT
+# Allow access to .css files
+<FilesMatch \\.(css|js)\$>
+ Allow from all
+</FilesMatch>
+
+# And no directory listings, either.
+Deny from all
+EOT
+ },
+
);
Bugzilla::Hook::process('install_filesystem', {
@@ -398,6 +426,7 @@ sub update_filesystem {
my $datadir = bz_locations->{'datadir'};
my $graphsdir = bz_locations->{'graphsdir'};
+ my $assetsdir = bz_locations->{'assetsdir'};
# If the graphs/ directory doesn't exist, we're upgrading from
# a version old enough that we need to update the $datadir/mining
# format.
@@ -431,6 +460,13 @@ sub update_filesystem {
_rename_file($oldparamsfile, "$datadir/$oldparamsfile");
}
+ # Remove old assets htaccess file to force recreation with correct values.
+ if (-e "$assetsdir/.htaccess") {
+ if (read_file("$assetsdir/.htaccess") =~ /<FilesMatch \\\.css\$>/) {
+ unlink("$assetsdir/.htaccess");
+ }
+ }
+
_create_files(%files);
if ($params->{index_html}) {
_create_files(%{$fs->{index_html}});
@@ -474,6 +510,7 @@ EOT
_remove_empty_css_files();
_convert_single_file_skins();
+ _remove_dynamic_assets();
}
sub _remove_empty_css_files {
@@ -518,6 +555,27 @@ sub _convert_single_file_skins {
}
}
+# delete all automatically generated css/js files to force recreation at the
+# next request.
+sub _remove_dynamic_assets {
+ my @files = (
+ glob(bz_locations()->{assetsdir} . '/*.css'),
+ glob(bz_locations()->{assetsdir} . '/*.js'),
+ );
+ foreach my $file (@files) {
+ unlink($file);
+ }
+
+ # remove old skins/assets directory
+ my $old_path = bz_locations()->{skinsdir} . '/assets';
+ if (-d $old_path) {
+ foreach my $file (glob("$old_path/*.css")) {
+ unlink($file);
+ }
+ rmdir($old_path);
+ }
+}
+
sub create_htaccess {
_create_files(%{FILESYSTEM()->{htaccess}});
diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm
index 2e67457ab..1c9c91345 100644
--- a/Bugzilla/Install/Requirements.pm
+++ b/Bugzilla/Install/Requirements.pm
@@ -288,7 +288,7 @@ sub OPTIONAL_MODULES {
package => 'JSON-RPC',
module => 'JSON::RPC',
version => 0,
- feature => ['jsonrpc'],
+ feature => ['jsonrpc', 'rest'],
},
{
package => 'JSON-XS',
@@ -301,7 +301,7 @@ sub OPTIONAL_MODULES {
package => 'Test-Taint',
module => 'Test::Taint',
version => 0,
- feature => ['jsonrpc', 'xmlrpc'],
+ feature => ['jsonrpc', 'xmlrpc', 'rest'],
},
{
# We need the 'utf8_mode' method of HTML::Parser, for HTML::Scrubber.
@@ -372,6 +372,27 @@ sub OPTIONAL_MODULES {
version => '0.96',
feature => ['mod_perl'],
},
+
+ # memcached
+ {
+ package => 'URI-Escape',
+ module => 'URI::Escape',
+ version => 0,
+ feature => ['memcached'],
+ },
+ {
+ package => 'Cache-Memcached',
+ module => 'Cache::Memcached',
+ version => '0',
+ feature => ['memcached'],
+ },
+
+ # BMO - metrics
+ {
+ package => 'ElasticSearch',
+ module => 'ElasticSearch',
+ version => '0',
+ },
);
my $extra_modules = _get_extension_requirements('OPTIONAL_MODULES');
@@ -385,6 +406,7 @@ use constant FEATURE_FILES => (
jsonrpc => ['Bugzilla/WebService/Server/JSONRPC.pm', 'jsonrpc.cgi'],
xmlrpc => ['Bugzilla/WebService/Server/XMLRPC.pm', 'xmlrpc.cgi',
'Bugzilla/WebService.pm', 'Bugzilla/WebService/*.pm'],
+ rest => ['Bugzilla/WebService/Server/REST.pm', 'rest.cgi'],
moving => ['importxml.pl'],
auth_ldap => ['Bugzilla/Auth/Verify/LDAP.pm'],
auth_radius => ['Bugzilla/Auth/Verify/RADIUS.pm'],
@@ -393,6 +415,7 @@ use constant FEATURE_FILES => (
'Bugzilla/JobQueue/*', 'jobqueue.pl'],
patch_viewer => ['Bugzilla/Attachment/PatchReader.pm'],
updates => ['Bugzilla/Update.pm'],
+ memcached => ['Bugzilla/Memcache.pm'],
);
# This implements the REQUIRED_MODULES and OPTIONAL_MODULES stuff
diff --git a/Bugzilla/Install/Util.pm b/Bugzilla/Install/Util.pm
index bd8942507..5f6c8bceb 100644
--- a/Bugzilla/Install/Util.pm
+++ b/Bugzilla/Install/Util.pm
@@ -382,7 +382,10 @@ sub include_languages {
# Basically, the way this works is that we have a list of languages
# that we *want*, and a list of languages that Bugzilla actually
- # supports.
+ # supports. If there is only one language installed, we take it.
+ my $supported = supported_languages();
+ return @$supported if @$supported == 1;
+
my $wanted;
if ($params->{language}) {
# We can pass several languages at once as an arrayref
@@ -393,7 +396,6 @@ sub include_languages {
else {
$wanted = _wanted_languages();
}
- my $supported = supported_languages();
my $actual = _wanted_to_actual_languages($wanted, $supported);
return @$actual;
}
diff --git a/Bugzilla/Job/BugMail.pm b/Bugzilla/Job/BugMail.pm
new file mode 100644
index 000000000..9c176b005
--- /dev/null
+++ b/Bugzilla/Job/BugMail.pm
@@ -0,0 +1,31 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Job::BugMail;
+
+use 5.10.1;
+use strict;
+
+use Bugzilla::BugMail;
+BEGIN { eval "use parent qw(Bugzilla::Job::Mailer)"; }
+
+sub work {
+ my ($class, $job) = @_;
+ my $success = eval {
+ Bugzilla::BugMail::dequeue($job->arg->{vars});
+ 1;
+ };
+ if (!$success) {
+ $job->failed($@);
+ undef $@;
+ }
+ else {
+ $job->completed;
+ }
+}
+
+1;
diff --git a/Bugzilla/JobQueue.pm b/Bugzilla/JobQueue.pm
index 7ea678345..669076dd5 100644
--- a/Bugzilla/JobQueue.pm
+++ b/Bugzilla/JobQueue.pm
@@ -27,12 +27,15 @@ use strict;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Install::Util qw(install_string);
+use File::Slurp;
use base qw(TheSchwartz);
+use fields qw(_worker_pidfile);
# This maps job names for Bugzilla::JobQueue to the appropriate modules.
# If you add new types of jobs, you should add a mapping here.
use constant JOB_MAP => {
send_mail => 'Bugzilla::Job::Mailer',
+ bug_mail => 'Bugzilla::Job::BugMail',
};
# Without a driver cache TheSchwartz opens a new database connection
@@ -40,6 +43,10 @@ use constant JOB_MAP => {
# across requests.
use constant DRIVER_CACHE_TIME => 300; # 5 minutes
+# To avoid memory leak/fragmentation, a worker process won't process more than
+# MAX_MESSAGES messages.
+use constant MAX_MESSAGES => 1000;
+
sub job_map {
if (!defined(Bugzilla->request_cache->{job_map})) {
my $job_map = JOB_MAP;
@@ -99,6 +106,74 @@ sub insert {
return $retval;
}
+# To avoid memory leaks/fragmentation which tends to happen for long running
+# perl processes; check for jobs, and spawn a new process to empty the queue.
+sub subprocess_worker {
+ my $self = shift;
+
+ my $command = "$0 -p '" . $self->{_worker_pidfile} . "' onepass";
+
+ while (1) {
+ my $time = (time);
+ my @jobs = $self->list_jobs({
+ funcname => $self->{all_abilities},
+ run_after => $time,
+ grabbed_until => $time,
+ limit => 1,
+ });
+ if (@jobs) {
+ $self->debug("Spawning queue worker process");
+ # Run the worker as a daemon
+ system $command;
+ # And poll the PID to detect when the working has finished.
+ # We do this instead of system() to allow for the INT signal to
+ # interrup us and trigger kill_worker().
+ my $pid = read_file($self->{_worker_pidfile}, err_mode => 'quiet');
+ if ($pid) {
+ sleep(3) while(kill(0, $pid));
+ }
+ $self->debug("Queue worker process completed");
+ } else {
+ $self->debug("No jobs found");
+ }
+ sleep(5);
+ }
+}
+
+sub kill_worker {
+ my $self = Bugzilla->job_queue();
+ if ($self->{_worker_pidfile} && -e $self->{_worker_pidfile}) {
+ my $worker_pid = read_file($self->{_worker_pidfile});
+ if ($worker_pid && kill(0, $worker_pid)) {
+ $self->debug("Stopping worker process");
+ system "$0 -f -p '" . $self->{_worker_pidfile} . "' stop";
+ }
+ }
+}
+
+sub set_pidfile {
+ my ($self, $pidfile) = @_;
+ $pidfile =~ s/^(.+)(\..+)$/$1.worker$2/;
+ $self->{_worker_pidfile} = $pidfile;
+}
+
+# Clear the request cache at the start of each run.
+sub work_once {
+ my $self = shift;
+ Bugzilla->clear_request_cache();
+ return $self->SUPER::work_once(@_);
+}
+
+# Never process more than MAX_MESSAGES in one batch, to avoid memory
+# leak/fragmentation issues.
+sub work_until_done {
+ my $self = shift;
+ my $count = 0;
+ while ($count++ < MAX_MESSAGES) {
+ $self->work_once or last;
+ }
+}
+
1;
__END__
diff --git a/Bugzilla/JobQueue/Runner.pm b/Bugzilla/JobQueue/Runner.pm
index 26755e78f..d45c36647 100644
--- a/Bugzilla/JobQueue/Runner.pm
+++ b/Bugzilla/JobQueue/Runner.pm
@@ -51,6 +51,7 @@ our $initscript = "bugzilla-queue";
sub gd_preconfig {
my $self = shift;
+ $self->{_run_command} = 'subprocess_worker';
my $pidfile = $self->{gd_args}{pidfile};
if (!$pidfile) {
$pidfile = bz_locations()->{datadir} . '/' . $self->{gd_progname}
@@ -196,21 +197,26 @@ sub gd_setup_signals {
$SIG{TERM} = sub { $self->gd_quit_event(); }
}
-sub gd_other_cmd {
- my ($self) = shift;
- if ($ARGV[0] eq "once") {
- $self->_do_work("work_once");
+sub gd_quit_event {
+ Bugzilla->job_queue->kill_worker();
+ exit(1);
+}
- exit(0);
+sub gd_other_cmd {
+ my ($self, $do, $locked) = @_;
+ if ($do eq "once") {
+ $self->{_run_command} = 'work_once';
+ } elsif ($do eq "onepass") {
+ $self->{_run_command} = 'work_until_done';
+ } else {
+ $self->SUPER::gd_other_cmd($do, $locked);
}
-
- $self->SUPER::gd_other_cmd();
}
sub gd_run {
my $self = shift;
-
- $self->_do_work("work");
+ $SIG{__DIE__} = \&Carp::confess if $self->{debug};
+ $self->_do_work($self->{_run_command});
}
sub _do_work {
@@ -218,6 +224,7 @@ sub _do_work {
my $jq = Bugzilla->job_queue();
$jq->set_verbose($self->{debug});
+ $jq->set_pidfile($self->{gd_pidfile});
foreach my $module (values %{ Bugzilla::JobQueue->job_map() }) {
eval "use $module";
$jq->can_do($module);
diff --git a/Bugzilla/Keyword.pm b/Bugzilla/Keyword.pm
index e2ecc29e5..cda401c59 100644
--- a/Bugzilla/Keyword.pm
+++ b/Bugzilla/Keyword.pm
@@ -27,6 +27,8 @@ use Bugzilla::Util;
#### Initialization ####
###############################
+use constant IS_CONFIG => 1;
+
use constant DB_COLUMNS => qw(
keyworddefs.id
keyworddefs.name
diff --git a/Bugzilla/Mailer.pm b/Bugzilla/Mailer.pm
index 1c4fb6188..381422821 100644
--- a/Bugzilla/Mailer.pm
+++ b/Bugzilla/Mailer.pm
@@ -55,6 +55,7 @@ BEGIN {
$Return::Value::NO_CLUCK = 1;
}
use Email::Send;
+use Sys::Hostname;
sub MessageToMTA {
my ($msg, $send_now) = (@_);
@@ -93,29 +94,6 @@ sub MessageToMTA {
# thus to hopefully avoid auto replies.
$email->header_set('Auto-Submitted', 'auto-generated');
- $email->walk_parts(sub {
- my ($part) = @_;
- return if $part->parts > 1; # Top-level
- my $content_type = $part->content_type || '';
- $content_type =~ /charset=['"](.+)['"]/;
- # If no charset is defined or is the default us-ascii,
- # then we encode the email to UTF-8 if Bugzilla has utf8 enabled.
- # XXX - This is a hack to workaround bug 723944.
- if (!$1 || $1 eq 'us-ascii') {
- my $body = $part->body;
- if (Bugzilla->params->{'utf8'}) {
- $part->charset_set('UTF-8');
- # encoding_set works only with bytes, not with utf8 strings.
- my $raw = $part->body_raw;
- if (utf8::is_utf8($raw)) {
- utf8::encode($raw);
- $part->body_set($raw);
- }
- }
- $part->encoding_set('quoted-printable') if !is_7bit_clean($body);
- }
- });
-
# MIME-Version must be set otherwise some mailsystems ignore the charset
$email->header_set('MIME-Version', '1.0') if !$email->header('MIME-Version');
@@ -140,7 +118,9 @@ sub MessageToMTA {
my $from = $email->header('From');
my ($hostname, @args);
+ my $mailer_class = $method;
if ($method eq "Sendmail") {
+ $mailer_class = 'Bugzilla::Send::Sendmail';
if (ON_WINDOWS) {
$Email::Send::Sendmail::SENDMAIL = SENDMAIL_EXE;
}
@@ -169,6 +149,12 @@ sub MessageToMTA {
}
}
+ # For tracking/diagnostic purposes, add our hostname
+ my $generated_by = $email->header('X-Generated-By') || '';
+ if ($generated_by =~ tr/\/// < 3) {
+ $email->header_set('X-Generated-By' => $generated_by . '/' . hostname() . "($$)");
+ }
+
if ($method eq "SMTP") {
push @args, Host => Bugzilla->params->{"smtpserver"},
username => Bugzilla->params->{"smtp_username"},
@@ -180,6 +166,32 @@ sub MessageToMTA {
Bugzilla::Hook::process('mailer_before_send',
{ email => $email, mailer_args => \@args });
+ # Allow for extensions to to drop the bugmail by clearing the 'to' header
+ return if $email->header('to') eq '';
+
+ $email->walk_parts(sub {
+ my ($part) = @_;
+ return if $part->parts > 1; # Top-level
+ my $content_type = $part->content_type || '';
+ $content_type =~ /charset=['"](.+)['"]/;
+ # If no charset is defined or is the default us-ascii,
+ # then we encode the email to UTF-8 if Bugzilla has utf8 enabled.
+ # XXX - This is a hack to workaround bug 723944.
+ if (!$1 || $1 eq 'us-ascii') {
+ my $body = $part->body;
+ if (Bugzilla->params->{'utf8'}) {
+ $part->charset_set('UTF-8');
+ # encoding_set works only with bytes, not with utf8 strings.
+ my $raw = $part->body_raw;
+ if (utf8::is_utf8($raw)) {
+ utf8::encode($raw);
+ $part->body_set($raw);
+ }
+ }
+ $part->encoding_set('quoted-printable') if !is_7bit_clean($body);
+ }
+ });
+
if ($method eq "Test") {
my $filename = bz_locations()->{'datadir'} . '/mailer.testfile';
open TESTFILE, '>>', $filename;
@@ -190,7 +202,7 @@ sub MessageToMTA {
else {
# This is useful for both Sendmail and Qmail, so we put it out here.
local $ENV{PATH} = SENDMAIL_PATH;
- my $mailer = Email::Send->new({ mailer => $method,
+ my $mailer = Email::Send->new({ mailer => $mailer_class,
mailer_args => \@args });
my $retval = $mailer->send($email);
ThrowCodeError('mail_send_error', { msg => $retval, mail => $email })
diff --git a/Bugzilla/Memcached.pm b/Bugzilla/Memcached.pm
new file mode 100644
index 000000000..fdafa0014
--- /dev/null
+++ b/Bugzilla/Memcached.pm
@@ -0,0 +1,490 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Memcached;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla::Error;
+use Bugzilla::Util qw(trick_taint);
+use Scalar::Util qw(blessed);
+use URI::Escape;
+use Encode;
+use Sys::Syslog qw(:DEFAULT);
+
+# memcached keys have a maximum length of 250 bytes
+use constant MAX_KEY_LENGTH => 250;
+
+sub _new {
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+ my $self = {};
+
+ # always return an object to simplify calling code when memcached is
+ # disabled.
+ if (Bugzilla->feature('memcached')
+ && Bugzilla->params->{memcached_servers})
+ {
+ require Cache::Memcached;
+ $self->{namespace} = Bugzilla->params->{memcached_namespace} || '';
+ $self->{memcached} =
+ Cache::Memcached->new({
+ servers => [ split(/[, ]+/, Bugzilla->params->{memcached_servers}) ],
+ namespace => $self->{namespace},
+ });
+ }
+ return bless($self, $class);
+}
+
+sub enabled {
+ return $_[0]->{memcached} ? 1 : 0;
+}
+
+sub set {
+ my ($self, $args) = @_;
+ return unless $self->{memcached};
+
+ # { key => $key, value => $value }
+ if (exists $args->{key}) {
+ $self->_set($args->{key}, $args->{value});
+ }
+
+ # { table => $table, id => $id, name => $name, data => $data }
+ elsif (exists $args->{table} && exists $args->{id} && exists $args->{name}) {
+ # For caching of Bugzilla::Object, we have to be able to clear the
+ # cached values when given either the object's id or name.
+ my ($table, $id, $name, $data) = @$args{qw(table id name data)};
+ $self->_set("$table.id.$id", $data);
+ if (defined $name) {
+ $self->_set("$table.name_id.$name", $id);
+ $self->_set("$table.id_name.$id", $name);
+ }
+ }
+
+ else {
+ ThrowCodeError('params_required', { function => "Bugzilla::Memcached::set",
+ params => [ 'key', 'table' ] });
+ }
+}
+
+sub get {
+ my ($self, $args) = @_;
+ return unless $self->{memcached};
+
+ # { key => $key }
+ if (exists $args->{key}) {
+ return $self->_get($args->{key});
+ }
+
+ # { table => $table, id => $id }
+ elsif (exists $args->{table} && exists $args->{id}) {
+ my ($table, $id) = @$args{qw(table id)};
+ return $self->_get("$table.id.$id");
+ }
+
+ # { table => $table, name => $name }
+ elsif (exists $args->{table} && exists $args->{name}) {
+ my ($table, $name) = @$args{qw(table name)};
+ return unless my $id = $self->_get("$table.name_id.$name");
+ return $self->_get("$table.id.$id");
+ }
+
+ else {
+ ThrowCodeError('params_required', { function => "Bugzilla::Memcached::get",
+ params => [ 'key', 'table' ] });
+ }
+}
+
+sub set_config {
+ my ($self, $args) = @_;
+ return unless $self->{memcached};
+
+ if (exists $args->{key}) {
+ return $self->_set($self->_config_prefix . '.' . $args->{key}, $args->{data});
+ }
+ else {
+ ThrowCodeError('params_required', { function => "Bugzilla::Memcached::set_config",
+ params => [ 'key' ] });
+ }
+}
+
+sub get_config {
+ my ($self, $args) = @_;
+ return unless $self->{memcached};
+
+ if (exists $args->{key}) {
+ return $self->_get($self->_config_prefix . '.' . $args->{key});
+ }
+ else {
+ ThrowCodeError('params_required', { function => "Bugzilla::Memcached::get_config",
+ params => [ 'key' ] });
+ }
+}
+
+sub clear {
+ my ($self, $args) = @_;
+ return unless $self->{memcached};
+
+ # { key => $key }
+ if (exists $args->{key}) {
+ $self->_delete($args->{key});
+ }
+
+ # { table => $table, id => $id }
+ elsif (exists $args->{table} && exists $args->{id}) {
+ my ($table, $id) = @$args{qw(table id)};
+ my $name = $self->_get("$table.id_name.$id");
+ $self->_delete("$table.id.$id");
+ $self->_delete("$table.name_id.$name") if defined $name;
+ $self->_delete("$table.id_name.$id");
+ }
+
+ # { table => $table, name => $name }
+ elsif (exists $args->{table} && exists $args->{name}) {
+ my ($table, $name) = @$args{qw(table name)};
+ return unless my $id = $self->_get("$table.name_id.$name");
+ $self->_delete("$table.id.$id");
+ $self->_delete("$table.name_id.$name");
+ $self->_delete("$table.id_name.$id");
+ }
+
+ else {
+ ThrowCodeError('params_required', { function => "Bugzilla::Memcached::clear",
+ params => [ 'key', 'table' ] });
+ }
+}
+
+sub clear_all {
+ my ($self) = @_;
+ return unless $self->{memcached};
+ $self->_inc_prefix("global");
+}
+
+sub clear_config {
+ my ($self, $args) = @_;
+ return unless $self->{memcached};
+ if ($args && exists $args->{key}) {
+ $self->_delete($self->_config_prefix . '.' . $args->{key});
+ }
+ else {
+ $self->_inc_prefix("config");
+ }
+}
+
+# in order to clear all our keys, we add a prefix to all our keys. when we
+# need to "clear" all current keys, we increment the prefix.
+sub _prefix {
+ my ($self, $name) = @_;
+ # we don't want to change prefixes in the middle of a request
+ my $request_cache = Bugzilla->request_cache;
+ my $request_cache_key = "memcached_prefix_$name";
+ if (!$request_cache->{$request_cache_key}) {
+ my $memcached = $self->{memcached};
+ my $prefix = $memcached->get($name);
+ if (!$prefix) {
+ $prefix = time();
+ if (!$memcached->add($name, $prefix)) {
+ # if this failed, either another process set the prefix, or
+ # memcached is down. assume we lost the race, and get the new
+ # value. if that fails, memcached is down so use a dummy
+ # prefix for this request.
+ $prefix = $memcached->get($name) || 0;
+ }
+ }
+ $request_cache->{$request_cache_key} = $prefix;
+ }
+ return $request_cache->{$request_cache_key};
+}
+
+sub _inc_prefix {
+ my ($self, $name) = @_;
+ my $memcached = $self->{memcached};
+ if (!$memcached->incr($name, 1)) {
+ $memcached->add($name, time());
+ }
+ delete Bugzilla->request_cache->{"memcached_prefix_$name"};
+
+ # BMO - log that we've wiped the cache
+ openlog('apache', 'cons,pid', 'local4');
+ syslog('notice', encode_utf8("[memcached] $name cache cleared"));
+ closelog();
+}
+
+sub _global_prefix {
+ return $_[0]->_prefix("global");
+}
+
+sub _config_prefix {
+ return $_[0]->_prefix("config");
+}
+
+sub _encode_key {
+ my ($self, $key) = @_;
+ $key = $self->_global_prefix . '.' . uri_escape_utf8($key);
+ return length($self->{namespace} . $key) > MAX_KEY_LENGTH
+ ? undef
+ : $key;
+}
+
+sub _set {
+ my ($self, $key, $value) = @_;
+ if (blessed($value)) {
+ # we don't support blessed objects
+ ThrowCodeError('param_invalid', { function => "Bugzilla::Memcached::set",
+ param => "value" });
+ }
+
+ $key = $self->_encode_key($key)
+ or return;
+ return $self->{memcached}->set($key, $value);
+}
+
+sub _get {
+ my ($self, $key) = @_;
+
+ $key = $self->_encode_key($key)
+ or return;
+ my $value = $self->{memcached}->get($key);
+ return unless defined $value;
+
+ # detaint returned values
+ # hashes and arrays are detainted just one level deep
+ if (ref($value) eq 'HASH') {
+ _detaint_hashref($value);
+ }
+ elsif (ref($value) eq 'ARRAY') {
+ foreach my $value (@$value) {
+ next unless defined $value;
+ # arrays of hashes and arrays are common
+ if (ref($value) eq 'HASH') {
+ _detaint_hashref($value);
+ }
+ elsif (ref($value) eq 'ARRAY') {
+ _detaint_arrayref($value);
+ }
+ elsif (!ref($value)) {
+ trick_taint($value);
+ }
+ }
+ }
+ elsif (!ref($value)) {
+ trick_taint($value);
+ }
+ return $value;
+}
+
+sub _detaint_hashref {
+ my ($hashref) = @_;
+ foreach my $value (values %$hashref) {
+ if (defined($value) && !ref($value)) {
+ trick_taint($value);
+ }
+ }
+}
+
+sub _detaint_arrayref {
+ my ($arrayref) = @_;
+ foreach my $value (@$arrayref) {
+ if (defined($value) && !ref($value)) {
+ trick_taint($value);
+ }
+ }
+}
+
+sub _delete {
+ my ($self, $key) = @_;
+ $key = $self->_encode_key($key)
+ or return;
+ return $self->{memcached}->delete($key);
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Memcached - Interface between Bugzilla and Memcached.
+
+=head1 SYNOPSIS
+
+ use Bugzilla;
+
+ my $memcached = Bugzilla->memcached;
+
+ # grab data from the cache. there is no need to check if memcached is
+ # available or enabled.
+ my $data = $memcached->get({ key => 'data_key' });
+ if (!defined $data) {
+ # not in cache, generate the data and populate the cache for next time
+ $data = some_long_process();
+ $memcached->set({ key => 'data_key', value => $data });
+ }
+ # do something with $data
+
+ # updating the profiles table directly shouldn't be attempted unless you know
+ # what you're doing. if you do update a table directly, you need to clear that
+ # object from memcached.
+ $dbh->do("UPDATE profiles SET request_count=10 WHERE login_name=?", undef, $login);
+ $memcached->clear({ table => 'profiles', name => $login });
+
+=head1 DESCRIPTION
+
+If Memcached is installed and configured, Bugzilla can use it to cache data
+across requests and between webheads. Unlike the request and process caches,
+only scalars, hashrefs, and arrayrefs can be stored in Memcached.
+
+Memcached integration is only required for large installations of Bugzilla --
+if you have multiple webheads then configuring Memcache is recommended.
+
+L<Bugzilla::Memcached> provides an interface to a Memcached server/servers, with
+the ability to get, set, or clear entries from the cache.
+
+The stored value must be an unblessed hashref, unblessed array ref, or a
+scalar. Currently nested data structures are supported but require manual
+de-tainting after reading from Memcached (flat data structures are automatically
+de-tainted).
+
+All values are stored in the Memcached systems using the prefix configured with
+the C<memcached_namespace> parameter, as well as an additional prefix managed
+by this class to allow all values to be cleared when C<checksetup.pl> is
+executed.
+
+Do not create an instance of this object directly, instead use
+L<Bugzilla-E<gt>memcached()|Bugzilla/memcached>.
+
+=head1 METHODS
+
+=over
+
+=item C<enabled>
+
+Returns true if Memcached support is available and enabled.
+
+=back
+
+=head2 Setting
+
+Adds a value to Memcached.
+
+=over
+
+=item C<set({ key =E<gt> $key, value =E<gt> $value })>
+
+Adds the C<value> using the specific C<key>.
+
+=item C<set({ table =E<gt> $table, id =E<gt> $id, name =E<gt> $name, data =E<gt> $data })>
+
+Adds the C<data> using a keys generated from the C<table>, C<id>, and C<name>.
+All three parameters must be provided, however C<name> can be provided but set
+to C<undef>.
+
+This is a convenience method which allows cached data to be later retrieved by
+specifying the C<table> and either the C<id> or C<name>.
+
+=item C<set_config({ key =E<gt> $key, data =E<gt> $data })>
+
+Adds the C<data> using the C<key> while identifying the data as part of
+Bugzilla's configuration (such as fields, products, components, groups, etc).
+Values set with C<set_config> are automatically cleared when changes are made
+to Bugzilla's configuration.
+
+=back
+
+=head2 Getting
+
+Retrieves a value from Memcached. Returns C<undef> if no matching values were
+found in the cache.
+
+=over
+
+=item C<get({ key =E<gt> $key })>
+
+Return C<value> with the specified C<key>.
+
+=item C<get({ table =E<gt> $table, id =E<gt> $id })>
+
+Return C<value> with the specified C<table> and C<id>.
+
+=item C<get({ table =E<gt> $table, name =E<gt> $name })>
+
+Return C<value> with the specified C<table> and C<name>.
+
+=item C<get_config({ key =E<gt> $key })>
+
+Return C<value> with the specified C<key> from the configuration cache. See
+C<set_config> for more information.
+
+=back
+
+=head2 Clearing
+
+Removes the matching value from Memcached.
+
+=over
+
+=item C<clear({ key =E<gt> $key })>
+
+Removes C<value> with the specified C<key>.
+
+=item C<clear({ table =E<gt> $table, id =E<gt> $id })>
+
+Removes C<value> with the specified C<table> and C<id>, as well as the
+corresponding C<table> and C<name> entry.
+
+=item C<clear({ table =E<gt> $table, name =E<gt> $name })>
+
+Removes C<value> with the specified C<table> and C<name>, as well as the
+corresponding C<table> and C<id> entry.
+
+=item C<clear_config({ key =E<gt> $key })>
+
+Remove C<value> with the specified C<key> from the configuration cache. See
+C<set_config> for more information.
+
+=item C<clear_config>
+
+Removes all configuration related values from the cache. See C<set_config> for
+more information.
+
+=item C<clear_all>
+
+Removes all values from the cache.
+
+=back
+
+=head1 Bugzilla::Object CACHE
+
+The main driver for Memcached integration is to allow L<Bugzilla::Object> based
+objects to be automatically cached in Memcache. This is enabled on a
+per-package basis by setting the C<USE_MEMCACHED> constant to any true value.
+
+The current implementation is an opt-in (USE_MEMCACHED is false by default),
+however this will change to opt-out once further testing has been completed
+(USE_MEMCACHED will be true by default).
+
+=head1 DIRECT DATABASE UPDATES
+
+If an object is cached and the database is updated directly (instead of via
+C<$object-E<gt>update()>), then it's possible for the data in the cache to be
+out of sync with the database.
+
+As an example let's consider an extension which adds a timestamp field
+C<last_activitiy_ts> to the profiles table and user object which contains the
+user's last activity. If the extension were to call C<$user-E<gt>update()>,
+then an audit entry would be created for each change to the C<last_activity_ts>
+field, which is undesirable.
+
+To remedy this, the extension updates the table directly. It's critical with
+Memcached that it then clears the cache:
+
+ $dbh->do("UPDATE profiles SET last_activity_ts=? WHERE userid=?",
+ undef, $timestamp, $user_id);
+ Bugzilla->memcached->clear({ table => 'profiles', id => $user_id });
+
diff --git a/Bugzilla/Metrics/Collector.pm b/Bugzilla/Metrics/Collector.pm
new file mode 100644
index 000000000..53cbac819
--- /dev/null
+++ b/Bugzilla/Metrics/Collector.pm
@@ -0,0 +1,171 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Metrics::Collector;
+
+use strict;
+use warnings;
+use 5.10.1;
+
+# the reporter needs to be a constant and use'd here to ensure it's loaded at
+# compile time.
+use constant REPORTER => 'Bugzilla::Metrics::Reporter::ElasticSearch';
+use Bugzilla::Metrics::Reporter::ElasticSearch;
+
+# Debugging reporter
+#use constant REPORTER => 'Bugzilla::Metrics::Reporter::STDERR';
+#use Bugzilla::Metrics::Reporter::STDERR;
+
+use Bugzilla::Constants;
+use Cwd qw(abs_path);
+use File::Basename;
+use Time::HiRes qw(gettimeofday clock_gettime CLOCK_MONOTONIC);
+
+sub new {
+ my ($class, $name) = @_;
+ my $self = {
+ root => undef,
+ head => undef,
+ time => scalar(gettimeofday()),
+ };
+ bless($self, $class);
+ $self->_start_timer({ type => 'main', name => $name });
+ return $self;
+}
+
+sub end {
+ my ($self, $timer) = @_;
+ my $is_head = $timer ? 0 : 1;
+ $timer ||= $self->{head};
+ $timer->{duration} += clock_gettime(CLOCK_MONOTONIC) - $timer->{start_time};
+ $self->{head} = $self->{head}->{parent} if $is_head;
+}
+
+sub cancel {
+ my ($self) = @_;
+ delete $self->{head};
+}
+
+sub DESTROY {
+ my ($self) = @_;
+ $self->finish() if $self->{head};
+}
+
+sub finish {
+ my ($self) = @_;
+ $self->end($self->{root});
+ delete $self->{head};
+
+ my $user = Bugzilla->user;
+ if ($ENV{MOD_PERL}) {
+ require Apache2::RequestUtil;
+ my $request = eval { Apache2::RequestUtil->request };
+ my $headers = $request ? $request->headers_in() : {};
+ $self->{env} = {
+ referer => $headers->{Referer},
+ request_method => $request->method,
+ request_uri => basename($request->unparsed_uri),
+ script_name => $request->uri,
+ user_agent => $headers->{'User-Agent'},
+ };
+ }
+ else {
+ $self->{env} = {
+ referer => $ENV{HTTP_REFERER},
+ request_method => $ENV{REQUEST_METHOD},
+ request_uri => $ENV{REQUEST_URI},
+ script_name => basename($ENV{SCRIPT_NAME}),
+ user_agent => $ENV{HTTP_USER_AGENT},
+ };
+ }
+ $self->{env}->{name} = $self->{root}->{name};
+ $self->{env}->{time} = $self->{time};
+ $self->{env}->{user_id} = $user->id;
+ $self->{env}->{login} = $user->login if $user->id;
+
+ # remove passwords from request_uri
+ $self->{env}->{request_uri} =~ s/\b((?:bugzilla_)?password=)(?:[^&]+|.+$)/$1x/gi;
+
+ $self->report();
+}
+
+sub name {
+ my ($self, $value) = @_;
+ $self->{root}->{name} = $value if defined $value;
+ return $self->{root}->{name};
+}
+
+sub db_start {
+ my ($self) = @_;
+ my $timer = $self->_start_timer({ type => 'db' });
+
+ my @stack;
+ my $i = 0;
+ state $path = quotemeta(abs_path(bz_locations()->{cgi_path}) . '/');
+ while (1) {
+ my @caller = caller($i);
+ last unless @caller;
+ my $file = $caller[1];
+ $file =~ s/^$path//o;
+ push @stack, "$file:$caller[2]"
+ unless substr($file, 0, 16) eq 'Bugzilla/Metrics';
+ last if $file =~ /\.(?:tmpl|cgi)$/;
+ $i++;
+ }
+ $timer->{stack} = \@stack;
+
+ return $timer;
+}
+
+sub template_start {
+ my ($self, $file) = @_;
+ $self->_start_timer({ type => 'tmpl', file => $file });
+}
+
+sub memcached_start {
+ my ($self, $key) = @_;
+ $self->_start_timer({ type => 'memcached', key => $key });
+}
+
+sub memcached_end {
+ my ($self, $hit) = @_;
+ $self->{head}->{result} = $hit ? 'hit' : 'miss';
+ $self->end();
+}
+
+sub resume {
+ my ($self, $timer) = @_;
+ $timer->{start_time} = clock_gettime(CLOCK_MONOTONIC);
+ return $timer;
+}
+
+sub _start_timer {
+ my ($self, $timer) = @_;
+ $timer->{start_time} = $timer->{first_time} = clock_gettime(CLOCK_MONOTONIC);
+ $timer->{duration} = 0;
+ $timer->{children} = [];
+
+ if ($self->{head}) {
+ $timer->{parent} = $self->{head};
+ push @{ $self->{head}->{children} }, $timer;
+ }
+ else {
+ $timer->{parent} = undef;
+ $self->{root} = $timer;
+ }
+ $self->{head} = $timer;
+
+ return $timer;
+}
+
+sub report {
+ my ($self) = @_;
+ my $class = REPORTER;
+ $class->DETACH ? $class->background($self) : $class->foreground($self);
+}
+
+1;
diff --git a/Bugzilla/Metrics/Memcached.pm b/Bugzilla/Metrics/Memcached.pm
new file mode 100644
index 000000000..8dccdb323
--- /dev/null
+++ b/Bugzilla/Metrics/Memcached.pm
@@ -0,0 +1,23 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Metrics::Memcached;
+
+use strict;
+use warnings;
+
+use parent 'Bugzilla::Memcached';
+
+sub _get {
+ my $self = shift;
+ Bugzilla->metrics->memcached_start($_[0]);
+ my $result = $self->SUPER::_get(@_);
+ Bugzilla->metrics->memcached_end($result);
+ return $result;
+}
+
+1;
diff --git a/Bugzilla/Metrics/Mysql.pm b/Bugzilla/Metrics/Mysql.pm
new file mode 100644
index 000000000..7719d1cac
--- /dev/null
+++ b/Bugzilla/Metrics/Mysql.pm
@@ -0,0 +1,148 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Metrics::Mysql;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use parent 'Bugzilla::DB::Mysql';
+
+sub do {
+ my ($self, @args) = @_;
+ Bugzilla->metrics->db_start($args[0]);
+ my $result = $self->SUPER::do(@args);
+ Bugzilla->metrics->end();
+ return $result;
+}
+
+sub selectall_arrayref {
+ my ($self, @args) = @_;
+ Bugzilla->metrics->db_start($args[0]);
+ my $result = $self->SUPER::selectall_arrayref(@args);
+ Bugzilla->metrics->end();
+ return $result;
+}
+
+sub selectall_hashref {
+ my ($self, @args) = @_;
+ Bugzilla->metrics->db_start($args[0]);
+ my $result = $self->SUPER::selectall_hashref(@args);
+ Bugzilla->metrics->end();
+ return $result;
+}
+
+sub selectcol_arrayref {
+ my ($self, @args) = @_;
+ Bugzilla->metrics->db_start($args[0]);
+ my $result = $self->SUPER::selectcol_arrayref(@args);
+ Bugzilla->metrics->end();
+ return $result;
+}
+
+sub selectrow_array {
+ my ($self, @args) = @_;
+ Bugzilla->metrics->db_start($args[0]);
+ my @result = $self->SUPER::selectrow_array(@args);
+ Bugzilla->metrics->end();
+ return wantarray ? @result : $result[0];
+}
+
+sub selectrow_arrayref {
+ my ($self, @args) = @_;
+ Bugzilla->metrics->db_start($args[0]);
+ my $result = $self->SUPER::selectrow_arrayref(@args);
+ Bugzilla->metrics->end();
+ return $result;
+}
+
+sub selectrow_hashref {
+ my ($self, @args) = @_;
+ Bugzilla->metrics->db_start($args[0]);
+ my $result = $self->SUPER::selectrow_hashref(@args);
+ Bugzilla->metrics->end();
+ return $result;
+}
+
+sub commit {
+ my ($self, @args) = @_;
+ Bugzilla->metrics->db_start('COMMIT');
+ my $result = $self->SUPER::commit(@args);
+ Bugzilla->metrics->end();
+ return $result;
+}
+
+sub prepare {
+ my ($self, @args) = @_;
+ my $sth = $self->SUPER::prepare(@args);
+ bless($sth, 'Bugzilla::Metrics::st');
+ return $sth;
+}
+
+package Bugzilla::Metrics::st;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use base 'DBI::st';
+
+sub execute {
+ my ($self, @args) = @_;
+ $self->{private_timer} = Bugzilla->metrics->db_start();
+ my $result = $self->SUPER::execute(@args);
+ Bugzilla->metrics->end();
+ return $result;
+}
+
+sub fetchrow_array {
+ my ($self, @args) = @_;
+ my $timer = $self->{private_timer};
+ Bugzilla->metrics->resume($timer);
+ my @result = $self->SUPER::fetchrow_array(@args);
+ Bugzilla->metrics->end($timer);
+ return wantarray ? @result : $result[0];
+}
+
+sub fetchrow_arrayref {
+ my ($self, @args) = @_;
+ my $timer = $self->{private_timer};
+ Bugzilla->metrics->resume($timer);
+ my $result = $self->SUPER::fetchrow_arrayref(@args);
+ Bugzilla->metrics->end($timer);
+ return $result;
+}
+
+sub fetchrow_hashref {
+ my ($self, @args) = @_;
+ my $timer = $self->{private_timer};
+ Bugzilla->metrics->resume($timer);
+ my $result = $self->SUPER::fetchrow_hashref(@args);
+ Bugzilla->metrics->end($timer);
+ return $result;
+}
+
+sub fetchall_arrayref {
+ my ($self, @args) = @_;
+ my $timer = $self->{private_timer};
+ Bugzilla->metrics->resume($timer);
+ my $result = $self->SUPER::fetchall_arrayref(@args);
+ Bugzilla->metrics->end($timer);
+ return $result;
+}
+
+sub fetchall_hashref {
+ my ($self, @args) = @_;
+ my $timer = $self->{private_timer};
+ Bugzilla->metrics->resume($timer);
+ my $result = $self->SUPER::fetchall_hashref(@args);
+ Bugzilla->metrics->end($timer);
+ return $result;
+}
+
+1;
diff --git a/Bugzilla/Metrics/Reporter.pm b/Bugzilla/Metrics/Reporter.pm
new file mode 100644
index 000000000..88a08c9b8
--- /dev/null
+++ b/Bugzilla/Metrics/Reporter.pm
@@ -0,0 +1,102 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Metrics::Reporter;
+
+use strict;
+use warnings;
+
+use Bugzilla::Constants;
+use File::Slurp;
+use File::Temp;
+use JSON;
+
+# most reporters should detach from the httpd process.
+# reporters which do not detach will block completion of the http response.
+use constant DETACH => 1;
+
+# class method to start the delivery script in the background
+sub background {
+ my ($class, $collector) = @_;
+
+ # we need to remove parent links to avoid looped structures, which
+ # encode_json chokes on
+ _walk_timers($collector->{root}, sub { delete $_[0]->{parent} });
+
+ # serialisation
+ my $json = encode_json({ env => $collector->{env}, times => $collector->{root} });
+
+ # write to temp filename
+ my $fh = File::Temp->new( UNLINK => 0 );
+ if (!$fh) {
+ warn "Failed to create temp file: $!\n";
+ return;
+ }
+ binmode($fh, ':utf8');
+ print $fh $json;
+ close($fh) or die "$fh : $!";
+ my $filename = $fh->filename;
+
+ # spawn delivery worker
+ my $command = bz_locations()->{'cgi_path'} . "/metrics.pl '$class' '$filename' &";
+ $ENV{PATH} = '';
+ system($command);
+}
+
+# run the reporter immediately
+sub foreground {
+ my ($class, $collector) = @_;
+ my $reporter = $class->new({ hashref => { env => $collector->{env}, times => $collector->{root} } });
+ $reporter->report();
+}
+
+sub new {
+ my ($invocant, $args) = @_;
+ my $class = ref($invocant) || $invocant;
+
+ # load from either a json_filename or hashref
+ my $self;
+ if ($args->{json_filename}) {
+ $self = decode_json(read_file($args->{json_filename}, binmode => ':utf8'));
+ unlink($args->{json_filename});
+ }
+ else {
+ $self = $args->{hashref};
+ }
+ bless($self, $class);
+
+ # remove redundant data
+ $self->walk_timers(sub {
+ my ($timer) = @_;
+ $timer->{start_time} = delete $timer->{first_time};
+ delete $timer->{children}
+ if exists $timer->{children} && !scalar(@{ $timer->{children} });
+ });
+
+ return $self;
+}
+
+sub walk_timers {
+ my ($self, $callback) = @_;
+ _walk_timers($self->{times}, $callback, undef);
+}
+
+sub _walk_timers {
+ my ($timer, $callback, $parent) = @_;
+ $callback->($timer, $parent);
+ if (exists $timer->{children}) {
+ foreach my $child (@{ $timer->{children} }) {
+ _walk_timers($child, $callback, $timer);
+ }
+ }
+}
+
+sub report {
+ die "abstract method call";
+}
+
+1;
diff --git a/Bugzilla/Metrics/Reporter/ElasticSearch.pm b/Bugzilla/Metrics/Reporter/ElasticSearch.pm
new file mode 100644
index 000000000..1bab2502c
--- /dev/null
+++ b/Bugzilla/Metrics/Reporter/ElasticSearch.pm
@@ -0,0 +1,104 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Metrics::Reporter::ElasticSearch;
+
+use strict;
+use warnings;
+
+use parent 'Bugzilla::Metrics::Reporter';
+
+use constant DETACH => 1;
+
+sub report {
+ my ($self) = @_;
+
+ # build path array and flatten
+ my @timers;
+ $self->walk_timers(sub {
+ my ($timer, $parent) = @_;
+ $timer->{id} = scalar(@timers);
+ if ($parent) {
+ if (exists $timer->{children}) {
+ if ($timer->{type} eq 'tmpl') {
+ $timer->{node} = 'tmpl: ' . $timer->{file};
+ }
+ elsif ($timer->{type} eq 'db') {
+ $timer->{node} = 'db';
+ }
+ else {
+ $timer->{node} = '?';
+ }
+ }
+ $timer->{path} = [ @{ $parent->{path} }, $parent->{node} ];
+ $timer->{parent} = $parent->{id};
+ }
+ else {
+ $timer->{path} = [ ];
+ $timer->{node} = $timer->{name};
+ }
+ push @timers, $timer;
+ });
+
+ # calculate timer-only durations
+ $self->walk_timers(sub {
+ my ($timer) = @_;
+ my $child_duration = 0;
+ if (exists $timer->{children}) {
+ foreach my $child (@{ $timer->{children} }) {
+ $child_duration += $child->{duration};
+ }
+ }
+ $timer->{this_duration} = $timer->{duration} - $child_duration;
+ });
+
+ # massage each timer
+ my $start_time = $self->{times}->{start_time};
+ foreach my $timer (@timers) {
+ # remove node name and children
+ delete $timer->{node};
+ delete $timer->{children};
+
+ # show relative times
+ $timer->{start_time} = $timer->{start_time} - $start_time;
+ delete $timer->{end_time};
+
+ # show times in ms instead of fractional seconds
+ foreach my $field (qw( start_time duration this_duration )) {
+ $timer->{$field} = sprintf('%.4f', $timer->{$field} * 1000) * 1;
+ }
+ }
+
+ # remove private data from env
+ delete $self->{env}->{user_agent};
+ delete $self->{env}->{referer};
+
+ # throw at ES
+ require ElasticSearch;
+ my $es = ElasticSearch->new(
+ servers => Bugzilla->params->{metrics_elasticsearch_server},
+ transport => 'http',
+ );
+ # the ElasticSearch module queries the server for a list of nodes and
+ # connects directly to a random node. that bypasses our load balancer so we
+ # disable that by setting the server list directly.
+ $es->transport->servers(Bugzilla->params->{metrics_elasticsearch_server});
+ # as the discovered node list is lazy-loaded, increase _refresh_in so it
+ # won't call ->refresh_servers()
+ $es->transport->{_refresh_in} = 1;
+ $es->index(
+ index => Bugzilla->params->{metrics_elasticsearch_index},
+ type => Bugzilla->params->{metrics_elasticsearch_type},
+ ttl => Bugzilla->params->{metrics_elasticsearch_ttl},
+ data => {
+ env => $self->{env},
+ times => \@timers,
+ },
+ );
+}
+
+1;
diff --git a/Bugzilla/Metrics/Reporter/STDERR.pm b/Bugzilla/Metrics/Reporter/STDERR.pm
new file mode 100644
index 000000000..f5bd38acb
--- /dev/null
+++ b/Bugzilla/Metrics/Reporter/STDERR.pm
@@ -0,0 +1,151 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Metrics::Reporter::STDERR;
+
+use strict;
+use warnings;
+
+use parent 'Bugzilla::Metrics::Reporter';
+
+use Data::Dumper;
+
+use constant DETACH => 0;
+
+sub report {
+ my ($self) = @_;
+
+ # count totals
+ $self->{total} = $self->{times}->{duration};
+ $self->{tmpl_count} = $self->{db_count} = $self->{mem_count} = 0;
+ $self->{total_tmpl} = $self->{total_db} = $self->{mem_hits} = 0;
+ $self->{mem_keys} = {};
+ $self->_tally($self->{times});
+
+ # calculate percentages
+ $self->{other} = $self->{total} - $self->{total_tmpl} - $self->{total_db};
+ if ($self->{total} * 1) {
+ $self->{perc_tmpl} = $self->{total_tmpl} / $self->{total} * 100;
+ $self->{perc_db} = $self->{total_db} / $self->{total} * 100;
+ $self->{perc_other} = $self->{other} / $self->{total} * 100;
+ } else {
+ $self->{perc_tmpl} = 0;
+ $self->{perc_db} = 0;
+ $self->{perc_other} = 0;
+ }
+ if ($self->{mem_count}) {
+ $self->{perc_mem} = $self->{mem_hits} / $self->{mem_count} * 100;
+ } else {
+ $self->{perm_mem} = 0;
+ }
+
+ # convert to ms and format
+ foreach my $key (qw( total total_tmpl total_db other )) {
+ $self->{$key} = sprintf("%.4f", $self->{$key} * 1000);
+ }
+ foreach my $key (qw( perc_tmpl perc_db perc_other perc_mem )) {
+ $self->{$key} = sprintf("%.1f", $self->{$key});
+ }
+
+ # massage each timer
+ my $start_time = $self->{times}->{start_time};
+ $self->walk_timers(sub {
+ my ($timer) = @_;
+ delete $timer->{parent};
+
+ # show relative times
+ $timer->{start_time} = $timer->{start_time} - $start_time;
+ delete $timer->{end_time};
+
+ # show times in ms instead of fractional seconds
+ foreach my $field (qw( start_time duration duration_this )) {
+ $timer->{$field} = sprintf('%.4f', $timer->{$field} * 1000) * 1
+ if exists $timer->{$field};
+ }
+ });
+
+ if (0) {
+ # dump timers to stderr
+ local $Data::Dumper::Indent = 1;
+ local $Data::Dumper::Terse = 1;
+ local $Data::Dumper::Sortkeys = sub {
+ my ($rh) = @_;
+ return [ sort { $b cmp $a } keys %$rh ];
+ };
+ print STDERR Dumper($self->{env});
+ print STDERR Dumper($self->{times});
+ }
+
+ # summary summary table too
+ print STDERR <<EOF;
+total time: $self->{total}
+ tmpl time: $self->{total_tmpl} ($self->{perc_tmpl}%) $self->{tmpl_count} hits
+ db time: $self->{total_db} ($self->{perc_db}%) $self->{db_count} hits
+other time: $self->{other} ($self->{perc_other}%)
+ memcached: $self->{perc_mem}% ($self->{mem_count} requests)
+EOF
+ my $tmpls = $self->{tmpl};
+ my $len = 0;
+ foreach my $file (keys %$tmpls) {
+ $len = length($file) if length($file) > $len;
+ }
+ foreach my $file (sort { $tmpls->{$b}->{count} <=> $tmpls->{$a}->{count} } keys %$tmpls) {
+ my $tmpl = $tmpls->{$file};
+ printf STDERR
+ "%${len}s: %2s hits %8.4f total %8.4f avg\n",
+ $file,
+ $tmpl->{count},
+ $tmpl->{duration} * 1000,
+ $tmpl->{duration} * 1000 / $tmpl->{count}
+ ;
+ }
+ my $keys = $self->{mem_keys};
+ $len = 0;
+ foreach my $key (keys %$keys) {
+ $len = length($key) if length($key) > $len;
+ }
+ foreach my $key (sort { $keys->{$a} <=> $keys->{$b} or $a cmp $b } keys %$keys) {
+ printf STDERR "%${len}s: %s\n", $key, $keys->{$key};
+ }
+}
+
+sub _tally {
+ my ($self, $timer) = @_;
+ if (exists $timer->{children}) {
+ foreach my $child (@{ $timer->{children} }) {
+ $self->_tally($child);
+ }
+ }
+
+ if ($timer->{type} eq 'db') {
+ $timer->{duration_this} = $timer->{duration};
+ $self->{total_db} += $timer->{duration};
+ $self->{db_count}++;
+
+ } elsif ($timer->{type} eq 'tmpl') {
+ my $child_duration = 0;
+ if (exists $timer->{children}) {
+ foreach my $child (@{ $timer->{children} }) {
+ $child_duration += $child->{duration};
+ }
+ }
+ $timer->{duration_this} = $timer->{duration} - $child_duration;
+
+ $self->{total_tmpl} += $timer->{duration} - $child_duration;
+ $self->{tmpl_count}++;
+ $self->{tmpl}->{$timer->{file}}->{count}++;
+ $self->{tmpl}->{$timer->{file}}->{duration} += $timer->{duration};
+
+ } elsif ($timer->{type} eq 'memcached') {
+ $timer->{duration_this} = $timer->{duration};
+ $self->{mem_count}++;
+ $self->{mem_keys}->{$timer->{key}}++;
+ $self->{mem_hits}++ if $timer->{result} eq 'hit';
+ }
+}
+
+1;
diff --git a/Bugzilla/Metrics/Template.pm b/Bugzilla/Metrics/Template.pm
new file mode 100644
index 000000000..a3b13f85d
--- /dev/null
+++ b/Bugzilla/Metrics/Template.pm
@@ -0,0 +1,23 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Metrics::Template;
+
+use strict;
+use warnings;
+
+use parent 'Bugzilla::Template';
+
+sub process {
+ my $self = shift;
+ Bugzilla->metrics->template_start($_[0]);
+ my $result = $self->SUPER::process(@_);
+ Bugzilla->metrics->end();
+ return $result;
+}
+
+1;
diff --git a/Bugzilla/Metrics/Template/Context.pm b/Bugzilla/Metrics/Template/Context.pm
new file mode 100644
index 000000000..ae8470b3f
--- /dev/null
+++ b/Bugzilla/Metrics/Template/Context.pm
@@ -0,0 +1,29 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Metrics::Template::Context;
+
+use strict;
+use warnings;
+
+use parent 'Bugzilla::Template::Context';
+
+sub process {
+ my $self = shift;
+
+ # we only want to measure files not template blocks
+ if (ref($_[0]) || substr($_[0], -5) ne '.tmpl') {
+ return $self->SUPER::process(@_);
+ }
+
+ Bugzilla->metrics->template_start($_[0]);
+ my $result = $self->SUPER::process(@_);
+ Bugzilla->metrics->end();
+ return $result;
+}
+
+1;
diff --git a/Bugzilla/Migrate.pm b/Bugzilla/Migrate.pm
index ee0dcab95..923fc36a4 100644
--- a/Bugzilla/Migrate.pm
+++ b/Bugzilla/Migrate.pm
@@ -459,8 +459,11 @@ sub translate_value {
}
my $field_obj = $self->bug_fields->{$field};
- if ($field eq 'creation_ts' or $field eq 'delta_ts'
- or ($field_obj and $field_obj->type == FIELD_TYPE_DATETIME))
+ if ($field eq 'creation_ts'
+ or $field eq 'delta_ts'
+ or ($field_obj and
+ ($field_obj->type == FIELD_TYPE_DATETIME
+ or $field_obj->type == FIELD_TYPE_DATE)))
{
$value = trim($value);
return undef if !$value;
@@ -827,7 +830,7 @@ sub _insert_comments {
$self->_do_table_insert('longdescs', \%copy);
$self->debug(" Inserted comment from " . $who->login, 2);
}
- $bug->_sync_fulltext();
+ $bug->_sync_fulltext( update_comments => 1 );
}
sub _insert_history {
diff --git a/Bugzilla/Milestone.pm b/Bugzilla/Milestone.pm
index 92bc2192a..d8fcd79a5 100644
--- a/Bugzilla/Milestone.pm
+++ b/Bugzilla/Milestone.pm
@@ -108,10 +108,12 @@ sub run_create_validators {
sub update {
my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->bz_start_transaction();
my $changes = $self->SUPER::update(@_);
if (exists $changes->{value}) {
- my $dbh = Bugzilla->dbh;
# The milestone value is stored in the bugs table instead of its ID.
$dbh->do('UPDATE bugs SET target_milestone = ?
WHERE target_milestone = ? AND product_id = ?',
@@ -121,7 +123,11 @@ sub update {
$dbh->do('UPDATE products SET defaultmilestone = ?
WHERE id = ? AND defaultmilestone = ?',
undef, ($self->name, $self->product_id, $changes->{value}->[0]));
+ Bugzilla->memcached->clear({ table => 'products', id => $self->product_id });
}
+ $dbh->bz_commit_transaction();
+ Bugzilla->memcached->clear_config();
+
return $changes;
}
@@ -129,6 +135,8 @@ sub remove_from_db {
my $self = shift;
my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
+
# The default milestone cannot be deleted.
if ($self->name eq $self->product->default_milestone) {
ThrowUserError('milestone_is_default', { milestone => $self });
@@ -157,8 +165,9 @@ sub remove_from_db {
Bugzilla->user->id, $timestamp);
}
}
+ $self->SUPER::remove_from_db();
- $dbh->do('DELETE FROM milestones WHERE id = ?', undef, $self->id);
+ $dbh->bz_commit_transaction();
}
################################
diff --git a/Bugzilla/Object.pm b/Bugzilla/Object.pm
index d4574abd2..8a7bba1c5 100644
--- a/Bugzilla/Object.pm
+++ b/Bugzilla/Object.pm
@@ -47,6 +47,15 @@ use constant AUDIT_CREATES => 1;
use constant AUDIT_UPDATES => 1;
use constant AUDIT_REMOVES => 1;
+# When USE_MEMCACHED is true, the class is suitable for serialisation to
+# Memcached. See documentation in Bugzilla::Memcached for more information.
+use constant USE_MEMCACHED => 1;
+
+# When IS_CONFIG is true, the class is used to track seldom changed
+# configuration objects. This includes, but is not limited to, fields, field
+# values, keywords, products, classifications, priorities, severities, etc.
+use constant IS_CONFIG => 0;
+
# This allows the JSON-RPC interface to return Bugzilla::Object instances
# as though they were hashes. In the future, this may be modified to return
# less information.
@@ -59,8 +68,44 @@ sub TO_JSON { return { %{ $_[0] } }; }
sub new {
my $invocant = shift;
my $class = ref($invocant) || $invocant;
- my $object = $class->_init(@_);
- bless($object, $class) if $object;
+ my $param = shift;
+
+ my $object = $class->_object_cache_get($param);
+ return $object if $object;
+
+ my ($data, $set_memcached);
+ if (Bugzilla->memcached->enabled
+ && $class->USE_MEMCACHED
+ && ref($param) eq 'HASH' && $param->{cache})
+ {
+ if (defined $param->{id}) {
+ $data = Bugzilla->memcached->get({
+ table => $class->DB_TABLE,
+ id => $param->{id},
+ });
+ }
+ elsif (defined $param->{name}) {
+ $data = Bugzilla->memcached->get({
+ table => $class->DB_TABLE,
+ name => $param->{name},
+ });
+ }
+ $set_memcached = $data ? 0 : 1;
+ }
+ $data ||= $class->_load_from_db($param);
+
+ if ($data && $set_memcached) {
+ Bugzilla->memcached->set({
+ table => $class->DB_TABLE,
+ id => $data->{$class->ID_FIELD},
+ name => $data->{$class->NAME_FIELD},
+ data => $data,
+ });
+ }
+
+ $object = $class->new_from_hash($data);
+ $class->_object_cache_set($param, $object);
+
return $object;
}
@@ -69,7 +114,7 @@ sub new {
# Bugzilla::Object, make sure that you modify bz_setup_database
# in Bugzilla::DB::Pg appropriately, to add the right LOWER
# index. You can see examples already there.
-sub _init {
+sub _load_from_db {
my $class = shift;
my ($param) = @_;
my $dbh = Bugzilla->dbh;
@@ -82,19 +127,19 @@ sub _init {
if (ref $param eq 'HASH') {
$id = $param->{id};
}
- my $object;
+ my $object_data;
if (defined $id) {
# We special-case if somebody specifies an ID, so that we can
# validate it as numeric.
detaint_natural($id)
|| ThrowCodeError('param_must_be_numeric',
- {function => $class . '::_init'});
+ {function => $class . '::_load_from_db'});
# Too large integers make PostgreSQL crash.
return if $id > MAX_INT_32;
- $object = $dbh->selectrow_hashref(qq{
+ $object_data = $dbh->selectrow_hashref(qq{
SELECT $columns FROM $table
WHERE $id_field = ?}, undef, $id);
} else {
@@ -121,11 +166,123 @@ sub _init {
}
map { trick_taint($_) } @values;
- $object = $dbh->selectrow_hashref(
+ $object_data = $dbh->selectrow_hashref(
"SELECT $columns FROM $table WHERE $condition", undef, @values);
}
+ return $object_data;
+}
- return $object;
+sub new_from_list {
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+ my ($id_list) = @_;
+ my $id_field = $class->ID_FIELD;
+
+ my @detainted_ids;
+ foreach my $id (@$id_list) {
+ detaint_natural($id) ||
+ ThrowCodeError('param_must_be_numeric',
+ {function => $class . '::new_from_list'});
+ # Too large integers make PostgreSQL crash.
+ next if $id > MAX_INT_32;
+ push(@detainted_ids, $id);
+ }
+
+ # We don't do $invocant->match because some classes have
+ # their own implementation of match which is not compatible
+ # with this one. However, match() still needs to have the right $invocant
+ # in order to do $class->DB_TABLE and so on.
+ my $list = match($invocant, { $id_field => \@detainted_ids });
+
+ # BMO: Populate the object cache with bug objects, which helps
+ # inline-history when viewing bugs with dependencies.
+ if ($class eq 'Bugzilla::Bug') {
+ foreach my $object (@$list) {
+ $class->_object_cache_set(
+ { id => $object->id, cache => 1 },
+ $object
+ );
+ }
+ }
+
+ return $list;
+}
+
+sub new_from_hash {
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+ my $object_data = shift || return;
+ $class->_serialisation_keys($object_data);
+ bless($object_data, $class);
+ $object_data->initialize();
+ return $object_data;
+}
+
+sub initialize {
+ # abstract
+}
+
+# Provides a mechanism for objects to be cached in the request_cahce
+
+sub object_cache_get {
+ my ($class, $id) = @_;
+ return $class->_object_cache_get(
+ { id => $id, cache => 1},
+ $class
+ );
+}
+
+sub object_cache_set {
+ my $self = shift;
+ return $self->_object_cache_set(
+ { id => $self->id, cache => 1 },
+ $self
+ );
+}
+
+sub _object_cache_get {
+ my $class = shift;
+ my ($param) = @_;
+ my $cache_key = $class->object_cache_key($param)
+ || return;
+ return Bugzilla->request_cache->{$cache_key};
+}
+
+sub _object_cache_set {
+ my $class = shift;
+ my ($param, $object) = @_;
+ my $cache_key = $class->object_cache_key($param)
+ || return;
+ Bugzilla->request_cache->{$cache_key} = $object;
+}
+
+sub _object_cache_remove {
+ my $class = shift;
+ my ($param, $object) = @_;
+ $param->{cache} = 1;
+ my $cache_key = $class->object_cache_key($param)
+ || return;
+ delete Bugzilla->request_cache->{$cache_key};
+}
+
+sub object_cache_key {
+ my $class = shift;
+ my ($param) = @_;
+ if (ref($param) && $param->{cache} && ($param->{id} || $param->{name})) {
+ $class = blessed($class) if blessed($class);
+ return $class . ',' . ($param->{id} || $param->{name});
+ } else {
+ return;
+ }
+}
+
+# To support serialisation, we need to capture the keys in an object's default
+# hashref.
+sub _serialisation_keys {
+ my ($class, $object) = @_;
+ my $cache = Bugzilla->request_cache->{serialisation_keys} ||= {};
+ $cache->{$class} = [ keys %$object ] if $object && !exists $cache->{$class};
+ return @{ $cache->{$class} };
}
sub check {
@@ -161,28 +318,6 @@ sub check {
return $obj;
}
-sub new_from_list {
- my $invocant = shift;
- my $class = ref($invocant) || $invocant;
- my ($id_list) = @_;
- my $id_field = $class->ID_FIELD;
-
- my @detainted_ids;
- foreach my $id (@$id_list) {
- detaint_natural($id) ||
- ThrowCodeError('param_must_be_numeric',
- {function => $class . '::new_from_list'});
- # Too large integers make PostgreSQL crash.
- next if $id > MAX_INT_32;
- push(@detainted_ids, $id);
- }
- # We don't do $invocant->match because some classes have
- # their own implementation of match which is not compatible
- # with this one. However, match() still needs to have the right $invocant
- # in order to do $class->DB_TABLE and so on.
- return match($invocant, { $id_field => \@detainted_ids });
-}
-
# Note: Future extensions to this could be:
# * Add a MATCH_JOIN constant so that we can join against
# certain other tables for the WHERE criteria.
@@ -228,8 +363,11 @@ sub match {
}
next;
}
-
- $class->_check_field($field, 'match');
+
+ # It's always safe to use the field defined by classes as being
+ # their ID field. In particular, this means that new_from_list()
+ # is exempted from this check.
+ $class->_check_field($field, 'match') unless $field eq $class->ID_FIELD;
if (ref $value eq 'ARRAY') {
# IN () is invalid SQL, and if we have an empty list
@@ -263,23 +401,46 @@ sub _do_list_select {
my $cols = join(',', $class->_get_db_columns);
my $order = $class->LIST_ORDER;
- my $sql = "SELECT $cols FROM $table";
- if (defined $where) {
- $sql .= " WHERE $where ";
+ # Unconditional requests for configuration data are cacheable.
+ my ($objects, $set_memcached, $memcached_key);
+ if (!defined $where
+ && Bugzilla->memcached->enabled
+ && $class->IS_CONFIG)
+ {
+ $memcached_key = "$class:get_all";
+ $objects = Bugzilla->memcached->get_config({ key => $memcached_key });
+ $set_memcached = $objects ? 0 : 1;
}
- $sql .= " ORDER BY $order";
-
- $sql .= " $postamble" if $postamble;
-
- my $dbh = Bugzilla->dbh;
- # Sometimes the values are tainted, but we don't want to untaint them
- # for the caller. So we copy the array. It's safe to untaint because
- # they're only used in placeholders here.
- my @untainted = @{ $values || [] };
- trick_taint($_) foreach @untainted;
- my $objects = $dbh->selectall_arrayref($sql, {Slice=>{}}, @untainted);
- bless ($_, $class) foreach @$objects;
- return $objects
+
+ if (!$objects) {
+ my $sql = "SELECT $cols FROM $table";
+ if (defined $where) {
+ $sql .= " WHERE $where ";
+ }
+ $sql .= " ORDER BY $order";
+ $sql .= " $postamble" if $postamble;
+
+ my $dbh = Bugzilla->dbh;
+ # Sometimes the values are tainted, but we don't want to untaint them
+ # for the caller. So we copy the array. It's safe to untaint because
+ # they're only used in placeholders here.
+ my @untainted = @{ $values || [] };
+ trick_taint($_) foreach @untainted;
+ $objects = $dbh->selectall_arrayref($sql, {Slice=>{}}, @untainted);
+ $class->_serialisation_keys($objects->[0]) if @$objects;
+ }
+
+ if ($objects && $set_memcached) {
+ Bugzilla->memcached->set_config({
+ key => $memcached_key,
+ data => $objects
+ });
+ }
+
+ foreach my $object (@$objects) {
+ $object = $class->new_from_hash($object);
+ }
+ return $objects;
}
###############################
@@ -332,12 +493,17 @@ sub set_all {
my %field_values = %$params;
my @sorted_names = $self->_sort_by_dep(keys %field_values);
+
foreach my $key (@sorted_names) {
# It's possible for one set_ method to delete a key from $params
# for another set method, so if that's happened, we don't call the
# other set method.
next if !exists $field_values{$key};
my $method = "set_$key";
+ if (!$self->can($method)) {
+ my $class = ref($self) || $self;
+ ThrowCodeError("unknown_method", { method => "${class}::${method}" });
+ }
$self->$method($field_values{$key}, \%field_values);
}
Bugzilla::Hook::process('object_end_of_set_all',
@@ -398,6 +564,13 @@ sub update {
$self->audit_log(\%changes) if $self->AUDIT_UPDATES;
$dbh->bz_commit_transaction();
+ if ($self->USE_MEMCACHED && @values) {
+ Bugzilla->memcached->clear({ table => $table, id => $self->id });
+ Bugzilla->memcached->clear_config()
+ if $self->IS_CONFIG;
+ }
+ $self->_object_cache_remove({ id => $self->id });
+ $self->_object_cache_remove({ name => $self->name }) if $self->name;
if (wantarray) {
return (\%changes, $old_self);
@@ -416,6 +589,13 @@ sub remove_from_db {
$self->audit_log(AUDIT_REMOVE) if $self->AUDIT_REMOVES;
$dbh->do("DELETE FROM $table WHERE $id_field = ?", undef, $self->id);
$dbh->bz_commit_transaction();
+ if ($self->USE_MEMCACHED) {
+ Bugzilla->memcached->clear({ table => $table, id => $self->id });
+ Bugzilla->memcached->clear_config()
+ if $self->IS_CONFIG;
+ }
+ $self->_object_cache_remove({ id => $self->id });
+ $self->_object_cache_remove({ name => $self->name }) if $self->name;
undef $self;
}
@@ -450,6 +630,13 @@ sub audit_log {
}
}
+sub flatten_to_hash {
+ my $self = shift;
+ my $class = blessed($self);
+ my %hash = map { $_ => $self->{$_} } $class->_serialisation_keys;
+ return \%hash;
+}
+
###############################
#### Subroutines ######
###############################
@@ -473,6 +660,13 @@ sub create {
my $object = $class->insert_create_data($field_values);
$dbh->bz_commit_transaction();
+ if (Bugzilla->memcached->enabled
+ && $class->USE_MEMCACHED
+ && $class->IS_CONFIG)
+ {
+ Bugzilla->memcached->clear_config();
+ }
+
return $object;
}
@@ -568,7 +762,7 @@ sub insert_create_data {
sub get_all {
my $class = shift;
- return @{$class->_do_list_select()};
+ return @{ $class->_do_list_select() };
}
###############################
@@ -977,6 +1171,17 @@ database matching the parameters you passed in.
=back
+=item C<initialize>
+
+=over
+
+=item B<Description>
+
+Abstract method to allow subclasses to perform initialization tasks after an
+object has been created.
+
+=back
+
=item C<check>
=over
@@ -1267,6 +1472,58 @@ Returns C<1> if the passed-in value is true, C<0> otherwise.
=back
+=head2 CACHE FUNCTIONS
+
+=over
+
+=item C<object_cache_get>
+
+=over
+
+=item B<Description>
+
+Class function which returns an object from the object-cache for the provided
+C<$id>.
+
+=item B<Params>
+
+Takes an integer C<$id> of the object to retrieve.
+
+=item B<Returns>
+
+Returns the object from the cache if found, otherwise returns C<undef>.
+
+=item B<Example>
+
+my $bug_from_cache = Bugzilla::Bug->object_cache_get(35);
+
+=back
+
+=item C<object_cache_set>
+
+=over
+
+=item B<Description>
+
+Object function which injects the object into the object-cache, using the
+object's C<id> as the key.
+
+=item B<Params>
+
+(none)
+
+=item B<Returns>
+
+(nothing)
+
+=item B<Example>
+
+$bug->object_cache_set();
+
+=back
+
+=back
+
=head1 CLASS FUNCTIONS
=over
diff --git a/Bugzilla/PatchReader.pm b/Bugzilla/PatchReader.pm
new file mode 100644
index 000000000..3b1ba3dda
--- /dev/null
+++ b/Bugzilla/PatchReader.pm
@@ -0,0 +1,117 @@
+package Bugzilla::PatchReader;
+
+use strict;
+
+=head1 NAME
+
+PatchReader - Utilities to read and manipulate patches and CVS
+
+=head1 SYNOPSIS
+
+ # Script that reads in a patch (in any known format), and prints
+ # out some information about it. Other common operations are
+ # outputting the patch in a raw unified diff format, outputting
+ # the patch information to Template::Toolkit templates, adding
+ # context to a patch from CVS, and narrowing the patch down to
+ # apply only to a single file or set of files.
+
+ use PatchReader::Raw;
+ use PatchReader::PatchInfoGrabber;
+ my $filename = 'filename.patch';
+
+ # Create the reader that parses the patch and the object that
+ # extracts info from the reader's datastream
+ my $reader = new PatchReader::Raw();
+ my $patch_info_grabber = new PatchReader::PatchInfoGrabber();
+ $reader->sends_data_to($patch_info_grabber);
+
+ # Iterate over the file
+ $reader->iterate_file($filename);
+
+ # Print the output
+ my $patch_info = $patch_info_grabber->patch_info();
+ print "Summary of Changed Files:\n";
+ while (my ($file, $info) = each %{$patch_info->{files}}) {
+ print "$file: +$info->{plus_lines} -$info->{minus_lines}\n";
+ }
+
+=head1 ABSTRACT
+
+This perl library allows you to manipulate patches programmatically by
+chaining together a variety of objects that read, manipulate, and output
+patch information:
+
+=over
+
+=item PatchReader::Raw
+
+Parse a patch in any format known to this author (unified, normal, cvs diff,
+among others)
+
+=item PatchReader::PatchInfoGrabber
+
+Grab summary info for sections of a patch in a nice hash
+
+=item PatchReader::AddCVSContext
+
+Add context to the patch by grabbing the original files from CVS
+
+=item PatchReader::NarrowPatch
+
+Narrow a patch down to only apply to a specific set of files
+
+=item PatchReader::DiffPrinter::raw
+
+Output the parsed patch in raw unified diff format
+
+=item PatchReader::DiffPrinter::template
+
+Output the parsed patch to L<Template::Toolkit> templates (can be used to make
+HTML output or anything else you please)
+
+=back
+
+Additionally, it is designed so that you can plug in your own objects that
+read the parsed data while it is being parsed (no need for the performance or
+memory problems that can come from reading in the entire patch all at once).
+You can do this by mimicking one of the existing readers (such as
+PatchInfoGrabber) and overriding the methods start_patch, start_file, section,
+end_file and end_patch.
+
+=head1 AUTHORS
+
+ John Keiser <jkeiser@cpan.org>
+ Teemu Mannermaa <tmannerm@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+ Copyright (C) 2003-2004, John Keiser and
+ Copyright (C) 2011-2012, Teemu Mannermaa.
+
+This module is free software; you can redistribute it and/or modify it under
+the terms of the Artistic License 1.0. For details, see the full text of the
+license at
+ <http://www.perlfoundation.org/artistic_license_1_0>.
+
+This module is distributed in the hope that it will be useful, but it is
+provided "as is" and without any warranty; without even the implied warranty
+of merchantability or fitness for a particular purpose.
+
+Files with different licenses or copyright holders:
+
+=over
+
+=item F<lib/PatchReader/CVSClient.pm>
+
+Portions created by Netscape are
+Copyright (C) 2003, Netscape Communications Corporation. All rights reserved.
+
+This file is subject to the terms of the Mozilla Public License, v. 2.0.
+
+=back
+
+=cut
+
+$Bugzilla::PatchReader::VERSION = '0.9.7';
+
+1
diff --git a/Bugzilla/PatchReader/AddCVSContext.pm b/Bugzilla/PatchReader/AddCVSContext.pm
new file mode 100644
index 000000000..910e45669
--- /dev/null
+++ b/Bugzilla/PatchReader/AddCVSContext.pm
@@ -0,0 +1,226 @@
+package Bugzilla::PatchReader::AddCVSContext;
+
+use Bugzilla::PatchReader::FilterPatch;
+use Bugzilla::PatchReader::CVSClient;
+use Cwd;
+use File::Temp;
+
+use strict;
+
+@Bugzilla::PatchReader::AddCVSContext::ISA = qw(Bugzilla::PatchReader::FilterPatch);
+
+# XXX If you need to, get the entire patch worth of files and do a single
+# cvs update of all files as soon as you find a file where you need to do a
+# cvs update, to avoid the significant connect overhead
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = $class->SUPER::new();
+ bless $this, $class;
+
+ $this->{CONTEXT} = $_[0];
+ $this->{CVSROOT} = $_[1];
+
+ return $this;
+}
+
+sub my_rmtree {
+ my ($this, $dir) = @_;
+ foreach my $file (glob("$dir/*")) {
+ if (-d $file) {
+ $this->my_rmtree($file);
+ } else {
+ trick_taint($file);
+ unlink $file;
+ }
+ }
+ trick_taint($dir);
+ rmdir $dir;
+}
+
+sub end_patch {
+ my $this = shift;
+ if (exists($this->{TMPDIR})) {
+ # Set as variable to get rid of taint
+ # One would like to use rmtree here, but that is not taint-safe.
+ $this->my_rmtree($this->{TMPDIR});
+ }
+}
+
+sub start_file {
+ my $this = shift;
+ my ($file) = @_;
+ $this->{HAS_CVS_CONTEXT} = !$file->{is_add} && !$file->{is_remove} &&
+ $file->{old_revision};
+ $this->{REVISION} = $file->{old_revision};
+ $this->{FILENAME} = $file->{filename};
+ $this->{SECTION_END} = -1;
+ $this->{TARGET}->start_file(@_) if $this->{TARGET};
+}
+
+sub end_file {
+ my $this = shift;
+ $this->flush_section();
+
+ if ($this->{FILE}) {
+ close $this->{FILE};
+ unlink $this->{FILE}; # If it fails, it fails ...
+ delete $this->{FILE};
+ }
+ $this->{TARGET}->end_file(@_) if $this->{TARGET};
+}
+
+sub next_section {
+ my $this = shift;
+ my ($section) = @_;
+ $this->{NEXT_PATCH_LINE} = $section->{old_start};
+ $this->{NEXT_NEW_LINE} = $section->{new_start};
+ foreach my $line (@{$section->{lines}}) {
+ # If this is a line requiring context ...
+ if ($line =~ /^[-\+]/) {
+ # Determine how much context is needed for both the previous section line
+ # and this one:
+ # - If there is no old line, start new section
+ # - If this is file context, add (old section end to new line) context to
+ # the existing section
+ # - If old end context line + 1 < new start context line, there is an empty
+ # space and therefore we end the old section and start the new one
+ # - Else we add (old start context line through new line) context to
+ # existing section
+ if (! exists($this->{SECTION})) {
+ $this->_start_section();
+ } elsif ($this->{CONTEXT} eq "file") {
+ $this->push_context_lines($this->{SECTION_END} + 1,
+ $this->{NEXT_PATCH_LINE} - 1);
+ } else {
+ my $start_context = $this->{NEXT_PATCH_LINE} - $this->{CONTEXT};
+ $start_context = $start_context > 0 ? $start_context : 0;
+ if (($this->{SECTION_END} + $this->{CONTEXT} + 1) < $start_context) {
+ $this->flush_section();
+ $this->_start_section();
+ } else {
+ $this->push_context_lines($this->{SECTION_END} + 1,
+ $this->{NEXT_PATCH_LINE} - 1);
+ }
+ }
+ push @{$this->{SECTION}{lines}}, $line;
+ if (substr($line, 0, 1) eq "+") {
+ $this->{SECTION}{plus_lines}++;
+ $this->{SECTION}{new_lines}++;
+ $this->{NEXT_NEW_LINE}++;
+ } else {
+ $this->{SECTION_END}++;
+ $this->{SECTION}{minus_lines}++;
+ $this->{SECTION}{old_lines}++;
+ $this->{NEXT_PATCH_LINE}++;
+ }
+ } else {
+ $this->{NEXT_PATCH_LINE}++;
+ $this->{NEXT_NEW_LINE}++;
+ }
+ # If this is context, for now lose it (later we should try and determine if
+ # we can just use it instead of pulling the file all the time)
+ }
+}
+
+sub determine_start {
+ my ($this, $line) = @_;
+ return 0 if $line < 0;
+ if ($this->{CONTEXT} eq "file") {
+ return 1;
+ } else {
+ my $start = $line - $this->{CONTEXT};
+ $start = $start > 0 ? $start : 1;
+ return $start;
+ }
+}
+
+sub _start_section {
+ my $this = shift;
+
+ # Add the context to the beginning
+ $this->{SECTION}{old_start} = $this->determine_start($this->{NEXT_PATCH_LINE});
+ $this->{SECTION}{new_start} = $this->determine_start($this->{NEXT_NEW_LINE});
+ $this->{SECTION}{old_lines} = 0;
+ $this->{SECTION}{new_lines} = 0;
+ $this->{SECTION}{minus_lines} = 0;
+ $this->{SECTION}{plus_lines} = 0;
+ $this->{SECTION_END} = $this->{SECTION}{old_start} - 1;
+ $this->push_context_lines($this->{SECTION}{old_start},
+ $this->{NEXT_PATCH_LINE} - 1);
+}
+
+sub flush_section {
+ my $this = shift;
+
+ if ($this->{SECTION}) {
+ # Add the necessary context to the end
+ if ($this->{CONTEXT} eq "file") {
+ $this->push_context_lines($this->{SECTION_END} + 1, "file");
+ } else {
+ $this->push_context_lines($this->{SECTION_END} + 1,
+ $this->{SECTION_END} + $this->{CONTEXT});
+ }
+ # Send the section and line notifications
+ $this->{TARGET}->next_section($this->{SECTION}) if $this->{TARGET};
+ delete $this->{SECTION};
+ $this->{SECTION_END} = 0;
+ }
+}
+
+sub push_context_lines {
+ my $this = shift;
+ # Grab from start to end
+ my ($start, $end) = @_;
+ return if $end ne "file" && $start > $end;
+
+ # If it's an added / removed file, don't do anything
+ return if ! $this->{HAS_CVS_CONTEXT};
+
+ # Get and open the file if necessary
+ if (!$this->{FILE}) {
+ my $olddir = getcwd();
+ if (! exists($this->{TMPDIR})) {
+ $this->{TMPDIR} = File::Temp::tempdir();
+ if (! -d $this->{TMPDIR}) {
+ die "Could not get temporary directory";
+ }
+ }
+ chdir($this->{TMPDIR}) or die "Could not cd $this->{TMPDIR}";
+ if (Bugzilla::PatchReader::CVSClient::cvs_co_rev($this->{CVSROOT}, $this->{REVISION}, $this->{FILENAME})) {
+ die "Could not check out $this->{FILENAME} r$this->{REVISION} from $this->{CVSROOT}";
+ }
+ open my $fh, $this->{FILENAME} or die "Could not open $this->{FILENAME}";
+ $this->{FILE} = $fh;
+ $this->{NEXT_FILE_LINE} = 1;
+ trick_taint($olddir); # $olddir comes from getcwd()
+ chdir($olddir) or die "Could not cd back to $olddir";
+ }
+
+ # Read through the file to reach the line we need
+ die "File read too far!" if $this->{NEXT_FILE_LINE} && $this->{NEXT_FILE_LINE} > $start;
+ my $fh = $this->{FILE};
+ while ($this->{NEXT_FILE_LINE} < $start) {
+ my $dummy = <$fh>;
+ $this->{NEXT_FILE_LINE}++;
+ }
+ my $i = $start;
+ for (; $end eq "file" || $i <= $end; $i++) {
+ my $line = <$fh>;
+ last if !defined($line);
+ $line =~ s/\r\n/\n/g;
+ push @{$this->{SECTION}{lines}}, " $line";
+ $this->{NEXT_FILE_LINE}++;
+ $this->{SECTION}{old_lines}++;
+ $this->{SECTION}{new_lines}++;
+ }
+ $this->{SECTION_END} = $i - 1;
+}
+
+sub trick_taint {
+ $_[0] =~ /^(.*)$/s;
+ $_[0] = $1;
+ return (defined($_[0]));
+}
+
+1;
diff --git a/Bugzilla/PatchReader/Base.pm b/Bugzilla/PatchReader/Base.pm
new file mode 100644
index 000000000..f2fd69a68
--- /dev/null
+++ b/Bugzilla/PatchReader/Base.pm
@@ -0,0 +1,23 @@
+package Bugzilla::PatchReader::Base;
+
+use strict;
+
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = {};
+ bless $this, $class;
+
+ return $this;
+}
+
+sub sends_data_to {
+ my $this = shift;
+ if (defined($_[0])) {
+ $this->{TARGET} = $_[0];
+ } else {
+ return $this->{TARGET};
+ }
+}
+
+1
diff --git a/Bugzilla/PatchReader/CVSClient.pm b/Bugzilla/PatchReader/CVSClient.pm
new file mode 100644
index 000000000..2f76fc18d
--- /dev/null
+++ b/Bugzilla/PatchReader/CVSClient.pm
@@ -0,0 +1,48 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::PatchReader::CVSClient;
+
+use strict;
+
+sub parse_cvsroot {
+ my $cvsroot = $_[0];
+ # Format: :method:[user[:password]@]server[:[port]]/path
+ if ($cvsroot =~ /^:([^:]*):(.*?)(\/.*)$/) {
+ my %retval;
+ $retval{protocol} = $1;
+ $retval{rootdir} = $3;
+ my $remote = $2;
+ if ($remote =~ /^(([^\@:]*)(:([^\@]*))?\@)?([^:]*)(:(.*))?$/) {
+ $retval{user} = $2;
+ $retval{password} = $4;
+ $retval{server} = $5;
+ $retval{port} = $7;
+ return %retval;
+ }
+ }
+
+ return (
+ rootdir => $cvsroot
+ );
+}
+
+sub cvs_co {
+ my ($cvsroot, @files) = @_;
+ my $cvs = $::cvsbin || "cvs";
+ return system($cvs, "-Q", "-d$cvsroot", "co", @files);
+}
+
+sub cvs_co_rev {
+ my ($cvsroot, $rev, @files) = @_;
+ my $cvs = $::cvsbin || "cvs";
+ return system($cvs, "-Q", "-d$cvsroot", "co", "-r$rev", @files);
+}
+
+1
diff --git a/Bugzilla/PatchReader/DiffPrinter/raw.pm b/Bugzilla/PatchReader/DiffPrinter/raw.pm
new file mode 100644
index 000000000..ceb425800
--- /dev/null
+++ b/Bugzilla/PatchReader/DiffPrinter/raw.pm
@@ -0,0 +1,61 @@
+package Bugzilla::PatchReader::DiffPrinter::raw;
+
+use strict;
+
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = {};
+ bless $this, $class;
+
+ $this->{OUTFILE} = @_ ? $_[0] : *STDOUT;
+ my $fh = $this->{OUTFILE};
+
+ return $this;
+}
+
+sub start_patch {
+}
+
+sub end_patch {
+}
+
+sub start_file {
+ my $this = shift;
+ my ($file) = @_;
+
+ my $fh = $this->{OUTFILE};
+ if ($file->{rcs_filename}) {
+ print $fh "Index: $file->{filename}\n";
+ print $fh "===================================================================\n";
+ print $fh "RCS file: $file->{rcs_filename}\n";
+ }
+ my $old_file = $file->{is_add} ? "/dev/null" : $file->{filename};
+ my $old_date = $file->{old_date_str} || "";
+ print $fh "--- $old_file\t$old_date";
+ print $fh "\t$file->{old_revision}" if $file->{old_revision};
+ print $fh "\n";
+ my $new_file = $file->{is_remove} ? "/dev/null" : $file->{filename};
+ my $new_date = $file->{new_date_str} || "";
+ print $fh "+++ $new_file\t$new_date";
+ print $fh "\t$file->{new_revision}" if $file->{new_revision};
+ print $fh "\n";
+}
+
+sub end_file {
+}
+
+sub next_section {
+ my $this = shift;
+ my ($section) = @_;
+
+ return unless $section->{old_start} || $section->{new_start};
+ my $fh = $this->{OUTFILE};
+ print $fh "@@ -$section->{old_start},$section->{old_lines} +$section->{new_start},$section->{new_lines} @@ $section->{func_info}\n";
+ foreach my $line (@{$section->{lines}}) {
+ $line =~ s/(\r?\n?)$/\n/;
+ print $fh $line;
+ }
+}
+
+1
diff --git a/Bugzilla/PatchReader/DiffPrinter/template.pm b/Bugzilla/PatchReader/DiffPrinter/template.pm
new file mode 100644
index 000000000..6545e9336
--- /dev/null
+++ b/Bugzilla/PatchReader/DiffPrinter/template.pm
@@ -0,0 +1,119 @@
+package Bugzilla::PatchReader::DiffPrinter::template;
+
+use strict;
+
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = {};
+ bless $this, $class;
+
+ $this->{TEMPLATE_PROCESSOR} = $_[0];
+ $this->{HEADER_TEMPLATE} = $_[1];
+ $this->{FILE_TEMPLATE} = $_[2];
+ $this->{FOOTER_TEMPLATE} = $_[3];
+ $this->{ARGS} = $_[4] || {};
+
+ $this->{ARGS}{file_count} = 0;
+ return $this;
+}
+
+sub start_patch {
+ my $this = shift;
+ $this->{TEMPLATE_PROCESSOR}->process($this->{HEADER_TEMPLATE}, $this->{ARGS})
+ || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error());
+}
+
+sub end_patch {
+ my $this = shift;
+ $this->{TEMPLATE_PROCESSOR}->process($this->{FOOTER_TEMPLATE}, $this->{ARGS})
+ || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error());
+}
+
+sub start_file {
+ my $this = shift;
+ $this->{ARGS}{file_count}++;
+ $this->{ARGS}{file} = shift;
+ $this->{ARGS}{file}{plus_lines} = 0;
+ $this->{ARGS}{file}{minus_lines} = 0;
+ @{$this->{ARGS}{sections}} = ();
+}
+
+sub end_file {
+ my $this = shift;
+ my $file = $this->{ARGS}{file};
+ if ($file->{canonical} && $file->{old_revision} && $this->{ARGS}{bonsai_url}) {
+ $this->{ARGS}{bonsai_prefix} = "$this->{ARGS}{bonsai_url}/cvsblame.cgi?file=$file->{filename}&amp;rev=$file->{old_revision}";
+ }
+ if ($file->{canonical} && $this->{ARGS}{lxr_url}) {
+ # Cut off the lxr root, if any
+ my $filename = $file->{filename};
+ $filename = substr($filename, length($this->{ARGS}{lxr_root}));
+ $this->{ARGS}{lxr_prefix} = "$this->{ARGS}{lxr_url}/source/$filename";
+ }
+
+ $this->{TEMPLATE_PROCESSOR}->process($this->{FILE_TEMPLATE}, $this->{ARGS})
+ || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error());
+ @{$this->{ARGS}{sections}} = ();
+ delete $this->{ARGS}{file};
+}
+
+sub next_section {
+ my $this = shift;
+ my ($section) = @_;
+
+ $this->{ARGS}{file}{plus_lines} += $section->{plus_lines};
+ $this->{ARGS}{file}{minus_lines} += $section->{minus_lines};
+
+ # Get groups of lines and print them
+ my $last_line_char = '';
+ my $context_lines = [];
+ my $plus_lines = [];
+ my $minus_lines = [];
+ foreach my $line (@{$section->{lines}}) {
+ $line =~ s/\r?\n?$//;
+ if ($line =~ /^ /) {
+ if ($last_line_char ne ' ') {
+ push @{$section->{groups}}, {context => $context_lines,
+ plus => $plus_lines,
+ minus => $minus_lines};
+ $context_lines = [];
+ $plus_lines = [];
+ $minus_lines = [];
+ }
+ $last_line_char = ' ';
+ push @{$context_lines}, substr($line, 1);
+ } elsif ($line =~ /^\+/) {
+ if ($last_line_char eq ' ' || $last_line_char eq '-' && @{$plus_lines}) {
+ push @{$section->{groups}}, {context => $context_lines,
+ plus => $plus_lines,
+ minus => $minus_lines};
+ $context_lines = [];
+ $plus_lines = [];
+ $minus_lines = [];
+ $last_line_char = '';
+ }
+ $last_line_char = '+';
+ push @{$plus_lines}, substr($line, 1);
+ } elsif ($line =~ /^-/) {
+ if ($last_line_char eq '+' && @{$minus_lines}) {
+ push @{$section->{groups}}, {context => $context_lines,
+ plus => $plus_lines,
+ minus => $minus_lines};
+ $context_lines = [];
+ $plus_lines = [];
+ $minus_lines = [];
+ $last_line_char = '';
+ }
+ $last_line_char = '-';
+ push @{$minus_lines}, substr($line, 1);
+ }
+ }
+
+ push @{$section->{groups}}, {context => $context_lines,
+ plus => $plus_lines,
+ minus => $minus_lines};
+ push @{$this->{ARGS}{sections}}, $section;
+}
+
+1
diff --git a/Bugzilla/PatchReader/FilterPatch.pm b/Bugzilla/PatchReader/FilterPatch.pm
new file mode 100644
index 000000000..dfe42e750
--- /dev/null
+++ b/Bugzilla/PatchReader/FilterPatch.pm
@@ -0,0 +1,43 @@
+package Bugzilla::PatchReader::FilterPatch;
+
+use strict;
+
+use Bugzilla::PatchReader::Base;
+
+@Bugzilla::PatchReader::FilterPatch::ISA = qw(Bugzilla::PatchReader::Base);
+
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = $class->SUPER::new();
+ bless $this, $class;
+
+ return $this;
+}
+
+sub start_patch {
+ my $this = shift;
+ $this->{TARGET}->start_patch(@_) if $this->{TARGET};
+}
+
+sub end_patch {
+ my $this = shift;
+ $this->{TARGET}->end_patch(@_) if $this->{TARGET};
+}
+
+sub start_file {
+ my $this = shift;
+ $this->{TARGET}->start_file(@_) if $this->{TARGET};
+}
+
+sub end_file {
+ my $this = shift;
+ $this->{TARGET}->end_file(@_) if $this->{TARGET};
+}
+
+sub next_section {
+ my $this = shift;
+ $this->{TARGET}->next_section(@_) if $this->{TARGET};
+}
+
+1
diff --git a/Bugzilla/PatchReader/FixPatchRoot.pm b/Bugzilla/PatchReader/FixPatchRoot.pm
new file mode 100644
index 000000000..e67fb2796
--- /dev/null
+++ b/Bugzilla/PatchReader/FixPatchRoot.pm
@@ -0,0 +1,130 @@
+package Bugzilla::PatchReader::FixPatchRoot;
+
+use Bugzilla::PatchReader::FilterPatch;
+use Bugzilla::PatchReader::CVSClient;
+
+use strict;
+
+@Bugzilla::PatchReader::FixPatchRoot::ISA = qw(Bugzilla::PatchReader::FilterPatch);
+
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = $class->SUPER::new();
+ bless $this, $class;
+
+ my %parsed = Bugzilla::PatchReader::CVSClient::parse_cvsroot($_[0]);
+ $this->{REPOSITORY_ROOT} = $parsed{rootdir};
+ $this->{REPOSITORY_ROOT} .= "/" if substr($this->{REPOSITORY_ROOT}, -1) ne "/";
+
+ return $this;
+}
+
+sub diff_root {
+ my $this = shift;
+ if (@_) {
+ $this->{DIFF_ROOT} = $_[0];
+ } else {
+ return $this->{DIFF_ROOT};
+ }
+}
+
+sub flush_delayed_commands {
+ my $this = shift;
+ return if ! $this->{DELAYED_COMMANDS};
+
+ my $commands = $this->{DELAYED_COMMANDS};
+ delete $this->{DELAYED_COMMANDS};
+ $this->{FORCE_COMMANDS} = 1;
+ foreach my $command_arr (@{$commands}) {
+ my $command = $command_arr->[0];
+ my $arg = $command_arr->[1];
+ if ($command eq "start_file") {
+ $this->start_file($arg);
+ } elsif ($command eq "end_file") {
+ $this->end_file($arg);
+ } elsif ($command eq "section") {
+ $this->next_section($arg);
+ }
+ }
+}
+
+sub end_patch {
+ my $this = shift;
+ $this->flush_delayed_commands();
+ $this->{TARGET}->end_patch(@_) if $this->{TARGET};
+}
+
+sub start_file {
+ my $this = shift;
+ my ($file) = @_;
+ # If the file is new, it will not have a filename that fits the repository
+ # root and therefore needs to be fixed up to have the same root as everyone
+ # else. At the same time we need to fix DIFF_ROOT too.
+ if (exists($this->{DIFF_ROOT})) {
+ # XXX Return error if there are multiple roots in the patch by verifying
+ # that the DIFF_ROOT is not different from the calculated diff root on this
+ # filename
+
+ $file->{filename} = $this->{DIFF_ROOT} . $file->{filename};
+
+ $file->{canonical} = 1;
+ } elsif ($file->{rcs_filename} &&
+ substr($file->{rcs_filename}, 0, length($this->{REPOSITORY_ROOT})) eq
+ $this->{REPOSITORY_ROOT}) {
+ # Since we know the repository we can determine where the user was in the
+ # repository when they did the diff by chopping off the repository root
+ # from the rcs filename
+ $this->{DIFF_ROOT} = substr($file->{rcs_filename},
+ length($this->{REPOSITORY_ROOT}));
+ $this->{DIFF_ROOT} =~ s/,v$//;
+ # If the RCS file exists in the Attic then we need to correct for
+ # this, stripping off the '/Attic' suffix in order to reduce the name
+ # to just the CVS root.
+ if ($this->{DIFF_ROOT} =~ m/Attic/) {
+ $this->{DIFF_ROOT} = substr($this->{DIFF_ROOT}, 0, -6);
+ }
+ # XXX More error checking--that filename exists and that it is in fact
+ # part of the rcs filename
+ $this->{DIFF_ROOT} = substr($this->{DIFF_ROOT}, 0,
+ -length($file->{filename}));
+ $this->flush_delayed_commands();
+
+ $file->{filename} = $this->{DIFF_ROOT} . $file->{filename};
+
+ $file->{canonical} = 1;
+ } else {
+ # DANGER Will Robinson. The first file in the patch is new. We will try
+ # "delayed command mode"
+ #
+ # (if force commands is on we are already in delayed command mode, and sadly
+ # this means the entire patch was unintelligible to us, so we just output
+ # whatever the hell was in the patch)
+
+ if (!$this->{FORCE_COMMANDS}) {
+ push @{$this->{DELAYED_COMMANDS}}, [ "start_file", { %{$file} } ];
+ return;
+ }
+ }
+ $this->{TARGET}->start_file($file) if $this->{TARGET};
+}
+
+sub end_file {
+ my $this = shift;
+ if (exists($this->{DELAYED_COMMANDS})) {
+ push @{$this->{DELAYED_COMMANDS}}, [ "end_file", { %{$_[0]} } ];
+ } else {
+ $this->{TARGET}->end_file(@_) if $this->{TARGET};
+ }
+}
+
+sub next_section {
+ my $this = shift;
+ if (exists($this->{DELAYED_COMMANDS})) {
+ push @{$this->{DELAYED_COMMANDS}}, [ "section", { %{$_[0]} } ];
+ } else {
+ $this->{TARGET}->next_section(@_) if $this->{TARGET};
+ }
+}
+
+1
diff --git a/Bugzilla/PatchReader/NarrowPatch.pm b/Bugzilla/PatchReader/NarrowPatch.pm
new file mode 100644
index 000000000..b6502f2f3
--- /dev/null
+++ b/Bugzilla/PatchReader/NarrowPatch.pm
@@ -0,0 +1,44 @@
+package Bugzilla::PatchReader::NarrowPatch;
+
+use Bugzilla::PatchReader::FilterPatch;
+
+use strict;
+
+@Bugzilla::PatchReader::NarrowPatch::ISA = qw(Bugzilla::PatchReader::FilterPatch);
+
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = $class->SUPER::new();
+ bless $this, $class;
+
+ $this->{INCLUDE_FILES} = [@_];
+
+ return $this;
+}
+
+sub start_file {
+ my $this = shift;
+ my ($file) = @_;
+ if (grep { $_ eq substr($file->{filename}, 0, length($_)) } @{$this->{INCLUDE_FILES}}) {
+ $this->{IS_INCLUDED} = 1;
+ $this->{TARGET}->start_file(@_) if $this->{TARGET};
+ }
+}
+
+sub end_file {
+ my $this = shift;
+ if ($this->{IS_INCLUDED}) {
+ $this->{TARGET}->end_file(@_) if $this->{TARGET};
+ $this->{IS_INCLUDED} = 0;
+ }
+}
+
+sub next_section {
+ my $this = shift;
+ if ($this->{IS_INCLUDED}) {
+ $this->{TARGET}->next_section(@_) if $this->{TARGET};
+ }
+}
+
+1
diff --git a/Bugzilla/PatchReader/PatchInfoGrabber.pm b/Bugzilla/PatchReader/PatchInfoGrabber.pm
new file mode 100644
index 000000000..8c52931ba
--- /dev/null
+++ b/Bugzilla/PatchReader/PatchInfoGrabber.pm
@@ -0,0 +1,45 @@
+package Bugzilla::PatchReader::PatchInfoGrabber;
+
+use Bugzilla::PatchReader::FilterPatch;
+
+use strict;
+
+@Bugzilla::PatchReader::PatchInfoGrabber::ISA = qw(Bugzilla::PatchReader::FilterPatch);
+
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = $class->SUPER::new();
+ bless $this, $class;
+
+ return $this;
+}
+
+sub patch_info {
+ my $this = shift;
+ return $this->{PATCH_INFO};
+}
+
+sub start_patch {
+ my $this = shift;
+ $this->{PATCH_INFO} = {};
+ $this->{TARGET}->start_patch(@_) if $this->{TARGET};
+}
+
+sub start_file {
+ my $this = shift;
+ my ($file) = @_;
+ $this->{PATCH_INFO}{files}{$file->{filename}} = { %{$file} };
+ $this->{FILE} = { %{$file} };
+ $this->{TARGET}->start_file(@_) if $this->{TARGET};
+}
+
+sub next_section {
+ my $this = shift;
+ my ($section) = @_;
+ $this->{PATCH_INFO}{files}{$this->{FILE}{filename}}{plus_lines} += $section->{plus_lines};
+ $this->{PATCH_INFO}{files}{$this->{FILE}{filename}}{minus_lines} += $section->{minus_lines};
+ $this->{TARGET}->next_section(@_) if $this->{TARGET};
+}
+
+1
diff --git a/Bugzilla/PatchReader/Raw.pm b/Bugzilla/PatchReader/Raw.pm
new file mode 100644
index 000000000..b58ed3a2d
--- /dev/null
+++ b/Bugzilla/PatchReader/Raw.pm
@@ -0,0 +1,268 @@
+package Bugzilla::PatchReader::Raw;
+
+#
+# USAGE:
+# use PatchReader::Raw;
+# my $parser = new PatchReader::Raw();
+# $parser->sends_data_to($my_target);
+# $parser->start_lines();
+# open FILE, "mypatch.patch";
+# while (<FILE>) {
+# $parser->next_line($_);
+# }
+# $parser->end_lines();
+#
+
+use strict;
+
+use Bugzilla::PatchReader::Base;
+
+@Bugzilla::PatchReader::Raw::ISA = qw(Bugzilla::PatchReader::Base);
+
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = $class->SUPER::new();
+ bless $this, $class;
+
+ return $this;
+}
+
+# We send these notifications:
+# start_patch({ patchname })
+# start_file({ filename, rcs_filename, old_revision, old_date_str, new_revision, new_date_str, is_add, is_remove })
+# next_section({ old_start, new_start, old_lines, new_lines, @lines })
+# end_patch
+# end_file
+sub next_line {
+ my $this = shift;
+ my ($line) = @_;
+
+ return if $line =~ /^\?/;
+
+ # patch header parsing
+ if ($line =~ /^---\s*([\S ]+)\s*\t([^\t\r\n]*)\s*(\S*)/) {
+ $this->_maybe_end_file();
+
+ if ($1 eq "/dev/null") {
+ $this->{FILE_STATE}{is_add} = 1;
+ } else {
+ $this->{FILE_STATE}{filename} = $1;
+ }
+ $this->{FILE_STATE}{old_date_str} = $2;
+ $this->{FILE_STATE}{old_revision} = $3 if $3;
+
+ $this->{IN_HEADER} = 1;
+
+ } elsif ($line =~ /^\+\+\+\s*([\S ]+)\s*\t([^\t\r\n]*)(\S*)/) {
+ if ($1 eq "/dev/null") {
+ $this->{FILE_STATE}{is_remove} = 1;
+ }
+ $this->{FILE_STATE}{new_date_str} = $2;
+ $this->{FILE_STATE}{new_revision} = $3 if $3;
+
+ $this->{IN_HEADER} = 1;
+
+ } elsif ($line =~ /^RCS file: ([\S ]+)/) {
+ $this->{FILE_STATE}{rcs_filename} = $1;
+
+ $this->{IN_HEADER} = 1;
+
+ } elsif ($line =~ /^retrieving revision (\S+)/) {
+ $this->{FILE_STATE}{old_revision} = $1;
+
+ $this->{IN_HEADER} = 1;
+
+ } elsif ($line =~ /^Index:\s*([\S ]+)/) {
+ $this->_maybe_end_file();
+
+ $this->{FILE_STATE}{filename} = $1;
+
+ $this->{IN_HEADER} = 1;
+
+ } elsif ($line =~ /^diff\s*(-\S+\s*)*(\S+)\s*(\S*)/ && $3) {
+ # Simple diff <dir> <dir>
+ $this->_maybe_end_file();
+ $this->{FILE_STATE}{filename} = $2;
+
+ $this->{IN_HEADER} = 1;
+
+ # section parsing
+ } elsif ($line =~ /^@@\s*-(\d+),?(\d*)\s*\+(\d+),?(\d*)\s*(?:@@\s*(.*))?/) {
+ $this->{IN_HEADER} = 0;
+
+ $this->_maybe_start_file();
+ $this->_maybe_end_section();
+ $2 = 0 if !defined($2);
+ $4 = 0 if !defined($4);
+ $this->{SECTION_STATE} = { old_start => $1, old_lines => $2,
+ new_start => $3, new_lines => $4,
+ func_info => $5,
+ minus_lines => 0, plus_lines => 0,
+ };
+
+ } elsif ($line =~ /^(\d+),?(\d*)([acd])(\d+),?(\d*)/) {
+ # Non-universal diff. Calculate as though it were universal.
+ $this->{IN_HEADER} = 0;
+
+ $this->_maybe_start_file();
+ $this->_maybe_end_section();
+
+ my $old_start;
+ my $old_lines;
+ my $new_start;
+ my $new_lines;
+ if ($3 eq 'a') {
+ # 'a' has the old number one off from diff -u ("insert after this line"
+ # vs. "insert at this line")
+ $old_start = $1 + 1;
+ $old_lines = 0;
+ } else {
+ $old_start = $1;
+ $old_lines = $2 ? ($2 - $1 + 1) : 1;
+ }
+ if ($3 eq 'd') {
+ # 'd' has the new number one off from diff -u ("delete after this line"
+ # vs. "delete at this line")
+ $new_start = $4 + 1;
+ $new_lines = 0;
+ } else {
+ $new_start = $4;
+ $new_lines = $5 ? ($5 - $4 + 1) : 1;
+ }
+
+ $this->{SECTION_STATE} = { old_start => $old_start, old_lines => $old_lines,
+ new_start => $new_start, new_lines => $new_lines,
+ minus_lines => 0, plus_lines => 0
+ };
+ }
+
+ # line parsing (only when inside a section)
+ return if $this->{IN_HEADER};
+ if ($line =~ /^ /) {
+ push @{$this->{SECTION_STATE}{lines}}, $line;
+ } elsif ($line =~ /^-/) {
+ $this->{SECTION_STATE}{minus_lines}++;
+ push @{$this->{SECTION_STATE}{lines}}, $line;
+ } elsif ($line =~ /^\+/) {
+ $this->{SECTION_STATE}{plus_lines}++;
+ push @{$this->{SECTION_STATE}{lines}}, $line;
+ } elsif ($line =~ /^< /) {
+ $this->{SECTION_STATE}{minus_lines}++;
+ push @{$this->{SECTION_STATE}{lines}}, "-" . substr($line, 2);
+ } elsif ($line =~ /^> /) {
+ $this->{SECTION_STATE}{plus_lines}++;
+ push @{$this->{SECTION_STATE}{lines}}, "+" . substr($line, 2);
+ }
+}
+
+sub start_lines {
+ my $this = shift;
+ die "No target specified: call sends_data_to!" if !$this->{TARGET};
+ delete $this->{FILE_STARTED};
+ delete $this->{FILE_STATE};
+ delete $this->{SECTION_STATE};
+ $this->{FILE_NEVER_STARTED} = 1;
+
+ $this->{TARGET}->start_patch(@_);
+}
+
+sub end_lines {
+ my $this = shift;
+ $this->_maybe_end_file();
+ $this->{TARGET}->end_patch(@_);
+}
+
+sub _init_state {
+ my $this = shift;
+ $this->{SECTION_STATE}{minus_lines} ||= 0;
+ $this->{SECTION_STATE}{plus_lines} ||= 0;
+}
+
+sub _maybe_start_file {
+ my $this = shift;
+ $this->_init_state();
+ if (exists($this->{FILE_STATE}) && !$this->{FILE_STARTED} ||
+ $this->{FILE_NEVER_STARTED}) {
+ $this->_start_file();
+ }
+}
+
+sub _maybe_end_file {
+ my $this = shift;
+ $this->_init_state();
+ return if $this->{IN_HEADER};
+
+ $this->_maybe_end_section();
+ if (exists($this->{FILE_STATE})) {
+ # Handle empty patch sections (if the file has not been started and we're
+ # already trying to end it, start it first!)
+ if (!$this->{FILE_STARTED}) {
+ $this->_start_file();
+ }
+
+ # Send end notification and set state
+ $this->{TARGET}->end_file($this->{FILE_STATE});
+ delete $this->{FILE_STATE};
+ delete $this->{FILE_STARTED};
+ }
+}
+
+sub _start_file {
+ my $this = shift;
+
+ # Send start notification and set state
+ if (!$this->{FILE_STATE}) {
+ $this->{FILE_STATE} = { filename => "file_not_specified_in_diff" };
+ }
+
+ # Send start notification and set state
+ $this->{TARGET}->start_file($this->{FILE_STATE});
+ $this->{FILE_STARTED} = 1;
+ delete $this->{FILE_NEVER_STARTED};
+}
+
+sub _maybe_end_section {
+ my $this = shift;
+ if (exists($this->{SECTION_STATE})) {
+ $this->{TARGET}->next_section($this->{SECTION_STATE});
+ delete $this->{SECTION_STATE};
+ }
+}
+
+sub iterate_file {
+ my $this = shift;
+ my ($filename) = @_;
+
+ open FILE, $filename or die "Could not open $filename: $!";
+ $this->start_lines($filename);
+ while (<FILE>) {
+ $this->next_line($_);
+ }
+ $this->end_lines($filename);
+ close FILE;
+}
+
+sub iterate_fh {
+ my $this = shift;
+ my ($fh, $filename) = @_;
+
+ $this->start_lines($filename);
+ while (<$fh>) {
+ $this->next_line($_);
+ }
+ $this->end_lines($filename);
+}
+
+sub iterate_string {
+ my $this = shift;
+ my ($id, $data) = @_;
+
+ $this->start_lines($id);
+ while ($data =~ /([^\n]*(\n|$))/g) {
+ $this->next_line($1);
+ }
+ $this->end_lines($id);
+}
+
+1
diff --git a/Bugzilla/Product.pm b/Bugzilla/Product.pm
index a0079a033..3e74b9afd 100644
--- a/Bugzilla/Product.pm
+++ b/Bugzilla/Product.pm
@@ -41,6 +41,8 @@ use constant DEFAULT_CLASSIFICATION_ID => 1;
#### Initialization ####
###############################
+use constant IS_CONFIG => 1;
+
use constant DB_TABLE => 'products';
use constant DB_COLUMNS => qw(
@@ -107,6 +109,7 @@ sub create {
Bugzilla::Hook::process('product_end_of_create', { product => $product });
$dbh->bz_commit_transaction();
+ Bugzilla->memcached->clear_config();
return $product;
}
@@ -114,7 +117,7 @@ sub create {
# for each product in the list, particularly with hundreds or thousands
# of products.
sub preload {
- my ($products, $preload_flagtypes) = @_;
+ my ($products, $preload_flagtypes, $flagtypes_params) = @_;
my %prods = map { $_->id => $_ } @$products;
my @prod_ids = keys %prods;
return unless @prod_ids;
@@ -132,7 +135,7 @@ sub preload {
}
}
if ($preload_flagtypes) {
- $_->flag_types foreach @$products;
+ $_->flag_types($flagtypes_params) foreach @$products;
}
}
@@ -262,6 +265,7 @@ sub update {
# Changes have been committed.
delete $self->{check_group_controls};
Bugzilla->user->clear_product_cache();
+ Bugzilla->memcached->clear_config();
return $changes;
}
@@ -320,6 +324,7 @@ sub remove_from_db {
$dbh->do("DELETE FROM products WHERE id = ?", undef, $self->id);
$dbh->bz_commit_transaction();
+ Bugzilla->memcached->clear_config();
# We have to delete these internal variables, else we get
# the old lists of products and classifications again.
@@ -779,7 +784,8 @@ sub user_has_access {
}
sub flag_types {
- my $self = shift;
+ my ($self, $params) = @_;
+ $params ||= {};
return $self->{'flag_types'} if defined $self->{'flag_types'};
@@ -787,7 +793,7 @@ sub flag_types {
my $cache = Bugzilla->request_cache->{flag_types_per_product} ||= {};
$self->{flag_types} = {};
my $prod_id = $self->id;
- my $flagtypes = Bugzilla::FlagType::match({ product_id => $prod_id });
+ my $flagtypes = Bugzilla::FlagType::match({ product_id => $prod_id, %$params });
foreach my $type ('bug', 'attachment') {
my @flags = grep { $_->target_type eq $type } @$flagtypes;
@@ -816,8 +822,8 @@ sub flag_types {
sub classification {
my $self = shift;
- $self->{'classification'} ||=
- new Bugzilla::Classification($self->classification_id);
+ $self->{'classification'} ||=
+ new Bugzilla::Classification({ id => $self->classification_id, cache => 1 });
return $self->{'classification'};
}
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm
index 8e419c0ee..86e6764ff 100644
--- a/Bugzilla/Search.pm
+++ b/Bugzilla/Search.pm
@@ -38,7 +38,6 @@ use base qw(Exporter);
@Bugzilla::Search::EXPORT = qw(
IsValidQueryType
split_order_term
- translate_old_column
);
use Bugzilla::Error;
@@ -48,6 +47,7 @@ use Bugzilla::Group;
use Bugzilla::User;
use Bugzilla::Field;
use Bugzilla::Search::Clause;
+use Bugzilla::Search::ClauseGroup;
use Bugzilla::Search::Condition qw(condition);
use Bugzilla::Status;
use Bugzilla::Keyword;
@@ -56,9 +56,10 @@ use Data::Dumper;
use Date::Format;
use Date::Parse;
use Scalar::Util qw(blessed);
-use List::MoreUtils qw(all part uniq);
+use List::MoreUtils qw(all firstidx part uniq);
use POSIX qw(INT_MAX);
use Storable qw(dclone);
+use Time::HiRes qw(gettimeofday tv_interval);
# Description Of Boolean Charts
# -----------------------------
@@ -130,8 +131,14 @@ use Storable qw(dclone);
# Constants #
#############
+# BMO - product aliases for searching
+use constant PRODUCT_ALIASES => {
+ 'Boot2Gecko' => 'Firefox OS',
+};
+
# When doing searches, NULL datetimes are treated as this date.
use constant EMPTY_DATETIME => '1970-01-01 00:00:00';
+use constant EMPTY_DATE => '1970-01-01';
# This is the regex for real numbers from Regexp::Common, modified to be
# more readable.
@@ -182,6 +189,8 @@ use constant OPERATORS => {
changedfrom => \&_changedfrom_changedto,
changedto => \&_changedfrom_changedto,
changedby => \&_changedby,
+ isempty => \&_isempty,
+ isnotempty => \&_isnotempty,
};
# Some operators are really just standard SQL operators, and are
@@ -208,6 +217,8 @@ use constant OPERATOR_REVERSE => {
lessthaneq => 'greaterthan',
greaterthan => 'lessthaneq',
greaterthaneq => 'lessthan',
+ isempty => 'isnotempty',
+ isnotempty => 'isempty',
# The following don't currently have reversals:
# casesubstring, anyexact, allwords, allwordssubstr
};
@@ -223,6 +234,12 @@ use constant NON_NUMERIC_OPERATORS => qw(
notregexp
);
+# These operators ignore the entered value
+use constant NO_VALUE_OPERATORS => qw(
+ isempty
+ isnotempty
+);
+
use constant MULTI_SELECT_OVERRIDE => {
notequals => \&_multiselect_negative,
notregexp => \&_multiselect_negative,
@@ -271,6 +288,9 @@ use constant OPERATOR_FIELD_OVERRIDE => {
'attach_data.thedata' => MULTI_SELECT_OVERRIDE,
# We check all attachment fields against this.
attachments => MULTI_SELECT_OVERRIDE,
+ assignee_last_login => {
+ _default => \&_assignee_last_login,
+ },
blocked => MULTI_SELECT_OVERRIDE,
bug_file_loc => { _non_changed => \&_nullable },
bug_group => MULTI_SELECT_OVERRIDE,
@@ -319,7 +339,8 @@ use constant OPERATOR_FIELD_OVERRIDE => {
_non_changed => \&_product_nonchanged,
},
tag => MULTI_SELECT_OVERRIDE,
-
+ comment_tag => MULTI_SELECT_OVERRIDE,
+
# Timetracking Fields
deadline => { _non_changed => \&_deadline },
percentage_complete => {
@@ -331,11 +352,16 @@ use constant OPERATOR_FIELD_OVERRIDE => {
changedafter => \&_work_time_changedbefore_after,
_default => \&_work_time,
},
+ last_visit_ts => {
+ _non_changed => \&_last_visit_ts,
+ _default => \&_last_visit_ts_invalid_operator,
+ },
# Custom Fields
FIELD_TYPE_FREETEXT, { _non_changed => \&_nullable },
FIELD_TYPE_BUG_ID, { _non_changed => \&_nullable_int },
FIELD_TYPE_DATETIME, { _non_changed => \&_nullable_datetime },
+ FIELD_TYPE_DATE, { _non_changed => \&_nullable_date },
FIELD_TYPE_TEXTAREA, { _non_changed => \&_nullable },
FIELD_TYPE_MULTI_SELECT, MULTI_SELECT_OVERRIDE,
FIELD_TYPE_BUG_URLS, MULTI_SELECT_OVERRIDE,
@@ -343,18 +369,36 @@ use constant OPERATOR_FIELD_OVERRIDE => {
# These are fields where special action is taken depending on the
# *value* passed in to the chart, sometimes.
-use constant SPECIAL_PARSING => {
- # Pronoun Fields (Ones that can accept %user%, etc.)
- assigned_to => \&_contact_pronoun,
- cc => \&_cc_pronoun,
- commenter => \&_commenter_pronoun,
- qa_contact => \&_contact_pronoun,
- reporter => \&_contact_pronoun,
-
- # Date Fields that accept the 1d, 1w, 1m, 1y, etc. format.
- creation_ts => \&_timestamp_translate,
- deadline => \&_timestamp_translate,
- delta_ts => \&_timestamp_translate,
+# This is a sub because custom fields are dynamic
+sub SPECIAL_PARSING {
+ my $map = {
+ # Pronoun Fields (Ones that can accept %user%, etc.)
+ assigned_to => \&_contact_pronoun,
+ cc => \&_cc_pronoun,
+ commenter => \&_commenter_pronoun,
+ qa_contact => \&_contact_pronoun,
+ reporter => \&_contact_pronoun,
+
+ # Date Fields that accept the 1d, 1w, 1m, 1y, etc. format.
+ creation_ts => \&_datetime_translate,
+ deadline => \&_date_translate,
+ delta_ts => \&_datetime_translate,
+
+ # last_visit field that accept both a 1d, 1w, 1m, 1y format and the
+ # %last_changed% pronoun.
+ last_visit_ts => \&_last_visit_datetime,
+
+ # BMO - Add ability to use pronoun for bug mentors field
+ bug_mentor => \&_commenter_pronoun,
+ };
+ foreach my $field (Bugzilla->active_custom_fields) {
+ if ($field->type == FIELD_TYPE_DATETIME) {
+ $map->{$field->name} = \&_datetime_translate;
+ } elsif ($field->type == FIELD_TYPE_DATE) {
+ $map->{$field->name} = \&_date_translate;
+ }
+ }
+ return $map;
};
# Information about fields that represent "users", used by _user_nonchanged.
@@ -387,6 +431,11 @@ use constant USER_FIELDS => {
field => 'setter_id',
join => { table => 'flags' },
},
+ # BMO - Ability to search for bugs with specific mentors
+ 'bug_mentor' => {
+ field => 'user_id',
+ join => { table => 'bug_mentors' },
+ }
};
# Backwards compatibility for times that we changed the names of fields
@@ -424,71 +473,98 @@ use constant COLUMN_DEPENDS => {
# certain columns in the buglist. For the most part, Search.pm uses
# DB::Schema to figure out what needs to be joined, but for some
# fields it needs a little help.
-use constant COLUMN_JOINS => {
- actual_time => {
- table => '(SELECT bug_id, SUM(work_time) AS total'
- . ' FROM longdescs GROUP BY bug_id)',
- join => 'INNER',
- },
- assigned_to => {
- from => 'assigned_to',
- to => 'userid',
- table => 'profiles',
- join => 'INNER',
- },
- reporter => {
- from => 'reporter',
- to => 'userid',
- table => 'profiles',
- join => 'INNER',
- },
- qa_contact => {
- from => 'qa_contact',
- to => 'userid',
- table => 'profiles',
- },
- component => {
- from => 'component_id',
- to => 'id',
- table => 'components',
- join => 'INNER',
- },
- product => {
- from => 'product_id',
- to => 'id',
- table => 'products',
- join => 'INNER',
- },
- classification => {
- table => 'classifications',
- from => 'map_product.classification_id',
- to => 'id',
- join => 'INNER',
- },
- 'flagtypes.name' => {
- as => 'map_flags',
- table => 'flags',
- extra => ['map_flags.attach_id IS NULL'],
- then_to => {
- as => 'map_flagtypes',
- table => 'flagtypes',
- from => 'map_flags.type_id',
+sub COLUMN_JOINS {
+ my $user = Bugzilla->user;
+
+ my $joins = {
+ actual_time => {
+ table => '(SELECT bug_id, SUM(work_time) AS total'
+ . ' FROM longdescs GROUP BY bug_id)',
+ join => 'INNER',
+ },
+ assigned_to => {
+ from => 'assigned_to',
+ to => 'userid',
+ table => 'profiles',
+ join => 'INNER',
+ },
+ assignee_last_login => {
+ as => 'assignee',
+ from => 'assigned_to',
+ to => 'userid',
+ table => 'profiles',
+ join => 'INNER',
+ },
+ reporter => {
+ from => 'reporter',
+ to => 'userid',
+ table => 'profiles',
+ join => 'INNER',
+ },
+ qa_contact => {
+ from => 'qa_contact',
+ to => 'userid',
+ table => 'profiles',
+ },
+ component => {
+ from => 'component_id',
to => 'id',
+ table => 'components',
+ join => 'INNER',
},
- },
- keywords => {
- table => 'keywords',
- then_to => {
- as => 'map_keyworddefs',
- table => 'keyworddefs',
- from => 'map_keywords.keywordid',
+ product => {
+ from => 'product_id',
to => 'id',
+ table => 'products',
+ join => 'INNER',
},
- },
- 'longdescs.count' => {
- table => 'longdescs',
- join => 'INNER',
- },
+ classification => {
+ table => 'classifications',
+ from => 'map_product.classification_id',
+ to => 'id',
+ join => 'INNER',
+ },
+ 'flagtypes.name' => {
+ as => 'map_flags',
+ table => 'flags',
+ extra => ['map_flags.attach_id IS NULL'],
+ then_to => {
+ as => 'map_flagtypes',
+ table => 'flagtypes',
+ from => 'map_flags.type_id',
+ to => 'id',
+ },
+ },
+ keywords => {
+ table => 'keywords',
+ then_to => {
+ as => 'map_keyworddefs',
+ table => 'keyworddefs',
+ from => 'map_keywords.keywordid',
+ to => 'id',
+ },
+ },
+ blocked => {
+ table => 'dependencies',
+ to => 'dependson',
+ },
+ dependson => {
+ table => 'dependencies',
+ to => 'blocked',
+ },
+ 'longdescs.count' => {
+ table => 'longdescs',
+ join => 'INNER',
+ },
+ last_visit_ts => {
+ as => 'bug_user_last_visit',
+ table => 'bug_user_last_visit',
+ extra => ['bug_user_last_visit.user_id = ' . $user->id],
+ from => 'bug_id',
+ to => 'bug_id',
+ },
+ };
+ return $joins;
};
# This constant defines the columns that can be selected in a query
@@ -552,15 +628,20 @@ sub COLUMNS {
. $dbh->sql_string_concat('map_flagtypes.name', 'map_flags.status')),
'keywords' => $dbh->sql_group_concat('DISTINCT map_keyworddefs.name'),
+
+ blocked => $dbh->sql_group_concat('DISTINCT map_blocked.blocked'),
+ dependson => $dbh->sql_group_concat('DISTINCT map_dependson.dependson'),
'longdescs.count' => 'COUNT(DISTINCT map_longdescs_count.comment_id)',
+ last_visit_ts => 'bug_user_last_visit.last_visit_ts',
+ assignee_last_login => 'assignee.last_seen_date',
);
# Backward-compatibility for old field names. Goes new_name => old_name.
- # These are here and not in translate_old_column because the rest of the
+ # These are here and not in _translate_old_column because the rest of the
# code actually still uses the old names, while the fielddefs table uses
# the new names (which is not the case for the fields handled by
- # translate_old_column).
+ # _translate_old_column).
my %old_names = (
creation_ts => 'opendate',
delta_ts => 'changeddate',
@@ -647,7 +728,9 @@ sub REPORT_COLUMNS {
# is here because it *always* goes into the GROUP BY as the first item,
# so it should be skipped when determining extra GROUP BY columns.
use constant GROUP_BY_SKIP => qw(
+ blocked
bug_id
+ dependson
flagtypes.name
keywords
longdescs.count
@@ -688,7 +771,128 @@ sub new {
# Public Accessors #
####################
-sub sql {
+sub data {
+ my $self = shift;
+ return $self->{data} if $self->{data};
+ my $dbh = Bugzilla->dbh;
+
+ # If all fields belong to the 'bugs' table, there is no need to split
+ # the original query into two pieces. Else we override the 'fields'
+ # argument to first get bug IDs based on the search criteria defined
+ # by the caller, and the desired fields are collected in the 2nd query.
+ my @orig_fields = $self->_input_columns;
+ my $all_in_bugs_table = 1;
+ foreach my $field (@orig_fields) {
+ next if $self->COLUMNS->{$field}->{name} =~ /^bugs\.\w+$/;
+ $self->{fields} = ['bug_id'];
+ $all_in_bugs_table = 0;
+ last;
+ }
+
+ # BMO - to avoid massive amounts of joins, if we're selecting a lot of
+ # tracking flags, replace them with placeholders. the values will be
+ # retrieved later and injected into the result.
+ my %tf_map = map { $_ => 1 } Bugzilla::Extension::TrackingFlags::Flag->get_all_names();
+ my @tf_selected = grep { exists $tf_map{$_} } @orig_fields;
+ # mysql has a limit of 61 joins, and we want to avoid massive amounts of joins
+ # 30 ensures we won't hit the limit, nor generate too many joins
+ if (scalar @tf_selected > 30) {
+ foreach my $column (@tf_selected) {
+ $self->COLUMNS->{$column}->{name} = "'---'";
+ }
+ $self->{tracking_flags} = \@tf_selected;
+ }
+ else {
+ $self->{tracking_flags} = [];
+ }
+
+ my $start_time = [gettimeofday()];
+ my $sql = $self->_sql;
+ # Do we just want bug IDs to pass to the 2nd query or all the data immediately?
+ my $func = $all_in_bugs_table ? 'selectall_arrayref' : 'selectcol_arrayref';
+ my $bug_ids = $dbh->$func($sql);
+ my @extra_data = ({sql => $sql, time => tv_interval($start_time)});
+ # Restore the original 'fields' argument, just in case.
+ $self->{fields} = \@orig_fields unless $all_in_bugs_table;
+
+ # BMO if the caller only wants the count, that's all we need to return
+ return $bug_ids->[0]->[0] if $self->_params->{count_only};
+
+ # If there are no bugs found, or all fields are in the 'bugs' table,
+ # there is no need for another query.
+ if (!scalar @$bug_ids || $all_in_bugs_table) {
+ $self->{data} = $bug_ids;
+ return wantarray ? ($self->{data}, \@extra_data) : $self->{data};
+ }
+
+ # Make sure the bug_id will be returned. If not, append it to the list.
+ my $pos = firstidx { $_ eq 'bug_id' } @orig_fields;
+ if ($pos < 0) {
+ push(@orig_fields, 'bug_id');
+ $pos = $#orig_fields;
+ }
+
+ # Now create a query with the buglist above as the single criteria
+ # and the fields that the caller wants. No need to redo security checks;
+ # the list has already been validated above.
+ my $search = $self->new('fields' => \@orig_fields,
+ 'params' => {bug_id => $bug_ids, bug_id_type => 'anyexact'},
+ 'sharer' => $self->_sharer_id,
+ 'user' => $self->_user,
+ 'allow_unlimited' => 1,
+ '_no_security_check' => 1);
+
+ $start_time = [gettimeofday()];
+ $sql = $search->_sql;
+ my $unsorted_data = $dbh->selectall_arrayref($sql);
+ push(@extra_data, {sql => $sql, time => tv_interval($start_time)});
+ # Let's sort the data. We didn't do it in the query itself because
+ # we already know in which order to sort bugs thanks to the first query,
+ # and this avoids additional table joins in the SQL query.
+ my %data = map { $_->[$pos] => $_ } @$unsorted_data;
+ $self->{data} = [map { $data{$_} } @$bug_ids];
+
+ # BMO - get tracking flags values, and insert into result
+ if (@{ $self->{tracking_flags} }) {
+ # read values
+ my $values;
+ $sql = "
+ SELECT bugs.bug_id, tracking_flags.name, tracking_flags_bugs.value
+ FROM bugs
+ LEFT JOIN tracking_flags_bugs ON tracking_flags_bugs.bug_id = bugs.bug_id
+ LEFT JOIN tracking_flags ON tracking_flags.id = tracking_flags_bugs.tracking_flag_id
+ WHERE " . $dbh->sql_in('bugs.bug_id', $bug_ids);
+ $start_time = [gettimeofday()];
+ my $rows = $dbh->selectall_arrayref($sql);
+ push(@extra_data, {sql => $sql, time => tv_interval($start_time)});
+ foreach my $row (@$rows) {
+ $values->{$row->[0]}{$row->[1]} = $row->[2] if defined($row->[2]);
+ }
+
+ # find the columns of the tracking flags
+ my %tf_pos;
+ for (my $i = 0; $i <= $#orig_fields; $i++) {
+ if (grep { $_ eq $orig_fields[$i] } @{ $self->{tracking_flags} }) {
+ $tf_pos{$orig_fields[$i]} = $i;
+ }
+ }
+
+ # replace the placeholder value with the field's value
+ foreach my $row (@{ $self->{data} }) {
+ my $bug_id = $row->[$pos];
+ next unless exists $values->{$bug_id};
+ foreach my $field (keys %{ $values->{$bug_id} }) {
+ if (exists $tf_pos{$field}) {
+ $row->[$tf_pos{$field}] = $values->{$bug_id}{$field};
+ }
+ }
+ }
+ }
+
+ return wantarray ? ($self->{data}, \@extra_data) : $self->{data};
+}
+
+sub _sql {
my ($self) = @_;
return $self->{sql} if $self->{sql};
my $dbh = Bugzilla->dbh;
@@ -710,7 +914,15 @@ sub sql {
? "\nORDER BY " . join(', ', $self->_sql_order_by) : '';
my $limit = $self->_sql_limit;
$limit = "\n$limit" if $limit;
-
+
+ # BMO allow fetching just the number of matching bugs
+ if ($self->_params->{count_only}) {
+ $select = 'COUNT(*) AS count';
+ $group_by = '';
+ $order_by = '';
+ $limit = '';
+ }
+
my $query = <<END;
SELECT $select
FROM $from
@@ -725,12 +937,30 @@ sub search_description {
my ($self, $params) = @_;
my $desc = $self->{'search_description'} ||= [];
if ($params) {
+
+ # BMO - product aliases
+ # display the new product name on the search results name to avoid a
+ # disparity between the search summary and the results.
+ if ($params->{field} eq 'product') {
+ my $aliased;
+ my @values = split(/,/, $params->{value});
+ foreach my $value (@values) {
+ if (exists PRODUCT_ALIASES->{lc($value)}) {
+ $value = PRODUCT_ALIASES->{lc($value)};
+ $aliased = 1;
+ }
+ }
+ if ($aliased) {
+ $params->{value} = join(',', @values);
+ }
+ }
+
push(@$desc, $params);
}
# Make sure that the description has actually been generated if
# people are asking for the whole thing.
else {
- $self->sql;
+ $self->_sql;
}
return $self->{'search_description'};
}
@@ -754,6 +984,21 @@ sub boolean_charts_to_custom_search {
}
}
+sub invalid_order_columns {
+ my ($self) = @_;
+ my @invalid_columns;
+ foreach my $order ($self->_input_order) {
+ next if defined $self->_validate_order_column($order);
+ push(@invalid_columns, $order);
+ }
+ return \@invalid_columns;
+}
+
+sub order {
+ my ($self) = @_;
+ return $self->_valid_order;
+}
+
######################
# Internal Accessors #
######################
@@ -819,7 +1064,7 @@ sub _extra_columns {
my ($self) = @_;
# Everything that's going to be in the ORDER BY must also be
# in the SELECT.
- push(@{ $self->{extra_columns} }, $self->_input_order_columns);
+ push(@{ $self->{extra_columns} }, $self->_valid_order_columns);
return @{ $self->{extra_columns} };
}
@@ -889,10 +1134,32 @@ sub _sql_select {
# The "order" that was requested by the consumer, exactly as it was
# requested.
sub _input_order { @{ $_[0]->{'order'} || [] } }
-# The input order with just the column names, and no ASC or DESC.
-sub _input_order_columns {
+# Requested order with invalid values removed and old names translated
+sub _valid_order {
+ my ($self) = @_;
+ return map { ($self->_validate_order_column($_)) } $self->_input_order;
+}
+# The valid order with just the column names, and no ASC or DESC.
+sub _valid_order_columns {
my ($self) = @_;
- return map { (split_order_term($_))[0] } $self->_input_order;
+ return map { (split_order_term($_))[0] } $self->_valid_order;
+}
+
+sub _validate_order_column {
+ my ($self, $order_item) = @_;
+
+ # Translate old column names
+ my ($field, $direction) = split_order_term($order_item);
+ $field = $self->_translate_old_column($field);
+
+ # Only accept valid columns
+ return if (!exists $self->COLUMNS->{$field});
+
+ # Relevance column can be used only with one or more fulltext searches
+ return if ($field eq 'relevance' && !$self->COLUMNS->{$field}->{name});
+
+ $direction = " $direction" if $direction;
+ return "$field$direction";
}
# A hashref that describes all the special stuff that has to be done
@@ -924,7 +1191,7 @@ sub _sql_order_by {
my ($self) = @_;
if (!$self->{sql_order_by}) {
my @order_by = map { $self->_translate_order_by_column($_) }
- $self->_input_order;
+ $self->_valid_order;
$self->{sql_order_by} = \@order_by;
}
return @{ $self->{sql_order_by} };
@@ -1069,7 +1336,7 @@ sub _select_order_joins {
my @column_join = $self->_column_join($field);
push(@joins, @column_join);
}
- foreach my $field ($self->_input_order_columns) {
+ foreach my $field ($self->_valid_order_columns) {
my $join_info = $self->_special_order->{$field}->{join};
if ($join_info) {
# Don't let callers modify SPECIAL_ORDER.
@@ -1088,6 +1355,7 @@ sub _standard_joins {
my ($self) = @_;
my $user = $self->_user;
my @joins;
+ return () if $self->{_no_security_check};
my $security_join = {
table => 'bug_group_map',
@@ -1126,8 +1394,8 @@ sub _translate_join {
die "join with no table: " . Dumper($join_info) if !$join_info->{table};
die "join with no 'as': " . Dumper($join_info) if !$join_info->{as};
-
- my $from_table = "bugs";
+
+ my $from_table = $join_info->{bugs_table} || "bugs";
my $from = $join_info->{from} || "bug_id";
if ($from =~ /^(\w+)\.(\w+)$/) {
($from_table, $from) = ($1, $2);
@@ -1164,6 +1432,7 @@ sub _translate_join {
# group security.
sub _standard_where {
my ($self) = @_;
+ return ('1=1') if $self->{_no_security_check};
# If replication lags badly between the shadow db and the main DB,
# it's possible for bugs to show up in searches before their group
# controls are properly set. To prevent this, when initially creating
@@ -1225,7 +1494,7 @@ sub _sql_group_by {
# And all items from ORDER BY must be in the GROUP BY. The above loop
# doesn't catch items that were put into the ORDER BY from SPECIAL_ORDER.
- foreach my $column ($self->_input_order_columns) {
+ foreach my $column ($self->_valid_order_columns) {
my $special_order = $self->_special_order->{$column}->{order};
next if !$special_order;
push(@extra_group_by, @$special_order);
@@ -1377,6 +1646,8 @@ sub _special_parse_chfield {
@fields = map { $_ eq '[Bug creation]' ? 'creation_ts' : $_ } @fields;
+ return undef unless ($date_from ne '' || $date_to ne '' || $value_to ne '');
+
my $clause = new Bugzilla::Search::Clause();
# It is always safe and useful to push delta_ts into the charts
@@ -1398,44 +1669,21 @@ sub _special_parse_chfield {
$clause->add('delta_ts', 'lessthaneq', $date_to);
}
- # Basically, we construct the chart like:
- #
- # (added_for_field1 = value OR added_for_field2 = value)
- # AND (date_field1_changed >= date_from OR date_field2_changed >= date_from)
- # AND (date_field1_changed <= date_to OR date_field2_changed <= date_to)
- #
- # Theoretically, all we *really* would need to do is look for the field id
- # in the bugs_activity table, because we've already limited the search
- # by delta_ts above, but there's no chart to do that, so we check the
- # change date of the fields.
-
- if ($value_to ne '') {
- my $value_clause = new Bugzilla::Search::Clause('OR');
- foreach my $field (@fields) {
- $value_clause->add($field, 'changedto', $value_to);
- }
- $clause->add($value_clause);
- }
+ # chfieldto is supposed to be a relative date or a date of the form
+ # YYYY-MM-DD, i.e. without the time appended to it. We append the
+ # time ourselves so that the end date is correctly taken into account.
+ $date_to .= ' 23:59:59' if $date_to =~ /^\d{4}-\d{1,2}-\d{1,2}$/;
- if ($date_from ne '') {
- my $from_clause = new Bugzilla::Search::Clause('OR');
- foreach my $field (@fields) {
- $from_clause->add($field, 'changedafter', $date_from);
- }
- $clause->add($from_clause);
- }
- if ($date_to ne '') {
- # chfieldto is supposed to be a relative date or a date of the form
- # YYYY-MM-DD, i.e. without the time appended to it. We append the
- # time ourselves so that the end date is correctly taken into account.
- $date_to .= ' 23:59:59' if $date_to =~ /^\d{4}-\d{1,2}-\d{1,2}$/;
+ my $join_clause = new Bugzilla::Search::Clause('OR');
- my $to_clause = new Bugzilla::Search::Clause('OR');
- foreach my $field (@fields) {
- $to_clause->add($field, 'changedbefore', $date_to);
- }
- $clause->add($to_clause);
+ foreach my $field (@fields) {
+ my $sub_clause = new Bugzilla::Search::ClauseGroup();
+ $sub_clause->add(condition($field, 'changedto', $value_to)) if $value_to ne '';
+ $sub_clause->add(condition($field, 'changedafter', $date_from)) if $date_from ne '';
+ $sub_clause->add(condition($field, 'changedbefore', $date_to)) if $date_to ne '';
+ $join_clause->add($sub_clause);
}
+ $clause->add($join_clause);
return $clause;
}
@@ -1472,7 +1720,7 @@ sub _special_parse_email {
$type = "anyexact" if $type eq "exact";
my $or_clause = new Bugzilla::Search::Clause('OR');
- foreach my $field (qw(assigned_to reporter cc qa_contact)) {
+ foreach my $field (qw(assigned_to reporter cc qa_contact bug_mentor)) {
if ($params->{"email$field$id"}) {
$or_clause->add($field, $type, $email);
}
@@ -1525,7 +1773,7 @@ sub _charts_to_conditions {
my $clause = $self->_charts;
my @joins;
$clause->walk_conditions(sub {
- my ($condition) = @_;
+ my ($clause, $condition) = @_;
return if !$condition->translated;
push(@joins, @{ $condition->translated->{joins} });
});
@@ -1545,7 +1793,7 @@ sub _params_to_data_structure {
my ($self) = @_;
# First we get the "special" charts, representing all the normal
- # field son the search page. This may modify _params, so it needs to
+ # fields on the search page. This may modify _params, so it needs to
# happen first.
my $clause = $self->_special_charts;
@@ -1554,7 +1802,7 @@ sub _params_to_data_structure {
# And then process the modern "custom search" format.
$clause->add( $self->_custom_search );
-
+
return $clause;
}
@@ -1585,7 +1833,9 @@ sub _boolean_charts {
my $identifier = "$chart_id-$and_id-$or_id";
my $field = $params->{"field$identifier"};
my $operator = $params->{"type$identifier"};
- my $value = $params->{"value$identifier"};
+ my $value = $params->{"value$identifier"};
+ # no-value operators ignore the value, however a value needs to be set
+ $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS;
$or_clause->add($field, $operator, $value);
}
$and_clause->add($or_clause);
@@ -1601,13 +1851,18 @@ sub _custom_search {
my ($self) = @_;
my $params = $self->_params;
- my $current_clause = new Bugzilla::Search::Clause($params->{j_top});
+ my $joiner = $params->{j_top} || '';
+ my $current_clause = $joiner eq 'AND_G'
+ ? new Bugzilla::Search::ClauseGroup()
+ : new Bugzilla::Search::Clause($joiner);
my @clause_stack;
foreach my $id ($self->_field_ids) {
my $field = $params->{"f$id"};
if ($field eq 'OP') {
- my $joiner = $params->{"j$id"};
- my $new_clause = new Bugzilla::Search::Clause($joiner);
+ my $joiner = $params->{"j$id"} || '';
+ my $new_clause = $joiner eq 'AND_G'
+ ? new Bugzilla::Search::ClauseGroup()
+ : new Bugzilla::Search::Clause($joiner);
$new_clause->negate($params->{"n$id"});
$current_clause->add($new_clause);
push(@clause_stack, $current_clause);
@@ -1623,6 +1878,8 @@ sub _custom_search {
my $operator = $params->{"o$id"};
my $value = $params->{"v$id"};
+ # no-value operators ignore the value, however a value needs to be set
+ $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS;
my $condition = condition($field, $operator, $value);
$condition->negate($params->{"n$id"});
$current_clause->add($condition);
@@ -1646,14 +1903,12 @@ sub _field_ids {
}
sub _handle_chart {
- my ($self, $chart_id, $condition) = @_;
+ my ($self, $chart_id, $clause, $condition) = @_;
my $dbh = Bugzilla->dbh;
my $params = $self->_params;
my ($field, $operator, $value) = $condition->fov;
-
- $field = FIELD_MAP->{$field} || $field;
-
return if (!defined $field or !defined $operator or !defined $value);
+ $field = FIELD_MAP->{$field} || $field;
my $string_value;
if (ref $value eq 'ARRAY') {
@@ -1684,16 +1939,20 @@ sub _handle_chart {
# on multiple values, like anyexact.
my %search_args = (
- chart_id => $chart_id,
- sequence => $chart_id,
- field => $field,
- full_field => $full_field,
- operator => $operator,
- value => $string_value,
- all_values => $value,
- joins => [],
- condition => $condition,
+ chart_id => $chart_id,
+ sequence => $chart_id,
+ field => $field,
+ full_field => $full_field,
+ operator => $operator,
+ value => $string_value,
+ all_values => $value,
+ joins => [],
+ bugs_table => 'bugs',
+ table_suffix => '',
+ condition => $condition,
);
+ $clause->update_search_args(\%search_args);
+
$search_args{quoted} = $self->_quote_unless_numeric(\%search_args);
# This should add a "term" selement to %search_args.
$self->do_search_function(\%search_args);
@@ -1709,7 +1968,12 @@ sub _handle_chart {
field => $field, type => $operator,
value => $string_value, term => $search_args{term},
});
-
+
+ foreach my $join (@{ $search_args{joins} }) {
+ $join->{bugs_table} = $search_args{bugs_table};
+ $join->{table_suffix} = $search_args{table_suffix};
+ }
+
$condition->translated(\%search_args);
}
@@ -1830,8 +2094,9 @@ sub _get_column_joins {
return $cache->{column_joins} if defined $cache->{column_joins};
my %column_joins = %{ COLUMN_JOINS() };
+ # BMO - add search object to hook
Bugzilla::Hook::process('buglist_column_joins',
- { column_joins => \%column_joins });
+ { column_joins => \%column_joins, search => $self });
$cache->{column_joins} = \%column_joins;
return $cache->{column_joins};
@@ -1866,6 +2131,13 @@ sub _quote_unless_numeric {
sub build_subselect {
my ($outer, $inner, $table, $cond, $negate) = @_;
+ if ($table =~ /\battach_data\b/) {
+ # It takes a long time to scan the whole attach_data table
+ # unconditionally, so we return the subselect and let the DB optimizer
+ # restrict the search based on other search criteria.
+ my $not = $negate ? "NOT" : "";
+ return "$outer $not IN (SELECT DISTINCT $inner FROM $table WHERE $cond)";
+ }
# Execute subselects immediately to avoid dependent subqueries, which are
# large performance hits on MySql
my $q = "SELECT DISTINCT $inner FROM $table WHERE $cond";
@@ -1951,22 +2223,44 @@ sub _word_terms {
#####################################
sub _timestamp_translate {
- my ($self, $args) = @_;
+ my ($self, $ignore_time, $args) = @_;
my $value = $args->{value};
my $dbh = Bugzilla->dbh;
return if $value !~ /^(?:[\+\-]?\d+[hdwmy]s?|now)$/i;
- # By default, the time is appended to the date, which we don't want
- # for deadlines.
$value = SqlifyDate($value);
- if ($args->{field} eq 'deadline') {
+ # By default, the time is appended to the date, which we don't always want.
+ if ($ignore_time) {
($value) = split(/\s/, $value);
}
$args->{value} = $value;
$args->{quoted} = $dbh->quote($value);
}
+sub _datetime_translate {
+ return shift->_timestamp_translate(0, @_);
+}
+
+sub _last_visit_datetime {
+ my ($self, $args) = @_;
+ my $value = $args->{value};
+
+ $self->_datetime_translate($args);
+ if ($value eq $args->{value}) {
+ # Failed to translate a datetime. let's try the pronoun expando.
+ if ($value eq '%last_changed%') {
+ $self->_add_extra_column('changeddate');
+ $args->{value} = $args->{quoted} = 'bugs.delta_ts';
+ }
+ }
+}
+
+
+sub _date_translate {
+ return shift->_timestamp_translate(1, @_);
+}
+
sub SqlifyDate {
my ($str) = @_;
my $fmt = "%Y-%m-%d %H:%M:%S";
@@ -2259,7 +2553,7 @@ sub _user_nonchanged {
# For negative operators, the system we're using here
# only works properly if we reverse the operator and check IS NULL
# in the WHERE.
- my $is_negative = $operator =~ /^no/ ? 1 : 0;
+ my $is_negative = $operator =~ /^(?:no|isempty)/ ? 1 : 0;
if ($is_negative) {
$args->{operator} = $self->_reverse_operator($operator);
}
@@ -2339,8 +2633,13 @@ sub _long_desc_changedbefore_after {
sub _long_desc_nonchanged {
my ($self, $args) = @_;
- my ($chart_id, $operator, $value, $joins) =
- @$args{qw(chart_id operator value joins)};
+ my ($chart_id, $operator, $value, $joins, $bugs_table) =
+ @$args{qw(chart_id operator value joins bugs_table)};
+
+ if ($operator =~ /^is(not)?empty$/) {
+ $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty');
+ return;
+ }
my $dbh = Bugzilla->dbh;
my $table = "longdescs_$chart_id";
@@ -2354,6 +2653,7 @@ sub _long_desc_nonchanged {
all_values => $value,
quoted => $dbh->quote($value),
joins => [],
+ bugs_table => $bugs_table,
};
$self->_do_operator_function($join_args);
@@ -2481,6 +2781,21 @@ sub _percentage_complete {
$self->_add_extra_column('actual_time');
}
+sub _last_visit_ts {
+ my ($self, $args) = @_;
+
+ $args->{full_field} = $self->COLUMNS->{last_visit_ts}->{name};
+ $self->_add_extra_column('last_visit_ts');
+}
+
+sub _last_visit_ts_invalid_operator {
+ my ($self, $args) = @_;
+
+ ThrowUserError('search_field_operator_invalid',
+ { field => $args->{field},
+ operator => $args->{operator} });
+}
+
sub _days_elapsed {
my ($self, $args) = @_;
my $dbh = Bugzilla->dbh;
@@ -2489,6 +2804,21 @@ sub _days_elapsed {
$dbh->sql_to_days('bugs.delta_ts') . ")";
}
+sub _assignee_last_login {
+ my ($self, $args) = @_;
+
+ push @{ $args->{joins} }, {
+ as => 'assignee',
+ table => 'profiles',
+ from => 'assigned_to',
+ to => 'userid',
+ join => 'INNER',
+ };
+ # coalesce to 1998 to make it easy to search for users who haven't logged
+ # in since we added last_seen_date
+ $args->{full_field} = "COALESCE(assignee.last_seen_date, '1998-01-01')";
+}
+
sub _component_nonchanged {
my ($self, $args) = @_;
@@ -2501,7 +2831,28 @@ sub _component_nonchanged {
sub _product_nonchanged {
my ($self, $args) = @_;
-
+
+ # BMO - product aliases
+ # swap out old product names for new ones
+ if (ref($args->{all_values})) {
+ my $aliased;
+ foreach my $value (@{ $args->{all_values} }) {
+ if (exists PRODUCT_ALIASES->{lc($value)}) {
+ $value = PRODUCT_ALIASES->{lc($value)};
+ $aliased = 1;
+ }
+ }
+ if ($aliased) {
+ $args->{value} = join(',', @{ $args->{all_values} });
+ $args->{quoted} = Bugzilla->dbh->quote($args->{value});
+ }
+ }
+ elsif (exists PRODUCT_ALIASES->{lc($args->{value})}) {
+ $args->{value} = PRODUCT_ALIASES->{lc($args->{value})};
+ $args->{all_values} = $args->{value};
+ $args->{quoted} = Bugzilla->dbh->quote($args->{value});
+ }
+
# Generate the restriction condition
$args->{full_field} = "products.name";
$self->_do_operator_function($args);
@@ -2544,6 +2895,13 @@ sub _nullable_datetime {
$args->{full_field} = "COALESCE($field, $empty)";
}
+sub _nullable_date {
+ my ($self, $args) = @_;
+ my $field = $args->{full_field};
+ my $empty = Bugzilla->dbh->quote(EMPTY_DATE);
+ $args->{full_field} = "COALESCE($field, $empty)";
+}
+
sub _deadline {
my ($self, $args) = @_;
my $field = $args->{full_field};
@@ -2586,7 +2944,7 @@ sub _owner_idle_time_greater_less {
"$ld_table.who IS NULL AND $act_table.who IS NULL";
} else {
$args->{term} =
- "$ld_table.who IS NOT NULL OR $act_table.who IS NOT NULL";
+ "($ld_table.who IS NOT NULL OR $act_table.who IS NOT NULL)";
}
}
@@ -2630,8 +2988,14 @@ sub _multiselect_multiple {
sub _flagtypes_nonchanged {
my ($self, $args) = @_;
- my ($chart_id, $operator, $value, $joins, $condition) =
- @$args{qw(chart_id operator value joins condition)};
+ my ($chart_id, $operator, $value, $joins, $bugs_table, $condition) =
+ @$args{qw(chart_id operator value joins bugs_table condition)};
+
+ if ($operator =~ /^is(not)?empty$/) {
+ $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty');
+ return;
+ }
+
my $dbh = Bugzilla->dbh;
# For 'not' operators, we need to negate the whole term.
@@ -2654,6 +3018,7 @@ sub _flagtypes_nonchanged {
all_values => $value,
quoted => $dbh->quote($value),
joins => [],
+ bugs_table => "bugs_$chart_id",
};
$self->_do_operator_function($subselect_args);
my $subselect_term = $subselect_args->{term};
@@ -2661,7 +3026,7 @@ sub _flagtypes_nonchanged {
# don't call build_subselect as this must run as a true sub-select
$args->{term} = "EXISTS (
SELECT 1
- FROM bugs bugs_$chart_id
+ FROM $bugs_table bugs_$chart_id
LEFT JOIN attachments AS attachments_$chart_id
ON bugs_$chart_id.bug_id = attachments_$chart_id.bug_id
LEFT JOIN flags AS flags_$chart_id
@@ -2670,7 +3035,7 @@ sub _flagtypes_nonchanged {
OR flags_$chart_id.attach_id IS NULL)
LEFT JOIN flagtypes AS flagtypes_$chart_id
ON flags_$chart_id.type_id = flagtypes_$chart_id.id
- WHERE bugs_$chart_id.bug_id = bugs.bug_id
+ WHERE bugs_$chart_id.bug_id = $bugs_table.bug_id
AND $subselect_term
)";
}
@@ -2740,6 +3105,13 @@ sub _multiselect_table {
"flags.status");
return "flags INNER JOIN flagtypes ON flags.type_id = flagtypes.id";
}
+ elsif ($field eq 'comment_tag') {
+ $args->{_extra_where} = " AND longdescs.isprivate = 0"
+ if !$self->_user->is_insider;
+ $args->{full_field} = 'longdescs_tags.tag';
+ return "longdescs INNER JOIN longdescs_tags".
+ " ON longdescs.comment_id = longdescs_tags.comment_id";
+ }
my $table = "bug_$field";
$args->{full_field} = "bug_$field.value";
return $table;
@@ -2747,12 +3119,126 @@ sub _multiselect_table {
sub _multiselect_term {
my ($self, $args, $not) = @_;
+ my ($operator) = $args->{operator};
+ # 'empty' operators require special handling
+ return $self->_multiselect_isempty($args, $not)
+ if $operator =~ /^is(not)?empty$/;
my $table = $self->_multiselect_table($args);
$self->_do_operator_function($args);
my $term = $args->{term};
$term .= $args->{_extra_where} || '';
my $select = $args->{_select_field} || 'bug_id';
- return build_subselect("bugs.bug_id", $select, $table, $term, $not);
+ return build_subselect("$args->{bugs_table}.bug_id", $select, $table, $term, $not);
+}
+
+# We can't use the normal operator_functions to build isempty queries which
+# join to different tables.
+sub _multiselect_isempty {
+ my ($self, $args, $not) = @_;
+ my ($field, $operator, $joins, $chart_id) = @$args{qw(field operator joins chart_id)};
+ my $dbh = Bugzilla->dbh;
+ $operator = $self->_reverseoperator($operator) if $not;
+ $not = $operator eq 'isnotempty' ? 'NOT' : '';
+
+ if ($field eq 'keywords') {
+ push @$joins, {
+ table => 'keywords',
+ as => "keywords_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ };
+ return "keywords_$chart_id.bug_id IS $not NULL";
+ }
+ elsif ($field eq 'bug_group') {
+ push @$joins, {
+ table => 'bug_group_map',
+ as => "bug_group_map_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ };
+ return "bug_group_map_$chart_id.bug_id IS $not NULL";
+ }
+ elsif ($field eq 'flagtypes.name') {
+ push @$joins, {
+ table => 'flags',
+ as => "flags_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ };
+ return "flags_$chart_id.bug_id IS $not NULL";
+ }
+ elsif ($field eq 'blocked' or $field eq 'dependson') {
+ my $to = $field eq 'blocked' ? 'dependson' : 'blocked';
+ push @$joins, {
+ table => 'dependencies',
+ as => "dependencies_$chart_id",
+ from => 'bug_id',
+ to => $to,
+ };
+ return "dependencies_$chart_id.$to IS $not NULL";
+ }
+ elsif ($field eq 'longdesc') {
+ my @extra = ( "longdescs_$chart_id.type != " . CMT_HAS_DUPE );
+ push @extra, "longdescs_$chart_id.isprivate = 0"
+ unless $self->_user->is_insider;
+ push @$joins, {
+ table => 'longdescs',
+ as => "longdescs_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ extra => \@extra,
+ };
+ return $not
+ ? "longdescs_$chart_id.thetext != ''"
+ : "longdescs_$chart_id.thetext = ''";
+ }
+ elsif ($field eq 'longdescs.isprivate') {
+ ThrowUserError('search_field_operator_invalid', { field => $field,
+ operator => $operator });
+ }
+ elsif ($field =~ /^attachments\.(.+)/) {
+ my $sub_field = $1;
+ if ($sub_field eq 'description' || $sub_field eq 'filename' || $sub_field eq 'mimetype') {
+ # can't be null/empty
+ return $not ? '1=1' : '1=2';
+ } else {
+ # all other fields which get here are boolean
+ ThrowUserError('search_field_operator_invalid', { field => $field,
+ operator => $operator });
+ }
+ }
+ elsif ($field eq 'attach_data.thedata') {
+ push @$joins, {
+ table => 'attachments',
+ as => "attachments_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ extra => [ $self->_user->is_insider ? '' : "attachments_$chart_id.isprivate = 0" ],
+ };
+ push @$joins, {
+ table => 'attach_data',
+ as => "attach_data_$chart_id",
+ from => "attachments_$chart_id.attach_id",
+ to => 'id',
+ };
+ return "attach_data_$chart_id.thedata IS $not NULL";
+ }
+ elsif ($field eq 'tag') {
+ push @$joins, {
+ table => 'bug_tag',
+ as => "bug_tag_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ };
+ push @$joins, {
+ table => 'tag',
+ as => "tag_$chart_id",
+ from => "bug_tag_$chart_id.tag_id",
+ to => 'id',
+ extra => [ "tag_$chart_id.user_id = " . ($self->_sharer_id || $self->_user->id) ],
+ };
+ return "tag_$chart_id.id IS $not NULL";
+ }
}
###############################
@@ -2829,14 +3315,14 @@ sub _anywordsubstr {
my ($self, $args) = @_;
my @terms = $self->_substring_terms($args);
- $args->{term} = join("\n\tOR ", @terms);
+ $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : '';
}
sub _allwordssubstr {
my ($self, $args) = @_;
my @terms = $self->_substring_terms($args);
- $args->{term} = join("\n\tAND ", @terms);
+ $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : '';
}
sub _nowordssubstr {
@@ -2848,19 +3334,19 @@ sub _nowordssubstr {
sub _anywords {
my ($self, $args) = @_;
-
+
my @terms = $self->_word_terms($args);
# Because _word_terms uses AND, we need to parenthesize its terms
# if there are more than one.
@terms = map("($_)", @terms) if scalar(@terms) > 1;
- $args->{term} = join("\n\tOR ", @terms);
+ $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : '';
}
sub _allwords {
my ($self, $args) = @_;
-
+
my @terms = $self->_word_terms($args);
- $args->{term} = join("\n\tAND ", @terms);
+ $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : '';
}
sub _nowords {
@@ -2971,6 +3457,27 @@ sub _changed_security_check {
}
}
+sub _isempty {
+ my ($self, $args) = @_;
+ my $full_field = $args->{full_field};
+ $args->{term} = "$full_field IS NULL OR $full_field = " . $self->_empty_value($args->{field});
+}
+
+sub _isnotempty {
+ my ($self, $args) = @_;
+ my $full_field = $args->{full_field};
+ $args->{term} = "$full_field IS NOT NULL AND $full_field != " . $self->_empty_value($args->{field});
+}
+
+sub _empty_value {
+ my ($self, $field) = @_;
+ my $field_obj = $self->_chart_fields->{$field};
+ return "0" if $field_obj->type == FIELD_TYPE_BUG_ID;
+ return Bugzilla->dbh->quote(EMPTY_DATETIME) if $field_obj->type == FIELD_TYPE_DATETIME;
+ return Bugzilla->dbh->quote(EMPTY_DATE) if $field_obj->type == FIELD_TYPE_DATE;
+ return "''";
+}
+
######################
# Public Subroutines #
######################
@@ -2979,7 +3486,8 @@ sub _changed_security_check {
sub IsValidQueryType
{
my ($queryType) = @_;
- if (grep { $_ eq $queryType } qw(specific advanced)) {
+ # BMO: Added google and instant
+ if (grep { $_ eq $queryType } qw(specific advanced google instant)) {
return 1;
}
return 0;
@@ -2995,8 +3503,8 @@ sub split_order_term {
# Used to translate old SQL fragments from buglist.cgi's "order" argument
# into our modern field IDs.
-sub translate_old_column {
- my ($column) = @_;
+sub _translate_old_column {
+ my ($self, $column) = @_;
# All old SQL fragments have a period in them somewhere.
return $column if $column !~ /\./;
@@ -3010,12 +3518,124 @@ sub translate_old_column {
# If it doesn't match the regexps above, check to see if the old
# SQL fragment matches the SQL of an existing column
- foreach my $key (%{ COLUMNS() }) {
- next unless exists COLUMNS->{$key}->{name};
- return $key if COLUMNS->{$key}->{name} eq $column;
+ foreach my $key (%{ $self->COLUMNS }) {
+ next unless exists $self->COLUMNS->{$key}->{name};
+ return $key if $self->COLUMNS->{$key}->{name} eq $column;
}
return $column;
}
+# BMO - make product aliases lowercase
+foreach my $name (keys %{ PRODUCT_ALIASES() }) {
+ PRODUCT_ALIASES->{lc($name)} = PRODUCT_ALIASES->{$name};
+ delete PRODUCT_ALIASES->{$name};
+}
+
1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Search - Provides methods to run queries against bugs.
+
+=head1 SYNOPSIS
+
+ use Bugzilla::Search;
+
+ my $search = new Bugzilla::Search({'fields' => \@fields,
+ 'params' => \%search_criteria,
+ 'sharer' => $sharer_id,
+ 'user' => $user_obj,
+ 'allow_unlimited' => 1});
+
+ my $data = $search->data;
+ my ($data, $extra_data) = $search->data;
+
+=head1 DESCRIPTION
+
+Search.pm represents a search object. It's the single way to collect
+data about bugs in a secure way. The list of bugs matching criteria
+defined by the caller are filtered based on the user privileges.
+
+=head1 METHODS
+
+=head2 new
+
+=over
+
+=item B<Description>
+
+Create a Bugzilla::Search object.
+
+=item B<Params>
+
+=over
+
+=item C<fields>
+
+An arrayref representing the bug attributes for which data is desired.
+Legal attributes are listed in the fielddefs DB table. At least one field
+must be defined, typically the 'bug_id' field.
+
+=item C<params>
+
+A hashref representing search criteria. Each key => value pair represents
+a search criteria, where the key is the search field and the value is the
+value for this field. At least one search criteria must be defined if the
+'search_allow_no_criteria' parameter is turned off, else an error is thrown.
+
+=item C<sharer>
+
+When a saved search is shared by a user, this is his user ID.
+
+=item C<user>
+
+A L<Bugzilla::User> object representing the user to whom the data is addressed.
+All security checks are done based on this user object, so it's not safe
+to share results of the query with other users as not all users have the
+same privileges or have the same role for all bugs in the list. If this
+parameter is not defined, then the currently logged in user is taken into
+account. If no user is logged in, then only public bugs will be returned.
+
+=item C<allow_unlimited>
+
+If set to a true value, the number of bugs retrieved by the query is not
+limited.
+
+=back
+
+=item B<Returns>
+
+A L<Bugzilla::Search> object.
+
+=back
+
+=head2 data
+
+=over
+
+=item B<Description>
+
+Returns bugs matching search criteria passed to C<new()>.
+
+=item B<Params>
+
+None
+
+=item B<Returns>
+
+In scalar context, this method returns a reference to a list of bugs.
+Each item of the list represents a bug, which is itself a reference to
+a list where each item represents a bug attribute, in the same order as
+specified in the C<fields> parameter of C<new()>.
+
+In list context, this methods also returns a reference to a list containing
+references to hashes. For each hash, two keys are defined: C<sql> contains
+the SQL query which has been executed, and C<time> contains the time spent
+to execute the SQL query, in seconds. There can be either a single hash, or
+two hashes if two SQL queries have been executed sequentially to get all the
+required data.
+
+=back
diff --git a/Bugzilla/Search/Clause.pm b/Bugzilla/Search/Clause.pm
index 5f5ea5b50..89210babb 100644
--- a/Bugzilla/Search/Clause.pm
+++ b/Bugzilla/Search/Clause.pm
@@ -42,6 +42,11 @@ sub children {
return $self->{children};
}
+sub update_search_args {
+ my ($self, $search_args) = @_;
+ # abstract
+}
+
sub joiner { return $_[0]->{joiner} }
sub has_translated_conditions {
@@ -83,7 +88,7 @@ sub walk_conditions {
my ($self, $callback) = @_;
foreach my $child (@{ $self->children }) {
if ($child->isa('Bugzilla::Search::Condition')) {
- $callback->($child);
+ $callback->($self, $child);
}
else {
$child->walk_conditions($callback);
diff --git a/Bugzilla/Search/ClauseGroup.pm b/Bugzilla/Search/ClauseGroup.pm
new file mode 100644
index 000000000..5b437afec
--- /dev/null
+++ b/Bugzilla/Search/ClauseGroup.pm
@@ -0,0 +1,96 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Search::ClauseGroup;
+
+use strict;
+
+use base qw(Bugzilla::Search::Clause);
+
+use Bugzilla::Error;
+use Bugzilla::Search::Condition qw(condition);
+use Bugzilla::Util qw(trick_taint);
+use List::MoreUtils qw(uniq);
+
+use constant UNSUPPORTED_FIELDS => qw(
+ attach_data.thedata
+ classification
+ commenter
+ component
+ longdescs.count
+ product
+ owner_idle_time
+);
+
+sub new {
+ my ($class) = @_;
+ my $self = bless({ joiner => 'AND' }, $class);
+ # Add a join back to the bugs table which will be used to group conditions
+ # for this clause
+ my $condition = Bugzilla::Search::Condition->new({});
+ $condition->translated({
+ joins => [{
+ table => 'bugs',
+ as => 'bugs_g0',
+ from => 'bug_id',
+ to => 'bug_id',
+ extra => [],
+ }],
+ term => '1 = 1',
+ });
+ $self->SUPER::add($condition);
+ $self->{group_condition} = $condition;
+ return $self;
+}
+
+sub add {
+ my ($self, @args) = @_;
+ my $field = scalar(@args) == 3 ? $args[0] : $args[0]->{field};
+
+ # We don't support nesting of conditions under this clause
+ if (scalar(@args) == 1 && !$args[0]->isa('Bugzilla::Search::Condition')) {
+ ThrowUserError('search_grouped_invalid_nesting');
+ }
+
+ # Ensure all conditions use the same field
+ if (!$self->{_field}) {
+ $self->{_field} = $field;
+ } elsif ($field ne $self->{_field}) {
+ ThrowUserError('search_grouped_field_mismatch');
+ }
+
+ # Unsupported fields
+ if (grep { $_ eq $field } UNSUPPORTED_FIELDS ) {
+ ThrowUserError('search_grouped_field_invalid', { field => $field });
+ }
+
+ $self->SUPER::add(@args);
+}
+
+sub update_search_args {
+ my ($self, $search_args) = @_;
+
+ # No need to change things if there's only one child condition
+ return unless scalar(@{ $self->children }) > 1;
+
+ # we want all the terms to use the same join table
+ if (!exists $self->{_first_chart_id}) {
+ $self->{_first_chart_id} = $search_args->{chart_id};
+ } else {
+ $search_args->{chart_id} = $self->{_first_chart_id};
+ }
+
+ my $suffix = '_g' . $self->{_first_chart_id};
+ $self->{group_condition}->{translated}->{joins}->[0]->{as} = "bugs$suffix";
+
+ $search_args->{full_field} =~ s/^bugs\./bugs$suffix\./;
+
+ $search_args->{table_suffix} = $suffix;
+ $search_args->{bugs_table} = "bugs$suffix";
+}
+
+1;
diff --git a/Bugzilla/Search/Quicksearch.pm b/Bugzilla/Search/Quicksearch.pm
index fd9d796d1..00a970292 100644
--- a/Bugzilla/Search/Quicksearch.pm
+++ b/Bugzilla/Search/Quicksearch.pm
@@ -116,6 +116,17 @@ use constant FIELD_OPERATOR => {
owner_idle_time => 'greaterthan',
};
+# Mappings for operators symbols to support operators other than "substring"
+use constant OPERATOR_SYMBOLS => {
+ ':' => 'substring',
+ '=' => 'equals',
+ '!=' => 'notequals',
+ '>=' => 'greaterthaneq',
+ '<=' => 'lessthaneq',
+ '>' => 'greaterthan',
+ '<' => 'lessthan',
+};
+
# We might want to put this into localconfig or somewhere
use constant PRODUCT_EXCEPTIONS => (
'row', # [Browser]
@@ -151,7 +162,7 @@ sub quicksearch {
# Retain backslashes and quotes, to know which strings are quoted,
# and which ones are not.
- my @words = parse_line('\s+', 1, $searchstring);
+ my @words = _parse_line('\s+', 1, $searchstring);
# If parse_line() returns no data, this means strings are badly quoted.
# Rather than trying to guess what the user wanted to do, we throw an error.
scalar(@words)
@@ -161,6 +172,8 @@ sub quicksearch {
ThrowUserError('quicksearch_invalid_query')
if ($words[0] =~ /^(?:AND|OR)$/ || $words[$#words] =~ /^(?:AND|OR|NOT)$/);
+ $fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0;
+
my (@qswords, @or_group);
while (scalar @words) {
my $word = shift @words;
@@ -187,6 +200,10 @@ sub quicksearch {
}
unshift(@words, "-$word");
}
+ # --comment and ++comment disable or enable fulltext searching
+ elsif ($word =~ /^(--|\+\+)comments?$/i) {
+ $fulltext = $1 eq '--' ? 0 : 1;
+ }
else {
# OR groups words together, as OR has higher precedence than AND.
push(@or_group, $word);
@@ -203,12 +220,12 @@ sub quicksearch {
shift(@qswords) if $bug_status_set;
my (@unknownFields, %ambiguous_fields);
- $fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0;
# Loop over all main-level QuickSearch words.
foreach my $qsword (@qswords) {
- my @or_operand = parse_line('\|', 1, $qsword);
+ my @or_operand = _parse_line('\|', 1, $qsword);
foreach my $term (@or_operand) {
+ next unless defined $term;
my $negate = substr($term, 0, 1) eq '-';
if ($negate) {
$term = substr($term, 1);
@@ -221,7 +238,7 @@ sub quicksearch {
# Having ruled out the special cases, we may now split
# by comma, which is another legal boolean OR indicator.
# Remove quotes from quoted words, if any.
- @words = parse_line(',', 0, $term);
+ @words = _parse_line(',', 0, $term);
foreach my $word (@words) {
if (!_special_field_syntax($word, $negate)) {
_default_quicksearch_word($word, $negate);
@@ -273,13 +290,36 @@ sub quicksearch {
# Parts of quicksearch() #
##########################
+sub _parse_line {
+ my ($delim, $keep, $line) = @_;
+ return () unless defined $line;
+
+ # parse_line always treats ' as a quote character, making it impossible
+ # to sanely search for contractions. As this behavour isn't
+ # configurable, we replace ' with a placeholder to hide it from the
+ # parser.
+
+ # only treat ' at the start or end of words as quotes
+ # it's easier to do this in reverse with regexes
+ $line =~ s/(^|\s|:)'/$1\001/g;
+ $line =~ s/'($|\s)/\001$1/g;
+ $line =~ s/\\?'/\000/g;
+ $line =~ tr/\001/'/;
+
+ my @words = parse_line($delim, $keep, $line);
+ foreach my $word (@words) {
+ $word =~ tr/\000/'/ if defined $word;
+ }
+ return @words;
+}
+
sub _bug_numbers_only {
my $searchstring = shift;
my $cgi = Bugzilla->cgi;
# Allow separation by comma or whitespace.
$searchstring =~ s/[,\s]+/,/g;
- if ($searchstring !~ /,/) {
+ if ($searchstring !~ /,/ && !i_am_webservice()) {
# Single bug number; shortcut to show_bug.cgi.
print $cgi->redirect(
-uri => correct_urlbase() . "show_bug.cgi?id=$searchstring");
@@ -298,9 +338,11 @@ sub _handle_alias {
if ($searchstring =~ /^([^,\s]+)$/) {
my $alias = $1;
# We use this direct SQL because we want quicksearch to be VERY fast.
- my $is_alias = Bugzilla->dbh->selectrow_array(
- q{SELECT 1 FROM bugs WHERE alias = ?}, undef, $alias);
- if ($is_alias) {
+ my $bug_id = Bugzilla->dbh->selectrow_array(
+ q{SELECT bug_id FROM bugs WHERE alias = ?}, undef, $alias);
+ # If the user cannot see the bug or if we are using a webservice,
+ # do not resolve its alias.
+ if ($bug_id && Bugzilla->user->can_see_bug($bug_id) && !i_am_webservice()) {
$alias = url_quote($alias);
print Bugzilla->cgi->redirect(
-uri => correct_urlbase() . "show_bug.cgi?id=$alias");
@@ -339,6 +381,7 @@ sub _handle_status_and_resolution {
sub _handle_special_first_chars {
my ($qsword, $negate) = @_;
+ return 0 if !defined $qsword || length($qsword) <= 1;
my $firstChar = substr($qsword, 0, 1);
my $baseWord = substr($qsword, 1);
@@ -377,23 +420,31 @@ sub _handle_field_names {
# Flag and requestee shortcut
if ($or_operand =~ /^(?:flag:)?([^\?]+\?)([^\?]*)$/) {
- my ($flagtype, $requestee) = ($1, $2);
- addChart('flagtypes.name', 'substring', $flagtype, $negate);
- if ($requestee) {
- # AND
- $chart++;
- $and = $or = 0;
- addChart('requestees.login_name', 'substring', $requestee, $negate);
+ # BMO: Do not treat custom fields as flags if value is ?
+ if ($1 !~ /^cf_/) {
+ my ($flagtype, $requestee) = ($1, $2);
+ addChart('flagtypes.name', 'substring', $flagtype, $negate);
+ if ($requestee) {
+ # AND
+ $chart++;
+ $and = $or = 0;
+ addChart('requestees.login_name', 'substring', $requestee, $negate);
+ }
+ return 1;
}
- return 1;
}
# Generic field1,field2,field3:value1,value2 notation.
# We have to correctly ignore commas and colons in quotes.
- my @field_values = parse_line(':', 1, $or_operand);
- if (scalar @field_values == 2) {
- my @fields = parse_line(',', 1, $field_values[0]);
- my @values = parse_line(',', 1, $field_values[1]);
+ # Longer operators must be tested first as we don't want single character
+ # operators such as <, > and = to be tested before <=, >= and !=.
+ my @operators = sort { length($b) <=> length($a) } keys %{ OPERATOR_SYMBOLS() };
+
+ foreach my $symbol (@operators) {
+ my @field_values = _parse_line($symbol, 1, $or_operand);
+ next unless scalar @field_values == 2;
+ my @fields = _parse_line(',', 1, $field_values[0]);
+ my @values = _parse_line(',', 1, $field_values[1]);
foreach my $field (@fields) {
my $translated = _translate_field_name($field);
# Skip and record any unknown fields
@@ -410,7 +461,10 @@ sub _handle_field_names {
$bug_status_set = 1;
}
foreach my $value (@values) {
- my $operator = FIELD_OPERATOR->{$translated} || 'substring';
+ next unless defined $value;
+ my $operator = FIELD_OPERATOR->{$translated}
+ || OPERATOR_SYMBOLS->{$symbol}
+ || 'substring';
# If the string was quoted to protect some special
# characters such as commas and colons, we need
# to remove quotes.
@@ -482,6 +536,7 @@ sub _translate_field_name {
sub _special_field_syntax {
my ($word, $negate) = @_;
+ return unless defined($word);
# P1-5 Syntax
if ($word =~ m/^P(\d+)(?:-(\d+))?$/i) {
@@ -517,6 +572,7 @@ sub _special_field_syntax {
sub _default_quicksearch_word {
my ($word, $negate) = @_;
+ return unless defined($word);
if (!grep { lc($word) eq $_ } PRODUCT_EXCEPTIONS and length($word) > 2) {
addChart('product', 'substring', $word, $negate);
@@ -535,10 +591,15 @@ sub _default_quicksearch_word {
addChart('short_desc', 'substring', $word, $negate);
addChart('status_whiteboard', 'substring', $word, $negate);
addChart('content', 'matches', _matches_phrase($word), $negate) if $fulltext;
+
+ # BMO Bug 664124 - Include the crash signature (sig:) field in default quicksearches
+ addChart('cf_crash_signature', 'substring', $word, $negate);
}
sub _handle_urls {
my ($word, $negate) = @_;
+ return unless defined($word);
+
# URL field (for IP addrs, host.names,
# scheme://urls)
if ($word =~ m/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/
diff --git a/Bugzilla/Search/Recent.pm b/Bugzilla/Search/Recent.pm
index 5f04b180b..b9f6428d7 100644
--- a/Bugzilla/Search/Recent.pm
+++ b/Bugzilla/Search/Recent.pm
@@ -53,6 +53,9 @@ use constant VALIDATORS => {
use constant UPDATE_COLUMNS => qw(bug_list list_order);
+# There's no gain to caching these objects
+use constant USE_MEMCACHED => 0;
+
###################
# DB Manipulation #
###################
@@ -65,12 +68,13 @@ sub create {
my $user_id = $search->user_id;
# Enforce there only being SAVE_NUM_SEARCHES per user.
- my $min_id = $dbh->selectrow_array(
- 'SELECT id FROM profile_search WHERE user_id = ? ORDER BY id DESC '
- . $dbh->sql_limit(1, SAVE_NUM_SEARCHES), undef, $user_id);
- if ($min_id) {
- $dbh->do('DELETE FROM profile_search WHERE user_id = ? AND id <= ?',
- undef, ($user_id, $min_id));
+ my @ids = @{ $dbh->selectcol_arrayref(
+ "SELECT id FROM profile_search WHERE user_id = ? ORDER BY id",
+ undef, $user_id) };
+ if (scalar(@ids) > SAVE_NUM_SEARCHES) {
+ splice(@ids, - SAVE_NUM_SEARCHES);
+ $dbh->do(
+ "DELETE FROM profile_search WHERE id IN (" . join(',', @ids) . ")");
}
$dbh->bz_commit_transaction();
return $search;
diff --git a/Bugzilla/Search/Saved.pm b/Bugzilla/Search/Saved.pm
index 99194112a..6e1587d20 100644
--- a/Bugzilla/Search/Saved.pm
+++ b/Bugzilla/Search/Saved.pm
@@ -199,6 +199,7 @@ sub rename_field_value {
}
$dbh->do("UPDATE $table SET query = ? WHERE $id_field = ?",
undef, $query, $id);
+ Bugzilla->memcached->clear({ table => $table, id => $id });
}
$dbh->bz_commit_transaction();
@@ -301,9 +302,8 @@ sub url { return $_[0]->{'query'}; }
sub user {
my ($self) = @_;
- return $self->{user} if defined $self->{user};
- $self->{user} = new Bugzilla::User($self->{userid});
- return $self->{user};
+ return $self->{user} ||=
+ Bugzilla::User->new({ id => $self->{userid}, cache => 1 });
}
############
diff --git a/Bugzilla/Send/Sendmail.pm b/Bugzilla/Send/Sendmail.pm
new file mode 100644
index 000000000..9513134f4
--- /dev/null
+++ b/Bugzilla/Send/Sendmail.pm
@@ -0,0 +1,95 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Send::Sendmail;
+
+use strict;
+
+use base qw(Email::Send::Sendmail);
+
+use Return::Value;
+use Symbol qw(gensym);
+
+sub send {
+ my ($class, $message, @args) = @_;
+ my $mailer = $class->_find_sendmail;
+
+ return failure "Couldn't find 'sendmail' executable in your PATH"
+ ." and Email::Send::Sendmail::SENDMAIL is not set"
+ unless $mailer;
+
+ return failure "Found $mailer but cannot execute it"
+ unless -x $mailer;
+
+ local $SIG{'CHLD'} = 'DEFAULT';
+
+ my $pipe = gensym;
+
+ open($pipe, "| $mailer -t -oi @args")
+ || return failure "Error executing $mailer: $!";
+ print($pipe $message->as_string)
+ || return failure "Error printing via pipe to $mailer: $!";
+ unless (close $pipe) {
+ return failure "error when closing pipe to $mailer: $!" if $!;
+ my ($error_message, $is_transient) = _map_exitcode($? >> 8);
+ if (Bugzilla->params->{'use_mailer_queue'}) {
+ # Return success for errors which are fatal so Bugzilla knows to
+ # remove them from the queue
+ if ($is_transient) {
+ return failure "error when closing pipe to $mailer: $error_message";
+ } else {
+ warn "error when closing pipe to $mailer: $error_message\n";
+ return success;
+ }
+ } else {
+ return failure "error when closing pipe to $mailer: $error_message";
+ }
+ }
+ return success;
+}
+
+sub _map_exitcode {
+ # Returns (error message, is_transient)
+ # from the sendmail source (sendmail/sysexit.h)
+ my $code = shift;
+ if ($code == 64) {
+ return ("Command line usage error (EX_USAGE)", 1);
+ } elsif ($code == 65) {
+ return ("Data format error (EX_DATAERR)", 1);
+ } elsif ($code == 66) {
+ return ("Cannot open input (EX_NOINPUT)", 1);
+ } elsif ($code == 67) {
+ return ("Addressee unknown (EX_NOUSER)", 0);
+ } elsif ($code == 68) {
+ return ("Host name unknown (EX_NOHOST)", 0);
+ } elsif ($code == 69) {
+ return ("Service unavailable (EX_UNAVAILABLE)", 1);
+ } elsif ($code == 70) {
+ return ("Internal software error (EX_SOFTWARE)", 1);
+ } elsif ($code == 71) {
+ return ("System error (EX_OSERR)", 1);
+ } elsif ($code == 72) {
+ return ("Critical OS file missing (EX_OSFILE)", 1);
+ } elsif ($code == 73) {
+ return ("Can't create output file (EX_CANTCREAT)", 1);
+ } elsif ($code == 74) {
+ return ("Input/output error (EX_IOERR)", 1);
+ } elsif ($code == 75) {
+ return ("Temp failure (EX_TEMPFAIL)", 1);
+ } elsif ($code == 76) {
+ return ("Remote error in protocol (EX_PROTOCOL)", 1);
+ } elsif ($code == 77) {
+ return ("Permission denied (EX_NOPERM)", 1);
+ } elsif ($code == 78) {
+ return ("Configuration error (EX_CONFIG)", 1);
+ } else {
+ return ("Unknown Error ($code)", 1);
+ }
+}
+
+1;
+
diff --git a/Bugzilla/Sentry.pm b/Bugzilla/Sentry.pm
new file mode 100644
index 000000000..ce45d4823
--- /dev/null
+++ b/Bugzilla/Sentry.pm
@@ -0,0 +1,322 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Sentry;
+
+use strict;
+use warnings;
+
+use base qw(Exporter);
+our @EXPORT = qw(
+ sentry_handle_error
+ sentry_should_notify
+);
+
+use Carp;
+use Data::Dumper;
+use DateTime;
+use File::Temp;
+use LWP::UserAgent;
+use Sys::Hostname;
+use URI;
+
+use Bugzilla::Constants;
+use Bugzilla::RNG qw(irand);
+use Bugzilla::Util;
+use Bugzilla::WebService::Constants;
+
+use constant CONFIG => {
+ # 'codes' lists the code-errors which are sent to sentry
+ codes => [qw(
+ bug_error
+ chart_datafile_corrupt
+ chart_dir_nonexistent
+ chart_file_open_fail
+ illegal_content_type_method
+ jobqueue_insert_failed
+ ldap_bind_failed
+ mail_send_error
+ template_error
+ token_generation_error
+ )],
+
+ # any error/warning messages matching these regex's will not be logged or
+ # sent to sentry
+ ignore => [
+ qr/^compiled template :\s*$/,
+ qr/^Use of uninitialized value \$compiled in concatenation \(\.\) or string/,
+ ],
+
+ # any error/warning messages matching these regex's will be logged but not
+ # sent to sentry
+ sentry_ignore => [
+ qr/Software caused connection abort/,
+ qr/Could not check out .*\/cvsroot/,
+ qr/Unicode character \S+ is illegal/,
+ qr/Lost connection to MySQL server during query/,
+ qr/Call me again when you have some data to chart/,
+ qr/relative paths are not allowed/,
+ ],
+
+ # (ab)use the logger to classify error/warning types
+ logger => [
+ {
+ match => [
+ qr/DBD::mysql/,
+ qr/Can't connect to the database/,
+ ],
+ logger => 'database_error',
+ },
+ {
+ match => [ qr/PatchReader/ ],
+ logger => 'patchreader',
+ },
+ {
+ match => [ qr/Use of uninitialized value/ ],
+ logger => 'uninitialized_warning',
+ },
+ ],
+};
+
+sub sentry_generate_id {
+ return sprintf('%04x%04x%04x%04x%04x%04x%04x%04x',
+ irand(0xffff), irand(0xffff),
+ irand(0xffff),
+ irand(0x0fff) | 0x4000,
+ irand(0x3fff) | 0x8000,
+ irand(0xffff), irand(0xffff), irand(0xffff)
+ );
+}
+
+sub sentry_should_notify {
+ my $code_error = shift;
+ return grep { $_ eq $code_error } @{ CONFIG->{codes} };
+}
+
+sub sentry_handle_error {
+ my $level = shift;
+ my @message = split(/\n/, shift);
+ my $id = sentry_generate_id();
+
+ my $is_error = $level eq 'error';
+ if ($level ne 'error' && $level ne 'warning') {
+ # it's a code-error
+ return 0 unless sentry_should_notify($level);
+ $is_error = 1;
+ $level = 'error';
+ }
+
+ # build traceback
+ my $traceback;
+ {
+ # for now don't show function arguments, in case they contain
+ # confidential data. waiting on bug 700683
+ #local $Carp::MaxArgLen = 256;
+ #local $Carp::MaxArgNums = 0;
+ local $Carp::MaxArgNums = -1;
+ local $Carp::CarpInternal{'CGI::Carp'} = 1;
+ local $Carp::CarpInternal{'Bugzilla::Error'} = 1;
+ local $Carp::CarpInternal{'Bugzilla::Sentry'} = 1;
+ $traceback = trim(Carp::longmess());
+ }
+
+ # strip timestamp
+ foreach my $line (@message) {
+ $line =~ s/^\[[^\]]+\] //;
+ }
+ my $message = join(" ", map { trim($_) } grep { $_ ne '' } @message);
+
+ # message content filtering
+ foreach my $re (@{ CONFIG->{ignore} }) {
+ return 0 if $message =~ $re;
+ }
+
+ # determine logger
+ my $logger;
+ foreach my $config (@{ CONFIG->{logger} }) {
+ foreach my $re (@{ $config->{match} }) {
+ if ($message =~ $re) {
+ $logger = $config->{logger};
+ last;
+ }
+ }
+ last if $logger;
+ }
+ $logger ||= $level;
+
+ # don't send to sentry unless configured
+ my $send_to_sentry = Bugzilla->params->{sentry_uri} ? 1 : 0;
+
+ # web service filtering
+ if ($send_to_sentry
+ && (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT || Bugzilla->error_mode == ERROR_MODE_JSON_RPC))
+ {
+ my ($code) = $message =~ /^(-?\d+): /;
+ if ($code
+ && !($code == ERROR_UNKNOWN_FATAL || $code == ERROR_UNKNOWN_TRANSIENT))
+ {
+ $send_to_sentry = 0;
+ }
+ }
+
+ # message content filtering
+ if ($send_to_sentry) {
+ foreach my $re (@{ CONFIG->{sentry_ignore} }) {
+ if ($message =~ $re) {
+ $send_to_sentry = 0;
+ last;
+ }
+ }
+ }
+
+ # for now, don't send patchreader errors to sentry
+ $send_to_sentry = 0
+ if $logger eq 'patchreader';
+
+ # log to apache's error_log
+ if ($send_to_sentry) {
+ _write_to_error_log("$message [#$id]", $is_error);
+ } else {
+ $traceback =~ s/\n/ /g;
+ _write_to_error_log("$message $traceback", $is_error);
+ }
+
+ return 0 unless $send_to_sentry;
+
+ my $user_data = undef;
+ eval {
+ my $user = Bugzilla->user;
+ if ($user->id) {
+ $user_data = {
+ id => $user->login,
+ name => $user->name,
+ };
+ }
+ };
+
+ my $uri = URI->new(Bugzilla->cgi->self_url);
+ $uri->query(undef);
+
+ foreach my $field (qw( QUERY_STRING REQUEST_URI HTTP_REFERER )) {
+ $ENV{$field} =~ s/\b((?:Bugzilla_password|password)=)[^ &]+/$1*/gi
+ if exists $ENV{$field};
+ }
+
+ my $data = {
+ event_id => $id,
+ message => $message,
+ timestamp => DateTime->now->iso8601(),
+ level => $level,
+ platform => 'Other',
+ logger => $logger,
+ server_name => hostname(),
+ 'sentry.interfaces.User' => $user_data,
+ 'sentry.interfaces.Http' => {
+ url => $uri->as_string,
+ method => $ENV{REQUEST_METHOD},
+ query_string => $ENV{QUERY_STRING},
+ env => \%ENV,
+ },
+ extra => {
+ stacktrace => $traceback,
+ },
+ };
+
+ my $fh = File::Temp->new( UNLINK => 0 );
+ if (!$fh) {
+ warn "Failed to create temp file: $!\n";
+ return;
+ }
+ print $fh Dumper($data);
+ close($fh) or die $!;
+ my $filename = $fh->filename;
+
+ my $command = bz_locations()->{'cgi_path'} . "/sentry.pl '$filename' &";
+ system($command);
+ return 1;
+}
+
+sub _write_to_error_log {
+ my ($message, $is_error) = @_;
+ if ($ENV{MOD_PERL}) {
+ require Apache2::Log;
+ if ($is_error) {
+ Apache2::ServerRec::log_error($message);
+ } else {
+ Apache2::ServerRec::warn($message);
+ }
+ } else {
+ print STDERR "$message\n";
+ }
+}
+
+# lifted from Bugzilla::Error
+sub _in_eval {
+ my $in_eval = 0;
+ for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) {
+ last if $sub =~ /^ModPerl/;
+ last if $sub =~ /^Bugzilla::Template/;
+ $in_eval = 1 if $sub =~ /^\(eval\)/;
+ }
+ return $in_eval;
+}
+
+sub _sentry_die_handler {
+ my $message = shift;
+ $message =~ s/^undef error - //;
+
+ # avoid recursion, and check for CGI::Carp::die failures
+ my $in_cgi_carp_die = 0;
+ for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) {
+ return if $sub =~ /:_sentry_die_handler$/;
+ $in_cgi_carp_die = 1 if $sub =~ /CGI::Carp::die$/;
+ }
+
+ return if _in_eval();
+
+ # mod_perl overrides exit to call die with this string
+ exit if $message =~ /\bModPerl::Util::exit\b/;
+
+ my $nested_error = '';
+ my $is_compilation_failure = $message =~ /\bcompilation (aborted|failed)\b/i;
+
+ # if we are called via CGI::Carp::die chances are something is seriously
+ # wrong, so skip trying to use ThrowTemplateError
+ if (!$in_cgi_carp_die && !$is_compilation_failure) {
+ eval { Bugzilla::Error::ThrowTemplateError($message) };
+ $nested_error = $@ if $@;
+ }
+
+ if ($is_compilation_failure ||
+ $in_cgi_carp_die ||
+ ($nested_error && $nested_error !~ /\bModPerl::Util::exit\b/)
+ ) {
+ sentry_handle_error('error', $message);
+
+ # and call the normal error management
+ # (ISE for web pages, error response for web services, etc)
+ CORE::die($message);
+ }
+ exit;
+}
+
+sub install_sentry_handler {
+ require CGI::Carp;
+ CGI::Carp::set_die_handler(\&_sentry_die_handler);
+ $main::SIG{__WARN__} = sub {
+ return if _in_eval();
+ sentry_handle_error('warning', shift);
+ };
+}
+
+BEGIN {
+ if ($ENV{SCRIPT_NAME} || $ENV{MOD_PERL}) {
+ install_sentry_handler();
+ }
+}
+
+1;
diff --git a/Bugzilla/Status.pm b/Bugzilla/Status.pm
index ffef600de..4a82f68d0 100644
--- a/Bugzilla/Status.pm
+++ b/Bugzilla/Status.pm
@@ -126,11 +126,21 @@ sub _check_value {
sub BUG_STATE_OPEN {
my $dbh = Bugzilla->dbh;
- my $cache = Bugzilla->request_cache;
- $cache->{status_bug_state_open} ||=
- $dbh->selectcol_arrayref('SELECT value FROM bug_status
- WHERE is_open = 1');
- return @{ $cache->{status_bug_state_open} };
+ my $request_cache = Bugzilla->request_cache;
+ my $cache_key = 'status_bug_state_open';
+ return @{ $request_cache->{$cache_key} }
+ if exists $request_cache->{$cache_key};
+
+ my $rows = Bugzilla->memcached->get_config({ key => $cache_key });
+ if (!$rows) {
+ $rows = $dbh->selectcol_arrayref(
+ 'SELECT value FROM bug_status WHERE is_open = 1'
+ );
+ Bugzilla->memcached->set_config({ key => $cache_key, data => $rows });
+ }
+
+ $request_cache->{$cache_key} = $rows;
+ return @$rows;
}
# Tells you whether or not the argument is a valid "open" state.
diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm
index 7fd3f0e8d..9bd0c51bd 100644
--- a/Bugzilla/Template.pm
+++ b/Bugzilla/Template.pm
@@ -51,9 +51,11 @@ use Bugzilla::Token;
use Cwd qw(abs_path);
use MIME::Base64;
use Date::Format ();
+use Digest::MD5 qw(md5_hex);
use File::Basename qw(basename dirname);
use File::Find;
use File::Path qw(rmtree mkpath);
+use File::Slurp;
use File::Spec;
use IO::Dir;
use List::MoreUtils qw(firstidx);
@@ -154,9 +156,10 @@ sub get_format {
# If you want to modify this routine, read the comments carefully
sub quoteUrls {
- my ($text, $bug, $comment, $user) = @_;
+ my ($text, $bug, $comment, $user, $bug_link_func) = @_;
return $text unless $text;
$user ||= Bugzilla->user;
+ $bug_link_func ||= \&get_bug_link;
# We use /g for speed, but uris can have other things inside them
# (http://foo/bug#3 for example). Filtering that out filters valid
@@ -168,6 +171,10 @@ sub quoteUrls {
# until we require Perl 5.13.9 or newer.
no warnings 'utf8';
+ # If the comment is already wrapped, we should ignore newlines when
+ # looking for matching regexps. Else we should take them into account.
+ my $s = ($comment && $comment->already_wrapped) ? qr/\s/ : qr/\h/;
+
# However, note that adding the title (for buglinks) can affect things
# In particular, attachment matches go before bug titles, so that titles
# with 'attachment 1' don't double match.
@@ -206,7 +213,7 @@ sub quoteUrls {
map { qr/$_/ } grep($_, Bugzilla->params->{'urlbase'},
Bugzilla->params->{'sslbase'})) . ')';
$text =~ s~\b(${urlbase_re}\Qshow_bug.cgi?id=\E([0-9]+)(\#c([0-9]+))?)\b
- ~($things[$count++] = get_bug_link($3, $1, { comment_num => $5, user => $user })) &&
+ ~($things[$count++] = $bug_link_func->($3, $1, { comment_num => $5, user => $user })) &&
("\x{FDD2}" . ($count-1) . "\x{FDD3}")
~egox;
@@ -234,7 +241,8 @@ sub quoteUrls {
~<a href=\"mailto:$2\">$1$2</a>~igx;
# attachment links
- $text =~ s~\b(attachment\s*\#?\s*(\d+)(?:\s+\[details\])?)
+ # BMO: don't make diff view the default for patches (Bug 652332)
+ $text =~ s~\b(attachment$s*\#?$s*(\d+)(?:$s+\[diff\])?(?:\s+\[details\])?)
~($things[$count++] = get_attachment_link($2, $1, $user)) &&
("\x{FDD2}" . ($count-1) . "\x{FDD3}")
~egmxi;
@@ -247,21 +255,21 @@ sub quoteUrls {
# Also, we can't use $bug_re?$comment_re? because that will match the
# empty string
my $bug_word = template_var('terms')->{bug};
- my $bug_re = qr/\Q$bug_word\E\s*\#?\s*(\d+)/i;
- my $comment_re = qr/comment\s*\#?\s*(\d+)/i;
- $text =~ s~\b($bug_re(?:\s*,?\s*$comment_re)?|$comment_re)
+ my $bug_re = qr/\Q$bug_word\E$s*\#?$s*(\d+)/i;
+ my $comment_re = qr/comment$s*\#?$s*(\d+)/i;
+ $text =~ s~\b($bug_re(?:$s*,?$s*$comment_re)?|$comment_re)
~ # We have several choices. $1 here is the link, and $2-4 are set
# depending on which part matched
- (defined($2) ? get_bug_link($2, $1, { comment_num => $3, user => $user }) :
+ (defined($2) ? $bug_link_func->($2, $1, { comment_num => $3, user => $user }) :
"<a href=\"$current_bugurl#c$4\">$1</a>")
- ~egox;
+ ~egx;
# Old duplicate markers. These don't use $bug_word because they are old
# and were never customizable.
$text =~ s~(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )
(\d+)
(?=\ \*\*\*\Z)
- ~get_bug_link($1, $1, { user => $user })
+ ~$bug_link_func->($1, $1, { user => $user })
~egmx;
# Now remove the encoding hacks in reverse order
@@ -278,7 +286,7 @@ sub get_attachment_link {
my $dbh = Bugzilla->dbh;
$user ||= Bugzilla->user;
- my $attachment = new Bugzilla::Attachment($attachid);
+ my $attachment = new Bugzilla::Attachment({ id => $attachid, cache => 1 });
if ($attachment) {
my $title = "";
@@ -295,19 +303,21 @@ sub get_attachment_link {
$title = html_quote(clean_text($title));
$link_text =~ s/ \[details\]$//;
+ $link_text =~ s/ \[diff\]$//;
my $linkval = "attachment.cgi?id=$attachid";
- # If the attachment is a patch, try to link to the diff rather
- # than the text, by default.
+ # If the attachment is a patch and patch_viewer feature is
+ # enabled, add link to the diff.
my $patchlink = "";
if ($attachment->ispatch and Bugzilla->feature('patch_viewer')) {
- $patchlink = '&amp;action=diff';
+ $patchlink = qq| <a href="${linkval}&amp;action=diff" title="$title">[diff]</a>|;
}
# Whitespace matters here because these links are in <pre> tags.
return qq|<span class="$className">|
- . qq|<a href="${linkval}${patchlink}" name="attach_${attachid}" title="$title">$link_text</a>|
+ . qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>|
. qq| <a href="${linkval}&amp;action=edit" title="$title">[details]</a>|
+ . qq|${patchlink}|
. qq|</span>|;
}
else {
@@ -328,8 +338,8 @@ sub get_bug_link {
$options->{user} ||= Bugzilla->user;
my $dbh = Bugzilla->dbh;
- if (defined $bug) {
- $bug = blessed($bug) ? $bug : new Bugzilla::Bug($bug);
+ if (defined $bug && $bug ne '') {
+ $bug = blessed($bug) ? $bug : new Bugzilla::Bug({ id => $bug, cache => 1 });
return $link_text if $bug->{error};
}
@@ -394,21 +404,18 @@ sub mtime_filter {
# Set up the skin CSS cascade:
#
-# 1. YUI CSS
-# 2. Standard Bugzilla stylesheet set (persistent)
-# 3. Standard Bugzilla stylesheet set (selectable)
-# 4. All third-party "skin" stylesheet sets (selectable)
-# 5. Page-specific styles
-# 6. Custom Bugzilla stylesheet set (persistent)
-#
-# "Selectable" skin file sets may be either preferred or alternate.
-# Exactly one is preferred, determined by the "skin" user preference.
+# 1. standard/global.css
+# 2. YUI CSS
+# 3. Standard Bugzilla stylesheet set
+# 4. Third-party "skin" stylesheet set, per user prefs
+# 5. Inline css passed to global/header.html.tmpl
+# 6. Custom Bugzilla stylesheet set
+
sub css_files {
my ($style_urls, $yui, $yui_css) = @_;
-
- # global.css goes on every page, and so does IE-fixes.css.
- my @requested_css = ('skins/standard/global.css', @$style_urls,
- 'skins/standard/IE-fixes.css');
+
+ # global.css belongs on every page
+ my @requested_css = ( 'skins/standard/global.css', @$style_urls );
my @yui_required_css;
foreach my $yui_name (@$yui) {
@@ -419,21 +426,18 @@ sub css_files {
my @css_sets = map { _css_link_set($_) } @requested_css;
- my %by_type = (standard => [], alternate => {}, skin => [], custom => []);
+ my %by_type = (standard => [], skin => [], custom => []);
foreach my $set (@css_sets) {
foreach my $key (keys %$set) {
- if ($key eq 'alternate') {
- foreach my $alternate_skin (keys %{ $set->{alternate} }) {
- my $files = $by_type{alternate}->{$alternate_skin} ||= [];
- push(@$files, $set->{alternate}->{$alternate_skin});
- }
- }
- else {
- push(@{ $by_type{$key} }, $set->{$key});
- }
+ push(@{ $by_type{$key} }, $set->{$key});
}
}
-
+
+ # build unified
+ $by_type{unified_standard_skin} = _concatenate_css($by_type{standard},
+ $by_type{skin});
+ $by_type{unified_custom} = _concatenate_css($by_type{custom});
+
return \%by_type;
}
@@ -441,42 +445,135 @@ sub _css_link_set {
my ($file_name) = @_;
my %set = (standard => mtime_filter($file_name));
-
- # We use (^|/) to allow Extensions to use the skins system if they
- # want.
- if ($file_name !~ m{(^|/)skins/standard/}) {
+
+ # We use (?:^|/) to allow Extensions to use the skins system if they want.
+ if ($file_name !~ m{(?:^|/)skins/standard/}) {
return \%set;
}
-
- my $skin_user_prefs = Bugzilla->user->settings->{skin};
+
+ my $skin = Bugzilla->user->settings->{skin}->{value};
my $cgi_path = bz_locations()->{'cgi_path'};
- # If the DB is not accessible, user settings are not available.
- my $all_skins = $skin_user_prefs ? $skin_user_prefs->legal_values : [];
- my %skin_urls;
- foreach my $option (@$all_skins) {
- next if $option eq 'standard';
- my $skin_file_name = $file_name;
- $skin_file_name =~ s{(^|/)skins/standard/}{skins/contrib/$option/};
- if (my $mtime = _mtime("$cgi_path/$skin_file_name")) {
- $skin_urls{$option} = mtime_filter($skin_file_name, $mtime);
- }
+ my $skin_file_name = $file_name;
+ $skin_file_name =~ s{(?:^|/)skins/standard/}{skins/contrib/$skin/};
+ if (my $mtime = _mtime("$cgi_path/$skin_file_name")) {
+ $set{skin} = mtime_filter($skin_file_name, $mtime);
}
- $set{alternate} = \%skin_urls;
-
- my $skin = $skin_user_prefs->{'value'};
- if ($skin ne 'standard' and defined $set{alternate}->{$skin}) {
- $set{skin} = delete $set{alternate}->{$skin};
- }
-
+
my $custom_file_name = $file_name;
- $custom_file_name =~ s{(^|/)skins/standard/}{skins/custom/};
+ $custom_file_name =~ s{(?:^|/)skins/standard/}{skins/custom/};
if (my $custom_mtime = _mtime("$cgi_path/$custom_file_name")) {
$set{custom} = mtime_filter($custom_file_name, $custom_mtime);
}
-
+
return \%set;
}
+sub _concatenate_css {
+ my @sources = map { @$_ } @_;
+ return unless @sources;
+
+ my %files =
+ map {
+ (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/;
+ $_ => $file;
+ } @sources;
+
+ my $cgi_path = bz_locations()->{cgi_path};
+ my $skins_path = bz_locations()->{assetsdir};
+
+ # build minified files
+ my @minified;
+ foreach my $source (@sources) {
+ next unless -e "$cgi_path/$files{$source}";
+ my $file = $skins_path . '/' . md5_hex($source) . '.css';
+ if (!-e $file) {
+ my $content = read_file("$cgi_path/$files{$source}");
+
+ # minify
+ $content =~ s{/\*.*?\*/}{}sg; # comments
+ $content =~ s{(^\s+|\s+$)}{}mg; # leading/trailing whitespace
+ $content =~ s{\n}{}g; # single line
+
+ # rewrite urls
+ $content =~ s{url\(([^\)]+)\)}{_css_url_rewrite($source, $1)}eig;
+
+ write_file($file, "/* $files{$source} */\n" . $content . "\n");
+ }
+ push @minified, $file;
+ }
+
+ # concat files
+ my $file = $skins_path . '/' . md5_hex(join(' ', @sources)) . '.css';
+ if (!-e $file) {
+ my $content = '';
+ foreach my $source (@minified) {
+ $content .= read_file($source);
+ }
+ write_file($file, $content);
+ }
+
+ $file =~ s/^\Q$cgi_path\E\///o;
+ return mtime_filter($file);
+}
+
+sub _css_url_rewrite {
+ my ($source, $url) = @_;
+ # rewrite relative urls as the unified stylesheet lives in a different
+ # directory from the source
+ $url =~ s/(^['"]|['"]$)//g;
+ return $url if substr($url, 0, 1) eq '/';
+ return 'url(../../' . dirname($source) . '/' . $url . ')';
+}
+
+sub _concatenate_js {
+ return @_ unless CONCATENATE_ASSETS;
+ my ($sources) = @_;
+ return [] unless $sources;
+ $sources = ref($sources) ? $sources : [ $sources ];
+
+ my %files =
+ map {
+ (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/;
+ $_ => $file;
+ } @$sources;
+
+ my $cgi_path = bz_locations()->{cgi_path};
+ my $skins_path = bz_locations()->{assetsdir};
+
+ # build minified files
+ my @minified;
+ foreach my $source (@$sources) {
+ next unless -e "$cgi_path/$files{$source}";
+ my $file = $skins_path . '/' . md5_hex($source) . '.js';
+ if (!-e $file) {
+ my $content = read_file("$cgi_path/$files{$source}");
+
+ # minimal minification
+ $content =~ s#/\*.*?\*/##sg; # block comments
+ $content =~ s#(^ +| +$)##gm; # leading/trailing spaces
+ $content =~ s#^//.+$##gm; # single line comments
+ $content =~ s#\n{2,}#\n#g; # blank lines
+ $content =~ s#(^\s+|\s+$)##g; # whitespace at the start/end of file
+
+ write_file($file, "/* $files{$source} */\n" . $content . "\n");
+ }
+ push @minified, $file;
+ }
+
+ # concat files
+ my $file = $skins_path . '/' . md5_hex(join(' ', @$sources)) . '.js';
+ if (!-e $file) {
+ my $content = '';
+ foreach my $source (@minified) {
+ $content .= read_file($source);
+ }
+ write_file($file, $content);
+ }
+
+ $file =~ s/^\Q$cgi_path\E\///o;
+ return [ $file ];
+}
+
# YUI dependency resolution
sub yui_resolve_deps {
my ($yui, $yui_deps) = @_;
@@ -553,10 +650,9 @@ $Template::Stash::SCALAR_OPS->{ 0 } =
$Template::Stash::SCALAR_OPS->{ truncate } =
sub {
my ($string, $length, $ellipsis) = @_;
- $ellipsis ||= "";
-
return $string if !$length || length($string) <= $length;
-
+
+ $ellipsis ||= '';
my $strlen = $length - length($ellipsis);
my $newstr = substr($string, 0, $strlen) . $ellipsis;
return $newstr;
@@ -612,6 +708,10 @@ sub create {
COMPILE_DIR => bz_locations()->{'template_cache'},
+ # Don't check for a template update until 1 hour has passed since the
+ # last check.
+ STAT_TTL => 60 * 60,
+
# Initialize templates (f.e. by loading plugins like Hook).
PRE_PROCESS => ["global/initialize.none.tmpl"],
@@ -663,6 +763,18 @@ sub create {
$var =~ s/>/\\x3e/g;
return $var;
},
+
+ # Sadly, different to the above. See http://www.json.org/
+ # for details.
+ json => sub {
+ my ($var) = @_;
+ $var =~ s/([\\\"\/])/\\$1/g;
+ $var =~ s/\n/\\n/g;
+ $var =~ s/\r/\\r/g;
+ $var =~ s/\f/\\f/g;
+ $var =~ s/\t/\\t/g;
+ return $var;
+ },
# Converts data to base64
base64 => sub {
@@ -839,9 +951,7 @@ sub create {
# (Wrapping the message in the WebService is unnecessary
# and causes awkward things like \n's appearing in error
# messages in JSON-RPC.)
- unless (Bugzilla->usage_mode == USAGE_MODE_JSON
- or Bugzilla->usage_mode == USAGE_MODE_XMLRPC)
- {
+ unless (i_am_webservice()) {
$var = wrap_comment($var, 72);
}
$var =~ s/\&nbsp;/ /g;
@@ -891,14 +1001,9 @@ sub create {
# Currently logged in user, if any
# If an sudo session is in progress, this is the user we're faking
'user' => sub { return Bugzilla->user; },
-
+
# Currenly active language
- # XXX Eventually this should probably be replaced with something
- # like Bugzilla->language.
- 'current_language' => sub {
- my ($language) = include_languages();
- return $language;
- },
+ 'current_language' => sub { return Bugzilla->current_language; },
# If an sudo session is in progress, this is the user who
# started the session.
@@ -909,7 +1014,7 @@ sub create {
# Allow templates to access docs url with users' preferred language
'docs_urlbase' => sub {
- my ($language) = include_languages();
+ my $language = Bugzilla->current_language;
my $docs_urlbase = Bugzilla->params->{'docs_urlbase'};
$docs_urlbase =~ s/\%lang\%/$language/;
return $docs_urlbase;
@@ -938,9 +1043,18 @@ sub create {
Bugzilla->fields({ by_name => 1 });
return $cache->{template_bug_fields};
},
-
+
+ # A general purpose cache to store rendered templates for reuse.
+ # Make sure to not mix language-specific data.
+ 'template_cache' => sub {
+ my $cache = Bugzilla->request_cache->{template_cache} ||= {};
+ $cache->{users} ||= {};
+ return $cache;
+ },
+
'css_files' => \&css_files,
yui_resolve_deps => \&yui_resolve_deps,
+ concatenate_js => \&_concatenate_js,
# Whether or not keywords are enabled, in this Bugzilla.
'use_keywords' => sub { return Bugzilla::Keyword->any_exist; },
@@ -985,8 +1099,17 @@ sub create {
'default_authorizer' => sub { return Bugzilla::Auth->new() },
},
};
-
- local $Template::Config::CONTEXT = 'Bugzilla::Template::Context';
+ # Use a per-process provider to cache compiled templates in memory across
+ # requests.
+ my $provider_key = join(':', @{ $config->{INCLUDE_PATH} });
+ my $shared_providers = Bugzilla->process_cache->{shared_providers} ||= {};
+ $shared_providers->{$provider_key} ||= Template::Provider->new($config);
+ $config->{LOAD_TEMPLATES} = [ $shared_providers->{$provider_key} ];
+
+ # BMO - use metrics subclass
+ local $Template::Config::CONTEXT = Bugzilla->metrics_enabled()
+ ? 'Bugzilla::Metrics::Template::Context'
+ : 'Bugzilla::Template::Context';
Bugzilla::Hook::process('template_before_create', { config => $config });
my $template = $class->new($config)
@@ -1066,6 +1189,9 @@ sub precompile_templates {
# If anything created a Template object before now, clear it out.
delete Bugzilla->request_cache->{template};
+ # Clear out the cached Provider object
+ Bugzilla->process_cache->{shared_providers} = undef;
+
print install_string('done') . "\n" if $output;
}
diff --git a/Bugzilla/Template/Context.pm b/Bugzilla/Template/Context.pm
index 7923603e5..b3e3446f6 100644
--- a/Bugzilla/Template/Context.pm
+++ b/Bugzilla/Template/Context.pm
@@ -95,6 +95,14 @@ sub stash {
return $stash;
}
+sub filter {
+ my ($self, $name, $args) = @_;
+ # If we pass an alias for the filter name, the filter code is cached
+ # instead of looking for it at each call.
+ # If the filter has arguments, then we can't cache it.
+ $self->SUPER::filter($name, $args, $args ? undef : $name);
+}
+
# We need a DESTROY sub for the same reason that Bugzilla::CGI does.
sub DESTROY {
my $self = shift;
diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm
index 9c2242f63..24df470ac 100644
--- a/Bugzilla/Token.pm
+++ b/Bugzilla/Token.pm
@@ -109,6 +109,8 @@ sub IssueEmailChangeToken {
$vars->{'newemailaddress'} = $new_email . $email_suffix;
$vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
$vars->{'token'} = $token;
+ # For SecureMail extension
+ $vars->{'to_user'} = $user;
$vars->{'emailaddress'} = $old_email . $email_suffix;
my $message;
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index 0bc49d9b1..259a7ea90 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -50,9 +50,12 @@ use Bugzilla::Product;
use Bugzilla::Classification;
use Bugzilla::Field;
use Bugzilla::Group;
+use Bugzilla::Hook;
+use Bugzilla::BugUserLastVisit;
use DateTime::TimeZone;
use List::Util qw(max);
+use List::MoreUtils qw(any);
use Scalar::Util qw(blessed);
use Storable qw(dclone);
use URI;
@@ -91,16 +94,21 @@ use constant DB_TABLE => 'profiles';
# that you passed in for "name" to new(). That's because historically
# Bugzilla::User used "name" for the realname field. This should be
# fixed one day.
-use constant DB_COLUMNS => (
- 'profiles.userid',
- 'profiles.login_name',
- 'profiles.realname',
- 'profiles.mybugslink AS showmybugslink',
- 'profiles.disabledtext',
- 'profiles.disable_mail',
- 'profiles.extern_id',
- 'profiles.is_enabled',
-);
+sub DB_COLUMNS {
+ my $dbh = Bugzilla->dbh;
+ return (
+ 'profiles.userid',
+ 'profiles.login_name',
+ 'profiles.realname',
+ 'profiles.mybugslink AS showmybugslink',
+ 'profiles.disabledtext',
+ 'profiles.disable_mail',
+ 'profiles.extern_id',
+ 'profiles.is_enabled',
+ $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date',
+ ),
+}
+
use constant NAME_FIELD => 'login_name';
use constant ID_FIELD => 'userid';
use constant LIST_ORDER => NAME_FIELD;
@@ -144,7 +152,7 @@ sub new {
my $class = ref($invocant) || $invocant;
my ($param) = @_;
- my $user = DEFAULT_USER;
+ my $user = { %{ DEFAULT_USER() } };
bless ($user, $class);
return $user unless $param;
@@ -162,7 +170,7 @@ sub super_user {
my $class = ref($invocant) || $invocant;
my ($param) = @_;
- my $user = dclone(DEFAULT_USER);
+ my $user = { %{ DEFAULT_USER() } };
$user->{groups} = [Bugzilla::Group->get_all];
$user->{bless_groups} = [Bugzilla::Group->get_all];
bless $user, $class;
@@ -171,20 +179,25 @@ sub super_user {
sub update {
my $self = shift;
+ my $options = shift;
+
my $changes = $self->SUPER::update(@_);
my $dbh = Bugzilla->dbh;
if (exists $changes->{login_name}) {
- # If we changed the login, silently delete any tokens.
- $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id);
+ # Delete all the tokens related to the userid
+ $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id)
+ unless $options->{keep_tokens};
# And rederive regex groups
$self->derive_regexp_groups();
}
# Logout the user if necessary.
- Bugzilla->logout_user($self)
- if (exists $changes->{login_name} || exists $changes->{disabledtext}
- || exists $changes->{cryptpassword});
+ Bugzilla->logout_user($self)
+ if (!$options->{keep_session}
+ && (exists $changes->{login_name}
+ || exists $changes->{disabledtext}
+ || exists $changes->{cryptpassword}));
# XXX Can update profiles_activity here as soon as it understands
# field names like login_name.
@@ -285,6 +298,24 @@ sub set_disabledtext {
$_[0]->set('is_enabled', $_[1] ? 0 : 1);
}
+sub update_last_seen_date {
+ my $self = shift;
+ return unless $self->id;
+ my $dbh = Bugzilla->dbh;
+ my $date = $dbh->selectrow_array(
+ 'SELECT ' . $dbh->sql_date_format('NOW()', '%Y-%m-%d'));
+
+ if (!$self->last_seen_date or $date ne $self->last_seen_date) {
+ $self->{last_seen_date} = $date;
+ # We don't use the normal update() routine here as we only
+ # want to update the last_seen_date column, not any other
+ # pending changes
+ $dbh->do("UPDATE profiles SET last_seen_date = ? WHERE userid = ?",
+ undef, $date, $self->id);
+ Bugzilla->memcached->clear({ table => 'profiles', id => $self->id });
+ }
+}
+
################################################################################
# Methods
################################################################################
@@ -299,6 +330,7 @@ sub is_enabled { $_[0]->{'is_enabled'} ? 1 : 0; }
sub showmybugslink { $_[0]->{showmybugslink}; }
sub email_disabled { $_[0]->{disable_mail}; }
sub email_enabled { !($_[0]->{disable_mail}); }
+sub last_seen_date { $_[0]->{last_seen_date}; }
sub cryptpassword {
my $self = shift;
# We don't store it because we never want it in the object (we
@@ -431,6 +463,31 @@ sub tags {
return $self->{tags};
}
+sub bugs_ignored {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ if (!defined $self->{'bugs_ignored'}) {
+ $self->{'bugs_ignored'} = $dbh->selectall_arrayref(
+ 'SELECT bugs.bug_id AS id,
+ bugs.bug_status AS status,
+ bugs.short_desc AS summary
+ FROM bugs
+ INNER JOIN email_bug_ignore
+ ON bugs.bug_id = email_bug_ignore.bug_id
+ WHERE user_id = ?',
+ { Slice => {} }, $self->id);
+ # Go ahead and load these into the visible bugs cache
+ # to speed up can_see_bug checks later
+ $self->visible_bugs([ map { $_->{'id'} } @{ $self->{'bugs_ignored'} } ]);
+ }
+ return $self->{'bugs_ignored'};
+}
+
+sub is_bug_ignored {
+ my ($self, $bug_id) = @_;
+ return (grep {$_->{'id'} == $bug_id} @{$self->bugs_ignored}) ? 1 : 0;
+}
+
##########################
# Saved Recent Bug Lists #
##########################
@@ -627,56 +684,104 @@ sub groups {
return $self->{groups} if defined $self->{groups};
return [] unless $self->id;
- my $dbh = Bugzilla->dbh;
- my $groups_to_check = $dbh->selectcol_arrayref(
- q{SELECT DISTINCT group_id
- FROM user_group_map
- WHERE user_id = ? AND isbless = 0}, undef, $self->id);
+ my $user_groups_key = "user_groups." . $self->id;
+ my $groups = Bugzilla->memcached->get_config({
+ key => $user_groups_key
+ });
- my $rows = $dbh->selectall_arrayref(
- "SELECT DISTINCT grantor_id, member_id
- FROM group_group_map
- WHERE grant_type = " . GROUP_MEMBERSHIP);
+ if (!$groups) {
+ my $dbh = Bugzilla->dbh;
+ my $groups_to_check = $dbh->selectcol_arrayref(
+ "SELECT DISTINCT group_id
+ FROM user_group_map
+ WHERE user_id = ? AND isbless = 0", undef, $self->id);
+
+ my $grant_type_key = 'group_grant_type_' . GROUP_MEMBERSHIP;
+ my $membership_rows = Bugzilla->memcached->get_config({
+ key => $grant_type_key,
+ });
+ if (!$membership_rows) {
+ $membership_rows = $dbh->selectall_arrayref(
+ "SELECT DISTINCT grantor_id, member_id
+ FROM group_group_map
+ WHERE grant_type = " . GROUP_MEMBERSHIP);
+ Bugzilla->memcached->set_config({
+ key => $grant_type_key,
+ data => $membership_rows,
+ });
+ }
- my %group_membership;
- foreach my $row (@$rows) {
- my ($grantor_id, $member_id) = @$row;
- push (@{ $group_membership{$member_id} }, $grantor_id);
- }
-
- # Let's walk the groups hierarchy tree (using FIFO)
- # On the first iteration it's pre-filled with direct groups
- # membership. Later on, each group can add its own members into the
- # FIFO. Circular dependencies are eliminated by checking
- # $checked_groups{$member_id} hash values.
- # As a result, %groups will have all the groups we are the member of.
- my %checked_groups;
- my %groups;
- while (scalar(@$groups_to_check) > 0) {
- # Pop the head group from FIFO
- my $member_id = shift @$groups_to_check;
-
- # Skip the group if we have already checked it
- if (!$checked_groups{$member_id}) {
- # Mark group as checked
- $checked_groups{$member_id} = 1;
-
- # Add all its members to the FIFO check list
- # %group_membership contains arrays of group members
- # for all groups. Accessible by group number.
- my $members = $group_membership{$member_id};
- my @new_to_check = grep(!$checked_groups{$_}, @$members);
- push(@$groups_to_check, @new_to_check);
-
- $groups{$member_id} = 1;
+ my %group_membership;
+ foreach my $row (@$membership_rows) {
+ my ($grantor_id, $member_id) = @$row;
+ push (@{ $group_membership{$member_id} }, $grantor_id);
}
- }
- $self->{groups} = Bugzilla::Group->new_from_list([keys %groups]);
+ # Let's walk the groups hierarchy tree (using FIFO)
+ # On the first iteration it's pre-filled with direct groups
+ # membership. Later on, each group can add its own members into the
+ # FIFO. Circular dependencies are eliminated by checking
+ # $checked_groups{$member_id} hash values.
+ # As a result, %groups will have all the groups we are the member of.
+ my %checked_groups;
+ my %groups;
+ while (scalar(@$groups_to_check) > 0) {
+ # Pop the head group from FIFO
+ my $member_id = shift @$groups_to_check;
+
+ # Skip the group if we have already checked it
+ if (!$checked_groups{$member_id}) {
+ # Mark group as checked
+ $checked_groups{$member_id} = 1;
+
+ # Add all its members to the FIFO check list
+ # %group_membership contains arrays of group members
+ # for all groups. Accessible by group number.
+ my $members = $group_membership{$member_id};
+ my @new_to_check = grep(!$checked_groups{$_}, @$members);
+ push(@$groups_to_check, @new_to_check);
+
+ $groups{$member_id} = 1;
+ }
+ }
+ $groups = [ keys %groups ];
+
+ Bugzilla->memcached->set_config({
+ key => $user_groups_key,
+ data => $groups,
+ });
+ }
+ $self->{groups} = Bugzilla::Group->new_from_list($groups);
return $self->{groups};
}
+sub last_visited {
+ my ($self) = @_;
+
+ return Bugzilla::BugUserLastVisit->match({ user_id => $self->id });
+}
+
+sub is_involved_in_bug {
+ my ($self, $bug) = @_;
+ my $user_id = $self->id;
+ my $user_login = $self->login;
+
+ return unless $user_id;
+ return 1 if $user_id == $bug->assigned_to->id;
+ return 1 if $user_id == $bug->reporter->id;
+
+ if (Bugzilla->params->{'useqacontact'} and $bug->qa_contact) {
+ return 1 if $user_id == $bug->qa_contact->id;
+ }
+
+ # BMO - Bug mentors are considered involved with the bug
+ return 1 if $bug->is_mentor($self);
+
+ return unless $bug->cc;
+ return any { $user_login eq $_ } @{ $bug->cc };
+}
+
# It turns out that calling ->id on objects a few hundred thousand
# times is pretty slow. (It showed up as a significant time contributor
# when profiling xt/search.t.) So we cache the group ids separately from
@@ -707,40 +812,48 @@ sub bless_groups {
return $self->{'bless_groups'} if defined $self->{'bless_groups'};
return [] unless $self->id;
- if ($self->in_group('editusers')) {
- # Users having editusers permissions may bless all groups.
+ if ($self->in_group('admin')) {
+ # Users having admin permissions may bless all groups.
$self->{'bless_groups'} = [Bugzilla::Group->get_all];
return $self->{'bless_groups'};
}
+ if (Bugzilla->params->{usevisibilitygroups}
+ && !$self->visible_groups_inherited) {
+ return [];
+ }
+
my $dbh = Bugzilla->dbh;
- # Get all groups for the user where:
- # + They have direct bless privileges
- # + They are a member of a group that inherits bless privs.
- my @group_ids = map {$_->id} @{ $self->groups };
- @group_ids = (-1) if !@group_ids;
- my $query =
- 'SELECT DISTINCT groups.id
- FROM groups, user_group_map, group_group_map AS ggm
- WHERE user_group_map.user_id = ?
- AND ( (user_group_map.isbless = 1
- AND groups.id=user_group_map.group_id)
- OR (groups.id = ggm.grantor_id
- AND ggm.grant_type = ' . GROUP_BLESS . '
- AND ' . $dbh->sql_in('ggm.member_id', \@group_ids)
- . ') )';
-
- # If visibilitygroups are used, restrict the set of groups.
- if (Bugzilla->params->{'usevisibilitygroups'}) {
- return [] if !$self->visible_groups_as_string;
- # Users need to see a group in order to bless it.
+ # Get all groups for the user where they have direct bless privileges.
+ my $query = "
+ SELECT DISTINCT group_id
+ FROM user_group_map
+ WHERE user_id = ?
+ AND isbless = 1";
+ if (Bugzilla->params->{usevisibilitygroups}) {
$query .= " AND "
- . $dbh->sql_in('groups.id', $self->visible_groups_inherited);
+ . $dbh->sql_in('group_id', $self->visible_groups_inherited);
+ }
+
+ # Get all groups for the user where they are a member of a group that
+ # inherits bless privs.
+ my @group_ids = map { $_->id } @{ $self->groups };
+ if (@group_ids) {
+ $query .= "
+ UNION
+ SELECT DISTINCT grantor_id
+ FROM group_group_map
+ WHERE grant_type = " . GROUP_BLESS . "
+ AND " . $dbh->sql_in('member_id', \@group_ids);
+ if (Bugzilla->params->{usevisibilitygroups}) {
+ $query .= " AND "
+ . $dbh->sql_in('grantor_id', $self->visible_groups_inherited);
+ }
}
my $ids = $dbh->selectcol_arrayref($query, undef, $self->id);
- return $self->{'bless_groups'} = Bugzilla::Group->new_from_list($ids);
+ return $self->{bless_groups} = Bugzilla::Group->new_from_list($ids);
}
sub in_group {
@@ -778,6 +891,14 @@ sub in_group_id {
return grep($_->id == $id, @{ $self->groups }) ? 1 : 0;
}
+# This is a helper to get all groups which have an icon to be displayed
+# besides the name of the commenter.
+sub groups_with_icon {
+ my $self = shift;
+
+ return $self->{groups_with_icon} //= [grep { $_->icon_url } @{ $self->groups }];
+}
+
sub get_products_by_permission {
my ($self, $group) = @_;
# Make sure $group exists on a per-product basis.
@@ -857,6 +978,14 @@ sub visible_bugs {
if (@check_ids) {
my $dbh = Bugzilla->dbh;
my $user_id = $self->id;
+
+ foreach my $id (@check_ids) {
+ my $orig_id = $id;
+ detaint_natural($id)
+ || ThrowCodeError('param_must_be_numeric', { param => $orig_id,
+ function => 'Bugzilla::User->visible_bugs'});
+ }
+
my $sth;
# Speed up the can_see_bug case.
if (scalar(@check_ids) == 1) {
@@ -1144,24 +1273,6 @@ sub can_set_flag {
|| $self->in_group_id($flag_type->grant_group_id)) ? 1 : 0;
}
-sub direct_group_membership {
- my $self = shift;
- my $dbh = Bugzilla->dbh;
-
- if (!$self->{'direct_group_membership'}) {
- my $gid = $dbh->selectcol_arrayref('SELECT id
- FROM groups
- INNER JOIN user_group_map
- ON groups.id = user_group_map.group_id
- WHERE user_id = ?
- AND isbless = 0',
- undef, $self->id);
- $self->{'direct_group_membership'} = Bugzilla::Group->new_from_list($gid);
- }
- return $self->{'direct_group_membership'};
-}
-
-
# visible_groups_inherited returns a reference to a list of all the groups
# whose members are visible to this user.
sub visible_groups_inherited {
@@ -1277,6 +1388,8 @@ sub derive_regexp_groups {
$group_delete->execute($id, $group, GRANT_REGEXP) if $present;
}
}
+
+ Bugzilla->memcached->clear_config({ key => "user_groups.$id" });
}
sub product_responsibilities {
@@ -1635,7 +1748,9 @@ our %names_to_events = (
'attachments.mimetype' => EVT_ATTACHMENT_DATA,
'attachments.ispatch' => EVT_ATTACHMENT_DATA,
'dependson' => EVT_DEPEND_BLOCK,
- 'blocked' => EVT_DEPEND_BLOCK);
+ 'blocked' => EVT_DEPEND_BLOCK,
+ 'product' => EVT_COMPONENT,
+ 'component' => EVT_COMPONENT);
# Returns true if the user wants mail for a given bug change.
# Note: the "+" signs before the constants suppress bareword quoting.
@@ -1654,7 +1769,7 @@ sub wants_bug_mail {
}
else {
# Catch-all for any change not caught by a more specific event
- $events{+EVT_OTHER} = 1;
+ $events{+EVT_OTHER} = 1;
}
# If the user is in a particular role and the value of that role
@@ -1719,7 +1834,19 @@ sub wants_bug_mail {
if ($wants_mail && $bug->bug_status eq 'UNCONFIRMED') {
$wants_mail &= $self->wants_mail([EVT_UNCONFIRMED], $relationship);
}
-
+
+ # BMO: add a hook to allow custom bugmail filtering
+ Bugzilla::Hook::process("user_wants_mail", {
+ user => $self,
+ wants_mail => \$wants_mail,
+ bug => $bug,
+ relationship => $relationship,
+ fieldDiffs => $fieldDiffs,
+ comments => $comments,
+ dep_mail => $dep_mail,
+ changer => $changer,
+ });
+
return $wants_mail;
}
@@ -1810,6 +1937,17 @@ sub is_timetracker {
return $self->{'is_timetracker'};
}
+sub can_tag_comments {
+ my $self = shift;
+
+ if (!defined $self->{'can_tag_comments'}) {
+ my $group = Bugzilla->params->{'comment_taggers_group'};
+ $self->{'can_tag_comments'} =
+ ($group && $self->in_group($group)) ? 1 : 0;
+ }
+ return $self->{'can_tag_comments'};
+}
+
sub get_userlist {
my $self = shift;
@@ -2008,6 +2146,9 @@ sub check_and_send_account_creation_confirmation {
ThrowUserError('account_creation_restricted');
}
+ # BMO - add a hook to allow extra validation prior to account creation.
+ Bugzilla::Hook::process("user_verify_login", { login => $login });
+
# Create and send a token for this new account.
require Bugzilla::Token;
Bugzilla::Token::issue_new_user_account_token($login);
@@ -2199,6 +2340,34 @@ groups.
Returns a hashref with tag IDs as key, and a hashref with tag 'id',
'name' and 'bug_count' as value.
+=item C<bugs_ignored>
+
+Returns an array of hashrefs containing information about bugs currently
+being ignored by the user.
+
+Each hashref contains the following information:
+
+=over
+
+=item C<id>
+
+C<int> The id of the bug.
+
+=item C<status>
+
+C<string> The current status of the bug.
+
+=item C<summary>
+
+C<string> The current summary of the bug.
+
+=back
+
+=item C<is_bug_ignored>
+
+Returns true if the user does not want email notifications for the
+specified bug ID, else returns false.
+
=back
=head2 Saved Recent Bug Lists
@@ -2363,7 +2532,7 @@ Determines whether or not a user is in the given group by id.
Returns an arrayref of L<Bugzilla::Group> objects.
The arrayref consists of the groups the user can bless, taking into account
-that having editusers permissions means that you can bless all groups, and
+that having admin permissions means that you can bless all groups, and
that you need to be able to see a group in order to bless it.
=item C<get_products_by_permission($group)>
@@ -2515,11 +2684,6 @@ Returns a reference to an array of users. The array is populated with hashrefs
containing the login, identity and visibility. Users that are not visible to this
user will have 'visible' set to zero.
-=item C<direct_group_membership>
-
-Returns a reference to an array of group objects. Groups the user belong to
-by group inheritance are excluded from the list.
-
=item C<visible_groups_inherited>
Returns a list of all groups whose members should be visible to this user.
@@ -2569,6 +2733,41 @@ i.e. if the 'insidergroup' parameter is set and the user belongs to this group.
Returns true if the user is a global watcher,
i.e. if the 'globalwatchers' parameter contains the user.
+=item C<can_tag_comments>
+
+Returns true if the user can attach tags to comments.
+i.e. if the 'comment_taggers_group' parameter is set and the user belongs to
+this group.
+
+=item C<last_visited>
+
+Returns an arrayref L<Bugzilla::BugUserLastVisit> objects.
+
+=item C<is_involved_in_bug($bug)>
+
+Returns true if any of the following conditions are met, false otherwise.
+
+=over
+
+=item *
+
+User is the assignee of the bug
+
+=item *
+
+User is the reporter of the bug
+
+=item *
+
+User is the QA contact of the bug (if Bugzilla is configured to use a QA
+contact)
+
+=item *
+
+User is in the cc list for the bug.
+
+=back
+
=back
=head1 CLASS FUNCTIONS
diff --git a/Bugzilla/User/Setting.pm b/Bugzilla/User/Setting.pm
index 958a95580..e083a251e 100644
--- a/Bugzilla/User/Setting.pm
+++ b/Bugzilla/User/Setting.pm
@@ -25,8 +25,12 @@ use base qw(Exporter);
# Module stuff
-@Bugzilla::User::Setting::EXPORT = qw(get_all_settings get_defaults
- add_setting);
+@Bugzilla::User::Setting::EXPORT = qw(
+ get_all_settings
+ get_defaults
+ add_setting
+ clear_settings_cache
+);
use Bugzilla::Error;
use Bugzilla::Util qw(trick_taint get_text);
@@ -166,21 +170,23 @@ sub add_setting {
sub get_all_settings {
my ($user_id) = @_;
- my $settings = get_defaults($user_id); # first get the defaults
+ my $settings = {};
my $dbh = Bugzilla->dbh;
- my $sth = $dbh->prepare(
- q{SELECT name, default_value, is_enabled, setting_value, subclass
- FROM setting
- LEFT JOIN profile_setting
- ON setting.name = profile_setting.setting_name
- WHERE profile_setting.user_id = ?
- ORDER BY name});
+ my $cache_key = "user_settings.$user_id";
+ my $rows = Bugzilla->memcached->get_config({ key => $cache_key });
+ if (!$rows) {
+ $rows = $dbh->selectall_arrayref(
+ q{SELECT name, default_value, is_enabled, setting_value, subclass
+ FROM setting
+ LEFT JOIN profile_setting
+ ON setting.name = profile_setting.setting_name
+ AND profile_setting.user_id = ?}, undef, ($user_id));
+ Bugzilla->memcached->set_config({ key => $cache_key, data => $rows });
+ }
- $sth->execute($user_id);
- while (my ($name, $default_value, $is_enabled, $value, $subclass)
- = $sth->fetchrow_array())
- {
+ foreach my $row (@$rows) {
+ my ($name, $default_value, $is_enabled, $value, $subclass) = @$row;
my $is_default;
@@ -192,13 +198,18 @@ sub get_all_settings {
}
$settings->{$name} = new Bugzilla::User::Setting(
- $name, $user_id, $is_enabled,
+ $name, $user_id, $is_enabled,
$default_value, $value, $is_default, $subclass);
}
return $settings;
}
+sub clear_settings_cache {
+ my ($user_id) = @_;
+ Bugzilla->memcached->clear_config({ key => "user_settings.$user_id" });
+}
+
sub get_defaults {
my ($user_id) = @_;
my $dbh = Bugzilla->dbh;
@@ -206,13 +217,11 @@ sub get_defaults {
$user_id ||= 0;
- my $sth = $dbh->prepare(q{SELECT name, default_value, is_enabled, subclass
- FROM setting
- ORDER BY name});
- $sth->execute();
- while (my ($name, $default_value, $is_enabled, $subclass)
- = $sth->fetchrow_array())
- {
+ my $rows = $dbh->selectall_arrayref(q{SELECT name, default_value, is_enabled, subclass
+ FROM setting});
+
+ foreach my $row (@$rows) {
+ my ($name, $default_value, $is_enabled, $subclass) = @$row;
$default_settings->{$name} = new Bugzilla::User::Setting(
$name, $user_id, $is_enabled, $default_value, $default_value, 1,
@@ -383,6 +392,13 @@ Params: C<$setting_name> - string - the name of the setting
C<$is_enabled> - boolean - if false, all users must use the global default
Returns: nothing
+=item C<clear_settings_cache($user_id)>
+
+Description: Clears cached settings data for the specified user. Must be
+ called after updating any user's setting.
+Params: C<$user_id> - integer - the user id.
+Returns: nothing
+
=begin private
=item C<_setting_exists>
diff --git a/Bugzilla/UserAgent.pm b/Bugzilla/UserAgent.pm
new file mode 100644
index 000000000..db95150e4
--- /dev/null
+++ b/Bugzilla/UserAgent.pm
@@ -0,0 +1,259 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Terry Weissman <terry@mozilla.org>
+# Dave Miller <justdave@syndicomm.com>
+# Joe Robins <jmrobins@tgix.com>
+# Gervase Markham <gerv@gerv.net>
+# Shane H. W. Travis <travis@sedsystems.ca>
+# Nitish Bezzala <nbezzala@yahoo.com>
+# Byron Jones <glob@mozilla.com>
+
+package Bugzilla::UserAgent;
+
+use strict;
+use base qw(Exporter);
+our @EXPORT = qw(detect_platform detect_op_sys);
+
+use Bugzilla::Field;
+use List::MoreUtils qw(natatime);
+
+use constant DEFAULT_VALUE => 'Other';
+
+use constant PLATFORMS_MAP => (
+ # PowerPC
+ qr/\(.*PowerPC.*\)/i => ["PowerPC", "Macintosh"],
+ # AMD64, Intel x86_64
+ qr/\(.*[ix0-9]86 (?:on |\()x86_64.*\)/ => ["IA32", "x86", "PC"],
+ qr/\(.*amd64.*\)/ => ["AMD64", "x86_64", "PC"],
+ qr/\(.*x86_64.*\)/ => ["AMD64", "x86_64", "PC"],
+ # Intel IA64
+ qr/\(.*IA64.*\)/ => ["IA64", "PC"],
+ # Intel x86
+ qr/\(.*Intel.*\)/ => ["IA32", "x86", "PC"],
+ qr/\(.*[ix0-9]86.*\)/ => ["IA32", "x86", "PC"],
+ # Versions of Windows that only run on Intel x86
+ qr/\(.*Win(?:dows |)[39M].*\)/ => ["IA32", "x86", "PC"],
+ qr/\(.*Win(?:dows |)16.*\)/ => ["IA32", "x86", "PC"],
+ # Sparc
+ qr/\(.*sparc.*\)/ => ["Sparc", "Sun"],
+ qr/\(.*sun4.*\)/ => ["Sparc", "Sun"],
+ # Alpha
+ qr/\(.*AXP.*\)/i => ["Alpha", "DEC"],
+ qr/\(.*[ _]Alpha.\D/i => ["Alpha", "DEC"],
+ qr/\(.*[ _]Alpha\)/i => ["Alpha", "DEC"],
+ # MIPS
+ qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"],
+ qr/\(.*MIPS.*\)/i => ["MIPS", "SGI"],
+ # 68k
+ qr/\(.*68K.*\)/ => ["68k", "Macintosh"],
+ qr/\(.*680[x0]0.*\)/ => ["68k", "Macintosh"],
+ # HP
+ qr/\(.*9000.*\)/ => ["PA-RISC", "HP"],
+ # ARM
+ qr/\(.*(?:iPad|iPhone).*\)/ => ["ARM"],
+ qr/\(.*ARM.*\)/ => ["ARM", "PocketPC"],
+ # PocketPC intentionally before PowerPC
+ qr/\(.*Windows CE.*PPC.*\)/ => ["ARM", "PocketPC"],
+ # PowerPC
+ qr/\(.*PPC.*\)/ => ["PowerPC", "Macintosh"],
+ qr/\(.*AIX.*\)/ => ["PowerPC", "Macintosh"],
+ # Stereotypical and broken
+ qr/\(.*Windows CE.*\)/ => ["ARM", "PocketPC"],
+ qr/\(.*Macintosh.*\)/ => ["68k", "Macintosh"],
+ qr/\(.*Mac OS [89].*\)/ => ["68k", "Macintosh"],
+ qr/\(.*WOW64.*\)/ => ["x86_64"],
+ qr/\(.*Win64.*\)/ => ["IA64"],
+ qr/\(Win.*\)/ => ["IA32", "x86", "PC"],
+ qr/\(.*Win(?:dows[ -])NT.*\)/ => ["IA32", "x86", "PC"],
+ qr/\(.*OSF.*\)/ => ["Alpha", "DEC"],
+ qr/\(.*HP-?UX.*\)/i => ["PA-RISC", "HP"],
+ qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"],
+ qr/\(.*(SunOS|Solaris).*\)/ => ["Sparc", "Sun"],
+ # Braindead old browsers who didn't follow convention:
+ qr/Amiga/ => ["68k", "Macintosh"],
+ qr/WinMosaic/ => ["IA32", "x86", "PC"],
+);
+
+use constant OS_MAP => (
+ # Sun
+ qr/\(.*Solaris.*\)/ => ["Solaris"],
+ qr/\(.*SunOS 5.11.*\)/ => [("OpenSolaris", "Opensolaris", "Solaris 11")],
+ qr/\(.*SunOS 5.10.*\)/ => ["Solaris 10"],
+ qr/\(.*SunOS 5.9.*\)/ => ["Solaris 9"],
+ qr/\(.*SunOS 5.8.*\)/ => ["Solaris 8"],
+ qr/\(.*SunOS 5.7.*\)/ => ["Solaris 7"],
+ qr/\(.*SunOS 5.6.*\)/ => ["Solaris 6"],
+ qr/\(.*SunOS 5.5.*\)/ => ["Solaris 5"],
+ qr/\(.*SunOS 5.*\)/ => ["Solaris"],
+ qr/\(.*SunOS.*sun4u.*\)/ => ["Solaris"],
+ qr/\(.*SunOS.*i86pc.*\)/ => ["Solaris"],
+ qr/\(.*SunOS.*\)/ => ["SunOS"],
+ # BSD
+ qr/\(.*BSD\/(?:OS|386).*\)/ => ["BSDI"],
+ qr/\(.*FreeBSD.*\)/ => ["FreeBSD"],
+ qr/\(.*OpenBSD.*\)/ => ["OpenBSD"],
+ qr/\(.*NetBSD.*\)/ => ["NetBSD"],
+ # Misc POSIX
+ qr/\(.*IRIX.*\)/ => ["IRIX"],
+ qr/\(.*OSF.*\)/ => ["OSF/1"],
+ qr/\(.*Linux.*\)/ => ["Linux"],
+ qr/\(.*BeOS.*\)/ => ["BeOS"],
+ qr/\(.*AIX.*\)/ => ["AIX"],
+ qr/\(.*OS\/2.*\)/ => ["OS/2"],
+ qr/\(.*QNX.*\)/ => ["Neutrino"],
+ qr/\(.*VMS.*\)/ => ["OpenVMS"],
+ qr/\(.*HP-?UX.*\)/ => ["HP-UX"],
+ qr/\(.*Android.*\)/ => ["Android"],
+ # Windows
+ qr/\(.*Windows XP.*\)/ => ["Windows XP"],
+ qr/\(.*Windows NT 6\.3.*\)/ => ["Windows 8.1"],
+ qr/\(.*Windows NT 6\.2.*\)/ => ["Windows 8"],
+ qr/\(.*Windows NT 6\.1.*\)/ => ["Windows 7"],
+ qr/\(.*Windows NT 6\.0.*\)/ => ["Windows Vista"],
+ qr/\(.*Windows NT 5\.2.*\)/ => ["Windows Server 2003"],
+ qr/\(.*Windows NT 5\.1.*\)/ => ["Windows XP"],
+ qr/\(.*Windows 2000.*\)/ => ["Windows 2000"],
+ qr/\(.*Windows NT 5.*\)/ => ["Windows 2000"],
+ qr/\(.*Win.*9[8x].*4\.9.*\)/ => ["Windows ME"],
+ qr/\(.*Win(?:dows |)M[Ee].*\)/ => ["Windows ME"],
+ qr/\(.*Win(?:dows |)98.*\)/ => ["Windows 98"],
+ qr/\(.*Win(?:dows |)95.*\)/ => ["Windows 95"],
+ qr/\(.*Win(?:dows |)16.*\)/ => ["Windows 3.1"],
+ qr/\(.*Win(?:dows[ -]|)NT.*\)/ => ["Windows NT"],
+ qr/\(.*Windows.*NT.*\)/ => ["Windows NT"],
+ # OS X
+ qr/\(.*(?:iPad|iPhone).*OS 7.*\)/ => ["iOS 7"],
+ qr/\(.*(?:iPad|iPhone).*OS 6.*\)/ => ["iOS 6"],
+ qr/\(.*(?:iPad|iPhone).*OS 5.*\)/ => ["iOS 5"],
+ qr/\(.*(?:iPad|iPhone).*OS 4.*\)/ => ["iOS 4"],
+ qr/\(.*(?:iPad|iPhone).*OS 3.*\)/ => ["iOS 3"],
+ qr/\(.*(?:iPad|iPhone).*\)/ => ["iOS"],
+ qr/\(.*Mac OS X (?:|Mach-O |\()10.6.*\)/ => ["Mac OS X 10.6"],
+ qr/\(.*Mac OS X (?:|Mach-O |\()10.5.*\)/ => ["Mac OS X 10.5"],
+ qr/\(.*Mac OS X (?:|Mach-O |\()10.4.*\)/ => ["Mac OS X 10.4"],
+ qr/\(.*Mac OS X (?:|Mach-O |\()10.3.*\)/ => ["Mac OS X 10.3"],
+ qr/\(.*Mac OS X (?:|Mach-O |\()10.2.*\)/ => ["Mac OS X 10.2"],
+ qr/\(.*Mac OS X (?:|Mach-O |\()10.1.*\)/ => ["Mac OS X 10.1"],
+ # Unfortunately, OS X 10.4 was the first to support Intel. This is fallback
+ # support because some browsers refused to include the OS Version.
+ qr/\(.*Intel.*Mac OS X.*\)/ => ["Mac OS X 10.4"],
+ # OS X 10.3 is the most likely default version of PowerPC Macs
+ # OS X 10.0 is more for configurations which didn't setup 10.x versions
+ qr/\(.*Mac OS X.*\)/ => [("Mac OS X 10.3", "Mac OS X 10.0", "Mac OS X")],
+ qr/\(.*Mac OS 9.*\)/ => [("Mac System 9.x", "Mac System 9.0")],
+ qr/\(.*Mac OS 8\.6.*\)/ => [("Mac System 8.6", "Mac System 8.5")],
+ qr/\(.*Mac OS 8\.5.*\)/ => ["Mac System 8.5"],
+ qr/\(.*Mac OS 8\.1.*\)/ => [("Mac System 8.1", "Mac System 8.0")],
+ qr/\(.*Mac OS 8\.0.*\)/ => ["Mac System 8.0"],
+ qr/\(.*Mac OS 8[^.].*\)/ => ["Mac System 8.0"],
+ qr/\(.*Mac OS 8.*\)/ => ["Mac System 8.6"],
+ qr/\(.*Darwin.*\)/ => [("Mac OS X 10.0", "Mac OS X")],
+ # Firefox OS
+ qr/\(Mobile;.*Gecko.*Firefox/ => ["Gonk (Firefox OS)"],
+ # Silly
+ qr/\(.*Mac.*PowerPC.*\)/ => ["Mac System 9.x"],
+ qr/\(.*Mac.*PPC.*\)/ => ["Mac System 9.x"],
+ qr/\(.*Mac.*68k.*\)/ => ["Mac System 8.0"],
+ # Evil
+ qr/Amiga/i => ["Other"],
+ qr/WinMosaic/ => ["Windows 95"],
+ qr/\(.*32bit.*\)/ => ["Windows 95"],
+ qr/\(.*16bit.*\)/ => ["Windows 3.1"],
+ qr/\(.*PowerPC.*\)/ => ["Mac System 9.x"],
+ qr/\(.*PPC.*\)/ => ["Mac System 9.x"],
+ qr/\(.*68K.*\)/ => ["Mac System 8.0"],
+);
+
+sub detect_platform {
+ my $userAgent = $ENV{'HTTP_USER_AGENT'} || '';
+ my @detected;
+ my $iterator = natatime(2, PLATFORMS_MAP);
+ while (my($re, $ra) = $iterator->()) {
+ if ($userAgent =~ $re) {
+ push @detected, @$ra;
+ }
+ }
+ return _pick_valid_field_value('rep_platform', @detected);
+}
+
+sub detect_op_sys {
+ my $userAgent = $ENV{'HTTP_USER_AGENT'} || '';
+ my @detected;
+ my $iterator = natatime(2, OS_MAP);
+ while (my($re, $ra) = $iterator->()) {
+ if ($userAgent =~ $re) {
+ push @detected, @$ra;
+ }
+ }
+ push(@detected, "Windows") if grep(/^Windows /, @detected);
+ push(@detected, "Mac OS") if grep(/^Mac /, @detected);
+ return _pick_valid_field_value('op_sys', @detected);
+}
+
+# Takes the name of a field and a list of possible values for that field.
+# Returns the first value in the list that is actually a valid value for that
+# field.
+# Returns 'Other' if none of the values match.
+sub _pick_valid_field_value {
+ my ($field, @values) = @_;
+ foreach my $value (@values) {
+ return $value if check_field($field, $value, undef, 1);
+ }
+ return DEFAULT_VALUE;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::UserAgent - UserAgent utilities for Bugzilla
+
+=head1 SYNOPSIS
+
+ use Bugzilla::UserAgent;
+ printf "platform: %s op-sys: %s\n", detect_platform(), detect_op_sys();
+
+=head1 DESCRIPTION
+
+The functions exported by this module all return information derived from the
+remote client's user agent.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<detect_platform>
+
+This function attempts to detect the remote client's platform from the
+presented user-agent. If a suitable value on the I<platform> field is found,
+that field value will be returned. If no suitable value is detected,
+C<detect_platform> returns I<Other>.
+
+=item C<detect_op_sys>
+
+This function attempts to detect the remote client's operating system from the
+presented user-agent. If a suitable value on the I<op_sys> field is found, that
+field value will be returned. If no suitable value is detected,
+C<detect_op_sys> returns I<Other>.
+
+=back
+
diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm
index 48507ff9e..2349dc9e9 100644
--- a/Bugzilla/Util.pm
+++ b/Bugzilla/Util.pm
@@ -28,6 +28,7 @@
package Bugzilla::Util;
+use 5.10.1;
use strict;
use base qw(Exporter);
@@ -35,7 +36,7 @@ use base qw(Exporter);
detaint_signed
html_quote url_quote xml_quote
css_class_quote html_light_quote
- i_am_cgi correct_urlbase remote_ip validate_ip
+ i_am_cgi i_am_webservice correct_urlbase remote_ip validate_ip
do_ssl_redirect_if_required use_attachbase
diff_arrays on_main_db
trim wrap_hard wrap_comment find_wrap_point
@@ -44,7 +45,7 @@ use base qw(Exporter);
bz_crypt generate_random_password
validate_email_syntax clean_text
get_text template_var disable_utf8
- detect_encoding);
+ enable_utf8 detect_encoding email_filter);
use Bugzilla::Constants;
use Bugzilla::RNG qw(irand);
@@ -57,7 +58,6 @@ use Digest;
use Email::Address;
use List::Util qw(first);
use Scalar::Util qw(tainted blessed);
-use Template::Filters;
use Text::Wrap;
use Encode qw(encode decode resolve_alias);
use Encode::Guess;
@@ -87,10 +87,17 @@ sub detaint_signed {
# visible strings.
# Bug 319331: Handle BiDi disruptions.
sub html_quote {
- my ($var) = Template::Filters::html_filter(@_);
+ my $var = shift;
+ $var =~ s/&/&amp;/g;
+ $var =~ s/</&lt;/g;
+ $var =~ s/>/&gt;/g;
+ $var =~ s/"/&quot;/g;
# Obscure '@'.
$var =~ s/\@/\&#64;/g;
- if (Bugzilla->params->{'utf8'}) {
+
+ state $use_utf8 = Bugzilla->params->{'utf8'};
+
+ if ($use_utf8) {
# Remove control characters if the encoding is utf8.
# Other multibyte encodings may be using this range; so ignore if not utf8.
$var =~ s/(?![\t\r\n])[[:cntrl:]]//g;
@@ -116,13 +123,16 @@ sub html_quote {
# |U+200e|Left-To-Right Mark |0xe2 0x80 0x8e |
# |U+200f|Right-To-Left Mark |0xe2 0x80 0x8f |
# --------------------------------------------------------
- $var =~ s/[\x{202a}-\x{202e}]//g;
+ $var =~ tr/\x{202a}-\x{202e}//d;
}
return $var;
}
sub html_light_quote {
my ($text) = @_;
+ # admin/table.html.tmpl calls |FILTER html_light| many times.
+ # There is no need to recreate the HTML::Scrubber object again and again.
+ my $scrubber = Bugzilla->process_cache->{html_scrubber};
# List of allowed HTML elements having no attributes.
my @allow = qw(b strong em i u p br abbr acronym ins del cite code var
@@ -144,7 +154,7 @@ sub html_light_quote {
$text =~ s#$chr($safe)$chr#<$1>#go;
return $text;
}
- else {
+ elsif (!$scrubber) {
# We can be less restrictive. We can accept elements with attributes.
push(@allow, qw(a blockquote q span));
@@ -187,14 +197,14 @@ sub html_light_quote {
},
);
- my $scrubber = HTML::Scrubber->new(default => \@default,
- allow => \@allow,
- rules => \@rules,
- comment => 0,
- process => 0);
-
- return $scrubber->scrub($text);
+ Bugzilla->process_cache->{html_scrubber} = $scrubber =
+ HTML::Scrubber->new(default => \@default,
+ allow => \@allow,
+ rules => \@rules,
+ comment => 0,
+ process => 0);
}
+ return $scrubber->scrub($text);
}
sub email_filter {
@@ -253,6 +263,13 @@ sub i_am_cgi {
return exists $ENV{'SERVER_SOFTWARE'} ? 1 : 0;
}
+sub i_am_webservice {
+ my $usage_mode = Bugzilla->usage_mode;
+ return $usage_mode == USAGE_MODE_XMLRPC
+ || $usage_mode == USAGE_MODE_JSON
+ || $usage_mode == USAGE_MODE_REST;
+}
+
# This exists as a separate function from Bugzilla::CGI::redirect_to_https
# because we don't want to create a CGI object during XML-RPC calls
# (doing so can mess up XML-RPC).
@@ -475,11 +492,11 @@ sub find_wrap_point {
if (!$string) { return 0 }
if (length($string) < $maxpos) { return length($string) }
my $wrappoint = rindex($string, ",", $maxpos); # look for comma
- if ($wrappoint < 0) { # can't find comma
+ if ($wrappoint <= 0) { # can't find comma
$wrappoint = rindex($string, " ", $maxpos); # look for space
- if ($wrappoint < 0) { # can't find space
+ if ($wrappoint <= 0) { # can't find space
$wrappoint = rindex($string, "-", $maxpos); # look for hyphen
- if ($wrappoint < 0) { # can't find hyphen
+ if ($wrappoint <= 0) { # can't find hyphen
$wrappoint = $maxpos; # just truncate it
} else {
$wrappoint++; # leave hyphen on the left side
@@ -531,9 +548,14 @@ sub datetime_from {
# In the database, this is the "0" date.
return undef if $date =~ /^0000/;
- # strptime($date) returns an empty array if $date has an invalid
- # date format.
- my @time = strptime($date);
+ my @time;
+ # Most dates will be in this format, avoid strptime's generic parser
+ if ($date =~ /^(\d{4})[\.-](\d{2})[\.-](\d{2})(?: (\d{2}):(\d{2}):(\d{2}))?$/) {
+ @time = ($6, $5, $4, $3, $2 - 1, $1 - 1900, undef);
+ }
+ else {
+ @time = strptime($date);
+ }
unless (scalar @time) {
# If an unknown timezone is passed (such as MSK, for Moskow),
@@ -545,10 +567,14 @@ sub datetime_from {
return undef if !@time;
- # strptime() counts years from 1900, and months from 0 (January).
- # We have to fix both values.
+ # strptime() counts years from 1900, except if they are older than 1901
+ # in which case it returns the full year (so 1890 -> 1890, but 1984 -> 84,
+ # and 3790 -> 1890). We make a guess and assume that 1100 <= year < 3000.
+ $time[5] += 1900 if $time[5] < 1100;
+
my %args = (
- year => $time[5] + 1900,
+ year => $time[5],
+ # Months start from 0 (January).
month => $time[4] + 1,
day => $time[3],
hour => $time[2],
@@ -609,13 +635,13 @@ sub bz_crypt {
$algorithm = $1;
}
+ # Wide characters cause crypt and Digest to die.
+ if (Bugzilla->params->{'utf8'}) {
+ utf8::encode($password) if utf8::is_utf8($password);
+ }
+
my $crypted_password;
if (!$algorithm) {
- # Wide characters cause crypt to die
- if (Bugzilla->params->{'utf8'}) {
- utf8::encode($password) if utf8::is_utf8($password);
- }
-
# Crypt the password.
$crypted_password = crypt($password, $salt);
@@ -730,10 +756,12 @@ sub get_text {
sub template_var {
my $name = shift;
- my $cache = Bugzilla->request_cache->{util_template_var} ||= {};
- my $template = Bugzilla->template_inner;
- my $lang = $template->context->{bz_language};
+ my $request_cache = Bugzilla->request_cache;
+ my $cache = $request_cache->{util_template_var} ||= {};
+ my $lang = $request_cache->{template_current_lang}->[0] || '';
return $cache->{$lang}->{$name} if defined $cache->{$lang};
+
+ my $template = Bugzilla->template_inner($lang);
my %vars;
# Note: If we suddenly start needing a lot of template_var variables,
# they should move into their own template, not field-descs.
@@ -750,11 +778,7 @@ sub template_var {
sub display_value {
my ($field, $value) = @_;
- my $value_descs = template_var('value_descs');
- if (defined $value_descs->{$field}->{$value}) {
- return $value_descs->{$field}->{$value};
- }
- return $value;
+ return template_var('value_descs')->{$field}->{$value} // $value;
}
sub disable_utf8 {
@@ -763,6 +787,12 @@ sub disable_utf8 {
}
}
+sub enable_utf8 {
+ if (Bugzilla->params->{'utf8'}) {
+ binmode STDOUT, ':utf8'; # Turn on UTF8 encoding.
+ }
+}
+
use constant UTF8_ACCIDENTAL => qw(shiftjis big5-eten euc-kr euc-jp);
sub detect_encoding {
diff --git a/Bugzilla/WebService.pm b/Bugzilla/WebService.pm
index 166707626..86fbe1fe3 100644
--- a/Bugzilla/WebService.pm
+++ b/Bugzilla/WebService.pm
@@ -52,15 +52,20 @@ This is the standard API for external programs that want to interact
with Bugzilla. It provides various methods in various modules.
You can interact with this API via
-L<XML-RPC|Bugzilla::WebService::Server::XMLRPC> or
-L<JSON-RPC|Bugzilla::WebService::Server::JSONRPC>.
+L<XML-RPC|Bugzilla::WebService::Server::XMLRPC>,
+L<JSON-RPC|Bugzilla::WebService::Server::JSONRPC> or
+L<REST|Bugzilla::WebService::Server::REST>.
=head1 CALLING METHODS
-Methods are grouped into "packages", like C<Bug> for
+Methods are grouped into "packages", like C<Bug> for
L<Bugzilla::WebService::Bug>. So, for example,
L<Bugzilla::WebService::Bug/get>, is called as C<Bug.get>.
+For REST, the "package" is more determined by the path
+used to access the resource. See each relevant method
+for specific details on how to access via REST.
+
=head1 PARAMETERS
The Bugzilla API takes the following various types of parameters:
@@ -79,6 +84,11 @@ A floating-point number. May be null.
A string. May be null.
+=item C<email>
+
+A string representing an email address. This value, when returned,
+may be filtered based on if the user is logged in or not. May be null.
+
=item C<dateTime>
A date/time. Represented differently in different interfaces to this API.
@@ -137,7 +147,7 @@ There are various ways to log in:
=item C<User.login>
-You can use L<Bugzilla::WebService::User/login> to log in as a Bugzilla
+You can use L<Bugzilla::WebService::User/login> to log in as a Bugzilla
user. This issues standard HTTP cookies that you must then use in future
calls, so your client must be capable of receiving and transmitting
cookies.
@@ -167,13 +177,24 @@ not expire.
=back
The C<Bugzilla_restrictlogin> and C<Bugzilla_rememberlogin> options
-are only used when you have also specified C<Bugzilla_login> and
+are only used when you have also specified C<Bugzilla_login> and
C<Bugzilla_password>.
Note that Bugzilla will return HTTP cookies along with the method
response when you use these arguments (just like the C<User.login> method
above).
+For REST, you may also use the C<username> and C<password> variable
+names instead of C<Bugzilla_login> and C<Bugzilla_password> as a
+convenience.
+
+=item B<Added in Bugzilla 5.0>
+
+An error is now thrown if you pass invalid cookies or an invalid token.
+You will need to log in again to get new cookies or a new token. Previous
+releases simply ignored invalid cookies and token support was added in
+Bugzilla B<5.0>.
+
=back
=head1 STABLE, EXPERIMENTAL, and UNSTABLE
@@ -258,6 +279,14 @@ be returned, the rest will not be included.
If you specify an empty array, then this function will return empty
hashes.
+Some RPC calls support specifying sub fields.
+
+If an RPC call states that it support sub field restrictions, you can
+restrict what information is returned within the first field. For example,
+if you call Products.get with an include_fields of components.name, then
+only the component name would be returned (and nothing else). You can
+include the main field, and exclude a sub field.
+
Invalid field names are ignored.
Example:
@@ -268,6 +297,9 @@ would return something like:
{ users => [{ id => 1, name => 'user@domain.com' }] }
+Note for REST, C<include_fields> may instead be a comma delimited string
+for GET type requests.
+
=item C<exclude_fields>
C<array> An array of strings, representing the (case-sensitive) names of
@@ -277,6 +309,13 @@ the returned hashes.
If you specify all the fields, then this function will return empty
hashes.
+Some RPC calls support specifying sub fields. If an RPC call states that
+it support sub field restrictions, you can restrict what information is
+returned within the first field. For example, if you call Products.get
+with an include_fields of components.name, then only the component name
+would be returned (and nothing else). You can include the main field,
+and exclude a sub field.
+
Invalid field names are ignored.
Specifying fields here overrides C<include_fields>, so if you specify a
@@ -290,6 +329,37 @@ would return something like:
{ users => [{ id => 1, real_name => 'John Smith' }] }
+Note for REST, C<exclude_fields> may instead be a comma delimited string
+for GET type requests.
+
+=back
+
+There are several shortcut identifiers to ask for only certain groups of
+fields to be returned or excluded.
+
+=over
+
+=item C<_all>
+
+All possible fields are returned if C<_all> is specified in C<include_fields>.
+
+=item C<_default>
+
+These fields are returned if C<include_fields> is empty or C<_default>
+is specified. All fields described in the documentation are returned
+by default unless specified otherwise.
+
+=item C<_extra>
+
+These fields are not returned by default and need to be manually specified
+in C<include_fields> either by field name, or using C<_extra>.
+
+=item C<_custom>
+
+Only custom fields are returned if C<_custom> is specified in C<include_fields>.
+This is normally specific to bug objects and not relevant for other returned
+objects.
+
=back
=head1 SEE ALSO
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm
index eb76b4131..4f5b4c39c 100644
--- a/Bugzilla/WebService/Bug.pm
+++ b/Bugzilla/WebService/Bug.pm
@@ -26,18 +26,27 @@ use strict;
use base qw(Bugzilla::WebService);
use Bugzilla::Comment;
+use Bugzilla::Comment::TagWeights;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Field;
use Bugzilla::WebService::Constants;
-use Bugzilla::WebService::Util qw(filter filter_wants validate);
+use Bugzilla::WebService::Util qw(extract_flags filter filter_wants validate translate);
use Bugzilla::Bug;
use Bugzilla::BugMail;
-use Bugzilla::Util qw(trick_taint trim);
+use Bugzilla::Util qw(trick_taint trim detaint_natural);
use Bugzilla::Version;
use Bugzilla::Milestone;
use Bugzilla::Status;
use Bugzilla::Token qw(issue_hash_token);
+use Bugzilla::Search;
+use Bugzilla::Product;
+use Bugzilla::FlagType;
+use Bugzilla::Search::Quicksearch;
+
+use List::Util qw(max);
+use List::MoreUtils qw(uniq);
+use Storable qw(dclone);
#############
# Constants #
@@ -45,10 +54,25 @@ use Bugzilla::Token qw(issue_hash_token);
use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component);
-use constant DATE_FIELDS => {
- comments => ['new_since'],
- search => ['last_change_time', 'creation_time'],
-};
+sub DATE_FIELDS {
+ my $fields = {
+ comments => ['new_since'],
+ create => [],
+ history => ['new_since'],
+ search => ['last_change_time', 'creation_time'],
+ update => []
+ };
+
+ # Add date related custom fields
+ foreach my $field (Bugzilla->active_custom_fields) {
+ next unless ($field->type == FIELD_TYPE_DATETIME
+ || $field->type == FIELD_TYPE_DATE);
+ push(@{ $fields->{create} }, $field->name);
+ push(@{ $fields->{update} }, $field->name);
+ }
+
+ return $fields;
+}
use constant BASE64_FIELDS => {
add_attachment => ['data'],
@@ -64,6 +88,20 @@ use constant READ_ONLY => qw(
search
);
+use constant ATTACHMENT_MAPPED_SETTERS => {
+ file_name => 'filename',
+ summary => 'description',
+};
+
+use constant ATTACHMENT_MAPPED_RETURNS => {
+ description => 'summary',
+ ispatch => 'is_patch',
+ isprivate => 'is_private',
+ isobsolete => 'is_obsolete',
+ filename => 'file_name',
+ mimetype => 'content_type',
+};
+
######################################################
# Add aliases here for old method name compatibility #
######################################################
@@ -82,6 +120,8 @@ BEGIN {
sub fields {
my ($self, $params) = validate(@_, 'ids', 'names');
+ Bugzilla->switch_to_shadow_db();
+
my @fields;
if (defined $params->{ids}) {
my $ids = $params->{ids};
@@ -117,11 +157,12 @@ sub fields {
my (@values, $has_values);
if ( ($field->is_select and $field->name ne 'product')
- or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS))
+ or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)
+ or $field->name eq 'keywords')
{
$has_values = 1;
@values = @{ $self->_legal_field_values({ field => $field }) };
- }
+ }
if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) {
$value_field = 'product';
@@ -211,6 +252,15 @@ sub _legal_field_values {
}
}
+ elsif ($field_name eq 'keywords') {
+ my @legal_keywords = Bugzilla::Keyword->get_all;
+ foreach my $value (@legal_keywords) {
+ push (@result, {
+ name => $self->type('string', $value->name),
+ description => $self->type('string', $value->description),
+ });
+ }
+ }
else {
my @values = Bugzilla::Field::Choice->type($field)->get_all();
foreach my $value (@values) {
@@ -242,7 +292,7 @@ sub comments {
my $bug_ids = $params->{ids} || [];
my $comment_ids = $params->{comment_ids} || [];
- my $dbh = Bugzilla->dbh;
+ my $dbh = Bugzilla->switch_to_shadow_db();
my $user = Bugzilla->user;
my %bugs;
@@ -289,31 +339,75 @@ sub comments {
return { bugs => \%bugs, comments => \%comments };
}
+sub render_comment {
+ my ($self, $params) = @_;
+
+ unless (defined $params->{text}) {
+ ThrowCodeError('params_required',
+ { function => 'Bug.render_comment',
+ params => ['text'] });
+ }
+
+ Bugzilla->switch_to_shadow_db();
+ my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef;
+
+ my $tmpl = '[% text FILTER quoteUrls(bug) %]';
+ my $html;
+ my $template = Bugzilla->template;
+ $template->process(
+ \$tmpl,
+ { bug => $bug, text => $params->{text}},
+ \$html
+ );
+
+ return { html => $html };
+}
+
# Helper for Bug.comments
sub _translate_comment {
- my ($self, $comment, $filters) = @_;
+ my ($self, $comment, $filters, $types, $prefix) = @_;
my $attach_id = $comment->is_about_attachment ? $comment->extra_data
: undef;
- return filter $filters, {
+
+ my $comment_hash = {
id => $self->type('int', $comment->id),
bug_id => $self->type('int', $comment->bug_id),
- creator => $self->type('string', $comment->author->login),
- author => $self->type('string', $comment->author->login),
+ creator => $self->type('email', $comment->author->login),
+ author => $self->type('email', $comment->author->login),
time => $self->type('dateTime', $comment->creation_ts),
+ creation_time => $self->type('dateTime', $comment->creation_ts),
is_private => $self->type('boolean', $comment->is_private),
text => $self->type('string', $comment->body_full),
attachment_id => $self->type('int', $attach_id),
};
+
+ # Don't load comment tags unless enabled
+ if (Bugzilla->params->{'comment_taggers_group'}) {
+ $comment_hash->{tags} = [
+ map { $self->type('string', $_) }
+ @{ $comment->tags }
+ ];
+ }
+
+ return filter($filters, $comment_hash, $types, $prefix);
}
sub get {
my ($self, $params) = validate(@_, 'ids');
+ Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id;
+
my $ids = $params->{ids};
- defined $ids || ThrowCodeError('param_required', { param => 'ids' });
+ (defined $ids && scalar @$ids)
+ || ThrowCodeError('param_required', { param => 'ids' });
+
+ my (@bugs, @faults, @hashes);
+
+ # Cache permissions for bugs. This highly reduces the number of calls to the DB.
+ # visible_bugs() is only able to handle bug IDs, so we have to skip aliases.
+ my @int = grep { $_ =~ /^\d+$/ } @$ids;
+ Bugzilla->user->visible_bugs(\@int);
- my @bugs;
- my @faults;
foreach my $bug_id (@$ids) {
my $bug;
if ($params->{permissive}) {
@@ -331,10 +425,18 @@ sub get {
else {
$bug = Bugzilla::Bug->check($bug_id);
}
- push(@bugs, $self->_bug_to_hash($bug, $params));
+ push(@bugs, $bug);
+ push(@hashes, $self->_bug_to_hash($bug, $params));
}
- return { bugs => \@bugs, faults => \@faults };
+ # Set the ETag before inserting the update tokens
+ # since the tokens will always be unique even if
+ # the data has not changed.
+ $self->bz_etag(\@hashes);
+
+ $self->_add_update_tokens($params, \@bugs, \@hashes);
+
+ return { bugs => \@hashes, faults => \@faults };
}
# this is a function that gets bug activity for list of bug ids
@@ -343,34 +445,39 @@ sub get {
sub history {
my ($self, $params) = validate(@_, 'ids');
+ Bugzilla->switch_to_shadow_db();
+
my $ids = $params->{ids};
defined $ids || ThrowCodeError('param_required', { param => 'ids' });
- my @return;
+ my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() };
+ $api_name{'bug_group'} = 'groups';
+ my @return;
foreach my $bug_id (@$ids) {
my %item;
my $bug = Bugzilla::Bug->check($bug_id);
$bug_id = $bug->id;
$item{id} = $self->type('int', $bug_id);
- my ($activity) = Bugzilla::Bug::GetBugActivity($bug_id);
+ my ($activity) = Bugzilla::Bug::GetBugActivity($bug_id, undef, $params->{new_since});
my @history;
foreach my $changeset (@$activity) {
my %bug_history;
$bug_history{when} = $self->type('dateTime', $changeset->{when});
- $bug_history{who} = $self->type('string', $changeset->{who});
+ $bug_history{who} = $self->type('email', $changeset->{who});
$bug_history{changes} = [];
foreach my $change (@{ $changeset->{changes} }) {
+ my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname};
my $attach_id = delete $change->{attachid};
if ($attach_id) {
$change->{attachment_id} = $self->type('int', $attach_id);
}
$change->{removed} = $self->type('string', $change->{removed});
$change->{added} = $self->type('string', $change->{added});
- $change->{field_name} = $self->type('string',
- delete $change->{fieldname});
+ $change->{field_name} = $self->type('string', $api_field);
+ delete $change->{fieldname};
push (@{$bug_history{changes}}, $change);
}
@@ -399,77 +506,127 @@ sub history {
sub search {
my ($self, $params) = @_;
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
- if ( defined($params->{offset}) and !defined($params->{limit}) ) {
- ThrowCodeError('param_required',
+ Bugzilla->switch_to_shadow_db();
+
+ my $match_params = dclone($params);
+ delete $match_params->{include_fields};
+ delete $match_params->{exclude_fields};
+
+ # Determine whether this is a quicksearch query
+ if (exists $match_params->{quicksearch}) {
+ my $quicksearch = quicksearch($match_params->{'quicksearch'});
+ my $cgi = Bugzilla::CGI->new($quicksearch);
+ $match_params = $cgi->Vars;
+ }
+
+ if ( defined($match_params->{offset}) and !defined($match_params->{limit}) ) {
+ ThrowCodeError('param_required',
{ param => 'limit', function => 'Bug.search()' });
}
my $max_results = Bugzilla->params->{max_search_results};
- unless (defined $params->{limit} && $params->{limit} == 0) {
- if (!defined $params->{limit} || $params->{limit} > $max_results) {
- $params->{limit} = $max_results;
+ unless (defined $match_params->{limit} && $match_params->{limit} == 0) {
+ if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) {
+ $match_params->{limit} = $max_results;
}
}
else {
- delete $params->{limit};
- delete $params->{offset};
+ delete $match_params->{limit};
+ delete $match_params->{offset};
}
- $params = Bugzilla::Bug::map_fields($params);
- delete $params->{WHERE};
+ $match_params = Bugzilla::Bug::map_fields($match_params);
- unless (Bugzilla->user->is_timetracker) {
- delete $params->{$_} foreach qw(estimated_time remaining_time deadline);
- }
+ my %options = ( fields => ['bug_id'] );
+
+ # Find the highest custom field id
+ my @field_ids = grep(/^f(\d+)$/, keys %$match_params);
+ my $last_field_id = @field_ids ? max @field_ids + 1 : 1;
# Do special search types for certain fields.
- if ( my $bug_when = delete $params->{delta_ts} ) {
- $params->{WHERE}->{'delta_ts >= ?'} = $bug_when;
+ if (my $change_when = delete $match_params->{'delta_ts'}) {
+ $match_params->{"f${last_field_id}"} = 'delta_ts';
+ $match_params->{"o${last_field_id}"} = 'greaterthaneq';
+ $match_params->{"v${last_field_id}"} = $change_when;
+ $last_field_id++;
+ }
+ if (my $creation_when = delete $match_params->{'creation_ts'}) {
+ $match_params->{"f${last_field_id}"} = 'creation_ts';
+ $match_params->{"o${last_field_id}"} = 'greaterthaneq';
+ $match_params->{"v${last_field_id}"} = $creation_when;
+ $last_field_id++;
}
- if (my $when = delete $params->{creation_ts}) {
- $params->{WHERE}->{'creation_ts >= ?'} = $when;
+
+ # Some fields require a search type such as short desc, keywords, etc.
+ foreach my $param (qw(short_desc longdesc status_whiteboard bug_file_loc)) {
+ if (defined $match_params->{$param} && !defined $match_params->{$param . '_type'}) {
+ $match_params->{$param . '_type'} = 'allwordssubstr';
+ }
}
- if (my $summary = delete $params->{short_desc}) {
- my @strings = ref $summary ? @$summary : ($summary);
- my @likes = ("short_desc LIKE ?") x @strings;
- my $clause = join(' OR ', @likes);
- $params->{WHERE}->{"($clause)"} = [map { "\%$_\%" } @strings];
+ if (defined $match_params->{'keywords'} && !defined $match_params->{'keywords_type'}) {
+ $match_params->{'keywords_type'} = 'allwords';
}
- if (my $whiteboard = delete $params->{status_whiteboard}) {
- my @strings = ref $whiteboard ? @$whiteboard : ($whiteboard);
- my @likes = ("status_whiteboard LIKE ?") x @strings;
- my $clause = join(' OR ', @likes);
- $params->{WHERE}->{"($clause)"} = [map { "\%$_\%" } @strings];
+
+ # Backwards compatibility with old method regarding role search
+ $match_params->{'reporter'} = delete $match_params->{'creator'} if $match_params->{'creator'};
+ foreach my $role (qw(assigned_to reporter qa_contact commenter cc)) {
+ next if !exists $match_params->{$role};
+ my $value = delete $match_params->{$role};
+ $match_params->{"f${last_field_id}"} = $role;
+ $match_params->{"o${last_field_id}"} = "anywordssubstr";
+ $match_params->{"v${last_field_id}"} = ref $value ? join(" ", @{$value}) : $value;
+ $last_field_id++;
}
# If no other parameters have been passed other than limit and offset
- # and a WHERE parameter was not created earlier, then we throw error
- # if system is configured to do so.
- if (!$params->{WHERE}
- && !grep(!/(limit|offset)/i, keys %$params)
+ # then we throw error if system is configured to do so.
+ if (!grep(!/^(limit|offset)$/, keys %$match_params)
&& !Bugzilla->params->{search_allow_no_criteria})
{
ThrowUserError('buglist_parameters_required');
}
- # We want include_fields and exclude_fields to be passed to
- # _bug_to_hash but not to Bugzilla::Bug->match so we copy the
- # params and delete those before passing to Bugzilla::Bug->match.
- my %match_params = %{ $params };
- delete $match_params{'include_fields'};
- delete $match_params{'exclude_fields'};
+ $options{order} = [ split(/\s*,\s*/, delete $match_params->{order}) ] if $match_params->{order};
+ $options{params} = $match_params;
- my $bugs = Bugzilla::Bug->match(\%match_params);
- my $visible = Bugzilla->user->visible_bugs($bugs);
- my @hashes = map { $self->_bug_to_hash($_, $params) } @$visible;
- return { bugs => \@hashes };
+ my $search = new Bugzilla::Search(%options);
+ my ($data) = $search->data;
+
+ # BMO if the caller only wants the count, that's all we need to return
+ if ($params->{count_only}) {
+ if (Bugzilla->usage_mode == USAGE_MODE_XMLRPC) {
+ return $data;
+ }
+ else {
+ return { bug_count => $data };
+ }
+ }
+
+ if (!scalar @$data) {
+ return { bugs => [] };
+ }
+
+ # Search.pm won't return bugs that the user shouldn't see so no filtering is needed.
+ my @bug_ids = map { $_->[0] } @$data;
+ my %bug_objects = map { $_->id => $_ } @{ Bugzilla::Bug->new_from_list(\@bug_ids) };
+ my @bugs = map { $bug_objects{$_} } @bug_ids;
+ @bugs = map { $self->_bug_to_hash($_, $params) } @bugs;
+
+ # BzAPI
+ Bugzilla->request_cache->{bzapi_search_bugs} = [ map { $bug_objects{$_} } @bug_ids ];
+
+ return { bugs => \@bugs };
}
sub possible_duplicates {
my ($self, $params) = validate(@_, 'product');
my $user = Bugzilla->user;
+ Bugzilla->switch_to_shadow_db();
+
# Undo the array-ification that validate() does, for "summary".
$params->{summary} || ThrowCodeError('param_required',
{ function => 'Bug.possible_duplicates', param => 'summary' });
@@ -484,12 +641,19 @@ sub possible_duplicates {
{ summary => $params->{summary}, products => \@products,
limit => $params->{limit} });
my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes;
+ $self->_add_update_tokens($params, $possible_dupes, \@hashes);
return { bugs => \@hashes };
}
sub update {
my ($self, $params) = validate(@_, 'ids');
+ # BMO: Don't allow updating of bugs if disabled
+ if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.');
+ }
+
my $user = Bugzilla->login(LOGIN_REQUIRED);
my $dbh = Bugzilla->dbh;
@@ -514,7 +678,8 @@ sub update {
# have valid "set_" functions in Bugzilla::Bug, but shouldn't be
# called using those field names.
delete $values{dependencies};
- delete $values{flags};
+
+ my $flags = delete $values{flags};
foreach my $bug (@bugs) {
if (!$user->can_edit_product($bug->product_obj->id) ) {
@@ -523,6 +688,10 @@ sub update {
}
$bug->set_all(\%values);
+ if ($flags) {
+ my ($old_flags, $new_flags) = extract_flags($flags, $bug);
+ $bug->set_flags($old_flags, $new_flags);
+ }
}
my %all_changes;
@@ -531,7 +700,7 @@ sub update {
$all_changes{$bug->id} = $bug->update();
}
$dbh->bz_commit_transaction();
-
+
foreach my $bug (@bugs) {
$bug->send_changes($all_changes{$bug->id});
}
@@ -584,16 +753,50 @@ sub update {
sub create {
my ($self, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # BMO: Don't allow updating of bugs if disabled
+ if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.');
+ }
+
Bugzilla->login(LOGIN_REQUIRED);
+
+ # Some fields cannot be sent to Bugzilla::Bug->create
+ foreach my $key (qw(login password token)) {
+ delete $params->{$key};
+ }
+
$params = Bugzilla::Bug::map_fields($params);
+
+ my $flags = delete $params->{flags};
+
+ # We start a nested transaction in case flag setting fails
+ # we want the bug creation to roll back as well.
+ $dbh->bz_start_transaction();
+
my $bug = Bugzilla::Bug->create($params);
- Bugzilla::BugMail::Send($bug->bug_id, { changer => $bug->reporter });
+
+ # Set bug flags
+ if ($flags) {
+ my ($flags, $new_flags) = extract_flags($flags, $bug);
+ $bug->set_flags($flags, $new_flags);
+ $bug->update($bug->creation_ts);
+ }
+
+ $dbh->bz_commit_transaction();
+
+ $bug->send_changes();
+
return { id => $self->type('int', $bug->bug_id) };
}
sub legal_values {
my ($self, $params) = @_;
+ Bugzilla->switch_to_shadow_db();
+
defined $params->{field}
or ThrowCodeError('param_required', { param => 'field' });
@@ -646,6 +849,12 @@ sub add_attachment {
my ($self, $params) = validate(@_, 'ids');
my $dbh = Bugzilla->dbh;
+ # BMO: Don't allow updating of bugs if disabled
+ if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.');
+ }
+
Bugzilla->login(LOGIN_REQUIRED);
defined $params->{ids}
|| ThrowCodeError('param_required', { param => 'ids' });
@@ -662,6 +871,8 @@ sub add_attachment {
$dbh->bz_start_transaction();
my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ my $flags = delete $params->{flags};
+
foreach my $bug (@bugs) {
my $attachment = Bugzilla::Attachment->create({
bug => $bug,
@@ -673,6 +884,13 @@ sub add_attachment {
ispatch => $params->{is_patch},
isprivate => $params->{is_private},
});
+
+ if ($flags) {
+ my ($old_flags, $new_flags) = extract_flags($flags, $bug, $attachment);
+ $attachment->set_flags($old_flags, $new_flags);
+ }
+
+ $attachment->update($timestamp);
my $comment = $params->{comment} || '';
$attachment->bug->add_comment($comment,
{ isprivate => $attachment->isprivate,
@@ -691,9 +909,107 @@ sub add_attachment {
return { attachments => \%attachments };
}
+sub update_attachment {
+ my ($self, $params) = validate(@_, 'ids');
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ my $dbh = Bugzilla->dbh;
+
+ my $ids = delete $params->{ids};
+ defined $ids || ThrowCodeError('param_required', { param => 'ids' });
+
+ # Some fields cannot be sent to set_all
+ foreach my $key (qw(login password token)) {
+ delete $params->{$key};
+ }
+
+ $params = translate($params, ATTACHMENT_MAPPED_SETTERS);
+
+ # Get all the attachments, after verifying that they exist and are editable
+ my @attachments = ();
+ my %bugs = ();
+ foreach my $id (@$ids) {
+ my $attachment = Bugzilla::Attachment->new($id)
+ || ThrowUserError("invalid_attach_id", { attach_id => $id });
+ my $bug = $attachment->bug;
+ $attachment->_check_bug;
+ $attachment->validate_can_edit
+ || ThrowUserError("illegal_attachment_edit", { attach_id => $id });
+
+ push @attachments, $attachment;
+ $bugs{$bug->id} = $bug;
+ }
+
+ my $flags = delete $params->{flags};
+ my $comment = delete $params->{comment};
+
+ # Update the values
+ foreach my $attachment (@attachments) {
+ $attachment->set_all($params);
+ if ($flags) {
+ my ($old_flags, $new_flags) = extract_flags($flags, $attachment->bug, $attachment);
+ $attachment->set_flags($old_flags, $new_flags);
+ }
+ }
+
+ $dbh->bz_start_transaction();
+
+ # Do the actual update and get information to return to user
+ my @result;
+ foreach my $attachment (@attachments) {
+ my $changes = $attachment->update();
+
+ if ($comment = trim($comment)) {
+ $attachment->bug->add_comment($comment,
+ { isprivate => $attachment->isprivate,
+ type => CMT_ATTACHMENT_UPDATED,
+ extra_data => $attachment->id });
+ }
+
+ $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS);
+
+ my %hash = (
+ id => $self->type('int', $attachment->id),
+ last_change_time => $self->type('dateTime', $attachment->modification_time),
+ changes => {},
+ );
+
+ foreach my $field (keys %$changes) {
+ my $change = $changes->{$field};
+
+ # We normalize undef to an empty string, so that the API
+ # stays consistent for things like Deadline that can become
+ # empty.
+ $hash{changes}->{$field} = {
+ removed => $self->type('string', $change->[0] // ''),
+ added => $self->type('string', $change->[1] // '')
+ };
+ }
+
+ push(@result, \%hash);
+ }
+
+ $dbh->bz_commit_transaction();
+
+ # Email users about the change
+ foreach my $bug (values %bugs) {
+ $bug->update();
+ $bug->send_changes();
+ }
+
+ # Return the information to the user
+ return { attachments => \@result };
+}
+
sub add_comment {
my ($self, $params) = @_;
+ # BMO: Don't allow updating of bugs if disabled
+ if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.');
+ }
+
#The user must login in order add a comment
Bugzilla->login(LOGIN_REQUIRED);
@@ -738,6 +1054,12 @@ sub add_comment {
sub update_see_also {
my ($self, $params) = @_;
+ # BMO: Don't allow updating of bugs if disabled
+ if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.');
+ }
+
my $user = Bugzilla->login(LOGIN_REQUIRED);
# Check parameters
@@ -785,6 +1107,8 @@ sub update_see_also {
sub attachments {
my ($self, $params) = validate(@_, 'ids', 'attachment_ids');
+ Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id;
+
if (!(defined $params->{ids}
or defined $params->{attachment_ids}))
{
@@ -821,6 +1145,105 @@ sub attachments {
return { bugs => \%bugs, attachments => \%attachments };
}
+sub flag_types {
+ my ($self, $params) = @_;
+ my $dbh = Bugzilla->switch_to_shadow_db();
+ my $user = Bugzilla->user;
+
+ defined $params->{product}
+ || ThrowCodeError('param_required',
+ { function => 'Bug.flag_types',
+ param => 'product' });
+
+ my $product = delete $params->{product};
+ my $component = delete $params->{component};
+
+ $product = Bugzilla::Product->check({ name => $product, cache => 1 });
+ $component = Bugzilla::Component->check(
+ { name => $component, product => $product, cache => 1 }) if $component;
+
+ my $flag_params = { product_id => $product->id };
+ $flag_params->{component_id} = $component->id if $component;
+ my $matched_flag_types = Bugzilla::FlagType::match($flag_params);
+
+ my $flag_types = { bug => [], attachment => [] };
+ foreach my $flag_type (@$matched_flag_types) {
+ push(@{ $flag_types->{bug} }, $self->_flagtype_to_hash($flag_type, $product))
+ if $flag_type->target_type eq 'bug';
+ push(@{ $flag_types->{attachment} }, $self->_flagtype_to_hash($flag_type, $product))
+ if $flag_type->target_type eq 'attachment';
+ }
+
+ return $flag_types;
+}
+
+sub update_comment_tags {
+ my ($self, $params) = @_;
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ Bugzilla->params->{'comment_taggers_group'}
+ || ThrowUserError("comment_tag_disabled");
+ $user->can_tag_comments
+ || ThrowUserError("auth_failure",
+ { group => Bugzilla->params->{'comment_taggers_group'},
+ action => "update",
+ object => "comment_tags" });
+
+ my $comment_id = $params->{comment_id}
+ // ThrowCodeError('param_required',
+ { function => 'Bug.update_comment_tags',
+ param => 'comment_id' });
+
+ my $comment = Bugzilla::Comment->new($comment_id)
+ || return [];
+ $comment->bug->check_is_visible();
+ if ($comment->is_private && !$user->is_insider) {
+ ThrowUserError('comment_is_private', { id => $comment_id });
+ }
+
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
+ foreach my $tag (@{ $params->{add} || [] }) {
+ $comment->add_tag($tag) if defined $tag;
+ }
+ foreach my $tag (@{ $params->{remove} || [] }) {
+ $comment->remove_tag($tag) if defined $tag;
+ }
+ $comment->update();
+ $dbh->bz_commit_transaction();
+
+ return $comment->tags;
+}
+
+sub search_comment_tags {
+ my ($self, $params) = @_;
+
+ Bugzilla->login(LOGIN_REQUIRED);
+ Bugzilla->params->{'comment_taggers_group'}
+ || ThrowUserError("comment_tag_disabled");
+ Bugzilla->user->can_tag_comments
+ || ThrowUserError("auth_failure", { group => Bugzilla->params->{'comment_taggers_group'},
+ action => "search",
+ object => "comment_tags"});
+
+ my $query = $params->{query};
+ $query
+ // ThrowCodeError('param_required', { param => 'query' });
+ my $limit = $params->{limit} || 7;
+ detaint_natural($limit)
+ || ThrowCodeError('param_must_be_numeric', { param => 'limit',
+ function => 'Bug.search_comment_tags' });
+
+
+ my $tags = Bugzilla::Comment::TagWeights->match({
+ WHERE => {
+ 'tag LIKE ?' => "\%$query\%",
+ },
+ LIMIT => $limit,
+ });
+ return [ map { $_->tag } @$tags ];
+}
+
##############################
# Private Helper Subroutines #
##############################
@@ -836,18 +1259,13 @@ sub _bug_to_hash {
# All the basic bug attributes are here, in alphabetical order.
# A bug attribute is "basic" if it doesn't require an additional
# database call to get the info.
- my %item = (
+ my %item = %{ filter $params, {
alias => $self->type('string', $bug->alias),
- classification => $self->type('string', $bug->classification),
- component => $self->type('string', $bug->component),
- creation_time => $self->type('dateTime', $bug->creation_ts),
id => $self->type('int', $bug->bug_id),
is_confirmed => $self->type('boolean', $bug->everconfirmed),
- last_change_time => $self->type('dateTime', $bug->delta_ts),
op_sys => $self->type('string', $bug->op_sys),
platform => $self->type('string', $bug->rep_platform),
priority => $self->type('string', $bug->priority),
- product => $self->type('string', $bug->product),
resolution => $self->type('string', $bug->resolution),
severity => $self->type('string', $bug->bug_severity),
status => $self->type('string', $bug->bug_status),
@@ -856,25 +1274,35 @@ sub _bug_to_hash {
url => $self->type('string', $bug->bug_file_loc),
version => $self->type('string', $bug->version),
whiteboard => $self->type('string', $bug->status_whiteboard),
- );
+ } };
-
- # First we handle any fields that require extra SQL calls.
- # We don't do the SQL calls at all if the filter would just
- # eliminate them anyway.
+ # First we handle any fields that require extra work (such as date parsing
+ # or SQL calls).
if (filter_wants $params, 'assigned_to') {
- $item{'assigned_to'} = $self->type('string', $bug->assigned_to->login);
+ $item{'assigned_to'} = $self->type('email', $bug->assigned_to->login);
+ $item{'assigned_to_detail'} = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to');
}
if (filter_wants $params, 'blocks') {
my @blocks = map { $self->type('int', $_) } @{ $bug->blocked };
$item{'blocks'} = \@blocks;
}
+ if (filter_wants $params, 'classification') {
+ $item{classification} = $self->type('string', $bug->classification);
+ }
+ if (filter_wants $params, 'component') {
+ $item{component} = $self->type('string', $bug->component);
+ }
if (filter_wants $params, 'cc') {
- my @cc = map { $self->type('string', $_) } @{ $bug->cc || [] };
+ my @cc = map { $self->type('email', $_) } @{ $bug->cc || [] };
$item{'cc'} = \@cc;
+ $item{'cc_detail'} = [ map { $self->_user_to_hash($_, $params, undef, 'cc') } @{ $bug->cc_users } ];
+ }
+ if (filter_wants $params, 'creation_time') {
+ $item{'creation_time'} = $self->type('dateTime', $bug->creation_ts);
}
if (filter_wants $params, 'creator') {
- $item{'creator'} = $self->type('string', $bug->reporter->login);
+ $item{'creator'} = $self->type('email', $bug->reporter->login);
+ $item{'creator_detail'} = $self->_user_to_hash($bug->reporter, $params, undef, 'creator');
}
if (filter_wants $params, 'depends_on') {
my @depends_on = map { $self->type('int', $_) } @{ $bug->dependson };
@@ -896,25 +1324,40 @@ sub _bug_to_hash {
@{ $bug->keyword_objects };
$item{'keywords'} = \@keywords;
}
+ if (filter_wants $params, 'last_change_time') {
+ $item{'last_change_time'} = $self->type('dateTime', $bug->delta_ts);
+ }
+ if (filter_wants $params, 'product') {
+ $item{product} = $self->type('string', $bug->product);
+ }
if (filter_wants $params, 'qa_contact') {
my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : '';
- $item{'qa_contact'} = $self->type('string', $qa_login);
+ $item{'qa_contact'} = $self->type('email', $qa_login);
+ if ($bug->qa_contact) {
+ $item{'qa_contact_detail'} = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact');
+ }
}
if (filter_wants $params, 'see_also') {
my @see_also = map { $self->type('string', $_->name) }
@{ $bug->see_also };
$item{'see_also'} = \@see_also;
}
+ if (filter_wants $params, 'flags') {
+ $item{'flags'} = [ map { $self->_flag_to_hash($_) } @{$bug->flags} ];
+ }
# And now custom fields
- my @custom_fields = Bugzilla->active_custom_fields;
+ my @custom_fields = Bugzilla->active_custom_fields({
+ product => $bug->product_obj, component => $bug->component_obj, bug_id => $bug->id });
foreach my $field (@custom_fields) {
my $name = $field->name;
- next if !filter_wants $params, $name;
+ next if !filter_wants($params, $name, ['default', 'custom']);
if ($field->type == FIELD_TYPE_BUG_ID) {
$item{$name} = $self->type('int', $bug->$name);
}
- elsif ($field->type == FIELD_TYPE_DATETIME) {
+ elsif ($field->type == FIELD_TYPE_DATETIME
+ || $field->type == FIELD_TYPE_DATE)
+ {
$item{$name} = $self->type('dateTime', $bug->$name);
}
elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
@@ -928,33 +1371,55 @@ sub _bug_to_hash {
# Timetracking fields are only sent if the user can see them.
if (Bugzilla->user->is_timetracker) {
- $item{'estimated_time'} = $self->type('double', $bug->estimated_time);
- $item{'remaining_time'} = $self->type('double', $bug->remaining_time);
- # No need to format $bug->deadline specially, because Bugzilla::Bug
- # already does it for us.
- $item{'deadline'} = $self->type('string', $bug->deadline);
- }
-
- if (Bugzilla->user->id) {
- my $token = issue_hash_token([$bug->id, $bug->delta_ts]);
- $item{'update_token'} = $self->type('string', $token);
+ if (filter_wants $params, 'estimated_time') {
+ $item{'estimated_time'} = $self->type('double', $bug->estimated_time);
+ }
+ if (filter_wants $params, 'remaining_time') {
+ $item{'remaining_time'} = $self->type('double', $bug->remaining_time);
+ }
+ if (filter_wants $params, 'deadline') {
+ # No need to format $bug->deadline specially, because Bugzilla::Bug
+ # already does it for us.
+ $item{'deadline'} = $self->type('string', $bug->deadline);
+ }
+ if (filter_wants $params, 'actual_time') {
+ $item{'actual_time'} = $self->type('double', $bug->actual_time);
+ }
}
# The "accessible" bits go here because they have long names and it
# makes the code look nicer to separate them out.
- $item{'is_cc_accessible'} = $self->type('boolean',
- $bug->cclist_accessible);
- $item{'is_creator_accessible'} = $self->type('boolean',
- $bug->reporter_accessible);
+ if (filter_wants $params, 'is_cc_accessible') {
+ $item{'is_cc_accessible'} = $self->type('boolean', $bug->cclist_accessible);
+ }
+ if (filter_wants $params, 'is_creator_accessible') {
+ $item{'is_creator_accessible'} = $self->type('boolean', $bug->reporter_accessible);
+ }
- return filter $params, \%item;
+ # BMO - support for special mentors field
+ if (filter_wants $params, 'mentors') {
+ $item{'mentors'}
+ = [ map { $self->type('email', $_->login) } @{ $bug->mentors || [] } ];
+ $item{'mentors_detail'}
+ = [ map { $self->_user_to_hash($_, $params, undef, 'mentors') } @{ $bug->mentors } ];
+ }
+
+ return \%item;
}
-sub _attachment_to_hash {
- my ($self, $attach, $filters) = @_;
+sub _user_to_hash {
+ my ($self, $user, $filters, $types, $prefix) = @_;
+ my $item = filter $filters, {
+ id => $self->type('int', $user->id),
+ real_name => $self->type('string', $user->name),
+ name => $self->type('email', $user->login),
+ email => $self->type('email', $user->email),
+ }, $types, $prefix;
+ return $item;
+}
- # Skipping attachment flags for now.
- delete $attach->{flags};
+sub _attachment_to_hash {
+ my ($self, $attach, $filters, $types, $prefix) = @_;
my $item = filter $filters, {
creation_time => $self->type('dateTime', $attach->attached),
@@ -968,23 +1433,114 @@ sub _attachment_to_hash {
is_private => $self->type('int', $attach->isprivate),
is_obsolete => $self->type('int', $attach->isobsolete),
is_patch => $self->type('int', $attach->ispatch),
- };
+ }, $types, $prefix;
# creator/attacher require an extra lookup, so we only send them if
# the filter wants them.
foreach my $field (qw(creator attacher)) {
- if (filter_wants $filters, $field) {
- $item->{$field} = $self->type('string', $attach->attacher->login);
+ if (filter_wants $filters, $field, $types, $prefix) {
+ $item->{$field} = $self->type('email', $attach->attacher->login);
}
}
- if (filter_wants $filters, 'data') {
+ if (filter_wants $filters, 'data', $types, $prefix) {
$item->{'data'} = $self->type('base64', $attach->data);
}
+ if (filter_wants $filters, 'size', $types, $prefix) {
+ $item->{'size'} = $self->type('int', $attach->datasize);
+ }
+
+ if (filter_wants $filters, 'flags', $types, $prefix) {
+ $item->{'flags'} = [ map { $self->_flag_to_hash($_) } @{$attach->flags} ];
+ }
+
+ return $item;
+}
+
+sub _flag_to_hash {
+ my ($self, $flag) = @_;
+
+ my $item = {
+ id => $self->type('int', $flag->id),
+ name => $self->type('string', $flag->name),
+ type_id => $self->type('int', $flag->type_id),
+ creation_date => $self->type('dateTime', $flag->creation_date),
+ modification_date => $self->type('dateTime', $flag->modification_date),
+ status => $self->type('string', $flag->status)
+ };
+
+ foreach my $field (qw(setter requestee)) {
+ my $field_id = $field . "_id";
+ $item->{$field} = $self->type('email', $flag->$field->login)
+ if $flag->$field_id;
+ }
+
+ return $item;
+}
+
+sub _flagtype_to_hash {
+ my ($self, $flagtype, $product) = @_;
+ my $user = Bugzilla->user;
+
+ my @values = ('X');
+ push(@values, '?') if ($flagtype->is_requestable && $user->can_request_flag($flagtype));
+ push(@values, '+', '-') if $user->can_set_flag($flagtype);
+
+ my $item = {
+ id => $self->type('int' , $flagtype->id),
+ name => $self->type('string' , $flagtype->name),
+ description => $self->type('string' , $flagtype->description),
+ type => $self->type('string' , $flagtype->target_type),
+ values => \@values,
+ is_active => $self->type('boolean', $flagtype->is_active),
+ is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble),
+ is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable)
+ };
+
+ if ($product) {
+ my $inclusions = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $product->id);
+ my $exclusions = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $product->id);
+ # if we have both inclusions and exclusions, the exclusions are redundant
+ $exclusions = [] if @$inclusions && @$exclusions;
+ # no need to return anything if there's just "any component"
+ $item->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne '';
+ $item->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne '';
+ }
+
return $item;
}
+sub _flagtype_clusions_to_hash {
+ my ($self, $clusions, $product_id) = @_;
+ my $result = [];
+ foreach my $key (keys %$clusions) {
+ my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2);
+ if ($prod_id == 0 || $prod_id == $product_id) {
+ if ($comp_id) {
+ my $component = Bugzilla::Component->new({ id => $comp_id, cache => 1 });
+ push @$result, $component->name;
+ }
+ else {
+ return [ '' ];
+ }
+ }
+ }
+ return $result;
+}
+
+sub _add_update_tokens {
+ my ($self, $params, $bugs, $hashes) = @_;
+
+ return if !Bugzilla->user->id;
+ return if !filter_wants($params, 'update_token');
+
+ for(my $i = 0; $i < @$bugs; $i++) {
+ my $token = issue_hash_token([$bugs->[$i]->id, $bugs->[$i]->delta_ts]);
+ $hashes->[$i]->{'update_token'} = $self->type('string', $token);
+ }
+}
+
1;
__END__
@@ -1004,6 +1560,10 @@ or get information about bugs that have already been filed.
See L<Bugzilla::WebService> for a description of how parameters are passed,
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
+Although the data input and output is the same for JSONRPC, XMLRPC and REST,
+the directions for how to access the data via REST is noted in each method
+where applicable.
+
=head1 Utility Functions
=head2 fields
@@ -1017,11 +1577,26 @@ B<UNSTABLE>
Get information about valid bug fields, including the lists of legal values
for each field.
+=item B<REST>
+
+You have several options for retreiving information about fields. The first
+part is the request method and the rest is the related path needed.
+
+To get information about all fields:
+
+GET /field/bug
+
+To get information related to a single field:
+
+GET /field/bug/<id_or_name>
+
+The returned data format is the same as below.
+
=item B<Params>
You can pass either field ids or field names.
-B<Note>: If neither C<ids> nor C<names> is specified, then all
+B<Note>: If neither C<ids> nor C<names> is specified, then all
non-obsolete fields will be returned.
In addition to the parameters below, this method also accepts the
@@ -1120,7 +1695,7 @@ values of the field are shown in the user interface. Can be null.
This is an array of hashes, representing the legal values for
select-type (drop-down and multiple-selection) fields. This is also
-populated for the C<component>, C<version>, and C<target_milestone>
+populated for the C<component>, C<version>, C<target_milestone>, and C<keywords>
fields, but not for the C<product> field (you must use
L<Product.get_accessible_products|Bugzilla::WebService::Product/get_accessible_products>
for that.
@@ -1153,6 +1728,11 @@ if the C<value_field> is set to one of the values listed in this array.
Note that for per-product fields, C<value_field> is set to C<'product'>
and C<visibility_values> will reflect which product(s) this value appears in.
+=item C<description>
+
+C<string> The description of the value. This item is only included for the
+C<keywords> field.
+
=item C<is_open>
C<boolean> For C<bug_status> values, determines whether this status
@@ -1206,8 +1786,107 @@ You specified an invalid field name or id.
=back
+=item REST API call added in Bugzilla B<5.0>
+
=back
+=head2 flag_types
+
+B<UNSTABLE>
+
+=over
+
+=item B<Description>
+
+Get information about valid flag types that can be set for bugs and attachments.
+
+=item B<REST>
+
+You have several options for retreiving information about flag types. The first
+part is the request method and the rest is the related path needed.
+
+To get information about all flag types for a product:
+
+GET /flag_types/<product>
+
+To get information about flag_types for a product and component:
+
+GET /flag_types/<product>/<component>
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+You must pass a product name and an optional component name.
+
+=over
+
+=item C<product> (string) - The name of a valid product.
+
+=item C<component> (string) - An optional valid component name associated with the product.
+
+=back
+
+=item B<Returns>
+
+A hash containing two keys, C<bug> and C<attachment>. Each key value is an array of hashes,
+containing the following keys:
+
+=over
+
+=item C<id>
+
+C<int> An integer id uniquely identifying this flag type.
+
+=item C<name>
+
+C<string> The name for the flag type.
+
+=item C<type>
+
+C<string> The target of the flag type which is either C<bug> or C<attachment>.
+
+=item C<description>
+
+C<string> The description of the flag type.
+
+=item C<values>
+
+C<array> An array of string values that the user can set on the flag type.
+
+=item C<is_requesteeble>
+
+C<boolean> Users can ask specific other users to set flags of this type.
+
+=item C<is_multiplicable>
+
+C<boolean> Multiple flags of this type can be set for the same bug or attachment.
+
+=back
+
+=item B<Errors>
+
+=over
+
+=item 106 (Product Access Denied)
+
+Either the product does not exist or you don't have access to it.
+
+=item 51 (Invalid Component)
+
+The component provided does not exist in the product.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<5.0>.
+
+=back
+
+=back
=head2 legal_values
@@ -1219,6 +1898,18 @@ B<DEPRECATED> - Use L</fields> instead.
Tells you what values are allowed for a particular field.
+=item B<REST>
+
+To get information on the values for a field based on field name:
+
+GET /field/bug/<field_name>/values
+
+To get information based on field name and a specific product:
+
+GET /field/bug/<field_name>/<product_id>/values
+
+The returned data format is the same as below.
+
=item B<Params>
=over
@@ -1251,6 +1942,14 @@ You specified a field that doesn't exist or isn't a drop-down field.
=back
+=item B<History>
+
+=over
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
=back
=head1 Bug Information
@@ -1269,6 +1968,18 @@ and/or attachment ids.
B<Note>: Private attachments will only be returned if you are in the
insidergroup or if you are the submitter of the attachment.
+=item B<REST>
+
+To get all current attachments for a bug:
+
+GET /bug/<bug_id>/attachment
+
+To get a specific attachment based on attachment ID:
+
+GET /bug/attachment/<attachment_id>
+
+The returned data format is the same as below.
+
=item B<Params>
B<Note>: At least one of C<ids> or C<attachment_ids> is required.
@@ -1329,6 +2040,10 @@ diagram above) are:
C<base64> The raw data of the attachment, encoded as Base64.
+=item C<size>
+
+C<int> The length (in bytes) of the attachment.
+
=item C<creation_time>
C<dateTime> The time the attachment was created.
@@ -1382,6 +2097,48 @@ Also returned as C<attacher>, for backwards-compatibility with older
Bugzillas. (However, this backwards-compatibility will go away in Bugzilla
5.0.)
+=item C<flags>
+
+An array of hashes containing the information about flags currently set
+for each attachment. Each flag hash contains the following items:
+
+=over
+
+=item C<id>
+
+C<int> The id of the flag.
+
+=item C<name>
+
+C<string> The name of the flag.
+
+=item C<type_id>
+
+C<int> The type id of the flag.
+
+=item C<creation_date>
+
+C<dateTime> The timestamp when this flag was originally created.
+
+=item C<modification_date>
+
+C<dateTime> The timestamp when the flag was last modified.
+
+=item C<status>
+
+C<string> The current status of the flag.
+
+=item C<setter>
+
+C<string> The login name of the user who created or last modified the flag.
+
+=item C<requestee>
+
+C<string> The login name of the user this flag has been requested to be granted or denied.
+Note, this field is only returned if a requestee is set.
+
+=back
+
=back
=item B<Errors>
@@ -1418,6 +2175,10 @@ C<summary>.
=back
+=item The C<flags> array was added in Bugzilla B<4.4>.
+
+=item REST API call added in Bugzilla B<5.0>.
+
=back
@@ -1432,6 +2193,18 @@ B<STABLE>
This allows you to get data about comments, given a list of bugs
and/or comment ids.
+=item B<REST>
+
+To get all comments for a particular bug using the bug ID or alias:
+
+GET /bug/<id_or_alias>/comment
+
+To get a specific comment based on the comment ID:
+
+GET /bug/comment/<comment_id>
+
+The returned data format is the same as below.
+
=item B<Params>
B<Note>: At least one of C<ids> or C<comment_ids> is required.
@@ -1522,6 +2295,13 @@ Bugzillas. (However, this backwards-compatibility will go away in Bugzilla
C<dateTime> The time (in Bugzilla's timezone) that the comment was added.
+=item creation_time
+
+C<dateTime> This is exactly same as the C<time> key. Use this field instead of
+C<time> for consistency with other methods including L</get> and L</attachments>.
+For compatibility, C<time> is still usable. However, please note that C<time>
+may be deprecated and removed in a future release.
+
=item is_private
C<boolean> True if this comment is private (only visible to a certain
@@ -1563,6 +2343,10 @@ C<creator>.
=back
+=item C<creation_time> was added in Bugzilla B<4.4>.
+
+=item REST API call added in Bugzilla B<5.0>.
+
=back
@@ -1578,6 +2362,14 @@ Gets information about particular bugs in the database.
Note: Can also be called as "get_bugs" for compatibilty with Bugzilla 3.0 API.
+=item B<REST>
+
+To get information about a particular bug using its ID or alias:
+
+GET /bug/<id_or_alias>
+
+The returned data format is the same as below.
+
=item B<Params>
In addition to the parameters below, this method also accepts the
@@ -1620,8 +2412,18 @@ Two items are returned:
An array of hashes that contains information about the bugs with
the valid ids. Each hash contains the following items:
+These fields are returned by default or by specifying C<_default>
+in C<include_fields>.
+
=over
+=item C<actual_time>
+
+C<double> The total number of hours that this bug has taken (so far).
+
+If you are not in the time-tracking group, this field will not be included
+in the return value.
+
=item C<alias>
C<string> The unique alias of this bug.
@@ -1630,6 +2432,11 @@ C<string> The unique alias of this bug.
C<string> The login name of the user to whom the bug is assigned.
+=item C<assigned_to_detail>
+
+C<hash> A hash containing detailed user information for the assigned_to. To see the
+keys included in the user detail hash, see below.
+
=item C<blocks>
C<array> of C<int>s. The ids of bugs that are "blocked" by this bug.
@@ -1639,6 +2446,11 @@ C<array> of C<int>s. The ids of bugs that are "blocked" by this bug.
C<array> of C<string>s. The login names of users on the CC list of this
bug.
+=item C<cc_detail>
+
+C<array> of hashes containing detailed user information for each of the cc list
+members. To see the keys included in the user detail hash, see below.
+
=item C<classification>
C<string> The name of the current classification the bug is in.
@@ -1655,6 +2467,11 @@ C<dateTime> When the bug was created.
C<string> The login name of the person who filed this bug (the reporter).
+=item C<creator_detail>
+
+C<hash> A hash containing detailed user information for the creator. To see the
+keys included in the user detail hash, see below.
+
=item C<deadline>
C<string> The day that this bug is due to be completed, in the format
@@ -1680,6 +2497,48 @@ take.
If you are not in the time-tracking group, this field will not be included
in the return value.
+=item C<flags>
+
+An array of hashes containing the information about flags currently set
+for the bug. Each flag hash contains the following items:
+
+=over
+
+=item C<id>
+
+C<int> The id of the flag.
+
+=item C<name>
+
+C<string> The name of the flag.
+
+=item C<type_id>
+
+C<int> The type id of the flag.
+
+=item C<creation_date>
+
+C<dateTime> The timestamp when this flag was originally created.
+
+=item C<modification_date>
+
+C<dateTime> The timestamp when the flag was last modified.
+
+=item C<status>
+
+C<string> The current status of the flag.
+
+=item C<setter>
+
+C<string> The login name of the user who created or last modified the flag.
+
+=item C<requestee>
+
+C<string> The login name of the user this flag has been requested to be granted or denied.
+Note, this field is only returned if a requestee is set.
+
+=back
+
=item C<groups>
C<array> of C<string>s. The names of all the groups that this bug is in.
@@ -1737,6 +2596,11 @@ C<string> The name of the product this bug is in.
C<string> The login name of the current QA Contact on the bug.
+=item C<qa_contact_detail>
+
+C<hash> A hash containing detailed user information for the qa_contact. To see the
+keys included in the user detail hash, see below.
+
=item C<remaining_time>
C<double> The number of hours of work remaining until work on this bug
@@ -1800,7 +2664,11 @@ C<string> The value of the "status whiteboard" field on the bug.
Every custom field in this installation will also be included in the
return value. Most fields are returned as C<string>s. However, some
-field types have different return values:
+field types have different return values.
+
+Normally custom fields are returned by default similar to normal bug
+fields or you can specify only custom fields by using C<_custom> in
+C<include_fields>.
=over
@@ -1812,6 +2680,30 @@ field types have different return values:
=back
+=item I<user detail hashes>
+
+Each user detail hash contains the following items:
+
+=over
+
+=item C<id>
+
+C<int> The user id for this user.
+
+=item C<real_name>
+
+C<string> The 'real' name for this user, if any.
+
+=item C<name>
+
+C<string> The user's Bugzilla login.
+
+=item C<email>
+
+C<string> The user's email address. Currently this is the same value as the name.
+
+=back
+
=back
=item C<faults> B<EXPERIMENTAL>
@@ -1869,6 +2761,8 @@ You do not have access to the bug_id you specified.
=item The following properties were added to this method's return values
in Bugzilla B<3.4>:
+=item REST API call added in Bugzilla B<5.0>
+
=over
=item For C<bugs>
@@ -1907,8 +2801,12 @@ C<op_sys>, C<platform>, C<qa_contact>, C<remaining_time>, C<see_also>,
C<target_milestone>, C<update_token>, C<url>, C<version>, C<whiteboard>,
and all custom fields.
-=back
+=item The C<flags> array was added in Bugzilla B<4.4>.
+=item The C<actual_time> item was added to the C<bugs> return value
+in Bugzilla B<4.4>.
+
+=back
=back
@@ -1922,6 +2820,14 @@ B<EXPERIMENTAL>
Gets the history of changes for particular bugs in the database.
+=item B<REST>
+
+To get the history for a specific bug ID:
+
+GET /bug/<bug_id>/history
+
+The returned data format will be the same as below.
+
=item B<Params>
=over
@@ -1933,7 +2839,12 @@ An array of numbers and strings.
If an element in the array is entirely numeric, it represents a bug_id
from the Bugzilla database to fetch. If it contains any non-numeric
characters, it is considered to be a bug alias instead, and the data bug
-with that alias will be loaded.
+with that alias will be loaded.
+
+item C<new_since>
+
+C<dateTime> If specified, the method will only return changes I<newer>
+than this time.
Note that it's possible for aliases to be disabled in Bugzilla, in which
case you will be told that you have specified an invalid bug_id if you
@@ -2002,6 +2913,8 @@ present in this hash.
=back
+=item REST API call added Bugzilla B<5.0>.
+
=back
=item B<Errors>
@@ -2014,6 +2927,10 @@ The same as L</get>.
=item Added in Bugzilla B<3.4>.
+=item Field names changed to be more consistent with other methods in Bugzilla B<4.4>.
+
+=item As of Bugzilla B<4.4>, field names now match names used by L<Bug.update|/"update"> for consistency.
+
=back
=back
@@ -2082,6 +2999,14 @@ B<UNSTABLE>
Allows you to search for bugs based on particular criteria.
+=item <REST>
+
+To search for bugs:
+
+GET /bug
+
+The URL parameters and the returned data format are the same as below.
+
=item B<Params>
Unless otherwise specified in the description of a parameter, bugs are
@@ -2102,10 +3027,19 @@ the "Foo" or "Bar" products, you'd pass:
product => ['Foo', 'Bar']
Some Bugzillas may treat your arguments case-sensitively, depending
-on what database system they are using. Most commonly, though, Bugzilla is
-not case-sensitive with the arguments passed (because MySQL is the
+on what database system they are using. Most commonly, though, Bugzilla is
+not case-sensitive with the arguments passed (because MySQL is the
most-common database to use with Bugzilla, and MySQL is not case sensitive).
+In addition to the fields listed below, you may also use criteria that
+is similar to what is used in the Advanced Search screen of the Bugzilla
+UI. This includes fields specified by C<Search by Change History> and
+C<Custom Search>. The easiest way to determine what the field names are and what
+format Bugzilla expects, is to first construct your query using the
+Advanced Search UI, execute it and use the query parameters in they URL
+as your key/value pairs for the WebService call. With REST, you can
+just reuse the query parameter portion in the REST call itself.
+
=over
=item C<alias>
@@ -2230,6 +3164,15 @@ C<string> Search the "Status Whiteboard" field on bugs for a substring.
Works the same as the C<summary> field described above, but searches the
Status Whiteboard field.
+=item C<count_only>
+
+C<boolean> If count_only set to true, only a single hash key called C<bug_count>
+will be returned which is the number of bugs that matched the search.
+
+=item C<quicksearch>
+
+C<string> Search for bugs using quicksearch syntax.
+
=back
=item B<Returns>
@@ -2269,6 +3212,13 @@ in Bugzilla B<4.0>.
C<limit> is set equal to zero. Otherwise maximum results returned are limited
by system configuration.
+=item REST API call added in Bugzilla B<5.0>.
+
+=item Updated to allow for full search capability similar to the Bugzilla UI
+in Bugzilla B<5.0>.
+
+=item Updated to allow quicksearch capability in Bugzilla B<5.0>.
+
=back
=back
@@ -2295,10 +3245,19 @@ The WebService interface may allow you to set things other than those listed
here, but realize that anything undocumented is B<UNSTABLE> and will very
likely change in the future.
+=item B<REST>
+
+To create a new bug in Bugzilla:
+
+POST /bug
+
+The params to include in the POST body as well as the returned data format,
+are the same as below.
+
=item B<Params>
Some params must be set, or an error will be thrown. These params are
-marked B<Required>.
+marked B<Required>.
Some parameters can have defaults set in Bugzilla, by the administrator.
If these parameters have defaults set, you can omit them. These parameters
@@ -2372,6 +3331,32 @@ Note that only certain statuses can be set on bug creation.
=item C<target_milestone> (string) - A valid target milestone for this
product.
+=item C<flags>
+
+C<array> An array of hashes with flags to add to the bug. To create a flag,
+at least the status and the type_id or name must be provided. An optional
+requestee can be passed if the flag type is requestable to a specific user.
+
+=over
+
+=item C<name>
+
+C<string> The name of the flag type.
+
+=item C<type_id>
+
+C<int> The internal flag type id.
+
+=item C<status>
+
+C<string> The flags new status (i.e. "?", "+", "-" or "X" to clear a flag).
+
+=item C<requestee>
+
+C<string> The login of the requestee if the flag type is requestable to a specific user.
+
+=back
+
=back
In addition to the above parameters, if your installation has any custom
@@ -2424,6 +3409,28 @@ that would cause a circular dependency between bugs.
You tried to restrict the bug to a group which does not exist, or which
you cannot use with this product.
+=item 129 (Flag Status Invalid)
+
+The flag status is invalid.
+
+=item 130 (Flag Modification Denied)
+
+You tried to request, grant, or deny a flag but only a user with the required
+permissions may make the change.
+
+=item 131 (Flag not Requestable from Specific Person)
+
+You can't ask a specific person for the flag.
+
+=item 133 (Flag Type not Unique)
+
+The flag type specified matches several flag types. You must specify
+the type id value to update or add a flag.
+
+=item 134 (Inactive Flag Type)
+
+The flag type is inactive and cannot be used to create new flags.
+
=item 504 (Invalid User)
Either the QA Contact, Assignee, or CC lists have some invalid user
@@ -2453,6 +3460,8 @@ loop errors had a generic code of C<32000>.
=back
+=item REST API call added in Bugzilla B<5.0>.
+
=back
@@ -2466,6 +3475,16 @@ B<UNSTABLE>
This allows you to add an attachment to a bug in Bugzilla.
+=item B<REST>
+
+To create attachment on a current bug:
+
+POST /bug/<bug_id>/attachment
+
+The params to include in the POST body, as well as the returned
+data format are the same as below. The C<ids> param will be
+overridden as it it pulled from the URL path.
+
=item B<Params>
=over
@@ -2517,6 +3536,32 @@ to the "insidergroup"), False if the attachment should be public.
Defaults to False if not specified.
+=item C<flags>
+
+C<array> An array of hashes with flags to add to the attachment. to create a flag,
+at least the status and the type_id or name must be provided. An optional requestee
+can be passed if the flag type is requestable to a specific user.
+
+=over
+
+=item C<name>
+
+C<string> The name of the flag type.
+
+=item C<type_id>
+
+C<int> The internal flag type id.
+
+=item C<status>
+
+C<string> The flags new status (i.e. "?", "+", "-" or "X" to clear a flag).
+
+=item C<requestee>
+
+C<string> The login of the requestee if the flag type is requestable to a specific user.
+
+=back
+
=back
=item B<Returns>
@@ -2531,6 +3576,28 @@ This method can throw all the same errors as L</get>, plus:
=over
+=item 129 (Flag Status Invalid)
+
+The flag status is invalid.
+
+=item 130 (Flag Modification Denied)
+
+You tried to request, grant, or deny a flag but only a user with the required
+permissions may make the change.
+
+=item 131 (Flag not Requestable from Specific Person)
+
+You can't ask a specific person for the flag.
+
+=item 133 (Flag Type not Unique)
+
+The flag type specified matches several flag types. You must specify
+the type id value to update or add a flag.
+
+=item 134 (Inactive Flag Type)
+
+The flag type is inactive and cannot be used to create new flags.
+
=item 600 (Attachment Too Large)
You tried to attach a file that was larger than Bugzilla will accept.
@@ -2564,8 +3631,227 @@ You set the "data" field to an empty string.
=back
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
+
+=head2 update_attachment
+
+B<UNSTABLE>
+
+=over
+
+=item B<Description>
+
+This allows you to update attachment metadata in Bugzilla.
+
+=item B<REST>
+
+To update attachment metadata on a current attachment:
+
+PUT /bug/attachment/<attach_id>
+
+The params to include in the POST body, as well as the returned
+data format are the same as below. The C<ids> param will be
+overridden as it it pulled from the URL path.
+
+=item B<Params>
+
+=over
+
+=item C<ids>
+
+B<Required> C<array> An array of integers -- the ids of the attachments you
+want to update.
+
+=item C<file_name>
+
+C<string> The "file name" that will be displayed
+in the UI for this attachment.
+
+=item C<summary>
+
+C<string> A short string describing the
+attachment.
+
+=item C<comment>
+
+C<string> An optional comment to add to the attachment's bug.
+
+=item C<content_type>
+
+C<string> The MIME type of the attachment, like
+C<text/plain> or C<image/png>.
+
+=item C<is_patch>
+
+C<boolean> True if Bugzilla should treat this attachment as a patch.
+If you specify this, you do not need to specify a C<content_type>.
+The C<content_type> of the attachment will be forced to C<text/plain>.
+
+=item C<is_private>
+
+C<boolean> True if the attachment should be private (restricted
+to the "insidergroup"), False if the attachment should be public.
+
+=item C<is_obsolete>
+
+C<boolean> True if the attachment is obsolete, False otherwise.
+
+=item C<flags>
+
+C<array> An array of hashes with changes to the flags. The following values
+can be specified. At least the status and one of type_id, id, or name must
+be specified. If a type_id or name matches a single currently set flag,
+the flag will be updated unless new is specified.
+
+=over
+
+=item C<name>
+
+C<string> The name of the flag that will be created or updated.
+
+=item C<type_id>
+
+C<int> The internal flag type id that will be created or updated. You will
+need to specify the C<type_id> if more than one flag type of the same name exists.
+
+=item C<status>
+
+C<string> The flags new status (i.e. "?", "+", "-" or "X" to clear a flag).
+
+=item C<requestee>
+
+C<string> The login of the requestee if the flag type is requestable to a specific user.
+
+=item C<id>
+
+C<int> Use id to specify the flag to be updated. You will need to specify the C<id>
+if more than one flag is set of the same name.
+
+=item C<new>
+
+C<boolean> Set to true if you specifically want a new flag to be created.
+
+=back
+
+=item B<Returns>
+
+A C<hash> with a single field, "attachment". This points to an array of hashes
+with the following fields:
+
+=over
+
+=item C<id>
+
+C<int> The id of the attachment that was updated.
+
+=item C<last_change_time>
+
+C<dateTime> The exact time that this update was done at, for this attachment.
+If no update was done (that is, no fields had their values changed and
+no comment was added) then this will instead be the last time the attachment
+was updated.
+
+=item C<changes>
+
+C<hash> The changes that were actually done on this bug. The keys are
+the names of the fields that were changed, and the values are a hash
+with two keys:
+
+=over
+
+=item C<added> (C<string>) The values that were added to this field.
+possibly a comma-and-space-separated list if multiple values were added.
+
+=item C<removed> (C<string>) The values that were removed from this
+field.
+
+=back
+
=back
+Here's an example of what a return value might look like:
+
+ {
+ attachments => [
+ {
+ id => 123,
+ last_change_time => '2010-01-01T12:34:56',
+ changes => {
+ summary => {
+ removed => 'Sample ptach',
+ added => 'Sample patch'
+ },
+ is_obsolete => {
+ removed => '0',
+ added => '1',
+ }
+ },
+ }
+ ]
+ }
+
+=item B<Errors>
+
+This method can throw all the same errors as L</get>, plus:
+
+=over
+
+=item 129 (Flag Status Invalid)
+
+The flag status is invalid.
+
+=item 130 (Flag Modification Denied)
+
+You tried to request, grant, or deny a flag but only a user with the required
+permissions may make the change.
+
+=item 131 (Flag not Requestable from Specific Person)
+
+You can't ask a specific person for the flag.
+
+=item 132 (Flag not Unique)
+
+The flag specified has been set multiple times. You must specify the id
+value to update the flag.
+
+=item 133 (Flag Type not Unique)
+
+The flag type specified matches several flag types. You must specify
+the type id value to update or add a flag.
+
+=item 134 (Inactive Flag Type)
+
+The flag type is inactive and cannot be used to create new flags.
+
+=item 601 (Invalid MIME Type)
+
+You specified a C<content_type> argument that was blank, not a valid
+MIME type, or not a MIME type that Bugzilla accepts for attachments.
+
+=item 603 (File Name Not Specified)
+
+You did not specify a valid for the C<file_name> argument.
+
+=item 604 (Summary Required)
+
+You did not specify a value for the C<summary> argument.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=back
=head2 add_comment
@@ -2577,6 +3863,15 @@ B<STABLE>
This allows you to add a comment to a bug in Bugzilla.
+=item B<REST>
+
+To create a comment on a current bug:
+
+POST /bug/<bug_id>/comment
+
+The params to include in the POST body as well as the returned data format,
+are the same as below.
+
=item B<Params>
=over
@@ -2653,6 +3948,10 @@ purposes if you wish.
=item Before Bugzilla B<3.6>, error 54 and error 114 had a generic error
code of 32000.
+=item REST API call added in Bugzilla B<5.0>.
+
+=item In Bugzilla B<5.0>, the following items were added to the bugs return value: C<assigned_to_detail>, C<creator_detail>, C<qa_contact_detail>.
+
=back
=back
@@ -2669,6 +3968,16 @@ B<UNSTABLE>
Allows you to update the fields of a bug. Automatically sends emails
out about the changes.
+=item B<REST>
+
+To update the fields of a current bug:
+
+PUT /bug/<bug_id>
+
+The params to include in the PUT body as well as the returned data format,
+are the same as below. The C<ids> param will be overridden as it is
+pulled from the URL path.
+
=item B<Params>
=over
@@ -2788,6 +4097,43 @@ duplicate bugs.
C<double> The total estimate of time required to fix the bug, in hours.
This is the I<total> estimate, not the amount of time remaining to fix it.
+=item C<flags>
+
+C<array> An array of hashes with changes to the flags. The following values
+can be specified. At least the status and one of type_id, id, or name must
+be specified. If a type_id or name matches a single currently set flag,
+the flag will be updated unless new is specified.
+
+=over
+
+=item C<name>
+
+C<string> The name of the flag that will be created or updated.
+
+=item C<type_id>
+
+C<int> The internal flag type id that will be created or updated. You will
+need to specify the C<type_id> if more than one flag type of the same name exists.
+
+=item C<status>
+
+C<string> The flags new status (i.e. "?", "+", "-" or "X" to clear a flag).
+
+=item C<requestee>
+
+C<string> The login of the requestee if the flag type is requestable to a specific user.
+
+=item C<id>
+
+C<int> Use id to specify the flag to be updated. You will need to specify the C<id>
+if more than one flag is set of the same name.
+
+=item C<new>
+
+C<boolean> Set to true if you specifically want a new flag to be created.
+
+=back
+
=item C<groups>
C<hash> The groups a bug is in. To modify this field, pass a hash, which
@@ -3106,6 +4452,33 @@ field.
You tried to change from one status to another, but the status workflow
rules don't allow that change.
+=item 129 (Flag Status Invalid)
+
+The flag status is invalid.
+
+=item 130 (Flag Modification Denied)
+
+You tried to request, grant, or deny a flag but only a user with the required
+permissions may make the change.
+
+=item 131 (Flag not Requestable from Specific Person)
+
+You can't ask a specific person for the flag.
+
+=item 132 (Flag not Unique)
+
+The flag specified has been set multiple times. You must specify the id
+value to update the flag.
+
+=item 133 (Flag Type not Unique)
+
+The flag type specified matches several flag types. You must specify
+the type id value to update or add a flag.
+
+=item 134 (Inactive Flag Type)
+
+The flag type is inactive and cannot be used to create new flags.
+
=back
=item B<History>
@@ -3114,6 +4487,10 @@ rules don't allow that change.
=item Added in Bugzilla B<4.0>.
+=item REST API call added Bugzilla B<5.0>.
+
+=item Added C<new_since> parameter if Bugzilla B<5.0>.
+
=back
=back
@@ -3230,3 +4607,46 @@ this bug.
=back
=back
+
+=head2 render_comment
+
+B<UNSTABLE>
+
+=over
+
+=item B<Description>
+
+Returns the HTML rendering of the provided comment text.
+
+=item B<Params>
+
+=over
+
+=item C<text>
+
+B<Required> C<strings> Text comment text to render.
+
+=item C<id>
+
+C<int> The ID of the bug to render the comment against.
+
+=back
+
+=item B<Returns>
+
+C<html> containing the HTML rendering.
+
+=item B<Errors>
+
+This method can throw all of the errors that L</get> throws.
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
diff --git a/Bugzilla/WebService/BugUserLastVisit.pm b/Bugzilla/WebService/BugUserLastVisit.pm
new file mode 100644
index 000000000..71b637fef
--- /dev/null
+++ b/Bugzilla/WebService/BugUserLastVisit.pm
@@ -0,0 +1,208 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::WebService::BugUserLastVisit;
+
+use 5.10.1;
+use strict;
+
+use parent qw(Bugzilla::WebService);
+
+use Bugzilla::Bug;
+use Bugzilla::Error;
+use Bugzilla::WebService::Util qw( validate filter );
+use Bugzilla::Constants;
+
+sub update {
+ my ($self, $params) = validate(@_, 'ids');
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+
+ $user->login(LOGIN_REQUIRED);
+
+ my $ids = $params->{ids} // [];
+ ThrowCodeError('param_required', { param => 'ids' }) unless @$ids;
+
+ # Cache permissions for bugs. This highly reduces the number of calls to the
+ # DB. visible_bugs() is only able to handle bug IDs, so we have to skip
+ # aliases.
+ $user->visible_bugs([grep /^[0-9]$/, @$ids]);
+
+ $dbh->bz_start_transaction();
+ my @results;
+ my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()');
+ foreach my $bug_id (@$ids) {
+ my $bug = Bugzilla::Bug->check({ id => $bug_id, cache => 1 });
+
+ ThrowUserError('user_not_involved', { bug_id => $bug->id })
+ unless $user->is_involved_in_bug($bug);
+
+ $bug->update_user_last_visit($user, $last_visit_ts);
+
+ push(
+ @results,
+ $self->_bug_user_last_visit_to_hash(
+ $bug, $last_visit_ts, $params
+ ));
+ }
+ $dbh->bz_commit_transaction();
+
+ return \@results;
+}
+
+sub get {
+ my ($self, $params) = validate(@_, 'ids');
+ my $user = Bugzilla->user;
+ my $ids = $params->{ids};
+
+ $user->login(LOGIN_REQUIRED);
+
+ if ($ids) {
+ # Cache permissions for bugs. This highly reduces the number of calls to
+ # the DB. visible_bugs() is only able to handle bug IDs, so we have to
+ # skip aliases.
+ $user->visible_bugs([grep /^[0-9]$/, @$ids]);
+ }
+
+ my @last_visits = @{ $user->last_visits };
+
+ if ($ids) {
+ # remove bugs that we arn't interested in if ids is passed in.
+ my %id_set = map { ($_ => 1) } @$ids;
+ @last_visits = grep { $id_set{ $_->bug_id } } @last_visits;
+ }
+
+ return [
+ map {
+ $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts,
+ $params)
+ } @last_visits
+ ];
+}
+
+sub _bug_user_last_visit_to_hash {
+ my ($self, $bug_id, $last_visit_ts, $params) = @_;
+
+ my %result = (id => $self->type('int', $bug_id),
+ last_visit_ts => $self->type('dateTime', $last_visit_ts));
+
+ return filter($params, \%result);
+}
+
+1;
+
+__END__
+=head1 NAME
+
+Bugzilla::WebService::BugUserLastVisit - Find and Store the last time a user
+visited a bug.
+
+=head1 METHODS
+
+See L<Bugzilla::WebService> for a description of how parameters are passed,
+and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
+
+Although the data input and output is the same for JSONRPC, XMLRPC and REST,
+the directions for how to access the data via REST is noted in each method
+where applicable.
+
+=head2 update
+
+B<EXPERIMENTAL>
+
+=over
+
+=item B<Description>
+
+Update the last visit time for the specified bug and current user.
+
+=item B<REST>
+
+To add a single bug id:
+
+ POST /rest/bug_user_last_visit/<bug-id>
+
+Tp add one or more bug ids at once:
+
+ POST /rest/bug_user_last_visit
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+=over
+
+=item C<ids> (array) - One or more bug ids to add.
+
+=back
+
+=item B<Returns>
+
+=over
+
+=item C<array> - An array of hashes containing the following:
+
+=over
+
+=item C<id> - (int) The bug id.
+
+=item C<last_visit_ts> - (string) The timestamp the user last visited the bug.
+
+=back
+
+=back
+
+=back
+
+=head2 get
+
+B<EXPERIMENTAL>
+
+=over
+
+=item B<Description>
+
+Get the last visited timestamp for one or more specified bug ids or get a
+list of the last 20 visited bugs and their timestamps.
+
+=item B<REST>
+
+To return the last visited timestamp for a single bug id:
+
+GET /rest/bug_visit/<bug-id>
+
+To return more than one bug timestamp or the last 20:
+
+GET /rest/bug_visit
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+=over
+
+=item C<ids> (integer) - One or more optional bug ids to get.
+
+=back
+
+=item B<Returns>
+
+=over
+
+=item C<array> - An array of hashes containing the following:
+
+=over
+
+=item C<id> - (int) The bug id.
+
+=item C<last_visit_ts> - (string) The timestamp the user last visited the bug.
+
+=back
+
+=back
+
+=back
diff --git a/Bugzilla/WebService/Bugzilla.pm b/Bugzilla/WebService/Bugzilla.pm
index efc822311..1fc15c3c3 100644
--- a/Bugzilla/WebService/Bugzilla.pm
+++ b/Bugzilla/WebService/Bugzilla.pm
@@ -98,6 +98,10 @@ This provides functions that tell you about Bugzilla in general.
See L<Bugzilla::WebService> for a description of how parameters are passed,
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
+Although the data input and output is the same for JSONRPC, XMLRPC and REST,
+the directions for how to access the data via REST is noted in each method
+where applicable.
+
=head2 version
B<STABLE>
@@ -108,6 +112,12 @@ B<STABLE>
Returns the current version of Bugzilla.
+=item B<REST>
+
+GET /version
+
+The returned data format is the same as below.
+
=item B<Params> (none)
=item B<Returns>
@@ -117,6 +127,14 @@ string.
=item B<Errors> (none)
+=item B<History>
+
+=over
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
=back
=head2 extensions
@@ -130,6 +148,12 @@ B<EXPERIMENTAL>
Gets information about the extensions that are currently installed and enabled
in this Bugzilla.
+=item B<REST>
+
+GET /extensions
+
+The returned data format is the same as below.
+
=item B<Params> (none)
=item B<Returns>
@@ -160,6 +184,8 @@ The return value looks something like this:
that the extensions define themselves. Before 3.6, the names of the
extensions depended on the directory they were in on the Bugzilla server.
+=item REST API call added in Bugzilla B<5.0>.
+
=back
=back
@@ -175,6 +201,12 @@ Use L</time> instead.
Returns the timezone that Bugzilla expects dates and times in.
+=item B<REST>
+
+GET /timezone
+
+The returned data format is the same as below.
+
=item B<Params> (none)
=item B<Returns>
@@ -189,6 +221,8 @@ string in (+/-)XXXX (RFC 2822) format.
=item As of Bugzilla B<3.6>, the timezone returned is always C<+0000>
(the UTC timezone).
+=item REST API call added in Bugzilla B<5.0>.
+
=back
=back
@@ -205,6 +239,12 @@ B<STABLE>
Gets information about what time the Bugzilla server thinks it is, and
what timezone it's running in.
+=item B<REST>
+
+GET /time
+
+The returned data format is the same as below.
+
=item B<Params> (none)
=item B<Returns>
@@ -215,7 +255,7 @@ A struct with the following items:
=item C<db_time>
-C<dateTime> The current time in UTC, according to the Bugzilla
+C<dateTime> The current time in UTC, according to the Bugzilla
I<database server>.
Note that Bugzilla assumes that the database and the webserver are running
@@ -225,7 +265,7 @@ rely on for doing searches and other input to the WebService.
=item C<web_time>
-C<dateTime> This is the current time in UTC, according to Bugzilla's
+C<dateTime> This is the current time in UTC, according to Bugzilla's
I<web server>.
This might be different by a second from C<db_time> since this comes from
@@ -241,7 +281,7 @@ versions of Bugzilla before 3.6.)
=item C<tz_name>
C<string> The literal string C<UTC>. (Exists only for backwards-compatibility
-with versions of Bugzilla before 3.6.)
+with versions of Bugzilla before 3.6.)
=item C<tz_short_name>
@@ -265,6 +305,8 @@ with versions of Bugzilla before 3.6.)
were in the UTC timezone, instead of returning information in the server's
local timezone.
+=item REST API call added in Bugzilla B<5.0>.
+
=back
=back
diff --git a/Bugzilla/WebService/Classification.pm b/Bugzilla/WebService/Classification.pm
new file mode 100644
index 000000000..1c4226fd6
--- /dev/null
+++ b/Bugzilla/WebService/Classification.pm
@@ -0,0 +1,208 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::WebService::Classification;
+
+use 5.10.1;
+use strict;
+
+use parent qw (Bugzilla::WebService);
+
+use Bugzilla::Classification;
+use Bugzilla::Error;
+use Bugzilla::WebService::Util qw(filter validate params_to_objects);
+
+use constant READ_ONLY => qw(
+ get
+);
+
+sub get {
+ my ($self, $params) = validate(@_, 'names', 'ids');
+
+ defined $params->{names} || defined $params->{ids}
+ || ThrowCodeError('params_required', { function => 'Classification.get',
+ params => ['names', 'ids'] });
+
+ my $user = Bugzilla->user;
+
+ Bugzilla->params->{'useclassification'}
+ || $user->in_group('editclassifications')
+ || ThrowUserError('auth_classification_not_enabled');
+
+ Bugzilla->switch_to_shadow_db;
+
+ my @classification_objs = @{ params_to_objects($params, 'Bugzilla::Classification') };
+ unless ($user->in_group('editclassifications')) {
+ my %selectable_class = map { $_->id => 1 } @{$user->get_selectable_classifications};
+ @classification_objs = grep { $selectable_class{$_->id} } @classification_objs;
+ }
+
+ my @classifications = map { $self->_classification_to_hash($_, $params) } @classification_objs;
+
+ return { classifications => \@classifications };
+}
+
+sub _classification_to_hash {
+ my ($self, $classification, $params) = @_;
+
+ my $user = Bugzilla->user;
+ return unless (Bugzilla->params->{'useclassification'} || $user->in_group('editclassifications'));
+
+ my $products = $user->in_group('editclassifications') ?
+ $classification->products : $user->get_selectable_products($classification->id);
+
+ return filter $params, {
+ id => $self->type('int', $classification->id),
+ name => $self->type('string', $classification->name),
+ description => $self->type('string', $classification->description),
+ sort_key => $self->type('int', $classification->sortkey),
+ products => [ map { $self->_product_to_hash($_, $params) } @$products ],
+ };
+}
+
+sub _product_to_hash {
+ my ($self, $product, $params) = @_;
+
+ return filter $params, {
+ id => $self->type('int', $product->id),
+ name => $self->type('string', $product->name),
+ description => $self->type('string', $product->description),
+ }, undef, 'products';
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Webservice::Classification - The Classification API
+
+=head1 DESCRIPTION
+
+This part of the Bugzilla API allows you to deal with the available Classifications.
+You will be able to get information about them as well as manipulate them.
+
+=head1 METHODS
+
+See L<Bugzilla::WebService> for a description of how parameters are passed,
+and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
+
+Although the data input and output is the same for JSONRPC, XMLRPC and REST,
+the directions for how to access the data via REST is noted in each method
+where applicable.
+
+=head1 Classification Retrieval
+
+=head2 get
+
+B<EXPERIMENTAL>
+
+=over
+
+=item B<Description>
+
+Returns a hash containing information about a set of classifications.
+
+=item B<REST>
+
+To return information on a single classification:
+
+GET /classification/<classification_id_or_name>
+
+The returned data format will be the same as below.
+
+=item B<Params>
+
+In addition to the parameters below, this method also accepts the
+standard L<include_fields|Bugzilla::WebService/include_fields> and
+L<exclude_fields|Bugzilla::WebService/exclude_fields> arguments.
+
+You could get classifications info by supplying their names and/or ids.
+So, this method accepts the following parameters:
+
+=over
+
+=item C<ids>
+
+An array of classification ids.
+
+=item C<names>
+
+An array of classification names.
+
+=back
+
+=item B<Returns>
+
+A hash with the key C<classifications> and an array of hashes as the corresponding value.
+Each element of the array represents a classification that the user is authorized to see
+and has the following keys:
+
+=over
+
+=item C<id>
+
+C<int> The id of the classification.
+
+=item C<name>
+
+C<string> The name of the classification.
+
+=item C<description>
+
+C<string> The description of the classificaion.
+
+=item C<sort_key>
+
+C<int> The value which determines the order the classification is sorted.
+
+=item C<products>
+
+An array of hashes. The array contains the products the user is authorized to
+access within the classification. Each hash has the following keys:
+
+=over
+
+=item C<name>
+
+C<string> The name of the product.
+
+=item C<id>
+
+C<int> The id of the product.
+
+=item C<description>
+
+C<string> The description of the product.
+
+=back
+
+=back
+
+=item B<Errors>
+
+=over
+
+=item 900 (Classification not enabled)
+
+Classification is not enabled on this installation.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<4.4>.
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm
index 3207356fa..4d36b877f 100644
--- a/Bugzilla/WebService/Constants.pm
+++ b/Bugzilla/WebService/Constants.pm
@@ -22,9 +22,22 @@ use base qw(Exporter);
our @EXPORT = qw(
WS_ERROR_CODE
+
+ STATUS_OK
+ STATUS_CREATED
+ STATUS_ACCEPTED
+ STATUS_NO_CONTENT
+ STATUS_MULTIPLE_CHOICES
+ STATUS_BAD_REQUEST
+ STATUS_NOT_FOUND
+ STATUS_GONE
+ REST_STATUS_CODE_MAP
+
ERROR_UNKNOWN_FATAL
ERROR_UNKNOWN_TRANSIENT
+
XMLRPC_CONTENT_TYPE_WHITELIST
+ REST_CONTENT_TYPE_WHITELIST
WS_DISPATCH
);
@@ -93,7 +106,12 @@ use constant WS_ERROR_CODE => {
comment_is_private => 110,
comment_id_invalid => 111,
comment_too_long => 114,
- comment_invalid_isprivate => 117,
+ comment_invalid_isprivate => 117,
+ # Comment tagging
+ comment_tag_disabled => 125,
+ comment_tag_invalid => 126,
+ comment_tag_too_long => 127,
+ comment_tag_too_short => 128,
# See Also errors
bug_url_invalid => 112,
bug_url_too_long => 112,
@@ -116,6 +134,13 @@ use constant WS_ERROR_CODE => {
missing_resolution => 121,
resolution_not_allowed => 122,
illegal_bug_status_transition => 123,
+ # Flag errors
+ flag_status_invalid => 129,
+ flag_update_denied => 130,
+ flag_type_requestee_disabled => 131,
+ flag_not_unique => 132,
+ flag_type_not_unique => 133,
+ flag_type_inactive => 134,
# Authentication errors are usually 300-400.
invalid_username_or_password => 300,
@@ -177,8 +202,48 @@ use constant WS_ERROR_CODE => {
unknown_method => -32601,
json_rpc_post_only => 32610,
json_rpc_invalid_callback => 32611,
- xmlrpc_illegal_content_type => 32612,
- json_rpc_illegal_content_type => 32613,
+ xmlrpc_illegal_content_type => 32612,
+ json_rpc_illegal_content_type => 32613,
+ rest_invalid_resource => 32614,
+};
+
+# RESTful webservices use the http status code
+# to describe whether a call was successful or
+# to describe the type of error that occurred.
+use constant STATUS_OK => 200;
+use constant STATUS_CREATED => 201;
+use constant STATUS_ACCEPTED => 202;
+use constant STATUS_NO_CONTENT => 204;
+use constant STATUS_MULTIPLE_CHOICES => 300;
+use constant STATUS_BAD_REQUEST => 400;
+use constant STATUS_NOT_AUTHORIZED => 401;
+use constant STATUS_NOT_FOUND => 404;
+use constant STATUS_GONE => 410;
+
+# The integer value is the error code above returned by
+# the related webvservice call. We choose the appropriate
+# http status code based on the error code or use the
+# default STATUS_BAD_REQUEST.
+use constant REST_STATUS_CODE_MAP => {
+ 51 => STATUS_NOT_FOUND,
+ 101 => STATUS_NOT_FOUND,
+ 102 => STATUS_NOT_AUTHORIZED,
+ 106 => STATUS_NOT_AUTHORIZED,
+ 109 => STATUS_NOT_AUTHORIZED,
+ 110 => STATUS_NOT_AUTHORIZED,
+ 113 => STATUS_NOT_AUTHORIZED,
+ 115 => STATUS_NOT_AUTHORIZED,
+ 120 => STATUS_NOT_AUTHORIZED,
+ 300 => STATUS_NOT_AUTHORIZED,
+ 301 => STATUS_NOT_AUTHORIZED,
+ 302 => STATUS_NOT_AUTHORIZED,
+ 303 => STATUS_NOT_AUTHORIZED,
+ 304 => STATUS_NOT_AUTHORIZED,
+ 410 => STATUS_NOT_AUTHORIZED,
+ 504 => STATUS_NOT_AUTHORIZED,
+ 505 => STATUS_NOT_AUTHORIZED,
+ 32614 => STATUS_NOT_FOUND,
+ _default => STATUS_BAD_REQUEST
};
# These are the fallback defaults for errors not in ERROR_CODE.
@@ -192,6 +257,14 @@ use constant XMLRPC_CONTENT_TYPE_WHITELIST => qw(
application/xml
);
+# The first content type specified is used as the default.
+use constant REST_CONTENT_TYPE_WHITELIST => qw(
+ application/json
+ application/javascript
+ text/javascript
+ text/html
+);
+
sub WS_DISPATCH {
# We "require" here instead of "use" above to avoid a dependency loop.
require Bugzilla::Hook;
@@ -199,11 +272,13 @@ sub WS_DISPATCH {
Bugzilla::Hook::process('webservice', { dispatch => \%hook_dispatch });
my $dispatch = {
- 'Bugzilla' => 'Bugzilla::WebService::Bugzilla',
- 'Bug' => 'Bugzilla::WebService::Bug',
- 'User' => 'Bugzilla::WebService::User',
- 'Product' => 'Bugzilla::WebService::Product',
- 'Group' => 'Bugzilla::WebService::Group',
+ 'Bugzilla' => 'Bugzilla::WebService::Bugzilla',
+ 'Bug' => 'Bugzilla::WebService::Bug',
+ 'Classification' => 'Bugzilla::WebService::Classification',
+ 'User' => 'Bugzilla::WebService::User',
+ 'Product' => 'Bugzilla::WebService::Product',
+ 'Group' => 'Bugzilla::WebService::Group',
+ 'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit',
%hook_dispatch
};
return $dispatch;
diff --git a/Bugzilla/WebService/Group.pm b/Bugzilla/WebService/Group.pm
index 65feb7a1a..b571a1062 100644
--- a/Bugzilla/WebService/Group.pm
+++ b/Bugzilla/WebService/Group.pm
@@ -61,6 +61,10 @@ get information about them.
See L<Bugzilla::WebService> for a description of how parameters are passed,
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
+Although the data input and output is the same for JSONRPC, XMLRPC and REST,
+the directions for how to access the data via REST is noted in each method
+where applicable.
+
=head1 Group Creation
=head2 create
@@ -73,9 +77,16 @@ B<UNSTABLE>
This allows you to create a new group in Bugzilla.
-=item B<Params>
+=item B<REST>
+
+POST /group
+
+The params to include in the POST body as well as the returned data format,
+are the same as below.
-Some params must be set, or an error will be thrown. These params are
+=item B<Params>
+
+Some params must be set, or an error will be thrown. These params are
marked B<Required>.
=over
@@ -96,7 +107,7 @@ name of the group.
C<string> A regular expression. Any user whose Bugzilla username matches
this regular expression will automatically be granted membership in this group.
-=item C<is_active>
+=item C<is_active>
C<boolean> C<True> if new group can be used for bugs, C<False> if this
is a group that will only contain users and no bugs will be restricted
@@ -110,7 +121,7 @@ if they are in this group.
=back
-=item B<Returns>
+=item B<Returns>
A hash with one element, C<id>. This is the id of the newly-created group.
@@ -136,6 +147,14 @@ You specified an invalid regular expression in the C<user_regexp> field.
=back
-=back
+=item B<History>
+
+=over
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
+=back
=cut
diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm
index 3cd0d0a6c..3be46bd6d 100644
--- a/Bugzilla/WebService/Product.pm
+++ b/Bugzilla/WebService/Product.pm
@@ -47,57 +47,95 @@ BEGIN { *get_products = \&get }
# Get the ids of the products the user can search
sub get_selectable_products {
- return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]};
+ Bugzilla->switch_to_shadow_db();
+ return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]};
}
# Get the ids of the products the user can enter bugs against
sub get_enterable_products {
- return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]};
+ Bugzilla->switch_to_shadow_db();
+ return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]};
}
# Get the union of the products the user can search and enter bugs against.
sub get_accessible_products {
- return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]};
+ Bugzilla->switch_to_shadow_db();
+ return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]};
}
# Get a list of actual products, based on list of ids or names
sub get {
- my ($self, $params) = validate(@_, 'ids', 'names');
-
- # Only products that are in the users accessible products,
- # can be allowed to be returned
- my $accessible_products = Bugzilla->user->get_accessible_products;
+ my ($self, $params) = validate(@_, 'ids', 'names', 'type');
+ my $user = Bugzilla->user;
+
+ defined $params->{ids} || defined $params->{names} || defined $params->{type}
+ || ThrowCodeError("params_required", { function => "Product.get",
+ params => ['ids', 'names', 'type'] });
+
+ Bugzilla->switch_to_shadow_db();
+
+ my $products = [];
+ if (defined $params->{type}) {
+ my %product_hash;
+ foreach my $type (@{ $params->{type} }) {
+ my $result = [];
+ if ($type eq 'accessible') {
+ $result = $user->get_accessible_products();
+ }
+ elsif ($type eq 'enterable') {
+ $result = $user->get_enterable_products();
+ }
+ elsif ($type eq 'selectable') {
+ $result = $user->get_selectable_products();
+ }
+ else {
+ ThrowUserError('get_products_invalid_type',
+ { type => $type });
+ }
+ map { $product_hash{$_->id} = $_ } @$result;
+ }
+ $products = [ values %product_hash ];
+ }
+ else {
+ $products = $user->get_accessible_products;
+ }
- my @requested_accessible;
+ my @requested_products;
if (defined $params->{ids}) {
# Create a hash with the ids the user wants
my %ids = map { $_ => 1 } @{$params->{ids}};
-
- # Return the intersection of this, by grepping the ids from
+
+ # Return the intersection of this, by grepping the ids from
# accessible products.
- push(@requested_accessible,
- grep { $ids{$_->id} } @$accessible_products);
+ push(@requested_products,
+ grep { $ids{$_->id} } @$products);
}
if (defined $params->{names}) {
# Create a hash with the names the user wants
my %names = map { lc($_) => 1 } @{$params->{names}};
-
- # Return the intersection of this, by grepping the names from
+
+ # Return the intersection of this, by grepping the names from
# accessible products, union'ed with products found by ID to
# avoid duplicates
foreach my $product (grep { $names{lc $_->name} }
- @$accessible_products) {
+ @$products) {
next if grep { $_->id == $product->id }
- @requested_accessible;
- push @requested_accessible, $product;
+ @requested_products;
+ push @requested_products, $product;
}
}
+ # If we just requested a specific type of products without
+ # specifying ids or names, then return the entire list.
+ if (!defined $params->{ids} && !defined $params->{names}) {
+ @requested_products = @$products;
+ }
+
# Now create a result entry for each.
my @products = map { $self->_product_to_hash($params, $_) }
- @requested_accessible;
+ @requested_products;
return { products => \@products };
}
@@ -105,7 +143,7 @@ sub create {
my ($self, $params) = @_;
Bugzilla->login(LOGIN_REQUIRED);
- Bugzilla->user->in_group('editcomponents')
+ Bugzilla->user->in_group('editcomponents')
|| ThrowUserError("auth_failure", { group => "editcomponents",
action => "add",
object => "products"});
@@ -143,45 +181,88 @@ sub _product_to_hash {
};
if (filter_wants($params, 'components')) {
$field_data->{components} = [map {
- $self->_component_to_hash($_)
+ $self->_component_to_hash($_, $params)
} @{$product->components}];
}
if (filter_wants($params, 'versions')) {
$field_data->{versions} = [map {
- $self->_version_to_hash($_)
+ $self->_version_to_hash($_, $params)
} @{$product->versions}];
}
if (filter_wants($params, 'milestones')) {
$field_data->{milestones} = [map {
- $self->_milestone_to_hash($_)
+ $self->_milestone_to_hash($_, $params)
} @{$product->milestones}];
}
return filter($params, $field_data);
}
sub _component_to_hash {
- my ($self, $component) = @_;
- return {
+ my ($self, $component, $params) = @_;
+ my $field_data = filter $params, {
id =>
$self->type('int', $component->id),
name =>
$self->type('string', $component->name),
description =>
- $self->type('string' , $component->description),
+ $self->type('string', $component->description),
default_assigned_to =>
- $self->type('string' , $component->default_assignee->login),
+ $self->type('email', $component->default_assignee->login),
default_qa_contact =>
- $self->type('string' , $component->default_qa_contact->login),
+ $self->type('email', $component->default_qa_contact->login),
sort_key => # sort_key is returned to match Bug.fields
0,
is_active =>
$self->type('boolean', $component->is_active),
- };
+ }, undef, 'components';
+
+ if (filter_wants($params, 'flag_types', undef, 'components')) {
+ $field_data->{flag_types} = {
+ bug =>
+ [map {
+ $self->_flag_type_to_hash($_)
+ } @{$component->flag_types->{'bug'}}],
+ attachment =>
+ [map {
+ $self->_flag_type_to_hash($_)
+ } @{$component->flag_types->{'attachment'}}],
+ };
+ }
+
+ return $field_data;
+}
+
+sub _flag_type_to_hash {
+ my ($self, $flag_type, $params) = @_;
+ return filter $params, {
+ id =>
+ $self->type('int', $flag_type->id),
+ name =>
+ $self->type('string', $flag_type->name),
+ description =>
+ $self->type('string', $flag_type->description),
+ cc_list =>
+ $self->type('string', $flag_type->cc_list),
+ sort_key =>
+ $self->type('int', $flag_type->sortkey),
+ is_active =>
+ $self->type('boolean', $flag_type->is_active),
+ is_requestable =>
+ $self->type('boolean', $flag_type->is_requestable),
+ is_requesteeble =>
+ $self->type('boolean', $flag_type->is_requesteeble),
+ is_multiplicable =>
+ $self->type('boolean', $flag_type->is_multiplicable),
+ grant_group =>
+ $self->type('int', $flag_type->grant_group_id),
+ request_group =>
+ $self->type('int', $flag_type->request_group_id),
+ }, undef, 'flag_types';
}
sub _version_to_hash {
- my ($self, $version) = @_;
- return {
+ my ($self, $version, $params) = @_;
+ return filter $params, {
id =>
$self->type('int', $version->id),
name =>
@@ -190,12 +271,12 @@ sub _version_to_hash {
0,
is_active =>
$self->type('boolean', $version->is_active),
- };
+ }, undef, 'versions';
}
sub _milestone_to_hash {
- my ($self, $milestone) = @_;
- return {
+ my ($self, $milestone, $params) = @_;
+ return filter $params, {
id =>
$self->type('int', $milestone->id),
name =>
@@ -204,7 +285,7 @@ sub _milestone_to_hash {
$self->type('int', $milestone->sortkey),
is_active =>
$self->type('boolean', $milestone->is_active),
- };
+ }, undef, 'milestones';
}
1;
@@ -225,6 +306,10 @@ get information about them.
See L<Bugzilla::WebService> for a description of how parameters are passed,
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
+Although the data input and output is the same for JSONRPC, XMLRPC and REST,
+the directions for how to access the data via REST is noted in each method
+where applicable.
+
=head1 List Products
=head2 get_selectable_products
@@ -237,15 +322,29 @@ B<EXPERIMENTAL>
Returns a list of the ids of the products the user can search on.
+=item B<REST>
+
+GET /product_selectable
+
+the returned data format is same as below.
+
=item B<Params> (none)
-=item B<Returns>
+=item B<Returns>
A hash containing one item, C<ids>, that contains an array of product
ids.
=item B<Errors> (none)
+=item B<History>
+
+=over
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
=back
=head2 get_enterable_products
@@ -259,6 +358,12 @@ B<EXPERIMENTAL>
Returns a list of the ids of the products the user can enter bugs
against.
+=item B<REST>
+
+GET /product_enterable
+
+the returned data format is same as below.
+
=item B<Params> (none)
=item B<Returns>
@@ -268,6 +373,14 @@ ids.
=item B<Errors> (none)
+=item B<History>
+
+=over
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
=back
=head2 get_accessible_products
@@ -281,6 +394,12 @@ B<UNSTABLE>
Returns a list of the ids of the products the user can search or enter
bugs against.
+=item B<REST>
+
+GET /product_accessible
+
+the returned data format is same as below.
+
=item B<Params> (none)
=item B<Returns>
@@ -290,6 +409,14 @@ ids.
=item B<Errors> (none)
+=item B<History>
+
+=over
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
=back
=head2 get
@@ -304,12 +431,32 @@ Returns a list of information about the products passed to it.
Note: Can also be called as "get_products" for compatibilty with Bugzilla 3.0 API.
+=item B<REST>
+
+To return information about a specific groups of products such as
+C<accessible>, C<selectable>, or C<enterable>:
+
+GET /product?type=accessible
+
+To return information about a specific product by C<id> or C<name>:
+
+GET /product/<product_id_or_name>
+
+You can also return information about more than one specific product
+by using the following in your query string:
+
+GET /product?ids=1&ids=2&ids=3 or GET /product?names=ProductOne&names=Product2
+
+the returned data format is same as below.
+
=item B<Params>
In addition to the parameters below, this method also accepts the
standard L<include_fields|Bugzilla::WebService/include_fields> and
L<exclude_fields|Bugzilla::WebService/exclude_fields> arguments.
+This RPC call supports sub field restrictions.
+
=over
=item C<ids>
@@ -320,9 +467,15 @@ An array of product ids
An array of product names
+=item C<type>
+
+The group of products to return. Valid values are: C<accessible> (default),
+C<selectable>, and C<enterable>. C<type> can be a single value or an array
+of values if more than one group is needed with duplicates removed.
+
=back
-=item B<Returns>
+=item B<Returns>
A hash containing one item, C<products>, that is an array of
hashes. Each hash describes a product, and has the following items:
@@ -400,6 +553,68 @@ and then secondly by their name.
C<boolean> A boolean indicating if the component is active. Inactive
components are not enabled for new bugs.
+=item C<flag_types>
+
+A hash containing the two items C<bug> and C<attachment> that each contains an
+array of hashes, where each hash describes a flagtype, and has the
+following items:
+
+=over
+
+=item C<id>
+
+C<int> Returns the ID of the flagtype.
+
+=item C<name>
+
+C<string> Returns the name of the flagtype.
+
+=item C<description>
+
+C<string> Returns the description of the flagtype.
+
+=item C<cc_list>
+
+C<string> Returns the concatenated CC list for the flagtype, as a single string.
+
+=item C<sort_key>
+
+C<int> Returns the sortkey of the flagtype.
+
+=item C<is_active>
+
+C<boolean> Returns whether the flagtype is active or disabled. Flags being
+in a disabled flagtype are not deleted. It only prevents you from
+adding new flags to it.
+
+=item C<is_requestable>
+
+C<boolean> Returns whether you can request for the given flagtype
+(i.e. whether the '?' flag is available or not).
+
+=item C<is_requesteeble>
+
+C<boolean> Returns whether you can ask someone specifically or not.
+
+=item C<is_multiplicable>
+
+C<boolean> Returns whether you can have more than one flag for the given
+flagtype in a given bug/attachment.
+
+=item C<grant_group>
+
+C<int> the group id that is allowed to grant/deny flags of this type.
+If the item is not included all users are allowed to grant/deny this
+flagtype.
+
+=item C<request_group>
+
+C<int> the group id that is allowed to request the flag if the flag
+is of the type requestable. If the item is not included all users
+are allowed request this flagtype.
+
+=back
+
=back
=item C<versions>
@@ -432,6 +647,11 @@ C<milestones>, C<default_milestone> and C<has_unconfirmed> were added to
the fields returned by C<get> as a replacement for C<internals>, which has
been removed.
+=item REST API call added in Bugzilla B<5.0>.
+
+=item In Bugzilla B<4.4>, C<flag_types> was added to the fields returned
+by C<get>.
+
=back
=back
@@ -448,9 +668,16 @@ B<EXPERIMENTAL>
This allows you to create a new product in Bugzilla.
-=item B<Params>
+=item B<REST>
-Some params must be set, or an error will be thrown. These params are
+POST /product
+
+The params to include in the POST body as well as the returned data format,
+are the same as below.
+
+=item B<Params>
+
+Some params must be set, or an error will be thrown. These params are
marked B<Required>.
=over
@@ -464,11 +691,11 @@ within Bugzilla.
B<Required> C<string> A description for this product. Allows some simple HTML.
-=item C<version>
+=item C<version>
B<Required> C<string> The default version for this product.
-=item C<has_unconfirmed>
+=item C<has_unconfirmed>
C<boolean> Allow the UNCONFIRMED status to be set on bugs in this product.
Default: true.
@@ -477,11 +704,11 @@ Default: true.
C<string> The name of the Classification which contains this product.
-=item C<default_milestone>
+=item C<default_milestone>
C<string> The default milestone for this product. Default: '---'.
-=item C<is_open>
+=item C<is_open>
C<boolean> True if the product is currently allowing bugs to be entered
into it. Default: true.
@@ -493,7 +720,7 @@ new product. Default: true.
=back
-=item B<Returns>
+=item B<Returns>
A hash with one element, id. This is the id of the newly-filed product.
@@ -529,4 +756,12 @@ You must specify a version for this product.
=back
+=item B<History>
+
+=over
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
=back
diff --git a/Bugzilla/WebService/Server.pm b/Bugzilla/WebService/Server.pm
index 206f0c657..e61a1f600 100644
--- a/Bugzilla/WebService/Server.pm
+++ b/Bugzilla/WebService/Server.pm
@@ -22,22 +22,34 @@ use Bugzilla::Error;
use Bugzilla::Util qw(datetime_from);
use Scalar::Util qw(blessed);
+use Digest::MD5 qw(md5_base64);
+
+use Storable qw(freeze);
sub handle_login {
my ($self, $class, $method, $full_method) = @_;
# Throw error if the supplied class does not exist or the method is private
ThrowCodeError('unknown_method', {method => $full_method}) if (!$class or $method =~ /^_/);
+ # BMO - use the class and method as the name, instead of the cgi filename
+ if (Bugzilla->metrics_enabled) {
+ Bugzilla->metrics->name("$class $method");
+ }
+
eval "require $class";
ThrowCodeError('unknown_method', {method => $full_method}) if $@;
return if ($class->login_exempt($method)
and !defined Bugzilla->input_params->{Bugzilla_login});
Bugzilla->login();
+
+ Bugzilla::Hook::process(
+ 'webservice_before_call',
+ { 'method' => $method, full_method => $full_method });
}
sub datetime_format_inbound {
my ($self, $time) = @_;
-
+
my $converted = datetime_from($time, Bugzilla->local_timezone);
if (!defined $converted) {
ThrowUserError('illegal_date', { date => $time });
@@ -63,4 +75,71 @@ sub datetime_format_outbound {
return $time->iso8601();
}
+# ETag support
+sub bz_etag {
+ my ($self, $data) = @_;
+ my $cache = Bugzilla->request_cache;
+ if (defined $data) {
+ # Serialize the data if passed a reference
+ local $Storable::canonical = 1;
+ $data = freeze($data) if ref $data;
+
+ # Wide characters cause md5_base64() to die.
+ utf8::encode($data) if utf8::is_utf8($data);
+
+ # Append content_type to the end of the data
+ # string as we want the etag to be unique to
+ # the content_type. We do not need this for
+ # XMLRPC as text/xml is always returned.
+ if (blessed($self) && $self->can('content_type')) {
+ $data .= $self->content_type if $self->content_type;
+ }
+
+ $cache->{'bz_etag'} = md5_base64($data);
+ }
+ return $cache->{'bz_etag'};
+}
+
1;
+
+=head1 NAME
+
+Bugzilla::WebService::Server - Base server class for the WebService API
+
+=head1 DESCRIPTION
+
+Bugzilla::WebService::Server is the base class for the individual WebService API
+servers such as XMLRPC, JSONRPC, and REST. You never actually create a
+Bugzilla::WebService::Server directly, you only make subclasses of it.
+
+=head1 FUNCTIONS
+
+=over
+
+=item C<bz_etag>
+
+This function is used to store an ETag value that will be used when returning
+the data by the different API server modules such as XMLRPC, or REST. The individual
+webservice methods can also set the value earlier in the process if needed such as
+before a unique update token is added. If a value is not set earlier, an etag will
+automatically be created using the returned data except in some cases when an error
+has occurred.
+
+=back
+
+=head1 SEE ALSO
+
+L<Bugzilla::WebService::Server::XMLRPC|XMLRPC>, L<Bugzilla::WebService::Server::JSONRPC|JSONRPC>,
+and L<Bugzilla::WebService::Server::REST|REST>.
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item handle_login
+
+=item datetime_format_outbound
+
+=item datetime_format_inbound
+
+=back
diff --git a/Bugzilla/WebService/Server/JSONRPC.pm b/Bugzilla/WebService/Server/JSONRPC.pm
index 373aa4fe0..0df4240e0 100644
--- a/Bugzilla/WebService/Server/JSONRPC.pm
+++ b/Bugzilla/WebService/Server/JSONRPC.pm
@@ -37,8 +37,8 @@ BEGIN {
use Bugzilla::Error;
use Bugzilla::WebService::Constants;
-use Bugzilla::WebService::Util qw(taint_data);
-use Bugzilla::Util qw(correct_urlbase trim disable_utf8);
+use Bugzilla::WebService::Util qw(taint_data fix_credentials);
+use Bugzilla::Util;
use HTTP::Message;
use MIME::Base64 qw(decode_base64 encode_base64);
@@ -87,6 +87,7 @@ sub response_header {
sub response {
my ($self, $response) = @_;
+ my $cgi = $self->cgi;
# Implement JSONP.
if (my $callback = $self->_bz_callback) {
@@ -108,9 +109,18 @@ sub response {
push(@header_args, "-$name", $value);
}
}
- my $cgi = $self->cgi;
- print $cgi->header(-status => $response->code, @header_args);
- print $response->content;
+
+ # ETag support
+ my $etag = $self->bz_etag;
+ if ($etag && $cgi->check_etag($etag)) {
+ push(@header_args, "-ETag", $etag);
+ print $cgi->header(-status => '304 Not Modified', @header_args);
+ }
+ else {
+ push(@header_args, "-ETag", $etag) if $etag;
+ print $cgi->header(-status => $response->code, @header_args);
+ print $response->content;
+ }
}
# The JSON-RPC 1.1 GET specification is not so great--you can't specify
@@ -222,6 +232,9 @@ sub type {
utf8::encode($value) if utf8::is_utf8($value);
$retval = encode_base64($value, '');
}
+ elsif ($type eq 'email' && Bugzilla->params->{'webservice_email_filter'}) {
+ $retval = email_filter($value);
+ }
return $retval;
}
@@ -267,7 +280,17 @@ sub _handle {
my $self = shift;
my ($obj) = @_;
$self->{_bz_request_id} = $obj->{id};
- return $self->SUPER::_handle(@_);
+
+ my $result = $self->SUPER::_handle(@_);
+
+ # Set the ETag if not already set in the webservice methods.
+ my $etag = $self->bz_etag;
+ if (!$etag && ref $result) {
+ my $data = $self->json->decode($result)->{'result'};
+ $self->bz_etag($data);
+ }
+
+ return $result;
}
# Make all error messages returned by JSON::RPC go into the 100000
@@ -364,6 +387,10 @@ sub _argument_type_check {
}
}
+ # Update the params to allow for several convenience key/values
+ # use for authentication
+ fix_credentials($params);
+
Bugzilla->input_params($params);
if ($self->request->method eq 'POST') {
diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm
new file mode 100644
index 000000000..69f97ef08
--- /dev/null
+++ b/Bugzilla/WebService/Server/REST.pm
@@ -0,0 +1,659 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::WebService::Server::REST;
+
+use 5.10.1;
+use strict;
+
+use parent qw(Bugzilla::WebService::Server::JSONRPC);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Hook;
+use Bugzilla::Util qw(correct_urlbase html_quote disable_utf8 enable_utf8);
+use Bugzilla::WebService::Constants;
+use Bugzilla::WebService::Util qw(taint_data fix_credentials);
+
+# Load resource modules
+use Bugzilla::WebService::Server::REST::Resources::Bug;
+use Bugzilla::WebService::Server::REST::Resources::Bugzilla;
+use Bugzilla::WebService::Server::REST::Resources::Classification;
+use Bugzilla::WebService::Server::REST::Resources::Group;
+use Bugzilla::WebService::Server::REST::Resources::Product;
+use Bugzilla::WebService::Server::REST::Resources::User;
+use Bugzilla::WebService::Server::REST::Resources::BugUserLastVisit;
+
+use Scalar::Util qw(blessed reftype);
+use MIME::Base64 qw(decode_base64);
+
+###########################
+# Public Method Overrides #
+###########################
+
+sub handle {
+ my ($self) = @_;
+
+ # Determine how the data should be represented. We do this early so
+ # errors will also be returned with the proper content type.
+ # If no accept header was sent or the content types specified were not
+ # matched, we default to the first type in the whitelist.
+ $self->content_type($self->_best_content_type(REST_CONTENT_TYPE_WHITELIST()));
+
+ # Using current path information, decide which class/method to
+ # use to serve the request. Throw error if no resource was found
+ # unless we were looking for OPTIONS
+ if (!$self->_find_resource($self->cgi->path_info)) {
+ if ($self->request->method eq 'OPTIONS'
+ && $self->bz_rest_options)
+ {
+ my $response = $self->response_header(STATUS_OK, "");
+ my $options_string = join(', ', @{ $self->bz_rest_options });
+ $response->header('Allow' => $options_string,
+ 'Access-Control-Allow-Methods' => $options_string);
+ return $self->response($response);
+ }
+
+ ThrowUserError("rest_invalid_resource",
+ { path => $self->cgi->path_info,
+ method => $self->request->method });
+ }
+
+ # Dispatch to the proper module
+ my $class = $self->bz_class_name;
+ my ($path) = $class =~ /::([^:]+)$/;
+ $self->path_info($path);
+ delete $self->{dispatch_path};
+ $self->dispatch({ $path => $class });
+
+ my $params = $self->_retrieve_json_params;
+
+ fix_credentials($params);
+
+ # Fix includes/excludes for each call
+ rest_include_exclude($params);
+
+ # Set callback name if content-type is 'application/javascript'
+ if ($params->{'callback'}
+ || $self->content_type eq 'application/javascript')
+ {
+ $self->_bz_callback($params->{'callback'} || 'callback');
+ }
+
+ Bugzilla->input_params($params);
+
+ # Set the JSON version to 1.1 and the id to the current urlbase
+ # also set up the correct handler method
+ my $obj = {
+ version => '1.1',
+ id => correct_urlbase(),
+ method => $self->bz_method_name,
+ params => $params
+ };
+
+ # Execute the handler
+ my $result = $self->_handle($obj);
+
+ if (!$self->error_response_header) {
+ return $self->response(
+ $self->response_header($self->bz_success_code || STATUS_OK, $result));
+ }
+
+ $self->response($self->error_response_header);
+}
+
+sub response {
+ my ($self, $response) = @_;
+
+ # If we have thrown an error, the 'error' key will exist
+ # otherwise we use 'result'. JSONRPC returns other data
+ # along with the result/error such as version and id which
+ # we will strip off for REST calls.
+ my $content = $response->content;
+
+ my $json_data = {};
+ if ($content) {
+ # Content is in bytes at this point and needs to be converted
+ # back to utf8 string.
+ enable_utf8();
+ utf8::decode($content) if !utf8::is_utf8($content);
+ $json_data = $self->json->decode($content);
+ }
+
+ my $result = {};
+ if (exists $json_data->{error}) {
+ $result = $json_data->{error};
+ $result->{error} = $self->type('boolean', 1);
+ $result->{documentation} = REST_DOC;
+ delete $result->{'name'}; # Remove JSONRPCError
+ }
+ elsif (exists $json_data->{result}) {
+ $result = $json_data->{result};
+ }
+
+ Bugzilla::Hook::process('webservice_rest_response',
+ { rpc => $self, result => \$result, response => $response });
+
+ # Access Control
+ $response->header("Access-Control-Allow-Origin", "*");
+ $response->header("Access-Control-Allow-Headers", "origin, content-type, accept");
+
+ # ETag support
+ my $etag = $self->bz_etag;
+ $self->bz_etag($result) if !$etag;
+
+ # If accessing through web browser, then display in readable format
+ if ($self->content_type eq 'text/html') {
+ $result = $self->json->pretty->canonical->allow_nonref->encode($result);
+
+ my $template = Bugzilla->template;
+ $content = "";
+ $template->process("rest.html.tmpl", { result => $result }, \$content)
+ || ThrowTemplateError($template->error());
+
+ $response->content_type('text/html');
+ }
+ else {
+ $content = $self->json->encode($result);
+ }
+
+ utf8::encode($content) if utf8::is_utf8($content);
+ disable_utf8();
+
+ $response->content($content);
+
+ $self->SUPER::response($response);
+}
+
+#######################################
+# Bugzilla::WebService Implementation #
+#######################################
+
+sub handle_login {
+ my $self = shift;
+
+ # If we're being called using GET, we don't allow cookie-based or Env
+ # login, because GET requests can be done cross-domain, and we don't
+ # want private data showing up on another site unless the user
+ # explicitly gives that site their username and password. (This is
+ # particularly important for JSONP, which would allow a remote site
+ # to use private data without the user's knowledge, unless we had this
+ # protection in place.) We do allow this for GET /login as we need to
+ # for Bugzilla::Auth::Persist::Cookie to create a login cookie that we
+ # can also use for Bugzilla_token support. This is OK as it requires
+ # a login and password to be supplied and will fail if they are not
+ # valid for the user.
+ if (!grep($_ eq $self->request->method, ('POST', 'PUT'))
+ && !($self->bz_class_name eq 'Bugzilla::WebService::User'
+ && $self->bz_method_name eq 'login'))
+ {
+ # XXX There's no particularly good way for us to get a parameter
+ # to Bugzilla->login at this point, so we pass this information
+ # around using request_cache, which is a bit of a hack. The
+ # implementation of it is in Bugzilla::Auth::Login::Stack.
+ Bugzilla->request_cache->{'auth_no_automatic_login'} = 1;
+ }
+
+ my $class = $self->bz_class_name;
+ my $method = $self->bz_method_name;
+ my $full_method = $class . "." . $method;
+
+ # Bypass JSONRPC::handle_login
+ Bugzilla::WebService::Server->handle_login($class, $method, $full_method);
+}
+
+############################
+# Private Method Overrides #
+############################
+
+# We do not want to run Bugzilla::WebService::Server::JSONRPC->_find_prodedure
+# as it determines the method name differently.
+sub _find_procedure {
+ my $self = shift;
+ if ($self->isa('JSON::RPC::Server::CGI')) {
+ return JSON::RPC::Server::_find_procedure($self, @_);
+ }
+ else {
+ return JSON::RPC::Legacy::Server::_find_procedure($self, @_);
+ }
+}
+
+sub _argument_type_check {
+ my $self = shift;
+ my $params;
+
+ if ($self->isa('JSON::RPC::Server::CGI')) {
+ $params = JSON::RPC::Server::_argument_type_check($self, @_);
+ }
+ else {
+ $params = JSON::RPC::Legacy::Server::_argument_type_check($self, @_);
+ }
+
+ # JSON-RPC 1.0 requires all parameters to be passed as an array, so
+ # we just pull out the first item and assume it's an object.
+ my $params_is_array;
+ if (ref $params eq 'ARRAY') {
+ $params = $params->[0];
+ $params_is_array = 1;
+ }
+
+ taint_data($params);
+
+ # Now, convert dateTime fields on input.
+ my $method = $self->bz_method_name;
+ my $pkg = $self->{dispatch_path}->{$self->path_info};
+ my @date_fields = @{ $pkg->DATE_FIELDS->{$method} || [] };
+ foreach my $field (@date_fields) {
+ if (defined $params->{$field}) {
+ my $value = $params->{$field};
+ if (ref $value eq 'ARRAY') {
+ $params->{$field} =
+ [ map { $self->datetime_format_inbound($_) } @$value ];
+ }
+ else {
+ $params->{$field} = $self->datetime_format_inbound($value);
+ }
+ }
+ }
+ my @base64_fields = @{ $pkg->BASE64_FIELDS->{$method} || [] };
+ foreach my $field (@base64_fields) {
+ if (defined $params->{$field}) {
+ $params->{$field} = decode_base64($params->{$field});
+ }
+ }
+
+ # This is the best time to do login checks.
+ $self->handle_login();
+
+ # Bugzilla::WebService packages call internal methods like
+ # $self->_some_private_method. So we have to inherit from
+ # that class as well as this Server class.
+ my $new_class = ref($self) . '::' . $pkg;
+ my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)";
+ eval "package $new_class;$isa_string;";
+ bless $self, $new_class;
+
+ # Allow extensions to modify the params post login
+ Bugzilla::Hook::process('webservice_rest_request',
+ { rpc => $self, params => $params });
+
+ if ($params_is_array) {
+ $params = [$params];
+ }
+
+ return $params;
+}
+
+###################
+# Utility Methods #
+###################
+
+sub bz_method_name {
+ my ($self, $method) = @_;
+ $self->{_bz_method_name} = $method if $method;
+ return $self->{_bz_method_name};
+}
+
+sub bz_class_name {
+ my ($self, $class) = @_;
+ $self->{_bz_class_name} = $class if $class;
+ return $self->{_bz_class_name};
+}
+
+sub bz_success_code {
+ my ($self, $value) = @_;
+ $self->{_bz_success_code} = $value if $value;
+ return $self->{_bz_success_code};
+}
+
+sub bz_rest_params {
+ my ($self, $params) = @_;
+ $self->{_bz_rest_params} = $params if $params;
+ return $self->{_bz_rest_params};
+}
+
+sub bz_rest_options {
+ my ($self, $options) = @_;
+ $self->{_bz_rest_options} = $options if $options;
+ return [ sort { $a cmp $b } @{ $self->{_bz_rest_options} } ];
+}
+
+sub rest_include_exclude {
+ my ($params) = @_;
+
+ if (exists $params->{'include_fields'} && !ref $params->{'include_fields'}) {
+ $params->{'include_fields'} = [ split(/[\s+,]/, $params->{'include_fields'}) ];
+ }
+ if (exists $params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) {
+ $params->{'exclude_fields'} = [ split(/[\s+,]/, $params->{'exclude_fields'}) ];
+ }
+
+ return $params;
+}
+
+##########################
+# Private Custom Methods #
+##########################
+
+sub _retrieve_json_params {
+ my $self = shift;
+
+ # Make a copy of the current input_params rather than edit directly
+ my $params = {};
+ %{$params} = %{ Bugzilla->input_params };
+
+ # First add any params we were able to pull out of the path
+ # based on the resource regexp
+ %{$params} = (%{$params}, %{$self->bz_rest_params}) if $self->bz_rest_params;
+
+ # Merge any additional query key/values with $obj->{params} if not a GET request
+ # We do this manually cause CGI.pm doesn't understand JSON strings.
+ if ($self->request->method ne 'GET') {
+ my $extra_params = {};
+ my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'};
+ if ($json) {
+ eval { $extra_params = $self->json->utf8(0)->decode($json); };
+ if ($@) {
+ ThrowUserError('json_rpc_invalid_params', { err_msg => $@ });
+ }
+ }
+
+ # Allow parameters in the query string if request was not GET.
+ # Note: query string parameters will override any matching params
+ # also specified in the request body.
+ foreach my $param ($self->cgi->url_param()) {
+ $extra_params->{$param} = $self->cgi->url_param($param);
+ }
+
+ %{$params} = (%{$params}, %{$extra_params}) if %{$extra_params};
+ }
+
+ return $params;
+}
+
+sub _find_resource {
+ my ($self, $path) = @_;
+
+ # Load in the WebService module from the dispatch map and then call
+ # $module->rest_resources to get the resources array ref.
+ my $resources = {};
+ foreach my $module (values %{ $self->{dispatch_path} }) {
+ eval("require $module") || die $@;
+ next if !$module->can('rest_resources');
+ $resources->{$module} = $module->rest_resources;
+ }
+
+ Bugzilla::Hook::process('webservice_rest_resources',
+ { rpc => $self, resources => $resources });
+
+ # Use the resources hash from each module loaded earlier to determine
+ # which handler to use based on a regex match of the CGI path.
+ # Also any matches found in the regex will be passed in later to the
+ # handler for possible use.
+ my $request_method = $self->request->method;
+
+ my (@matches, $handler_found, $handler_method, $handler_class);
+ foreach my $class (keys %{ $resources }) {
+ # The resource data for each module needs to be
+ # an array ref with an even number of elements
+ # to work correctly.
+ next if (ref $resources->{$class} ne 'ARRAY'
+ || scalar @{ $resources->{$class} } % 2 != 0);
+
+ while (my $regex = shift @{ $resources->{$class} }) {
+ my $options_data = shift @{ $resources->{$class} };
+ next if ref $options_data ne 'HASH';
+
+ if (@matches = ($path =~ $regex)) {
+ # If a specific path is accompanied by a OPTIONS request
+ # method, the user is asking for a list of possible request
+ # methods for a specific path.
+ $self->bz_rest_options([ keys %{ $options_data } ]);
+
+ if ($options_data->{$request_method}) {
+ my $resource_data = $options_data->{$request_method};
+ $self->bz_class_name($class);
+
+ # The method key/value can be a simple scalar method name
+ # or a anonymous subroutine so we execute it here.
+ my $method = ref $resource_data->{method} eq 'CODE'
+ ? $resource_data->{method}->($self)
+ : $resource_data->{method};
+ $self->bz_method_name($method);
+
+ # Pull out any parameters parsed from the URL path
+ # and store them for use by the method.
+ if ($resource_data->{params}) {
+ $self->bz_rest_params($resource_data->{params}->(@matches));
+ }
+
+ # If a special success code is needed for this particular
+ # method, then store it for later when generating response.
+ if ($resource_data->{success_code}) {
+ $self->bz_success_code($resource_data->{success_code});
+ }
+ $handler_found = 1;
+ }
+ }
+ last if $handler_found;
+ }
+ last if $handler_found;
+ }
+
+ return $handler_found;
+}
+
+sub _best_content_type {
+ my ($self, @types) = @_;
+ return ($self->_simple_content_negotiation(@types))[0] || '*/*';
+}
+
+sub _simple_content_negotiation {
+ my ($self, @types) = @_;
+ my @accept_types = $self->_get_content_prefs();
+ # Return the types as-is if no accept header sent, since sorting will be a no-op.
+ if (!@accept_types) {
+ return @types;
+ }
+ my $score = sub { $self->_score_type(shift, @accept_types) };
+ return sort {$score->($b) <=> $score->($a)} @types;
+}
+
+sub _score_type {
+ my ($self, $type, @accept_types) = @_;
+ my $score = scalar(@accept_types);
+ for my $accept_type (@accept_types) {
+ return $score if $type eq $accept_type;
+ $score--;
+ }
+ return 0;
+}
+
+sub _get_content_prefs {
+ my $self = shift;
+ my $default_weight = 1;
+ my @prefs;
+
+ # Parse the Accept header, and save type name, score, and position.
+ my @accept_types = split /,/, $self->cgi->http('accept') || '';
+ my $order = 0;
+ for my $accept_type (@accept_types) {
+ my ($weight) = ($accept_type =~ /q=(\d\.\d+|\d+)/);
+ my ($name) = ($accept_type =~ m#(\S+/[^;]+)#);
+ next unless $name;
+ push @prefs, { name => $name, order => $order++};
+ if (defined $weight) {
+ $prefs[-1]->{score} = $weight;
+ } else {
+ $prefs[-1]->{score} = $default_weight;
+ $default_weight -= 0.001;
+ }
+ }
+
+ # Sort the types by score, subscore by order, and pull out just the name
+ @prefs = map {$_->{name}} sort {$b->{score} <=> $a->{score} ||
+ $a->{order} <=> $b->{order}} @prefs;
+ return @prefs;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::WebService::Server::REST - The REST Interface to Bugzilla
+
+=head1 DESCRIPTION
+
+This documentation describes things about the Bugzilla WebService that
+are specific to REST. For a general overview of the Bugzilla WebServices,
+see L<Bugzilla::WebService>. The L<Bugzilla::WebService::Server::REST>
+module is a sub-class of L<Bugzilla::WebService::Server::JSONRPC> so any
+method documentation not found here can be viewed in it's POD.
+
+Please note that I<everything> about this REST interface is
+B<EXPERIMENTAL>. If you want a fully stable API, please use the
+C<Bugzilla::WebService::Server::XMLRPC|XML-RPC> interface.
+
+=head1 CONNECTING
+
+The endpoint for the REST interface is the C<rest.cgi> script in
+your Bugzilla installation. If using Apache and mod_rewrite is installed
+and enabled, you can also use /rest/ as your endpoint. For example, if your
+Bugzilla is at C<bugzilla.yourdomain.com>, then your REST client would
+access the API via: C<http://bugzilla.yourdomain.com/rest/bug/35> which
+looks cleaner.
+
+=head1 BROWSING
+
+If the Accept: header of a request is set to text/html (as it is by an
+ordinary web browser) then the API will return the JSON data as a HTML
+page which the browser can display. In other words, you can play with the
+API using just your browser and see results in a human-readable form.
+This is a good way to try out the various GET calls, even if you can't use
+it for POST or PUT.
+
+=head1 DATA FORMAT
+
+The REST API only supports JSON input, and either JSON and JSONP output.
+So objects sent and received must be in JSON format. Basically since
+the REST API is a sub class of the JSONRPC API, you can refer to
+L<JSONRPC|Bugzilla::WebService::Server::JSONRPC> for more information
+on data types that are valid for REST.
+
+On every request, you must set both the "Accept" and "Content-Type" HTTP
+headers to the MIME type of the data format you are using to communicate with
+the API. Content-Type tells the API how to interpret your request, and Accept
+tells it how you want your data back. "Content-Type" must be "application/json".
+"Accept" can be either that, or "application/javascript" for JSONP - add a "callback"
+parameter to name your callback.
+
+Parameters may also be passed in as part of the query string for non-GET requests
+and will override any matching parameters in the request body.
+
+=head1 AUTHENTICATION
+
+Along with viewing data as an anonymous user, you may also see private information
+if you have a Bugzilla account by providing your login credentials.
+
+=over
+
+=item Login name and password
+
+Pass in as query parameters of any request:
+
+login=fred@example.com&password=ilovecheese
+
+Remember to URL encode any special characters, which are often seen in passwords and to
+also enable SSL support.
+
+=item Login token
+
+By calling GET /login?login=fred@example.com&password=ilovecheese, you get back
+a C<token> value which can then be passed to each subsequent call as
+authentication. This is useful for third party clients that cannot use cookies
+and do not want to store a user's login and password in the client. You can also
+pass in "token" as a convenience.
+
+=back
+
+=head1 ERRORS
+
+When an error occurs over REST, a hash structure is returned with the key C<error>
+set to C<true>.
+
+The error contents look similar to:
+
+ { "error": true, "message": "Some message here", "code": 123 }
+
+Every error has a "code", as described in L<Bugzilla::WebService/ERRORS>.
+Errors with a numeric C<code> higher than 100000 are errors thrown by
+the JSON-RPC library that Bugzilla uses, not by Bugzilla.
+
+=head1 UTILITY FUNCTIONS
+
+=over
+
+=item B<handle>
+
+This method overrides the handle method provided by JSONRPC so that certain
+actions related to REST such as determining the proper resource to use,
+loading query parameters, etc. can be done before the proper WebService
+method is executed.
+
+=item B<response>
+
+This method overrides the response method provided by JSONRPC so that
+the response content can be altered for REST before being returned to
+the client.
+
+=item B<handle_login>
+
+This method determines the proper WebService all to make based on class
+and method name determined earlier. Then calls L<Bugzilla::WebService::Server::handle_login>
+which will attempt to authenticate the client.
+
+=item B<bz_method_name>
+
+The WebService method name that matches the path used by the client.
+
+=item B<bz_class_name>
+
+The WebService class containing the method that matches the path used by the client.
+
+=item B<bz_rest_params>
+
+Each REST resource contains a hash key called C<params> that is a subroutine reference.
+This subroutine will return a hash structure based on matched values from the path
+information that is formatted properly for the WebService method that will be called.
+
+=item B<bz_rest_options>
+
+When a client uses the OPTIONS request method along with a specific path, they are
+requesting the list of request methods that are valid for the path. Such as for the
+path /bug, the valid request methods are GET (search) and POST (create). So the
+client would receive in the response header, C<Access-Control-Allow-Methods: GET, POST>.
+
+=item B<bz_success_code>
+
+Each resource can specify a specific SUCCESS CODE if the operation completes successfully.
+OTherwise STATUS OK (200) is the default returned.
+
+=item B<rest_include_exclude>
+
+Normally the WebService methods required C<include_fields> and C<exclude_fields> to be an
+array of field names. REST allows for the values for these to be instead comma delimited
+string of field names. This method converts the latter into the former so the WebService
+methods will not complain.
+
+=back
+
+=head1 SEE ALSO
+
+L<Bugzilla::WebService>
diff --git a/Bugzilla/WebService/Server/REST/Resources/Bug.pm b/Bugzilla/WebService/Server/REST/Resources/Bug.pm
new file mode 100644
index 000000000..d0f470fcd
--- /dev/null
+++ b/Bugzilla/WebService/Server/REST/Resources/Bug.pm
@@ -0,0 +1,190 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::WebService::Server::REST::Resources::Bug;
+
+use 5.10.1;
+use strict;
+
+use Bugzilla::WebService::Constants;
+use Bugzilla::WebService::Bug;
+
+BEGIN {
+ *Bugzilla::WebService::Bug::rest_resources = \&_rest_resources;
+};
+
+sub _rest_resources {
+ my $rest_resources = [
+ qr{^/bug$}, {
+ GET => {
+ method => 'search',
+ },
+ POST => {
+ method => 'create',
+ status_code => STATUS_CREATED
+ }
+ },
+ qr{^/bug/([^/]+)$}, {
+ GET => {
+ method => 'get',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ }
+ },
+ PUT => {
+ method => 'update',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ }
+ }
+ },
+ qr{^/bug/([^/]+)/comment$}, {
+ GET => {
+ method => 'comments',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ }
+ },
+ POST => {
+ method => 'add_comment',
+ params => sub {
+ return { id => $_[0] };
+ },
+ success_code => STATUS_CREATED
+ }
+ },
+ qr{^/bug/comment/([^/]+)$}, {
+ GET => {
+ method => 'comments',
+ params => sub {
+ return { comment_ids => [ $_[0] ] };
+ }
+ }
+ },
+ qr{^/bug/comment/tags/([^/]+)$}, {
+ GET => {
+ method => 'search_comment_tags',
+ params => sub {
+ return { query => $_[0] };
+ },
+ },
+ },
+ qr{^/bug/comment/([^/]+)/tags$}, {
+ PUT => {
+ method => 'update_comment_tags',
+ params => sub {
+ return { comment_id => $_[0] };
+ },
+ },
+ },
+ qr{^/bug/([^/]+)/history$}, {
+ GET => {
+ method => 'history',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ },
+ }
+ },
+ qr{^/bug/([^/]+)/attachment$}, {
+ GET => {
+ method => 'attachments',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ }
+ },
+ POST => {
+ method => 'add_attachment',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ },
+ success_code => STATUS_CREATED
+ }
+ },
+ qr{^/bug/attachment/([^/]+)$}, {
+ GET => {
+ method => 'attachments',
+ params => sub {
+ return { attachment_ids => [ $_[0] ] };
+ }
+ },
+ PUT => {
+ method => 'update_attachment',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ }
+ }
+ },
+ qr{^/field/bug$}, {
+ GET => {
+ method => 'fields',
+ }
+ },
+ qr{^/field/bug/([^/]+)$}, {
+ GET => {
+ method => 'fields',
+ params => sub {
+ my $value = $_[0];
+ my $param = 'names';
+ $param = 'ids' if $value =~ /^\d+$/;
+ return { $param => [ $_[0] ] };
+ }
+ }
+ },
+ qr{^/field/bug/([^/]+)/values$}, {
+ GET => {
+ method => 'legal_values',
+ params => sub {
+ return { field => $_[0] };
+ }
+ }
+ },
+ qr{^/field/bug/([^/]+)/([^/]+)/values$}, {
+ GET => {
+ method => 'legal_values',
+ params => sub {
+ return { field => $_[0],
+ product_id => $_[1] };
+ }
+ }
+ },
+ qr{^/flag_types/([^/]+)/([^/]+)$}, {
+ GET => {
+ method => 'flag_types',
+ params => sub {
+ return { product => $_[0],
+ component => $_[1] };
+ }
+ }
+ },
+ qr{^/flag_types/([^/]+)$}, {
+ GET => {
+ method => 'flag_types',
+ params => sub {
+ return { product => $_[0] };
+ }
+ }
+ }
+ ];
+ return $rest_resources;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Webservice::Server::REST::Resources::Bug - The REST API for creating,
+changing, and getting the details of bugs.
+
+=head1 DESCRIPTION
+
+This part of the Bugzilla REST API allows you to file a new bug in Bugzilla,
+or get information about bugs that have already been filed.
+
+See L<Bugzilla::WebService::Bug> for more details on how to use this part of
+the REST API.
diff --git a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm
new file mode 100644
index 000000000..a434d4bef
--- /dev/null
+++ b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm
@@ -0,0 +1,52 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::WebService::Server::REST::Resources::BugUserLastVisit;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+BEGIN {
+ *Bugzilla::WebService::BugUserLastVisit::rest_resources = \&_rest_resources;
+}
+
+sub _rest_resources {
+ return [
+ # bug-id
+ qr{^/bug_user_last_visit/(\d+)$}, {
+ GET => {
+ method => 'get',
+ params => sub {
+ return { ids => $_[0] };
+ },
+ },
+ POST => {
+ method => 'update',
+ params => sub {
+ return { ids => $_[0] };
+ },
+ },
+ },
+ ];
+}
+
+1;
+__END__
+
+=head1 NAME
+
+Bugzilla::Webservice::Server::REST::Resources::BugUserLastVisit - The
+BugUserLastVisit REST API
+
+=head1 DESCRIPTION
+
+This part of the Bugzilla REST API allows you to lookup and update the last time
+a user visited a bug.
+
+See L<Bugzilla::WebService::BugUserLastVisit> for more details on how to use
+this part of the REST API.
diff --git a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm
new file mode 100644
index 000000000..1c86f77bc
--- /dev/null
+++ b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm
@@ -0,0 +1,69 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::WebService::Server::REST::Resources::Bugzilla;
+
+use 5.10.1;
+use strict;
+
+use Bugzilla::WebService::Constants;
+use Bugzilla::WebService::Bugzilla;
+
+BEGIN {
+ *Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources;
+};
+
+sub _rest_resources {
+ my $rest_resources = [
+ qr{^/version$}, {
+ GET => {
+ method => 'version'
+ }
+ },
+ qr{^/extensions$}, {
+ GET => {
+ method => 'extensions'
+ }
+ },
+ qr{^/timezone$}, {
+ GET => {
+ method => 'timezone'
+ }
+ },
+ qr{^/time$}, {
+ GET => {
+ method => 'time'
+ }
+ },
+ qr{^/last_audit_time$}, {
+ GET => {
+ method => 'last_audit_time'
+ }
+ },
+ qr{^/parameters$}, {
+ GET => {
+ method => 'parameters'
+ }
+ }
+ ];
+ return $rest_resources;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::WebService::Bugzilla - Global functions for the webservice interface.
+
+=head1 DESCRIPTION
+
+This provides functions that tell you about Bugzilla in general.
+
+See L<Bugzilla::WebService::Bugzilla> for more details on how to use this part
+of the REST API.
diff --git a/Bugzilla/WebService/Server/REST/Resources/Classification.pm b/Bugzilla/WebService/Server/REST/Resources/Classification.pm
new file mode 100644
index 000000000..5bb697ac1
--- /dev/null
+++ b/Bugzilla/WebService/Server/REST/Resources/Classification.pm
@@ -0,0 +1,49 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::WebService::Server::REST::Resources::Classification;
+
+use 5.10.1;
+use strict;
+
+use Bugzilla::WebService::Constants;
+use Bugzilla::WebService::Classification;
+
+BEGIN {
+ *Bugzilla::WebService::Classification::rest_resources = \&_rest_resources;
+};
+
+sub _rest_resources {
+ my $rest_resources = [
+ qr{^/classification/([^/]+)$}, {
+ GET => {
+ method => 'get',
+ params => sub {
+ my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+ return { $param => [ $_[0] ] };
+ }
+ }
+ }
+ ];
+ return $rest_resources;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Webservice::Server::REST::Resources::Classification - The Classification REST API
+
+=head1 DESCRIPTION
+
+This part of the Bugzilla REST API allows you to deal with the available Classifications.
+You will be able to get information about them as well as manipulate them.
+
+See L<Bugzilla::WebService::Bug> for more details on how to use this part
+of the REST API.
diff --git a/Bugzilla/WebService/Server/REST/Resources/Group.pm b/Bugzilla/WebService/Server/REST/Resources/Group.pm
new file mode 100644
index 000000000..9200d609d
--- /dev/null
+++ b/Bugzilla/WebService/Server/REST/Resources/Group.pm
@@ -0,0 +1,56 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::WebService::Server::REST::Resources::Group;
+
+use 5.10.1;
+use strict;
+
+use Bugzilla::WebService::Constants;
+use Bugzilla::WebService::Group;
+
+BEGIN {
+ *Bugzilla::WebService::Group::rest_resources = \&_rest_resources;
+};
+
+sub _rest_resources {
+ my $rest_resources = [
+ qr{^/group$}, {
+ POST => {
+ method => 'create',
+ success_code => STATUS_CREATED
+ }
+ },
+ qr{^/group/([^/]+)$}, {
+ PUT => {
+ method => 'update',
+ params => sub {
+ my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+ return { $param => [ $_[0] ] };
+ }
+ }
+ }
+ ];
+ return $rest_resources;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Webservice::Server::REST::Resources::Group - The REST API for
+creating, changing, and getting information about Groups.
+
+=head1 DESCRIPTION
+
+This part of the Bugzilla REST API allows you to create Groups and
+get information about them.
+
+See L<Bugzilla::WebService::Group> for more details on how to use this part
+of the REST API.
diff --git a/Bugzilla/WebService/Server/REST/Resources/Product.pm b/Bugzilla/WebService/Server/REST/Resources/Product.pm
new file mode 100644
index 000000000..acee3887b
--- /dev/null
+++ b/Bugzilla/WebService/Server/REST/Resources/Product.pm
@@ -0,0 +1,82 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::WebService::Server::REST::Resources::Product;
+
+use 5.10.1;
+use strict;
+
+use Bugzilla::WebService::Constants;
+use Bugzilla::WebService::Product;
+
+use Bugzilla::Error;
+
+BEGIN {
+ *Bugzilla::WebService::Product::rest_resources = \&_rest_resources;
+};
+
+sub _rest_resources {
+ my $rest_resources = [
+ qr{^/product_accessible$}, {
+ GET => {
+ method => 'get_accessible_products'
+ }
+ },
+ qr{^/product_enterable$}, {
+ GET => {
+ method => 'get_enterable_products'
+ }
+ },
+ qr{^/product_selectable$}, {
+ GET => {
+ method => 'get_selectable_products'
+ }
+ },
+ qr{^/product$}, {
+ GET => {
+ method => 'get'
+ },
+ POST => {
+ method => 'create',
+ success_code => STATUS_CREATED
+ }
+ },
+ qr{^/product/([^/]+)$}, {
+ GET => {
+ method => 'get',
+ params => sub {
+ my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+ return { $param => [ $_[0] ] };
+ }
+ },
+ PUT => {
+ method => 'update',
+ params => sub {
+ my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+ return { $param => [ $_[0] ] };
+ }
+ }
+ },
+ ];
+ return $rest_resources;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Webservice::Server::REST::Resources::Product - The Product REST API
+
+=head1 DESCRIPTION
+
+This part of the Bugzilla REST API allows you to list the available Products and
+get information about them.
+
+See L<Bugzilla::WebService::Bug> for more details on how to use this part of
+the REST API.
diff --git a/Bugzilla/WebService/Server/REST/Resources/User.pm b/Bugzilla/WebService/Server/REST/Resources/User.pm
new file mode 100644
index 000000000..badbc94b2
--- /dev/null
+++ b/Bugzilla/WebService/Server/REST/Resources/User.pm
@@ -0,0 +1,80 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::WebService::Server::REST::Resources::User;
+
+use 5.10.1;
+use strict;
+
+use Bugzilla::WebService::Constants;
+use Bugzilla::WebService::User;
+
+BEGIN {
+ *Bugzilla::WebService::User::rest_resources = \&_rest_resources;
+};
+
+sub _rest_resources {
+ my $rest_resources = [
+ qr{^/valid_login$}, {
+ GET => {
+ method => 'valid_login'
+ }
+ },
+ qr{^/login$}, {
+ GET => {
+ method => 'login'
+ }
+ },
+ qr{^/logout$}, {
+ GET => {
+ method => 'logout'
+ }
+ },
+ qr{^/user$}, {
+ GET => {
+ method => 'get'
+ },
+ POST => {
+ method => 'create',
+ success_code => STATUS_CREATED
+ }
+ },
+ qr{^/user/([^/]+)$}, {
+ GET => {
+ method => 'get',
+ params => sub {
+ my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+ return { $param => [ $_[0] ] };
+ }
+ },
+ PUT => {
+ method => 'update',
+ params => sub {
+ my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+ return { $param => [ $_[0] ] };
+ }
+ }
+ }
+ ];
+ return $rest_resources;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Webservice::Server::REST::Resources::User - The User Account REST API
+
+=head1 DESCRIPTION
+
+This part of the Bugzilla REST API allows you to get User information as well
+as create User Accounts.
+
+See L<Bugzilla::WebService::Bug> for more details on how to use this part of
+the REST API.
diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm
index fc297421a..8d9108122 100644
--- a/Bugzilla/WebService/Server/XMLRPC.pm
+++ b/Bugzilla/WebService/Server/XMLRPC.pm
@@ -30,9 +30,10 @@ if ($ENV{MOD_PERL}) {
}
use Bugzilla::WebService::Constants;
+use Bugzilla::Util;
-# Allow WebService methods to call XMLRPC::Lite's type method directly
BEGIN {
+ # Allow WebService methods to call XMLRPC::Lite's type method directly
*Bugzilla::WebService::type = sub {
my ($self, $type, $value) = @_;
if ($type eq 'dateTime') {
@@ -41,8 +42,19 @@ BEGIN {
$value = Bugzilla::WebService::Server->datetime_format_outbound($value);
$value =~ s/-//g;
}
+ elsif ($type eq 'email') {
+ $type = 'string';
+ if (Bugzilla->params->{'webservice_email_filter'}) {
+ $value = email_filter($value);
+ }
+ }
return XMLRPC::Data->type($type)->value($value);
};
+
+ # Add support for ETags into XMLRPC WebServices
+ *Bugzilla::WebService::bz_etag = sub {
+ return Bugzilla::WebService::Server->bz_etag($_[1]);
+ };
}
sub initialize {
@@ -56,22 +68,38 @@ sub initialize {
sub make_response {
my $self = shift;
+ my $cgi = Bugzilla->cgi;
$self->SUPER::make_response(@_);
# XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around
# its cookies in Bugzilla::CGI, so we need to copy them over.
- foreach my $cookie (@{Bugzilla->cgi->{'Bugzilla_cookie_list'}}) {
+ foreach my $cookie (@{$cgi->{'Bugzilla_cookie_list'}}) {
$self->response->headers->push_header('Set-Cookie', $cookie);
}
# Copy across security related headers from Bugzilla::CGI
- foreach my $header (split(/[\r\n]+/, Bugzilla->cgi->header)) {
+ foreach my $header (split(/[\r\n]+/, $cgi->header)) {
my ($name, $value) = $header =~ /^([^:]+): (.*)/;
if (!$self->response->headers->header($name)) {
$self->response->headers->header($name => $value);
}
}
+
+ # ETag support
+ my $etag = $self->bz_etag;
+ if (!$etag) {
+ my $data = $self->response->as_string;
+ $etag = $self->bz_etag($data);
+ }
+
+ if ($etag && $cgi->check_etag($etag)) {
+ $self->response->headers->push_header('ETag', $etag);
+ $self->response->headers->push_header('status', '304 Not Modified');
+ }
+ elsif ($etag) {
+ $self->response->headers->push_header('ETag', $etag);
+ }
}
sub handle_login {
diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm
index deb7518ec..988ae3cd5 100644
--- a/Bugzilla/WebService/User.pm
+++ b/Bugzilla/WebService/User.pm
@@ -28,7 +28,10 @@ use Bugzilla::Error;
use Bugzilla::Group;
use Bugzilla::User;
use Bugzilla::Util qw(trim);
-use Bugzilla::WebService::Util qw(filter validate);
+use Bugzilla::WebService::Util qw(filter filter_wants validate);
+use Bugzilla::Hook;
+
+use List::Util qw(first);
# Don't need auth to login
use constant LOGIN_EXEMPT => {
@@ -70,14 +73,36 @@ sub login {
$input_params->{'Bugzilla_password'} = $params->{password};
$input_params->{'Bugzilla_remember'} = $remember;
- Bugzilla->login();
- return { id => $self->type('int', Bugzilla->user->id) };
+ my $user = Bugzilla->login();
+
+ my $result = { id => $self->type('int', $user->id) };
+
+ # We will use the stored cookie value combined with the user id
+ # to create a token that can be used with future requests in the
+ # query parameters
+ my $login_cookie = first { $_->name eq 'Bugzilla_logincookie' }
+ @{ Bugzilla->cgi->{'Bugzilla_cookie_list'} };
+ if ($login_cookie) {
+ $result->{'token'} = $user->id . "-" . $login_cookie->value;
+ }
+
+ return $result;
}
sub logout {
my $self = shift;
Bugzilla->logout;
- return undef;
+}
+
+sub valid_login {
+ my ($self, $params) = @_;
+ defined $params->{login}
+ || ThrowCodeError('param_required', { param => 'login' });
+ Bugzilla->login();
+ if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) {
+ return $self->type('boolean', 1);
+ }
+ return $self->type('boolean', 0);
}
#################
@@ -124,7 +149,9 @@ sub create {
# $call = $rpc->call( 'User.get', { ids => [1,2,3],
# names => ['testusera@redhat.com', 'testuserb@redhat.com'] });
sub get {
- my ($self, $params) = validate(@_, 'names', 'ids');
+ my ($self, $params) = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups');
+
+ Bugzilla->switch_to_shadow_db();
defined($params->{names}) || defined($params->{ids})
|| defined($params->{match})
@@ -152,11 +179,11 @@ sub get {
}
my $in_group = $self->_filter_users_by_group(
\@user_objects, $params);
- @users = map {filter $params, {
+ @users = map { filter $params, {
id => $self->type('int', $_->id),
- real_name => $self->type('string', $_->name),
- name => $self->type('string', $_->login),
- }} @$in_group;
+ real_name => $self->type('string', $_->name),
+ name => $self->type('email', $_->login),
+ } } @$in_group;
return { users => \@users };
}
@@ -196,33 +223,47 @@ sub get {
}
}
}
-
- my $in_group = $self->_filter_users_by_group(
- \@user_objects, $params);
- if (Bugzilla->user->in_group('editusers')) {
- @users =
- map {filter $params, {
- id => $self->type('int', $_->id),
- real_name => $self->type('string', $_->name),
- name => $self->type('string', $_->login),
- email => $self->type('string', $_->email),
- can_login => $self->type('boolean', $_->is_enabled ? 1 : 0),
- email_enabled => $self->type('boolean', $_->email_enabled),
- login_denied_text => $self->type('string', $_->disabledtext),
- }} @$in_group;
-
- }
- else {
- @users =
- map {filter $params, {
- id => $self->type('int', $_->id),
- real_name => $self->type('string', $_->name),
- name => $self->type('string', $_->login),
- email => $self->type('string', $_->email),
- can_login => $self->type('boolean', $_->is_enabled ? 1 : 0),
- }} @$in_group;
+
+ my $in_group = $self->_filter_users_by_group(\@user_objects, $params);
+ foreach my $user (@$in_group) {
+ my $user_info = filter $params, {
+ id => $self->type('int', $user->id),
+ real_name => $self->type('string', $user->name),
+ name => $self->type('email', $user->login),
+ email => $self->type('email', $user->email),
+ can_login => $self->type('boolean', $user->is_enabled ? 1 : 0),
+ };
+
+ if (Bugzilla->user->in_group('editusers')) {
+ $user_info->{email_enabled} = $self->type('boolean', $user->email_enabled);
+ $user_info->{login_denied_text} = $self->type('string', $user->disabledtext);
+ }
+
+ if (Bugzilla->user->id == $user->id) {
+ if (filter_wants($params, 'saved_searches')) {
+ $user_info->{saved_searches} = [
+ map { $self->_query_to_hash($_) } @{ $user->queries }
+ ];
+ }
+ }
+
+ if (filter_wants($params, 'groups')) {
+ if (Bugzilla->user->id == $user->id || Bugzilla->user->in_group('editusers')) {
+ $user_info->{groups} = [
+ map { $self->_group_to_hash($_) } @{ $user->groups }
+ ];
+ }
+ else {
+ $user_info->{groups} = $self->_filter_bless_groups($user->groups);
+ }
+ }
+
+ push(@users, $user_info);
}
+ Bugzilla::Hook::process('webservice_user_get',
+ { webservice => $self, params => $params, users => \@users });
+
return { users => \@users };
}
@@ -259,6 +300,40 @@ sub _user_in_any_group {
return 0;
}
+sub _filter_bless_groups {
+ my ($self, $groups) = @_;
+ my $user = Bugzilla->user;
+
+ my @filtered_groups;
+ foreach my $group (@$groups) {
+ next unless ($user->in_group('editusers') || $user->can_bless($group->id));
+ push(@filtered_groups, $self->_group_to_hash($group));
+ }
+
+ return \@filtered_groups;
+}
+
+sub _group_to_hash {
+ my ($self, $group) = @_;
+ my $item = {
+ id => $self->type('int', $group->id),
+ name => $self->type('string', $group->name),
+ description => $self->type('string', $group->description),
+ };
+ return $item;
+}
+
+sub _query_to_hash {
+ my ($self, $query) = @_;
+ my $item = {
+ id => $self->type('int', $query->id),
+ name => $self->type('string', $query->name),
+ url => $self->type('string', $query->url),
+ };
+
+ return $item;
+}
+
1;
__END__
@@ -277,6 +352,10 @@ log in/out using an existing account.
See L<Bugzilla::WebService> for a description of how parameters are passed,
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
+Although the data input and output is the same for JSONRPC, XMLRPC and REST,
+the directions for how to access the data via REST is noted in each method
+where applicable.
+
=head1 Logging In and Out
=head2 login
@@ -295,7 +374,7 @@ etc. This method logs in an user.
=over
-=item C<login> (string) - The user's login name.
+=item C<login> (string) - The user's login name.
=item C<password> (string) - The user's password.
@@ -311,10 +390,14 @@ management of cookies across sessions.
=item B<Returns>
-On success, a hash containing one item, C<id>, the numeric id of the
-user that was logged in. A set of http cookies is also sent with the
-response. These cookies must be sent along with any future requests
-to the webservice, for the duration of the session.
+On success, a hash containing two items, C<id>, the numeric id of the
+user that was logged in, and a C<token> which can be passed in
+the parameters as authentication in other calls. A set of http cookies
+is also sent with the response. These cookies *or* the token can be sent
+along with any future requests to the webservice, for the duration of the
+session. Note that cookies are not accepted for GET requests for JSONRPC
+and REST for security reasons. You may, however, use the token or valid
+login parameters for those requests.
=item B<Errors>
@@ -360,6 +443,50 @@ Log out the user. Does nothing if there is no user logged in.
=back
+=head2 valid_login
+
+B<UNSTABLE>
+
+=over
+
+=item B<Description>
+
+This method will verify whether a client's cookies or current login
+token is still valid or have expired. A valid username must be provided
+as well that matches.
+
+=item B<Params>
+
+=over
+
+=item C<login>
+
+The login name that matches the provided cookies or token.
+
+=item C<token>
+
+(string) Persistent login token current being used for authentication (optional).
+Cookies passed by client will be used before the token if both provided.
+
+=back
+
+=item B<Returns>
+
+Returns true/false depending on if the current cookies or token are valid
+for the provided username.
+
+=item B<Errors> (none)
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
=head1 Account Creation
=head2 offer_account_by_email
@@ -419,6 +546,13 @@ actually receive an email. This function does not check that.
You must be logged in and have the C<editusers> privilege in order to
call this function.
+=item B<REST>
+
+POST /user
+
+The params to include in the POST body as well as the returned data format,
+are the same as below.
+
=item B<Params>
=over
@@ -462,6 +596,8 @@ password is under three characters.)
=item Error 503 (Password Too Long) removed in Bugzilla B<3.6>.
+=item REST API call added in Bugzilla B<5.0>.
+
=back
=back
@@ -478,6 +614,18 @@ B<STABLE>
Gets information about user accounts in Bugzilla.
+=item B<REST>
+
+To get information about a single user:
+
+GET /user/<user_id_or_name>
+
+To search for users by name, group using URL params same as below:
+
+GET /user
+
+The returned data format is the same as below.
+
=item B<Params>
B<Note>: At least one of C<ids>, C<names>, or C<match> must be specified.
@@ -539,6 +687,14 @@ match string. Setting C<include_disabled> to C<true> will include disabled
users in the returned results even if their username doesn't fully match
the input string.
+=item B<History>
+
+=over
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
=back
=item B<Returns>
@@ -581,10 +737,60 @@ C<string> A text field that holds the reason for disabling a user from logging
into bugzilla, if empty then the user account is enabled. Otherwise it is
disabled/closed.
+=item groups
+
+C<array> An array of group hashes the user is a member of. Each hash describes
+the group and contains the following items:
+
+=over
+
+=item id
+
+C<int> The group id
+
+=item name
+
+C<string> The name of the group
+
+=item description
+
+C<string> The description for the group
+
+=back
+
+=over
+
+=item saved_searches
+
+C<array> An array of hashes, each of which represents a user's saved search and has
+the following keys:
+
+=over
+
+=item id
+
+C<int> An integer id uniquely identifying the saved search.
+
+=item name
+
+C<string> The name of the saved search.
+
+=item url
+
+C<string> The CGI parameters for the saved search.
+
+=back
+
+B<Note>: The elements of the returned array (i.e. hashes) are ordered by the
+name of each saved search.
+
+=back
+
B<Note>: If you are not logged in to Bugzilla when you call this function, you
will only be returned the C<id>, C<name>, and C<real_name> items. If you are
logged in and not in editusers group, you will only be returned the C<id>, C<name>,
-C<real_name>, C<email>, and C<can_login> items.
+C<real_name>, C<email>, and C<can_login> items. The groups returned are filtered
+based on your permission to bless each group.
=back
@@ -625,9 +831,15 @@ exist or you do not belong to it.
=item C<include_disabled> added in Bugzilla B<4.0>. Default behavior
for C<match> has changed to only returning enabled accounts.
+=item C<groups> Added in Bugzilla B<4.4>.
+
+=item C<saved_searches> Added in Bugzilla B<4.4>.
+
=item Error 804 has been added in Bugzilla 4.0.9 and 4.2.4. It's now
illegal to pass a group name you don't belong to.
=back
+=item REST API call added in Bugzilla B<5.0>.
+
=back
diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm
index fe4105ca2..856fd3481 100644
--- a/Bugzilla/WebService/Util.pm
+++ b/Bugzilla/WebService/Util.pm
@@ -21,6 +21,13 @@
package Bugzilla::WebService::Util;
use strict;
+
+use Bugzilla::Flag;
+use Bugzilla::FlagType;
+use Bugzilla::Error;
+
+use Storable qw(dclone);
+
use base qw(Exporter);
# We have to "require", not "use" this, because otherwise it tries to
@@ -28,36 +35,165 @@ use base qw(Exporter);
require Test::Taint;
our @EXPORT_OK = qw(
+ extract_flags
filter
filter_wants
taint_data
validate
+ translate
+ params_to_objects
+ fix_credentials
);
-sub filter ($$) {
- my ($params, $hash) = @_;
+sub extract_flags {
+ my ($flags, $bug, $attachment) = @_;
+ my (@new_flags, @old_flags);
+
+ my $flag_types = $attachment ? $attachment->flag_types : $bug->flag_types;
+ my $current_flags = $attachment ? $attachment->flags : $bug->flags;
+
+ # Copy the user provided $flags as we may call extract_flags more than
+ # once when editing multiple bugs or attachments.
+ my $flags_copy = dclone($flags);
+
+ foreach my $flag (@$flags_copy) {
+ my $id = $flag->{id};
+ my $type_id = $flag->{type_id};
+
+ my $new = delete $flag->{new};
+ my $name = delete $flag->{name};
+
+ if ($id) {
+ my $flag_obj = grep($id == $_->id, @$current_flags);
+ $flag_obj || ThrowUserError('object_does_not_exist',
+ { class => 'Bugzilla::Flag', id => $id });
+ }
+ elsif ($type_id) {
+ my $type_obj = grep($type_id == $_->id, @$flag_types);
+ $type_obj || ThrowUserError('object_does_not_exist',
+ { class => 'Bugzilla::FlagType', id => $type_id });
+ if (!$new) {
+ my @flag_matches = grep($type_id == $_->type->id, @$current_flags);
+ @flag_matches > 1 && ThrowUserError('flag_not_unique',
+ { value => $type_id });
+ if (!@flag_matches) {
+ delete $flag->{id};
+ }
+ else {
+ delete $flag->{type_id};
+ $flag->{id} = $flag_matches[0]->id;
+ }
+ }
+ }
+ elsif ($name) {
+ my @type_matches = grep($name eq $_->name, @$flag_types);
+ @type_matches > 1 && ThrowUserError('flag_type_not_unique',
+ { value => $name });
+ @type_matches || ThrowUserError('object_does_not_exist',
+ { class => 'Bugzilla::FlagType', name => $name });
+ if ($new) {
+ delete $flag->{id};
+ $flag->{type_id} = $type_matches[0]->id;
+ }
+ else {
+ my @flag_matches = grep($name eq $_->type->name, @$current_flags);
+ @flag_matches > 1 && ThrowUserError('flag_not_unique', { value => $name });
+ if (@flag_matches) {
+ $flag->{id} = $flag_matches[0]->id;
+ }
+ else {
+ delete $flag->{id};
+ $flag->{type_id} = $type_matches[0]->id;
+ }
+ }
+ }
+
+ if ($flag->{id}) {
+ push(@old_flags, $flag);
+ }
+ else {
+ push(@new_flags, $flag);
+ }
+ }
+
+ return (\@old_flags, \@new_flags);
+}
+
+sub filter($$;$$) {
+ my ($params, $hash, $types, $prefix) = @_;
my %newhash = %$hash;
foreach my $key (keys %$hash) {
- delete $newhash{$key} if !filter_wants($params, $key);
+ delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix);
}
return \%newhash;
}
-sub filter_wants ($$) {
- my ($params, $field) = @_;
+sub filter_wants($$;$$) {
+ my ($params, $field, $types, $prefix) = @_;
+
+ # Since this is operation is resource intensive, we will cache the results
+ # This assumes that $params->{*_fields} doesn't change between calls
+ my $cache = Bugzilla->request_cache->{filter_wants} ||= {};
+ $field = "${prefix}.${field}" if $prefix;
+
+ if (exists $cache->{$field}) {
+ return $cache->{$field};
+ }
+
+ # Mimic old behavior if no types provided
+ my %field_types = map { $_ => 1 } (ref $types ? @$types : ($types || 'default'));
+
my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] };
my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] };
- if (defined $params->{include_fields}) {
- return 0 if !$include{$field};
+ my %include_types;
+ my %exclude_types;
+
+ # Only return default fields if nothing is specified
+ $include_types{default} = 1 if !%include;
+
+ # Look for any field types requested
+ foreach my $key (keys %include) {
+ next if $key !~ /^_(.*)$/;
+ $include_types{$1} = 1;
+ delete $include{$key};
+ }
+ foreach my $key (keys %exclude) {
+ next if $key !~ /^_(.*)$/;
+ $exclude_types{$1} = 1;
+ delete $exclude{$key};
+ }
+
+ # Explicit inclusion/exclusion
+ return $cache->{$field} = 0 if $exclude{$field};
+ return $cache->{$field} = 1 if $include{$field};
+
+ # If the user has asked to include all or exclude all
+ return $cache->{$field} = 0 if $exclude_types{'all'};
+ return $cache->{$field} = 1 if $include_types{'all'};
+
+ # If the user has not asked for any fields specifically or if the user has asked
+ # for one or more of the field's types (and not excluded them)
+ foreach my $type (keys %field_types) {
+ return $cache->{$field} = 0 if $exclude_types{$type};
+ return $cache->{$field} = 1 if $include_types{$type};
+ }
+
+ my $wants = 0;
+ if ($prefix) {
+ # Include the field if the parent is include (and this one is not excluded)
+ $wants = 1 if $include{$prefix};
}
- if (defined $params->{exclude_fields}) {
- return 0 if $exclude{$field};
+ else {
+ # We want to include this if one of the sub keys is included
+ my $key = $field . '.';
+ my $len = length($key);
+ $wants = 1 if grep { substr($_, 0, $len) eq $key } keys %include;
}
- return 1;
+ return $cache->{$field} = $wants;
}
sub taint_data {
@@ -78,7 +214,7 @@ sub _delete_bad_keys {
# However, we need to validate our argument names in some way.
# We know that all hash keys passed in to the WebService will
# match \w+, so we delete any key that doesn't match that.
- if ($key !~ /^\w+$/) {
+ if ($key !~ /^[\w\.\-]+$/) {
delete $item->{$key};
}
}
@@ -108,6 +244,51 @@ sub validate {
return ($self, $params);
}
+sub translate {
+ my ($params, $mapped) = @_;
+ my %changes;
+ while (my ($key,$value) = each (%$params)) {
+ my $new_field = $mapped->{$key} || $key;
+ $changes{$new_field} = $value;
+ }
+ return \%changes;
+}
+
+sub params_to_objects {
+ my ($params, $class) = @_;
+ my (@objects, @objects_by_ids);
+
+ @objects = map { $class->check($_) }
+ @{ $params->{names} } if $params->{names};
+
+ @objects_by_ids = map { $class->check({ id => $_ }) }
+ @{ $params->{ids} } if $params->{ids};
+
+ push(@objects, @objects_by_ids);
+ my %seen;
+ @objects = grep { !$seen{$_->id}++ } @objects;
+ return \@objects;
+}
+
+sub fix_credentials {
+ my ($params) = @_;
+ # Allow user to pass in login=foo&password=bar as a convenience
+ # even if not calling GET /login. We also do not delete them as
+ # GET /login requires "login" and "password".
+ if (exists $params->{'login'} && exists $params->{'password'}) {
+ $params->{'Bugzilla_login'} = $params->{'login'};
+ $params->{'Bugzilla_password'} = $params->{'password'};
+ }
+ # Allow user to pass token=12345678 as a convenience which becomes
+ # "Bugzilla_token" which is what the auth code looks for.
+ if (exists $params->{'token'}) {
+ $params->{'Bugzilla_token'} = $params->{'token'};
+ }
+
+ # Allow extensions to modify the credential data before login
+ Bugzilla::Hook::process('webservice_fix_credentials', { params => $params });
+}
+
__END__
=head1 NAME
@@ -136,6 +317,13 @@ of WebService methods. Given a hash (the second argument to this subroutine),
this will remove any keys that are I<not> in C<include_fields> and then remove
any keys that I<are> in C<exclude_fields>.
+An optional third option can be passed that prefixes the field name to allow
+filtering of data two or more levels deep.
+
+For example, if you want to filter out the C<id> key/value in components returned
+by Product.get, you would use the value C<component.id> in your C<exclude_fields>
+list.
+
=head2 filter_wants
Returns C<1> if a filter would preserve the specified field when passing
@@ -147,3 +335,31 @@ This helps in the validation of parameters passed into the WebService
methods. Currently it converts listed parameters into an array reference
if the client only passed a single scalar value. It modifies the parameters
hash in place so other parameters should be unaltered.
+
+=head2 params_to_objects
+
+Creates objects of the type passed in as the second parameter, using the
+parameters passed to a WebService method (the first parameter to this function).
+Helps make life simpler for WebService methods that internally create objects
+via both "ids" and "names" fields. Also de-duplicates objects that were loaded
+by both "ids" and "names". Returns an arrayref of objects.
+
+=head2 fix_credentials
+
+Allows for certain parameters related to authentication such as Bugzilla_login,
+Bugzilla_password, and Bugzilla_token to have shorter named equivalents passed in.
+This function converts the shorter versions to their respective internal names.
+
+=head2 extract_flags
+
+Subroutine that takes a list of hashes that are potential flag changes for
+both bugs and attachments. Then breaks the list down into two separate lists
+based on if the change is to add a new flag or to update an existing flag.
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item taint_data
+
+=back
diff --git a/Build.PL b/Build.PL
deleted file mode 100644
index 024a56024..000000000
--- a/Build.PL
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/perl
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-use 5.10.1;
-use strict;
-use warnings;
-
-use FindBin qw($RealBin);
-use lib ($RealBin, "$RealBin/lib");
-
-use Module::Build 0.36_14;
-
-use Bugzilla::Install::Requirements qw(REQUIRED_MODULES OPTIONAL_MODULES);
-use Bugzilla::Constants qw(BUGZILLA_VERSION);
-
-sub requires {
- my $requirements = REQUIRED_MODULES();
- my $hrequires = {};
- foreach my $module (@$requirements) {
- $hrequires->{$module->{module}} = $module->{version};
- }
- return $hrequires;
-};
-
-sub build_requires {
- return requires();
-}
-
-sub recommends {
- my $recommends = OPTIONAL_MODULES();
- my @blacklist = ('Apache-SizeLimit', 'mod_perl'); # Does not compile properly on Travis
- my $hrecommends = {};
- foreach my $module (@$recommends) {
- next if grep($_ eq $module->{package}, @blacklist);
- $hrecommends->{$module->{module}} = $module->{version};
- }
- return $hrecommends;
-}
-
-my $build = Module::Build->new(
- module_name => 'Bugzilla',
- dist_abstract => <<END,
-Bugzilla is a free bug-tracking system that is developed by an active
-community of volunteers. You can install and use it without having to
-pay any license fee.
-END
- dist_version_from => 'Bugzilla/Constants.pm',
- dist_version => BUGZILLA_VERSION,
- requires => requires(),
- recommends => recommends(),
- license => 'Mozilla_2_0',
- create_readme => 0,
- create_makefile_pl => 0
-);
-
-$build->create_build_script;
diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP
deleted file mode 100644
index 69204e63f..000000000
--- a/MANIFEST.SKIP
+++ /dev/null
@@ -1,53 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-#!start included /usr/share/perl5/ExtUtils/MANIFEST.SKIP
-# Avoid version control files.
-\B\.git\b
-\B\.bzr\b
-\B\.bzrignore\b
-\B\.gitignore\b
-\B\.gitrev\b
-\B\.patch\b
-
-# Avoid Makemaker generated and utility files.
-\bMANIFEST\.bak
-\bMakefile$
-\bblib/
-\bMakeMaker-\d
-\bpm_to_blib\.ts$
-\bpm_to_blib$
-\bblibdirs\.ts$ # 6.18 through 6.25 generated this
-
-# Avoid Module::Build generated and utility files.
-\bBuild$
-\b_build/
-
-# Avoid temp and backup files.
-~$
-\.old$
-\#$
-\b\.#
-\.bak$
-\.swp$
-
-#!end included /usr/share/perl5/ExtUtils/MANIFEST.SKIP
-
-# Avoid Module::Build generated and utility files.
-\bBuild$
-\bBuild.bat$
-\b_build
-\bBuild.COM$
-\bBUILD.COM$
-\bbuild.com$
-
-# Avoid archives of this distribution
-\bBugzilla-[\d\.\_]+
-
-# Bugzilla specific avoids
-\bdata\/\b
-\blocalconfig$
diff --git a/README b/README
index 041aebc13..f9464c464 100644
--- a/README
+++ b/README
@@ -90,3 +90,4 @@ Support and installation questions should be directed to the
support-bugzilla@lists.mozilla.org mailing list.
Further support information is at http://www.bugzilla.org/support/
+
diff --git a/attachment.cgi b/attachment.cgi
index d707d68c0..3ffcda821 100755
--- a/attachment.cgi
+++ b/attachment.cgi
@@ -52,6 +52,7 @@ use Bugzilla::Attachment;
use Bugzilla::Attachment::PatchReader;
use Bugzilla::Token;
use Bugzilla::Keyword;
+use Bugzilla::Hook;
use Encode qw(encode find_encoding);
@@ -76,6 +77,12 @@ local our $vars = {};
my $action = $cgi->param('action') || 'view';
my $format = $cgi->param('format') || '';
+# BMO: Don't allow updating of bugs if disabled
+if (Bugzilla->params->{disable_bug_updates} && $cgi->request_method eq 'POST') {
+ ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.');
+}
+
# You must use the appropriate urlbase/sslbase param when doing anything
# but viewing an attachment, or a raw diff.
if ($action ne 'view'
@@ -174,7 +181,7 @@ sub validateID {
{ attach_id => scalar $cgi->param($param) });
# Make sure the attachment exists in the database.
- my $attachment = new Bugzilla::Attachment($attach_id)
+ my $attachment = new Bugzilla::Attachment({ id => $attach_id, cache => 1 })
|| ThrowUserError("invalid_attach_id", { attach_id => $attach_id });
return $attachment if ($dont_validate_access || check_can_access($attachment));
@@ -186,7 +193,7 @@ sub check_can_access {
my $user = Bugzilla->user;
# Make sure the user is authorized to access this attachment's bug.
- Bugzilla::Bug->check($attachment->bug_id);
+ Bugzilla::Bug->check({ id => $attachment->bug_id, cache => 1 });
if ($attachment->isprivate && $user->id != $attachment->attacher->id
&& !$user->is_insider)
{
@@ -381,6 +388,9 @@ sub view {
# Return the appropriate HTTP response headers.
$attachment->datasize || ThrowUserError("attachment_removed");
+ # BMO add a hook for github url redirection
+ Bugzilla::Hook::process('attachment_view', { attachment => $attachment });
+
$filename =~ s/^.*[\/\\]//;
# escape quotes and backslashes in the filename, per RFCs 2045/822
$filename =~ s/\\/\\\\/g; # escape backslashes
@@ -449,10 +459,9 @@ sub diff {
# HTML page.
sub viewall {
# Retrieve and validate parameters
- my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
- my $bugid = $bug->id;
+ my $bug = Bugzilla::Bug->check({ id => scalar $cgi->param('bugid'), cache => 1 });
- my $attachments = Bugzilla::Attachment->get_attachments_by_bug($bugid);
+ my $attachments = Bugzilla::Attachment->get_attachments_by_bug($bug);
# Ignore deleted attachments.
@$attachments = grep { $_->datasize } @$attachments;
@@ -497,7 +506,8 @@ sub enter {
my $flag_types = Bugzilla::FlagType::match({'target_type' => 'attachment',
'product_id' => $bug->product_id,
- 'component_id' => $bug->component_id});
+ 'component_id' => $bug->component_id,
+ 'is_active' => 1});
$vars->{'flag_types'} = $flag_types;
$vars->{'any_flags_requesteeble'} =
grep { $_->is_requestable && $_->is_requesteeble } @$flag_types;
@@ -540,6 +550,13 @@ sub insert {
my $data_fh = $cgi->upload('data');
my $attach_text = $cgi->param('attach_text');
+ if ($attach_text) {
+ # Convert to unix line-endings if pasting a patch
+ if (scalar($cgi->param('ispatch'))) {
+ $attach_text =~ s/[\012\015]{1,2}/\012/g;
+ }
+ }
+
my $attachment = Bugzilla::Attachment->create(
{bug => $bug,
creation_ts => $timestamp,
@@ -559,6 +576,8 @@ sub insert {
$obsolete_attachment->update($timestamp);
}
+ # BMO - allow pre-processing of attachment flags
+ Bugzilla::Hook::process('create_attachment_flags', { bug => $bug });
my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi(
$bug, $attachment, $vars, SKIP_REQUESTEE_ON_ERROR);
$attachment->set_flags($flags, $new_flags);
@@ -618,9 +637,7 @@ sub edit {
my $attachment = validateID();
my $bugattachments =
- Bugzilla::Attachment->get_attachments_by_bug($attachment->bug_id);
- # We only want attachment IDs.
- @$bugattachments = map { $_->id } @$bugattachments;
+ Bugzilla::Attachment->get_attachments_by_bug($attachment->bug);
my $any_flags_requesteeble =
grep { $_->is_requestable && $_->is_requesteeble } @{$attachment->flag_types};
@@ -652,7 +669,7 @@ sub update {
my $attachment = validateID();
my $bug = $attachment->bug;
$attachment->_check_bug;
- my $can_edit = $attachment->validate_can_edit($bug->product_id);
+ my $can_edit = $attachment->validate_can_edit;
if ($can_edit) {
$attachment->set_description(scalar $cgi->param('description'));
@@ -705,11 +722,35 @@ sub update {
extra_data => $attachment->id });
}
+ my ($flags, $new_flags) =
+ Bugzilla::Flag->extract_flags_from_cgi($bug, $attachment, $vars);
+
if ($can_edit) {
- my ($flags, $new_flags) =
- Bugzilla::Flag->extract_flags_from_cgi($bug, $attachment, $vars);
$attachment->set_flags($flags, $new_flags);
}
+ # Requestees can set flags targetted to them, even if they cannot
+ # edit the attachment. Flag setters can edit their own flags too.
+ elsif (scalar @$flags) {
+ my @flag_ids = map { $_->{id} } @$flags;
+ my $flag_objs = Bugzilla::Flag->new_from_list(\@flag_ids);
+ my %flag_list = map { $_->id => $_ } @$flag_objs;
+
+ my @editable_flags;
+ foreach my $flag (@$flags) {
+ my $flag_obj = $flag_list{$flag->{id}};
+ if ($flag_obj->setter_id == $user->id
+ || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id))
+ {
+ push(@editable_flags, $flag);
+ }
+ }
+
+ if (scalar @editable_flags) {
+ $attachment->set_flags(\@editable_flags, []);
+ # Flag changes must be committed.
+ $can_edit = 1;
+ }
+ }
# Figure out when the changes were made.
my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
@@ -779,7 +820,6 @@ sub delete_attachment {
# The token is valid. Delete the content of the attachment.
my $msg;
$vars->{'attachment'} = $attachment;
- $vars->{'date'} = $date;
$vars->{'reason'} = clean_text($cgi->param('reason') || '');
$template->process("attachment/delete_reason.txt.tmpl", $vars, \$msg)
diff --git a/buglist.cgi b/buglist.cgi
index a7eb98df5..f2cc0a53c 100755
--- a/buglist.cgi
+++ b/buglist.cgi
@@ -56,18 +56,19 @@ my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh;
my $template = Bugzilla->template;
my $vars = {};
-my $buffer = $cgi->query_string();
# We have to check the login here to get the correct footer if an error is
# thrown and to prevent a logged out user to use QuickSearch if 'requirelogin'
# is turned 'on'.
my $user = Bugzilla->login();
+$cgi->redirect_search_url();
+
+my $buffer = $cgi->query_string();
if (length($buffer) == 0) {
ThrowUserError("buglist_parameters_required");
}
-$cgi->redirect_search_url();
# Determine whether this is a quicksearch query.
my $searchstring = $cgi->param('quicksearch');
@@ -315,23 +316,6 @@ sub GetGroups {
return [values %legal_groups];
}
-sub _close_standby_message {
- my ($contenttype, $disposition, $serverpush) = @_;
- my $cgi = Bugzilla->cgi;
-
- # Close the "please wait" page, then open the buglist page
- if ($serverpush) {
- print $cgi->multipart_end();
- print $cgi->multipart_start(-type => $contenttype,
- -content_disposition => $disposition);
- }
- else {
- print $cgi->header(-type => $contenttype,
- -content_disposition => $disposition);
- }
-}
-
-
################################################################################
# Command Execution
################################################################################
@@ -359,17 +343,10 @@ $params ||= new Bugzilla::CGI($cgi);
# if available. We have to do this now, even though we return HTTP headers
# at the end, because the fact that there is a remembered query gets
# forgotten in the process of retrieving it.
-my @time = localtime(time());
-my $date = sprintf "%04d-%02d-%02d", 1900+$time[5],$time[4]+1,$time[3];
-my $filename = "bugs-$date.$format->{extension}";
+my $disp_prefix = "bugs";
if ($cmdtype eq "dorem" && $remaction =~ /^run/) {
- $filename = $cgi->param('namedcmd') . "-$date.$format->{extension}";
- # Remove white-space from the filename so the user cannot tamper
- # with the HTTP headers.
- $filename =~ s/\s/_/g;
+ $disp_prefix = $cgi->param('namedcmd');
}
-$filename =~ s/\\/\\\\/g; # escape backslashes
-$filename =~ s/"/\\"/g; # escape quotes
# Take appropriate action based on user's request.
if ($cmdtype eq "dorem") {
@@ -382,7 +359,7 @@ if ($cmdtype eq "dorem") {
# so that it can be modified easily.
$vars->{'searchname'} = $cgi->param('namedcmd');
if (!$cgi->param('sharer_id') ||
- $cgi->param('sharer_id') == Bugzilla->user->id) {
+ $cgi->param('sharer_id') == $user->id) {
$vars->{'searchtype'} = "saved";
$vars->{'search_id'} = $query_id;
}
@@ -442,6 +419,7 @@ if ($cmdtype eq "dorem") {
$dbh->do('DELETE FROM namedquery_group_map
WHERE namedquery_id = ?',
undef, $query_id);
+ Bugzilla->memcached->clear({ table => 'namedqueries', id => $query_id });
}
# Now reset the cached queries
@@ -490,14 +468,16 @@ elsif (($cmdtype eq "doit") && defined $cgi->param('remtype')) {
or ThrowUserError('no_tag_to_edit', {action => $action});
my @buglist;
- # Validate all bug IDs before editing tags in any of them.
- foreach my $bug_id (split(/[\s,]+/, $cgi->param('bug_ids'))) {
- next unless $bug_id;
- push(@buglist, Bugzilla::Bug->check($bug_id));
- }
+ if ($cgi->param('bug_ids')) {
+ # Validate all bug IDs before editing tags in any of them.
+ foreach my $bug_id (split(/[\s,]+/, $cgi->param('bug_ids'))) {
+ next unless $bug_id;
+ push(@buglist, Bugzilla::Bug->check($bug_id));
+ }
- foreach my $bug (@buglist) {
- $bug->$method($query_name);
+ foreach my $bug (@buglist) {
+ $bug->$method($query_name);
+ }
}
$vars->{'message'} = 'tag_updated';
@@ -697,69 +677,39 @@ if (!$order || $order =~ /^reuse/i) {
}
}
+my @order_columns;
if ($order) {
# Convert the value of the "order" form field into a list of columns
# by which to sort the results.
ORDER: for ($order) {
/^Bug Number$/ && do {
- $order = "bug_id";
+ @order_columns = ("bug_id");
last ORDER;
};
/^Importance$/ && do {
- $order = "priority,bug_severity";
+ @order_columns = ("priority", "bug_severity");
last ORDER;
};
/^Assignee$/ && do {
- $order = "assigned_to,bug_status,priority,bug_id";
+ @order_columns = ("assigned_to", "bug_status", "priority",
+ "bug_id");
last ORDER;
};
/^Last Changed$/ && do {
- $order = "changeddate,bug_status,priority,assigned_to,bug_id";
+ @order_columns = ("changeddate", "bug_status", "priority",
+ "assigned_to", "bug_id");
last ORDER;
};
do {
- my (@order, @invalid_fragments);
-
- # A custom list of columns. Make sure each column is valid.
- foreach my $fragment (split(/,/, $order)) {
- $fragment = trim($fragment);
- next unless $fragment;
- my ($column_name, $direction) = split_order_term($fragment);
- $column_name = translate_old_column($column_name);
-
- # Special handlings for certain columns
- next if $column_name eq 'relevance' && !$fulltext;
-
- if (exists $columns->{$column_name}) {
- $direction = " $direction" if $direction;
- push(@order, "$column_name$direction");
- }
- else {
- push(@invalid_fragments, $fragment);
- }
- }
- if (scalar @invalid_fragments) {
- $vars->{'message'} = 'invalid_column_name';
- $vars->{'invalid_fragments'} = \@invalid_fragments;
- }
-
- $order = join(",", @order);
- # Now that we have checked that all columns in the order are valid,
- # detaint the order string.
- trick_taint($order) if $order;
+ # A custom list of columns. Bugzilla::Search will validate items.
+ @order_columns = split(/\s*,\s*/, $order);
};
}
}
-if (!$order) {
+if (!scalar @order_columns) {
# DEFAULT
- $order = "bug_status,priority,assigned_to,bug_id";
-}
-
-my @orderstrings = split(/,\s*/, $order);
-
-if ($fulltext and grep { /^relevance/ } @orderstrings) {
- $vars->{'message'} = 'buglist_sorted_by_relevance'
+ @order_columns = ("bug_status", "priority", "assigned_to", "bug_id");
}
# In the HTML interface, by default, we limit the returned results,
@@ -773,10 +723,19 @@ if ($format->{'extension'} eq 'html' && !defined $params->param('limit')) {
# Generate the basic SQL query that will be used to generate the bug list.
my $search = new Bugzilla::Search('fields' => \@selectcolumns,
'params' => scalar $params->Vars,
- 'order' => \@orderstrings,
+ 'order' => \@order_columns,
'sharer' => $sharer_id);
-my $query = $search->sql;
-$vars->{'search_description'} = $search->search_description;
+
+$order = join(',', $search->order);
+
+if (scalar @{$search->invalid_order_columns}) {
+ $vars->{'message'} = 'invalid_column_name';
+ $vars->{'invalid_fragments'} = $search->invalid_order_columns;
+}
+
+if ($fulltext and grep { /^relevance/ } $search->order) {
+ $vars->{'message'} = 'buglist_sorted_by_relevance'
+}
# We don't want saved searches and other buglist things to save
# our default limit.
@@ -786,21 +745,6 @@ $params->delete('limit') if $vars->{'default_limited'};
# Query Execution
################################################################################
-if ($cgi->param('debug')
- && Bugzilla->params->{debug_group}
- && $user->in_group(Bugzilla->params->{debug_group})
-) {
- $vars->{'debug'} = 1;
- $vars->{'query'} = $query;
- # Explains are limited to admins because you could use them to figure
- # out how many hidden bugs are in a particular product (by doing
- # searches and looking at the number of rows the explain says it's
- # examining).
- if (Bugzilla->user->in_group('admin')) {
- $vars->{'query_explain'} = $dbh->bz_explain($query);
- }
-}
-
# Time to use server push to display an interim message to the user until
# the query completes and we can display the bug list.
if ($serverpush) {
@@ -833,9 +777,28 @@ $::SIG{TERM} = 'DEFAULT';
$::SIG{PIPE} = 'DEFAULT';
# Execute the query.
-my $buglist_sth = $dbh->prepare($query);
-$buglist_sth->execute();
+my ($data, $extra_data) = $search->data;
+$vars->{'search_description'} = $search->search_description;
+if ($cgi->param('debug')
+ && Bugzilla->params->{debug_group}
+ && $user->in_group(Bugzilla->params->{debug_group})
+) {
+ $vars->{'debug'} = 1;
+ $vars->{'queries'} = $extra_data;
+ my $query_time = 0;
+ $query_time += $_->{'time'} foreach @$extra_data;
+ $vars->{'query_time'} = $query_time;
+ # Explains are limited to admins because you could use them to figure
+ # out how many hidden bugs are in a particular product (by doing
+ # searches and looking at the number of rows the explain says it's
+ # examining).
+ if ($user->in_group('admin')) {
+ foreach my $query (@$extra_data) {
+ $query->{explain} = $dbh->bz_explain($query->{sql});
+ }
+ }
+}
################################################################################
# Results Retrieval
@@ -867,14 +830,14 @@ my @bugidlist;
my @bugs; # the list of records
-while (my @row = $buglist_sth->fetchrow_array()) {
+foreach my $row (@$data) {
my $bug = {}; # a record
# Slurp the row of data into the record.
# The second from last column in the record is the number of groups
# to which the bug is restricted.
foreach my $column (@selectcolumns) {
- $bug->{$column} = shift @row;
+ $bug->{$column} = shift @$row;
}
# Process certain values further (i.e. date format conversion).
@@ -979,10 +942,10 @@ $vars->{'order'} = $order;
$vars->{'caneditbugs'} = 1;
$vars->{'time_info'} = $time_info;
-if (!Bugzilla->user->in_group('editbugs')) {
+if (!$user->in_group('editbugs')) {
foreach my $product (keys %$bugproducts) {
my $prod = new Bugzilla::Product({name => $product});
- if (!Bugzilla->user->in_group('editbugs', $prod->id)) {
+ if (!$user->in_group('editbugs', $prod->id)) {
$vars->{'caneditbugs'} = 0;
last;
}
@@ -990,7 +953,7 @@ if (!Bugzilla->user->in_group('editbugs')) {
}
my @bugowners = keys %$bugowners;
-if (scalar(@bugowners) > 1 && Bugzilla->user->in_group('editbugs')) {
+if (scalar(@bugowners) > 1 && $user->in_group('editbugs')) {
my $suffix = Bugzilla->params->{'emailsuffix'};
map(s/$/$suffix/, @bugowners) if $suffix;
my $bugowners = join(",", @bugowners);
@@ -1001,7 +964,10 @@ if (scalar(@bugowners) > 1 && Bugzilla->user->in_group('editbugs')) {
# the list more compact.
$vars->{'splitheader'} = $cgi->cookie('SPLITHEADER') ? 1 : 0;
-$vars->{'quip'} = GetQuip();
+if ($user->settings->{'display_quips'}->{'value'} eq 'on') {
+ $vars->{'quip'} = GetQuip();
+}
+
$vars->{'currenttime'} = localtime(time());
# See if there's only one product in all the results (or only one product
@@ -1019,14 +985,13 @@ elsif (my @product_input = $cgi->param('product')) {
}
# We only want the template to use it if the user can actually
# enter bugs against it.
-if ($one_product && Bugzilla->user->can_enter_product($one_product)) {
+if ($one_product && $user->can_enter_product($one_product)) {
$vars->{'one_product'} = $one_product;
}
# The following variables are used when the user is making changes to multiple bugs.
if ($dotweak && scalar @bugs) {
if (!$vars->{'caneditbugs'}) {
- _close_standby_message('text/html', 'inline', $serverpush);
ThrowUserError('auth_failure', {group => 'editbugs',
action => 'modify',
object => 'multiple_bugs'});
@@ -1038,7 +1003,7 @@ if ($dotweak && scalar @bugs) {
$vars->{'token'} = issue_session_token('buglist_mass_change');
Bugzilla->switch_to_shadow_db();
- $vars->{'products'} = Bugzilla->user->get_enterable_products;
+ $vars->{'products'} = $user->get_enterable_products;
$vars->{'platforms'} = get_legal_field_values('rep_platform');
$vars->{'op_sys'} = get_legal_field_values('op_sys');
$vars->{'priorities'} = get_legal_field_values('priority');
@@ -1133,10 +1098,7 @@ if ($format->{'extension'} eq "csv") {
$vars->{'human'} = $cgi->param('human');
}
-# Suggest a name for the bug list if the user wants to save it as a file.
-$disposition .= "; filename=\"$filename\"";
-
-_close_standby_message($contenttype, $disposition, $serverpush);
+$cgi->close_standby_message($contenttype, $disposition);
################################################################################
# Content Generation
diff --git a/bzr-update.sh b/bzr-update.sh
new file mode 100644
index 000000000..e1d88e5d7
--- /dev/null
+++ b/bzr-update.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+HOST=`hostname -s`
+TAG="current-staging"
+[ "$HOST" == "mradm02" -o "$HOST" == "ip-admin02" ] && TAG="current-production"
+echo "+ bzr pull --overwrite -rtag:$TAG"
+output=`bzr pull --overwrite -rtag:$TAG 2>&1`
+echo "$output"
+echo "$output" | grep "Now on revision" | sed -e 's/Now on revision //' -e 's/\.$//' | xargs -i{} echo bzr pull --overwrite -r{} \# `date` >> `dirname $0`/cvs-update.log
+contrib/fixperms.pl
diff --git a/chart.cgi b/chart.cgi
index e7a0f5e8b..89fefbc3f 100755
--- a/chart.cgi
+++ b/chart.cgi
@@ -274,7 +274,8 @@ sub assertCanCreate {
# Check permission for frequency
my $min_freq = 7;
- if ($cgi->param('frequency') < $min_freq && !$user->in_group("admin")) {
+ # Upstreaming: denied, as this min_freq feature is going away.
+ if ($cgi->param('frequency') < $min_freq && !$user->in_group("bz_canusewhines")) {
ThrowUserError("illegal_frequency", { 'minimum' => $min_freq });
}
}
diff --git a/checksetup.pl b/checksetup.pl
index 6d9230dd9..501138c6d 100755
--- a/checksetup.pl
+++ b/checksetup.pl
@@ -144,6 +144,8 @@ Bugzilla::DB::bz_create_database() if $lc_hash->{'db_check'};
# now get a handle to the database:
my $dbh = Bugzilla->dbh;
+# Clear all keys from Memcached to ensure we see the correct schema.
+Bugzilla->memcached->clear_all();
# Create the tables, and do any database-specific schema changes.
$dbh->bz_setup_database();
# Populate the tables that hold the values for the <select> fields.
@@ -236,6 +238,9 @@ Bugzilla::Hook::process('install_before_final_checks', { silent => $silent });
# Final checks
###########################################################################
+# Clear all keys from Memcached
+Bugzilla->memcached->clear_all();
+
# Check if the default parameter for urlbase is still set, and if so, give
# notification that they should go and visit editparams.cgi
if (Bugzilla->params->{'urlbase'} eq '') {
diff --git a/clean-bug-user-last-visit.pl b/clean-bug-user-last-visit.pl
new file mode 100755
index 000000000..9884b7c48
--- /dev/null
+++ b/clean-bug-user-last-visit.pl
@@ -0,0 +1,38 @@
+#!/usr/bin/perl -wT
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+=head1 NAME
+
+clean-bug-user-last-visit.pl
+
+=head1 DESCRIPTION
+
+This utility script cleans out entries from the bug_user_last_visit table that
+are older than (a configurable) number of days.
+
+It takes no arguments and produces no output except in the case of errors.
+
+=cut
+
+use 5.10.1;
+use strict;
+use warnings;
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my $dbh = Bugzilla->dbh;
+my $sql = 'DELETE FROM bug_user_last_visit WHERE last_visit_ts < '
+ . $dbh->sql_date_math('NOW()',
+ '-',
+ Bugzilla->params->{last_visit_keep_days},
+ 'DAY');
+$dbh->do($sql);
diff --git a/collectstats.pl b/collectstats.pl
index 1487e5a72..c5db30b5f 100755
--- a/collectstats.pl
+++ b/collectstats.pl
@@ -504,8 +504,7 @@ sub CollectSeriesData {
'fields' => ["bug_id"],
'allow_unlimited' => 1,
'user' => $user);
- my $sql = $search->sql;
- $data = $shadow_dbh->selectall_arrayref($sql);
+ $data = $search->data;
};
if (!$@) {
diff --git a/config.cgi b/config.cgi
index 2c82fdc59..fcc5d82ed 100755
--- a/config.cgi
+++ b/config.cgi
@@ -44,15 +44,16 @@ use Digest::MD5 qw(md5_base64);
my $user = Bugzilla->login(LOGIN_OPTIONAL);
my $cgi = Bugzilla->cgi;
+# Get data from the shadow DB as they don't change very often.
+Bugzilla->switch_to_shadow_db;
+
# If the 'requirelogin' parameter is on and the user is not
# authenticated, return empty fields.
if (Bugzilla->params->{'requirelogin'} && !$user->id) {
display_data();
+ exit;
}
-# Get data from the shadow DB as they don't change very often.
-Bugzilla->switch_to_shadow_db;
-
# Pass a bunch of Bugzilla configuration to the templates.
my $vars = {};
$vars->{'priority'} = get_legal_field_values('priority');
@@ -82,7 +83,7 @@ if ($cgi->param('product')) {
}
# We set the 2nd argument to 1 to also preload flag types.
-Bugzilla::Product::preload($vars->{'products'}, 1);
+Bugzilla::Product::preload($vars->{'products'}, 1, { is_active => 1 });
# Allow consumers to specify whether or not they want flag data.
if (defined $cgi->param('flags')) {
@@ -136,31 +137,13 @@ sub display_data {
utf8::encode($digest_data) if utf8::is_utf8($digest_data);
my $digest = md5_base64($digest_data);
- # ETag support.
- my $if_none_match = $cgi->http('If-None-Match') || "";
- my $found304;
- my @if_none = split(/[\s,]+/, $if_none_match);
- foreach my $if_none (@if_none) {
- # remove quotes from begin and end of the string
- $if_none =~ s/^\"//g;
- $if_none =~ s/\"$//g;
- if ($if_none eq $digest or $if_none eq '*') {
- # leave the loop after the first match
- $found304 = $if_none;
- last;
- }
- }
-
- if ($found304) {
- print $cgi->header(-type => 'text/html',
- -ETag => $found304,
+ if ($cgi->check_etag($digest)) {
+ print $cgi->header(-ETag => $digest,
-status => '304 Not Modified');
+ exit;
}
- else {
- # Return HTTP headers.
- print $cgi->header (-ETag => $digest,
- -type => $format->{'ctype'});
- print $output;
- }
- exit;
+
+ print $cgi->header (-ETag => $digest,
+ -type => $format->{'ctype'});
+ print $output;
}
diff --git a/contrib/addcustomfield.pl b/contrib/addcustomfield.pl
new file mode 100755
index 000000000..4fa6589e4
--- /dev/null
+++ b/contrib/addcustomfield.pl
@@ -0,0 +1,63 @@
+#!/usr/bin/perl -wT
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# Contributor(s): Frédéric Buclin <LpSolit@gmail.com>
+# David Miller <justdave@mozilla.com>
+
+use strict;
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Field;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my %types = (
+ 'freetext' => FIELD_TYPE_FREETEXT,
+ 'single_select' => FIELD_TYPE_SINGLE_SELECT,
+ 'multi_select' => FIELD_TYPE_MULTI_SELECT,
+ 'textarea' => FIELD_TYPE_TEXTAREA,
+ 'datetime' => FIELD_TYPE_DATETIME,
+ 'date' => FIELD_TYPE_DATE,
+ 'bug_id' => FIELD_TYPE_BUG_ID,
+ 'bug_urls' => FIELD_TYPE_BUG_URLS,
+ 'keywords' => FIELD_TYPE_KEYWORDS,
+);
+
+my $syntax =
+ "syntax: addcustomfield.pl <field name> [field type]\n\n" .
+ "valid field types:\n " . join("\n ", sort keys %types) . "\n\n" .
+ "the default field type is single_select\n";
+
+my $name = shift || die $syntax;
+my $type = lc(shift || 'single_select');
+exists $types{$type} || die "Invalid field type '$type'.\n\n$syntax";
+$type = $types{$type};
+
+Bugzilla::Field->create({
+ name => $name,
+ description => 'Please give me a description!',
+ type => $type,
+ mailhead => 0,
+ enter_bug => 0,
+ obsolete => 1,
+ custom => 1,
+ buglist => 1,
+});
+print "Done!\n";
+
+my $urlbase = Bugzilla->params->{urlbase};
+print "Please visit ${urlbase}editfields.cgi?action=edit&name=$name to finish setting up this field.\n";
diff --git a/contrib/clear-memcached.pl b/contrib/clear-memcached.pl
new file mode 100755
index 000000000..01202ce7c
--- /dev/null
+++ b/contrib/clear-memcached.pl
@@ -0,0 +1,27 @@
+#!/usr/bin/perl
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+
+use FindBin qw($Bin);
+use lib "$Bin/..";
+use lib "$Bin/../lib";
+
+use Bugzilla;
+use Bugzilla::Constants;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (Bugzilla->memcached->{memcached}) {
+ Bugzilla->memcached->clear_all();
+ print "memcached cleared\n";
+} else {
+ print "memcached is not enabled\n";
+}
diff --git a/contrib/clear-templates.pl b/contrib/clear-templates.pl
new file mode 100755
index 000000000..8b0864d46
--- /dev/null
+++ b/contrib/clear-templates.pl
@@ -0,0 +1,40 @@
+#!/usr/bin/perl -w
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Install::Filesystem qw(fix_dir_permissions);
+use File::Path qw(mkpath rmtree);
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+$| = 1;
+
+# rename the current directory and create a new empty one
+# the templates will lazy-compile on demand
+
+my $path = bz_locations()->{'template_cache'};
+my $delete_path = "$path.deleteme";
+
+print "clearing $path\n";
+
+rmtree("$delete_path") if -e "$delete_path";
+rename($path, $delete_path)
+ or die "renaming '$path' to '$delete_path' failed: $!\n";
+
+mkpath($path)
+ or die "creating '$path' failed: $!\n";
+fix_dir_permissions($path);
+
+# delete the temp directory (it's ok if this fails)
+
+rmtree("$delete_path");
diff --git a/contrib/convert-workflow.pl b/contrib/convert-workflow.pl
index 3dce21f12..e4baf18d6 100755
--- a/contrib/convert-workflow.pl
+++ b/contrib/convert-workflow.pl
@@ -164,6 +164,7 @@ if ($enable_unconfirmed) {
$dbh->do('UPDATE products SET allows_unconfirmed = 1');
}
$dbh->bz_commit_transaction();
+Bugzilla->memcached->clear_all();
print <<END;
Done. There are some things you may want to fix, now:
diff --git a/contrib/fix_comment_text.pl b/contrib/fix_comment_text.pl
new file mode 100755
index 000000000..f17bbc3d4
--- /dev/null
+++ b/contrib/fix_comment_text.pl
@@ -0,0 +1,75 @@
+#!/usr/bin/perl
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Initial Developer of the Original Code is Mozilla Foundation
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+#===============================================================================
+#
+# FILE: fix_comment_text.pl
+#
+# USAGE: ./fix_comment_text.pl <comment_id>
+#
+# DESCRIPTION: Updates a comment in Bugzilla with the text after __DATA__
+#
+# OPTIONS: <comment_id> - The comment id from longdescs with the comment
+# to be replaced.
+# REQUIREMENTS: ---
+# BUGS: ---
+# NOTES: ---
+# AUTHOR: David Lawrence (:dkl), dkl@mozilla.com
+# COMPANY: Mozilla Foundation
+# VERSION: 1.0
+# CREATED: 06/20/2011 03:40:22 PM
+# REVISION: ---
+#===============================================================================
+
+use strict;
+use warnings;
+
+use lib ".";
+
+use Bugzilla;
+use Bugzilla::Util qw(detaint_natural);
+
+my $comment_id = shift;
+
+if (!detaint_natural($comment_id)) {
+ print "Error: invalid comment id or comment id not provided.\n" .
+ "Usage: ./fix_comment_text.pl <comment_id>\n";
+ exit(1);
+}
+
+my $dbh = Bugzilla->dbh;
+
+my $comment = join("", <DATA>);
+
+if ($comment =~ /ENTER NEW COMMENT TEXT HERE/) {
+ print "Please enter the new comment text in the script " .
+ "after the __DATA__ marker.\n";
+ exit(1);
+}
+
+$dbh->bz_start_transaction;
+
+Bugzilla->dbh->do(
+ "UPDATE longdescs SET thetext = ? WHERE comment_id = ?",
+ undef, $comment, $comment_id);
+
+$dbh->bz_commit_transaction;
+
+exit(0);
+
+__DATA__
+ENTER NEW COMMENT TEXT HERE BELOW THE __DATA__ MARKER!
diff --git a/contrib/merge-users.pl b/contrib/merge-users.pl
index ee6ec8628..aeb8156ad 100755
--- a/contrib/merge-users.pl
+++ b/contrib/merge-users.pl
@@ -50,6 +50,7 @@ use Bugzilla;
use Bugzilla::Constants;
use Bugzilla::Util;
use Bugzilla::User;
+use Bugzilla::Hook;
use Getopt::Long;
use Pod::Usage;
@@ -156,10 +157,32 @@ foreach my $table (qw(logincookies tokens profiles)) {
# Start the transaction
$dbh->bz_start_transaction();
+# BMO - pre-work hook
+Bugzilla::Hook::process('merge_users_before', { old_id => $old_id, new_id => $new_id });
+
# Delete old records from logincookies and tokens tables.
$dbh->do('DELETE FROM logincookies WHERE userid = ?', undef, $old_id);
$dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $old_id);
+# Special care needs to be done with bug_user_last_visit table as the
+# source user and destination user may have visited the same bug id at one time.
+# In this case we remove the one with the oldest timestamp.
+my $dupe_ids = $dbh->selectcol_arrayref("
+ SELECT earlier.id
+ FROM bug_user_last_visit as earlier
+ INNER JOIN bug_user_last_visit as later
+ ON (earlier.user_id != later.user_id
+ AND earlier.last_visit_ts < later.last_visit_ts
+ AND earlier.bug_id = later.bug_id)
+ WHERE (earlier.user_id = ? OR earlier.user_id = ?)
+ AND (later.user_id = ? OR later.user_id = ?)",
+ undef, $old_id, $new_id, $old_id, $new_id);
+
+if (@$dupe_ids) {
+ $dbh->do("DELETE FROM bug_user_last_visit WHERE " .
+ $dbh->sql_in('id', $dupe_ids));
+}
+
# Migrate records from old user to new user.
foreach my $table (keys %changes) {
foreach my $column_list (@{ $changes{$table} }) {
@@ -234,7 +257,15 @@ $dbh->do('DELETE FROM profiles WHERE userid = ?', undef, $old_id);
my $user = new Bugzilla::User($new_id);
$user->derive_regexp_groups();
+# BMO - post-work hook
+Bugzilla::Hook::process('merge_users_after', { old_id => $old_id, new_id => $new_id });
+
# Commit the transaction
$dbh->bz_commit_transaction();
+# It's complex to determine which items now need to be flushed from memcached.
+# As user merge is expected to be a rare event, we just flush the entire cache
+# when users are merged.
+Bugzilla->memcached->clear_all();
+
print "Done.\n";
diff --git a/contrib/moco-ldap-check.pl b/contrib/moco-ldap-check.pl
new file mode 100755
index 000000000..7a3a6ca8c
--- /dev/null
+++ b/contrib/moco-ldap-check.pl
@@ -0,0 +1,542 @@
+#!/usr/bin/perl
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+
+use FindBin qw($Bin);
+use lib "$Bin/..";
+use lib "$Bin/../lib";
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Group;
+use Bugzilla::Mailer;
+use Data::Dumper;
+use File::Slurp;
+use Getopt::Long;
+use Net::LDAP;
+use Safe;
+
+#
+
+use constant BUGZILLA_IGNORE => <<'EOF';
+ infra+bot@mozilla.com # Mozilla Infrastructure Bot
+ qa-auto@mozilla.com # QA Desktop Automation
+ qualys@mozilla.com # Qualys Security Scanner
+ recruiting@mozilla.com # Recruiting
+ release@mozilla.com # Mozilla RelEng Bot
+ sumo-dev@mozilla.com # SUMOdev [:sumodev]
+ airmozilla@mozilla.com # Air Mozilla
+ ux-review@mozilla.com
+ release-mgmt@mozilla.com
+ reps@mozilla.com
+ moz_bug_r_a4@mozilla.com # Security contractor
+ nightwatch@mozilla.com # Security distribution list for whines
+EOF
+
+use constant LDAP_IGNORE => <<'EOF';
+ airmozilla@mozilla.com # Air Mozilla
+EOF
+
+# REPORT_SENDER has to be a valid @mozilla.com LDAP account
+use constant REPORT_SENDER => 'bjones@mozilla.com';
+
+use constant BMO_RECIPIENTS => qw(
+ glob@mozilla.com
+ dkl@mozilla.com
+);
+
+use constant SUPPORT_RECIPIENTS => qw(
+ desktop@mozilla.com
+);
+
+#
+
+my ($ldap_host, $ldap_user, $ldap_pass, $debug, $no_update);
+GetOptions('h=s' => \$ldap_host,
+ 'u=s' => \$ldap_user,
+ 'p=s' => \$ldap_pass,
+ 'd' => \$debug,
+ 'n' => \$no_update);
+die "syntax: -h ldap_host -u ldap_user -p ldap_pass\n"
+ unless $ldap_host && $ldap_user && $ldap_pass;
+
+my $data_dir = bz_locations()->{'datadir'} . '/moco-ldap-check';
+mkdir($data_dir) unless -d $data_dir;
+
+if ($ldap_user !~ /,/) {
+ $ldap_user = "mail=$ldap_user,o=com,dc=mozilla";
+}
+
+#
+# group members
+#
+
+my @bugzilla_ignore;
+foreach my $line (split(/\n/, BUGZILLA_IGNORE)) {
+ $line =~ s/^([^#]+)#.*$/$1/;
+ $line =~ s/(^\s+|\s+$)//g;
+ push @bugzilla_ignore, clean_email($line);
+}
+
+my @bugzilla_moco;
+if ($no_update && -s "$data_dir/bugzilla_moco.last") {
+ $debug && print "Using cached user list from Bugzilla...\n";
+ my $ra = deserialise("$data_dir/bugzilla_moco.last");
+ @bugzilla_moco = @$ra;
+} else {
+ $debug && print "Getting user list from Bugzilla...\n";
+
+ my $group = Bugzilla::Group->new({ name => 'mozilla-corporation' })
+ or die "Failed to find group mozilla-corporation\n";
+
+ foreach my $user (@{ $group->members_non_inherited }) {
+ next unless $user->is_enabled;
+ my $mail = clean_email($user->login);
+ my $name = trim($user->name);
+ $name =~ s/\s+/ /g;
+ next if grep { $mail eq $_ } @bugzilla_ignore;
+ push @bugzilla_moco, {
+ mail => $user->login,
+ canon => $mail,
+ name => $name,
+ };
+ }
+
+ @bugzilla_moco = sort { $a->{mail} cmp $b->{mail} } @bugzilla_moco;
+ serialise("$data_dir/bugzilla_moco.last", \@bugzilla_moco);
+}
+
+#
+# build list of current mo-co bugmail accounts
+#
+
+my @ldap_ignore;
+foreach my $line (split(/\n/, LDAP_IGNORE)) {
+ $line =~ s/^([^#]+)#.*$/$1/;
+ $line =~ s/(^\s+|\s+$)//g;
+ push @ldap_ignore, canon_email($line);
+}
+
+my %ldap;
+if ($no_update && -s "$data_dir/ldap.last") {
+ $debug && print "Using cached user list from LDAP...\n";
+ my $rh = deserialise("$data_dir/ldap.last");
+ %ldap = %$rh;
+} else {
+ $debug && print "Logging into LDAP as $ldap_user...\n";
+ my $ldap = Net::LDAP->new($ldap_host,
+ scheme => 'ldaps', onerror => 'die') or die "$@";
+ $ldap->bind($ldap_user, password => $ldap_pass);
+ foreach my $ldap_base ('o=com,dc=mozilla', 'o=org,dc=mozilla') {
+ $debug && print "Getting user list from LDAP $ldap_base...\n";
+ my $result = $ldap->search(
+ base => $ldap_base,
+ scope => 'sub',
+ filter => '(mail=*)',
+ attrs => ['mail', 'bugzillaEmail', 'emailAlias', 'cn', 'employeeType'],
+ );
+ foreach my $entry ($result->entries) {
+ my ($name, $bugMail, $mail, $type) =
+ map { $entry->get_value($_) || '' }
+ qw(cn bugzillaEmail mail employeeType);
+ next if $type eq 'DISABLED';
+ $mail = lc $mail;
+ next if grep { $_ eq canon_email($mail) } @ldap_ignore;
+ $bugMail = '' if $bugMail !~ /\@/;
+ $bugMail =~ s/(^\s+|\s+$)//g;
+ if ($bugMail =~ / /) {
+ $bugMail = (grep { /\@/ } split / /, $bugMail)[0];
+ }
+ $name =~ s/\s+/ /g;
+ $ldap{$mail}{name} = trim($name);
+ $ldap{$mail}{bugmail} = $bugMail;
+ $ldap{$mail}{bugmail_canon} = canon_email($bugMail);
+ $ldap{$mail}{aliases} = [];
+ foreach my $alias (
+ @{$entry->get_value('emailAlias', asref => 1) || []}
+ ) {
+ push @{$ldap{$mail}{aliases}}, canon_email($alias);
+ }
+ }
+ $debug && printf "Found %s entries\n", scalar($result->entries);
+ }
+ serialise("$data_dir/ldap.last", \%ldap);
+}
+
+#
+# validate all bugmail entries from the phonebook
+#
+
+my %bugzilla_login;
+if ($no_update && -s "$data_dir/bugzilla_login.last") {
+ $debug && print "Using cached bugzilla checks...\n";
+ my $rh = deserialise("$data_dir/bugzilla_login.last");
+ %bugzilla_login = %$rh;
+} else {
+ my %logins;
+ foreach my $mail (keys %ldap) {
+ $logins{$mail} = 1;
+ $logins{$ldap{$mail}{bugmail}} = 1 if $ldap{$mail}{bugmail};
+ }
+ my @logins = sort keys %logins;
+ $debug && print "Checking " . scalar(@logins) . " bugmail accounts...\n";
+
+ foreach my $login (@logins) {
+ if (Bugzilla::User->new({ name => $login })) {
+ $bugzilla_login{$login} = 1;
+ }
+ }
+ serialise("$data_dir/bugzilla_login.last", \%bugzilla_login);
+}
+
+#
+# load previous ldap list
+#
+
+my %ldap_old;
+{
+ my $rh = deserialise("$data_dir/ldap.data");
+ %ldap_old = %$rh if $rh;
+}
+
+#
+# save current ldap list
+#
+
+{
+ serialise("$data_dir/ldap.data", \%ldap);
+}
+
+#
+# new ldap accounts
+#
+
+my @new_ldap;
+{
+ foreach my $mail (sort keys %ldap) {
+ next if exists $ldap_old{$mail};
+ push @new_ldap, {
+ mail => $mail,
+ name => $ldap{$mail}{name},
+ bugmail => $ldap{$mail}{bugmail},
+ };
+ }
+}
+
+#
+# deleted ldap accounts
+#
+
+my @gone_ldap_bmo;
+my @gone_ldap_no_bmo;
+{
+ foreach my $mail (sort keys %ldap_old) {
+ next if exists $ldap{$mail};
+ if ($ldap_old{$mail}{bugmail}) {
+ push @gone_ldap_bmo, {
+ mail => $mail,
+ name => $ldap_old{$mail}{name},
+ bugmail => $ldap_old{$mail}{bugmail},
+ }
+ } else {
+ push @gone_ldap_no_bmo, {
+ mail => $mail,
+ name => $ldap_old{$mail}{name},
+ }
+ }
+ }
+}
+
+#
+# check bugmail entry for all users in bmo/moco group
+#
+
+my @suspect_bugzilla;
+my @invalid_bugzilla;
+foreach my $rh (@bugzilla_moco) {
+ my @check = ($rh->{mail}, $rh->{canon});
+ if ($rh->{mail} =~ /^([^\@]+)\@mozilla\.org$/) {
+ push @check, "$1\@mozilla.com";
+ }
+
+ my $exists;
+ foreach my $check (@check) {
+ $exists = 0;
+
+ # don't complain about deleted accounts
+ if (grep { $_->{mail} eq $check } (@gone_ldap_bmo, @gone_ldap_no_bmo)) {
+ $exists = 1;
+ last;
+ }
+
+ # check for matching bugmail entry
+ foreach my $mail (sort keys %ldap) {
+ next unless $ldap{$mail}{bugmail_canon} eq $check;
+ $exists = 1;
+ last;
+ }
+ last if $exists;
+
+ # check for matching mail
+ $exists = 0;
+ foreach my $mail (sort keys %ldap) {
+ next unless $mail eq $check;
+ $exists = 1;
+ last;
+ }
+ last if $exists;
+
+ # check for matching email alias
+ $exists = 0;
+ foreach my $mail (sort keys %ldap) {
+ next unless grep { $check eq $_ } @{$ldap{$mail}{aliases}};
+ $exists = 1;
+ last;
+ }
+ last if $exists;
+ }
+
+ if (!$exists) {
+ # flag the account
+ if ($rh->{mail} =~ /\@mozilla\.(com|org)$/i) {
+ push @invalid_bugzilla, {
+ mail => $rh->{mail},
+ name => $rh->{name},
+ };
+ } else {
+ push @suspect_bugzilla, {
+ mail => $rh->{mail},
+ name => $rh->{name},
+ };
+ }
+ }
+}
+
+#
+# check bugmail entry for ldap users
+#
+
+my @ldap_unblessed;
+my @invalid_ldap;
+my @invalid_bugmail;
+foreach my $mail (sort keys %ldap) {
+ # try to find the bmo account
+ my $found;
+ foreach my $address ($ldap{$mail}{bugmail}, $ldap{$mail}{bugmail_canon}, $mail, @{$ldap{$mail}{aliases}}) {
+ if (exists $bugzilla_login{$address}) {
+ $found = $address;
+ last;
+ }
+ }
+
+ # not on bmo
+ if (!$found) {
+ # if they have specified a bugmail account, warn, otherwise ignore
+ if ($ldap{$mail}{bugmail}) {
+ if (grep { $_->{canon} eq $ldap{$mail}{bugmail_canon} } @bugzilla_moco) {
+ push @invalid_bugmail, {
+ mail => $mail,
+ name => $ldap{$mail}{name},
+ bugmail => $ldap{$mail}{bugmail},
+ };
+ } else {
+ push @invalid_ldap, {
+ mail => $mail,
+ name => $ldap{$mail}{name},
+ bugmail => $ldap{$mail}{bugmail},
+ };
+ }
+ }
+ next;
+ }
+
+ # warn about mismatches
+ if ($ldap{$mail}{bugmail} && $found ne $ldap{$mail}{bugmail}) {
+ push @invalid_bugmail, {
+ mail => $mail,
+ name => $ldap{$mail}{name},
+ bugmail => $ldap{$mail}{bugmail},
+ };
+ }
+
+ # warn about unblessed accounts
+ if ($mail =~ /\@mozilla\.com$/) {
+ unless (grep { $_->{mail} eq $found || $_->{canon} eq canon_email($found) } @bugzilla_moco) {
+ push @ldap_unblessed, {
+ mail => $mail,
+ name => $ldap{$mail}{name},
+ bugmail => $ldap{$mail}{bugmail} || $mail,
+ };
+ }
+ }
+}
+
+#
+# reports
+#
+
+my @bmo_report;
+push @bmo_report, generate_report(
+ 'new ldap accounts',
+ 'no action required',
+ @new_ldap);
+
+push @bmo_report, generate_report(
+ 'deleted ldap accounts',
+ 'disable bmo account',
+ @gone_ldap_bmo);
+
+push @bmo_report, generate_report(
+ 'deleted ldap accounts',
+ 'no action required (no bmo account)',
+ @gone_ldap_no_bmo);
+
+push @bmo_report, generate_report(
+ 'suspect bugzilla accounts',
+ 'remove from mo-co if required',
+ @suspect_bugzilla);
+
+push @bmo_report, generate_report(
+ 'miss-configured bugzilla accounts',
+ 'ask owner to update phonebook, disable if not on phonebook',
+ @invalid_bugzilla);
+
+push @bmo_report, generate_report(
+ 'ldap accounts without mo-co group',
+ 'verify, and add mo-co group to bmo account',
+ @ldap_unblessed);
+
+push @bmo_report, generate_report(
+ 'missmatched bugmail entries on ldap accounts',
+ 'ask owner to update phonebook',
+ @invalid_bugmail);
+
+push @bmo_report, generate_report(
+ 'invalid bugmail entries on ldap accounts',
+ 'ask owner to update phonebook',
+ @invalid_ldap);
+
+if (!scalar @bmo_report) {
+ push @bmo_report, '**';
+ push @bmo_report, '** nothing to report \o/';
+ push @bmo_report, '**';
+}
+
+email_report(\@bmo_report, 'moco-ldap-check', BMO_RECIPIENTS);
+
+my @support_report;
+
+push @support_report, generate_report(
+ 'Missmatched "Bugzilla Email" entries on LDAP accounts',
+ 'Ask owner to update phonebook, or update directly',
+ @invalid_bugmail);
+
+push @support_report, generate_report(
+ 'Invalid "Bugzilla Email" entries on LDAP accounts',
+ 'Ask owner to update phonebook',
+ @invalid_ldap);
+
+if (scalar @support_report) {
+ email_report(\@support_report, 'Invalid "Bugzilla Email" entries in LDAP', SUPPORT_RECIPIENTS);
+}
+
+#
+#
+#
+
+sub generate_report {
+ my ($title, $action, @lines) = @_;
+
+ my $count = scalar @lines;
+ return unless $count;
+
+ my @report;
+ push @report, '';
+ push @report, '**';
+ push @report, "** $title ($count)";
+ push @report, "** [ $action ]";
+ push @report, '**';
+ push @report, '';
+
+ my $max_length = 0;
+ foreach my $rh (@lines) {
+ $max_length = length($rh->{mail}) if length($rh->{mail}) > $max_length;
+ }
+
+ foreach my $rh (@lines) {
+ my $template = "%-${max_length}s %s";
+ my @fields = ($rh->{mail}, $rh->{name});
+
+ if ($rh->{bugmail}) {
+ $template .= ' (%s)';
+ push @fields, $rh->{bugmail};
+ };
+
+ push @report, sprintf($template, @fields);
+ }
+
+ return @report;
+}
+
+sub email_report {
+ my ($report, $subject, @recipients) = @_;
+ unshift @$report, (
+ "Subject: $subject",
+ 'X-Bugzilla-Type: moco-ldap-check',
+ 'From: ' . REPORT_SENDER,
+ 'To: ' . join(',', @recipients),
+ );
+ if ($debug) {
+ print "\n", join("\n", @$report), "\n";
+ } else {
+ MessageToMTA(join("\n", @$report));
+ }
+}
+
+sub clean_email {
+ my $email = shift;
+ $email = trim($email);
+ $email = $1 if $email =~ /^(\S+)/;
+ $email =~ s/&#64;/@/;
+ $email = lc $email;
+ return $email;
+}
+
+sub canon_email {
+ my $email = shift;
+ $email = clean_email($email);
+ $email =~ s/^([^\+]+)\+[^\@]+(\@.+)$/$1$2/;
+ return $email;
+}
+
+sub trim {
+ my $value = shift;
+ $value =~ s/(^\s+|\s+$)//g;
+ return $value;
+}
+
+sub serialise {
+ my ($filename, $ref) = @_;
+ local $Data::Dumper::Purity = 1;
+ local $Data::Dumper::Deepcopy = 1;
+ local $Data::Dumper::Sortkeys = 1;
+ write_file($filename, Dumper($ref));
+}
+
+sub deserialise {
+ my ($filename) = @_;
+ return unless -s $filename;
+ my $cpt = Safe->new();
+ $cpt->reval('our ' . read_file($filename))
+ || die "$!";
+ return ${$cpt->varglob('VAR1')};
+}
+
diff --git a/contrib/nagios_blocker_checker.pl b/contrib/nagios_blocker_checker.pl
new file mode 100755
index 000000000..768053126
--- /dev/null
+++ b/contrib/nagios_blocker_checker.pl
@@ -0,0 +1,201 @@
+#!/usr/bin/perl
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+
+use FindBin qw($Bin);
+use lib "$Bin/..";
+use lib "$Bin/../lib";
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Product;
+use Bugzilla::User;
+use Getopt::Long;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my $config = {
+ # filter by assignee, product or component
+ assignee => '',
+ product => '',
+ component => '',
+ unassigned => 'nobody@mozilla.org',
+ # severities
+ severity => 'major,critical,blocker',
+ # time in hours to wait before paging/warning
+ major_alarm => 24,
+ major_warn => 20,
+ critical_alarm => 8,
+ critical_warn => 5,
+ blocker_alarm => 0,
+ blocker_warn => 0,
+ any_alarm => 24,
+ any_warn => 20,
+};
+
+my $usage = <<EOF;
+FILTERS
+
+ the filter determines which bugs to check, either by assignee, product or the
+ product's component. For backward compatibility, if just an email address is
+ provided, it will be used as the assignee.
+
+ --assignee <email> filter bugs by assignee
+ --product <name> filter bugs by product name
+ --component <name> filter bugs by product's component name
+ --unassigned <email> set the unassigned user (default: $config->{unassigned})
+
+SEVERITIES
+
+ by default alerts and warnings will be generated for 'major', 'critical', and
+ 'blocker' bugs. you can alter this list with the 'severity' switch.
+
+ setting severity to 'any' will result in alerting on unassigned bugs
+ regardless of severity.
+
+ --severity <major|critical|blocker>[,..]
+ --severity any
+
+TIMING
+
+ time in hours to wait before paging or warning.
+
+ --major_alarm <hours> (default: $config->{major_alarm})
+ --major_warn <hours> (default: $config->{major_warn})
+ --critical_alarm <hours> (default: $config->{critical_alarm})
+ --critical_warn <hours> (default: $config->{critical_warn})
+ --blocker_alarm <hours> (default: $config->{blocker_alarm})
+ --blocker_warn <hours> (default: $config->{blocker_warn})
+
+ when severity checking is set to "any", use the any_* switches instead:
+
+ --any_alarm <hours> (default: $config->{any_alarm})
+ --any_warn <hours> (default: $config->{any_warn})
+
+EXAMPLES
+
+ nagios_blocker_checker.pl --assignee server-ops\@mozilla-org.bugs
+ nagios_blocker_checker.pl server-ops\@mozilla-org.bugs
+ nagios_blocker_checker.pl --product 'Release Engineering' \
+ --component 'Loan Requests' \
+ --severity any --any_warn 24 --any_alarm 24
+EOF
+
+die($usage) unless GetOptions(
+ 'assignee=s' => \$config->{assignee},
+ 'product=s' => \$config->{product},
+ 'component=s' => \$config->{component},
+ 'severity=s' => \$config->{severity},
+ 'major_alarm=i' => \$config->{major_alarm},
+ 'major_warn=i' => \$config->{major_warn},
+ 'critical_alarm=i' => \$config->{critical_alarm},
+ 'critical_warn=i' => \$config->{critical_warn},
+ 'blocker_alarm=i' => \$config->{blocker_alarm},
+ 'blocker_warn=i' => \$config->{blocker_warn},
+ 'any_alarm=i' => \$config->{any_alarm},
+ 'any_warn=i' => \$config->{any_warn},
+ 'help|?' => \$config->{help},
+);
+$config->{assignee} = $ARGV[0] if !$config->{assignee} && @ARGV;
+die $usage if
+ $config->{help}
+ || !($config->{assignee} || $config->{product})
+ || ($config->{assignee} && $config->{product})
+ || ($config->{component} && !$config->{product})
+ || !$config->{severity};
+
+#
+
+use constant NAGIOS_OK => 0;
+use constant NAGIOS_WARNING => 1;
+use constant NAGIOS_CRITICAL => 2;
+use constant NAGIOS_NAMES => [qw( OK WARNING CRITICAL )];
+
+my $dbh = Bugzilla->switch_to_shadow_db;
+my $any_severity = $config->{severity} eq 'any';
+my ($where, @values);
+
+if ($config->{assignee}) {
+ $where = 'bugs.assigned_to = ?';
+ push @values, Bugzilla::User->check({ name => $config->{assignee} })->id;
+
+} elsif ($config->{component}) {
+ $where = 'bugs.product_id = ? AND bugs.component_id = ? AND bugs.assigned_to = ?';
+ my $product = Bugzilla::Product->check({ name => $config->{product} });
+ push @values, $product->id;
+ push @values, Bugzilla::Component->check({ product => $product, name => $config->{component} })->id;
+ push @values, Bugzilla::User->check({ name => $config->{unassigned} })->id;
+
+} else {
+ $where = 'bugs.product_id = ? AND bugs.assigned_to = ?';
+ push @values, Bugzilla::Product->check({ name => $config->{product} })->id;
+ push @values, Bugzilla::User->check({ name => $config->{unassigned} })->id;
+}
+
+if (!$any_severity) {
+ $where .= ' AND bug_severity IN (' .
+ join(',', map { $dbh->quote($_) } split(/,/, $config->{severity})) . ')';
+}
+
+my $sql = <<EOF;
+ SELECT bug_id, bug_severity, UNIX_TIMESTAMP(bugs.creation_ts) AS ts
+ FROM bugs
+ WHERE $where
+ AND COALESCE(resolution, '') = ''
+EOF
+
+my $bugs = {
+ 'major' => [],
+ 'critical' => [],
+ 'blocker' => [],
+ 'any' => [],
+};
+my $current_state = NAGIOS_OK;
+my $current_time = time;
+
+foreach my $bug (@{ $dbh->selectall_arrayref($sql, { Slice => {} }, @values) }) {
+ my $severity = $any_severity ? 'any' : $bug->{bug_severity};
+ my $age = ($current_time - $bug->{ts}) / 3600;
+
+ if ($age > $config->{"${severity}_alarm"}) {
+ $current_state = NAGIOS_CRITICAL;
+ push @{$bugs->{$severity}}, $bug->{bug_id};
+
+ } elsif ($age > $config->{"${severity}_warn"}) {
+ if ($current_state < NAGIOS_WARNING) {
+ $current_state = NAGIOS_WARNING;
+ }
+ push @{$bugs->{$severity}}, $bug->{bug_id};
+
+ }
+}
+
+print "bugs " . NAGIOS_NAMES->[$current_state] . ": ";
+if ($current_state == NAGIOS_OK) {
+ if ($config->{severity} eq 'any') {
+ print "No unassigned bugs found.";
+ } else {
+ print "No $config->{severity} bugs found."
+ }
+}
+foreach my $severity (qw( blocker critical major any )) {
+ my $list = $bugs->{$severity};
+ if (@$list) {
+ printf
+ "%s %s %s found https://bugzil.la/" . join(',', @$list) . " ",
+ scalar(@$list),
+ ($any_severity ? 'unassigned' : $severity),
+ (scalar(@$list) == 1 ? 'bug' : 'bugs');
+ }
+}
+print "\n";
+
+exit $current_state;
diff --git a/contrib/new-yui3.pl b/contrib/new-yui3.pl
new file mode 100755
index 000000000..2d1eeddc2
--- /dev/null
+++ b/contrib/new-yui3.pl
@@ -0,0 +1,80 @@
+#!/usr/bin/perl -w
+#
+# Updates the version of YUI3 used by Bugzilla. Just pass the path to
+# an unzipped yui release directory, like:
+#
+# contrib/new-yui3.pl /path/to/yui3/
+#
+
+use strict;
+
+use FindBin;
+use File::Find;
+use File::Basename;
+
+use constant EXCLUDES => qw(
+ gallery-*
+);
+
+sub usage {
+ my $error = shift;
+ print "$error\n";
+ print <<USAGE;
+Usage: contrib/new-yui3.pl /path/to/yui3/files
+
+Eg. contrib/new-yui3.pl /home/dkl/downloads/yui3
+The yui3 directory should contain the 'build' directory
+from the downloaded YUI3 tarball containing the module
+related files.
+USAGE
+ exit(1);
+}
+
+#############
+# Main Code #
+#############
+
+my $SOURCE_PATH = shift;
+my $DEST_PATH = "$FindBin::Bin/../js/yui3";
+
+if (!$SOURCE_PATH || !-d "$SOURCE_PATH/build") {
+ usage("Source path not found!");
+}
+
+mkdir($DEST_PATH) unless -d $DEST_PATH;
+
+my $exclude_string = join(" ", map { "--exclude $_" } EXCLUDES);
+my $command = "rsync -av --delete $exclude_string " .
+ "$SOURCE_PATH/build/ $DEST_PATH/";
+system($command) == 0 or usage("system '$command' failed: $?");
+
+find(
+ sub {
+ my $delete = 0;
+ my $filename = basename $File::Find::name;
+ if ($filename =~ /-debug\.js$/
+ || $filename =~ /-coverage\.js$/)
+ {
+ $delete = 1;
+ }
+ elsif ($filename =~ /-skin\.css$/) {
+ my $temp_filename = $filename;
+ $temp_filename =~ s/-skin//;
+ if (-e $temp_filename) {
+ $delete = 1;
+ }
+ }
+ elsif ($filename =~ /-min\.js/) {
+ $filename =~ s/-min//;
+ if (-e $filename) {
+ $delete = 1;
+ }
+ }
+ return if !$delete;
+ print "deleting $filename\n";
+ unlink($filename) || usage($!);
+ },
+ $DEST_PATH
+);
+
+exit(0);
diff --git a/contrib/nuke-bugs.pl b/contrib/nuke-bugs.pl
new file mode 100755
index 000000000..0226c1726
--- /dev/null
+++ b/contrib/nuke-bugs.pl
@@ -0,0 +1,69 @@
+#!/usr/bin/perl -w
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+
+use lib qw(.);
+
+use Bugzilla;
+use Bugzilla::Constants;
+
+use Getopt::Long;
+
+# This SQL is designed to delete the bugs and other activity in a Bugzilla database
+# so that one can use for development purposes or to start using it as a fresh installation.
+# Other data will be retained such as products, versions, flags, profiles, etc.
+
+$| = 1;
+my $trace = 0;
+
+GetOptions("trace" => \$trace) || exit;
+
+my $dbh = Bugzilla->dbh;
+
+$dbh->{TraceLevel} = 1 if $trace;
+
+print <<EOF;
+WARNING - This will delete all bugs, hit <enter> to continue or <ctrl-c> to cancel - WARNING
+EOF
+getc();
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+$dbh->bz_start_transaction();
+
+print "Deleting all bug data...\n";
+
+delete_from_table('bug_group_map');
+delete_from_table('bugs_activity');
+delete_from_table('cc');
+delete_from_table('dependencies');
+delete_from_table('duplicates');
+delete_from_table('flags');
+delete_from_table('keywords');
+delete_from_table('attach_data');
+delete_from_table('attachments');
+delete_from_table('bug_group_map');
+delete_from_table('bugs');
+delete_from_table('longdescs');
+
+$dbh->do($dbh->_bz_real_schema->get_set_serial_sql('bugs', 'bug_id', 1));
+
+$dbh->bz_commit_transaction();
+
+# This has to happen outside of the transaction
+$dbh->do("DELETE FROM bugs_fulltext");
+
+print "All done!\n";
+
+sub delete_from_table {
+ my $table = shift;
+ print "Deleting from $table...";
+ $dbh->do("DELETE FROM $table");
+ print "done.\n";
+}
diff --git a/contrib/recode.pl b/contrib/recode.pl
index f8de12eb1..e74e06c07 100755
--- a/contrib/recode.pl
+++ b/contrib/recode.pl
@@ -42,8 +42,10 @@ use constant MAX_STRING_LEN => 25;
# For certain tables, we can't automatically determine their Primary Key.
# So, we specify it here as a string.
use constant SPECIAL_KEYS => {
+ # bugs_activity since 4.4 has a unique primary key added
bugs_activity => 'bug_id,bug_when,fieldid',
profile_setting => 'user_id,setting_name',
+ # profiles_activity since 4.4 has a unique primary key added
profiles_activity => 'userid,profiles_when,fieldid',
setting_value => 'name,value',
# longdescs didn't used to have a PK, before 2.20.
diff --git a/contrib/reorg-tools/README b/contrib/reorg-tools/README
new file mode 100644
index 000000000..4e5d6eb4d
--- /dev/null
+++ b/contrib/reorg-tools/README
@@ -0,0 +1,9 @@
+Upstreaming attempt: https://bugzilla.mozilla.org/show_bug.cgi?id=616499
+
+Included in this directory is a group of tools we've used for moving components
+around in a Bugzilla 3.2 install on bugzilla.mozilla.org.
+
+They may require tweaking if you use them on your own install. Putting them
+here to make it easier to collaborate on them and keep them up-to-date.
+Hopefully Bugzilla upstream will be able to just do this from the web UI
+eventually.
diff --git a/contrib/reorg-tools/convert_date_time_date.pl b/contrib/reorg-tools/convert_date_time_date.pl
new file mode 100755
index 000000000..a2e9bfffc
--- /dev/null
+++ b/contrib/reorg-tools/convert_date_time_date.pl
@@ -0,0 +1,62 @@
+#!/usr/bin/perl -w
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+
+use Cwd 'abs_path';
+use File::Basename;
+use FindBin;
+use lib "$FindBin::Bin/../..";
+use lib "$FindBin::Bin/../../lib";
+
+use Bugzilla;
+use Bugzilla::Constants;
+
+sub usage() {
+ print <<USAGE;
+Usage: convert_date_time_date.pl <column>
+
+E.g.: convert_date_time_date.pl cf_due_date
+Converts a datetime field (FIELD_TYPE_DATETIME) to a date field type
+(FIELD_TYPE_DATE).
+
+Note: Any time portion will be lost but the date portion will be preserved.
+USAGE
+}
+
+#############################################################################
+# MAIN CODE
+#############################################################################
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 1) {
+ usage();
+ exit();
+}
+
+my $column = shift;
+
+print <<EOF;
+Converting bugs.${column} from FIELD_TYPE_DATETIME to FIELD_TYPE_DATE.
+
+Note: Any time portion will be lost but the date portion will be preserved.
+
+Press <Ctrl-C> to stop or <Enter> to continue...
+EOF
+getc();
+
+Bugzilla->dbh->bz_alter_column('bugs', $column, { TYPE => 'DATE' });
+Bugzilla->dbh->do("UPDATE fielddefs SET type = ? WHERE name = ?",
+ undef, FIELD_TYPE_DATE, $column);
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
+
+print "\ndone.\n";
diff --git a/contrib/reorg-tools/fix_all_open_status_queries.pl b/contrib/reorg-tools/fix_all_open_status_queries.pl
new file mode 100755
index 000000000..7c8d8be68
--- /dev/null
+++ b/contrib/reorg-tools/fix_all_open_status_queries.pl
@@ -0,0 +1,144 @@
+#!/usr/bin/perl -w
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Status;
+use Bugzilla::Util;
+
+sub usage() {
+ print <<USAGE;
+Usage: fix_all_open_status_queries.pl <new_open_status>
+
+E.g.: fix_all_open_status_queries.pl READY
+This will add a new open state to user queries which currently look for
+all open bugs by listing every open status in their query criteria.
+For users who only look for bug_status=__open__, they will get the new
+open status automatically.
+USAGE
+}
+
+sub do_namedqueries {
+ my ($new_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $replace_count = 0;
+
+ my $query = $dbh->selectall_arrayref("SELECT id, query FROM namedqueries");
+
+ if ($query) {
+ $dbh->bz_start_transaction();
+
+ my $sth = $dbh->prepare("UPDATE namedqueries SET query = ? WHERE id = ?");
+
+ foreach my $row (@$query) {
+ my ($id, $old_query) = @$row;
+ my $new_query = all_open_states($new_status, $old_query);
+ if ($new_query) {
+ trick_taint($new_query);
+ $sth->execute($new_query, $id);
+ $replace_count++;
+ }
+ }
+
+ $dbh->bz_commit_transaction();
+ }
+
+ print "namedqueries: $replace_count replacements made.\n";
+}
+
+# series
+sub do_series {
+ my ($new_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $replace_count = 0;
+
+ my $query = $dbh->selectall_arrayref("SELECT series_id, query FROM series");
+
+ if ($query) {
+ $dbh->bz_start_transaction();
+
+ my $sth = $dbh->prepare("UPDATE series SET query = ? WHERE series_id = ?");
+
+ foreach my $row (@$query) {
+ my ($series_id, $old_query) = @$row;
+ my $new_query = all_open_states($new_status, $old_query);
+ if ($new_query) {
+ trick_taint($new_query);
+ $sth->execute($new_query, $series_id);
+ $replace_count++;
+ }
+ }
+
+ $dbh->bz_commit_transaction();
+ }
+
+ print "series: $replace_count replacements made.\n";
+}
+
+sub all_open_states {
+ my ($new_status, $query) = @_;
+
+ my @open_states = Bugzilla::Status::BUG_STATE_OPEN();
+ my $cgi = Bugzilla::CGI->new($query);
+ my @query_states = $cgi->param('bug_status');
+
+ my ($removed, $added) = diff_arrays(\@query_states, \@open_states);
+
+ if (scalar @$added == 1 && $added->[0] eq $new_status) {
+ push(@query_states, $new_status);
+ $cgi->param('bug_status', @query_states);
+ return $cgi->canonicalise_query();
+ }
+
+ return '';
+}
+
+sub validate_status {
+ my ($status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $exists = $dbh->selectrow_array("SELECT 1 FROM bug_status
+ WHERE value = ?",
+ undef, $status);
+ return $exists ? 1 : 0;
+}
+
+#############################################################################
+# MAIN CODE
+#############################################################################
+# This is a pure command line script.
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 1) {
+ usage();
+ exit(1);
+}
+
+my ($new_status) = @ARGV;
+
+$new_status = uc($new_status);
+
+if (!validate_status($new_status)) {
+ print "Invalid status: $new_status\n\n";
+ usage();
+ exit(1);
+}
+
+print "Adding new status '$new_status'.\n\n";
+
+do_namedqueries($new_status);
+do_series($new_status);
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
+
+exit(0);
diff --git a/contrib/reorg-tools/fixgroupqueries.pl b/contrib/reorg-tools/fixgroupqueries.pl
new file mode 100755
index 000000000..0bd64cd40
--- /dev/null
+++ b/contrib/reorg-tools/fixgroupqueries.pl
@@ -0,0 +1,123 @@
+#!/usr/bin/perl -w
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Gervase Markham <gerv@gerv.net>
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Util;
+
+sub usage() {
+ print <<USAGE;
+Usage: fixgroupqueries.pl <oldvalue> <newvalue>
+
+E.g.: fixgroupqueries.pl w-security webtools-security
+will change all occurrences of "w-security" to "webtools-security" in the
+appropriate places in the namedqueries.
+
+Note that all parameters are case-sensitive.
+USAGE
+}
+
+sub do_namedqueries($$) {
+ my ($old, $new) = @_;
+ $old = url_quote($old);
+ $new = url_quote($new);
+
+ my $dbh = Bugzilla->dbh;
+
+ my $replace_count = 0;
+ my $query = $dbh->selectall_arrayref("SELECT id, query FROM namedqueries");
+ if ($query) {
+ my $sth = $dbh->prepare("UPDATE namedqueries SET query = ?
+ WHERE id = ?");
+
+ foreach my $row (@$query) {
+ my ($id, $query) = @$row;
+ if (($query =~ /field\d+-\d+-\d+=bug_group/) &&
+ ($query =~ /(?:^|&|;)value\d+-\d+-\d+=$old(?:;|&|$)/)) {
+ $query =~ s/((?:^|&|;)value\d+-\d+-\d+=)$old(;|&|$)/$1$new$2/;
+ $sth->execute($query, $id);
+ $replace_count++;
+ }
+ }
+ }
+
+ print "namedqueries: $replace_count replacements made.\n";
+}
+
+# series
+sub do_series($$) {
+ my ($old, $new) = @_;
+ $old = url_quote($old);
+ $new = url_quote($new);
+
+ my $dbh = Bugzilla->dbh;
+ #$dbh->bz_start_transaction();
+
+ my $replace_count = 0;
+ my $query = $dbh->selectall_arrayref("SELECT series_id, query
+ FROM series");
+ if ($query) {
+ my $sth = $dbh->prepare("UPDATE series SET query = ?
+ WHERE series_id = ?");
+ foreach my $row (@$query) {
+ my ($series_id, $query) = @$row;
+
+ if (($query =~ /field\d+-\d+-\d+=bug_group/) &&
+ ($query =~ /(?:^|&|;)value\d+-\d+-\d+=$old(?:;|&|$)/)) {
+ $query =~ s/((?:^|&|;)value\d+-\d+-\d+=)$old(;|&|$)/$1$new$2/;
+ $sth->execute($query, $series_id);
+ $replace_count++;
+ }
+ }
+ }
+
+ #$dbh->bz_commit_transaction();
+ print "series: $replace_count replacements made.\n";
+}
+
+#############################################################################
+# MAIN CODE
+#############################################################################
+# This is a pure command line script.
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 2) {
+ usage();
+ exit();
+}
+
+my ($old, $new) = @ARGV;
+
+print "Changing all instances of '$old' to '$new'.\n\n";
+
+#do_namedqueries($old, $new);
+do_series($old, $new);
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
+
+exit(0);
diff --git a/contrib/reorg-tools/fixqueries.pl b/contrib/reorg-tools/fixqueries.pl
new file mode 100755
index 000000000..221213058
--- /dev/null
+++ b/contrib/reorg-tools/fixqueries.pl
@@ -0,0 +1,136 @@
+#!/usr/bin/perl -w
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Gervase Markham <gerv@gerv.net>
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Util;
+
+sub usage() {
+ print <<USAGE;
+Usage: fixqueries.pl <parameter> <oldvalue> <newvalue>
+
+E.g.: fixqueries.pl product FoodReplicator SeaMonkey
+will change all occurrences of "FoodReplicator" to "Seamonkey" in the
+appropriate places in the namedqueries, series and series_categories tables.
+
+Note that all parameters are case-sensitive.
+USAGE
+}
+
+sub do_namedqueries($$$) {
+ my ($field, $old, $new) = @_;
+ $old = url_quote($old);
+ $new = url_quote($new);
+
+ my $dbh = Bugzilla->dbh;
+ #$dbh->bz_start_transaction();
+
+ my $replace_count = 0;
+ my $query = $dbh->selectall_arrayref("SELECT id, query FROM namedqueries");
+ if ($query) {
+ my $sth = $dbh->prepare("UPDATE namedqueries SET query = ?
+ WHERE id = ?");
+
+ foreach my $row (@$query) {
+ my ($id, $query) = @$row;
+ if ($query =~ /(?:^|&|;)$field=$old(?:&|$|;)/) {
+ $query =~ s/((?:^|&|;)$field=)$old(;|&|$)/$1$new$2/;
+ $sth->execute($query, $id);
+ $replace_count++;
+ }
+ }
+ }
+
+ #$dbh->bz_commit_transaction();
+ print "namedqueries: $replace_count replacements made.\n";
+}
+
+# series
+sub do_series($$$) {
+ my ($field, $old, $new) = @_;
+ $old = url_quote($old);
+ $new = url_quote($new);
+
+ my $dbh = Bugzilla->dbh;
+ #$dbh->bz_start_transaction();
+
+ my $replace_count = 0;
+ my $query = $dbh->selectall_arrayref("SELECT series_id, query
+ FROM series");
+ if ($query) {
+ my $sth = $dbh->prepare("UPDATE series SET query = ?
+ WHERE series_id = ?");
+ foreach my $row (@$query) {
+ my ($series_id, $query) = @$row;
+
+ if ($query =~ /(?:^|&|;)$field=$old(?:&|$|;)/) {
+ $query =~ s/((?:^|&|;)$field=)$old(;|&|$)/$1$new$2/;
+ $replace_count++;
+ }
+
+ $sth->execute($query, $series_id);
+ }
+ }
+
+ #$dbh->bz_commit_transaction();
+ print "series: $replace_count replacements made.\n";
+}
+
+# series_categories
+sub do_series_categories($$) {
+ my ($old, $new) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->do("UPDATE series_categories SET name = ? WHERE name = ?",
+ undef,
+ ($new, $old));
+}
+
+#############################################################################
+# MAIN CODE
+#############################################################################
+# This is a pure command line script.
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 3) {
+ usage();
+ exit();
+}
+
+my ($field, $old, $new) = @ARGV;
+
+print "Changing all instances of '$old' to '$new'.\n\n";
+
+do_namedqueries($field, $old, $new);
+do_series($field, $old, $new);
+do_series_categories($old, $new);
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
+
+exit(0);
+
diff --git a/contrib/reorg-tools/migrate_crash_signatures.pl b/contrib/reorg-tools/migrate_crash_signatures.pl
new file mode 100755
index 000000000..4323c1d01
--- /dev/null
+++ b/contrib/reorg-tools/migrate_crash_signatures.pl
@@ -0,0 +1,132 @@
+#!/usr/bin/perl
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Initial Developer of the Original Code is Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+#===============================================================================
+#
+# FILE: migrate_crash_signatures.pl
+#
+# USAGE: ./migrate_crash_signatures.pl
+#
+# DESCRIPTION: Migrate current summary data on matched bugs to the
+# new cf_crash_signature custom fields.
+#
+# OPTIONS: No params, then performs dry-run without updating the database.
+# If a true value is passed as single argument, then the database
+# is updated.
+# REQUIREMENTS: None
+# BUGS: 577724
+# NOTES: None
+# AUTHOR: David Lawrence (dkl@mozilla.com),
+# COMPANY: Mozilla Corproation
+# VERSION: 1.0
+# CREATED: 05/31/2011 03:57:52 PM
+# REVISION: 1
+#===============================================================================
+
+use strict;
+use warnings;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Util;
+
+use Data::Dumper;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my $UPDATE_DB = shift; # Pass true value as single argument to perform database update
+
+my $dbh = Bugzilla->dbh;
+
+# User to make changes as
+my $user_id = $dbh->selectrow_array(
+ "SELECT userid FROM profiles WHERE login_name='nobody\@mozilla.org'");
+$user_id or die "Can't find user ID for 'nobody\@mozilla.org'\n";
+
+my $field_id = $dbh->selectrow_array(
+ "SELECT id FROM fielddefs WHERE name = 'cf_crash_signature'");
+$field_id or die "Can't find field ID for 'cf_crash_signature' field\n";
+
+# Search criteria
+# a) crash or topcrash keyword,
+# b) not have [notacrash] in whiteboard,
+# c) have a properly formulated [@ ...]
+
+# crash and topcrash keyword ids
+my $crash_keyword_id = $dbh->selectrow_array(
+ "SELECT id FROM keyworddefs WHERE name = 'crash'");
+$crash_keyword_id or die "Can't find keyword id for 'crash'\n";
+
+my $topcrash_keyword_id = $dbh->selectrow_array(
+ "SELECT id FROM keyworddefs WHERE name = 'topcrash'");
+$topcrash_keyword_id or die "Can't find keyword id for 'topcrash'\n";
+
+# main search query
+my $bugs = $dbh->selectall_arrayref("
+ SELECT bugs.bug_id, bugs.short_desc
+ FROM bugs LEFT JOIN keywords ON bugs.bug_id = keywords.bug_id
+ WHERE (keywords.keywordid = ? OR keywords.keywordid = ?)
+ AND bugs.status_whiteboard NOT REGEXP '\\\\[notacrash\\\\]'
+ AND bugs.short_desc REGEXP '\\\\[@.+\\\\]'
+ AND (bugs.cf_crash_signature IS NULL OR bugs.cf_crash_signature = '')
+ ORDER BY bugs.bug_id",
+ {'Slice' => {}}, $crash_keyword_id, $topcrash_keyword_id);
+
+my $bug_count = scalar @$bugs;
+$bug_count or die "No bugs were found in matching search criteria.\n";
+
+print "Migrating $bug_count bugs to new crash signature field\n";
+
+$dbh->bz_start_transaction() if $UPDATE_DB;
+
+foreach my $bug (@$bugs) {
+ my $bug_id = $bug->{'bug_id'};
+ my $summary = $bug->{'short_desc'};
+
+ print "Updating bug $bug_id ...";
+
+ my @signatures;
+ while ($summary =~ /(\[\@(?:\[.*\]|[^\[])*\])/g) {
+ push(@signatures, $1);
+ }
+
+ if (@signatures && $UPDATE_DB) {
+ my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ $dbh->do("UPDATE bugs SET cf_crash_signature = ? WHERE bug_id = ?",
+ undef, join("\n", @signatures), $bug_id);
+ $dbh->do("INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) " .
+ "VALUES (?, ?, ?, ?, '', ?)",
+ undef, $bug_id, $user_id, $timestamp, $field_id, join("\n", @signatures));
+ $dbh->do("UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?",
+ undef, $timestamp, $timestamp, $bug_id);
+ }
+ elsif (@signatures) {
+ print Dumper(\@signatures);
+ }
+
+ print "done.\n";
+}
+
+if ($UPDATE_DB) {
+ $dbh->bz_commit_transaction();
+
+ # It's complex to determine which items now need to be flushed from memcached.
+ # As this is expected to be a rare event, we just flush the entire cache.
+ Bugzilla->memcached->clear_all();
+}
diff --git a/contrib/reorg-tools/migrate_orange_bugs.pl b/contrib/reorg-tools/migrate_orange_bugs.pl
new file mode 100755
index 000000000..4902464a3
--- /dev/null
+++ b/contrib/reorg-tools/migrate_orange_bugs.pl
@@ -0,0 +1,158 @@
+#!/usr/bin/perl -wT
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+#===============================================================================
+#
+# FILE: migrate_orange_bugs.pl
+#
+# USAGE: ./migrate_orange_bugs.pl [--remove]
+#
+# DESCRIPTION: Add intermittent-keyword to bugs with [orange] stored in
+# whiteboard field. If --remove, then also remove the [orange]
+# value from whiteboard.
+#
+# OPTIONS: Without --doit, does a dry-run without updating the database.
+# If --doit is passed, then the database is updated.
+# --remove will remove [orange] from the whiteboard.
+# REQUIREMENTS: None
+# BUGS: 791758
+# NOTES: None
+# AUTHOR: David Lawrence (dkl@mozilla.com),
+# COMPANY: Mozilla Corproation
+# VERSION: 1.0
+# CREATED: 10/31/2012
+# REVISION: 1
+#===============================================================================
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Field;
+use Bugzilla::User;
+use Bugzilla::Keyword;
+
+use Getopt::Long;
+use Term::ANSIColor qw(colored);
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my ($remove_whiteboard, $help, $doit);
+GetOptions("r|remove" => \$remove_whiteboard,
+ "h|help" => \$help, 'doit' => \$doit);
+
+sub usage {
+ my $error = shift || "";
+ print colored(['red'], $error) if $error;
+ print <<USAGE;
+Usage: migrate_orange_bugs.pl [--remove|-r] [--help|-h] [--doit]
+
+E.g.: migrate_orange_bugs.pl --remove --doit
+This script will add the intermittent-failure keyword to any bugs that
+contant [orange] in the status whiteboard. If the --remove option is
+given, then [orange] will be removed from the whiteboard as well.
+
+Pass --doit to make the database changes permanent.
+USAGE
+ exit(1);
+}
+
+# Exit if help was requested
+usage() if $help;
+
+# User to make changes as
+my $user_id = login_to_id('nobody@mozilla.org');
+$user_id or usage("Can't find user ID for 'nobody\@mozilla.org'\n");
+
+my $keywords_field_id = get_field_id('keywords');
+$keywords_field_id or usage("Can't find field ID for 'keywords' field\n");
+
+my $whiteboard_field_id = get_field_id('status_whiteboard');
+$whiteboard_field_id or usage("Can't find field ID for 'whiteboard' field\n");
+
+# intermittent-keyword id (assumes already created)
+my $keyword_obj = Bugzilla::Keyword->new({ name => 'intermittent-failure' });
+$keyword_obj or usage("Can't find keyword id for 'intermittent-failure'\n");
+my $keyword_id = $keyword_obj->id;
+
+my $dbh = Bugzilla->dbh;
+
+my $bugs = $dbh->selectall_arrayref("
+ SELECT DISTINCT bugs.bug_id, bugs.status_whiteboard
+ FROM bugs WHERE bugs.status_whiteboard LIKE '%[orange]%'
+ OR bugs.status_whiteboard LIKE '%[tb-orange]%'",
+ {'Slice' => {}});
+
+my $bug_count = scalar @$bugs;
+$bug_count or usage("No bugs were found in matching search criteria.\n");
+
+print colored(['green'], "Processing $bug_count [orange] bugs\n");
+
+$dbh->bz_start_transaction() if $doit;
+
+foreach my $bug (@$bugs) {
+ my $bug_id = $bug->{'bug_id'};
+ my $whiteboard = $bug->{'status_whiteboard'};
+
+ print "Checking bug $bug_id ... ";
+
+ my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+
+ my $keyword_present = $dbh->selectrow_array("
+ SELECT bug_id FROM keywords WHERE bug_id = ? AND keywordid = ?",
+ undef, $bug_id, $keyword_id);
+
+ if (!$keyword_present) {
+ print "adding keyword ... ";
+
+ if ($doit) {
+ $dbh->do("INSERT INTO keywords (bug_id, keywordid) VALUES (?, ?)",
+ undef, $bug_id, $keyword_id);
+ $dbh->do("INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) " .
+ "VALUES (?, ?, ?, ?, '', 'intermittent-failure')",
+ undef, $bug_id, $user_id, $timestamp, $keywords_field_id);
+ $dbh->do("UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?",
+ undef, $timestamp, $timestamp, $bug_id);
+ }
+ }
+
+ if ($remove_whiteboard) {
+ print "removing whiteboard ... ";
+
+ if ($doit) {
+ my $old_whiteboard = $whiteboard;
+ $whiteboard =~ s/\[(tb-)?orange\]//ig;
+
+ $dbh->do("UPDATE bugs SET status_whiteboard = ? WHERE bug_id = ?",
+ undef, $whiteboard, $bug_id);
+ $dbh->do("INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) " .
+ "VALUES (?, ?, ?, ?, ?, ?)",
+ undef, $bug_id, $user_id, $timestamp, $whiteboard_field_id, $old_whiteboard, $whiteboard);
+ $dbh->do("UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?",
+ undef, $timestamp, $timestamp, $bug_id);
+ }
+ }
+
+ print "done.\n";
+}
+
+$dbh->bz_commit_transaction() if $doit;
+
+if ($doit) {
+ # It's complex to determine which items now need to be flushed from memcached.
+ # As this is expected to be a rare event, we just flush the entire cache.
+ Bugzilla->memcached->clear_all();
+
+ print colored(['green'], "DATABASE WAS UPDATED\n");
+}
+else {
+ print colored(['red'], "DATABASE WAS NOT UPDATED\n");
+}
+
+exit(0);
diff --git a/contrib/reorg-tools/move_dupes_to_invalid.pl b/contrib/reorg-tools/move_dupes_to_invalid.pl
new file mode 100755
index 000000000..25106ce9c
--- /dev/null
+++ b/contrib/reorg-tools/move_dupes_to_invalid.pl
@@ -0,0 +1,101 @@
+#!/usr/bin/perl
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+
+use FindBin '$RealBin';
+use lib "$RealBin/../..", "$RealBin/../../lib";
+
+use Bugzilla;
+use Bugzilla::User;
+use Bugzilla::Constants;
+use Bugzilla::Util qw(detaint_natural);
+use Bugzilla::Install::Util qw(indicate_progress);
+
+use Pod::Usage;
+
+BEGIN { Bugzilla->extensions(); }
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+pod2usage(1) unless @ARGV;
+
+my $dbh = Bugzilla->dbh;
+
+# Allow nobody@mozilla.org to edit bugs
+my $user = Bugzilla::User->check({ name => 'nobody@mozilla.org' });
+Bugzilla->set_user($user);
+$user->{'groups'} = [ Bugzilla::Group->new({ name => 'editbugs' }) ];
+
+my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+
+foreach my $dupe_of_bug (@ARGV) {
+ detaint_natural($dupe_of_bug) || pod2usage(1);
+ my $duped_bugs = $dbh->selectcol_arrayref("
+ SELECT DISTINCT dupe FROM duplicates WHERE dupe_of = ?",
+ undef, $dupe_of_bug);
+ my $bug_count = @$duped_bugs;
+
+ die "There are no duplicate bugs to move for bug $dupe_of_bug.\n"
+ if $bug_count == 0;
+
+ print STDERR <<EOF;
+Moving $bug_count duplicate bugs from bug $dupe_of_bug to the 'Invalid Bugs' product.
+
+Press <Ctrl-C> to stop or <Enter> to continue...
+EOF
+ getc();
+
+ $dbh->bz_start_transaction;
+ my $count = 0;
+ foreach my $duped_bug_id (@$duped_bugs) {
+ # Change product to "Invalid Bugs" and component to "General"
+ # Change resolution to "INVALID" instead of duplicate
+ # Change version to "unspecified" and milestone to "---"
+ # Reset assignee to default
+ # Reset QA contact to default
+ my $bug_obj = Bugzilla::Bug->new($duped_bug_id);
+ my $params = {
+ product => 'Invalid Bugs',
+ component => 'General',
+ resolution => 'INVALID',
+ version => 'unspecified',
+ target_milestone => '---',
+ reset_assigned_to => 1,
+ reset_qa_contact => 1
+ };
+ $params->{bug_status} = 'RESOLVED' if $bug_obj->status->is_open;
+ $bug_obj->set_all($params);
+ $bug_obj->update($timestamp);
+
+ $dbh->do("UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?",
+ undef, $timestamp, $timestamp, $duped_bug_id);
+
+ $count++;
+ indicate_progress({ current => $count, total => $bug_count, every => 1 });
+ }
+
+ Bugzilla::Hook::process('reorg_move_bugs', { bug_ids => [ $dupe_of_bug, @$duped_bugs ] });
+
+ $dbh->bz_commit_transaction();
+}
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
+
+__END__
+
+=head1 NAME
+
+move_dupes_to_invalid.pl - Script used to move dupes of a given bug to the 'Invalid Bugs' product.
+
+=head1 SYNOPSIS
+
+ move_dupes_to_invalid.pl <bug_id> [<bug_id> ...]
diff --git a/contrib/reorg-tools/move_flag_types.pl b/contrib/reorg-tools/move_flag_types.pl
new file mode 100755
index 000000000..7b7fe2081
--- /dev/null
+++ b/contrib/reorg-tools/move_flag_types.pl
@@ -0,0 +1,172 @@
+#!/usr/bin/perl
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Initial Developer of the Original Code is Mozilla Foundation
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+#===============================================================================
+#
+# FILE: move_flag_types.pl
+#
+# USAGE: ./move_flag_types.pl
+#
+# DESCRIPTION: Move current set flag from one type_id to another
+# based on product and optionally component.
+#
+# OPTIONS: ---
+# REQUIREMENTS: ---
+# BUGS: ---
+# NOTES: ---
+# AUTHOR: David Lawrence (:dkl), dkl@mozilla.com
+# COMPANY: Mozilla Foundation
+# VERSION: 1.0
+# CREATED: 08/22/2011 05:18:06 PM
+# REVISION: ---
+#===============================================================================
+
+=head1 NAME
+
+move_flag_types.pl - Move currently set flags from one type id to another based
+on product and optionally component.
+
+=head1 SYNOPSIS
+
+This script will move bugs matching a specific product (and optionally a component)
+from one flag type id to another if the bug has the flag set to either +, -, or ?.
+
+./move_flag_types.pl --old-id 4 --new-id 720 --product Firefox --component Installer
+
+=head1 OPTIONS
+
+=over
+
+=item B<--help|-h|?>
+
+Print a brief help message and exits.
+
+=item B<--oldid|-o>
+
+Old flag type id. Use editflagtypes.cgi to determine the type id from the URL.
+
+=item B<--newid|-n>
+
+New flag type id. Use editflagtypes.cgi to determine the type id from the URL.
+
+=item B<--product|-p>
+
+The product that the bugs most be assigned to.
+
+=item B<--component|-c>
+
+Optional: The component of the given product that the bugs must be assigned to.
+
+=item B<--doit|-d>
+
+Without this argument, changes are not actually committed to the database.
+
+=back
+
+=cut
+
+use strict;
+use warnings;
+
+use lib '.';
+
+use Bugzilla;
+use Getopt::Long;
+use Pod::Usage;
+
+my %params;
+GetOptions(\%params, 'help|h|?', 'oldid|o=s', 'newid|n=s',
+ 'product|p=s', 'component|c:s', 'doit|d') or pod2usage(1);
+
+if ($params{'help'} || !$params{'oldid'}
+ || !$params{'newid'} || !$params{'product'}) {
+ pod2usage({ -message => "Missing required argument",
+ -exitval => 1 });
+}
+
+# Set defaults
+$params{'doit'} ||= 0;
+$params{'component'} ||= '';
+
+my $dbh = Bugzilla->dbh;
+
+# Get the flag names
+my $old_flag_name = $dbh->selectrow_array(
+ "SELECT name FROM flagtypes WHERE id = ?",
+ undef, $params{'oldid'});
+my $new_flag_name = $dbh->selectrow_array(
+ "SELECT name FROM flagtypes WHERE id = ?",
+ undef, $params{'newid'});
+
+# Find the product id
+my $product_id = $dbh->selectrow_array(
+ "SELECT id FROM products WHERE name = ?",
+ undef, $params{'product'});
+
+# Find the component id if not __ANY__
+my $component_id;
+if ($params{'component'}) {
+ $component_id = $dbh->selectrow_array(
+ "SELECT id FROM components WHERE name = ? AND product_id = ?",
+ undef, $params{'component'}, $product_id);
+}
+
+my @query_args = ($params{'oldid'});
+
+my $flag_query = "SELECT flags.id AS flag_id, flags.bug_id AS bug_id
+ FROM flags JOIN bugs ON flags.bug_id = bugs.bug_id
+ WHERE flags.type_id = ? ";
+
+if ($component_id) {
+ # No need to compare against product_id as component_id is already
+ # tied to a specific product
+ $flag_query .= "AND bugs.component_id = ?";
+ push(@query_args, $component_id);
+}
+else {
+ # All bugs for a product regardless of component
+ $flag_query .= "AND bugs.product_id = ?";
+ push(@query_args, $product_id);
+}
+
+my $flags = $dbh->selectall_arrayref($flag_query, undef, @query_args);
+
+if (@$flags) {
+ print "Moving '" . scalar @$flags . "' flags " .
+ "from $old_flag_name (" . $params{'oldid'} . ") " .
+ "to $new_flag_name (" . $params{'newid'} . ")...\n";
+
+ if (!$params{'doit'}) {
+ print "Pass the argument --doit or -d to permanently make changes to the database.\n";
+ }
+ else {
+ my $flag_update_sth = $dbh->prepare("UPDATE flags SET type_id = ? WHERE id = ?");
+
+ foreach my $flag (@$flags) {
+ my ($flag_id, $bug_id) = @$flag;
+ print "Bug: $bug_id Flag: $flag_id\n";
+ $flag_update_sth->execute($params{'newid'}, $flag_id);
+ }
+ }
+
+ # It's complex to determine which items now need to be flushed from memcached.
+ # As this is expected to be a rare event, we just flush the entire cache.
+ Bugzilla->memcached->clear_all();
+}
+else {
+ print "No flags to move\n";
+}
diff --git a/contrib/reorg-tools/move_os.pl b/contrib/reorg-tools/move_os.pl
new file mode 100755
index 000000000..96b58d616
--- /dev/null
+++ b/contrib/reorg-tools/move_os.pl
@@ -0,0 +1,81 @@
+#!/usr/bin/perl
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin '$RealBin';
+use lib "$RealBin/../..", "$RealBin/../../lib";
+
+use Bugzilla;
+use Bugzilla::Field;
+use Bugzilla::Constants;
+
+use Getopt::Long qw( :config gnu_getopt );
+use Pod::Usage;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my ($from_os, $to_os);
+GetOptions('from=s' => \$from_os, 'to=s' => \$to_os);
+
+pod2usage(1) unless defined $from_os && defined $to_os;
+
+
+my $check_from_os = Bugzilla::Field::Choice->type('op_sys')->match({ value => $from_os });
+my $check_to_os = Bugzilla::Field::Choice->type('op_sys')->match({ value => $to_os });
+die "Cannot move $from_os because it does not exist\n"
+ unless @$check_from_os == 1;
+die "Cannot move $from_os because $to_os doesn't exist.\n"
+ unless @$check_to_os == 1;
+
+my $dbh = Bugzilla->dbh;
+my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+my $bug_ids = $dbh->selectcol_arrayref(q{SELECT bug_id FROM bugs WHERE bugs.op_sys = ?}, undef, $from_os);
+my $field = Bugzilla::Field->check({ name => 'op_sys', cache => 1 });
+my $nobody = Bugzilla::User->check({ name => 'nobody@mozilla.org', cache => 1 });
+
+my $bug_count = @$bug_ids;
+if ($bug_count == 0) {
+ warn "There are no bugs to move.\n";
+ exit 1;
+}
+
+print STDERR <<EOF;
+About to move $bug_count bugs from $from_os to $to_os.
+
+Press <Ctrl-C> to stop or <Enter> to continue...
+EOF
+getc();
+
+$dbh->bz_start_transaction;
+foreach my $bug_id (@$bug_ids) {
+ warn "Moving $bug_id...\n";
+
+ $dbh->do(q{INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added)
+ VALUES (?, ?, ?, ?, ?, ?)},
+ undef, $bug_id, $nobody->id, $timestamp, $field->id, $from_os, $to_os);
+ $dbh->do(q{UPDATE bugs SET op_sys = ?, delta_ts = ?, lastdiffed = ? WHERE bug_id = ?},
+ undef, $to_os, $timestamp, $timestamp, $bug_id);
+}
+$dbh->bz_commit_transaction;
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
+
+__END__
+
+=head1 NAME
+
+move_os.pl - move the os on all bugs with a particular os to a new os
+
+=head1 SYNOPSIS
+
+ move_os.pl --from 'Windows 8 Metro' --to 'Windows 8.1'
diff --git a/contrib/reorg-tools/movebugs.pl b/contrib/reorg-tools/movebugs.pl
new file mode 100755
index 000000000..7ffca3615
--- /dev/null
+++ b/contrib/reorg-tools/movebugs.pl
@@ -0,0 +1,182 @@
+#!/usr/bin/perl -w
+use strict;
+
+use Cwd 'abs_path';
+use File::Basename;
+use FindBin;
+use lib "$FindBin::Bin/../..";
+use lib "$FindBin::Bin/../../lib";
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::FlagType;
+use Bugzilla::Hook;
+use Bugzilla::Util;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 4) {
+ die <<USAGE;
+Usage: movebugs.pl <old-product> <old-component> <new-product> <new-component>
+
+Eg. movebugs.pl mozilla.org bmo bugzilla.mozilla.org admin
+Will move all bugs in the mozilla.org:bmo component to the
+bugzilla.mozilla.org:admin component.
+
+The new product must have matching versions, milestones, and flags from the old
+product (will be validated by this script).
+USAGE
+}
+
+my ($old_product, $old_component, $new_product, $new_component) = @ARGV;
+
+my $dbh = Bugzilla->dbh;
+
+my $old_product_id = $dbh->selectrow_array(
+ "SELECT id FROM products WHERE name=?",
+ undef, $old_product);
+$old_product_id
+ or die "Can't find product ID for '$old_product'.\n";
+
+my $old_component_id = $dbh->selectrow_array(
+ "SELECT id FROM components WHERE name=? AND product_id=?",
+ undef, $old_component, $old_product_id);
+$old_component_id
+ or die "Can't find component ID for '$old_component'.\n";
+
+my $new_product_id = $dbh->selectrow_array(
+ "SELECT id FROM products WHERE name=?",
+ undef, $new_product);
+$new_product_id
+ or die "Can't find product ID for '$new_product'.\n";
+
+my $new_component_id = $dbh->selectrow_array(
+ "SELECT id FROM components WHERE name=? AND product_id=?",
+ undef, $new_component, $new_product_id);
+$new_component_id
+ or die "Can't find component ID for '$new_component'.\n";
+
+my $product_field_id = $dbh->selectrow_array(
+ "SELECT id FROM fielddefs WHERE name = 'product'");
+$product_field_id
+ or die "Can't find field ID for 'product' field\n";
+my $component_field_id = $dbh->selectrow_array(
+ "SELECT id FROM fielddefs WHERE name = 'component'");
+$component_field_id
+ or die "Can't find field ID for 'component' field\n";
+
+my $user_id = $dbh->selectrow_array(
+ "SELECT userid FROM profiles WHERE login_name='nobody\@mozilla.org'");
+$user_id
+ or die "Can't find user ID for 'nobody\@mozilla.org'\n";
+
+$dbh->bz_start_transaction();
+
+# build list of bugs
+my $ra_ids = $dbh->selectcol_arrayref(
+ "SELECT bug_id FROM bugs WHERE product_id=? AND component_id=?",
+ undef, $old_product_id, $old_component_id);
+my $bug_count = scalar @$ra_ids;
+$bug_count
+ or die "No bugs were found in '$old_component'\n";
+my $where_sql = 'bug_id IN (' . join(',', @$ra_ids) . ')';
+
+# check versions
+my @missing_versions;
+my $ra_versions = $dbh->selectcol_arrayref(
+ "SELECT DISTINCT version FROM bugs WHERE $where_sql");
+foreach my $version (@$ra_versions) {
+ my $has_version = $dbh->selectrow_array(
+ "SELECT 1 FROM versions WHERE product_id=? AND value=?",
+ undef, $new_product_id, $version);
+ push @missing_versions, $version unless $has_version;
+}
+
+# check milestones
+my @missing_milestones;
+my $ra_milestones = $dbh->selectcol_arrayref(
+ "SELECT DISTINCT target_milestone FROM bugs WHERE $where_sql");
+foreach my $milestone (@$ra_milestones) {
+ my $has_milestone = $dbh->selectrow_array(
+ "SELECT 1 FROM milestones WHERE product_id=? AND value=?",
+ undef, $new_product_id, $milestone);
+ push @missing_milestones, $milestone unless $has_milestone;
+}
+
+# check flags
+my @missing_flags;
+my $ra_old_types = $dbh->selectcol_arrayref(
+ "SELECT DISTINCT type_id
+ FROM flags
+ INNER JOIN flagtypes ON flagtypes.id = flags.type_id
+ WHERE $where_sql");
+my $ra_new_types =
+ Bugzilla::FlagType::match({ product_id => $new_product_id,
+ component_id => $new_component_id });
+foreach my $old_type (@$ra_old_types) {
+ unless (grep { $_->id == $old_type } @$ra_new_types) {
+ my $flagtype = Bugzilla::FlagType->new($old_type);
+ push @missing_flags, $flagtype->name . ' (' . $flagtype->target_type . ')';
+ }
+}
+
+# show missing
+my $missing_error = '';
+if (@missing_versions) {
+ $missing_error .= "'$new_product' is missing the following version(s):\n " .
+ join("\n ", @missing_versions) . "\n";
+}
+if (@missing_milestones) {
+ $missing_error .= "'$new_product' is missing the following milestone(s):\n " .
+ join("\n ", @missing_milestones) . "\n";
+}
+if (@missing_flags) {
+ $missing_error .= "'$new_product'::'$new_component' is missing the following flag(s):\n " .
+ join("\n ", @missing_flags) . "\n";
+}
+die $missing_error if $missing_error;
+
+# confirmation
+print <<EOF;
+About to move $bug_count bugs
+From '$old_product' : '$old_component'
+To '$new_product' : '$new_component'
+
+Press <Ctrl-C> to stop or <Enter> to continue...
+EOF
+getc();
+
+print "Moving $bug_count bugs from $old_product:$old_component to $new_product:$new_component\n";
+
+# update bugs
+$dbh->do(
+ "UPDATE bugs SET product_id=?, component_id=? WHERE $where_sql",
+ undef, $new_product_id, $new_component_id);
+
+# touch bugs
+$dbh->do("UPDATE bugs SET delta_ts=NOW() WHERE $where_sql");
+$dbh->do("UPDATE bugs SET lastdiffed=NOW() WHERE $where_sql");
+
+# update bugs_activity
+$dbh->do(
+ "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added)
+ SELECT bug_id, ?, delta_ts, ?, ?, ? FROM bugs WHERE $where_sql",
+ undef,
+ $user_id, $product_field_id, $old_product, $new_product);
+$dbh->do(
+ "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added)
+ SELECT bug_id, ?, delta_ts, ?, ?, ? FROM bugs WHERE $where_sql",
+ undef,
+ $user_id, $component_field_id, $old_component, $new_component);
+
+Bugzilla::Hook::process('reorg_move_bugs', { bug_ids => $ra_ids } );
+
+$dbh->bz_commit_transaction();
+
+foreach my $bug_id (@$ra_ids) {
+ Bugzilla->memcached->clear({ table => 'bugs', id => $bug_id });
+}
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
diff --git a/contrib/reorg-tools/movecomponent.pl b/contrib/reorg-tools/movecomponent.pl
new file mode 100755
index 000000000..bdbfdc976
--- /dev/null
+++ b/contrib/reorg-tools/movecomponent.pl
@@ -0,0 +1,145 @@
+#!/usr/bin/perl -w
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+
+use FindBin '$RealBin';
+use lib "$RealBin/../..", "$RealBin/../../lib";
+
+use Bugzilla;
+use Bugzilla::Component;
+use Bugzilla::Constants;
+use Bugzilla::Field;
+use Bugzilla::Hook;
+use Bugzilla::Product;
+use Bugzilla::Util;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 3) {
+ die <<USAGE;
+Usage: movecomponent.pl <oldproduct> <newproduct> <component>
+
+E.g.: movecomponent.pl ReplicationEngine FoodReplicator SeaMonkey
+will move the component "SeaMonkey" from the product "ReplicationEngine"
+to the product "FoodReplicator".
+
+Important: You must make sure the milestones and versions of the bugs in the
+component are available in the new product. See syncmsandversions.pl.
+
+USAGE
+}
+
+my ($old_product_name, $new_product_name, $component_name) = @ARGV;
+my $old_product = Bugzilla::Product->check({ name => $old_product_name });
+my $new_product = Bugzilla::Product->check({ name => $new_product_name });
+my $component = Bugzilla::Component->check({ product => $old_product, name => $component_name });
+my $field_id = get_field_id('product');
+
+my $dbh = Bugzilla->dbh;
+
+# check versions
+my @missing_versions;
+my $ra_versions = $dbh->selectcol_arrayref(
+ "SELECT DISTINCT version FROM bugs WHERE component_id = ?",
+ undef, $component->id);
+foreach my $version (@$ra_versions) {
+ my $has_version = $dbh->selectrow_array(
+ "SELECT 1 FROM versions WHERE product_id = ? AND value = ?",
+ undef, $new_product->id, $version);
+ push @missing_versions, $version unless $has_version;
+}
+
+# check milestones
+my @missing_milestones;
+my $ra_milestones = $dbh->selectcol_arrayref(
+ "SELECT DISTINCT target_milestone FROM bugs WHERE component_id = ?",
+ undef, $component->id);
+foreach my $milestone (@$ra_milestones) {
+ my $has_milestone = $dbh->selectrow_array(
+ "SELECT 1 FROM milestones WHERE product_id=? AND value=?",
+ undef, $new_product->id, $milestone);
+ push @missing_milestones, $milestone unless $has_milestone;
+}
+
+my $missing_error = '';
+if (@missing_versions) {
+ $missing_error .= "'$new_product_name' is missing the following version(s):\n " .
+ join("\n ", @missing_versions) . "\n";
+}
+if (@missing_milestones) {
+ $missing_error .= "'$new_product_name' is missing the following milestone(s):\n " .
+ join("\n ", @missing_milestones) . "\n";
+}
+die $missing_error if $missing_error;
+
+# confirmation
+print <<EOF;
+About to move the component '$component_name'
+From '$old_product_name'
+To '$new_product_name'
+
+Press <Ctrl-C> to stop or <Enter> to continue...
+EOF
+getc();
+
+print "Moving '$component_name' from '$old_product_name' to '$new_product_name'...\n\n";
+$dbh->bz_start_transaction();
+
+my $ra_ids = $dbh->selectcol_arrayref(
+ "SELECT bug_id FROM bugs WHERE product_id=? AND component_id=?",
+ undef, $old_product->id, $component->id);
+
+# Bugs table
+$dbh->do("UPDATE bugs SET product_id = ? WHERE component_id = ?",
+ undef,
+ ($new_product->id, $component->id));
+
+# Flags tables
+$dbh->do("UPDATE flaginclusions SET product_id = ? WHERE component_id = ?",
+ undef,
+ ($new_product->id, $component->id));
+
+$dbh->do("UPDATE flagexclusions SET product_id = ? WHERE component_id = ?",
+ undef,
+ ($new_product->id, $component->id));
+
+# Components
+$dbh->do("UPDATE components SET product_id = ? WHERE id = ?",
+ undef,
+ ($new_product->id, $component->id));
+
+Bugzilla::Hook::process('reorg_move_component', {
+ old_product => $old_product,
+ new_product => $new_product,
+ component => $component,
+} );
+
+# Mark bugs as touched
+$dbh->do("UPDATE bugs SET delta_ts = NOW()
+ WHERE component_id = ?", undef, $component->id);
+$dbh->do("UPDATE bugs SET lastdiffed = NOW()
+ WHERE component_id = ?", undef, $component->id);
+
+# Update bugs_activity
+my $userid = 1; # nobody@mozilla.org
+
+$dbh->do("INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed,
+ added)
+ SELECT bug_id, ?, delta_ts, ?, ?, ?
+ FROM bugs WHERE component_id = ?",
+ undef,
+ ($userid, $field_id, $old_product_name, $new_product_name, $component->id));
+
+Bugzilla::Hook::process('reorg_move_bugs', { bug_ids => $ra_ids } );
+
+$dbh->bz_commit_transaction();
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
diff --git a/contrib/reorg-tools/reassign_open_bugs.pl b/contrib/reorg-tools/reassign_open_bugs.pl
new file mode 100755
index 000000000..6496f9a95
--- /dev/null
+++ b/contrib/reorg-tools/reassign_open_bugs.pl
@@ -0,0 +1,87 @@
+#!/usr/bin/perl
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin '$RealBin';
+use lib "$RealBin/../..", "$RealBin/../../lib";
+
+use Bugzilla;
+use Bugzilla::User;
+use Bugzilla::Constants;
+
+use Getopt::Long qw( :config gnu_getopt );
+use Pod::Usage;
+
+# Load extensions for monkeypatched $user->clear_last_statistics_ts()
+BEGIN { Bugzilla->extensions(); }
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my ($from, $to);
+GetOptions(
+ "from|f=s" => \$from,
+ "to|t=s" => \$to,
+);
+
+pod2usage(1) unless defined $from && defined $to;
+
+my $dbh = Bugzilla->dbh;
+
+my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+my $field = Bugzilla::Field->check({ name => 'assigned_to', cache => 1 });
+my $from_user = Bugzilla::User->check({ name => $from, cache => 1 });
+my $to_user = Bugzilla::User->check({ name => $to, cache => 1 });
+
+my $bugs = $dbh->selectcol_arrayref(q{SELECT bug_id
+ FROM bugs
+ LEFT JOIN bug_status
+ ON bug_status.value = bugs.bug_status
+ WHERE bug_status.is_open = 1
+ AND bugs.assigned_to = ?}, undef, $from_user->id);
+my $bug_count = @$bugs;
+if ($bug_count == 0) {
+ warn "There are no bugs to move.\n";
+ exit 1;
+}
+
+print STDERR <<EOF;
+About to move $bug_count bugs from $from to $to.
+
+Press <Ctrl-C> to stop or <Enter> to continue...
+EOF
+getc();
+
+$dbh->bz_start_transaction;
+foreach my $bug_id (@$bugs) {
+ warn "Updating bug $bug_id\n";
+ $dbh->do(q{INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added)
+ VALUES (?, ?, ?, ?, ?, ?)},
+ undef, $bug_id, $to_user->id, $timestamp, $field->id, $from_user->login, $to_user->login);
+ $dbh->do(q{UPDATE bugs SET assigned_to = ?, delta_ts = ?, lastdiffed = ? WHERE bug_id = ?},
+ undef, $to_user->id, $timestamp, $timestamp, $bug_id);
+}
+$from_user->clear_last_statistics_ts();
+$to_user->clear_last_statistics_ts();
+$dbh->bz_commit_transaction;
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
+
+__END__
+
+=head1 NAME
+
+reassign-open-bugs.pl - reassign all open bugs from one user to another.
+
+=head1 SYNOPSIS
+
+ reassign-open-bugs.pl --from general@js.bugs --to nobody@mozilla.org
diff --git a/contrib/reorg-tools/reset_default_user.pl b/contrib/reorg-tools/reset_default_user.pl
new file mode 100755
index 000000000..173d03849
--- /dev/null
+++ b/contrib/reorg-tools/reset_default_user.pl
@@ -0,0 +1,145 @@
+#!/usr/bin/perl -wT
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+
+use lib '.';
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::User;
+use Bugzilla::Field;
+use Bugzilla::Util qw(trick_taint);
+
+use Getopt::Long;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my $dbh = Bugzilla->dbh;
+
+my $field_name = "";
+my $product = "";
+my $component = "";
+my $help = "";
+my %user_cache = ();
+
+my $result = GetOptions('field=s' => \$field_name,
+ 'product=s' => \$product,
+ 'component=s' => \$component,
+ 'help|h' => \$help);
+
+sub usage {
+ print <<USAGE;
+Usage: reset_default_user.pl --field <fieldname> --product <product> [--component <component>] [--help]
+
+This script will load all bugs matching the product, and optionally component,
+and reset the default user value back to the default value for the component.
+Valid field names are assigned_to and qa_contact.
+USAGE
+}
+
+if (!$product || $help
+ || ($field_name ne 'assigned_to' && $field_name ne 'qa_contact'))
+{
+ usage();
+ exit(1);
+}
+
+# We will need these for entering into bugs_activity
+my $who = Bugzilla::User->new({ name => 'nobody@mozilla.org' });
+my $field = Bugzilla::Field->new({ name => $field_name });
+
+trick_taint($product);
+my $product_id = $dbh->selectrow_array(
+ "SELECT id FROM products WHERE name = ?",
+ undef, $product);
+$product_id or die "Can't find product ID for '$product'.\n";
+
+my $component_id;
+my $default_user_id;
+if ($component) {
+ trick_taint($component);
+ my $colname = $field->name eq 'qa_contact'
+ ? 'initialqacontact'
+ : 'initialowner';
+ ($component_id, $default_user_id) = $dbh->selectrow_array(
+ "SELECT id, $colname FROM components " .
+ "WHERE name = ? AND product_id = ?",
+ undef, $component, $product_id);
+ $component_id or die "Can't find component ID for '$component'.\n";
+ $user_cache{$default_user_id} ||= Bugzilla::User->new($default_user_id);
+}
+
+# build list of bugs
+my $bugs_query = "SELECT bug_id, qa_contact, component_id " .
+ "FROM bugs WHERE product_id = ?";
+my @args = ($product_id);
+
+if ($component_id) {
+ $bugs_query .= " AND component_id = ? AND qa_contact != ?";
+ push(@args, $component_id, $default_user_id);
+}
+
+my $bugs = $dbh->selectall_arrayref($bugs_query, {Slice => {}}, @args);
+my $bug_count = scalar @$bugs;
+$bug_count
+ or die "No bugs were found.\n";
+
+# confirmation
+print <<EOF;
+About to reset $field_name for $bug_count bugs.
+
+Press <Ctrl-C> to stop or <Enter> to continue...
+EOF
+getc();
+
+$dbh->bz_start_transaction();
+
+foreach my $bug (@$bugs) {
+ my $bug_id = $bug->{bug_id};
+ my $old_user_id = $bug->{$field->name};
+ my $old_comp_id = $bug->{component_id};
+
+ # If only changing one component, we already have the default user id
+ my $new_user_id;
+ if ($default_user_id) {
+ $new_user_id = $default_user_id;
+ }
+ else {
+ my $colname = $field->name eq 'qa_contact'
+ ? 'initialqacontact'
+ : 'initialowner';
+ $new_user_id = $dbh->selectrow_array(
+ "SELECT $colname FROM components WHERE id = ?",
+ undef, $old_comp_id);
+ }
+
+ if ($old_user_id != $new_user_id) {
+ print "Resetting " . $field->name . " for bug $bug_id ...";
+
+ # Use the cached version if already exists
+ my $old_user = $user_cache{$old_user_id} ||= Bugzilla::User->new($old_user_id);
+ my $new_user = $user_cache{$new_user_id} ||= Bugzilla::User->new($new_user_id);
+
+ my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+
+ $dbh->do("UPDATE bugs SET " . $field->name . " = ? WHERE bug_id = ?",
+ undef, $new_user_id, $bug_id);
+ $dbh->do("INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) " .
+ "VALUES (?, ?, ?, ?, ?, ?)",
+ undef, $bug_id, $who->id, $timestamp, $field->id, $old_user->login, $new_user->login);
+ $dbh->do("UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?",
+ undef, $timestamp, $timestamp, $bug_id);
+
+ Bugzilla->memcached->clear({ table => 'bugs', id => $bug_id });
+
+ print "done.\n";
+ }
+}
+
+$dbh->bz_commit_transaction();
diff --git a/contrib/reorg-tools/syncflags.pl b/contrib/reorg-tools/syncflags.pl
new file mode 100755
index 000000000..8e039f7bb
--- /dev/null
+++ b/contrib/reorg-tools/syncflags.pl
@@ -0,0 +1,88 @@
+#!/usr/bin/perl -w
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Gervase Markham <gerv@gerv.net>
+
+# See also https://bugzilla.mozilla.org/show_bug.cgi?id=119569
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+
+sub usage() {
+ print <<USAGE;
+Usage: syncflags.pl <srcproduct> <tgtproduct>
+
+E.g.: syncflags.pl FoodReplicator SeaMonkey
+will copy any flag inclusions (only) for the product "FoodReplicator"
+so matching inclusions exist for the product "SeaMonkey". This script is
+normally used prior to moving components from srcproduct to tgtproduct.
+USAGE
+
+ exit(1);
+}
+
+#############################################################################
+# MAIN CODE
+#############################################################################
+
+# This is a pure command line script.
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 2) {
+ usage();
+ exit();
+}
+
+my ($srcproduct, $tgtproduct) = @ARGV;
+
+my $dbh = Bugzilla->dbh;
+
+# Find product IDs
+my $srcprodid = $dbh->selectrow_array("SELECT id FROM products WHERE name = ?",
+ undef, $srcproduct);
+if (!$srcprodid) {
+ print "Can't find product ID for '$srcproduct'.\n";
+ exit(1);
+}
+
+my $tgtprodid = $dbh->selectrow_array("SELECT id FROM products WHERE name = ?",
+ undef, $tgtproduct);
+if (!$tgtprodid) {
+ print "Can't find product ID for '$tgtproduct'.\n";
+ exit(1);
+}
+
+$dbh->do("INSERT INTO flaginclusions(component_id, type_id, product_id)
+ SELECT fi1.component_id, fi1.type_id, ? FROM flaginclusions fi1
+ LEFT JOIN flaginclusions fi2
+ ON fi1.type_id = fi2.type_id
+ AND fi2.product_id = ?
+ WHERE fi1.product_id = ?
+ AND fi2.type_id IS NULL",
+ undef,
+ $tgtprodid, $tgtprodid, $srcprodid);
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
diff --git a/contrib/reorg-tools/syncmsandversions.pl b/contrib/reorg-tools/syncmsandversions.pl
new file mode 100755
index 000000000..20e88252e
--- /dev/null
+++ b/contrib/reorg-tools/syncmsandversions.pl
@@ -0,0 +1,122 @@
+#!/usr/bin/perl -w
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Gervase Markham <gerv@gerv.net>
+
+# See also https://bugzilla.mozilla.org/show_bug.cgi?id=119569
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+
+sub usage() {
+ print <<USAGE;
+Usage: syncmsandversions.pl <srcproduct> <tgtproduct>
+
+E.g.: syncmsandversions.pl FoodReplicator SeaMonkey
+will copy any versions and milstones in the product "FoodReplicator"
+which do not exist in product "SeaMonkey" into it. This script is normally
+used prior to moving components from srcproduct to tgtproduct.
+USAGE
+
+ exit(1);
+}
+
+#############################################################################
+# MAIN CODE
+#############################################################################
+
+# This is a pure command line script.
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 2) {
+ usage();
+ exit();
+}
+
+my ($srcproduct, $tgtproduct) = @ARGV;
+
+my $dbh = Bugzilla->dbh;
+
+# Find product IDs
+my $srcprodid = $dbh->selectrow_array("SELECT id FROM products WHERE name = ?",
+ undef, $srcproduct);
+if (!$srcprodid) {
+ print "Can't find product ID for '$srcproduct'.\n";
+ exit(1);
+}
+
+my $tgtprodid = $dbh->selectrow_array("SELECT id FROM products WHERE name = ?",
+ undef, $tgtproduct);
+if (!$tgtprodid) {
+ print "Can't find product ID for '$tgtproduct'.\n";
+ exit(1);
+}
+
+$dbh->bz_start_transaction();
+
+$dbh->do("
+ INSERT INTO milestones(value, sortkey, isactive, product_id)
+ SELECT m1.value, m1.sortkey, m1.isactive, ?
+ FROM milestones m1
+ LEFT JOIN milestones m2 ON m1.value = m2.value
+ AND m2.product_id = ?
+ WHERE m1.product_id = ?
+ AND m2.value IS NULL
+ ",
+ undef,
+ $tgtprodid, $tgtprodid, $srcprodid);
+
+$dbh->do("
+ INSERT INTO versions(value, isactive, product_id)
+ SELECT v1.value, v1.isactive, ?
+ FROM versions v1
+ LEFT JOIN versions v2 ON v1.value = v2.value
+ AND v2.product_id = ?
+ WHERE v1.product_id = ?
+ AND v2.value IS NULL
+ ",
+ undef,
+ $tgtprodid, $tgtprodid, $srcprodid);
+
+$dbh->do("
+ INSERT INTO group_control_map (group_id, product_id, entry, membercontrol,
+ othercontrol, canedit, editcomponents,
+ editbugs, canconfirm)
+ SELECT g1.group_id, ?, g1.entry, g1.membercontrol, g1.othercontrol,
+ g1.canedit, g1.editcomponents, g1.editbugs, g1.canconfirm
+ FROM group_control_map g1
+ LEFT JOIN group_control_map g2 ON g1.product_id = ?
+ AND g2.product_id = ?
+ AND g1.group_id = g2.group_id
+ WHERE g1.product_id = ?
+ AND g2.group_id IS NULL
+ ",
+ undef,
+ $tgtprodid, $srcprodid, $tgtprodid, $srcprodid);
+
+$dbh->bz_commit_transaction();
+
+# It's complex to determine which items now need to be flushed from memcached.
+# As this is expected to be a rare event, we just flush the entire cache.
+Bugzilla->memcached->clear_all();
diff --git a/contrib/sanitizeme.pl b/contrib/sanitizeme.pl
new file mode 100755
index 000000000..2e7613a6e
--- /dev/null
+++ b/contrib/sanitizeme.pl
@@ -0,0 +1,214 @@
+#!/usr/bin/perl -wT
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is the Mozilla
+# Corporation. Portions created by Mozilla are
+# Copyright (C) 2006 Mozilla Foundation. All Rights Reserved.
+#
+# Contributor(s): Myk Melez <myk@mozilla.org>
+# Alex Brugh <alex@cs.umn.edu>
+# Dave Miller <justdave@mozilla.com>
+# Byron Jones <glob@mozilla.com>
+
+use strict;
+
+use lib qw(.);
+
+use Bugzilla;
+use Bugzilla::Bug;
+use Bugzilla::Constants;
+use Bugzilla::Hook;
+use Bugzilla::Util;
+use List::MoreUtils qw(uniq);
+
+use Getopt::Long;
+
+my $dbh = Bugzilla->dbh;
+
+# This SQL is designed to sanitize a copy of a Bugzilla database so that it
+# doesn't contain any information that can't be viewed from a web browser by
+# a user who is not logged in.
+
+# Last validated against Bugzilla version 4.0
+
+my ($dry_run, $from_cron, $keep_attachments, $keep_groups,
+ $keep_passwords, $keep_insider, $trace, $enable_email) = (0, 0, 0, '', 0, 0, 0, 0);
+my $keep_groups_sql = '';
+
+GetOptions(
+ "dry-run" => \$dry_run,
+ "from-cron" => \$from_cron,
+ "keep-attachments" => \$keep_attachments,
+ "keep-passwords" => \$keep_passwords,
+ "keep-insider" => \$keep_insider,
+ "keep-groups:s" => \$keep_groups,
+ "trace" => \$trace,
+ "enable-email" => \$enable_email,
+) or exit;
+
+if ($keep_groups ne '') {
+ my @groups;
+ foreach my $group_id (split(/\s*,\s*/, $keep_groups)) {
+ my $group;
+ if ($group_id =~ /\D/) {
+ $group = Bugzilla::Group->new({ name => $group_id });
+ } else {
+ $group = Bugzilla::Group->new($group_id);
+ }
+ die "Invalid group '$group_id'\n" unless $group;
+ push @groups, $group->id;
+ }
+ $keep_groups_sql = "NOT IN (" . join(",", @groups) . ")";
+}
+
+$dbh->{TraceLevel} = 1 if $trace;
+
+if ($dry_run) {
+ print "** dry run : no changes to the database will be made **\n";
+ $dbh->bz_start_transaction();
+}
+eval {
+ delete_non_public_products();
+ delete_secure_bugs();
+ delete_deleted_comments();
+ delete_insider_comments() unless $keep_insider;
+ delete_security_groups();
+ delete_sensitive_user_data();
+ delete_attachment_data() unless $keep_attachments;
+ delete_bug_user_last_visit();
+ Bugzilla::Hook::process('db_sanitize');
+ disable_email_delivery() unless $enable_email;
+ print "All done!\n";
+ $dbh->bz_rollback_transaction() if $dry_run;
+};
+if ($@) {
+ $dbh->bz_rollback_transaction() if $dry_run;
+ die "$@" if $@;
+}
+
+sub delete_non_public_products {
+ # Delete all non-public products, and all data associated with them
+ my @products = Bugzilla::Product->get_all();
+ my $mandatory = CONTROLMAPMANDATORY;
+ foreach my $product (@products) {
+ # if there are any mandatory groups on the product, nuke it and
+ # everything associated with it (including the bugs)
+ Bugzilla->params->{'allowbugdeletion'} = 1; # override this in memory for now
+ my $mandatorygroups = $dbh->selectcol_arrayref("SELECT group_id FROM group_control_map WHERE product_id = ? AND (membercontrol = $mandatory)", undef, $product->id);
+ if (0 < scalar(@$mandatorygroups)) {
+ print "Deleting product '" . $product->name . "'...\n";
+ $product->remove_from_db();
+ }
+ }
+}
+
+sub delete_secure_bugs {
+ # Delete all data for bugs in security groups.
+ my $buglist = $dbh->selectall_arrayref(
+ $keep_groups
+ ? "SELECT DISTINCT bug_id FROM bug_group_map WHERE group_id $keep_groups_sql"
+ : "SELECT DISTINCT bug_id FROM bug_group_map"
+ );
+ $|=1; # disable buffering so the bug progress counter works
+ my $numbugs = scalar(@$buglist);
+ my $bugnum = 0;
+ print "Deleting $numbugs bugs in " . ($keep_groups ? 'non-' : '') . "security groups...\n";
+ foreach my $row (@$buglist) {
+ my $bug_id = $row->[0];
+ $bugnum++;
+ print "\r$bugnum/$numbugs" unless $from_cron;
+ my $bug = new Bugzilla::Bug($bug_id);
+ $bug->remove_from_db();
+ }
+ print "\rDone \n" unless $from_cron;
+}
+
+sub delete_deleted_comments {
+ # Delete all comments tagged as 'deleted'
+ my $comment_ids = $dbh->selectcol_arrayref("SELECT comment_id FROM longdescs_tags WHERE tag='deleted'");
+ return unless @$comment_ids;
+ print "Deleting 'deleted' comments...\n";
+ my @bug_ids = uniq @{
+ $dbh->selectcol_arrayref("SELECT bug_id FROM longdescs WHERE comment_id IN (" . join(',', @$comment_ids) . ")")
+ };
+ $dbh->do("DELETE FROM longdescs WHERE comment_id IN (" . join(',', @$comment_ids) . ")");
+ foreach my $bug_id (@bug_ids) {
+ Bugzilla::Bug->new($bug_id)->_sync_fulltext(update_comments => 1);
+ }
+}
+
+sub delete_insider_comments {
+ # Delete all 'insidergroup' comments and attachments
+ print "Deleting 'insidergroup' comments and attachments...\n";
+ $dbh->do("DELETE FROM longdescs WHERE isprivate = 1");
+ $dbh->do("DELETE attach_data FROM attachments JOIN attach_data ON attachments.attach_id = attach_data.id WHERE attachments.isprivate = 1");
+ $dbh->do("DELETE FROM attachments WHERE isprivate = 1");
+ $dbh->do("UPDATE bugs_fulltext SET comments = comments_noprivate");
+}
+
+sub delete_security_groups {
+ # Delete all security groups.
+ print "Deleting " . ($keep_groups ? 'non-' : '') . "security groups...\n";
+ $dbh->do("DELETE user_group_map FROM groups JOIN user_group_map ON groups.id = user_group_map.group_id WHERE groups.isbuggroup = 1");
+ $dbh->do("DELETE group_group_map FROM groups JOIN group_group_map ON (groups.id = group_group_map.member_id OR groups.id = group_group_map.grantor_id) WHERE groups.isbuggroup = 1");
+ $dbh->do("DELETE group_control_map FROM groups JOIN group_control_map ON groups.id = group_control_map.group_id WHERE groups.isbuggroup = 1");
+ $dbh->do("UPDATE flagtypes LEFT JOIN groups ON flagtypes.grant_group_id = groups.id SET grant_group_id = NULL WHERE groups.isbuggroup = 1");
+ $dbh->do("UPDATE flagtypes LEFT JOIN groups ON flagtypes.request_group_id = groups.id SET request_group_id = NULL WHERE groups.isbuggroup = 1");
+ if ($keep_groups) {
+ $dbh->do("DELETE FROM groups WHERE isbuggroup = 1 AND id $keep_groups_sql");
+ } else {
+ $dbh->do("DELETE FROM groups WHERE isbuggroup = 1");
+ }
+}
+
+sub delete_sensitive_user_data {
+ # Remove sensitive user account data.
+ print "Deleting sensitive user account data...\n";
+ $dbh->do("UPDATE profiles SET cryptpassword = 'deleted'") unless $keep_passwords;
+ $dbh->do("DELETE FROM profiles_activity");
+ $dbh->do("DELETE FROM profile_search");
+ $dbh->do("DELETE FROM namedqueries");
+ $dbh->do("DELETE FROM tokens");
+ $dbh->do("DELETE FROM logincookies");
+ $dbh->do("DELETE FROM login_failure");
+ $dbh->do("DELETE FROM audit_log");
+ # queued bugmail
+ $dbh->do("DELETE FROM ts_error");
+ $dbh->do("DELETE FROM ts_exitstatus");
+ $dbh->do("DELETE FROM ts_funcmap");
+ $dbh->do("DELETE FROM ts_job");
+ $dbh->do("DELETE FROM ts_note");
+}
+
+sub delete_attachment_data {
+ # Delete unnecessary attachment data.
+ print "Removing attachment data to preserve disk space...\n";
+ $dbh->do("UPDATE attach_data SET thedata = ''");
+}
+
+sub delete_bug_user_last_visit {
+ print "Removing all entries from bug_user_last_visit...\n";
+ $dbh->do('TRUNCATE TABLE bug_user_last_visit');
+}
+
+sub disable_email_delivery {
+ # turn off email delivery for all users.
+ print "Turning off email delivery...\n";
+ $dbh->do("UPDATE profiles SET disable_mail = 1");
+
+ # Also clear out the default flag cc as well since they do not
+ # have to be in the profiles table
+ $dbh->do("UPDATE flagtypes SET cc_list = NULL");
+}
diff --git a/contrib/sendbugmail.pl b/contrib/sendbugmail.pl
index 51de9407d..d0c7d63b7 100755
--- a/contrib/sendbugmail.pl
+++ b/contrib/sendbugmail.pl
@@ -1,4 +1,4 @@
-#!/usr/bin/perl -w
+#!/usr/bin/perl -wT
#
# sendbugmail.pl
#
@@ -12,6 +12,8 @@
#
# Usage: perl -T contrib/sendbugmail.pl bug_id user_email
+use 5.10.1;
+use strict;
use lib qw(. lib);
use Bugzilla;
@@ -22,7 +24,7 @@ use Bugzilla::User;
my $dbh = Bugzilla->dbh;
sub usage {
- print STDERR "Usage: $0 bug_id user_email\n";
+ say STDERR "Usage: $0 bug_id user_email";
exit;
}
@@ -36,7 +38,7 @@ my $changer = $ARGV[1];
# Validate the bug number.
if (!($bugnum =~ /^(\d+)$/)) {
- print STDERR "Bug number \"$bugnum\" not numeric.\n";
+ say STDERR "Bug number \"$bugnum\" not numeric.";
usage();
}
@@ -46,19 +48,19 @@ my ($id) = $dbh->selectrow_array("SELECT bug_id FROM bugs WHERE bug_id = ?",
undef, $bugnum);
if (!$id) {
- print STDERR "Bug number $bugnum does not exist.\n";
+ say STDERR "Bug number $bugnum does not exist.";
usage();
}
# Validate the changer address.
my $match = Bugzilla->params->{'emailregexp'};
if ($changer !~ /$match/) {
- print STDERR "Changer \"$changer\" doesn't match email regular expression.\n";
+ say STDERR "Changer \"$changer\" doesn't match email regular expression.";
usage();
}
my $changer_user = new Bugzilla::User({ name => $changer });
unless ($changer_user) {
- print STDERR "\"$changer\" is not a valid user.\n";
+ say STDERR "\"$changer\" is not a valid user.";
usage();
}
@@ -67,26 +69,15 @@ my $outputref = Bugzilla::BugMail::Send($bugnum, {'changer' => $changer_user });
# Report the results.
my $sent = scalar(@{$outputref->{sent}});
-my $excluded = scalar(@{$outputref->{excluded}});
if ($sent) {
- print "email sent to $sent recipients:\n";
+ say "email sent to $sent recipients:";
} else {
- print "No email sent.\n";
+ say "No email sent.";
}
foreach my $sent (@{$outputref->{sent}}) {
- print " $sent\n";
-}
-
-if ($excluded) {
- print "$excluded recipients excluded:\n";
-} else {
- print "No recipients excluded.\n";
-}
-
-foreach my $excluded (@{$outputref->{excluded}}) {
- print " $excluded\n";
+ say " $sent";
}
# This document is copyright (C) 2004 Perforce Software, Inc. All rights
diff --git a/contrib/sendunsentbugmail.pl b/contrib/sendunsentbugmail.pl
index 6ddbd2e4c..7771cfeff 100755
--- a/contrib/sendunsentbugmail.pl
+++ b/contrib/sendunsentbugmail.pl
@@ -21,8 +21,8 @@
# Contributor(s): Dave Miller <justdave@bugzilla.org>
# Myk Melez <myk@mozilla.org>
+use 5.10.1;
use strict;
-
use lib qw(. lib);
use Bugzilla;
@@ -40,28 +40,21 @@ my $list = $dbh->selectcol_arrayref(
' ORDER BY bug_id');
if (scalar(@$list) > 0) {
- print "OK, now attempting to send unsent mail\n";
- print scalar(@$list) . " bugs found with possibly unsent mail.\n\n";
+ say "OK, now attempting to send unsent mail";
+ say scalar(@$list) . " bugs found with possibly unsent mail.\n";
foreach my $bugid (@$list) {
my $start_time = time;
- print "Sending mail for bug $bugid...\n";
+ say "Sending mail for bug $bugid...";
my $outputref = Bugzilla::BugMail::Send($bugid);
if ($ARGV[0] && $ARGV[0] eq "--report") {
- print "Mail sent to:\n";
- foreach (sort @{$outputref->{sent}}) {
- print $_ . "\n";
- }
-
- print "Excluded:\n";
- foreach (sort @{$outputref->{excluded}}) {
- print $_ . "\n";
- }
+ say "Mail sent to:";
+ say $_ foreach (sort @{$outputref->{sent}});
}
else {
- my ($sent, $excluded) = (scalar(@{$outputref->{sent}}),scalar(@{$outputref->{excluded}}));
- print "$sent mails sent, $excluded people excluded.\n";
- print "Took " . (time - $start_time) . " seconds.\n\n";
- }
+ my $sent = scalar @{$outputref->{sent}};
+ say "$sent mails sent.";
+ say "Took " . (time - $start_time) . " seconds.\n";
+ }
}
- print "Unsent mail has been sent.\n";
+ say "Unsent mail has been sent.";
}
diff --git a/contrib/verify-user.pl b/contrib/verify-user.pl
new file mode 100755
index 000000000..d12cd745f
--- /dev/null
+++ b/contrib/verify-user.pl
@@ -0,0 +1,129 @@
+#!/usr/bin/perl -wT
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Myk Melez <myk@mozilla.org>
+# Dave Miller <justdave@bugzilla.org>
+
+# See if a user account has ever done anything
+
+# ./verify-user.pl foo@baz.com
+
+use strict;
+
+use lib qw(.);
+
+use Bugzilla;
+use Bugzilla::Util;
+use Bugzilla::DB;
+use Bugzilla::Constants;
+
+# Make sure accounts were specified on the command line and exist.
+my $user = $ARGV[0] || die "You must specify an user.\n";
+my $dbh = Bugzilla->dbh;
+my $sth;
+
+#$sth = $dbh->prepare("SELECT name, count(*) as qty from bugs, products where reporter=198524 and product_id=products.id group by name order by qty desc");
+#$sth->execute();
+#my $results = $sth->fetchall_arrayref();
+#use Data::Dumper;
+#print Data::Dumper::Dumper($results);
+#exit;
+
+trick_taint($user);
+if ($user =~ /^\d+$/) { # user ID passed instead of email
+ $sth = $dbh->prepare('SELECT login_name FROM profiles WHERE userid = ?');
+ $sth->execute($user);
+ ($user) = $sth->fetchrow_array || die "The user with ID $ARGV[0] does not exist.\n";
+ print "User $ARGV[0]'s login name is $user.\n";
+}
+$sth = $dbh->prepare("SELECT userid FROM profiles WHERE login_name = ?");
+$sth->execute($user);
+my ($user_id) = $sth->fetchrow_array || die "The user $user does not exist.\n";
+
+print "${user}'s ID is $user_id.\n";
+
+$sth = $dbh->prepare("SELECT DISTINCT ipaddr FROM logincookies WHERE userid = ?");
+$sth->execute($user_id);
+my $iplist = $sth->fetchall_arrayref;
+if (@$iplist > 0) {
+ print "This user has recently connected from the following IP addresses:\n";
+ foreach my $ip (@$iplist) {
+ print $$ip[0] . "\n";
+ }
+}
+
+
+# A list of tables and columns to be checked.
+my $columns = {
+ attachments => ['submitter_id'] ,
+ bugs => ['assigned_to', 'reporter', 'qa_contact'] ,
+ bugs_activity => ['who'] ,
+ cc => ['who'] ,
+ components => ['initialowner', 'initialqacontact'] ,
+ flags => ['setter_id', 'requestee_id'] ,
+ logincookies => ['userid'] ,
+ longdescs => ['who'] ,
+ namedqueries => ['userid'] ,
+ profiles_activity => ['userid', 'who'] ,
+ quips => ['userid'] ,
+ series => ['creator'] ,
+ tokens => ['userid'] ,
+ user_group_map => ['user_id'] ,
+ votes => ['who'] ,
+ watch => ['watcher', 'watched'] ,
+
+};
+
+my $fields = 0;
+# Check records for user.
+foreach my $table (keys(%$columns)) {
+ foreach my $column (@{$columns->{$table}}) {
+ $sth = $dbh->prepare("SELECT COUNT(*) FROM $table WHERE $column = ?");
+ if ($table eq 'user_group_map') {
+ $sth = $dbh->prepare("SELECT COUNT(*) FROM $table WHERE $column = ? AND grant_type = " . GRANT_DIRECT);
+ }
+ $sth->execute($user_id);
+ my ($val) = $sth->fetchrow_array;
+ $fields++ if $val;
+ print "$table.$column: $val\n" if $val;
+ }
+}
+
+print "The user is mentioned in $fields fields.\n";
+
+if ($::ARGV[1] && $::ARGV[1] eq '-r') {
+ if ($fields == 0) {
+ $sth = $dbh->prepare("SELECT login_name FROM profiles WHERE login_name = ?");
+ my $count = 0;
+ print "Finding an unused recycle ID";
+ do {
+ $count++;
+ $sth->execute(sprintf("reuseme%03d\@bugzilla.org", $count));
+ print ".";
+ } while (my ($match) = $sth->fetchrow_array());
+ printf "\nUsing reuseme%03d\@bugzilla.org.\n", $count;
+ $dbh->do("DELETE FROM user_group_map WHERE user_id=?",undef,$user_id);
+ $dbh->do("UPDATE profiles SET realname='', cryptpassword='randomgarbage' WHERE userid=?",undef,$user_id);
+ $dbh->do("UPDATE profiles SET login_name=? WHERE userid=?",undef,sprintf("reuseme%03d\@bugzilla.org",$count),$user_id);
+ }
+ else {
+ print "Account has been used, so not recycling.\n";
+ }
+}
diff --git a/describecomponents.cgi b/describecomponents.cgi
index ee1361284..ed1f2388c 100755
--- a/describecomponents.cgi
+++ b/describecomponents.cgi
@@ -41,7 +41,9 @@ print $cgi->header();
# This script does nothing but displaying mostly static data.
Bugzilla->switch_to_shadow_db;
-my $product_name = trim($cgi->param('product') || '');
+my $product_name = trim($cgi->param('product') || '');
+my $component_mark = trim($cgi->param('component') || '');
+
my $product = new Bugzilla::Product({'name' => $product_name});
unless ($product && $user->can_access_product($product->name)) {
@@ -82,7 +84,8 @@ unless ($product && $user->can_access_product($product->name)) {
# End Data/Security Validation
######################################################################
-$vars->{'product'} = $product;
+$vars->{'product'} = $product;
+$vars->{'component_mark'} = $component_mark;
$template->process("reports/components.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
diff --git a/describekeywords.cgi b/describekeywords.cgi
index 9796b77d5..b8ed9bb48 100755
--- a/describekeywords.cgi
+++ b/describekeywords.cgi
@@ -38,7 +38,17 @@ my $vars = {};
# Run queries against the shadow DB.
Bugzilla->switch_to_shadow_db;
-$vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
+# Hide bug counts for security keywords from users who aren't a member of the
+# security group
+my $can_see_security = Bugzilla->user->in_group('security-group');
+my $keywords = Bugzilla::Keyword->get_all_with_bug_count();
+foreach my $keyword (@$keywords) {
+ $keyword->{'bug_count'} = 0
+ if $keyword->name =~ /^(?:sec|csec|wsec|opsec)-/
+ && !$can_see_security;
+}
+
+$vars->{'keywords'} = $keywords;
$vars->{'caneditkeywords'} = Bugzilla->user->in_group("editkeywords");
print Bugzilla->cgi->header();
diff --git a/docs/en/xml/using.xml b/docs/en/xml/using.xml
index 53766ef34..05b415021 100644
--- a/docs/en/xml/using.xml
+++ b/docs/en/xml/using.xml
@@ -1409,6 +1409,15 @@
their <quote>Field/recipient specific options</quote> setting.
</para>
+ <para>
+ The <quote>Ignore Bugs</quote> section lets you specify a
+ comma-separated list of bugs from which you never want to get any
+ email notification of any kind. Removing a bug from this list will
+ re-enable email notification for this bug. This is especially useful
+ e.g. if you are the reporter of a very noisy bug which you are not
+ interested in anymore or if you are watching someone who is in such
+ a noisy bug.
+ </para>
</section>
<section id="savedsearches" xreflabel="Saved Searches">
diff --git a/editclassifications.cgi b/editclassifications.cgi
index db9dd7f0a..3ab89f120 100755
--- a/editclassifications.cgi
+++ b/editclassifications.cgi
@@ -198,9 +198,10 @@ if ($action eq 'update') {
if ($action eq 'reclassify') {
my $classification = Bugzilla::Classification->check($class_name);
-
+
my $sth = $dbh->prepare("UPDATE products SET classification_id = ?
WHERE name = ?");
+ my @names;
if (defined $cgi->param('add_products')) {
check_token_data($token, 'reclassify_classifications');
@@ -208,6 +209,7 @@ if ($action eq 'reclassify') {
foreach my $prod ($cgi->param("prodlist")) {
trick_taint($prod);
$sth->execute($classification->id, $prod);
+ push @names, $prod;
}
}
delete_token($token);
@@ -216,7 +218,8 @@ if ($action eq 'reclassify') {
if (defined $cgi->param('myprodlist')) {
foreach my $prod ($cgi->param("myprodlist")) {
trick_taint($prod);
- $sth->execute(1,$prod);
+ $sth->execute(1, $prod);
+ push @names, $prod;
}
}
delete_token($token);
@@ -226,6 +229,11 @@ if ($action eq 'reclassify') {
$vars->{'classification'} = $classification;
$vars->{'token'} = issue_session_token('reclassify_classifications');
+ foreach my $name (@names) {
+ Bugzilla->memcached->clear({ table => 'products', name => $name });
+ }
+ Bugzilla->memcached->clear_config();
+
LoadTemplate($action);
}
diff --git a/editsettings.cgi b/editsettings.cgi
index d375a3d5d..981495893 100755
--- a/editsettings.cgi
+++ b/editsettings.cgi
@@ -62,6 +62,7 @@ if ($action eq 'update') {
}
$vars->{'message'} = 'default_settings_updated';
$vars->{'changes_saved'} = $changed;
+ Bugzilla->memcached->clear_config();
delete_token($token);
}
diff --git a/editusers.cgi b/editusers.cgi
index 4182f6875..b6f5a6c52 100755
--- a/editusers.cgi
+++ b/editusers.cgi
@@ -80,7 +80,8 @@ if ($action eq 'search') {
my $matchstr = trim($cgi->param('matchstr'));
my $matchtype = $cgi->param('matchtype');
my $grouprestrict = $cgi->param('grouprestrict') || '0';
- my $query = 'SELECT DISTINCT userid, login_name, realname, is_enabled ' .
+ my $query = 'SELECT DISTINCT userid, login_name, realname, is_enabled, ' .
+ $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date ' .
'FROM profiles';
my @bindValues;
my $nextCondition;
@@ -341,6 +342,7 @@ if ($action eq 'search') {
($otherUserID, $userid,
get_field_id('bug_group'),
join(', ', @groupsRemovedFrom), join(', ', @groupsAddedTo)));
+ Bugzilla->memcached->clear_config({ key => "user_groups.$otherUserID" })
}
# XXX: should create profiles_activity entries for blesser changes.
@@ -490,10 +492,6 @@ if ($action eq 'search') {
my $sth_set_bug_timestamp =
$dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?');
- my $sth_updateFlag = $dbh->prepare('INSERT INTO bugs_activity
- (bug_id, attach_id, who, bug_when, fieldid, removed, added)
- VALUES (?, ?, ?, ?, ?, ?, ?)');
-
# Flags
my $flag_ids =
$dbh->selectcol_arrayref('SELECT id FROM flags WHERE requestee_id = ?',
@@ -508,16 +506,15 @@ if ($action eq 'search') {
# so we have to log these changes manually.
my %bugs;
push(@{$bugs{$_->bug_id}->{$_->attach_id || 0}}, $_) foreach @$flags;
- my $fieldid = get_field_id('flagtypes.name');
foreach my $bug_id (keys %bugs) {
foreach my $attach_id (keys %{$bugs{$bug_id}}) {
my @old_summaries = Bugzilla::Flag->snapshot($bugs{$bug_id}->{$attach_id});
$_->_set_requestee() foreach @{$bugs{$bug_id}->{$attach_id}};
my @new_summaries = Bugzilla::Flag->snapshot($bugs{$bug_id}->{$attach_id});
my ($removed, $added) =
- Bugzilla::Flag->update_activity(\@old_summaries, \@new_summaries);
- $sth_updateFlag->execute($bug_id, $attach_id || undef, $userid,
- $timestamp, $fieldid, $removed, $added);
+ Bugzilla::Flag->update_activity(\@old_summaries, \@new_summaries);
+ LogActivityEntry($bug_id, 'flagtypes.name', $removed, $added,
+ $userid, $timestamp, undef, $attach_id);
}
$sth_set_bug_timestamp->execute($timestamp, $bug_id);
$updatedbugs{$bug_id} = 1;
@@ -645,6 +642,11 @@ if ($action eq 'search') {
$dbh->bz_commit_transaction();
delete_token($token);
+ # It's complex to determine which items now need to be flushed from
+ # memcached. As user deletion is expected to be a rare event, we just
+ # flush the entire cache when a user is deleted.
+ Bugzilla->memcached->clear_all();
+
$vars->{'message'} = 'account_deleted';
$vars->{'otheruser'}{'login'} = $otherUser->login;
$vars->{'restrictablegroups'} = $user->bless_groups();
@@ -658,8 +660,17 @@ if ($action eq 'search') {
}
###########################################################################
-} elsif ($action eq 'activity') {
+} elsif ($action eq 'activity' || $action eq 'admin_activity') {
my $otherUser = check_user($otherUserID, $otherUserLogin);
+ my $activity_who = "profiles_activity.who";
+ my $activity_userid = "profiles_activity.userid";
+
+ if ($action eq 'admin_activity') {
+ $editusers || ThrowUserError("auth_failure", { group => "editusers",
+ action => "admin_activity",
+ object => "users" });
+ ($activity_userid, $activity_who) = ($activity_who, $activity_userid);
+ }
$vars->{'profile_changes'} = $dbh->selectall_arrayref(
"SELECT profiles.login_name AS who, " .
@@ -668,14 +679,15 @@ if ($action eq 'search') {
profiles_activity.oldvalue AS removed,
profiles_activity.newvalue AS added
FROM profiles_activity
- INNER JOIN profiles ON profiles_activity.who = profiles.userid
+ INNER JOIN profiles ON $activity_who = profiles.userid
INNER JOIN fielddefs ON fielddefs.id = profiles_activity.fieldid
- WHERE profiles_activity.userid = ?
+ WHERE $activity_userid = ?
ORDER BY profiles_activity.profiles_when",
{'Slice' => {}},
$otherUser->id);
$vars->{'otheruser'} = $otherUser;
+ $vars->{'action'} = $action;
$template->process("account/profile-activity.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
diff --git a/enter_bug.cgi b/enter_bug.cgi
index 5b684a965..f0c86b9ee 100755
--- a/enter_bug.cgi
+++ b/enter_bug.cgi
@@ -51,6 +51,7 @@ use Bugzilla::Keyword;
use Bugzilla::Token;
use Bugzilla::Field;
use Bugzilla::Status;
+use Bugzilla::UserAgent;
my $user = Bugzilla->login(LOGIN_REQUIRED);
@@ -62,9 +63,21 @@ my $dbh = Bugzilla->dbh;
my $template = Bugzilla->template;
my $vars = {};
+# BMO add a hook for the guided extension
+Bugzilla::Hook::process('enter_bug_start', { vars => $vars });
+
# All pages point to the same part of the documentation.
$vars->{'doc_section'} = 'bugreports.html';
+if (!$vars->{'disable_guided'}) {
+ # Purpose: force guided format for newbies
+ $cgi->param(-name=>'format', -value=>'guided')
+ if !$cgi->param('format') && !$user->in_group('canconfirm');
+
+ $cgi->delete('format')
+ if ($cgi->param('format') && ($cgi->param('format') eq "__default__"));
+}
+
my $product_name = trim($cgi->param('product') || '');
# Will contain the product object the bug is created in.
my $product;
@@ -74,8 +87,14 @@ if ($product_name eq '') {
my @enterable_products = @{$user->get_enterable_products};
ThrowUserError('no_products') unless scalar(@enterable_products);
- my $classification = Bugzilla->params->{'useclassification'} ?
- scalar($cgi->param('classification')) : '__all';
+ # MOZILLA CUSTOMIZATION
+ # skip the classification selection page
+ my $classification;
+ if (Bugzilla->params->{'useclassification'}) {
+ $classification = scalar($cgi->param('classification')) || '__all';
+ } else {
+ $classification = '__all';
+ }
# Unless a real classification name is given, we sort products
# by classification.
@@ -166,198 +185,6 @@ sub formvalue {
return Bugzilla->cgi->param($name) || $default || "";
}
-# Takes the name of a field and a list of possible values for that
-# field. Returns the first value in the list that is actually a
-# valid value for that field.
-# The field should be named after its DB table.
-# Returns undef if none of the platforms match.
-sub pick_valid_field_value (@) {
- my ($field, @values) = @_;
- my $dbh = Bugzilla->dbh;
-
- foreach my $value (@values) {
- return $value if $dbh->selectrow_array(
- "SELECT 1 FROM $field WHERE value = ?", undef, $value);
- }
- return undef;
-}
-
-sub pickplatform {
- return formvalue("rep_platform") if formvalue("rep_platform");
-
- my @platform;
-
- if (Bugzilla->params->{'defaultplatform'}) {
- @platform = Bugzilla->params->{'defaultplatform'};
- } else {
- # If @platform is a list, this function will return the first
- # item in the list that is a valid platform choice. If
- # no choice is valid, we return "Other".
- for ($ENV{'HTTP_USER_AGENT'}) {
- #PowerPC
- /\(.*PowerPC.*\)/i && do {push @platform, ("PowerPC", "Macintosh");};
- #AMD64, Intel x86_64
- /\(.*amd64.*\)/ && do {push @platform, ("AMD64", "x86_64", "PC");};
- /\(.*x86_64.*\)/ && do {push @platform, ("AMD64", "x86_64", "PC");};
- #Intel Itanium
- /\(.*IA64.*\)/ && do {push @platform, "IA64";};
- #Intel x86
- /\(.*Intel.*\)/ && do {push @platform, ("IA32", "x86", "PC");};
- /\(.*[ix0-9]86.*\)/ && do {push @platform, ("IA32", "x86", "PC");};
- #Versions of Windows that only run on Intel x86
- /\(.*Win(?:dows |)[39M].*\)/ && do {push @platform, ("IA32", "x86", "PC");};
- /\(.*Win(?:dows |)16.*\)/ && do {push @platform, ("IA32", "x86", "PC");};
- #Sparc
- /\(.*sparc.*\)/ && do {push @platform, ("Sparc", "Sun");};
- /\(.*sun4.*\)/ && do {push @platform, ("Sparc", "Sun");};
- #Alpha
- /\(.*AXP.*\)/i && do {push @platform, ("Alpha", "DEC");};
- /\(.*[ _]Alpha.\D/i && do {push @platform, ("Alpha", "DEC");};
- /\(.*[ _]Alpha\)/i && do {push @platform, ("Alpha", "DEC");};
- #MIPS
- /\(.*IRIX.*\)/i && do {push @platform, ("MIPS", "SGI");};
- /\(.*MIPS.*\)/i && do {push @platform, ("MIPS", "SGI");};
- #68k
- /\(.*68K.*\)/ && do {push @platform, ("68k", "Macintosh");};
- /\(.*680[x0]0.*\)/ && do {push @platform, ("68k", "Macintosh");};
- #HP
- /\(.*9000.*\)/ && do {push @platform, ("PA-RISC", "HP");};
- #ARM
- /\(.*ARM.*\)/ && do {push @platform, ("ARM", "PocketPC");};
- #PocketPC intentionally before PowerPC
- /\(.*Windows CE.*PPC.*\)/ && do {push @platform, ("ARM", "PocketPC");};
- #PowerPC
- /\(.*PPC.*\)/ && do {push @platform, ("PowerPC", "Macintosh");};
- /\(.*AIX.*\)/ && do {push @platform, ("PowerPC", "Macintosh");};
- #Stereotypical and broken
- /\(.*Windows CE.*\)/ && do {push @platform, ("ARM", "PocketPC");};
- /\(.*Macintosh.*\)/ && do {push @platform, ("68k", "Macintosh");};
- /\(.*Mac OS [89].*\)/ && do {push @platform, ("68k", "Macintosh");};
- /\(.*Win64.*\)/ && do {push @platform, "IA64";};
- /\(Win.*\)/ && do {push @platform, ("IA32", "x86", "PC");};
- /\(.*Win(?:dows[ -])NT.*\)/ && do {push @platform, ("IA32", "x86", "PC");};
- /\(.*OSF.*\)/ && do {push @platform, ("Alpha", "DEC");};
- /\(.*HP-?UX.*\)/i && do {push @platform, ("PA-RISC", "HP");};
- /\(.*IRIX.*\)/i && do {push @platform, ("MIPS", "SGI");};
- /\(.*(SunOS|Solaris).*\)/ && do {push @platform, ("Sparc", "Sun");};
- #Braindead old browsers who didn't follow convention:
- /Amiga/ && do {push @platform, ("68k", "Macintosh");};
- /WinMosaic/ && do {push @platform, ("IA32", "x86", "PC");};
- }
- }
-
- return pick_valid_field_value('rep_platform', @platform) || "Other";
-}
-
-sub pickos {
- if (formvalue('op_sys') ne "") {
- return formvalue('op_sys');
- }
-
- my @os = ();
-
- if (Bugzilla->params->{'defaultopsys'}) {
- @os = Bugzilla->params->{'defaultopsys'};
- } else {
- # This function will return the first
- # item in @os that is a valid platform choice. If
- # no choice is valid, we return "Other".
- for ($ENV{'HTTP_USER_AGENT'}) {
- /\(.*IRIX.*\)/ && do {push @os, "IRIX";};
- /\(.*OSF.*\)/ && do {push @os, "OSF/1";};
- /\(.*Linux.*\)/ && do {push @os, "Linux";};
- /\(.*Solaris.*\)/ && do {push @os, "Solaris";};
- /\(.*SunOS.*\)/ && do {
- /\(.*SunOS 5.11.*\)/ && do {push @os, ("OpenSolaris", "Opensolaris", "Solaris 11");};
- /\(.*SunOS 5.10.*\)/ && do {push @os, "Solaris 10";};
- /\(.*SunOS 5.9.*\)/ && do {push @os, "Solaris 9";};
- /\(.*SunOS 5.8.*\)/ && do {push @os, "Solaris 8";};
- /\(.*SunOS 5.7.*\)/ && do {push @os, "Solaris 7";};
- /\(.*SunOS 5.6.*\)/ && do {push @os, "Solaris 6";};
- /\(.*SunOS 5.5.*\)/ && do {push @os, "Solaris 5";};
- /\(.*SunOS 5.*\)/ && do {push @os, "Solaris";};
- /\(.*SunOS.*sun4u.*\)/ && do {push @os, "Solaris";};
- /\(.*SunOS.*i86pc.*\)/ && do {push @os, "Solaris";};
- /\(.*SunOS.*\)/ && do {push @os, "SunOS";};
- };
- /\(.*HP-?UX.*\)/ && do {push @os, "HP-UX";};
- /\(.*BSD.*\)/ && do {
- /\(.*BSD\/(?:OS|386).*\)/ && do {push @os, "BSDI";};
- /\(.*FreeBSD.*\)/ && do {push @os, "FreeBSD";};
- /\(.*OpenBSD.*\)/ && do {push @os, "OpenBSD";};
- /\(.*NetBSD.*\)/ && do {push @os, "NetBSD";};
- };
- /\(.*BeOS.*\)/ && do {push @os, "BeOS";};
- /\(.*AIX.*\)/ && do {push @os, "AIX";};
- /\(.*OS\/2.*\)/ && do {push @os, "OS/2";};
- /\(.*QNX.*\)/ && do {push @os, "Neutrino";};
- /\(.*VMS.*\)/ && do {push @os, "OpenVMS";};
- /\(.*Win.*\)/ && do {
- /\(.*Windows XP.*\)/ && do {push @os, "Windows XP";};
- /\(.*Windows NT 6\.2.*\)/ && do {push @os, "Windows 8";};
- /\(.*Windows NT 6\.1.*\)/ && do {push @os, "Windows 7";};
- /\(.*Windows NT 6\.0.*\)/ && do {push @os, "Windows Vista";};
- /\(.*Windows NT 5\.2.*\)/ && do {push @os, "Windows Server 2003";};
- /\(.*Windows NT 5\.1.*\)/ && do {push @os, "Windows XP";};
- /\(.*Windows 2000.*\)/ && do {push @os, "Windows 2000";};
- /\(.*Windows NT 5.*\)/ && do {push @os, "Windows 2000";};
- /\(.*Win.*9[8x].*4\.9.*\)/ && do {push @os, "Windows ME";};
- /\(.*Win(?:dows |)M[Ee].*\)/ && do {push @os, "Windows ME";};
- /\(.*Win(?:dows |)98.*\)/ && do {push @os, "Windows 98";};
- /\(.*Win(?:dows |)95.*\)/ && do {push @os, "Windows 95";};
- /\(.*Win(?:dows |)16.*\)/ && do {push @os, "Windows 3.1";};
- /\(.*Win(?:dows[ -]|)NT.*\)/ && do {push @os, "Windows NT";};
- /\(.*Windows.*NT.*\)/ && do {push @os, "Windows NT";};
- };
- /\(.*Mac OS X.*\)/ && do {
- /\(.*Mac OS X (?:|Mach-O |\()10.8.*\)/ && do {push @os, "Mac OS X 10.8";};
- /\(.*Mac OS X (?:|Mach-O |\()10.7.*\)/ && do {push @os, "Mac OS X 10.7";};
- /\(.*Mac OS X (?:|Mach-O |\()10.6.*\)/ && do {push @os, "Mac OS X 10.6";};
- /\(.*Mac OS X (?:|Mach-O |\()10.5.*\)/ && do {push @os, "Mac OS X 10.5";};
- /\(.*Mac OS X (?:|Mach-O |\()10.4.*\)/ && do {push @os, "Mac OS X 10.4";};
- /\(.*Mac OS X (?:|Mach-O |\()10.3.*\)/ && do {push @os, "Mac OS X 10.3";};
- /\(.*Mac OS X (?:|Mach-O |\()10.2.*\)/ && do {push @os, "Mac OS X 10.2";};
- /\(.*Mac OS X (?:|Mach-O |\()10.1.*\)/ && do {push @os, "Mac OS X 10.1";};
- # Unfortunately, OS X 10.4 was the first to support Intel. This is
- # fallback support because some browsers refused to include the OS
- # Version.
- /\(.*Intel.*Mac OS X.*\)/ && do {push @os, "Mac OS X 10.4";};
- # OS X 10.3 is the most likely default version of PowerPC Macs
- # OS X 10.0 is more for configurations which didn't setup 10.x versions
- /\(.*Mac OS X.*\)/ && do {push @os, ("Mac OS X 10.3", "Mac OS X 10.0", "Mac OS X");};
- };
- /\(.*32bit.*\)/ && do {push @os, "Windows 95";};
- /\(.*16bit.*\)/ && do {push @os, "Windows 3.1";};
- /\(.*Mac OS \d.*\)/ && do {
- /\(.*Mac OS 9.*\)/ && do {push @os, ("Mac System 9.x", "Mac System 9.0");};
- /\(.*Mac OS 8\.6.*\)/ && do {push @os, ("Mac System 8.6", "Mac System 8.5");};
- /\(.*Mac OS 8\.5.*\)/ && do {push @os, "Mac System 8.5";};
- /\(.*Mac OS 8\.1.*\)/ && do {push @os, ("Mac System 8.1", "Mac System 8.0");};
- /\(.*Mac OS 8\.0.*\)/ && do {push @os, "Mac System 8.0";};
- /\(.*Mac OS 8[^.].*\)/ && do {push @os, "Mac System 8.0";};
- /\(.*Mac OS 8.*\)/ && do {push @os, "Mac System 8.6";};
- };
- /\(.*Darwin.*\)/ && do {push @os, ("Mac OS X 10.0", "Mac OS X");};
- # Silly
- /\(.*Mac.*\)/ && do {
- /\(.*Mac.*PowerPC.*\)/ && do {push @os, "Mac System 9.x";};
- /\(.*Mac.*PPC.*\)/ && do {push @os, "Mac System 9.x";};
- /\(.*Mac.*68k.*\)/ && do {push @os, "Mac System 8.0";};
- };
- # Evil
- /Amiga/i && do {push @os, "Other";};
- /WinMosaic/ && do {push @os, "Windows 95";};
- /\(.*PowerPC.*\)/ && do {push @os, "Mac System 9.x";};
- /\(.*PPC.*\)/ && do {push @os, "Mac System 9.x";};
- /\(.*68K.*\)/ && do {push @os, "Mac System 8.0";};
- }
- }
-
- push(@os, "Windows") if grep(/^Windows /, @os);
- push(@os, "Mac OS") if grep(/^Mac /, @os);
-
- return pick_valid_field_value('op_sys', @os) || "Other";
-}
##############################################################################
# End of subroutines
##############################################################################
@@ -420,19 +247,21 @@ $default{'product'} = $product->name;
if ($cloned_bug_id) {
- $default{'component_'} = $cloned_bug->component;
- $default{'priority'} = $cloned_bug->priority;
- $default{'bug_severity'} = $cloned_bug->bug_severity;
- $default{'rep_platform'} = $cloned_bug->rep_platform;
- $default{'op_sys'} = $cloned_bug->op_sys;
-
- $vars->{'short_desc'} = $cloned_bug->short_desc;
- $vars->{'bug_file_loc'} = $cloned_bug->bug_file_loc;
- $vars->{'keywords'} = $cloned_bug->keywords;
- $vars->{'dependson'} = join (", ", $cloned_bug_id, @{$cloned_bug->dependson});
- $vars->{'blocked'} = join (", ", @{$cloned_bug->blocked});
- $vars->{'deadline'} = $cloned_bug->deadline;
- $vars->{'estimated_time'} = $cloned_bug->estimated_time;
+ # BMO: allow form value component to override the cloned bug component
+ $default{'component_'} = formvalue('component') || $cloned_bug->component;
+ $default{'priority'} = $cloned_bug->priority;
+ $default{'bug_severity'} = $cloned_bug->bug_severity;
+ $default{'rep_platform'} = $cloned_bug->rep_platform;
+ $default{'op_sys'} = $cloned_bug->op_sys;
+
+ $vars->{'short_desc'} = $cloned_bug->short_desc;
+ $vars->{'bug_file_loc'} = $cloned_bug->bug_file_loc;
+ $vars->{'keywords'} = $cloned_bug->keywords;
+ $vars->{'dependson'} = join (", ", $cloned_bug_id, @{$cloned_bug->dependson});
+ $vars->{'blocked'} = join (", ", @{$cloned_bug->blocked});
+ $vars->{'deadline'} = $cloned_bug->deadline;
+ $vars->{'estimated_time'} = $cloned_bug->estimated_time;
+ $vars->{'status_whiteboard'} = $cloned_bug->status_whiteboard;
if (defined $cloned_bug->cc) {
$vars->{'cc'} = join (", ", @{$cloned_bug->cc});
@@ -465,15 +294,22 @@ if ($cloned_bug_id) {
$vars->{'comment_is_private'} = $isprivate;
}
+ # BMO Bug 1019747
+ $vars->{'cloned_bug'} = $cloned_bug;
+
+ # BMO Allow mentors to be cloned as well
+ $vars->{'bug_mentors'} = join(', ', map { $_->login } @{ $cloned_bug->mentors });
+
} # end of cloned bug entry form
else {
-
$default{'component_'} = formvalue('component');
$default{'priority'} = formvalue('priority', Bugzilla->params->{'defaultpriority'});
$default{'bug_severity'} = formvalue('bug_severity', Bugzilla->params->{'defaultseverity'});
- $default{'rep_platform'} = pickplatform();
- $default{'op_sys'} = pickos();
+ $default{'rep_platform'} = formvalue('rep_platform',
+ Bugzilla->params->{'defaultplatform'} || detect_platform());
+ $default{'op_sys'} = formvalue('op_sys',
+ Bugzilla->params->{'defaultopsys'} || detect_op_sys());
$vars->{'alias'} = formvalue('alias');
$vars->{'short_desc'} = formvalue('short_desc');
@@ -483,12 +319,17 @@ else {
$vars->{'blocked'} = formvalue('blocked');
$vars->{'deadline'} = formvalue('deadline');
$vars->{'estimated_time'} = formvalue('estimated_time');
+ $vars->{'bug_ignored'} = formvalue('bug_ignored');
+ $vars->{'see_also'} = formvalue('see_also');
$vars->{'cc'} = join(', ', $cgi->param('cc'));
$vars->{'comment'} = formvalue('comment');
$vars->{'comment_is_private'} = formvalue('comment_is_private');
+ # BMO Add support for mentors
+ $vars->{'bug_mentors'} = formvalue('bug_mentors');
+
} # end of normal/bookmarked entry form
diff --git a/errors/401.html b/errors/401.html
new file mode 100644
index 000000000..55c04398d
--- /dev/null
+++ b/errors/401.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Access Denied</title>
+ <style type="text/css">
+ body {
+ margin: 1em 2em;
+ background-color: #455372;
+ color: #ddd;
+ font-family: sans-serif;
+ }
+ h1, h3 {
+ color: #fff;
+ }
+ a {
+ color: #fff;
+ text-decoration: none;
+ }
+ #buggie {
+ float: left;
+ }
+ #content {
+ margin-left: 100px;
+ padding-top: 20px;
+ }
+ </style>
+ </head>
+ <body>
+ <img src="/images/buggie.png" id="buggie" alt="buggie" width="78" height="215">
+ <div id="content">
+ <h1>Authentication Required</h1>
+ <p>This server could not verify that you are authorized to access
+ that url. you either supplied the wrong credentials (e.g., bad
+ password), or your browser doesn't understand how to supply the
+ credentials required.</p>
+ <h3>Error 401</h3>
+ <p><a href="/">bugzilla.mozilla.org</a></p>
+ </div>
+ </body>
+</html>
diff --git a/errors/403.html b/errors/403.html
new file mode 100644
index 000000000..35325cfea
--- /dev/null
+++ b/errors/403.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Access Denied</title>
+ <style type="text/css">
+ body {
+ margin: 1em 2em;
+ background-color: #455372;
+ color: #ddd;
+ font-family: sans-serif;
+ }
+ h1, h3 {
+ color: #fff;
+ }
+ a {
+ color: #fff;
+ text-decoration: none;
+ }
+ #buggie {
+ float: left;
+ }
+ #content {
+ margin-left: 100px;
+ padding-top: 20px;
+ }
+ </style>
+ </head>
+ <body>
+ <img src="/images/buggie.png" id="buggie" alt="buggie" width="78" height="215">
+ <div id="content">
+ <h1>Access Denied</h1>
+ <p>Access to the requested resource has been denied.</p>
+ <h3>Error 403</h3>
+ <p><a href="/">bugzilla.mozilla.org</a></p>
+ </div>
+ </body>
+</html>
diff --git a/errors/404.html b/errors/404.html
new file mode 100644
index 000000000..56c72d0e2
--- /dev/null
+++ b/errors/404.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Object Not Found</title>
+ <style type="text/css">
+ body {
+ margin: 1em 2em;
+ background-color: #455372;
+ color: #ddd;
+ font-family: sans-serif;
+ }
+ h1, h3 {
+ color: #fff;
+ }
+ a {
+ color: #fff;
+ text-decoration: none;
+ }
+ #buggie {
+ float: left;
+ }
+ #content {
+ margin-left: 100px;
+ padding-top: 20px;
+ }
+ </style>
+ </head>
+ <body>
+ <img src="/images/buggie.png" id="buggie" alt="buggie" width="78" height="215">
+ <div id="content">
+ <h1>Object Not Found</h1>
+ <p>The requested URL was not found on this server.</p>
+ <h3>Error 404</h3>
+ <p><a href="/">bugzilla.mozilla.org</a></p>
+ </div>
+ </body>
+</html>
diff --git a/errors/500.html b/errors/500.html
new file mode 100644
index 000000000..2ffd6bad3
--- /dev/null
+++ b/errors/500.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Internal Server Error</title>
+ <style type="text/css">
+ body {
+ margin: 1em 2em;
+ background-color: #455372;
+ color: #ddd;
+ font-family: sans-serif;
+ }
+ h1, h3 {
+ color: #fff;
+ }
+ a {
+ color: #fff;
+ text-decoration: none;
+ }
+ #buggie {
+ float: left;
+ }
+ #content {
+ margin-left: 100px;
+ padding-top: 20px;
+ }
+ </style>
+ </head>
+ <body>
+ <img src="/images/buggie.png" id="buggie" alt="buggie" width="78" height="215">
+ <div id="content">
+ <h1>Internal Server Error</h1>
+ <p>The server encountered an internal error and was unable to complete your request.</p>
+ <h3>Error 500</h3>
+ <p><a href="/">bugzilla.mozilla.org</a></p>
+ </div>
+ </body>
+</html>
diff --git a/extensions/AntiSpam/Config.pm b/extensions/AntiSpam/Config.pm
new file mode 100644
index 000000000..92c32f629
--- /dev/null
+++ b/extensions/AntiSpam/Config.pm
@@ -0,0 +1,21 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::AntiSpam;
+use strict;
+
+use constant NAME => 'AntiSpam';
+use constant REQUIRED_MODULES => [
+ {
+ package => 'Email-Address',
+ module => 'Email::Address',
+ version => 0,
+ },
+];
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/AntiSpam/Extension.pm b/extensions/AntiSpam/Extension.pm
new file mode 100644
index 000000000..7a0f27b31
--- /dev/null
+++ b/extensions/AntiSpam/Extension.pm
@@ -0,0 +1,349 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::AntiSpam;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Error;
+use Bugzilla::Group;
+use Bugzilla::Util qw(remote_ip trick_taint);
+use Email::Address;
+use Encode;
+use Socket;
+use Sys::Syslog qw(:DEFAULT setlogsock);
+
+our $VERSION = '1';
+
+#
+# project honeypot integration
+#
+
+sub _project_honeypot_blocking {
+ my ($self, $api_key, $login) = @_;
+ my $ip = remote_ip();
+ return unless $ip =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
+ my $lookup = "$api_key.$4.$3.$2.$1.dnsbl.httpbl.org";
+ return unless my $packed = gethostbyname($lookup);
+ my $honeypot = inet_ntoa($packed);
+ return unless $honeypot =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
+ my ($status, $days, $threat, $type) = ($1, $2, $3, $4);
+
+ return if $status != 127
+ || $threat < Bugzilla->params->{honeypot_threat_threshold};
+
+ _syslog(sprintf("[audit] blocked <%s> from creating %s, honeypot %s", $ip, $login, $honeypot));
+ ThrowUserError('account_creation_restricted');
+}
+
+sub config_modify_panels {
+ my ($self, $args) = @_;
+ push @{ $args->{panels}->{auth}->{params} }, {
+ name => 'honeypot_api_key',
+ type => 't',
+ default => '',
+ };
+ push @{ $args->{panels}->{auth}->{params} }, {
+ name => 'honeypot_threat_threshold',
+ type => 't',
+ default => '32',
+ };
+}
+
+#
+# comment blocking
+#
+
+sub _comment_blocking {
+ my ($self, $params) = @_;
+
+ # as we want to use this sparingly, we only block comments on bugs which
+ # the user didn't report, and skip it completely if the user is in the
+ # editbugs group.
+ my $user = Bugzilla->user;
+ return if $user->in_group('editbugs');
+ # new bug
+ return unless $params->{bug_id};
+ # existing bug
+ my $bug = ref($params->{bug_id})
+ ? $params->{bug_id}
+ : Bugzilla::Bug->new($params->{bug_id});
+ return if $bug->reporter->id == $user->id;
+
+ my $blocklist = Bugzilla->dbh->selectcol_arrayref(
+ 'SELECT word FROM antispam_comment_blocklist'
+ );
+ return unless @$blocklist;
+
+ my $regex = '\b(?:' . join('|', map { quotemeta } @$blocklist) . ')\b';
+ if ($params->{thetext} =~ /$regex/i) {
+ ThrowUserError('antispam_comment_blocked');
+ }
+}
+
+#
+# domain blocking
+#
+
+sub _domain_blocking {
+ my ($self, $login) = @_;
+ my $address = Email::Address->new(undef, $login);
+ my $blocked = Bugzilla->dbh->selectrow_array(
+ "SELECT 1 FROM antispam_domain_blocklist WHERE domain=?",
+ undef,
+ $address->host
+ );
+ if ($blocked) {
+ _syslog(sprintf("[audit] blocked <%s> from creating %s, blacklisted domain", remote_ip(), $login));
+ ThrowUserError('account_creation_restricted');
+ }
+}
+
+#
+# ip blocking
+#
+
+sub _ip_blocking {
+ my ($self, $login) = @_;
+ my $ip = remote_ip();
+ trick_taint($ip);
+ my $blocked = Bugzilla->dbh->selectrow_array(
+ "SELECT 1 FROM antispam_ip_blocklist WHERE ip_address=?",
+ undef,
+ $ip
+ );
+ if ($blocked) {
+ _syslog(sprintf("[audit] blocked <%s> from creating %s, blacklisted IP", $ip, $login));
+ ThrowUserError('account_creation_restricted');
+ }
+}
+
+#
+# spam user disabling
+#
+
+sub comment_after_add_tag {
+ my ($self, $args) = @_;
+ my $tag = lc($args->{tag});
+ return unless $tag eq 'spam' or $tag eq 'abusive';
+ my $comment = $args->{comment};
+ my $author = $comment->author;
+
+ # exclude disabled users
+ return if !$author->is_enabled;
+
+ # exclude users by group
+ return if $author->in_group(Bugzilla->params->{antispam_spammer_exclude_group});
+
+ # exclude users who are no longer new
+ return if !$author->is_new;
+
+ # exclude users who haven't made enough comments
+ my $count = $tag eq 'spam'
+ ? Bugzilla->params->{antispam_spammer_comment_count}
+ : Bugzilla->params->{antispam_abusive_comment_count};
+ return if $author->comment_count < $count;
+
+ # get user's comments
+ trick_taint($tag);
+ my $comments = Bugzilla->dbh->selectall_arrayref("
+ SELECT longdescs.comment_id,longdescs_tags.id
+ FROM longdescs
+ LEFT JOIN longdescs_tags
+ ON longdescs_tags.comment_id = longdescs.comment_id
+ AND longdescs_tags.tag = ?
+ WHERE longdescs.who = ?
+ ORDER BY longdescs.bug_when
+ ", undef, $tag, $author->id);
+
+ # this comment needs to be counted too
+ my $comment_id = $comment->id;
+ foreach my $ra (@$comments) {
+ if ($ra->[0] == $comment_id) {
+ $ra->[1] = 1;
+ last;
+ }
+ }
+
+ # throw away comment id and negate bool to make it a list of not-spam/abuse
+ $comments = [ map { $_->[1] ? 0 : 1 } @$comments ];
+
+ my $reason;
+
+ # check if the first N comments are spam/abuse
+ if (!scalar(grep { $_ } @$comments[0..($count - 1)])) {
+ $reason = "first $count comments are $tag";
+ }
+
+ # check if the last N comments are spam/abuse
+ elsif (!scalar(grep { $_ } @$comments[-$count..-1])) {
+ $reason = "last $count comments are $tag";
+ }
+
+ # disable
+ if ($reason) {
+ $author->set_disabledtext(
+ $tag eq 'spam'
+ ? Bugzilla->params->{antispam_spammer_disable_text}
+ : Bugzilla->params->{antispam_abusive_disable_text}
+ );
+ $author->set_disable_mail(1);
+ $author->update();
+ _syslog(sprintf("[audit] antispam disabled <%s>: %s", $author->login, $reason));
+ }
+}
+
+#
+# hooks
+#
+
+sub object_end_of_create_validators {
+ my ($self, $args) = @_;
+ if ($args->{class} eq 'Bugzilla::Comment') {
+ $self->_comment_blocking($args->{params});
+ }
+}
+
+sub user_verify_login {
+ my ($self, $args) = @_;
+ if (my $api_key = Bugzilla->params->{honeypot_api_key}) {
+ $self->_project_honeypot_blocking($api_key, $args->{login});
+ }
+ $self->_ip_blocking($args->{login});
+ $self->_domain_blocking($args->{login});
+}
+
+sub editable_tables {
+ my ($self, $args) = @_;
+ my $tables = $args->{tables};
+ # allow these tables to be edited with the EditTables extension
+ $tables->{antispam_domain_blocklist} = {
+ id_field => 'id',
+ order_by => 'domain',
+ blurb => 'List of fully qualified domain names to block at account creation time.',
+ group => 'can_configure_antispam',
+ };
+ $tables->{antispam_comment_blocklist} = {
+ id_field => 'id',
+ order_by => 'word',
+ blurb => "List of whole words that will cause comments containing \\b\$word\\b to be blocked.\n" .
+ "This only applies to comments on bugs which the user didn't report.\n" .
+ "Users in the editbugs group are exempt from comment blocking.",
+ group => 'can_configure_antispam',
+ };
+ $tables->{antispam_ip_blocklist} = {
+ id_field => 'id',
+ order_by => 'ip_address',
+ blurb => 'List of IPv4 addresses which are prevented from creating accounts.',
+ group => 'can_configure_antispam',
+ };
+}
+
+sub config_add_panels {
+ my ($self, $args) = @_;
+ my $modules = $args->{panel_modules};
+ $modules->{AntiSpam} = "Bugzilla::Extension::AntiSpam::Config";
+}
+
+#
+# installation
+#
+
+sub install_before_final_checks {
+ if (!Bugzilla::Group->new({ name => 'can_configure_antispam' })) {
+ Bugzilla::Group->create({
+ name => 'can_configure_antispam',
+ description => 'Can configure Anti-Spam measures',
+ isbuggroup => 0,
+ });
+ }
+}
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'antispam_domain_blocklist'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ domain => {
+ TYPE => 'VARCHAR(255)',
+ NOTNULL => 1,
+ },
+ comment => {
+ TYPE => 'VARCHAR(255)',
+ NOTNULL => 1,
+ },
+ ],
+ INDEXES => [
+ antispam_domain_blocklist_idx => {
+ FIELDS => [ 'domain' ],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'antispam_comment_blocklist'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ word => {
+ TYPE => 'VARCHAR(255)',
+ NOTNULL => 1,
+ },
+ ],
+ INDEXES => [
+ antispam_comment_blocklist_idx => {
+ FIELDS => [ 'word' ],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'antispam_ip_blocklist'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ ip_address => {
+ TYPE => 'VARCHAR(15)',
+ NOTNULL => 1,
+ },
+ comment => {
+ TYPE => 'VARCHAR(255)',
+ NOTNULL => 1,
+ },
+ ],
+ INDEXES => [
+ antispam_ip_blocklist_idx => {
+ FIELDS => [ 'ip_address' ],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+}
+
+#
+# utilities
+#
+
+sub _syslog {
+ my $message = shift;
+ openlog('apache', 'cons,pid', 'local4');
+ syslog('notice', encode_utf8($message));
+ closelog();
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/AntiSpam/lib/Config.pm b/extensions/AntiSpam/lib/Config.pm
new file mode 100644
index 000000000..c8e1255c2
--- /dev/null
+++ b/extensions/AntiSpam/lib/Config.pm
@@ -0,0 +1,75 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::AntiSpam::Config;
+
+use strict;
+use warnings;
+
+use Bugzilla::Config::Common;
+use Bugzilla::Group;
+
+our $sortkey = 511;
+
+sub get_param_list {
+ my ($class) = @_;
+
+ my @param_list = (
+ {
+ name => 'antispam_spammer_exclude_group',
+ type => 's',
+ choices => \&_get_all_group_names,
+ default => 'canconfirm',
+ checker => \&check_group
+ },
+ {
+ name => 'antispam_spammer_comment_count',
+ type => 't',
+ default => '3',
+ checker => \&check_numeric
+ },
+ {
+ name => 'antispam_spammer_disable_text',
+ type => 'l',
+ default =>
+ "This account has been automatically disabled as a result of " .
+ "a high number of spam comments.<br>\n<br>\n" .
+ "Please contact the address at the end of this message if " .
+ "you believe this to be an error."
+ },
+ {
+ name => 'antispam_abusive_comment_count',
+ type => 't',
+ default => '5',
+ checker => \&check_numeric
+ },
+ {
+ name => 'antispam_abusive_disable_text',
+ type => 'l',
+ default =>
+ "This account has been automatically disabled as a result of " .
+ "a high number of comments tagged as abusive.<br>\n<br>\n" .
+ "All interactions on Bugzilla should follow our " .
+ "<a href=\"https://bugzilla.mozilla.org/page.cgi?id=etiquette.html\">" .
+ "etiquette guidelines</a>.<br>\n<br>\n" .
+ "Please contact the address at the end of this message if you " .
+ "believe this to be an error, or if you would like your account " .
+ "reactivated in order to interact within our etiquette " .
+ "guidelines."
+ },
+ );
+
+ return @param_list;
+}
+
+sub _get_all_group_names {
+ my @group_names = map {$_->name} Bugzilla::Group->get_all;
+ unshift(@group_names, '');
+ return \@group_names;
+}
+
+1;
diff --git a/extensions/AntiSpam/template/en/default/admin/params/antispam.html.tmpl b/extensions/AntiSpam/template/en/default/admin/params/antispam.html.tmpl
new file mode 100644
index 000000000..76299f546
--- /dev/null
+++ b/extensions/AntiSpam/template/en/default/admin/params/antispam.html.tmpl
@@ -0,0 +1,37 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%
+ title = "Anti-Spam"
+ desc = "Edit Anti-Spam Configuration"
+%]
+
+[% param_descs =
+{
+ antispam_spammer_exclude_group =>
+ "Users in this group will be excluded from automatic disabling."
+
+ antispam_spammer_comment_count =>
+ "If a user has made at least this many comments, and either their first " _
+ "NNN comments or their last NNN comments have been tagged as spam, their " _
+ "account will be automatically disabled."
+
+ antispam_spammer_disable_text =>
+ "This message will be displayed to the user when they try to log " _
+ "in after their account is disabled due to spam."
+
+ antispam_abusive_comment_count =>
+ "If a user has made at least this many comments, and either their first " _
+ "NNN comments or their last NNN comments have been tagged as abusive, their " _
+ "account will be automatically disabled."
+
+ antispam_abusive_disable_text =>
+ "This message will be displayed to the user when they try to log " _
+ "in after their account is disabled due to abuse."
+}
+%]
diff --git a/extensions/AntiSpam/template/en/default/hook/admin/admin-end_links_right.html.tmpl b/extensions/AntiSpam/template/en/default/hook/admin/admin-end_links_right.html.tmpl
new file mode 100644
index 000000000..e55475d98
--- /dev/null
+++ b/extensions/AntiSpam/template/en/default/hook/admin/admin-end_links_right.html.tmpl
@@ -0,0 +1,16 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF user.in_group('can_configure_antispam') %]
+ <dt id="antispam" >AntiSpam</dt>
+ <dd>
+ <a href="page.cgi?id=edit_table.html&amp;table=antispam_domain_blocklist">Domain Blocklist</a><br>
+ <a href="page.cgi?id=edit_table.html&amp;table=antispam_comment_blocklist">Comment Blocklist</a><br>
+ <a href="page.cgi?id=edit_table.html&amp;table=antispam_ip_blocklist">IP Address Blocklist</a><br>
+ </dd>
+[% END %]
diff --git a/extensions/AntiSpam/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl b/extensions/AntiSpam/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl
new file mode 100644
index 000000000..e8e67eccb
--- /dev/null
+++ b/extensions/AntiSpam/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl
@@ -0,0 +1,16 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF panel.name == "auth" %]
+ [% panel.param_descs.honeypot_api_key =
+ 'API Key for http://www.projecthoneypot.org'
+ %]
+ [% panel.param_descs.honeypot_threat_threshold =
+ 'Users will be unable to create accounts if their honeypot threat score is this value or higher.'
+ %]
+[% END -%]
diff --git a/extensions/AntiSpam/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/AntiSpam/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..44410ca2f
--- /dev/null
+++ b/extensions/AntiSpam/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "antispam_comment_blocked" %]
+ [% title = "Comment Blocked" %]
+ Your comment contains one or more words deemed inappropriate for use by the
+ administrators of this site.
+[% END %]
diff --git a/extensions/BMO/Config.pm b/extensions/BMO/Config.pm
new file mode 100644
index 000000000..93445f576
--- /dev/null
+++ b/extensions/BMO/Config.pm
@@ -0,0 +1,48 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the BMO Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Gervase Markham
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Gervase Markham <gerv@gerv.net>
+
+package Bugzilla::Extension::BMO;
+use strict;
+
+use constant NAME => 'BMO';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'Tie-IxHash',
+ module => 'Tie::IxHash',
+ version => 0
+ },
+ {
+ package => 'Sys-Syslog',
+ module => 'Sys::Syslog',
+ version => 0
+ },
+ {
+ package => 'File-MimeInfo',
+ module => 'File::MimeInfo::Magic',
+ version => '0'
+ },
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/BMO/Extension.pm b/extensions/BMO/Extension.pm
new file mode 100644
index 000000000..c28238197
--- /dev/null
+++ b/extensions/BMO/Extension.pm
@@ -0,0 +1,1561 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the BMO Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Gervase Markham.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Gervase Markham <gerv@gerv.net>
+# David Lawrence <dkl@mozilla.com>
+# Byron Jones <glob@mozilla.com>
+
+package Bugzilla::Extension::BMO;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Field;
+use Bugzilla::Group;
+use Bugzilla::Mailer;
+use Bugzilla::Product;
+use Bugzilla::Status;
+use Bugzilla::Token;
+use Bugzilla::Install::Filesystem;
+use Bugzilla::User;
+use Bugzilla::User::Setting;
+use Bugzilla::Util;
+
+use Date::Parse;
+use DateTime;
+use Encode qw(find_encoding encode_utf8);
+use File::MimeInfo::Magic;
+use List::MoreUtils qw(natatime);
+use Scalar::Util qw(blessed);
+use Sys::Syslog qw(:DEFAULT setlogsock);
+
+use Bugzilla::Extension::BMO::Constants;
+use Bugzilla::Extension::BMO::FakeBug;
+use Bugzilla::Extension::BMO::Data;
+
+our $VERSION = '0.1';
+
+#
+# Monkey-patched methods
+#
+
+BEGIN {
+ *Bugzilla::Bug::last_closed_date = \&_last_closed_date;
+ *Bugzilla::Product::default_security_group = \&_default_security_group;
+ *Bugzilla::Product::default_security_group_obj = \&_default_security_group_obj;
+ *Bugzilla::Product::group_always_settable = \&_group_always_settable;
+ *Bugzilla::check_default_product_security_group = \&_check_default_product_security_group;
+}
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my $file = $args->{'file'};
+ my $vars = $args->{'vars'};
+
+ $vars->{'cf_hidden_in_product'} = \&cf_hidden_in_product;
+
+ if ($file =~ /^list\/list/) {
+ # Purpose: enable correct sorting of list table
+ # Matched to changes in list/table.html.tmpl
+ my %db_order_column_name_map = (
+ 'map_components.name' => 'component',
+ 'map_products.name' => 'product',
+ 'map_reporter.login_name' => 'reporter',
+ 'map_assigned_to.login_name' => 'assigned_to',
+ 'delta_ts' => 'opendate',
+ 'creation_ts' => 'changeddate',
+ );
+
+ my @orderstrings = split(/,\s*/, $vars->{'order'});
+
+ # contains field names of the columns being used to sort the table.
+ my @order_columns;
+ foreach my $o (@orderstrings) {
+ $o =~ s/bugs.//;
+ $o = $db_order_column_name_map{$o} if
+ grep($_ eq $o, keys(%db_order_column_name_map));
+ next if (grep($_ eq $o, @order_columns));
+ push(@order_columns, $o);
+ }
+
+ $vars->{'order_columns'} = \@order_columns;
+
+ # fields that have a custom sortkey. (So they are correctly sorted
+ # when using js)
+ my @sortkey_fields = qw(bug_status resolution bug_severity priority
+ rep_platform op_sys);
+
+ my %columns_sortkey;
+ foreach my $field (@sortkey_fields) {
+ $columns_sortkey{$field} = _get_field_values_sort_key($field);
+ }
+ $columns_sortkey{'target_milestone'} = _get_field_values_sort_key('milestones');
+
+ $vars->{'columns_sortkey'} = \%columns_sortkey;
+ }
+ elsif ($file =~ /^bug\/create\/create[\.-](.*)/) {
+ my $format = $1;
+ if (!$vars->{'cloned_bug_id'}) {
+ # Allow status whiteboard values to be bookmarked
+ $vars->{'status_whiteboard'} =
+ Bugzilla->cgi->param('status_whiteboard') || "";
+ }
+
+ # Purpose: for pretty product chooser
+ $vars->{'format'} = Bugzilla->cgi->param('format');
+
+ if ($format eq 'doc.html.tmpl') {
+ my $versions = Bugzilla::Product->new({ name => 'Core' })->versions;
+ $vars->{'versions'} = [ reverse @$versions ];
+ }
+ }
+
+
+ if ($file =~ /^list\/list/ || $file =~ /^bug\/create\/create[\.-]/) {
+ # hack to allow the bug entry templates to use check_can_change_field
+ # to see if various field values should be available to the current user.
+ $vars->{'default'} = Bugzilla::Extension::BMO::FakeBug->new($vars->{'default'} || {});
+ }
+
+ if ($file =~ /^attachment\/diff-header\./) {
+ my $attachid = $vars->{attachid} ? $vars->{attachid} : $vars->{newid};
+ $vars->{attachment} = Bugzilla::Attachment->new({ id => $attachid, cache => 1 })
+ if $attachid;
+ }
+}
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my $page = $args->{'page_id'};
+ my $vars = $args->{'vars'};
+
+ if ($page eq 'user_activity.html') {
+ require Bugzilla::Extension::BMO::Reports::UserActivity;
+ Bugzilla::Extension::BMO::Reports::UserActivity::report($vars);
+
+ } elsif ($page eq 'triage_reports.html') {
+ require Bugzilla::Extension::BMO::Reports::Triage;
+ Bugzilla::Extension::BMO::Reports::Triage::report($vars);
+ }
+ elsif ($page eq 'group_admins.html') {
+ require Bugzilla::Extension::BMO::Reports::Groups;
+ Bugzilla::Extension::BMO::Reports::Groups::admins_report($vars);
+ }
+ elsif ($page eq 'group_membership.html' or $page eq 'group_membership.txt') {
+ require Bugzilla::Extension::BMO::Reports::Groups;
+ Bugzilla::Extension::BMO::Reports::Groups::membership_report($page, $vars);
+ }
+ elsif ($page eq 'group_members.html' or $page eq 'group_members.json') {
+ require Bugzilla::Extension::BMO::Reports::Groups;
+ Bugzilla::Extension::BMO::Reports::Groups::members_report($vars);
+ }
+ elsif ($page eq 'email_queue.html') {
+ require Bugzilla::Extension::BMO::Reports::EmailQueue;
+ Bugzilla::Extension::BMO::Reports::EmailQueue::report($vars);
+ }
+ elsif ($page eq 'release_tracking_report.html') {
+ require Bugzilla::Extension::BMO::Reports::ReleaseTracking;
+ Bugzilla::Extension::BMO::Reports::ReleaseTracking::report($vars);
+ }
+ elsif ($page eq 'product_security_report.html') {
+ require Bugzilla::Extension::BMO::Reports::ProductSecurity;
+ Bugzilla::Extension::BMO::Reports::ProductSecurity::report($vars);
+ }
+ elsif ($page eq 'fields.html') {
+ # Recently global/field-descs.none.tmpl and bug/field-help.none.tmpl
+ # were changed for better performance and are now only loaded once.
+ # I have not found an easy way to allow our hook template to check if
+ # it is called from pages/fields.html.tmpl. So we set a value in request_cache
+ # that our hook template can see.
+ Bugzilla->request_cache->{'bmo_fields_page'} = 1;
+ }
+ elsif ($page eq 'query_database.html') {
+ query_database($vars);
+ }
+}
+
+sub _get_field_values_sort_key {
+ my ($field) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $fields = $dbh->selectall_arrayref(
+ "SELECT value, sortkey FROM $field
+ ORDER BY sortkey, value");
+
+ my %field_values;
+ foreach my $field (@$fields) {
+ my ($value, $sortkey) = @$field;
+ $field_values{$value} = $sortkey;
+ }
+ return \%field_values;
+}
+
+sub active_custom_fields {
+ my ($self, $args) = @_;
+ my $fields = $args->{'fields'};
+ my $params = $args->{'params'};
+ my $product = $params->{'product'};
+ my $component = $params->{'component'};
+
+ return if !$product;
+
+ my $product_name = blessed $product ? $product->name : $product;
+ my $component_name = blessed $component ? $component->name : $component;
+
+ my @tmp_fields;
+ foreach my $field (@$$fields) {
+ next if cf_hidden_in_product($field->name, $product_name, $component_name);
+ push(@tmp_fields, $field);
+ }
+ $$fields = \@tmp_fields;
+}
+
+sub cf_hidden_in_product {
+ my ($field_name, $product_name, $component_name) = @_;
+
+ # If used in buglist.cgi, we pass in one_product which is a Bugzilla::Product
+ # elsewhere, we just pass the name of the product.
+ $product_name = blessed($product_name)
+ ? $product_name->name
+ : $product_name;
+
+ # Also in buglist.cgi, we pass in a list of components instead
+ # of a single component name everywhere else.
+ my $component_list = [];
+ if ($component_name) {
+ $component_list = ref $component_name
+ ? $component_name
+ : [ $component_name ];
+ }
+
+ foreach my $field_re (keys %$cf_visible_in_products) {
+ if ($field_name =~ $field_re) {
+ # If no product given, for example more than one product
+ # in buglist.cgi, then hide field by default
+ return 1 if !$product_name;
+
+ my $products = $cf_visible_in_products->{$field_re};
+ foreach my $product (keys %$products) {
+ my $components = $products->{$product};
+
+ my $found_component = 0;
+ if (@$components) {
+ foreach my $component (@$components) {
+ if (ref($component) eq 'Regexp') {
+ if (grep($_ =~ $component, @$component_list)) {
+ $found_component = 1;
+ last;
+ }
+ } else {
+ if (grep($_ eq $component, @$component_list)) {
+ $found_component = 1;
+ last;
+ }
+ }
+ }
+ }
+
+ # If product matches and at at least one component matches
+ # from component_list (if a matching component was required),
+ # we allow the field to be seen
+ if ($product eq $product_name && (!@$components || $found_component)) {
+ return 0;
+ }
+ }
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+# Purpose: CC certain email addresses on bugmail when a bug is added or
+# removed from a particular group.
+sub bugmail_recipients {
+ my ($self, $args) = @_;
+ my $bug = $args->{'bug'};
+ my $recipients = $args->{'recipients'};
+ my $diffs = $args->{'diffs'};
+
+ if (@$diffs) {
+ # Changed bug
+ foreach my $ref (@$diffs) {
+ my $old = $ref->{old};
+ my $new = $ref->{new};
+ my $fieldname = $ref->{field_name};
+
+ if ($fieldname eq "bug_group") {
+ _cc_if_special_group($old, $recipients);
+ _cc_if_special_group($new, $recipients);
+ }
+ }
+ } else {
+ # Determine if it's a new bug, or a comment without a field change
+ my $comment_count = scalar @{$bug->comments};
+ if ($comment_count == 1) {
+ # New bug
+ foreach my $group (@{ $bug->groups_in }) {
+ _cc_if_special_group($group->{'name'}, $recipients);
+ }
+ }
+ }
+}
+
+sub _cc_if_special_group {
+ my ($group, $recipients) = @_;
+
+ return if !$group;
+
+ if (exists $group_change_notification{$group}) {
+ foreach my $login (@{ $group_change_notification{$group} }) {
+ my $id = login_to_id($login);
+ $recipients->{$id}->{+REL_CC} = Bugzilla::BugMail::BIT_DIRECT();
+ }
+ }
+}
+
+sub _check_trusted {
+ my ($field, $trusted, $priv_results) = @_;
+
+ my $needed_group = $trusted->{'_default'} || "";
+ foreach my $dfield (keys %$trusted) {
+ if ($field =~ $dfield) {
+ $needed_group = $trusted->{$dfield};
+ }
+ }
+ if ($needed_group && !Bugzilla->user->in_group($needed_group)) {
+ push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ }
+}
+
+sub _is_field_set {
+ my $value = shift;
+ return $value ne '---' && $value !~ /\?$/;
+}
+
+sub bug_check_can_change_field {
+ my ($self, $args) = @_;
+ my $bug = $args->{'bug'};
+ my $field = $args->{'field'};
+ my $new_value = $args->{'new_value'};
+ my $old_value = $args->{'old_value'};
+ my $priv_results = $args->{'priv_results'};
+ my $user = Bugzilla->user;
+
+ if ($field =~ /^cf/ && !@$priv_results && $new_value ne '---') {
+ # "other" custom field setters restrictions
+ if (exists $cf_setters->{$field}) {
+ my $in_group = 0;
+ foreach my $group (@{$cf_setters->{$field}}) {
+ if ($user->in_group($group, $bug->product_id)) {
+ $in_group = 1;
+ last;
+ }
+ }
+ if (!$in_group) {
+ push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ }
+ }
+ }
+ elsif ($field eq 'resolution' && $new_value eq 'EXPIRED') {
+ # The EXPIRED resolution should only be settable by gerv.
+ if ($user->login ne 'gerv@mozilla.org') {
+ push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ }
+
+ } elsif ($field eq 'resolution' && $new_value eq 'FIXED') {
+ # You need at least canconfirm to mark a bug as FIXED
+ if (!$user->in_group('canconfirm', $bug->{'product_id'})) {
+ push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ }
+
+ } elsif (
+ ($field eq 'bug_status' && $old_value eq 'VERIFIED')
+ || ($field eq 'dup_id' && $bug->status->name eq 'VERIFIED')
+ || ($field eq 'resolution' && $bug->status->name eq 'VERIFIED')
+ ) {
+ # You need at least editbugs to reopen a resolved/verified bug
+ if (!$user->in_group('editbugs', $bug->{'product_id'})) {
+ push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ }
+
+ } elsif ($user->in_group('canconfirm', $bug->{'product_id'})) {
+ # Canconfirm is really "cantriage"; users with canconfirm can also mark
+ # bugs as DUPLICATE, WORKSFORME, and INCOMPLETE.
+ if ($field eq 'bug_status'
+ && is_open_state($old_value)
+ && !is_open_state($new_value))
+ {
+ push (@$priv_results, PRIVILEGES_REQUIRED_NONE);
+ }
+ elsif ($field eq 'resolution' &&
+ ($new_value eq 'DUPLICATE' ||
+ $new_value eq 'WORKSFORME' ||
+ $new_value eq 'INCOMPLETE'))
+ {
+ push (@$priv_results, PRIVILEGES_REQUIRED_NONE);
+ }
+
+ } elsif ($field eq 'bug_status') {
+ # Disallow reopening of bugs which have been resolved for > 1 year
+ if (is_open_state($new_value)
+ && !is_open_state($old_value)
+ && $bug->resolution eq 'FIXED')
+ {
+ my $days_ago = DateTime->now(time_zone => Bugzilla->local_timezone);
+ $days_ago->subtract(days => 365);
+ my $last_closed = datetime_from($bug->last_closed_date);
+ if ($last_closed lt $days_ago) {
+ push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ }
+ }
+ }
+}
+
+# link up various Mozilla-specific strings
+sub bug_format_comment {
+ my ($self, $args) = @_;
+ my $regexes = $args->{'regexes'};
+
+ # link to crash-stats
+ # Only match if not already in an URL using the negative lookbehind (?<!\/)
+ push (@$regexes, {
+ match => qr/(?<!\/)\bbp-([a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-
+ [a-f0-9]{4}\-[a-f0-9]{12})\b/x,
+ replace => sub {
+ my $args = shift;
+ my $match = html_quote($args->{matches}->[0]);
+ return qq{<a href="https://crash-stats.mozilla.com/report/index/$match">bp-$match</a>};
+ }
+ });
+
+ # link to CVE/CAN security releases
+ push (@$regexes, {
+ match => qr/(?<!\/|=)\b((?:CVE|CAN)-\d{4}-\d{4})\b/,
+ replace => sub {
+ my $args = shift;
+ my $match = html_quote($args->{matches}->[0]);
+ return qq{<a href="http://cve.mitre.org/cgi-bin/cvename.cgi?name=$match">$match</a>};
+ }
+ });
+
+ # link to svn.m.o
+ push (@$regexes, {
+ match => qr/\br(\d{4,})\b/,
+ replace => sub {
+ my $args = shift;
+ my $match = html_quote($args->{matches}->[0]);
+ return qq{<a href="http://viewvc.svn.mozilla.org/vc?view=rev&amp;revision=$match">r$match</a>};
+ }
+ });
+
+ # link bzr commit messages
+ push (@$regexes, {
+ match => qr/\b(Committing\s+to:\sbzr\+ssh:\/\/
+ (?:[^\@]+\@)?(bzr\.mozilla\.org[^\n]+)\n.*?\bCommitted\s)
+ (revision\s(\d+))/sx,
+ replace => sub {
+ my $args = shift;
+ my $preamble = html_quote($args->{matches}->[0]);
+ my $url = html_quote($args->{matches}->[1]);
+ my $text = html_quote($args->{matches}->[2]);
+ my $id = html_quote($args->{matches}->[3]);
+ $url =~ s/\s+$//;
+ $url =~ s/\/$//;
+ return qq{$preamble<a href="http://$url/revision/$id">$text</a>};
+ }
+ });
+
+ # link git.mozilla.org commit messages
+ push (@$regexes, {
+ match => qr#^(To\s(?:ssh://)?(?:[^\@]+\@)?git\.mozilla\.org[:/](.+?\.git)\n
+ \s+)([0-9a-z]+\.\.([0-9a-z]+)\s+\S+\s->\s\S+)#mx,
+ replace => sub {
+ my $args = shift;
+ my $preamble = html_quote($args->{matches}->[0]);
+ my $repo = html_quote($args->{matches}->[1]);
+ my $text = $args->{matches}->[2];
+ my $revision = $args->{matches}->[3];
+ return qq#$preamble<a href="http://git.mozilla.org/?p=$repo;a=commitdiff;h=$revision">$text</a>#;
+ }
+ });
+
+ # link to hg.m.o
+ # Note: for grouping in this regexp, always use non-capturing parentheses.
+ my $hgrepos = join('|', qw!(?:releases/)?comm-[\w.]+
+ (?:releases/)?mozilla-[\w.]+
+ (?:releases/)?mobile-[\w.]+
+ tracemonkey
+ tamarin-[\w.]+
+ camino!);
+
+ push (@$regexes, {
+ match => qr/\b(($hgrepos)\s+changeset:?\s+(?:\d+:)?([0-9a-fA-F]{12}))\b/,
+ replace => sub {
+ my $args = shift;
+ my $text = html_quote($args->{matches}->[0]);
+ my $repo = html_quote($args->{matches}->[1]);
+ my $id = html_quote($args->{matches}->[2]);
+ $repo = 'integration/mozilla-inbound' if $repo eq 'mozilla-inbound';
+ return qq{<a href="https://hg.mozilla.org/$repo/rev/$id">$text</a>};
+ }
+ });
+}
+
+sub quicksearch_map {
+ my ($self, $args) = @_;
+ my $map = $args->{'map'};
+
+ foreach my $name (keys %$map) {
+ if ($name =~ /cf_crash_signature$/) {
+ $map->{'sig'} = $name;
+ }
+ }
+}
+
+sub object_end_of_create {
+ my ($self, $args) = @_;
+ my $class = $args->{class};
+
+ if ($class eq 'Bugzilla::User') {
+ my $user = $args->{object};
+
+ # Log real IP addresses for auditing
+ _syslog(sprintf('[audit] <%s> created user %s', remote_ip(), $user->login));
+
+ # Add default searches to new user's footer
+ my $dbh = Bugzilla->dbh;
+
+ my $sharer = Bugzilla::User->new({ name => 'nobody@mozilla.org' })
+ or return;
+ my $group = Bugzilla::Group->new({ name => 'everyone' })
+ or return;
+
+ foreach my $definition (@default_named_queries) {
+ my ($namedquery_id) = _get_named_query($sharer->id, $group->id, $definition);
+ $dbh->do(
+ "INSERT INTO namedqueries_link_in_footer(namedquery_id,user_id) VALUES (?,?)",
+ undef,
+ $namedquery_id, $user->id
+ );
+ }
+
+ } elsif ($class eq 'Bugzilla::Bug') {
+ # Log real IP addresses for auditing
+ _syslog(sprintf('[audit] %s <%s> created bug %s',
+ Bugzilla->user->login, remote_ip(), $args->{object}->id));
+ }
+}
+
+sub _get_named_query {
+ my ($sharer_id, $group_id, $definition) = @_;
+ my $dbh = Bugzilla->dbh;
+ # find existing namedquery
+ my ($namedquery_id) = $dbh->selectrow_array(
+ "SELECT id FROM namedqueries WHERE userid=? AND name=?",
+ undef,
+ $sharer_id, $definition->{name}
+ );
+ return $namedquery_id if $namedquery_id;
+ # create namedquery
+ $dbh->do(
+ "INSERT INTO namedqueries(userid,name,query) VALUES (?,?,?)",
+ undef,
+ $sharer_id, $definition->{name}, $definition->{query}
+ );
+ $namedquery_id = $dbh->bz_last_key();
+ # and share it
+ $dbh->do(
+ "INSERT INTO namedquery_group_map(namedquery_id,group_id) VALUES (?,?)",
+ undef,
+ $namedquery_id, $group_id,
+ );
+ return $namedquery_id;
+}
+
+# Automatically CC users to bugs based on group & product
+sub bug_end_of_create {
+ my ($self, $args) = @_;
+ my $bug = $args->{'bug'};
+
+ foreach my $group_name (keys %group_auto_cc) {
+ my $group_obj = Bugzilla::Group->new({ name => $group_name });
+ if ($group_obj && $bug->in_group($group_obj)) {
+ my $ra_logins = exists $group_auto_cc{$group_name}->{$bug->product}
+ ? $group_auto_cc{$group_name}->{$bug->product}
+ : $group_auto_cc{$group_name}->{'_default'};
+ foreach my $login (@$ra_logins) {
+ $bug->add_cc($login);
+ }
+ }
+ }
+}
+
+# detect github pull requests and reviewboard reviews, set the content-type
+sub attachment_process_data {
+ my ($self, $args) = @_;
+ my $attributes = $args->{attributes};
+
+ # must be a text attachment
+ return unless $attributes->{mimetype} eq 'text/plain';
+
+ # check the attachment size, and get attachment content if it isn't too large
+ my $data = $attributes->{data};
+ my $url;
+ if (blessed($data) && blessed($data) eq 'Fh') {
+ # filehandle
+ my $size = -s $data;
+ return if $size > 256;
+ sysread($data, $url, $size);
+ seek($data, 0, 0);
+ } else {
+ # string
+ $url = $data;
+ }
+
+ if (my $content_type = _get_review_content_type($url)) {
+ $attributes->{mimetype} = $content_type;
+ $attributes->{ispatch} = 0;
+ }
+}
+
+sub _get_review_content_type {
+ my ($url) = @_;
+
+ # trim and check for the pull request url
+ return unless defined $url;
+ return if length($url) > 256;
+ $url = trim($url);
+ return if $url =~ /\s/;
+
+ if ($url =~ m#^https://github\.com/[^/]+/[^/]+/pull/\d+/?$#i) {
+ return GITHUB_PR_CONTENT_TYPE;
+ }
+ if ($url =~ m#^https?://reviewboard(?:-dev)?\.(?:allizom|mozilla)\.org/r/\d+/?#i) {
+ return RB_REQUEST_CONTENT_TYPE;
+ }
+ return;
+}
+
+# redirect automatically to github urls
+sub attachment_view {
+ my ($self, $args) = @_;
+ my $attachment = $args->{attachment};
+ my $cgi = Bugzilla->cgi;
+
+ # don't redirect if the content-type is specified explicitly
+ return if defined $cgi->param('content_type');
+
+ # must be our github/reviewboard content-type
+ return unless
+ $attachment->contenttype eq GITHUB_PR_CONTENT_TYPE
+ or $attachment->contenttype eq RB_REQUEST_CONTENT_TYPE;
+
+ # must still be a valid url
+ return unless _get_review_content_type($attachment->data);
+
+ # redirect
+ print $cgi->redirect(trim($attachment->data));
+ exit;
+}
+
+sub install_before_final_checks {
+ my ($self, $args) = @_;
+
+ # Add product chooser setting
+ add_setting('product_chooser',
+ ['pretty_product_chooser', 'full_product_chooser'],
+ 'pretty_product_chooser');
+
+ # Add option to inject x-bugzilla headers into the message body to work
+ # around gmail filtering limitations
+ add_setting('headers_in_body', ['on', 'off'], 'off');
+
+ # Migrate from 'gmail_threading' setting to 'bugmail_new_prefix'
+ my $dbh = Bugzilla->dbh;
+ if ($dbh->selectrow_array("SELECT 1 FROM setting WHERE name='gmail_threading'")) {
+ $dbh->bz_start_transaction();
+ $dbh->do("UPDATE profile_setting
+ SET setting_value='on-temp'
+ WHERE setting_name='gmail_threading' AND setting_value='Off'");
+ $dbh->do("UPDATE profile_setting
+ SET setting_value='off'
+ WHERE setting_name='gmail_threading' AND setting_value='On'");
+ $dbh->do("UPDATE profile_setting
+ SET setting_value='on'
+ WHERE setting_name='gmail_threading' AND setting_value='on-temp'");
+ $dbh->do("UPDATE profile_setting
+ SET setting_name='bugmail_new_prefix'
+ WHERE setting_name='gmail_threading'");
+ $dbh->do("DELETE FROM setting WHERE name='gmail_threading'");
+ $dbh->bz_commit_transaction();
+ }
+}
+
+# Migrate old is_active stuff to new patch (is in core in 4.2), The old column
+# name was 'is_active', the new one is 'isactive' (no underscore).
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+
+ if ($dbh->bz_column_info('milestones', 'is_active')) {
+ $dbh->do("UPDATE milestones SET isactive = 0 WHERE is_active = 0;");
+ $dbh->bz_drop_column('milestones', 'is_active');
+ $dbh->bz_drop_column('milestones', 'is_searchable');
+ }
+}
+
+sub _last_closed_date {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ return $self->{'last_closed_date'} if defined $self->{'last_closed_date'};
+
+ my $closed_statuses = "'" . join("','", map { $_->name } closed_bug_statuses()) . "'";
+ my $status_field_id = get_field_id('bug_status');
+
+ $self->{'last_closed_date'} = $dbh->selectrow_array("
+ SELECT bugs_activity.bug_when
+ FROM bugs_activity
+ WHERE bugs_activity.fieldid = ?
+ AND bugs_activity.added IN ($closed_statuses)
+ AND bugs_activity.bug_id = ?
+ ORDER BY bugs_activity.bug_when DESC " . $dbh->sql_limit(1),
+ undef, $status_field_id, $self->id
+ );
+
+ return $self->{'last_closed_date'};
+}
+
+sub field_end_of_create {
+ my ($self, $args) = @_;
+ my $field = $args->{'field'};
+
+ # Create an IT bug so Mozilla's DBAs so they can update the grants for metrics
+
+ if (Bugzilla->params->{'urlbase'} ne 'https://bugzilla.mozilla.org/'
+ && Bugzilla->params->{'urlbase'} ne 'https://bugzilla.allizom.org/')
+ {
+ return;
+ }
+
+ my $name = $field->name;
+
+ if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
+ Bugzilla->set_user(Bugzilla::User->check({ name => 'nobody@mozilla.org' }));
+ print "Creating IT permission grant bug for new field '$name'...";
+ }
+
+ my $bug_data = {
+ short_desc => "Custom field '$name' added to bugzilla.mozilla.org",
+ product => 'mozilla.org',
+ component => 'Server Operations: Database',
+ bug_severity => 'normal',
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'other',
+ };
+
+ my $comment = <<COMMENT;
+The custom field '$name' has been added to the BMO database.
+Please run the following on bugzilla1.db.scl3.mozilla.com:
+COMMENT
+
+ if ($field->type == FIELD_TYPE_SINGLE_SELECT
+ || $field->type == FIELD_TYPE_MULTI_SELECT) {
+ $comment .= <<COMMENT;
+ GRANT SELECT ON `bugs`.`$name` TO 'metrics'\@'10.22.70.20_';
+ GRANT SELECT ON `bugs`.`$name` TO 'metrics'\@'10.22.70.21_';
+COMMENT
+ }
+ if ($field->type == FIELD_TYPE_MULTI_SELECT) {
+ $comment .= <<COMMENT;
+ GRANT SELECT ON `bugs`.`bug_$name` TO 'metrics'\@'10.22.70.20_';
+ GRANT SELECT ON `bugs`.`bug_$name` TO 'metrics'\@'10.22.70.21_';
+COMMENT
+ }
+ if ($field->type != FIELD_TYPE_MULTI_SELECT) {
+ $comment .= <<COMMENT;
+ GRANT SELECT ($name) ON `bugs`.`bugs` TO 'metrics'\@'10.22.70.20_';
+ GRANT SELECT ($name) ON `bugs`.`bugs` TO 'metrics'\@'10.22.70.21_';
+COMMENT
+ }
+
+ $bug_data->{'comment'} = $comment;
+
+ my $old_error_mode = Bugzilla->error_mode;
+ Bugzilla->error_mode(ERROR_MODE_DIE);
+
+ my $new_bug = eval { Bugzilla::Bug->create($bug_data) };
+
+ my $error = $@;
+ undef $@;
+ Bugzilla->error_mode($old_error_mode);
+
+ if ($error || !($new_bug && $new_bug->{'bug_id'})) {
+ warn "Error creating IT bug for new field $name: $error";
+ if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
+ print "\nError: $error\n";
+ }
+ }
+ else {
+ Bugzilla::BugMail::Send($new_bug->id, { changer => Bugzilla->user });
+ if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
+ print "bug " . $new_bug->id . " created.\n";
+ }
+ }
+}
+
+sub webservice {
+ my ($self, $args) = @_;
+
+ my $dispatch = $args->{dispatch};
+ $dispatch->{BMO} = "Bugzilla::Extension::BMO::WebService";
+}
+
+our $search_content_matches;
+BEGIN {
+ $search_content_matches = \&Bugzilla::Search::_content_matches;
+}
+
+sub search_operator_field_override {
+ my ($self, $args) = @_;
+ my $search = $args->{'search'};
+ my $operators = $args->{'operators'};
+
+ my $cgi = Bugzilla->cgi;
+ my @comments = $cgi->param('comments');
+ my $exclude_comments = scalar(@comments) && !grep { $_ eq '1' } @comments;
+
+ if ($cgi->param('query_format')
+ && $cgi->param('query_format') eq 'specific'
+ && $exclude_comments
+ ) {
+ # use the non-comment operator
+ $operators->{'content'}->{matches} = \&_short_desc_matches;
+ $operators->{'content'}->{notmatches} = \&_short_desc_matches;
+
+ } else {
+ # restore default content operator
+ $operators->{'content'}->{matches} = $search_content_matches;
+ $operators->{'content'}->{notmatches} = $search_content_matches;
+ }
+}
+
+sub _short_desc_matches {
+ # copy of Bugzilla::Search::_content_matches with comment searching removed
+
+ my ($self, $args) = @_;
+ my ($chart_id, $joins, $fields, $operator, $value) =
+ @$args{qw(chart_id joins fields operator value)};
+ my $dbh = Bugzilla->dbh;
+
+ # Add the fulltext table to the query so we can search on it.
+ my $table = "bugs_fulltext_$chart_id";
+ push(@$joins, { table => 'bugs_fulltext', as => $table });
+
+ # Create search terms to add to the SELECT and WHERE clauses.
+ my ($term, $rterm) =
+ $dbh->sql_fulltext_search("$table.short_desc", $value, 2);
+ $rterm = $term if !$rterm;
+
+ # The term to use in the WHERE clause.
+ if ($operator =~ /not/i) {
+ $term = "NOT($term)";
+ }
+ $args->{term} = $term;
+
+ my $current = $self->COLUMNS->{'relevance'}->{name};
+ $current = $current ? "$current + " : '';
+ # For NOT searches, we just add 0 to the relevance.
+ my $select_term = $operator =~ /not/ ? 0 : "($current$rterm)";
+ $self->COLUMNS->{'relevance'}->{name} = $select_term;
+}
+
+sub mailer_before_send {
+ my ($self, $args) = @_;
+ my $email = $args->{email};
+
+ _log_sent_email($email);
+
+ # $bug->mentors is added by the Review extension
+ if (Bugzilla::Bug->can('mentors')) {
+ _add_mentors_header($email);
+ }
+
+ # insert x-bugzilla headers into the body
+ _inject_headers_into_body($email);
+}
+
+# Log a summary of bugmail sent to the syslog, for auditing and monitoring
+sub _log_sent_email {
+ my $email = shift;
+
+ my $recipient = $email->header('to');
+ return unless $recipient;
+
+ my $subject = $email->header('Subject');
+
+ my $bug_id = $email->header('X-Bugzilla-ID');
+ if (!$bug_id && $subject =~ /[\[\(]Bug (\d+)/i) {
+ $bug_id = $1;
+ }
+ $bug_id = $bug_id ? "bug-$bug_id" : '-';
+
+ my $message_type;
+ my $type = $email->header('X-Bugzilla-Type');
+ my $reason = $email->header('X-Bugzilla-Reason');
+ if ($type eq 'whine' || $type eq 'request' || $type eq 'admin') {
+ $message_type = $type;
+ } elsif ($reason && $reason ne 'None') {
+ $message_type = $reason;
+ } else {
+ $message_type = $email->header('X-Bugzilla-Watch-Reason');
+ }
+ $message_type ||= $type || '?';
+
+ $subject =~ s/[\[\(]Bug \d+[\]\)]\s*//;
+
+ _syslog("[bugmail] $recipient ($message_type) $bug_id $subject");
+}
+
+# Add X-Bugzilla-Mentors field to bugmail
+sub _add_mentors_header {
+ my $email = shift;
+ return unless my $bug_id = $email->header('X-Bugzilla-ID');
+ return unless my $bug = Bugzilla::Bug->new({ id => $bug_id, cache => 1 });
+ return unless my $mentors = $bug->mentors;
+ return unless @$mentors;
+ $email->header_set('X-Bugzilla-Mentors', join(', ', map { $_->login } @$mentors));
+}
+
+sub _inject_headers_into_body {
+ my $email = shift;
+ my $replacement = '';
+
+ my $recipient = Bugzilla::User->new({ name => $email->header('To'), cache => 1 });
+ if ($recipient
+ && $recipient->settings->{headers_in_body}->{value} eq 'on')
+ {
+ my @headers;
+ my $it = natatime(2, $email->header_pairs);
+ while (my ($name, $value) = $it->()) {
+ next unless $name =~ /^X-Bugzilla-(.+)/;
+ if ($name eq 'X-Bugzilla-Flags' || $name eq 'X-Bugzilla-Changed-Field-Names') {
+ # these are multi-value fields, split on space
+ foreach my $v (split(/\s+/, $value)) {
+ push @headers, "$name: $v";
+ }
+ }
+ elsif ($name eq 'X-Bugzilla-Changed-Fields') {
+ # cannot split on space for this field, because field names contain
+ # spaces. instead work from a list of field names.
+ my @fields =
+ map { $_->description }
+ @{ Bugzilla->fields };
+ # these aren't real fields, but exist in the headers
+ push @fields, ('Comment Created', 'Attachment Created');
+ @fields =
+ sort { length($b) <=> length($a) }
+ @fields;
+ while ($value ne '') {
+ foreach my $field (@fields) {
+ if ($value eq $field) {
+ push @headers, "$name: $field";
+ $value = '';
+ last;
+ }
+ if (substr($value, 0, length($field) + 1) eq $field . ' ') {
+ push @headers, "$name: $field";
+ $value = substr($value, length($field) + 1);
+ last;
+ }
+ }
+ }
+ }
+ else {
+ push @headers, "$name: $value";
+ }
+ }
+ $replacement = join("\n", @headers);
+ }
+
+ # update the message body
+ if (scalar($email->parts) > 1) {
+ $email->walk_parts(sub {
+ my ($part) = @_;
+
+ # skip top-level
+ return if $part->parts > 1;
+
+ # do not filter attachments such as patches, etc.
+ return if
+ $part->header('Content-Disposition')
+ && $part->header('Content-Disposition') =~ /attachment/;
+
+ # text/plain|html only
+ return unless $part->content_type =~ /^text\/(?:html|plain)/;
+
+ # hide in html content
+ if ($replacement && $part->content_type =~ /^text\/html/) {
+ $replacement = '<pre style="font-size: 0pt; color: #fff">' . $replacement . '</pre>';
+ }
+
+ # and inject
+ _replace_placeholder_in_part($part, $replacement);
+ });
+
+ # force Email::MIME to re-create all the parts. without this
+ # as_string() doesn't return the updated body for multi-part sub-parts.
+ $email->parts_set([ $email->subparts ]);
+ }
+ else {
+ # text-only email
+ _replace_placeholder_in_part($email, $replacement);
+ }
+}
+
+sub _replace_placeholder_in_part {
+ my ($part, $replacement) = @_;
+
+ # fix encoding
+ my $body = $part->body;
+ if (Bugzilla->params->{'utf8'}) {
+ $part->charset_set('UTF-8');
+ my $raw = $part->body_raw;
+ if (utf8::is_utf8($raw)) {
+ utf8::encode($raw);
+ $part->body_set($raw);
+ }
+ }
+ $part->encoding_set('quoted-printable') if !is_7bit_clean($body);
+
+ # replace
+ my $placeholder = quotemeta('@@body-headers@@');
+ $body = $part->body_str;
+ $body =~ s/$placeholder/$replacement/;
+ $part->body_str_set($body);
+}
+
+sub _syslog {
+ my $message = shift;
+ openlog('apache', 'cons,pid', 'local4');
+ syslog('notice', encode_utf8($message));
+ closelog();
+}
+
+sub post_bug_after_creation {
+ my ($self, $args) = @_;
+ return unless my $format = Bugzilla->input_params->{format};
+ my $bug = $args->{vars}->{bug};
+
+ if ($format eq 'employee-incident'
+ && $bug->component eq 'Server Operations: Desktop Issues')
+ {
+ $self->_post_employee_incident_bug($args);
+ }
+ elsif ($format eq 'swag') {
+ $self->_post_gear_bug($args);
+ }
+ elsif ($format eq 'mozpr') {
+ $self->_post_mozpr_bug($args);
+ }
+}
+
+sub _post_employee_incident_bug {
+ my ($self, $args) = @_;
+ my $vars = $args->{vars};
+ my $bug = $vars->{bug};
+
+ my $error_mode_cache = Bugzilla->error_mode;
+ Bugzilla->error_mode(ERROR_MODE_DIE);
+
+ my $template = Bugzilla->template;
+ my $cgi = Bugzilla->cgi;
+
+ my ($investigate_bug, $ssh_key_bug);
+ my $old_user = Bugzilla->user;
+ eval {
+ Bugzilla->set_user(Bugzilla::User->new({ name => 'nobody@mozilla.org' }));
+ my $new_user = Bugzilla->user;
+
+ # HACK: User needs to be in the editbugs and primary bug's group to allow
+ # setting of dependencies.
+ $new_user->{'groups'} = [ Bugzilla::Group->new({ name => 'editbugs' }),
+ Bugzilla::Group->new({ name => 'infra' }),
+ Bugzilla::Group->new({ name => 'infrasec' }) ];
+
+ my $recipients = { changer => $new_user };
+ $vars->{original_reporter} = $old_user;
+
+ my $comment;
+ $cgi->param('display_action', '');
+ $template->process('bug/create/comment-employee-incident.txt.tmpl', $vars, \$comment)
+ || ThrowTemplateError($template->error());
+
+ $investigate_bug = Bugzilla::Bug->create({
+ short_desc => 'Investigate Lost Device',
+ product => 'mozilla.org',
+ component => 'Security Assurance: Incident',
+ status_whiteboard => '[infrasec:incident]',
+ bug_severity => 'critical',
+ cc => [ 'jstevensen@mozilla.com' ],
+ groups => [ 'infrasec' ],
+ comment => $comment,
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'other',
+ dependson => $bug->bug_id,
+ });
+ $bug->set_all({ blocked => { add => [ $investigate_bug->bug_id ] }});
+ Bugzilla::BugMail::Send($investigate_bug->id, $recipients);
+
+ Bugzilla->set_user($old_user);
+ $vars->{original_reporter} = '';
+ $comment = '';
+ $cgi->param('display_action', 'ssh');
+ $template->process('bug/create/comment-employee-incident.txt.tmpl', $vars, \$comment)
+ || ThrowTemplateError($template->error());
+
+ $ssh_key_bug = Bugzilla::Bug->create({
+ short_desc => 'Disable/Regenerate SSH Key',
+ product => $bug->product,
+ component => $bug->component,
+ bug_severity => 'critical',
+ cc => $bug->cc,
+ groups => [ map { $_->{name} } @{ $bug->groups } ],
+ comment => $comment,
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'other',
+ dependson => $bug->bug_id,
+ });
+ $bug->set_all({ blocked => { add => [ $ssh_key_bug->bug_id ] }});
+ Bugzilla::BugMail::Send($ssh_key_bug->id, $recipients);
+ };
+ my $error = $@;
+
+ Bugzilla->set_user($old_user);
+ Bugzilla->error_mode($error_mode_cache);
+
+ if ($error || !$investigate_bug || !$ssh_key_bug) {
+ warn "Failed to create additional employee-incident bug: $error" if $error;
+ $vars->{'message'} = 'employee_incident_creation_failed';
+ }
+}
+
+sub _post_gear_bug {
+ my ($self, $args) = @_;
+ my $vars = $args->{vars};
+ my $bug = $vars->{bug};
+ my $input = Bugzilla->input_params;
+
+ my ($team, $code) = $input->{teamcode} =~ /^(.+?) \((\d+)\)$/;
+ my @request = (
+ "Date Required: $input->{date_required}",
+ "$input->{firstname} $input->{lastname}",
+ $input->{email},
+ $input->{mozspace},
+ $team,
+ $code,
+ $input->{purpose},
+ );
+ my @recipient = (
+ "$input->{shiptofirstname} $input->{shiptolastname}",
+ $input->{shiptoemail},
+ $input->{shiptoaddress1},
+ $input->{shiptoaddress2},
+ $input->{shiptocity},
+ $input->{shiptostate},
+ $input->{shiptopostcode},
+ $input->{shiptocountry},
+ "Phone: $input->{shiptophone}",
+ $input->{shiptoidrut},
+ );
+
+ # the csv has 14 item fields
+ my @items = map { trim($_) } split(/\n/, $input->{items});
+ my @csv;
+ while (@items) {
+ my @batch;
+ if (scalar(@items) > 14) {
+ @batch = splice(@items, 0, 14);
+ }
+ else {
+ @batch = @items;
+ push @batch, '' for scalar(@items)..13;
+ @items = ();
+ }
+ push @csv, [ @request, @batch, @recipient ];
+ }
+
+ # csv quoting and concat
+ foreach my $line (@csv) {
+ foreach my $field (@$line) {
+ if ($field =~ s/"/""/g || $field =~ /,/) {
+ $field = qq#"$field"#;
+ }
+ }
+ $line = join(',', @$line);
+ }
+
+ $self->_add_attachment($args, {
+ data => join("\n", @csv),
+ description => "Items (CSV)",
+ filename => "gear_" . $bug->id . ".csv",
+ mimetype => "text/csv",
+ });
+ $bug->update($bug->creation_ts);
+}
+
+sub _post_mozpr_bug {
+ my ($self, $args) = @_;
+ my $vars = $args->{vars};
+ my $bug = $vars->{bug};
+ my $input = Bugzilla->input_params;
+
+ if ($input->{proj_mat_file}) {
+ $self->_add_attachment($args, {
+ data => $input->{proj_mat_file_attach},
+ description => $input->{proj_mat_file_desc},
+ filename => scalar $input->{proj_mat_file_attach},
+ });
+ }
+ if ($input->{pr_mat_file}) {
+ $self->_add_attachment($args, {
+ data => $input->{pr_mat_file_attach},
+ description => $input->{pr_mat_file_desc},
+ filename => scalar $input->{pr_mat_file_attach},
+ });
+ }
+ $bug->update($bug->creation_ts);
+}
+
+sub _add_attachment {
+ my ($self, $args, $attachment_args) = @_;
+
+ my $bug = $args->{vars}->{bug};
+ $attachment_args->{bug} = $bug;
+ $attachment_args->{creation_ts} = $bug->creation_ts;
+ $attachment_args->{ispatch} = 0 unless exists $attachment_args->{ispatch};
+ $attachment_args->{isprivate} = 0 unless exists $attachment_args->{isprivate};
+ $attachment_args->{mimetype} ||= $self->_detect_content_type($attachment_args->{data});
+
+ # If the attachment cannot be successfully added to the bug,
+ # we notify the user, but we don't interrupt the bug creation process.
+ my $old_error_mode = Bugzilla->error_mode;
+ Bugzilla->error_mode(ERROR_MODE_DIE);
+ my $attachment;
+ eval {
+ $attachment = Bugzilla::Attachment->create($attachment_args);
+ };
+ warn "$@" if $@;
+ Bugzilla->error_mode($old_error_mode);
+
+ if ($attachment) {
+ # Insert comment for attachment
+ $bug->add_comment('', { isprivate => 0,
+ type => CMT_ATTACHMENT_CREATED,
+ extra_data => $attachment->id });
+ delete $bug->{attachments};
+ }
+ else {
+ $args->{vars}->{'message'} = 'attachment_creation_failed';
+ }
+
+ # Note: you must call $bug->update($bug->creation_ts) after adding all attachments
+}
+
+# bugzilla's content_type detection makes assumptions about form fields, which
+# means we can't use it here. this code is lifted from
+# Bugzilla::Attachment::get_content_type and the TypeSniffer extension.
+sub _detect_content_type {
+ my ($self, $data) = @_;
+ my $cgi = Bugzilla->cgi;
+
+ # browser provided content-type
+ my $content_type = $cgi->uploadInfo($data)->{'Content-Type'};
+ $content_type = 'image/png' if $content_type eq 'image/x-png';
+
+ if ($content_type eq 'application/octet-stream') {
+ # detect from filename
+ my $filename = scalar($data);
+ if (my $from_filename = mimetype($filename)) {
+ return $from_filename;
+ }
+ }
+
+ return $content_type || 'application/octet-stream';
+}
+
+sub buglist_columns {
+ my ($self, $args) = @_;
+ my $columns = $args->{columns};
+ $columns->{'cc_count'} = {
+ name => '(SELECT COUNT(*) FROM cc WHERE cc.bug_id = bugs.bug_id)',
+ title => 'CC Count',
+ };
+ $columns->{'dupe_count'} = {
+ name => '(SELECT COUNT(*) FROM duplicates WHERE duplicates.dupe_of = bugs.bug_id)',
+ title => 'Duplicate Count',
+ };
+}
+
+sub enter_bug_start {
+ my ($self, $args) = @_;
+ # if configured with create_bug_formats, force users into a custom bug
+ # format (can be overridden with a __standard__ format)
+ my $cgi = Bugzilla->cgi;
+ if ($cgi->param('format') && $cgi->param('format') eq '__standard__') {
+ $cgi->delete('format');
+ } elsif (my $format = forced_format($cgi->param('product'))) {
+ $cgi->param('format', $format);
+ }
+
+ # If product eq 'mozilla.org' and format eq 'itrequest', then
+ # switch to the new 'Infrastructure & Operations' product.
+ if ($cgi->param('product') && $cgi->param('product') eq 'mozilla.org'
+ && $cgi->param('format') && $cgi->param('format') eq 'itrequest')
+ {
+ $cgi->param('product', 'Infrastructure & Operations');
+ }
+
+ # map renamed groups
+ $cgi->param('groups', _map_groups($cgi->param('groups')));
+}
+
+sub bug_before_create {
+ my ($self, $args) = @_;
+ my $params = $args->{params};
+ if (exists $params->{groups}) {
+ # map renamed groups
+ $params->{groups} = [ _map_groups($params->{groups}) ];
+ }
+}
+
+sub _map_groups {
+ my (@groups) = @_;
+ return unless @groups;
+ @groups = @{ $groups[0] } if ref($groups[0]);
+ return map {
+ # map mozilla-corporation-confidential => mozilla-employee-confidential
+ $_ eq 'mozilla-corporation-confidential'
+ ? 'mozilla-employee-confidential'
+ : $_
+ } @groups;
+}
+
+sub forced_format {
+ # note: this is also called from the guided bug entry extension
+ my ($product) = @_;
+ return undef unless defined $product;
+
+ # always work on the correct product name
+ $product = Bugzilla::Product->new({ name => $product, cache => 1 })
+ unless blessed($product);
+ return undef unless $product;
+
+ # check for a forced-format entry
+ my $forced = $create_bug_formats{$product->name}
+ || return;
+
+ # should this user be included?
+ my $user = Bugzilla->user;
+ my $include = ref($forced->{include}) ? $forced->{include} : [ $forced->{include} ];
+ foreach my $inc (@$include) {
+ return $forced->{format} if $user->in_group($inc);
+ }
+
+ return undef;
+}
+
+sub query_database {
+ my ($vars) = @_;
+
+ # validate group membership
+ my $user = Bugzilla->user;
+ $user->in_group('query_database')
+ || ThrowUserError('auth_failure', { group => 'query_database',
+ action => 'access',
+ object => 'query_database' });
+
+ # read query
+ my $input = Bugzilla->input_params;
+ my $query = $input->{query};
+ $vars->{query} = $query;
+
+ if ($query) {
+ trick_taint($query);
+ $vars->{executed} = 1;
+
+ # add limit if missing
+ if ($query !~ /\sLIMIT\s+\d+\s*$/si) {
+ $query .= ' LIMIT 1000';
+ $vars->{query} = $query;
+ }
+
+ # log query
+ _syslog(sprintf("[db_query] %s %s", $user->login, $query));
+
+ # connect to database and execute
+ # switching to the shadow db gives us a read-only connection
+ my $dbh = Bugzilla->switch_to_shadow_db();
+ my $sth;
+ eval {
+ $sth = $dbh->prepare($query);
+ $sth->execute();
+ };
+ if ($@) {
+ $vars->{sql_error} = $@;
+ return;
+ }
+
+ # build result
+ my $columns = $sth->{NAME};
+ my $rows;
+ while (my @row = $sth->fetchrow_array) {
+ push @$rows, \@row;
+ }
+
+ # return results
+ $vars->{columns} = $columns;
+ $vars->{rows} = $rows;
+ }
+}
+
+# you can always file bugs into a product's default security group, as well as
+# into any of the groups in @always_fileable_groups
+sub _group_always_settable {
+ my ($self, $group) = @_;
+ return
+ $group->name eq $self->default_security_group
+ || ((grep { $_ eq $group->name } @always_fileable_groups) ? 1 : 0);
+}
+
+sub _default_security_group {
+ my ($self) = @_;
+ return exists $product_sec_groups{$self->name}
+ ? $product_sec_groups{$self->name}
+ : $product_sec_groups{_default};
+}
+
+sub _default_security_group_obj {
+ my ($self) = @_;
+ return unless my $group_name = $self->default_security_group;
+ return Bugzilla::Group->new({ name => $group_name, cache => 1 })
+}
+
+# called from the verify version, component, and group page.
+# if we're making a group invalid, stuff the default group into the cgi param
+# to make it checked by default.
+sub _check_default_product_security_group {
+ my ($self, $product, $invalid_groups, $optional_group_controls) = @_;
+ return unless my $group = $product->default_security_group_obj;
+ if (@$invalid_groups) {
+ my $cgi = Bugzilla->cgi;
+ my @groups = $cgi->param('groups');
+ push @groups, $group->name unless grep { $_ eq $group->name } @groups;
+ $cgi->param('groups', @groups);
+ }
+}
+
+sub install_filesystem {
+ my ($self, $args) = @_;
+ my $files = $args->{files};
+ my $extensions_dir = bz_locations()->{extensionsdir};
+ $files->{"$extensions_dir/BMO/bin/migrate-github-pull-requests.pl"} = {
+ perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE
+ };
+}
+
+# "deleted" comment tag
+
+sub config_modify_panels {
+ my ($self, $args) = @_;
+ push @{ $args->{panels}->{groupsecurity}->{params} }, {
+ name => 'delete_comments_group',
+ type => 's',
+ choices => \&Bugzilla::Config::GroupSecurity::_get_all_group_names,
+ default => 'admin',
+ checker => \&check_group
+ };
+}
+
+sub comment_after_add_tag {
+ my ($self, $args) = @_;
+ my $tag = $args->{tag};
+ return unless lc($tag) eq 'deleted';
+
+ my $group_name = Bugzilla->params->{delete_comments_group};
+ if (!$group_name || !Bugzilla->user->in_group($group_name)) {
+ ThrowUserError('auth_failure', { group => $group_name,
+ action => 'delete',
+ object => 'comments' });
+ }
+}
+
+sub comment_after_remove_tag {
+ my ($self, $args) = @_;
+ my $tag = $args->{tag};
+ return unless lc($tag) eq 'deleted';
+
+ my $group_name = Bugzilla->params->{delete_comments_group};
+ if (!$group_name || !Bugzilla->user->in_group($group_name)) {
+ ThrowUserError('auth_failure', { group => $group_name,
+ action => 'delete',
+ object => 'comments' });
+ }
+}
+
+BEGIN {
+ *Bugzilla::Comment::has_tag = \&_comment_has_tag;
+}
+
+sub _comment_has_tag {
+ my ($self, $test_tag) = @_;
+ $test_tag = lc($test_tag);
+ foreach my $tag (@{ $self->tags }) {
+ return 1 if lc($tag) eq $test_tag;
+ }
+ return 0;
+}
+
+sub bug_comments {
+ my ($self, $args) = @_;
+ my $can_delete = Bugzilla->user->in_group(Bugzilla->params->{delete_comments_group});
+ my $comments = $args->{comments};
+ my @deleted = grep { $_->has_tag('deleted') } @$comments;
+ while (my $comment = pop @deleted) {
+ for (my $i = scalar(@$comments) - 1; $i >= 0; $i--) {
+ if ($comment == $comments->[$i]) {
+ if ($can_delete) {
+ # don't remove comment from users who can "delete" them
+ # just collapse it instead
+ $comment->{collapsed} = 1;
+ }
+ else {
+ # otherwise, remove it from the array
+ splice(@$comments, $i, 1);
+ }
+ last;
+ }
+ }
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/BMO/bin/bug_1022707.pl b/extensions/BMO/bin/bug_1022707.pl
new file mode 100755
index 000000000..c27757220
--- /dev/null
+++ b/extensions/BMO/bin/bug_1022707.pl
@@ -0,0 +1,50 @@
+#!/usr/bin/perl
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../..";
+
+use Bugzilla;
+use Bugzilla::Constants qw( USAGE_MODE_CMDLINE );
+BEGIN { Bugzilla->extensions() }
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my $dbh = Bugzilla->dbh;
+
+my $sql = q{
+ SELECT flags.id FROM flags
+ INNER JOIN bugs ON bugs.bug_id = flags.bug_id
+ WHERE type_id = 748
+ AND bugs.product_id != 21
+};
+
+print "Searching for suitable flags..\n";
+my $flag_ids = $dbh->selectcol_arrayref($sql);
+my $total = @$flag_ids;
+
+die "No suitable flags found\n" unless $total;
+print "About to fix $total flags\n";
+print "Press <enter> to start, or ^C to cancel...\n";
+readline;
+
+my $update_fsa_sql= "UPDATE flag_state_activity SET type_id = 4 WHERE " . $dbh->sql_in('flag_id', $flag_ids);
+my $update_flags_sql = "UPDATE flags SET type_id = 4 WHERE " . $dbh->sql_in('id', $flag_ids);
+
+$dbh->bz_start_transaction();
+$dbh->do($update_fsa_sql);
+$dbh->do($update_flags_sql);
+$dbh->bz_commit_transaction();
+
+Bugzilla->memcached->clear_all();
+
+print "Done.\n";
diff --git a/extensions/BMO/bin/migrate-github-pull-requests.pl b/extensions/BMO/bin/migrate-github-pull-requests.pl
new file mode 100755
index 000000000..de71a7856
--- /dev/null
+++ b/extensions/BMO/bin/migrate-github-pull-requests.pl
@@ -0,0 +1,90 @@
+#!/usr/bin/perl
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../..";
+
+use Bugzilla;
+BEGIN { Bugzilla->extensions() }
+
+use Bugzilla::Extension::BMO::Data;
+use Bugzilla::Field;
+use Bugzilla::Install::Util qw(indicate_progress);
+use Bugzilla::User;
+use Bugzilla::Util qw(trim);
+
+my $dbh = Bugzilla->dbh;
+my $nobody = Bugzilla::User->check({ name => 'nobody@mozilla.org' });
+my $field = Bugzilla::Field->check({ name => 'attachments.mimetype' });
+
+# grab list of suitable attachments
+
+my $sql = <<EOF;
+SELECT attachments.attach_id,
+ attachments.bug_id,
+ attachments.mimetype,
+ attach_data.thedata
+ FROM attachments
+ INNER JOIN attach_data ON attach_data.id = attachments.attach_id
+ WHERE ispatch = 0
+ AND mimetype = 'text/plain'
+ AND thedata IS NOT NULL
+ AND LENGTH(thedata) > 0
+ AND LENGTH(thedata) <= 256
+EOF
+print "Searching for suitable attachments..\n";
+my $attachments = $dbh->selectall_arrayref($sql, { Slice => {} });
+my ($current, $total, $updated) = (1, scalar(@$attachments), 0);
+
+die "No suitable attachments found\n" unless $total;
+print "About to check $total attachments for github pull requests, and\n";
+print "update content-type if required.\n";
+print "Press <enter> to start, or ^C to cancel...\n";
+<>;
+
+foreach my $attachment (@$attachments) {
+ indicate_progress({ current => $current++, total => $total, every => 25 });
+
+ # check payload
+ my $url = trim($attachment->{thedata});
+ next if $url =~ /\s/;
+ next unless $url =~ m#^https://github\.com/[^/]+/[^/]+/pull/\d+\/?$#i;
+
+ $dbh->bz_start_transaction;
+
+ # set content-type
+ $dbh->do(
+ "UPDATE attachments SET mimetype = ? WHERE attach_id = ?",
+ undef,
+ GITHUB_PR_CONTENT_TYPE, $attachment->{attach_id}
+ );
+
+ # insert into bugs_activity
+ my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ $dbh->do(
+ "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added)
+ VALUES (?, ?, ?, ?, ?, ?)",
+ undef,
+ $attachment->{bug_id}, $nobody->id, $timestamp, $field->id,
+ $attachment->{mimetype}, GITHUB_PR_CONTENT_TYPE
+ );
+ $dbh->do(
+ "UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?",
+ undef,
+ $timestamp, $timestamp, $attachment->{bug_id}
+ );
+
+ $dbh->bz_commit_transaction;
+ $updated++;
+}
+
+print "Attachments updated: $updated\n";
diff --git a/extensions/BMO/lib/Constants.pm b/extensions/BMO/lib/Constants.pm
new file mode 100644
index 000000000..23eaae9cb
--- /dev/null
+++ b/extensions/BMO/lib/Constants.pm
@@ -0,0 +1,33 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the BMO Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2007
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# David Lawrence <dkl@mozilla.com>
+
+package Bugzilla::Extension::BMO::Constants;
+use strict;
+use base qw(Exporter);
+our @EXPORT = qw(
+ REQUEST_MAX_ATTACH_LINES
+);
+
+# Maximum attachment size in lines that will be sent with a
+# requested attachment flag notification.
+use constant REQUEST_MAX_ATTACH_LINES => 1000;
+
+1;
diff --git a/extensions/BMO/lib/Data.pm b/extensions/BMO/lib/Data.pm
new file mode 100644
index 000000000..7d0ec05fd
--- /dev/null
+++ b/extensions/BMO/lib/Data.pm
@@ -0,0 +1,242 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BMO::Data;
+use strict;
+
+use base qw(Exporter);
+use Tie::IxHash;
+
+our @EXPORT = qw( $cf_visible_in_products
+ %group_change_notification
+ $cf_setters
+ @always_fileable_groups
+ %group_auto_cc
+ %product_sec_groups
+ %create_bug_formats
+ @default_named_queries
+ GITHUB_PR_CONTENT_TYPE
+ RB_REQUEST_CONTENT_TYPE );
+
+use constant GITHUB_PR_CONTENT_TYPE => 'text/x-github-pull-request';
+use constant RB_REQUEST_CONTENT_TYPE => 'text/x-review-board-request';
+
+# Which custom fields are visible in which products and components.
+#
+# By default, custom fields are visible in all products. However, if the name
+# of the field matches any of these regexps, it is only visible if the
+# product (and component if necessary) is a member of the attached hash. []
+# for component means "all".
+#
+# IxHash keeps them in insertion order, and so we get regexp priorities right.
+our $cf_visible_in_products;
+tie(%$cf_visible_in_products, "Tie::IxHash",
+ qr/^cf_colo_site$/ => {
+ "mozilla.org" => [
+ "Server Operations",
+ "Server Operations: DCOps",
+ "Server Operations: Projects",
+ "Server Operations: RelEng",
+ "Server Operations: Security",
+ ],
+ "Infrastructure & Operations" => [
+ "RelOps",
+ "RelOps: Puppet"
+ ],
+ },
+ qw/^cf_office$/ => {
+ "mozilla.org" => ["Server Operations: Desktop Issues"],
+ },
+ qr/^cf_crash_signature$/ => {
+ "Add-on SDK" => [],
+ "addons.mozilla.org" => [],
+ "Android Background Services" => [],
+ "Calendar" => [],
+ "Camino" => [],
+ "Composer" => [],
+ "Core" => [],
+ "Directory" => [],
+ "Fennec" => [],
+ "Firefox" => [],
+ "Firefox for Android" => [],
+ "Firefox for Metro" => [],
+ "Firefox OS" => [],
+ "JSS" => [],
+ "MailNews Core" => [],
+ "Mozilla Labs" => [],
+ "Mozilla Localizations" => [],
+ "mozilla.org" => [],
+ "Mozilla Services" => [],
+ "NSPR" => [],
+ "NSS" => [],
+ "Other Applications" => [],
+ "Penelope" => [],
+ "Plugins" => [],
+ "Release Engineering" => [],
+ "Rhino" => [],
+ "SeaMonkey" => [],
+ "Tamarin" => [],
+ "Tech Evangelism" => [],
+ "Testing" => [],
+ "Thunderbird" => [],
+ "Toolkit" => [],
+ },
+ qw/^cf_due_date$/ => {
+ "bugzilla.mozilla.org" => [],
+ "Community Building" => [],
+ "Data & BI Services Team" => [],
+ "Developer Engagement" => [],
+ "Infrastructure & Operations" => [],
+ "Marketing" => [],
+ "mozilla.org" => ["Security Assurance: Review Request"],
+ "Mozilla Reps" => [],
+ },
+ qw/^cf_locale$/ => {
+ "Mozilla Localizations" => ['Other'],
+ "www.mozilla.org" => [],
+ },
+ qw/^cf_mozilla_project$/ => {
+ "Data & BI Services Team" => [],
+ },
+ qw/^cf_machine_state$/ => {
+ "Release Engineering" => ["Buildduty"],
+ },
+);
+
+# Who to CC on particular bugmails when certain groups are added or removed.
+our %group_change_notification = (
+ 'addons-security' => ['amo-editors@mozilla.org'],
+ 'bugzilla-security' => ['security@bugzilla.org'],
+ 'client-services-security' => ['amo-admins@mozilla.org', 'web-security@mozilla.org'],
+ 'core-security' => ['security@mozilla.org'],
+ 'mozilla-services-security' => ['web-security@mozilla.org'],
+ 'tamarin-security' => ['tamarinsecurity@adobe.com'],
+ 'websites-security' => ['web-security@mozilla.org'],
+ 'webtools-security' => ['web-security@mozilla.org'],
+);
+
+# Who can set custom flags (use full field names only, not regex's)
+our $cf_setters = {
+ 'cf_colo_site' => ['infra', 'build'],
+};
+
+# Groups in which you can always file a bug, regardless of product or user.
+our @always_fileable_groups = qw(
+ addons-security
+ bugzilla-security
+ client-services-security
+ consulting
+ core-security
+ finance
+ infra
+ infrasec
+ l20n-security
+ marketing-private
+ mozilla-confidential
+ mozilla-employee-confidential
+ mozilla-foundation-confidential
+ mozilla-engagement
+ mozilla-messaging-confidential
+ partner-confidential
+ payments-confidential
+ tamarin-security
+ websites-security
+ webtools-security
+ winqual-data
+);
+
+# Mapping of products to their security bits
+our %product_sec_groups = (
+ "addons.mozilla.org" => 'client-services-security',
+ "Air Mozilla" => 'mozilla-employee-confidential',
+ "Android Background Services" => 'mozilla-services-security',
+ "Audio/Visual Infrastructure" => 'mozilla-employee-confidential',
+ "AUS" => 'client-services-security',
+ "Bugzilla" => 'bugzilla-security',
+ "bugzilla.mozilla.org" => 'bugzilla-security',
+ "Community Tools" => 'websites-security',
+ "Data & BI Services Team" => 'metrics-private',
+ "Developer Documentation" => 'websites-security',
+ "Developer Ecosystem" => 'client-services-security',
+ "Finance" => 'finance',
+ "Firefox Health Report" => 'mozilla-services-security',
+ "Infrastructure & Operations" => 'mozilla-employee-confidential',
+ "Input" => 'websites-security',
+ "Intellego" => 'intellego-team',
+ "Internet Public Policy" => 'mozilla-employee-confidential',
+ "L20n" => 'l20n-security',
+ "Legal" => 'legal',
+ "Marketing" => 'marketing-private',
+ "Marketplace" => 'client-services-security',
+ "Mozilla Communities" => 'mozilla-communities-security',
+ "Mozilla Corporation" => 'mozilla-employee-confidential',
+ "Mozilla Developer Network" => 'websites-security',
+ "Mozilla Foundation" => 'mozilla-employee-confidential',
+ "Mozilla Grants" => 'grants',
+ "mozillaignite" => 'websites-security',
+ "Mozilla Messaging" => 'mozilla-messaging-confidential',
+ "Mozilla Metrics" => 'metrics-private',
+ "mozilla.org" => 'mozilla-employee-confidential',
+ "Mozilla PR" => 'pr-private',
+ "Mozilla QA" => 'mozilla-employee-confidential',
+ "Mozilla Reps" => 'mozilla-reps',
+ "Mozilla Services" => 'mozilla-services-security',
+ "Popcorn" => 'websites-security',
+ "Privacy" => 'privacy',
+ "quality.mozilla.org" => 'websites-security',
+ "Release Engineering" => 'mozilla-employee-confidential',
+ "Snippets" => 'websites-security',
+ "Socorro" => 'client-services-security',
+ "support.mozillamessaging.com" => 'websites-security',
+ "support.mozilla.org" => 'websites-security',
+ "Talkback" => 'talkback-private',
+ "Tamarin" => 'tamarin-security',
+ "Testopia" => 'bugzilla-security',
+ "Tree Management" => 'mozilla-employee-confidential',
+ "Web Apps" => 'client-services-security',
+ "Webmaker" => 'websites-security',
+ "Websites" => 'websites-security',
+ "Webtools" => 'webtools-security',
+ "www.mozilla.org" => 'websites-security',
+ "Mozilla Foundation Operations" => 'mozilla-foundation-operations',
+ "_default" => 'core-security'
+);
+
+# Automatically CC users to bugs filed into configured groups and products
+our %group_auto_cc = (
+ 'partner-confidential' => {
+ 'Marketing' => ['jbalaco@mozilla.com'],
+ '_default' => ['mbest@mozilla.com'],
+ },
+);
+
+# Force create-bug template by product
+# Users in 'include' group will be forced into using the form.
+our %create_bug_formats = (
+ 'Mozilla Developer Network' => {
+ 'format' => 'mdn',
+ 'include' => 'everyone',
+ },
+ 'Legal' => {
+ 'format' => 'legal',
+ 'include' => 'everyone',
+ },
+ 'Internet Public Policy' => {
+ 'format' => 'ipp',
+ 'include' => 'everyone',
+ },
+);
+
+# List of named queries which will be added to new users' footer
+our @default_named_queries = (
+ {
+ name => 'Bugs Filed Today',
+ query => 'query_format=advanced&chfieldto=Now&chfield=[Bug creation]&chfieldfrom=-24h&order=bug_id',
+ },
+);
+
+1;
diff --git a/extensions/BMO/lib/FakeBug.pm b/extensions/BMO/lib/FakeBug.pm
new file mode 100644
index 000000000..6127cb560
--- /dev/null
+++ b/extensions/BMO/lib/FakeBug.pm
@@ -0,0 +1,42 @@
+package Bugzilla::Extension::BMO::FakeBug;
+
+# hack to allow the bug entry templates to use check_can_change_field to see if
+# various field values should be available to the current user
+
+use strict;
+
+use Bugzilla::Bug;
+
+our $AUTOLOAD;
+
+sub new {
+ my $class = shift;
+ my $self = shift;
+ bless $self, $class;
+ return $self;
+}
+
+sub AUTOLOAD {
+ my $self = shift;
+ my $name = $AUTOLOAD;
+ $name =~ s/.*://;
+ return exists $self->{$name} ? $self->{$name} : undef;
+}
+
+sub check_can_change_field {
+ my $self = shift;
+ return Bugzilla::Bug::check_can_change_field($self, @_)
+}
+
+sub _changes_everconfirmed {
+ my $self = shift;
+ return Bugzilla::Bug::_changes_everconfirmed($self, @_)
+}
+
+sub everconfirmed {
+ my $self = shift;
+ return ($self->{'status'} == 'UNCONFIRMED') ? 0 : 1;
+}
+
+1;
+
diff --git a/extensions/BMO/lib/Reports/EmailQueue.pm b/extensions/BMO/lib/Reports/EmailQueue.pm
new file mode 100644
index 000000000..f1383aac7
--- /dev/null
+++ b/extensions/BMO/lib/Reports/EmailQueue.pm
@@ -0,0 +1,84 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BMO::Reports::EmailQueue;
+use strict;
+use warnings;
+
+use Bugzilla::Error;
+use Scalar::Util qw(blessed);
+use Storable ();
+
+sub report {
+ my ($vars, $filter) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+
+ $user->in_group('admin') || $user->in_group('infra')
+ || ThrowUserError('auth_failure', { group => 'admin',
+ action => 'run',
+ object => 'email_queue' });
+
+ my $query = "
+ SELECT j.jobid,
+ j.arg,
+ j.insert_time,
+ j.run_after AS run_time,
+ COUNT(e.jobid) AS error_count,
+ MAX(e.error_time) AS error_time,
+ e.message AS error_message
+ FROM ts_job j
+ LEFT JOIN ts_error e ON e.jobid = j.jobid
+ GROUP BY j.jobid
+ ORDER BY j.run_after";
+
+ $vars->{'jobs'} = $dbh->selectall_arrayref($query, { Slice => {} });
+ foreach my $job (@{ $vars->{'jobs'} }) {
+ eval {
+ my ($recipient, $description);
+ my $arg = _cond_thaw(delete $job->{arg});
+
+ if (exists $arg->{vars}) {
+ my $vars = $arg->{vars};
+ $recipient = $vars->{to_user}->{login_name};
+ $description = '[Bug ' . $vars->{bug}->{bug_id} . '] ' . $vars->{bug}->{short_desc};
+ } elsif (exists $arg->{msg}) {
+ my $msg = $arg->{msg};
+ if (ref($msg) && blessed($msg) eq 'Email::MIME') {
+ $recipient = $msg->header('to');
+ $description = $msg->header('subject');
+ } else {
+ ($recipient) = $msg =~ /\nTo: ([^\n]+)/;
+ ($description) = $msg =~ /\nSubject: ([^\n]+)/;
+ }
+ }
+
+ if ($recipient) {
+ $job->{subject} = "<$recipient> $description";
+ }
+ };
+ }
+ $vars->{'now'} = (time);
+}
+
+sub _cond_thaw {
+ my $data = shift;
+ my $magic = eval { Storable::read_magic($data); };
+ if ($magic && $magic->{major} && $magic->{major} >= 2 && $magic->{major} <= 5) {
+ my $thawed = eval { Storable::thaw($data) };
+ if ($@) {
+ # false alarm... looked like a Storable, but wasn't.
+ return $data;
+ }
+ return $thawed;
+ } else {
+ return $data;
+ }
+}
+
+
+1;
diff --git a/extensions/BMO/lib/Reports/Groups.pm b/extensions/BMO/lib/Reports/Groups.pm
new file mode 100644
index 000000000..ab0f1efa4
--- /dev/null
+++ b/extensions/BMO/lib/Reports/Groups.pm
@@ -0,0 +1,243 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BMO::Reports::Groups;
+use strict;
+use warnings;
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Group;
+use Bugzilla::User;
+use Bugzilla::Util qw(trim);
+
+sub admins_report {
+ my ($vars) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+
+ ($user->in_group('editusers') || $user->in_group('infrasec'))
+ || ThrowUserError('auth_failure', { group => 'editusers',
+ action => 'run',
+ object => 'group_admins' });
+
+ my $query = "
+ SELECT groups.name, " .
+ $dbh->sql_group_concat('profiles.login_name', "','", 1) . "
+ FROM groups
+ LEFT JOIN user_group_map
+ ON user_group_map.group_id = groups.id
+ AND user_group_map.isbless = 1
+ AND user_group_map.grant_type = 0
+ LEFT JOIN profiles
+ ON user_group_map.user_id = profiles.userid
+ WHERE groups.isbuggroup = 1
+ GROUP BY groups.name";
+
+ my @groups;
+ foreach my $group (@{ $dbh->selectall_arrayref($query) }) {
+ my @admins;
+ if ($group->[1]) {
+ foreach my $admin (split(/,/, $group->[1])) {
+ push(@admins, Bugzilla::User->new({ name => $admin }));
+ }
+ }
+ push(@groups, { name => $group->[0], admins => \@admins });
+ }
+
+ $vars->{'groups'} = \@groups;
+}
+
+sub membership_report {
+ my ($page, $vars) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+ my $cgi = Bugzilla->cgi;
+
+ ($user->in_group('editusers') || $user->in_group('infrasec'))
+ || ThrowUserError('auth_failure', { group => 'editusers',
+ action => 'run',
+ object => 'group_admins' });
+
+ my $who = $cgi->param('who');
+ if (!defined($who) || $who eq '') {
+ if ($page eq 'group_membership.txt') {
+ print $cgi->redirect("page.cgi?id=group_membership.html&output=txt");
+ exit;
+ }
+ $vars->{'output'} = $cgi->param('output');
+ return;
+ }
+
+ Bugzilla::User::match_field({ 'who' => {'type' => 'multi'} });
+ $who = Bugzilla->input_params->{'who'};
+ $who = ref($who) ? $who : [ $who ];
+
+ my @users;
+ foreach my $login (@$who) {
+ my $u = Bugzilla::User->new(login_to_id($login, 1));
+
+ # this is lifted from $user->groups()
+ # we need to show which groups are direct and which are inherited
+
+ my $groups_to_check = $dbh->selectcol_arrayref(
+ q{SELECT DISTINCT group_id
+ FROM user_group_map
+ WHERE user_id = ? AND isbless = 0}, undef, $u->id);
+
+ my $rows = $dbh->selectall_arrayref(
+ "SELECT DISTINCT grantor_id, member_id
+ FROM group_group_map
+ WHERE grant_type = " . GROUP_MEMBERSHIP);
+
+ my %group_membership;
+ foreach my $row (@$rows) {
+ my ($grantor_id, $member_id) = @$row;
+ push (@{ $group_membership{$member_id} }, $grantor_id);
+ }
+
+ my %checked_groups;
+ my %direct_groups;
+ my %indirect_groups;
+ my %groups;
+
+ foreach my $member_id (@$groups_to_check) {
+ $direct_groups{$member_id} = 1;
+ }
+
+ while (scalar(@$groups_to_check) > 0) {
+ my $member_id = shift @$groups_to_check;
+ if (!$checked_groups{$member_id}) {
+ $checked_groups{$member_id} = 1;
+ my $members = $group_membership{$member_id};
+ my @new_to_check = grep(!$checked_groups{$_}, @$members);
+ push(@$groups_to_check, @new_to_check);
+ foreach my $id (@new_to_check) {
+ $indirect_groups{$id} = $member_id;
+ }
+ $groups{$member_id} = 1;
+ }
+ }
+
+ my @groups;
+ my $ra_groups = Bugzilla::Group->new_from_list([keys %groups]);
+ foreach my $group (@$ra_groups) {
+ my $via;
+ if ($direct_groups{$group->id}) {
+ $via = '';
+ } else {
+ foreach my $g (@$ra_groups) {
+ if ($g->id == $indirect_groups{$group->id}) {
+ $via = $g->name;
+ last;
+ }
+ }
+ }
+ push @groups, {
+ name => $group->name,
+ desc => $group->description,
+ via => $via,
+ };
+ }
+
+ push @users, {
+ user => $u,
+ groups => \@groups,
+ };
+ }
+
+ $vars->{'who'} = $who;
+ $vars->{'users'} = \@users;
+}
+
+sub members_report {
+ my ($vars) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+ my $cgi = Bugzilla->cgi;
+
+ ($user->in_group('editusers') || $user->in_group('infrasec'))
+ || ThrowUserError('auth_failure', { group => 'editusers',
+ action => 'run',
+ object => 'group_admins' });
+
+ my $include_disabled = $cgi->param('include_disabled') ? 1 : 0;
+ $vars->{'include_disabled'} = $include_disabled;
+
+ # don't allow all groups, to avoid putting pain on the servers
+ my @group_names =
+ sort
+ grep { !/^(?:bz_.+|canconfirm|editbugs|editbugs-team|everyone)$/ }
+ map { lc($_->name) }
+ Bugzilla::Group->get_all;
+ unshift(@group_names, '');
+ $vars->{'groups'} = \@group_names;
+
+ # load selected group
+ my $group = lc(trim($cgi->param('group') || ''));
+ $group = '' unless grep { $_ eq $group } @group_names;
+ return if $group eq '';
+ my $group_obj = Bugzilla::Group->new({ name => $group });
+ $vars->{'group'} = $group;
+
+ # direct members
+ my @types = (
+ {
+ name => 'direct',
+ members => _filter_userlist($group_obj->members_direct, $include_disabled),
+ },
+ );
+
+ # indirect members, by group
+ foreach my $member_group (sort @{ $group_obj->grant_direct(GROUP_MEMBERSHIP) }) {
+ push @types, {
+ name => $member_group->name,
+ members => _filter_userlist($member_group->members_direct, $include_disabled),
+ },
+ }
+
+ # make it easy for the template to detect an empty group
+ my $has_members = 0;
+ foreach my $type (@types) {
+ $has_members += scalar(@{ $type->{members} });
+ last if $has_members;
+ }
+ @types = () unless $has_members;
+
+ if (@types) {
+ # add last-login
+ my $user_ids = join(',', map { map { $_->id } @{ $_->{members} } } @types);
+ my $tokens = $dbh->selectall_hashref("
+ SELECT profiles.userid,
+ (SELECT DATEDIFF(curdate(), logincookies.lastused) lastseen
+ FROM logincookies
+ WHERE logincookies.userid = profiles.userid
+ ORDER BY lastused DESC
+ LIMIT 1) lastseen
+ FROM profiles
+ WHERE userid IN ($user_ids)",
+ 'userid');
+ foreach my $type (@types) {
+ foreach my $member (@{ $type->{members} }) {
+ $member->{lastseen} =
+ defined $tokens->{$member->id}->{lastseen}
+ ? $tokens->{$member->id}->{lastseen}
+ : '>' . MAX_LOGINCOOKIE_AGE;
+ }
+ }
+ }
+
+ $vars->{'types'} = \@types;
+}
+
+sub _filter_userlist {
+ my ($list, $include_disabled) = @_;
+ $list = [ grep { $_->is_enabled } @$list ] unless $include_disabled;
+ return [ sort { lc($a->identity) cmp lc($b->identity) } @$list ];
+}
+
+1;
diff --git a/extensions/BMO/lib/Reports/ProductSecurity.pm b/extensions/BMO/lib/Reports/ProductSecurity.pm
new file mode 100644
index 000000000..2324e725a
--- /dev/null
+++ b/extensions/BMO/lib/Reports/ProductSecurity.pm
@@ -0,0 +1,67 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BMO::Reports::ProductSecurity;
+use strict;
+use warnings;
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Product;
+
+sub report {
+ my ($vars) = @_;
+ my $user = Bugzilla->user;
+
+ ($user->in_group('admin') || $user->in_group('infrasec'))
+ || ThrowUserError('auth_failure', { group => 'admin',
+ action => 'run',
+ object => 'product_security' });
+
+ my $moco = Bugzilla::Group->new({ name => 'mozilla-employee-confidential' })
+ or return;
+
+ my $products = [];
+ foreach my $product (@{ Bugzilla::Product->match({}) }) {
+ my $default_group = $product->default_security_group_obj;
+ my $group_controls = $product->group_controls();
+
+ my $item = {
+ name => $product->name,
+ default_security_group => $product->default_security_group,
+ group_visibility => 'None/None',
+ moco => exists $group_controls->{$moco->id},
+ };
+
+ if ($default_group) {
+ if (my $control = $group_controls->{$default_group->id}) {
+ $item->{group_visibility} = control_to_string($control->{membercontrol}) .
+ '/' . control_to_string($control->{othercontrol});
+ }
+ }
+
+ $item->{group_problem} = $default_group ? '' : "Invalid group " . $product->default_security_group;
+ $item->{visibility_problem} = 'Default security group should be Shown/Shown'
+ if ($item->{group_visibility} ne 'Shown/Shown')
+ && ($item->{group_visibility} ne 'Mandatory/Mandatory')
+ && ($item->{group_visibility} ne 'Default/Default');
+
+ push @$products, $item;
+ }
+ $vars->{products} = $products;
+}
+
+sub control_to_string {
+ my ($control) = @_;
+ return 'NA' if $control == CONTROLMAPNA;
+ return 'Shown' if $control == CONTROLMAPSHOWN;
+ return 'Default' if $control == CONTROLMAPDEFAULT;
+ return 'Mandatory' if $control == CONTROLMAPMANDATORY;
+ return '';
+}
+
+1;
diff --git a/extensions/BMO/lib/Reports/ReleaseTracking.pm b/extensions/BMO/lib/Reports/ReleaseTracking.pm
new file mode 100644
index 000000000..5a07ae196
--- /dev/null
+++ b/extensions/BMO/lib/Reports/ReleaseTracking.pm
@@ -0,0 +1,409 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BMO::Reports::ReleaseTracking;
+use strict;
+use warnings;
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Extension::BMO::Util;
+use Bugzilla::Field;
+use Bugzilla::FlagType;
+use Bugzilla::Util qw(correct_urlbase trick_taint);
+use JSON qw(-convert_blessed_universally);
+use List::MoreUtils qw(uniq);
+
+sub report {
+ my ($vars) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $input = Bugzilla->input_params;
+ my $user = Bugzilla->user;
+
+ my @flag_names = qw(
+ approval-mozilla-release
+ approval-mozilla-beta
+ approval-mozilla-aurora
+ approval-mozilla-central
+ approval-comm-release
+ approval-comm-beta
+ approval-comm-aurora
+ approval-calendar-release
+ approval-calendar-beta
+ approval-calendar-aurora
+ approval-mozilla-esr10
+ );
+
+ my @flags_json;
+ my @fields_json;
+ my @products_json;
+
+ #
+ # tracking flags
+ #
+
+ my $all_products = $user->get_selectable_products;
+ my @usable_products;
+
+ # build list of flags and their matching products
+
+ my @invalid_flag_names;
+ foreach my $flag_name (@flag_names) {
+ # grab all matching flag_types
+ my @flag_types = @{Bugzilla::FlagType::match({ name => $flag_name, is_active => 1 })};
+
+ # remove invalid flags
+ if (!@flag_types) {
+ push @invalid_flag_names, $flag_name;
+ next;
+ }
+
+ # we need a list of products, based on inclusions/exclusions
+ my @products;
+ my %flag_types;
+ foreach my $flag_type (@flag_types) {
+ $flag_types{$flag_type->name} = $flag_type->id;
+ my $has_all = 0;
+ my @exclusion_ids;
+ my @inclusion_ids;
+ foreach my $flag_type (@flag_types) {
+ if (scalar keys %{$flag_type->inclusions}) {
+ my $inclusions = $flag_type->inclusions;
+ foreach my $key (keys %$inclusions) {
+ push @inclusion_ids, ($inclusions->{$key} =~ /^(\d+)/);
+ }
+ } elsif (scalar keys %{$flag_type->exclusions}) {
+ my $exclusions = $flag_type->exclusions;
+ foreach my $key (keys %$exclusions) {
+ push @exclusion_ids, ($exclusions->{$key} =~ /^(\d+)/);
+ }
+ } else {
+ $has_all = 1;
+ last;
+ }
+ }
+
+ if ($has_all) {
+ push @products, @$all_products;
+ } elsif (scalar @exclusion_ids) {
+ push @products, @$all_products;
+ foreach my $exclude_id (uniq @exclusion_ids) {
+ @products = grep { $_->id != $exclude_id } @products;
+ }
+ } else {
+ foreach my $include_id (uniq @inclusion_ids) {
+ push @products, grep { $_->id == $include_id } @$all_products;
+ }
+ }
+ }
+ @products = uniq @products;
+ push @usable_products, @products;
+ my @product_ids = map { $_->id } sort { lc($a->name) cmp lc($b->name) } @products;
+
+ push @flags_json, {
+ name => $flag_name,
+ id => $flag_types{$flag_name} || 0,
+ products => \@product_ids,
+ fields => [],
+ };
+ }
+ foreach my $flag_name (@invalid_flag_names) {
+ @flag_names = grep { $_ ne $flag_name } @flag_names;
+ }
+ @usable_products = uniq @usable_products;
+
+ # build a list of tracking flags for each product
+ # also build the list of all fields
+
+ my @unlink_products;
+ foreach my $product (@usable_products) {
+ my @fields =
+ grep { is_active_status_field($_) }
+ Bugzilla->active_custom_fields({ product => $product });
+ my @field_ids = map { $_->id } @fields;
+ if (!scalar @fields) {
+ push @unlink_products, $product;
+ next;
+ }
+
+ # product
+ push @products_json, {
+ name => $product->name,
+ id => $product->id,
+ fields => \@field_ids,
+ };
+
+ # add fields to flags
+ foreach my $rh (@flags_json) {
+ if (grep { $_ eq $product->id } @{$rh->{products}}) {
+ push @{$rh->{fields}}, @field_ids;
+ }
+ }
+
+ # add fields to fields_json
+ foreach my $field (@fields) {
+ my $existing = 0;
+ foreach my $rh (@fields_json) {
+ if ($rh->{id} == $field->id) {
+ $existing = 1;
+ last;
+ }
+ }
+ if (!$existing) {
+ push @fields_json, {
+ name => $field->name,
+ id => $field->id,
+ };
+ }
+ }
+ }
+ foreach my $rh (@flags_json) {
+ my @fields = uniq @{$rh->{fields}};
+ $rh->{fields} = \@fields;
+ }
+
+ # remove products which aren't linked with status fields
+
+ foreach my $rh (@flags_json) {
+ my @product_ids;
+ foreach my $id (@{$rh->{products}}) {
+ unless (grep { $_->id == $id } @unlink_products) {
+ push @product_ids, $id;
+ }
+ $rh->{products} = \@product_ids;
+ }
+ }
+
+ #
+ # rapid release dates
+ #
+
+ my @ranges;
+ my $start_date = string_to_datetime('2011-08-16');
+ my $end_date = $start_date->clone->add(weeks => 6)->add(days => -1);
+ my $now_date = string_to_datetime('2012-11-19');
+
+ while ($start_date <= $now_date) {
+ unshift @ranges, {
+ value => sprintf("%s-%s", $start_date->ymd(''), $end_date->ymd('')),
+ label => sprintf("%s and %s", $start_date->ymd('-'), $end_date->ymd('-')),
+ };
+
+ $start_date = $end_date->clone;;
+ $start_date->add(days => 1);
+ $end_date->add(weeks => 6);
+ }
+
+ # 2012-11-20 - 2013-01-06 was a 7 week release cycle instead of 6
+ $start_date = string_to_datetime('2012-11-20');
+ $end_date = $start_date->clone->add(weeks => 7)->add(days => -1);
+ unshift @ranges, {
+ value => sprintf("%s-%s", $start_date->ymd(''), $end_date->ymd('')),
+ label => sprintf("%s and %s", $start_date->ymd('-'), $end_date->ymd('-')),
+ };
+
+ # Back on track with 6 week releases
+ $start_date = string_to_datetime('2013-01-08');
+ $end_date = $start_date->clone->add(weeks => 6)->add(days => -1);
+ $now_date = time_to_datetime((time));
+
+ while ($start_date <= $now_date) {
+ unshift @ranges, {
+ value => sprintf("%s-%s", $start_date->ymd(''), $end_date->ymd('')),
+ label => sprintf("%s and %s", $start_date->ymd('-'), $end_date->ymd('-')),
+ };
+
+ $start_date = $end_date->clone;;
+ $start_date->add(days => 1);
+ $end_date->add(weeks => 6);
+ }
+
+ push @ranges, {
+ value => '*',
+ label => 'Anytime',
+ };
+
+ #
+ # run report
+ #
+
+ if ($input->{q} && !$input->{edit}) {
+ my $q = _parse_query($input->{q});
+
+ my @where;
+ my @params;
+ my $query = "
+ SELECT DISTINCT b.bug_id
+ FROM bugs b
+ INNER JOIN flags f ON f.bug_id = b.bug_id ";
+
+ if ($q->{start_date}) {
+ $query .= "INNER JOIN bugs_activity a ON a.bug_id = b.bug_id ";
+ }
+
+ if (grep($_ == FIELD_TYPE_EXTENSION, map { $_->{type} } @{ $q->{fields} })) {
+ $query .= "LEFT JOIN tracking_flags_bugs AS tfb ON tfb.bug_id = b.bug_id " .
+ "LEFT JOIN tracking_flags AS tf ON tfb.tracking_flag_id = tf.id ";
+ }
+
+ $query .= "WHERE ";
+
+ if ($q->{start_date}) {
+ push @where, "(a.fieldid = ?)";
+ push @params, $q->{field_id};
+
+ push @where, "(a.bug_when >= ?)";
+ push @params, $q->{start_date} . ' 00:00:00';
+ push @where, "(a.bug_when < ?)";
+ push @params, $q->{end_date} . ' 00:00:00';
+
+ push @where, "(a.added LIKE ?)";
+ push @params, '%' . $q->{flag_name} . $q->{flag_status} . '%';
+ }
+
+ my ($type_id) = $dbh->selectrow_array(
+ "SELECT id FROM flagtypes WHERE name = ?",
+ undef,
+ $q->{flag_name}
+ );
+ push @where, "(f.type_id = ?)";
+ push @params, $type_id;
+
+ push @where, "(f.status = ?)";
+ push @params, $q->{flag_status};
+
+ if ($q->{product_id}) {
+ push @where, "(b.product_id = ?)";
+ push @params, $q->{product_id};
+ }
+
+ if (scalar @{$q->{fields}}) {
+ my @fields;
+ foreach my $field (@{$q->{fields}}) {
+ my $field_sql = "(";
+ if ($field->{type} == FIELD_TYPE_EXTENSION) {
+ $field_sql .= "tf.name = " . $dbh->quote($field->{name}) . " AND COALESCE(tfb.value, '')";
+ }
+ else {
+ $field_sql .= "b." . $field->{name};
+ }
+ $field_sql .= " " . ($field->{value} eq '+' ? '' : 'NOT ') . "IN ('fixed','verified'))";
+ push(@fields, $field_sql);
+ }
+ my $join = uc $q->{join};
+ push @where, '(' . join(" $join ", @fields) . ')';
+ }
+
+ $query .= join("\nAND ", @where);
+
+ if ($input->{debug}) {
+ print "Content-Type: text/plain\n\n";
+ $query =~ s/\?/\000/g;
+ foreach my $param (@params) {
+ $query =~ s/\000/'$param'/;
+ }
+ print "$query\n";
+ exit;
+ }
+
+ my $bugs = $dbh->selectcol_arrayref($query, undef, @params);
+ push @$bugs, 0 unless @$bugs;
+
+ my $urlbase = correct_urlbase();
+ my $cgi = Bugzilla->cgi;
+ print $cgi->redirect(
+ -url => "${urlbase}buglist.cgi?bug_id=" . join(',', @$bugs)
+ );
+ exit;
+ }
+
+ #
+ # set template vars
+ #
+
+ my $json = JSON->new();
+ if (0) {
+ # debugging
+ $json->shrink(0);
+ $json->canonical(1);
+ $vars->{flags_json} = $json->pretty->encode(\@flags_json);
+ $vars->{products_json} = $json->pretty->encode(\@products_json);
+ $vars->{fields_json} = $json->pretty->encode(\@fields_json);
+ } else {
+ $json->shrink(1);
+ $vars->{flags_json} = $json->encode(\@flags_json);
+ $vars->{products_json} = $json->encode(\@products_json);
+ $vars->{fields_json} = $json->encode(\@fields_json);
+ }
+
+ $vars->{flag_names} = \@flag_names;
+ $vars->{ranges} = \@ranges;
+ $vars->{default_query} = $input->{q};
+ foreach my $field (qw(product flags range)) {
+ $vars->{$field} = $input->{$field};
+ }
+}
+
+sub _parse_query {
+ my $q = shift;
+ my @query = split(/:/, $q);
+ my $query;
+
+ # field_id for flag changes
+ $query->{field_id} = get_field_id('flagtypes.name');
+
+ # flag_name
+ my $flag_name = shift @query;
+ @{Bugzilla::FlagType::match({ name => $flag_name, is_active => 1 })}
+ or ThrowUserError('report_invalid_parameter', { name => 'flag_name' });
+ trick_taint($flag_name);
+ $query->{flag_name} = $flag_name;
+
+ # flag_status
+ my $flag_status = shift @query;
+ $flag_status =~ /^([\?\-\+])$/
+ or ThrowUserError('report_invalid_parameter', { name => 'flag_status' });
+ $query->{flag_status} = $1;
+
+ # date_range -> from_ymd to_ymd
+ my $date_range = shift @query;
+ if ($date_range ne '*') {
+ $date_range =~ /^(\d\d\d\d)(\d\d)(\d\d)-(\d\d\d\d)(\d\d)(\d\d)$/
+ or ThrowUserError('report_invalid_parameter', { name => 'date_range' });
+ $query->{start_date} = "$1-$2-$3";
+ $query->{end_date} = "$4-$5-$6";
+ }
+
+ # product_id
+ my $product_id = shift @query;
+ $product_id =~ /^(\d+)$/
+ or ThrowUserError('report_invalid_parameter', { name => 'product_id' });
+ $query->{product_id} = $1;
+
+ # join
+ my $join = shift @query;
+ $join =~ /^(and|or)$/
+ or ThrowUserError('report_invalid_parameter', { name => 'join' });
+ $query->{join} = $1;
+
+ # fields
+ my @fields;
+ foreach my $field (@query) {
+ $field =~ /^(\d+)([\-\+])$/
+ or ThrowUserError('report_invalid_parameter', { name => 'fields' });
+ my ($id, $value) = ($1, $2);
+ my $field_obj = Bugzilla::Field->new($id)
+ or ThrowUserError('report_invalid_parameter', { name => 'field_id' });
+ push @fields, { id => $id, value => $value,
+ name => $field_obj->name, type => $field_obj->type };
+ }
+ $query->{fields} = \@fields;
+
+ return $query;
+}
+
+1;
diff --git a/extensions/BMO/lib/Reports/Triage.pm b/extensions/BMO/lib/Reports/Triage.pm
new file mode 100644
index 000000000..debb50577
--- /dev/null
+++ b/extensions/BMO/lib/Reports/Triage.pm
@@ -0,0 +1,217 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BMO::Reports::Triage;
+use strict;
+
+use Bugzilla::Component;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Product;
+use Bugzilla::User;
+use Bugzilla::Util qw(detaint_natural);
+use Date::Parse;
+
+# set an upper limit on the *unfiltered* number of bugs to process
+use constant MAX_NUMBER_BUGS => 4000;
+
+sub report {
+ my ($vars, $filter) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $input = Bugzilla->input_params;
+ my $user = Bugzilla->user;
+
+ if (exists $input->{'action'} && $input->{'action'} eq 'run' && $input->{'product'}) {
+
+ # load product and components from input
+
+ my $product = Bugzilla::Product->new({ name => $input->{'product'} })
+ || ThrowUserError('invalid_object', { object => 'Product', value => $input->{'product'} });
+
+ my @component_ids;
+ if ($input->{'component'} ne '') {
+ my $ra_components = ref($input->{'component'})
+ ? $input->{'component'} : [ $input->{'component'} ];
+ foreach my $component_name (@$ra_components) {
+ my $component = Bugzilla::Component->new({ name => $component_name, product => $product })
+ || ThrowUserError('invalid_object', { object => 'Component', value => $component_name });
+ push @component_ids, $component->id;
+ }
+ }
+
+ # determine which comment filters to run
+
+ my $filter_commenter = $input->{'filter_commenter'};
+ my $filter_commenter_on = $input->{'commenter'};
+ my $filter_last = $input->{'filter_last'};
+ my $filter_last_period = $input->{'last'};
+
+ if (!$filter_commenter || $filter_last) {
+ $filter_commenter = '1';
+ $filter_commenter_on = 'reporter';
+ }
+
+ my $filter_commenter_id;
+ if ($filter_commenter && $filter_commenter_on eq 'is') {
+ Bugzilla::User::match_field({ 'commenter_is' => {'type' => 'single'} });
+ my $user = Bugzilla::User->new({ name => $input->{'commenter_is'} })
+ || ThrowUserError('invalid_object', { object => 'User', value => $input->{'commenter_is'} });
+ $filter_commenter_id = $user ? $user->id : 0;
+ }
+
+ my $filter_last_time;
+ if ($filter_last) {
+ if ($filter_last_period eq 'is') {
+ $filter_last_period = -1;
+ $filter_last_time = str2time($input->{'last_is'} . " 00:00:00") || 0;
+ } else {
+ detaint_natural($filter_last_period);
+ $filter_last_period = 14 if $filter_last_period < 14;
+ }
+ }
+
+ # form sql queries
+
+ my $now = (time);
+ my $bugs_sql = "
+ SELECT bug_id, short_desc, reporter, creation_ts
+ FROM bugs
+ WHERE product_id = ?
+ AND bug_status = 'UNCONFIRMED'";
+ if (@component_ids) {
+ $bugs_sql .= " AND component_id IN (" . join(',', @component_ids) . ")";
+ }
+ $bugs_sql .= "
+ ORDER BY creation_ts
+ ";
+
+ my $comment_count_sql = "
+ SELECT COUNT(*)
+ FROM longdescs
+ WHERE bug_id = ?
+ ";
+
+ my $comment_sql = "
+ SELECT who, bug_when, type, thetext, extra_data
+ FROM longdescs
+ WHERE bug_id = ?
+ ";
+ if (!Bugzilla->user->is_insider) {
+ $comment_sql .= " AND isprivate = 0 ";
+ }
+ $comment_sql .= "
+ ORDER BY bug_when DESC
+ LIMIT 1
+ ";
+
+ my $attach_sql = "
+ SELECT description, isprivate
+ FROM attachments
+ WHERE attach_id = ?
+ ";
+
+ # work on an initial list of bugs
+
+ my $list = $dbh->selectall_arrayref($bugs_sql, undef, $product->id);
+ my @bugs;
+
+ # this can be slow to process, resulting in 'service unavailable' errors from zeus
+ # so if too many bugs are returned, throw an error
+
+ if (scalar(@$list) > MAX_NUMBER_BUGS) {
+ ThrowUserError('report_too_many_bugs');
+ }
+
+ foreach my $entry (@$list) {
+ my ($bug_id, $summary, $reporter_id, $creation_ts) = @$entry;
+
+ next unless $user->can_see_bug($bug_id);
+
+ # get last comment information
+
+ my ($comment_count) = $dbh->selectrow_array($comment_count_sql, undef, $bug_id);
+ my ($commenter_id, $comment_ts, $type, $comment, $extra)
+ = $dbh->selectrow_array($comment_sql, undef, $bug_id);
+ my $commenter = 0;
+
+ # apply selected filters
+
+ if ($filter_commenter) {
+ next if $comment_count <= 1;
+
+ if ($filter_commenter_on eq 'reporter') {
+ next if $commenter_id != $reporter_id;
+
+ } elsif ($filter_commenter_on eq 'noconfirm') {
+ $commenter = Bugzilla::User->new({ id => $commenter_id, cache => 1 });
+ next if $commenter_id != $reporter_id
+ || $commenter->in_group('canconfirm');
+
+ } elsif ($filter_commenter_on eq 'is') {
+ next if $commenter_id != $filter_commenter_id;
+ }
+ } else {
+ $input->{'commenter'} = '';
+ $input->{'commenter_is'} = '';
+ }
+
+ if ($filter_last) {
+ my $comment_time = str2time($comment_ts)
+ or next;
+ if ($filter_last_period == -1) {
+ next if $comment_time >= $filter_last_time;
+ } else {
+ next if $now - $comment_time <= 60 * 60 * 24 * $filter_last_period;
+ }
+ } else {
+ $input->{'last'} = '';
+ $input->{'last_is'} = '';
+ }
+
+ # get data for attachment comments
+
+ if ($comment eq '' && $type == CMT_ATTACHMENT_CREATED) {
+ my ($description, $is_private) = $dbh->selectrow_array($attach_sql, undef, $extra);
+ next if $is_private && !Bugzilla->user->is_insider;
+ $comment = "(Attachment) " . $description;
+ }
+
+ # truncate long comments
+
+ if (length($comment) > 80) {
+ $comment = substr($comment, 0, 80) . '...';
+ }
+
+ # build bug hash for template
+
+ my $bug = {};
+ $bug->{id} = $bug_id;
+ $bug->{summary} = $summary;
+ $bug->{reporter} = Bugzilla::User->new({ id => $reporter_id, cache => 1 });
+ $bug->{creation_ts} = $creation_ts;
+ $bug->{commenter} = $commenter || Bugzilla::User->new({ id => $commenter_id, cache => 1 });
+ $bug->{comment_ts} = $comment_ts;
+ $bug->{comment} = $comment;
+ $bug->{comment_count} = $comment_count;
+ push @bugs, $bug;
+ }
+
+ @bugs = sort { $b->{comment_ts} cmp $a->{comment_ts} } @bugs;
+
+ $vars->{bugs} = \@bugs;
+ } else {
+ $input->{action} = '';
+ }
+
+ if (!$input->{filter_commenter} && !$input->{filter_last}) {
+ $input->{filter_commenter} = 1;
+ }
+
+ $vars->{'input'} = $input;
+}
+
+1;
diff --git a/extensions/BMO/lib/Reports/UserActivity.pm b/extensions/BMO/lib/Reports/UserActivity.pm
new file mode 100644
index 000000000..04810c2ec
--- /dev/null
+++ b/extensions/BMO/lib/Reports/UserActivity.pm
@@ -0,0 +1,327 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BMO::Reports::UserActivity;
+use strict;
+use warnings;
+
+use Bugzilla::Error;
+use Bugzilla::Extension::BMO::Util;
+use Bugzilla::User;
+use Bugzilla::Util qw(trim);
+use DateTime;
+
+sub report {
+ my ($vars) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $input = Bugzilla->input_params;
+
+ my @who = ();
+ my $from = trim($input->{'from'} || '');
+ my $to = trim($input->{'to'} || '');
+ my $action = $input->{'action'} || '';
+
+ # fix non-breaking hyphens
+ $from =~ s/\N{U+2011}/-/g;
+ $to =~ s/\N{U+2011}/-/g;
+
+ if ($from eq '') {
+ my $dt = DateTime->now()->subtract('weeks' => 1);
+ $from = $dt->ymd('-');
+ }
+ if ($to eq '') {
+ my $dt = DateTime->now();
+ $to = $dt->ymd('-');
+ }
+
+ if ($action eq 'run') {
+ if (!exists $input->{'who'} || $input->{'who'} eq '') {
+ ThrowUserError('user_activity_missing_username');
+ }
+ Bugzilla::User::match_field({ 'who' => {'type' => 'multi'} });
+
+ my $from_dt = string_to_datetime($from);
+ $from = $from_dt->ymd();
+
+ my $to_dt = string_to_datetime($to);
+ $to = $to_dt->ymd();
+
+ my ($activity_joins, $activity_where) = ('', '');
+ my ($attachments_joins, $attachments_where) = ('', '');
+ my ($tags_activity_joins, $tags_activity_where) = ('', '');
+ if (Bugzilla->params->{"insidergroup"}
+ && !Bugzilla->user->in_group(Bugzilla->params->{'insidergroup'}))
+ {
+ $activity_joins = "LEFT JOIN attachments
+ ON attachments.attach_id = bugs_activity.attach_id";
+ $activity_where = "AND COALESCE(attachments.isprivate, 0) = 0";
+ $attachments_where = $activity_where;
+
+ $tags_activity_joins = 'LEFT JOIN longdescs
+ ON longdescs_tags_activity.comment_id = longdescs.comment_id';
+ $tags_activity_where = 'AND COALESCE(longdescs.isprivate, 0) = 0';
+ }
+
+ my @who_bits;
+ foreach my $who (
+ ref $input->{'who'}
+ ? @{$input->{'who'}}
+ : $input->{'who'}
+ ) {
+ push @who, $who;
+ push @who_bits, '?';
+ }
+ my $who_bits = join(',', @who_bits);
+
+ if (!@who) {
+ my $template = Bugzilla->template;
+ my $cgi = Bugzilla->cgi;
+ my $vars = {};
+ $vars->{'script'} = $cgi->url(-relative => 1);
+ $vars->{'fields'} = {};
+ $vars->{'matches'} = [];
+ $vars->{'matchsuccess'} = 0;
+ $vars->{'matchmultiple'} = 1;
+ print $cgi->header();
+ $template->process("global/confirm-user-match.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+ exit;
+ }
+
+ $from_dt = $from_dt->ymd() . ' 00:00:00';
+ $to_dt = $to_dt->ymd() . ' 23:59:59';
+ my @params;
+ for (1..5) {
+ push @params, @who;
+ push @params, ($from_dt, $to_dt);
+ }
+
+ my $order = ($input->{'group'} && $input->{'group'} eq 'bug')
+ ? 'bug_id, bug_when' : 'bug_when';
+
+ my $comment_filter = '';
+ if (!Bugzilla->user->is_insider) {
+ $comment_filter = 'AND longdescs.isprivate = 0';
+ }
+
+ my $query = "
+ SELECT
+ fielddefs.name,
+ bugs_activity.bug_id,
+ bugs_activity.attach_id,
+ ".$dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s')." AS ts,
+ bugs_activity.removed,
+ bugs_activity.added,
+ profiles.login_name,
+ bugs_activity.comment_id,
+ bugs_activity.bug_when
+ FROM bugs_activity
+ $activity_joins
+ LEFT JOIN fielddefs
+ ON bugs_activity.fieldid = fielddefs.id
+ INNER JOIN profiles
+ ON profiles.userid = bugs_activity.who
+ WHERE profiles.login_name IN ($who_bits)
+ AND bugs_activity.bug_when >= ? AND bugs_activity.bug_when <= ?
+ $activity_where
+
+ UNION ALL
+
+ SELECT
+ 'comment_tag' AS name,
+ longdescs_tags_activity.bug_id,
+ NULL as attach_id,
+ ".$dbh->sql_date_format('longdescs_tags_activity.bug_when',
+ '%Y.%m.%d %H:%i:%s') . " AS bug_when,
+ longdescs_tags_activity.removed,
+ longdescs_tags_activity.added,
+ profiles.login_name,
+ longdescs_tags_activity.comment_id,
+ longdescs_tags_activity.bug_when
+ FROM longdescs_tags_activity
+ $tags_activity_joins
+ INNER JOIN profiles
+ ON profiles.userid = longdescs_tags_activity.who
+ WHERE profiles.login_name IN ($who_bits)
+ AND longdescs_tags_activity.bug_when >= ?
+ AND longdescs_tags_activity.bug_when <= ?
+ $tags_activity_where
+
+ UNION ALL
+
+ SELECT
+ 'bug_id' AS name,
+ bugs.bug_id,
+ NULL AS attach_id,
+ ".$dbh->sql_date_format('bugs.creation_ts', '%Y.%m.%d %H:%i:%s')." AS ts,
+ '(new bug)' AS removed,
+ bugs.short_desc AS added,
+ profiles.login_name,
+ NULL AS comment_id,
+ bugs.creation_ts AS bug_when
+ FROM bugs
+ INNER JOIN profiles
+ ON profiles.userid = bugs.reporter
+ WHERE profiles.login_name IN ($who_bits)
+ AND bugs.creation_ts >= ? AND bugs.creation_ts <= ?
+
+ UNION ALL
+
+ SELECT
+ 'longdesc' AS name,
+ longdescs.bug_id,
+ NULL AS attach_id,
+ DATE_FORMAT(longdescs.bug_when, '%Y.%m.%d %H:%i:%s') AS ts,
+ '' AS removed,
+ '' AS added,
+ profiles.login_name,
+ longdescs.comment_id AS comment_id,
+ longdescs.bug_when
+ FROM longdescs
+ INNER JOIN profiles
+ ON profiles.userid = longdescs.who
+ WHERE profiles.login_name IN ($who_bits)
+ AND longdescs.bug_when >= ? AND longdescs.bug_when <= ?
+ $comment_filter
+
+ UNION ALL
+
+ SELECT
+ 'attachments.description' AS name,
+ attachments.bug_id,
+ attachments.attach_id,
+ ".$dbh->sql_date_format('attachments.creation_ts', '%Y.%m.%d %H:%i:%s')." AS ts,
+ '(new attachment)' AS removed,
+ attachments.description AS added,
+ profiles.login_name,
+ NULL AS comment_id,
+ attachments.creation_ts AS bug_when
+ FROM attachments
+ INNER JOIN profiles
+ ON profiles.userid = attachments.submitter_id
+ WHERE profiles.login_name IN ($who_bits)
+ AND attachments.creation_ts >= ? AND attachments.creation_ts <= ?
+ $attachments_where
+
+ ORDER BY $order ";
+
+ my $list = $dbh->selectall_arrayref($query, undef, @params);
+
+ if ($input->{debug}) {
+ while (my $param = shift @params) {
+ $query =~ s/\?/$dbh->quote($param)/e;
+ }
+ $vars->{debug_sql} = $query;
+ }
+
+ my @operations;
+ my $operation = {};
+ my $changes = [];
+ my $incomplete_data = 0;
+ my %bug_ids;
+
+ foreach my $entry (@$list) {
+ my ($fieldname, $bugid, $attachid, $when, $removed, $added, $who,
+ $comment_id) = @$entry;
+ my %change;
+ my $activity_visible = 1;
+
+ next unless Bugzilla->user->can_see_bug($bugid);
+
+ # check if the user should see this field's activity
+ if ($fieldname eq 'remaining_time'
+ || $fieldname eq 'estimated_time'
+ || $fieldname eq 'work_time'
+ || $fieldname eq 'deadline')
+ {
+ $activity_visible = Bugzilla->user->is_timetracker;
+ }
+ elsif ($fieldname eq 'longdescs.isprivate'
+ && !Bugzilla->user->is_insider
+ && $added)
+ {
+ $activity_visible = 0;
+ }
+ else {
+ $activity_visible = 1;
+ }
+
+ if ($activity_visible) {
+ # Check for the results of an old Bugzilla data corruption bug
+ if (($added eq '?' && $removed eq '?')
+ || ($added =~ /^\? / || $removed =~ /^\? /)) {
+ $incomplete_data = 1;
+ }
+
+ # Start a new changeset if required (depends on the grouping type)
+ my $is_new_changeset;
+ if ($order eq 'bug_when') {
+ $is_new_changeset =
+ $operation->{'who'} &&
+ (
+ $who ne $operation->{'who'}
+ || $when ne $operation->{'when'}
+ || $bugid != $operation->{'bug'}
+ );
+ } else {
+ $is_new_changeset =
+ $operation->{'bug'} &&
+ $bugid != $operation->{'bug'};
+ }
+ if ($is_new_changeset) {
+ $operation->{'changes'} = $changes;
+ push (@operations, $operation);
+ $operation = {};
+ $changes = [];
+ }
+
+ $bug_ids{$bugid} = 1;
+
+ $operation->{'bug'} = $bugid;
+ $operation->{'who'} = $who;
+ $operation->{'when'} = $when;
+
+ $change{'fieldname'} = $fieldname;
+ $change{'attachid'} = $attachid;
+ $change{'removed'} = $removed;
+ $change{'added'} = $added;
+ $change{'when'} = $when;
+
+ if ($comment_id) {
+ $change{'comment'} = Bugzilla::Comment->new($comment_id);
+ next if $change{'comment'}->count == 0;
+ }
+
+ if ($attachid) {
+ $change{'attach'} = Bugzilla::Attachment->new($attachid);
+ }
+
+ push (@$changes, \%change);
+ }
+ }
+
+ if ($operation->{'who'}) {
+ $operation->{'changes'} = $changes;
+ push (@operations, $operation);
+ }
+
+ $vars->{'incomplete_data'} = $incomplete_data;
+ $vars->{'operations'} = \@operations;
+
+ my @bug_ids = sort { $a <=> $b } keys %bug_ids;
+ $vars->{'bug_ids'} = \@bug_ids;
+ }
+
+ $vars->{'action'} = $action;
+ $vars->{'who'} = join(',', @who);
+ $vars->{'who_count'} = scalar @who;
+ $vars->{'from'} = $from;
+ $vars->{'to'} = $to;
+ $vars->{'group'} = $input->{'group'};
+}
+
+1;
diff --git a/extensions/BMO/lib/Util.pm b/extensions/BMO/lib/Util.pm
new file mode 100644
index 000000000..df781b9d2
--- /dev/null
+++ b/extensions/BMO/lib/Util.pm
@@ -0,0 +1,90 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BMO::Util;
+use strict;
+use warnings;
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Date::Parse;
+use DateTime;
+
+use base qw(Exporter);
+
+our @EXPORT = qw( string_to_datetime
+ time_to_datetime
+ parse_date
+ is_active_status_field );
+
+sub string_to_datetime {
+ my $input = shift;
+ my $time = parse_date($input)
+ or ThrowUserError('report_invalid_date', { date => $input });
+ return time_to_datetime($time);
+}
+
+sub time_to_datetime {
+ my $time = shift;
+ return DateTime->from_epoch(epoch => $time)
+ ->set_time_zone('local')
+ ->truncate(to => 'day');
+}
+
+sub parse_date {
+ my ($str) = @_;
+ if ($str =~ /^(-|\+)?(\d+)([hHdDwWmMyY])$/) {
+ # relative date
+ my ($sign, $amount, $unit, $date) = ($1, $2, lc $3, time);
+ my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date);
+ $amount = -$amount if $sign && $sign eq '+';
+ if ($unit eq 'w') {
+ # convert weeks to days
+ $amount = 7 * $amount + $wday;
+ $unit = 'd';
+ }
+ if ($unit eq 'd') {
+ $date -= $sec + 60 * $min + 3600 * $hour + 24 * 3600 * $amount;
+ return $date;
+ }
+ elsif ($unit eq 'y') {
+ return str2time(sprintf("%4d-01-01 00:00:00", $year + 1900 - $amount));
+ }
+ elsif ($unit eq 'm') {
+ $month -= $amount;
+ while ($month < 0) { $year--; $month += 12; }
+ return str2time(sprintf("%4d-%02d-01 00:00:00", $year + 1900, $month + 1));
+ }
+ elsif ($unit eq 'h') {
+ # Special case 0h for 'beginning of this hour'
+ if ($amount == 0) {
+ $date -= $sec + 60 * $min;
+ } else {
+ $date -= 3600 * $amount;
+ }
+ return $date;
+ }
+ return undef;
+ }
+ return str2time($str);
+}
+
+sub is_active_status_field {
+ my ($field) = @_;
+
+ if ($field->type == FIELD_TYPE_EXTENSION
+ && $field->isa('Bugzilla::Extension::TrackingFlags::Flag')
+ && $field->flag_type eq 'tracking'
+ && $field->name =~ /_status_/
+ ) {
+ return $field->is_active;
+ }
+
+ return 0;
+}
+
+1;
diff --git a/extensions/BMO/lib/WebService.pm b/extensions/BMO/lib/WebService.pm
new file mode 100644
index 000000000..ed94aabfc
--- /dev/null
+++ b/extensions/BMO/lib/WebService.pm
@@ -0,0 +1,200 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with the
+# License. You may obtain a copy of the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
+# the specific language governing rights and limitations under the License.
+#
+# The Original Code is the BMO Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Mozilla Foundation. Portions created
+# by the Initial Developer are Copyright (C) 2011 the Mozilla Foundation. All
+# Rights Reserved.
+#
+# Contributor(s):
+# Dave Lawrence <dkl@mozilla.com>
+
+package Bugzilla::Extension::BMO::WebService;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::WebService);
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Util qw(detaint_natural trick_taint);
+use Bugzilla::WebService::Util qw(validate);
+use Bugzilla::Field;
+
+sub getBugsConfirmer {
+ my ($self, $params) = validate(@_, 'names');
+ my $dbh = Bugzilla->dbh;
+
+ defined($params->{names})
+ || ThrowCodeError('params_required',
+ { function => 'BMO.getBugsConfirmer', params => ['names'] });
+
+ my @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} };
+
+ # start filtering to remove duplicate user ids
+ @user_objects = values %{{ map { $_->id => $_ } @user_objects }};
+
+ my $fieldid = get_field_id('bug_status');
+
+ my $query = "SELECT DISTINCT bugs_activity.bug_id
+ FROM bugs_activity
+ LEFT JOIN bug_group_map
+ ON bugs_activity.bug_id = bug_group_map.bug_id
+ WHERE bugs_activity.fieldid = ?
+ AND bugs_activity.added = 'NEW'
+ AND bugs_activity.removed = 'UNCONFIRMED'
+ AND bugs_activity.who = ?
+ AND bug_group_map.bug_id IS NULL
+ ORDER BY bugs_activity.bug_id";
+
+ my %users;
+ foreach my $user (@user_objects) {
+ my $bugs = $dbh->selectcol_arrayref($query, undef, $fieldid, $user->id);
+ $users{$user->login} = $bugs;
+ }
+
+ return \%users;
+}
+
+sub getBugsVerifier {
+ my ($self, $params) = validate(@_, 'names');
+ my $dbh = Bugzilla->dbh;
+
+ defined($params->{names})
+ || ThrowCodeError('params_required',
+ { function => 'BMO.getBugsVerifier', params => ['names'] });
+
+ my @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} };
+
+ # start filtering to remove duplicate user ids
+ @user_objects = values %{{ map { $_->id => $_ } @user_objects }};
+
+ my $fieldid = get_field_id('bug_status');
+
+ my $query = "SELECT DISTINCT bugs_activity.bug_id
+ FROM bugs_activity
+ LEFT JOIN bug_group_map
+ ON bugs_activity.bug_id = bug_group_map.bug_id
+ WHERE bugs_activity.fieldid = ?
+ AND bugs_activity.removed = 'RESOLVED'
+ AND bugs_activity.added = 'VERIFIED'
+ AND bugs_activity.who = ?
+ AND bug_group_map.bug_id IS NULL
+ ORDER BY bugs_activity.bug_id";
+
+ my %users;
+ foreach my $user (@user_objects) {
+ my $bugs = $dbh->selectcol_arrayref($query, undef, $fieldid, $user->id);
+ $users{$user->login} = $bugs;
+ }
+
+ return \%users;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Extension::BMO::Webservice - The BMO WebServices API
+
+=head1 DESCRIPTION
+
+This module contains API methods that are useful to user's of bugzilla.mozilla.org.
+
+=head1 METHODS
+
+See L<Bugzilla::WebService> for a description of how parameters are passed,
+and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
+
+=head2 getBugsConfirmer
+
+B<UNSTABLE>
+
+=over
+
+=item B<Description>
+
+This method returns public bug ids that a given user has confirmed (changed from
+C<UNCONFIRMED> to C<NEW>).
+
+=item B<Params>
+
+You pass a field called C<names> that is a list of Bugzilla login names to find bugs for.
+
+=over
+
+=item C<names> (array) - An array of strings representing Bugzilla login names.
+
+=back
+
+=item B<Returns>
+
+=over
+
+A hash of Bugzilla login names. Each name points to an array of bug ids that the user has confirmed.
+
+=back
+
+=item B<Errors>
+
+=item B<History>
+
+=over
+
+=item Added in BMO Bugzilla B<4.0>.
+
+=back
+
+=back
+
+=head2 getBugsVerifier
+
+B<UNSTABLE>
+
+=over
+
+=item B<Description>
+
+This method returns public bug ids that a given user has verified (changed from
+C<RESOLVED> to C<VERIFIED>).
+
+=item B<Params>
+
+You pass a field called C<names> that is a list of Bugzilla login names to find bugs for.
+
+=over
+
+=item C<names> (array) - An array of strings representing Bugzilla login names.
+
+=back
+
+=item B<Returns>
+
+=over
+
+A hash of Bugzilla login names. Each name points to an array of bug ids that the user has verified.
+
+=back
+
+=item B<Errors>
+
+=item B<History>
+
+=over
+
+=item Added in BMO Bugzilla B<4.0>.
+
+=back
+
+=back
diff --git a/extensions/BMO/t/bug_format_comment.t b/extensions/BMO/t/bug_format_comment.t
new file mode 100644
index 000000000..0356684e9
--- /dev/null
+++ b/extensions/BMO/t/bug_format_comment.t
@@ -0,0 +1,84 @@
+#!/usr/bin/perl -T
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+use strict;
+use warnings;
+use lib qw( . lib );
+
+use Test::More;
+use Bugzilla;
+use Bugzilla::Extension;
+
+my $class = Bugzilla::Extension->load('extensions/BMO/Extension.pm',
+ 'extensions/BMO/Config.pm');
+ok( $class->can('bug_format_comment'), 'the function exists');
+
+my $bmo = $class->new;
+ok($bmo, "got a new bmo extension");
+
+my $text = <<'END_OF_LINKS';
+# crash stats, a fake one
+bp-deadbeef-deaf-beef-beed-cafefeed1337
+
+# CVE/CAN security things
+CVE-2014-0160
+
+# svn
+r2424
+
+# bzr commit
+Committing to: bzr+ssh://dlawrence%40mozilla.com@bzr.mozilla.org/bmo/4.2
+modified extensions/Review/Extension.pm
+Committed revision 9257.
+
+# git with scp-style address
+To gitolite3@git.mozilla.org:bugzilla/bugzilla.git
+ 36f56bd..eab44b1 nouri -> nouri
+
+# git with uri (with login)
+To ssh://gitolite3@git.mozilla.org/bugzilla/bugzilla.git
+ 36f56bd..eab44b1 withuri -> withuri
+
+# git with uri (without login)
+To ssh://git.mozilla.org/bugzilla/bugzilla.git
+ 36f56bd..eab44b1 nologin -> nologin
+END_OF_LINKS
+
+my @regexes;
+
+$bmo->bug_format_comment({ regexes => \@regexes });
+
+ok(@regexes > 0, "got some regexes to play with");
+
+foreach my $re (@regexes) {
+ my ($match, $replace) = @$re{qw(match replace)};
+ if (ref($replace) eq 'CODE') {
+ $text =~ s/$match/$replace->({matches => [ $1, $2, $3, $4,
+ $5, $6, $7, $8,
+ $9, $10]})/egx;
+ }
+ else {
+ $text =~ s/$match/$replace/egx;
+ }
+}
+
+my @links = (
+ '<a href="https://crash-stats.mozilla.com/report/index/deadbeef-deaf-beef-beed-cafefeed1337">bp-deadbeef-deaf-beef-beed-cafefeed1337</a>',
+ '<a href="http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-0160">CVE-2014-0160</a>',
+ '<a href="http://viewvc.svn.mozilla.org/vc?view=rev&amp;revision=2424">r2424</a>',
+ '<a href="http://git.mozilla.org/?p=bugzilla/bugzilla.git;a=commit;h=eab44b1">36f56bd..eab44b1 withuri -> withuri</a>',
+ '<a href="http://git.mozilla.org/?p=bugzilla/bugzilla.git;a=commit;h=eab44b1">36f56bd..eab44b1 nouri -> nouri</a>',
+ '<a href="http://git.mozilla.org/?p=bugzilla/bugzilla.git;a=commit;h=eab44b1">36f56bd..eab44b1 nologin -> nologin</a>',
+ 'http://bzr.mozilla.org/bmo/4.2/revision/9257',
+);
+
+foreach my $link (@links) {
+ ok(index($text, $link) > -1, "check for $link");
+}
+
+
+done_testing;
diff --git a/extensions/BMO/template/en/default/account/create.html.tmpl b/extensions/BMO/template/en/default/account/create.html.tmpl
new file mode 100644
index 000000000..38acb6320
--- /dev/null
+++ b/extensions/BMO/template/en/default/account/create.html.tmpl
@@ -0,0 +1,178 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham <gerv@gerv.net>
+ # Byron Jones <glob@mozilla.com>
+ #%]
+
+[%# INTERFACE
+ # none
+ #
+ # Param("maintainer") is used to display the maintainer's email.
+ # Param("emailsuffix") is used to pre-fill the email field.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% title = BLOCK %]
+ Create a new [% terms.Bugzilla %] account
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = title
+ style_urls = [ 'extensions/BMO/web/styles/create_account.css' ]
+%]
+
+<script type="text/javascript">
+function onSubmit() {
+ var email = document.getElementById('login').value;
+ if (email == '') {
+ alert('You must enter your email address.');
+ return false;
+ }
+ var isValid =
+ email.match(/@/)
+ && email.match(/@.+\./)
+ && !email.match(/\.$/)
+ && !email.match(/[\\()&<>,'"\[\]]/)
+ ;
+ if (!isValid) {
+ alert(
+ "The e-mail address doesn't pass our syntax checking for a legal " +
+ "email address.\n\nA legal address must contain exactly one '@', and " +
+ "at least one '.' after the @.\n\nIt must also not contain any of " +
+ "these special characters: \ ( ) & < > , ; : \" [ ], or any whitespace."
+ );
+ return false;
+ }
+ return true;
+}
+</script>
+
+<table border="0" id="create-account">
+<tr>
+
+<td width="50%" id="create-account-left" valign="top">
+
+ <h2 class="column-header">I need help using a Mozilla Product</h2>
+
+ <table border="0" id="product-list">
+ [% INCLUDE product
+ icon = "firefox"
+ name = "Firefox Support"
+ url = "http://support.mozilla.com/"
+ desc = "Support for the Firefox web browser."
+ %]
+ [% INCLUDE product
+ icon = "firefox"
+ name = "Firefox for Mobile Support"
+ url = "http://support.mozilla.com/mobile"
+ desc = "Support for the Firefox Mobile web browser."
+ %]
+ [% INCLUDE product
+ icon = "thunderbird"
+ name = "Thunderbird Support"
+ url = "http://www.mozillamessaging.com/support/"
+ desc = "Support for Thunderbird email client."
+ %]
+ [% INCLUDE product
+ icon = "other"
+ name = "Support for other products"
+ url = "http://www.mozilla.org/projects/"
+ desc = "Support for products not listed here."
+ %]
+ [% INCLUDE product
+ icon = "input"
+ name = "Feedback"
+ url = "http://input.mozilla.com/feedback"
+ desc = "Report issues with a web site that you use, or provide quick feedback for Firefox."
+ %]
+ </table>
+
+</td>
+
+<td width="50%" id="create-account-right" valign="top">
+
+ <h2 class="column-header">I want to help</h2>
+
+ <div id="right-blurb">
+ <p>
+ Great! There are three things to know and do:
+ </p>
+ <ol>
+ <li>
+ Please consider reading our
+ <a href="https://developer.mozilla.org/en/Bug_writing_guidelines" target="_blank">[% terms.bug %] writing guidelines</a>.
+ </li>
+ <li>
+ [% terms.Bugzilla %] is a public place, so what you type and your email address will be visible
+ to all logged-in users. Some people use an
+ <a href="http://email.about.com/od/freeemailreviews/tp/free_email.htm" target="_blank">alternative email address</a>
+ for this reason.
+ </li>
+ <li>
+ Please give us an email address you want to use. Once we confirm that it works,
+ you'll be asked to set a password and then you can start filing [% terms.bugs %] and helping fix them.
+ </li>
+ </ol>
+ </div>
+
+ <h2 class="column-header">Create an account</h2>
+
+ <form method="post" action="createaccount.cgi" onsubmit="return onSubmit()">
+ <table id="create-account-form">
+ <tr>
+ <td class="label">Email Address:</td>
+ <td>
+ <input size="35" id="login" name="login" placeholder="you@example.com">[% Param('emailsuffix') FILTER html %]</td>
+ <td>
+ <input type="hidden" id="token" name="token" value="[% issue_hash_token(['create_account']) FILTER html %]">
+ <input type="submit" value="Create Account">
+ </td>
+ </tr>
+ </table>
+ </form>
+
+ [% Hook.process('additional_methods') %]
+
+</td>
+
+</tr>
+</table>
+
+<p id="bmo-admin">
+ If you think there's something wrong with [% terms.Bugzilla %], you can
+ <a href="mailto:bugzilla-admin@mozilla.org">send an email to the admins</a>, but
+ remember, they can't file [% terms.bugs %] for you, or solve tech support problems.
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
+
+[% BLOCK product %]
+ <tr>
+ <td valign="top">
+ <a href="[% url FILTER none %]"><img
+ src="extensions/BMO/web/producticons/[% icon FILTER uri %].png"
+ border="0" width="64" height="64"></a>
+ </td>
+ <td valign="top">
+ <h2><a href="[% url FILTER none %]">[% name FILTER html %]</a></h2>
+ <div>[% desc FILTER html %]</div>
+ </td>
+ </tr>
+[% END %]
+
diff --git a/extensions/BMO/template/en/default/bug/create/comment-automative.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-automative.txt.tmpl
new file mode 100644
index 000000000..c23a6427d
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-automative.txt.tmpl
@@ -0,0 +1,52 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+[% PROCESS global/variables.none.tmpl %]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi +%]
+>>Problem:
+[%+ cgi.param('desc_problem') %]
+
+>>Solution:
+[%+ cgi.param('desc_solution') %]
+
+>>Mozilla Top Level Goal:
+[%+ cgi.param('desc_top_level_goal') %]
+
+>>Existing [% terms.Bug %]:
+[% IF cgi.param('existing_bug') %]
+[%+ terms.Bug %] [% cgi.param("existing_bug") %]
+[% ELSE %]
+No [% terms.bug %]
+[% END %]
+
+>>Per-Commit:
+[%+ cgi.param('per_commit') || 'No' %]
+
+>>Data other than Pass/Fail:
+[%+ cgi.param('desc_data_produce') || 'No' %]
+
+>>Prototype Date:
+[%+ cgi.param("prototype_date") || 'Not provided' %]
+
+>>Production Date:
+[%+ cgi.param("production_date") || 'Not provided' %]
+
+>>Most Valuable Piece:
+[%+ cgi.param('most_valuable_piece') || 'Not provided' %]
+
+>>Responsible Engineer:
+[%+ cgi.param('responsible_engineer') || 'Not provided' %]
+
+>>Manager:
+[%+ cgi.param('manager') || 'Not provided' %]
+
+>>Other Teams/External Dependencies:
+[%+ cgi.param('other_teams') || 'Not provided' %]
+
+>>Additional Info:
+[%+ cgi.param('additional_info') || 'Not provided' %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-creative.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-creative.txt.tmpl
new file mode 100644
index 000000000..311736b5e
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-creative.txt.tmpl
@@ -0,0 +1,39 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi +%]
+>>Project/Request Title:
+[%+ cgi.param('short_desc') %]
+
+>>Project Overview:
+[%+ cgi.param('overview') %]
+
+>> Creative Help Needed:
+Copy: [% IF cgi.param('type_copy') %] Yes [% ELSE %] No [% END %]
+Design: [% IF cgi.param('type_design') %] Yes [% ELSE %] No [% END %]
+Video: [% IF cgi.param('type_video') %] Yes [% ELSE %] No [% END %]
+Other: [% IF cgi.param('type_other') %][% cgi.param('type_other_text') %][% ELSE %]No[% END %]
+
+>>Creative Specs:
+[%+ cgi.param("specs") %]
+
+>>CTA and Design:
+[%+ cgi.param('cta_design') %]
+
+>>Creative Due Date:
+[%+ cgi.param("cf_due_date") || 'Not provided' %]
+
+>>Launch Date:
+[%+ cgi.param("launch_date") || 'Not provided' %]
+
+>>Mozilla Goal:
+[%+ IF cgi.param("goal_other") %][% cgi.param("goal_other") %][% ELSE %][% cgi.param("goal") %][% END %]
+
+>>Points of Contact:
+[%+ cgi.param('cc').join(', ') || 'Not provided' %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-dev-engagement-event.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-dev-engagement-event.txt.tmpl
new file mode 100644
index 000000000..cb7473e22
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-dev-engagement-event.txt.tmpl
@@ -0,0 +1,84 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+::
+
+Name:
+[%+ cgi.param('name') %]
+
+Email Address:
+[%+ cgi.param('email') %]
+
+Role in relation to event:
+[%+ cgi.param('role') %]
+
+::
+
+Event Name:
+[%+ cgi.param('event') %]
+
+Start Date:
+[%+ cgi.param('start_date') %]
+
+End Date:
+[%+ cgi.param('end_date') %]
+
+Event Location:
+[%+ cgi.param('location') || "-" %]
+
+Venue:
+[%+ cgi.param('venue') || "-" %]
+
+Weblink:
+[%+ cgi.param('link') || "-" %]
+
+Expected Attendees:
+[%+ cgi.param('attendees') || "-" %]
+
+Event Description:
+[%+ cgi.param('desc') || "-" %]
+
+Primary Audience:
+[%+ cgi.param('audience') || "-" %]
+
+Relevant Products:
+[% "\n* Firefox OS" IF cgi.param('product-fxos') %]
+[% "\n* Firefox Web Browser" IF cgi.param('product-fx') %]
+[% "\n* Webmaker" IF cgi.param('product-webmaker') %]
+[% "\n* Persona" IF cgi.param('product-persona') %]
+[% "\n* Marketplace" IF cgi.param('product-marketplace') %]
+[% "\n* Thunderbird" IF cgi.param('product-tb') %]
+[% "\n* The Free and Open Web" IF cgi.param('product-fow') %]
+[% "\n* Other: " _ cgi.param('product-other-text') IF cgi.param('product-other') %]
+
+::
+
+Requests:
+[% "\n* Keynote Presentation" IF cgi.param('request-keynote') %]
+[% "\n* Talk Presentation" IF cgi.param('request-talk') %]
+[% "\n* Workshop" IF cgi.param('request-workshop') %]
+[% "\n* Sponsorship" IF cgi.param('request-sponsorship') %]
+[% "\n* Other: " _ cgi.param('request-other-text') IF cgi.param('request-other') %]
+
+Suggested sponsorship amount/level:
+[%+ cgi.param('sponsorship-suggestion') || "-" %]
+
+Already Registered Mozillians:
+[%+ cgi.param('mozillians') || "-" %]
+
+Requesting A Specific Person:
+[%+ cgi.param('specific') || "-" %]
+
+Alternative Person:
+[%+ cgi.param('fallback') || "-" %]
+
+Anything Else:
+[%+ cgi.param('else') || "-" %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-doc.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-doc.txt.tmpl
new file mode 100644
index 000000000..4c878a867
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-doc.txt.tmpl
@@ -0,0 +1,20 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi +%]
+:: Developer Documentation Request
+
+ Request Type: [% cgi.param("type") %]
+ Gecko Version: [% cgi.param("gecko") %]
+ Technical Contact: [% cgi.param("cc") %]
+
+:: Details
+
+[%+ cgi.param("details") %]
+
diff --git a/extensions/BMO/template/en/default/bug/create/comment-employee-incident.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-employee-incident.txt.tmpl
new file mode 100644
index 000000000..1b0902d64
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-employee-incident.txt.tmpl
@@ -0,0 +1,57 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the BMO Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% IF cgi.param('incident_type') == 'stolen' %]
+[% IF original_reporter -%]
+Reporter: [% original_reporter.identity FILTER none %]
+[%- END -%]
+
+ [% IF cgi.param('display_action') %]
+ [% IF cgi.param('display_action') == 'ldap' %]
+Action needed: Please immediately reset the LDAP password for this user.
+ [% ELSIF cgi.param('display_action') == 'ssh' %]
+Action needed: Please immediately disable the SSH key for this user.
+ [% END %]
+
+The user reported that their mobile or laptop device has been lost or stolen.
+This ticket was automatically generated from the employee incident reporting
+form. An additional ticket has been filed (see blocker bugs) for InfraSec to
+review the impact of this lost device.
+ [% END %]
+
+Type of device: [% cgi.param('device') %]
+Was the device encrypted?: [% cgi.param('encrypted') %]
+Any user data on the device?: [% cgi.param('userdata') %]
+ [% IF cgi.param('userdata') == 'Yes' %]
+Sensitive data on the device:
+[%+ cgi.param('sensitivedata') %]
+ [% END %]
+Browser configured to remember passwords?: [% cgi.param('rememberpasswords') %]
+ [% IF cgi.param('rememberpasswords') == 'Yes' %]
+Critical sites:
+[%+ cgi.param('criticalsites') %]
+ [% END %]
+[% END %]
+[% IF cgi.param('comment') %]
+Extra Notes:
+[%+ cgi.param('comment') %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-finance.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-finance.txt.tmpl
new file mode 100644
index 000000000..f0427b4c5
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-finance.txt.tmpl
@@ -0,0 +1,35 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Request Type: [% cgi.param('component') %]
+Summary: [% cgi.param('short_desc') %]
+Priority to your Team: [% cgi.param('team_priority') %]
+Timeframe for Signature: [% cgi.param('signature_time') %]
+
+Name of Other Party:
+[%+ cgi.param('other_party') %]
+
+Business Objective:
+[%+ cgi.param('business_obj') %]
+
+What is this purchase?:
+[%+ cgi.param('what_purchase') %]
+
+Why is this purchase needed?:
+[%+ cgi.param('why_purchase') %]
+
+What is the risk if this is not purchased?:
+[%+ cgi.param('risk_purchase') %]
+
+What is the alternative?:
+[%+ cgi.param('alternative_purchase') %]
+
+Total Cost: [% cgi.param('total_cost') %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-fxos-betaprogram.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-fxos-betaprogram.txt.tmpl
new file mode 100644
index 000000000..9370ff03c
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-fxos-betaprogram.txt.tmpl
@@ -0,0 +1,24 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Phone:
+[%+ cgi.param('phone') == 'Other' ? cgi.param('phone_other') : cgi.param('phone') %]
+
+Firefox OS Version:
+[%+ cgi.param('fxos_version') %]
+
+Issue Details:
+[%+ cgi.param('details') %]
+
+[% IF cgi.param('app') %]
+Associated App:
+[%+ cgi.param('app') %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-fxos-feature.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-fxos-feature.txt.tmpl
new file mode 100644
index 000000000..65224bfba
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-fxos-feature.txt.tmpl
@@ -0,0 +1,24 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi +%]
+>> Feature Request Title:
+[%+ cgi.param('short_desc') %]
+
+>> Description of feature, or problem to be solved
+[%+ cgi.param("description") %]
+
+>> Impact of implementing the feature/solution
+[%+ cgi.param("implement_impact") %]
+
+>> Impact of NOT implementing the feature/solution
+[%+ cgi.param("not_implement_impact") %]
+
+>> Date required
+[%+ cgi.param("date_required") %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-fxos-mcts-waiver.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-fxos-mcts-waiver.txt.tmpl
new file mode 100644
index 000000000..abad3f3c4
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-fxos-mcts-waiver.txt.tmpl
@@ -0,0 +1,36 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+[% PROCESS global/variables.none.tmpl %]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi +%]
+> Company Name
+[%+ cgi.param('company_name') %]
+
+> Device Description
+[%+ cgi.param('device_desc') %]
+
+> Firefox OS Release
+[%+ cgi.param('ffos_release') %]
+
+> Branding Tier
+[%+ cgi.param('branding_tier') %]
+
+> Distribution Countries
+[%+ cgi.param('dist_countries') %]
+
+> Distribution Channel
+[%+ cgi.param('dist_channel') %]
+
+> Reason for Waiver Request
+[%+ cgi.param('reason') %]
+
+> Rationale for Granting Waiver Request
+[%+ cgi.param('rationale') %]
+
+> Impact Analysis
+[%+ cgi.param('impact') %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-fxos-partner.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-fxos-partner.txt.tmpl
new file mode 100644
index 000000000..aa26d778f
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-fxos-partner.txt.tmpl
@@ -0,0 +1,23 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+What are the steps to reproduce?:
+[%+ cgi.param('steps_to_reproduce') %]
+
+What was the actual behavior?:
+[%+ cgi.param('actual_behavior') %]
+
+What was the expected behavior?:
+[%+ cgi.param('expected_behavior') %]
+
+What build were you using?: [% cgi.param('build') %]
+
+What are the requirements?: [% cgi.param('requirements') %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-fxos-preload-app.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-fxos-preload-app.txt.tmpl
new file mode 100644
index 000000000..a4e489724
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-fxos-preload-app.txt.tmpl
@@ -0,0 +1,28 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+[% PROCESS global/variables.none.tmpl %]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi +%]
+
+>>Company Name
+[%+ cgi.param('company_name') %]
+
+>>Apps Business Development Contact
+[%+ cgi.param('apps_business_dev_contact') %]
+
+>>Name of Firefox Marketplace apps of interest to you:
+[%+ cgi.param('preload_apps') %]
+
+>>Countries where your device will be distributed
+[%+ cgi.param('countries') %]
+
+>>Release Information
+[%+ cgi.param('release_info') %]
+
+>>Device Information
+[%+ cgi.param('device_info') %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-ipp.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-ipp.txt.tmpl
new file mode 100644
index 000000000..5c73587a9
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-ipp.txt.tmpl
@@ -0,0 +1,30 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi +%]
+:: Internet Public Policy Issue
+
+Region/Country: [% cgi.param("region") %]
+
+:: Description
+
+[%+ cgi.param("desc") %]
+
+:: Relevance
+
+[%+ cgi.param("relevance") %]
+
+Goal: [% cgi.param("goal") %]
+When: [% cgi.param("when") %]
+
+[% IF cgi.param("additional") %]
+:: Additional Information
+
+[%+ cgi.param("additional") %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-legal.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-legal.txt.tmpl
new file mode 100644
index 000000000..eb00a88d9
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-legal.txt.tmpl
@@ -0,0 +1,39 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the BMO Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Priority for your Team:
+[%+ cgi.param('teampriority') %]
+
+Timeframe for Completion:
+[%+ cgi.param('timeframe') %]
+
+Goal:
+[%+ cgi.param('goal') %]
+
+Business Objective:
+[%+ cgi.param('busobj') %]
+
+Other Party:
+[%+ cgi.param('otherparty') %]
+
+Description:
+[%+ cgi.param("comment") %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-mdn.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-mdn.txt.tmpl
new file mode 100644
index 000000000..60a443d2b
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-mdn.txt.tmpl
@@ -0,0 +1,66 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi +%]
+
+[% IF cgi.param('request_type') == 'Bug' %]
+What did you do?
+================
+[%+ cgi.param('bug_actions') %]
+
+What happened?
+==============
+[%+ cgi.param('bug_actual_results') %]
+
+What should have happened?
+==========================
+[%+ cgi.param('bug_expected_results') %]
+
+[% ELSIF cgi.param('request_type') == 'Feature' %]
+What problems would this solve?
+===============================
+[%+ cgi.param('feature_problem_solving') %]
+
+Who would use this?
+===================
+[%+ cgi.param('feature_audience') %]
+
+What would users see?
+=====================
+[%+ cgi.param('feature_interface') %]
+
+What would users do? What would happen as a result?
+===================================================
+[%+ cgi.param('feature_process') %]
+
+[% ELSIF cgi.param('request_type') == 'Change' %]
+What feature should be changed? Please provide the URL of the feature if possible.
+==================================================================================
+[%+ cgi.param('change_feature') %]
+
+What problems would this solve?
+===============================
+[%+ cgi.param('change_problem_solving') %]
+
+Who would use this?
+===================
+[%+ cgi.param('change_audience') %]
+
+What would users see?
+=====================
+[%+ cgi.param('change_interface') %]
+
+What would users do? What would happen as a result?
+===================================================
+[%+ cgi.param('change_process') %]
+
+[% END %]
+Is there anything else we should know?
+======================================
+[%+ cgi.param("description") %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-mobile-compat.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-mobile-compat.txt.tmpl
new file mode 100644
index 000000000..37b7d98d5
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-mobile-compat.txt.tmpl
@@ -0,0 +1,33 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi +%]
+
+Site: [%+ cgi.param("bug_file_loc") %]
+[%+ cgi.param("short_desc") %]
+
+:: Steps To Reproduce
+
+[%+ cgi.param("desc") %]
+
+:: Expected Result
+
+[%+ cgi.param("expected_result") %]
+
+:: Actual Result
+
+[%+ cgi.param("actual_result") %]
+
+:: Additional Information
+
+Software Version: [% cgi.param("software_version") %]
+[% IF cgi.param("device") %]
+Device Information: [% cgi.param("device") %]
+[% END %]
+Reporter's User Agent: [% cgi.param("user_agent") %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-mozlist.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-mozlist.txt.tmpl
new file mode 100644
index 000000000..c62461d42
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-mozlist.txt.tmpl
@@ -0,0 +1,44 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham <gerv@gerv.net>
+ #%]
+[%# INTERFACE:
+ # This template has no interface.
+ #
+ # Form variables from a bug submission (i.e. the fields on a template from
+ # enter_bug.cgi) can be access via Bugzilla.cgi.param. It can be used to
+ # pull out various custom fields and format an initial Description entry
+ # from them.
+ #%]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+List Name: [% cgi.param("listName") %]
+List Admin: [% cgi.param("listAdmin") %]
+
+Short Description:
+[%+ cgi.param("listShortDesc") %]
+
+[% IF cgi.param("listType") != "mozilla.com" %]
+Long Description:
+[%+ cgi.param("listLongDesc") %]
+[% END %]
+
+Justification / Special Instructions:
+
+[%+ cgi.param("comment") IF cgi.param("comment") %]
+
diff --git a/extensions/BMO/template/en/default/bug/create/comment-mozpr.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-mozpr.txt.tmpl
new file mode 100644
index 000000000..bfd421388
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-mozpr.txt.tmpl
@@ -0,0 +1,130 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi +%]
+[% PROCESS global/variables.none.tmpl +%]
+
+ Project Title: [% cgi.param("short_desc") %]
+
+Project Description and Scope:
+[%+ cgi.param("desc") %]
+
+== Timings
+
+ Start Date: [% cgi.param("start_date") %]
+ Announcement Date: [% cgi.param("announce_date") %]
+ Internal Deadline: [% cgi.param("deadline") %]
+
+== Owners
+
+ Project PR Owner: [% cgi.param("pr_owner") %]
+[%~ " " _ cgi.param("pr_owner_other") IF cgi.param("pr_owner") == "Other:" %]
+ Project Owner: [% cgi.param("owner") %]
+
+== RASCI
+
+ Responsible: [% cgi.param("rasci_r") || "-" %]
+ Approver: [% cgi.param("rasci_a") %]
+ Supporter: [% cgi.param("rasci_s") || "-" %]
+ Consultant: [% cgi.param("rasci_c") || "-" %]
+ Informed: [% cgi.param("rasci_i") || "-" %]
+
+== Details
+
+ Tier: [% cgi.param("tier") %]
+ PR Approach: [% cgi.param("pr_approach") %]
+Product Group Focus: [% cgi.param("group_focus") %]
+[%~ " " _ cgi.param("group_focus_other") IF cgi.param("group_focus") == "Other:" %]
+ Region: [% cgi.param("region") %]
+[%~ " " _ cgi.param("region_other") IF cgi.param("region") == "Other:" %]
+
+== Goals, Audience, and Messages
+
+Project Goals:
+[%+ cgi.param("project_goals") %]
+
+PR Goals:
+[%+ cgi.param("pr_goals") %]
+
+Company Goal: [% cgi.param("company_goal") %]
+
+Audiences:
+[% FOREACH audience = cgi.param("audience") %]
+ - [% audience %]
+[% " " _ cgi.param("audience_other") IF audience == "Other:" %]
+[% END %]
+
+Key Messages:
+[%+ cgi.param("key_messages") %]
+[% IF cgi.param("proj_mat_online") %]
+
+== Project Materials - Online Documentation
+
+ Description: [% cgi.param("proj_mat_online_desc") %]
+ Link: [% cgi.param("proj_mat_online_link") %]
+[% END %]
+[% IF cgi.param("proj_mat_file") %]
+
+== Project Materials - Attached
+
+ Description: [% cgi.param("proj_mat_file_desc") %]
+ File Name: [% cgi.param("proj_mat_file_attach") %]
+[% END %]
+[% IF cgi.param("pr_mat_online") %]
+
+== PR Project Materials - Online Documentation
+
+ Description: [% cgi.param("pr_mat_online_desc") %]
+ Link: [% cgi.param("pr_mat_online_link") %]
+[% END %]
+[% IF cgi.param("pr_mat_file") %]
+
+== PR Project Materials - Attached
+
+ Description: [% cgi.param("pr_mat_file_desc") %]
+ File Name: [% cgi.param("pr_mat_file_attach") %]
+[% END %]
+
+== Requirements
+
+ Metrica Coverage: [% cgi.param("metrica") %]
+[% IF cgi.param("press_center") %]
+
+Press Center Update:
+[% FOREACH option = cgi.param("press_center") %]
+ - [% option %]
+[% " " _ cgi.param("press_center_other") IF option == "Other:" %]
+[% END %]
+[% END %]
+[% IF cgi.param("resources") || cgi.param("internal_resources") %]
+
+ Internal Resources:
+[% " " _ cgi.param("resources") IF cgi.param("resources") %]
+[% FOREACH option = cgi.param("internal_resources") %]
+ - [% option %]
+[% " " _ cgi.param("internal_resources_other") IF option == "Other:" %]
+[% END %]
+[% END %]
+[% IF cgi.param("resources") || cgi.param("external_resources") %]
+
+ External Resources:
+[% FOREACH option = cgi.param("external_resources") %]
+ - [% option %]
+[% " " _ cgi.param("external_resources_other") IF option == "Other:" %]
+[% END %]
+[% END %]
+
+ Localization: [% cgi.param("localization") %]
+[%~ " " _ cgi.param("localization_other") IF cgi.param("localization") == "Other:" %]
+
+== Budget
+
+ Budget: [% cgi.param("budget") %]
+[%~ " " _ cgi.param("budget_extra") IF cgi.param("budget") == "Extra" %]
+
diff --git a/extensions/BMO/template/en/default/bug/create/comment-privacy-data.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-privacy-data.txt.tmpl
new file mode 100644
index 000000000..279d59b6b
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-privacy-data.txt.tmpl
@@ -0,0 +1,30 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Where does this data come from:
+
+[%+ cgi.param('source') %]
+
+What people and things does this data describe, and what fields does it contain:
+
+[%+ cgi.param('data_desc') %]
+
+What parts of this data do you want to release:
+
+[%+ cgi.param('release') %]
+
+Why are we releasing this data, and what do we hope people will do with it:
+
+[%+ cgi.param('why') %]
+
+Is there a particular time by which you would like to release this data:
+
+[%+ cgi.param('when') %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-recoverykey.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-recoverykey.txt.tmpl
new file mode 100644
index 000000000..9a38af7cc
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-recoverykey.txt.tmpl
@@ -0,0 +1,28 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the BMO Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Recovery Key: [% cgi.param('recoverykey') %]
+Asset Tag Number: [% cgi.param('assettag') %]
+
+[% IF cgi.param('comment') %]
+[%+ cgi.param('comment') %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-swag.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-swag.txt.tmpl
new file mode 100644
index 000000000..920d392da
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-swag.txt.tmpl
@@ -0,0 +1,50 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi +%]
+[% PROCESS global/variables.none.tmpl +%]
+:: Gear Requested
+
+ Purpose of Gear: [% cgi.param("purpose") %] [%+ cgi.param("purpose_other") %]
+ Date Required: [% cgi.param("date_required") || "-" %]
+
+[%+ cgi.param("items") %]
+
+:: Requester
+
+ Name: [% cgi.param('firstname') %] [% cgi.param('lastname') %]
+ Email: [% cgi.param('email') %]
+ Mozilla Space: [% cgi.param('mozspace') || "-" %]
+ Team/Department: [% cgi.param('teamcode') %]
+
+:: Recipient
+
+[% IF cgi.param("purpose") == "Mozillian Recognition" %]
+This [% terms.bug %] needs recipient shipping information: [% cgi.param("recognition_shipping") ? "Yes" : "No" %]
+This [% terms.bug %] needs recipient size information: [% cgi.param("recognition_sizing") ? "Yes" : "No" %]
+[% END %]
+
+ Name: [%+ cgi.param("shiptofirstname") +%] [%+ cgi.param("shiptolastname") +%]
+ Email: [%+ cgi.param("shiptoemail") +%]
+[% IF cgi.param("shiptoaddress1") %]
+ Address:
+ [%+ cgi.param("shiptoaddress1") +%]
+ [%+ cgi.param("shiptoaddress2") +%]
+ [%+ cgi.param("shiptocity") +%] [%+ cgi.param("shiptostate") +%] [%+ cgi.param("shiptopostcode") +%]
+ [%+ cgi.param("shiptocountry") %]
+ Phone: [% cgi.param("shiptophone") %]
+ Personal ID/RUT: [% cgi.param("shiptoidrut") || "-" %]
+[% END %]
+
+[% IF cgi.param("comment") %]
+:: Comments
+
+[%+ cgi.param("comment") %]
+[% END %]
+
diff --git a/extensions/BMO/template/en/default/bug/create/comment-user-engagement.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-user-engagement.txt.tmpl
new file mode 100644
index 000000000..cff8f23b8
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-user-engagement.txt.tmpl
@@ -0,0 +1,36 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi +%]
+>>Project/Request Title:
+[%+ cgi.param('short_desc') %]
+
+>>Project Goals:
+[%+ cgi.param('goals') %]
+
+>>Who are you trying to reach?:
+[%+ cgi.param("audience") %]
+
+>>Localization:
+[%+ cgi.param("localization") %]
+
+>>Destination URL:
+[%+ cgi.param("bug_file_loc") %]
+
+>>Timing:
+[%+ cgi.param("timing_date") %]
+
+>>Success:
+[%+ cgi.param("success") %]
+
+>>Mozilla Goal:
+[%+ cgi.param("mozilla_goal") %]
+
+>>Points of Contact:
+[%+ cgi.param('cc') || 'Not provided' %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-automative.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-automative.html.tmpl
new file mode 100644
index 000000000..cbe2da910
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-automative.html.tmpl
@@ -0,0 +1,276 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_style = BLOCK %]
+#automative_form {
+ padding: 10px;
+}
+#automative_form .required:after {
+ content: " *";
+ color: red;
+}
+#automative_form .field_label {
+ font-weight: bold;
+}
+#automative_form .field_desc {
+ padding-bottom: 3px;
+}
+#automative_form .field_desc,
+#automative_form .head_desc {
+ width: 600px;
+ word-wrap: normal;
+}
+#automative_form .head_desc {
+ padding-top: 5px;
+ padding-bottom: 12px;
+}
+#automative_form .form_section {
+ margin-bottom: 10px;
+}
+#automative_form textarea {
+ font-family: inherit;
+ font-size: inherit;
+}
+#automative_form em {
+ font-size: 1em;
+}
+.yui-calcontainer {
+ z-index: 2;
+}
+[% END %]
+
+[% inline_javascript = BLOCK %]
+function validateAndSubmit() {
+ 'use strict';
+ var alert_text = '';
+ var requiredLabels = YAHOO.util.Selector.query('label.required');
+ if (requiredLabels) {
+ requiredLabels.forEach(function (label) {
+ var name = label.getAttribute('for');
+ var ids = YAHOO.util.Selector.query(
+ '#automative_form *[name="' + name + '"]'
+ ).map(function (e) {
+ return e.id
+ });
+
+ if (ids && ids[0]) {
+ if (!isFilledOut(ids[0])) {
+ var desc = label.textContent || name;
+ alert_text +=
+ "Please enter a value for " +
+ desc.replace(/[\r\n]+/, "").replace(/\s+/g, " ") +
+ "\n";
+ }
+ }
+ });
+ }
+
+ if (alert_text != '') {
+ alert(alert_text);
+ return false;
+ }
+ return true;
+}
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Automation Request Form"
+ style = inline_style
+ javascript = inline_javascript
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/field.js', 'js/util.js' ]
+ yui = [ "autocomplete", "calendar", "selector" ]
+%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+<form id="automative_form" method="post" action="post_bug.cgi"
+ enctype="multipart/form-data" onSubmit="return validateAndSubmit();">
+ <input type="hidden" name="format" value="automative">
+ <input type="hidden" name="product" value="Testing">
+ <input type="hidden" name="component" value="General">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+ <input type="hidden" name="assigned_to" value="jgriffin@mozilla.com">
+
+ <div class="head_desc">
+ Welcome to the Automation Request Form!
+ </div>
+
+ <div class="form_section">
+ <label for="short_desc" class="field_label required">Summary</label>
+ <div class="field_desc">
+ One-line summary of the problem you'd like automation to help solve
+ </div>
+ <input type="text" name="short_desc" id="short_desc" size="80">
+ </div>
+
+ <div class="form_section">
+ <label for="desc_problem" class="field_label required">Problem</label>
+ <div class="field_desc">
+ Detailed description of the problem
+ </div>
+ <textarea id="desc_problem" name="desc_problem"
+ cols="80" rows="5"></textarea>
+ </div>
+
+ <div class="form_section">
+ <label for="desc_solution" class="field_label required">Solution</label>
+ <div class="field_desc">
+ Detailed description of the proposed automation solution
+ </div>
+ <textarea id="desc_solution" name="desc_solution"
+ cols="80" rows="5"></textarea>
+ </div>
+
+ <div class="form_section">
+ <label for="desc_top_level_goal" class="field_label required">Top Level
+ Goal</label>
+ <div class="field_desc">Describe the top-level project goal which this is
+ supporting</div>
+ <textarea id="desc_top_level_goal" name="desc_top_level_goal" cols="80"
+ rows="5"></textarea>
+ </div>
+
+ <div class="form_section">
+ <label for="existing_bug" class="field_label">Existing [% terms.Bug %]
+ number </label>
+ <div class="field_desc"> Existing [% terms.bug %] (if any) </div>
+ <input type="text" name="existing_bug" id="existing_bug" size="80">
+ </div>
+
+ <div class="form_section">
+ <label for="per_commit" class="field_label">Run per-commit?</label>
+ <div class="field_desc">
+ Does this automation need to be run per-commit and report to TBPL? Can it
+ be run less frequently?
+ </div>
+ <input type="text" name="per_commit" id="per_commit" size="80">
+ </div>
+
+ <div class="form_section">
+ <label for="desc_data_produce" class="field_label">Data capture?</label>
+ <div class="field_desc">If this automation will report data other than
+ pass/fail (e.g. some sort of performance metric), describe the data that
+ you'd like to have the automation produce. Do we already have a method of
+ capturing this kind of data, or do we need to develop one?</div>
+ <textarea id="desc_data_produce" name="desc_data_produce" cols="80"
+ rows="5"></textarea>
+ </div>
+
+ <div class="form_section">
+ <label for="prototype_date" class="field_label">Prototype Date</label>
+ <div class="field_desc">
+ When is a prototype needed?
+ </div>
+ <input name="prototype_date" size="20" id="prototype_date" value=""
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_prototype_date"
+ onclick="showCalendar('prototype_date')">
+ <span>Calendar</span>
+ </button>
+ <div id="con_calendar_prototype_date"></div>
+ <script type="text/javascript">
+ createCalendar('prototype_date')
+ </script>
+ </div>
+
+ <div class="form_section">
+ <label for="production_date" class="field_label">Production Date</label>
+ <div class="field_desc">
+ When is a finished project running in production needed?
+ </div>
+ <input name="production_date" size="20" id="production_date" value=""
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_production_date"
+ onclick="showCalendar('production_date')">
+ <span>Calendar</span>
+ </button>
+ <div id="con_calendar_production_date"></div>
+ <script type="text/javascript">
+ createCalendar('production_date')
+ </script>
+ </div>
+
+ <div class="form_section">
+ <label for="most_valuable_piece" class="field_label">Most Valuable
+ Piece?</label>
+ <div class="field_desc">If there are multiple pieces, tests, or features in
+ the proposed automation, what is the single most valuable piece?</div>
+ <input type="text" name="most_valuable_piece" id="most_valuable_piece"
+ size="80">
+ </div>
+
+ <div class="form_section">
+ <label for="responsible_engineer" class="field_label">Responsible
+ Engineer</label>
+ <div class="field_desc">
+ Which engineer is responsible for working with the automation engineer for
+ information, support, and troubleshooting?
+ </div>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "responsible_engineer"
+ name => "responsible_engineer"
+ value => ""
+ size => 80
+ classes => ["bz_userfield"]
+ %]
+ </div>
+
+ <div class="form_section">
+ <label for="manager" class="field_label">Manager</label>
+ <div class="field_desc">
+ Which manager/project manager is responsible for issues related to
+ milestones and priorities?
+ </div>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "manager"
+ name => "manager"
+ value => ""
+ size => 80
+ classes => ["bz_userfield"]
+ %]
+ </div>
+
+ <div class="form_section">
+ <label for="other_teams" class="field_label">Other Teams</label>
+ <div class="field_desc">
+ What other teams are involved and are there any other external
+ dependencies?
+ </div>
+ <textarea id="other_teams" name="other_teams" cols="80"
+ rows="5"></textarea>
+ </div>
+
+ <div class="form_section">
+ <label for="additional_info" class="field_label">Additional
+ Information</label>
+ <div class="field_desc">
+ Additional information
+ </div>
+ <textarea id="additional_info" name="additional_info" cols="80"
+ rows="5"></textarea>
+ </div>
+
+ <input type="submit" id="commit" value="Submit">
+
+ <p>
+ [ <span class="required_star">*</span> <span class="required_explanation">
+ Required Field</span> ]
+ </p>
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-creative.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-creative.html.tmpl
new file mode 100644
index 000000000..0c4fad8d6
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-creative.html.tmpl
@@ -0,0 +1,259 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_style = BLOCK %]
+#creative_form {
+ padding: 10px;
+}
+#creative_form .required:after {
+ content: " *";
+ color: red;
+}
+#creative_form .field_label {
+ font-weight: bold;
+}
+#creative_form .field_desc {
+ padding-bottom: 3px;
+}
+#creative_form .field_desc,
+#creative_form .head_desc {
+ width: 600px;
+ word-wrap: normal;
+}
+#creative_form .head_desc {
+ padding-top: 5px;
+ padding-bottom: 12px;
+}
+#creative_form .form_section {
+ margin-bottom: 10px;
+}
+#creative_form textarea {
+ font-family: inherit;
+ font-size: inherit;
+}
+#creative_form em {
+ font-size: 1em;
+}
+.yui-calcontainer {
+ z-index: 2;
+}
+[% END %]
+
+[% inline_javascript = BLOCK %]
+function validateAndSubmit() {
+ var alert_text = '';
+ if (!isFilledOut('overview')) alert_text += 'Please enter a value for Project Overview.\n';
+ if (!isFilledOut('short_desc')) alert_text += 'Please enter a value for Request Title.\n';
+ if (!isFilledOut('specs')) alert_text += 'Please enter a value for Creative Specs.\n';
+ if (!isFilledOut('cta_design')) alert_text += 'Please enter a value for CTA Design.\n';
+ if (!isFilledOut('cf_due_date')) alert_text += 'Please enter a value for the creative due date.\n';
+ if (!isFilledOut('goal')) alert_text += 'Please select a value for Mozilla Goal.\n';
+ if (YAHOO.util.Dom.get('goal').value == 'Other') {
+ if (!isFilledOut('goal_other')) alert_text += 'Please select a value for Mozilla Goal Other.\n';
+ }
+ if (YAHOO.util.Dom.get('type_copy').checked == false
+ && YAHOO.util.Dom.get('type_design').checked == false
+ && YAHOO.util.Dom.get('type_video').checked == false
+ && YAHOO.util.Dom.get('type_other').checked == false)
+ {
+ alert_text += 'Please select at least one type of help needed.\n';
+ }
+ if (YAHOO.util.Dom.get('type_other').checked == true) {
+ if (!isFilledOut('type_other_text')) alert_text += 'Please enter a value for other type of help needed.\n';
+ }
+ if (alert_text != '') {
+ alert(alert_text);
+ return false;
+ }
+ return true;
+}
+function toggleGoalOther() {
+ var goal_select = YAHOO.util.Dom.get('goal');
+ if (goal_select.options[goal_select.selectedIndex].value == 'Other') {
+ YAHOO.util.Dom.removeClass('goal_other','bz_default_hidden');
+ }
+ else {
+ YAHOO.util.Dom.addClass('goal_other','bz_default_hidden');
+ }
+}
+function toggleTypeOther(element) {
+ var other_text = YAHOO.util.Dom.get('type_other_text');
+ if (element.checked == true) {
+ other_text.disabled = false;
+ other_text.focus();
+ }
+ else {
+ other_text.disabled = true;
+ }
+}
+
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Creative Initiation Form"
+ style = inline_style
+ javascript = inline_javascript
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/field.js', 'js/util.js' ]
+ yui = [ "autocomplete", "calendar" ]
+%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+<form id="creative_form" method="post" action="post_bug.cgi" enctype="multipart/form-data"
+ onSubmit="return validateAndSubmit();">
+ <input type="hidden" name="format" value="creative">
+ <input type="hidden" name="product" value="Marketing">
+ <input type="hidden" name="component" value="Design">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+<img title="Creative Initiation Form" src="extensions/BMO/web/images/creative.png">
+
+<div class="head_desc">
+ Have a new project or campaign that requires copy, design, video or other awesomeness
+ from your friendly neighborhood Brand Team? Please use this form to tell us about it
+ and we'll get back to you with next steps as soon as possible.
+</div>
+
+<div class="form_section">
+ <label for="short_desc" class="field_label required">Project / Request Title</label>
+ <div class="field_desc">
+ Describe your project or request in a few words or a short phrase.
+ </div>
+ <input type="text" name="short_desc" id="short_desc" size="80">
+</div>
+
+<div class="form_section">
+ <label for="overview" class="field_label required">Project Overview</label>
+ <div class="field_desc">
+ Briefly describe the background, goals and objectives for this project.
+ </div>
+ <textarea id="overview" name="overview" cols="80" rows="5"></textarea>
+</div>
+
+<div class="form_section">
+ <label for="specs" class="field_label required">Creative Specs and Deliverables</label>
+ <div class="field_desc">
+ Select what sort of help you need (check at least one or more)
+ </div>
+ <input type="checkbox" name="type_copy" id="type_copy" value="1">Copy<br>
+ <input type="checkbox" name="type_design" id="type_design" value="1">Design<br>
+ <input type="checkbox" name="type_video" id="type_video" value="1">Video<br>
+ <input type="checkbox" name="type_other" id="type_other" value="1" onchange="toggleTypeOther(this);">Other&nbsp;&nbsp;
+ <input type="text" name="type_other_text" id="type_other_text"><br>
+ <br>
+ <div class="field_desc">
+ <strong class="required">Specs</strong><br>
+ What is the final deliverable and what format should it be delivered in?
+ Please include information on the format, image/file size, word count, video length,
+ etc. We like details here.
+ </div>
+ <textarea id="specs" name="specs" cols="80" rows="5"></textarea>
+ <br>
+ <br>
+ <div class="field_desc">
+ <strong class="required">CTAs and design directions</strong><br>
+ Provide as much information as possible. Make sure to include links to documents with copy,
+ mock-ups, wireframes, or any other information or assets that could help with direction.
+ </div>
+ <textarea id="cta_design" name="cta_design" cols="80" rows="5"></textarea>
+</div>
+
+<div class="form_section">
+ <label for="cf_due_date" class="field_label required">Creative Due Date</label>
+ <div class="field_desc">
+ Working backwards from your launch/go-live date, when do you need final assets?
+ </div>
+ <input name="cf_due_date" size="20" id="cf_due_date" value=""
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_cf_due_date"
+ onclick="showCalendar('cf_due_date')">
+ <span>Calendar</span>
+ </button>
+ <div id="con_calendar_cf_due_date"></div>
+ <script type="text/javascript">
+ createCalendar('cf_due_date')
+ </script>
+</div>
+
+<div class="form_section">
+ <label for="launch_date" class="field_label">Launch Date</label>
+ <div class="field_desc">
+ When will your project go forth into the world?
+ </div>
+ <input name="launch_date" size="20" id="launch_date" value=""
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_launch_date"
+ onclick="showCalendar('launch_date')">
+ <span>Calendar</span>
+ </button>
+ <div id="con_calendar_launch_date"></div>
+ <script type="text/javascript">
+ createCalendar('launch_date')
+ </script>
+</div>
+
+<div class="form_section">
+ <label for="goal" class="field_label required">Mozilla Goal</label>
+ <div class="field_desc">
+ Which high-level Mozilla goal does this project support?
+ </div>
+ <select id="goal" name="goal"
+ onchange="toggleGoalOther();">
+ <option value="">Please select..</option>
+ <option value="Firefox OS">Firefox OS</option>
+ <option value="Firefox Browser">Firefox Browser</option>
+ <option value="Million Mozillians">Million Mozillians</option>
+ <option value="Services">Services</option>
+ <option value="Org Support">Org Support</option>
+ <option value="Other">Other</option>
+ </select>
+ <br>
+ <input type="text" name="goal_other" id="goal_other" size="40"
+ class="bz_default_hidden" value="">
+</div>
+
+<div class="form_section">
+ <label for="cc" class="field_label">Points of Contact</label>
+ <div class="field_desc">
+ Who should be kept in the loop and informed of updates (and CC'd on the [% terms.bug %])?
+ </div>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => ""
+ size => 80
+ classes => ["bz_userfield"]
+ multiple => 5
+ %]
+</div>
+
+<div class="head_desc">
+ Thanks! Once you hit submit, your request will go off into the vortex of creative magic.
+ (Actually, it goes to [% terms.Bugzilla %], but that doesn't sound as cool.) We'll be in touch soon
+ with next steps and to let you know if we need any additional info.
+</div>
+
+<input type="submit" id="commit" value="Submit">
+
+<p>
+ [ <span class="required_star">*</span> <span class="required_explanation">Required Field</span> ]
+</p>
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-dev-engagement-event.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-dev-engagement-event.html.tmpl
new file mode 100644
index 000000000..ef6737098
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-dev-engagement-event.html.tmpl
@@ -0,0 +1,537 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_css = BLOCK %]
+ #bug_form {
+ max-width: 50em;
+ }
+
+ #bug_form th {
+ text-align: left;
+ padding-top: 0.5em;
+ }
+
+ #bug_form .section-head {
+ font-size: larger;
+ padding-top: 1em;
+ }
+
+ #bug_form th:not(.section-head), #bug_form td {
+ padding-left: 2em;
+ }
+
+ #bug_form .mandatory {
+ color: red;
+ }
+
+ #bug_form .blurb {
+ font-style: italic;
+ }
+
+ #bug_form .wide {
+ width: 40em;
+ }
+
+ #bug_form input[disabled] {
+ background: transparent;
+ }
+[% END %]
+
+[% inline_js = BLOCK %]
+// <script>
+ function onRequestOtherChange() {
+ var cb = document.getElementById('request-other');
+ var input = document.getElementById('request-other-text');
+ input.disabled = !cb.checked;
+ if (cb.checked)
+ input.focus();
+ }
+
+ function onRequestSponsorshipChange() {
+ var cb = document.getElementById('request-sponsorship');
+ if (cb.checked) {
+ YAHOO.util.Dom.removeClass('sponsorship-suggestion-fields', 'bz_default_hidden');
+ }
+ else {
+ YAHOO.util.Dom.addClass('sponsorship-suggestion-fields', 'bz_default_hidden');
+ }
+ }
+
+ function onProductOtherChange() {
+ var cb = document.getElementById('product-other');
+ var input = document.getElementById('product-other-text');
+ input.disabled = !cb.checked;
+ if (cb.checked)
+ input.focus();
+ }
+
+ function onSubmit() {
+ if (document.getElementById('request-other').checked
+ && !isFilledOut('request-other-text')
+ ) {
+ document.getElementById('request-other').checked = false;
+ onRequestOtherChange();
+ }
+
+ var alert_text = '';
+
+ if (!isFilledOut('name'))
+ alert_text += "Please enter your name.\n";
+ if (!isFilledOut('email'))
+ alert_text += "Please enter your email address.\n";
+ if (!isFilledOut('role'))
+ alert_text += "Please enter your role.\n";
+
+ if (!isFilledOut('event'))
+ alert_text += "Please enter the event name.\n";
+ if (!isFilledOut('start_date'))
+ alert_text += "Please enter the event start date.\n";
+ if (!isFilledOut('end_date'))
+ alert_text += "Please enter the event end date.\n";
+ if (!isFilledOut('attendees'))
+ alert_text += "Please enter number of expected attendees.\n";
+ if (!isFilledOut('audience'))
+ alert_text += "Please enter primary audience.\n";
+
+
+ var wb = '';
+ if (document.getElementById('request-keynote').checked)
+ wb += '[keynote] ';
+ if (document.getElementById('request-talk').checked)
+ wb += '[talk] ';
+ if (document.getElementById('request-workshop').checked)
+ wb += '[workshop] ';
+ if (document.getElementById('request-sponsorship').checked)
+ wb += '[sponsorship] ';
+ if (document.getElementById('request-other').checked)
+ wb += '[other] ';
+ if (wb == '')
+ alert_text += "Please select what you're requesting.\n";
+
+ if (alert_text != '') {
+ alert(alert_text);
+ return false;
+ }
+
+ document.getElementById('status_whiteboard').value = wb.replace(/ $/, '');
+ var summary = document.getElementById('event').value + ', ' + long_start_date();
+ var loc = document.getElementById('location').value;
+ if (loc)
+ summary = summary + ' (' + loc + ')';
+ document.getElementById('short_desc').value = summary;
+ document.getElementById('bug_file_loc').value = document.getElementById('link').value;
+ document.getElementById('cf_due_date').value = document.getElementById('start_date').value;
+
+ return true;
+ }
+
+ function long_start_date() {
+ var ymd = document.getElementById('start_date').value.split('-');
+ if (ymd.length != 3)
+ return '';
+ var month = YAHOO.bugzilla.calendar_start_date.cfg.getProperty('MONTHS_LONG')[ymd[1] - 1];
+ return month + ' ' + ymd[0];
+ }
+
+ YAHOO.util.Event.onDOMReady(function() {
+ createCalendar('start_date');
+ createCalendar('end_date');
+ onRequestOtherChange();
+ onRequestSponsorshipChange();
+ onProductOtherChange();
+ });
+// </script>
+[% END %]
+
+[% mandatory = BLOCK %]
+ <span class="mandatory" title="Mandatory">*</span>
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Developer Events Request Form"
+ style = inline_css
+ style_urls = [ 'skins/standard/enter_bug.css' ]
+ javascript = inline_js
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js', 'js/field.js', 'js/util.js' ]
+ yui = [ 'calendar' ]
+%]
+
+<h2>Developer Events Request Form</h2>
+
+<form method="post" action="post_bug.cgi" id="bug_form" class="enter_bug_form"
+ enctype="multipart/form-data" onsubmit="return onSubmit();">
+<input type="hidden" name="format" value="dev-engagement-event">
+<input type="hidden" name="product" value="Developer Engagement">
+<input type="hidden" name="short_desc" id="short_desc" value="">
+<input type="hidden" name="component" value="Events Request">
+<input type="hidden" name="rep_platform" value="All">
+<input type="hidden" name="op_sys" value="All">
+<input type="hidden" name="priority" value="--">
+<input type="hidden" name="version" value="unspecified">
+<input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+<input type="hidden" name="comment" id="comment" value="">
+<input type="hidden" name="status_whiteboard" id="status_whiteboard" value="">
+<input type="hidden" name="bug_file_loc" id="bug_file_loc" value="">
+<input type="hidden" name="cf_due_date" id="cf_due_date" value="">
+<input type="hidden" name="groups" id="groups" value="mozilla-employee-confidential">
+<input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table id="bug_form">
+
+<tr>
+ <td>
+ <p>
+ Hi! Thanks so much for asking Mozilla to participate at your event!
+ </p>
+ <p>
+ The Developer Events Team evaluates each request individually, based on
+ multiple criteria, including quarterly goals and priorities. We meet at
+ least biweekly, and this form is designed to gather all the information
+ we need to evaluate each request at these meetings. Please take a minute
+ to fill it out thoroughly so we can process your request as soon as
+ possible.
+ </p>
+ <p>
+ Please review our <a href="https://wiki.mozilla.org/Engagement/Developer_Engagement/Event_request_guidelines">
+ event request guidelines</a> for information about how we evaluate requests.
+ </p>
+ </td>
+</tr>
+
+<tr>
+ <th class="section-head">
+ First, tell us about yourself!
+ </th>
+</tr>
+
+<tr>
+ <th>
+ What is your name? [% mandatory FILTER none %]
+ </th>
+</tr>
+<tr>
+ <td>
+ <input type="text" name="name" id="name" size="40" class="wide"
+ value="[% user.name FILTER html %]">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ Please provide your email address. [% mandatory FILTER none %]
+ </th>
+</tr>
+<tr>
+ <td>
+ <input type="text" name="email" id="email" size="40" class="wide"
+ value="[% user.login FILTER html %]">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ What is your role in relation to this event? [% mandatory FILTER none %]
+ </th>
+</tr>
+<tr>
+ <td>
+ <div class="blurb">
+ eg. organizer, speaker/atendee (past), speaker/attendee (current), etc.
+ </div>
+ <input type="text" name="role" id="role" size="40" class="wide">
+ </td>
+</tr>
+
+<tr>
+ <th class="section-head">
+ Let's start with the basics.
+ </th>
+</tr>
+
+<tr>
+ <th>
+ Event Name [% mandatory FILTER none %]
+ </th>
+</tr>
+<tr>
+ <td>
+ <input type="text" name="event" id="event" size="40" class="wide">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ Start Date [% mandatory FILTER none %]
+ </th>
+</tr>
+<tr>
+ <td>
+ <input type="text" name="start_date" id="start_date" size="15" class="date"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_start_date"
+ onclick="showCalendar('start_date')">
+ <span>Calendar</span>
+ </button>
+ <div id="con_calendar_start_date"></div>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ End Date [% mandatory FILTER none %]
+ </th>
+</tr>
+<tr>
+ <td>
+ <input type="text" name="end_date" id="end_date" size="15" class="date"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_end_date"
+ onclick="showCalendar('end_date')">
+ <span>Calendar</span>
+ </button>
+ <div id="con_calendar_end_date"></div>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ Event Location
+ </th>
+</tr>
+<tr>
+ <td>
+ <div class="blurb">
+ Include city, state, and country. Please write "Multiple" if this event
+ takes place across several locations.
+ </div>
+ <input type="text" name="location" id="location" size="40" class="wide">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ Venue
+ </th>
+</tr>
+<tr>
+ <td>
+ <div class="blurb">
+ What is the name of the venue where your event will be held? Enter TBD if
+ you don't know yet.
+ </div>
+ <input type="text" name="venue" id="venue" size="40" class="wide">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ Weblink
+ </th>
+</tr>
+<tr>
+ <td>
+ <div class="blurb">
+ Weblink to the event site, Eventbrite page, Lanyrd page, Meetup page, etc.
+ </div>
+ <input type="text" name="link" id="link" size="40" class="wide">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ Number of expected attendees [% mandatory FILTER none %]
+ </th>
+</tr>
+<tr>
+ <td>
+ <input type="text" name="attendees" id="attendees" size="15">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ Please give a [short] description of the event. [% mandatory FILTER none %]
+ </th>
+</tr>
+<tr>
+ <td>
+ <div class="blurb">
+ Include track topics, presentation topics, event format.
+ </div>
+ <textarea name="desc" id="desc" rows="10" cols="40" class="wide"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ Who is the primary audience for this event? [% mandatory FILTER none %]
+ </th>
+</tr>
+<tr>
+ <td>
+ <div class="blurb">
+ Developers (specify coding language and platform), business development,
+ marketing associates, corporate executives, etc.
+ </div>
+ <input type="text" name="audience" id="audience" size="40" class="wide">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ Which Mozilla products/projects are most relevant to this event? [% mandatory FILTER none %]
+ </th>
+</tr>
+<tr>
+ <td>
+ <div class="blurb">
+ Please select all that apply.
+ See <a href="https://www.mozilla.org/en-US/products/" target="_blank">mozilla.org/products</a>
+ for more information about Mozilla products.
+ </div>
+ <input type="checkbox" name="product-fxos" id="product-fxos">
+ <label for="product-fxos">Firefox OS</label><br>
+ <input type="checkbox" name="product-fx" id="product-fx">
+ <label for="product-fx">Firefox Web Browser</label><br>
+ <input type="checkbox" name="product-webmaker" id="product-webmaker">
+ <label for="product-webmaker">Webmaker</label><br>
+ <input type="checkbox" name="product-persona" id="product-persona">
+ <label for="product-persona">Persona</label><br>
+ <input type="checkbox" name="product-marketplace" id="product-marketplace">
+ <label for="product-marketplace">Marketplace</label><br>
+ <input type="checkbox" name="product-tb" id="product-tb">
+ <label for="product-tb">Thunderbird</label><br>
+ <input type="checkbox" name="product-fow" id="product-fow">
+ <label for="product-fow">The Free and Open Web</label><br>
+ <input type="checkbox" name="product-other" id="product-other" onchange="onProductOtherChange()">
+ <label for="product-other">Other:</label>
+ <input type="text" name="product-other-text" id="product-other-text" size="40" disabled>
+ </td>
+</tr>
+
+<tr>
+ <th class="section-head">
+ Tell us more about what you're looking for!
+ </th>
+</tr>
+
+<tr>
+ <th>
+ What are you requesting from Mozilla? [% mandatory FILTER none %]
+ </th>
+</tr>
+<tr>
+ <td>
+ <div class="blurb">
+ Please select all that apply.
+ </div>
+ <input type="checkbox" name="request-keynote" id="request-keynote">
+ <label for="request-keynote">Keynote Presentation</label><br>
+ <input type="checkbox" name="request-talk" id="request-talk">
+ <label for="request-talk">Talk Presentation (non-keynote)</label><br>
+ <input type="checkbox" name="request-workshop" id="request-workshop">
+ <label for="request-workshop">Workshop</label><br>
+ <input type="checkbox" name="request-sponsorship" id="request-sponsorship" onchange="onRequestSponsorshipChange()">
+ <label for="request-sponsorship">Sponsorship</label><br>
+ <input type="checkbox" name="request-other" id="request-other" onchange="onRequestOtherChange()">
+ <label for="request-other">Other:</label>
+ <input type="text" name="request-other-text" id="request-other-text" size="40" disabled>
+ </td>
+</tr>
+
+<tbody id="sponsorship-suggestion-fields">
+ <tr>
+ <th>
+ If requesting sponsorship, what amount/level do you suggest?
+ </th>
+ </tr>
+ <tr>
+ <td>
+ <input type="text" name="sponsorship-suggestion" id="sponsorship-suggestion" size="40" class="wide">
+ </td>
+ </tr>
+</tbody>
+
+<tr>
+ <th>
+ Please list the names of anyone from Mozilla who are already registered to
+ attend, speak, or participate in this event.
+ </th>
+</tr>
+<tr>
+ <td>
+ <input type="text" name="mozillians" id="mozillians" size="40" class="wide">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ Are you requesting a specific person to present or participate at this
+ event? If so, please list their name(s).
+ </th>
+</tr>
+<tr>
+ <td>
+ <input type="text" name="specific" id="specific" size="40" class="wide">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ If this individual is unable to attend/speak/participate in this event, is
+ there anyone else you would like to request?
+ </th>
+</tr>
+<tr>
+ <td>
+ <input type="text" name="fallback" id="fallback" size="40" class="wide">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ Please upload a Sponsorship Prospectus if you have one.
+ </th>
+</tr>
+<tr>
+ <td>
+ <input type="file" name="data" id="data" size="40">
+ <input type="hidden" name="contenttypemethod" value="autodetect">
+ <input type="hidden" id="description" name="description" value="Sponsorship Prospectus">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ Anything else that may help us review this request?
+ </th>
+</tr>
+<tr>
+ <td>
+ <input type="text" name="else" id="else" size="40" class="wide">
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+</tr>
+<tr>
+ <td>
+ <input type="submit" id="commit" value="Submit Request">
+ </td>
+</tr>
+
+</table>
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-doc.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-doc.html.tmpl
new file mode 100644
index 000000000..5b75976d9
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-doc.html.tmpl
@@ -0,0 +1,222 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_style = BLOCK %]
+#doc_form th {
+ text-align: right;
+}
+
+#short_desc, #details {
+ width: 100%;
+}
+[% END %]
+
+[% inline_javascript = BLOCK %]
+function validateAndSubmit() {
+ var alert_text = '';
+ if (!isFilledOut('type')) alert_text += 'Please select the "Request Type".\n';
+ if (!isFilledOut('short_desc')) alert_text += 'Please enter a "Summary".\n';
+ if (!isFilledOut('gecko')) alert_text += 'Please select the "Gecko Version".\n';
+ if (!isFilledOut('details')) alert_text += 'Please enter some "Details".\n';
+ if (alert_text != '') {
+ alert(alert_text);
+ return false;
+ }
+ return true;
+}
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Developer Documentation Request"
+ style = inline_style
+ javascript = inline_javascript
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/field.js', 'js/util.js', 'js/bug.js' ]
+ yui = [ 'autocomplete', 'datatable', 'button' ]
+%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+<h1>Developer Documentation Request</h1>
+
+<p>
+ Use this form to request <b>new documentation</b> or <b>corrections</b> to existing documentation.<br>
+ [ <span class="required_star">*</span> <span class="required_explanation">Required Fields</span> ]
+</p>
+
+<form method="post" action="post_bug.cgi" enctype="multipart/form-data"
+ onSubmit="return validateAndSubmit();">
+ <input type="hidden" name="format" value="doc">
+ <input type="hidden" name="product" value="Developer Documentation">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="status_whiteboard" id="status_whiteboard" value="">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table id="doc_form">
+
+<tr>
+ <th class="required">Request Type</th>
+ <td>
+ <select name="type" id="type">
+ <option value="">Please select..</option>
+ <option value="New Documentation">New Documentation</option>
+ <option value="Correction" [% "selected" IF cgi.param('bug_file_loc') %]>Correction</option>
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <th class="required">Topic</th>
+ <td>
+ <select name="component" id="component">
+ [% FOREACH component = product.components %]
+ <option value="[% component.name FILTER html %]"
+ [% " selected" IF component.name == "General" %]
+ title="[% component.description FILTER html %]">
+ [% component.name FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <th class="required">Summary</th>
+ <td>
+ Please provide a brief summary of what documentation you're requesting, or
+ what problem you're reporting in existing documentation:<br>
+ <input type="text" name="short_desc" id="short_desc" size="60">
+ </td>
+</tr>
+
+[% IF feature_enabled('jsonrpc') AND !cloned_bug_id %]
+ <tr id="possible_duplicates_container" class="bz_default_hidden">
+ <th>Possible<br>Duplicates:</th>
+ <td colspan="3">
+ <div id="possible_duplicates"></div>
+ <script type="text/javascript">
+ var dt_columns = [
+ { key: "id", label: "[% field_descs.bug_id FILTER js %]",
+ formatter: YAHOO.bugzilla.dupTable.formatBugLink },
+ { key: "summary",
+ label: "[% field_descs.short_desc FILTER js %]",
+ formatter: "text" },
+ { key: "status",
+ label: "[% field_descs.bug_status FILTER js %]",
+ formatter: YAHOO.bugzilla.dupTable.formatStatus },
+ { key: "update_token", label: '',
+ formatter: YAHOO.bugzilla.dupTable.formatCcButton }
+ ];
+ YAHOO.bugzilla.dupTable.addCcMessage = "Add Me to the CC List";
+ YAHOO.bugzilla.dupTable.init({
+ container: 'possible_duplicates',
+ columns: dt_columns,
+ product_name: '[% product.name FILTER js %]',
+ summary_field: 'short_desc',
+ options: {
+ MSG_LOADING: 'Searching for possible duplicates...',
+ MSG_EMPTY: 'No possible duplicates found.',
+ SUMMARY: 'Possible Duplicates'
+ }
+ });
+ </script>
+ </td>
+ </tr>
+[% END %]
+
+<tr>
+ <th>Page to Update</th>
+ <td>
+ <input type="text" name="bug_file_loc" id="short_desc" size="60"
+ value="[% bug_file_loc FILTER html %]">
+ </td>
+</tr>
+
+<tr>
+ <th>Technical Contact</th>
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => ""
+ size => 60
+ classes => ["bz_userfield"]
+ multiple => 5
+ %]
+ <br>
+ <a href="https://developer.mozilla.org/en-US/docs/Project:Subject-matter_experts"
+ target="_blank" id="common_topic_experts">
+ List of common topic experts</a>
+ </td>
+</tr>
+
+<tr>
+ <th class="required">Gecko Version</th>
+ <td>
+ <select name="gecko" id="gecko">
+ [% FOREACH version = versions %]
+ <option value="[% version.name FILTER html %]"
+ [% " selected" IF version.name == "unspecified" %]>
+ [% version.name FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <th class="required">Details</th>
+ <td>
+ <textarea id="details" name="details" cols="50" rows="10"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <th>Development [% terms.Bug %]</th>
+ <td>
+ <input type="text" id="blocked" name="blocked" size="10">
+ <i>Corresponding development [% terms.bug %].</i>
+ </td>
+</tr>
+
+<tr>
+ <th class="required">Urgency</th>
+ <td>
+ <select name="priority" id="priority">
+ <option value="P1">Immediately</option>
+ <option value="P2">Before Release</option>
+ <option value="P3">Before Aurora</option>
+ <option value="P4">Before Beta</option>
+ <option value="P5" selected>No Rush</option>
+ </select>
+ <br>
+ Due to the volume of requests, the documentation team can't commit to
+ meeting specific deadlines for given documentation requests, but we will do
+ our best.
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td><input type="submit" id="commit" value="Submit Request"></td>
+</tr>
+
+</table>
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-employee-incident.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-employee-incident.html.tmpl
new file mode 100644
index 000000000..164dd482c
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-employee-incident.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/redirect.html.tmpl
+ url = "https://mozilla.service-now.com/com.glideapp.servicecatalog_cat_item_view.do?sysparm_id=4f9468ef184a30004a467ddd1a20df63"
+%]
diff --git a/extensions/BMO/template/en/default/bug/create/create-finance.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-finance.html.tmpl
new file mode 100644
index 000000000..fa8dc5f5b
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-finance.html.tmpl
@@ -0,0 +1,257 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_style = BLOCK %]
+ #bug_form input[type=text], #bug_form input[type=file], #cc_autocomplete, #bug_form textarea {
+ width: 100%;
+ }
+[% END %]
+
+[% inline_js = BLOCK %]
+ var compdesc = new Array();
+ [% FOREACH comp = product.components %]
+ compdesc['[% comp.name FILTER js %]'] = '[% comp.description FILTER js %]';
+ [% END %]
+ function showCompDesc(component) {
+ var value = component.value;
+ document.getElementById('comp_description').innerHTML = compdesc[value];
+ }
+
+ function onSubmit() {
+ var alert_text = '';
+ if (!isFilledOut('component'))
+ alert_text += "Please select a value for request type.\n";
+ if (!isFilledOut('short_desc'))
+ alert_text += "Please enter a value for the summary.\n";
+ if (!isFilledOut('team_priority'))
+ alert_text += "Please select a value for team priority.\n";
+ if (!isFilledOut('signature_time'))
+ alert_text += "Please enter a value for signture timeframe.\n";
+ if (!isFilledOut('other_party'))
+ alert_text += "Please enter a value for the name of other party.\n";
+ if (!isFilledOut('business_obj'))
+ alert_text += "Please enter a value for business objective.\n";
+ if (!isFilledOut('what_purchase'))
+ alert_text += "Please enter a value for what you are purchasing.\n";
+ if (!isFilledOut('why_purchase'))
+ alert_text += "Please enter a value for why the purchase is needed.\n";
+ if (!isFilledOut('risk_purchase'))
+ alert_text += "Please enter a value for the risk if not purchased.\n";
+ if (!isFilledOut('alternative_purchase'))
+ alert_text += "Please enter a value for the purchase alternative.\n";
+ if (!isFilledOut('total_cost'))
+ alert_text += "Please enter a value for total cost.\n";
+ if (!isFilledOut('attachment'))
+ alert_text += "Please enter an attachment.\n";
+
+ if (alert_text != '') {
+ alert(alert_text);
+ return false;
+ }
+
+ return true;
+ }
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Finance"
+ style = inline_style
+ style_urls = [ 'skins/standard/enter_bug.css' ]
+ javascript = inline_js
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/attachment.js', 'js/field.js', 'js/util.js' ]
+ onload = "showCompDesc(document.getElementById('component'));"
+%]
+
+<h2>Finance</h2>
+
+<p>All fields are mandatory</p>
+
+<form method="post" action="post_bug.cgi" id="bug_form" class="enter_bug_form"
+ enctype="multipart/form-data" onsubmit="return onSubmit();">
+<input type="hidden" name="format" value="finance">
+<input type="hidden" name="product" value="Finance">
+<input type="hidden" name="rep_platform" value="All">
+<input type="hidden" name="op_sys" value="Other">
+<input type="hidden" name="priority" value="--">
+<input type="hidden" name="version" value="unspecified">
+<input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+<input type="hidden" name="comment" id="comment" value="">
+<input type="hidden" name="groups" id="groups" value="finance">
+<input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table>
+
+<tr>
+ <th>
+ <label for="component">Request Type:</label>
+ </th>
+ <td>
+ <select name="component" id="component" onchange="showCompDesc(this);">
+ [%- FOREACH c = product.components %]
+ [% NEXT IF NOT c.is_active %]
+ <option value="[% c.name FILTER html %]"
+ id="v[% c.id FILTER html %]_component"
+ [% IF c.name == default.component_ %]
+ selected="selected"
+ [% END %]>
+ [% c.name FILTER html -%]
+ </option>
+ [%- END %]
+ </select
+ </td>
+</tr>
+
+<tr>
+ <td></td>
+ <td id="comp_description" align="left" style="color: green; padding-left: 1em"></td>
+</tr>
+
+<tr>
+ <th>
+ <label for="short_desc">Description:</label>
+ </th>
+ <td>
+ <i>Short description of what is being asked to sign</i><br>
+ <input name="short_desc" id="short_desc" size="60"
+ value="[% short_desc FILTER html %]">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="team_priority">Priority to your Team:</label>
+ </th>
+ <td>
+ <select id="team_priority" name="team_priority">
+ <option value="Low">Low</option>
+ <option value="Medium">Medium</option>
+ <option value="High">High</option>
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="signature_time">Timeframe for Signature:</label>
+ </th>
+ <td>
+ <select id="signature_time" name="signature_time">
+ <option value="24 hours">Within 24 hours</option>
+ <option value="2 days">2 days</option>
+ <option value="A week">A week</option>
+ <option value="2 - 4 weeks" selected>2 -4 weeks</option>
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="other_party">Name of Other Party:</label>
+ </th>
+ <td>
+ <i>Include full legal entity name and any other relevant contact information</i><br>
+ <textarea id="other_party" name="other_party"
+ rows="5" cols="40"></textarea>
+ </td>
+<tr>
+
+<tr>
+ <th>
+ <label for="business_obj">Business Objective:</label>
+ </th>
+ <td>
+ <i>
+ Which Initiative or Overall goal this purchase is for. i.e. B2G, Data Center, Network, etc.</i><br>
+ <textarea id="business_obj" name="business_obj" rows="5" cols="40"></textarea>
+ </td>
+<tr>
+
+<tr>
+ <th>
+ <label for="what_purchase">If this is a purchase order,<br>what are we purchasing?</label>
+ </th>
+ <td>
+ <i>
+ Describe your request, what items are we purchasing, including number of
+ units if available.<br>Also provide context and background. Enter No if not
+ a purchase order.</i><br>
+ <textarea name="what_purchase" id="what_purchase" rows="5" cols="40"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="why_purchase">Why is this purchase needed?</label>
+ </th>
+ <td>
+ <i>
+ Why do we need this? What is the work around if this is not approved?</i><br>
+ <textarea name="why_purchase" id="why_purchase" rows="5" cols="40"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="risk_purchase">What is the risk if<br>this is not purchased?</label>
+ </th>
+ <td>
+ <i>
+ What will happen if this is not purchased?</i><br>
+ <textarea name="risk_purchase" id="risk_purchase" rows="5" cols="40"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="alternative_purchase">What is the alternative?</label>
+ </th>
+ <td>
+ <i>
+ How did the team come to this recommendation? Did we get other bids, if so, how many?</i><br>
+ <textarea name="alternative_purchase" id="alternative_purchase" rows="5" cols="40"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="total_cost">Total Cost</label>
+ </th>
+ <td>
+ <input type="text" name="total_cost" id="total_cost" value="" size="60">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="attachment">Attachment:</label>
+ </th>
+ <td>
+ <i>Upload document that needs to be signed. If this is a Purchase Request form,<br>
+ also upload any supporting document such as draft SOW, quote, order form, etc.</i>
+ <div>
+ <input type="file" id="attachment" name="data" size="50">
+ <input type="hidden" name="contenttypemethod" value="autodetect">
+ <input type="hidden" name="description" value="Finance Document">
+ </div>
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td>
+ <input type="submit" id="commit" value="Submit Request">
+ </td>
+</tr>
+</table>
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-fxos-betaprogram.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-fxos-betaprogram.html.tmpl
new file mode 100644
index 000000000..3f8bbdd71
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-fxos-betaprogram.html.tmpl
@@ -0,0 +1,180 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% phones = [
+ 'ZTE Open',
+ 'Alcatel One Touch',
+ 'LG'
+] %]
+
+[% inline_css = BLOCK %]
+ #dogfood {
+ margin: 0 5em 0 2em;
+ }
+ #dogfood th {
+ text-align: left;
+ padding-top: 1em;
+ }
+[% END %]
+
+[% inline_js = BLOCK %]
+ function onSubmit() {
+ var alert_text = '';
+
+ var phone = false;
+ [% FOREACH phone = phones %]
+ if (document.getElementById('phone-cb-[% phone FILTER js %]').checked)
+ phone = true;
+ [% END %]
+ if (document.getElementById('phone-cb-other').checked && isFilledOut('phone-other'))
+ phone = true;
+ if (!phone)
+ alert_text += "Please select the type of phone you have.\n";
+
+ if (!isFilledOut('fxos-version'))
+ alert_text += "Please provide the version of Firefox OS you are running.\n";
+
+ if (!isFilledOut('short_desc'))
+ alert_text += "Please enter a value for the summary.\n";
+
+ if (!isFilledOut('details'))
+ alert_text += "Please describe your issue in more detail.\n";
+
+ if (alert_text != '') {
+ alert(alert_text);
+ return false;
+ }
+ return true;
+ }
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Firefox OS Beta Program $terms.Bug Submission"
+ style = inline_css
+ style_urls = [ 'skins/standard/enter_bug.css' ]
+ javascript = inline_js
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js', 'js/field.js', 'js/util.js' ]
+%]
+
+<h2>Firefox OS Beta Program [% terms.Bug %] Submission</h2>
+
+<div id="public_place">
+ As [% terms.Bugzilla %] is a public place, don't include any private or
+ personally identifying content.
+</div>
+
+<form method="post" action="post_bug.cgi" id="bug_form" class="enter_bug_form"
+ enctype="multipart/form-data" onsubmit="return onSubmit();">
+<input type="hidden" name="format" value="fxos-betaprogram">
+<input type="hidden" name="created-format" value="fxos-betaprogram">
+<input type="hidden" name="product" value="Firefox OS">
+<input type="hidden" name="component" value="BetaTriage">
+<input type="hidden" name="rep_platform" value="ARM">
+<input type="hidden" name="op_sys" value="Gonk (Firefox OS)">
+<input type="hidden" name="priority" value="--">
+<input type="hidden" name="version" value="unspecified">
+<input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+<input type="hidden" name="comment" id="comment" value="">
+<input type="hidden" name="status_whiteboard" id="status_whiteboard" value="[dogfood]">
+<input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table id="dogfood">
+
+<tr>
+ <th>
+ What phone do you have?
+ </th>
+</tr>
+<tr>
+ <td>
+ [% FOREACH phone = phones %]
+ <input type="radio" name="phone" id="phone-cb-[% phone FILTER html %]"
+ value="[% phone FILTER html %]">
+ <label for="phone-cb-[% phone FILTER html %]">[% phone FILTER html %]</label><br>
+ [% END %]
+ <input type="radio" name="phone" id="phone-cb-other" value="Other">
+ <input type="text" name="phone_other" id="phone-other" placeholder="Other" size="20"
+ onfocus="document.getElementById('phone-cb-other').checked = true">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ What version of Firefox OS are you running?
+ </th>
+</tr>
+<tr>
+ <td>
+ <i>
+ Please check settings &gt; device information &gt; more information > OS version<br>
+ </i>
+ <input type="text" name="fxos_version" id="fxos-version" size="20">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ Please summarize your issue in one sentence:
+ </th>
+</tr>
+<tr>
+ <td>
+ <input type="text" name="short_desc" id="short_desc" size="60">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ Please describe your issue in more detail. If you have steps to
+ reproduce the problem, please include them here:
+ </th>
+</tr>
+<tr>
+ <td>
+ <textarea id="details" name="details" rows="5" cols="60"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ If your issue is associated with a specific app, which one
+ </th>
+</tr>
+<tr>
+ <td>
+ <input type="text" name="app" id="app" size="60">
+ </td>
+</tr>
+
+<tr>
+ <th>Security:</th>
+</tr>
+<tr>
+ <td>
+ <input type="checkbox" name="groups" id="default_security_group"
+ value="[% product.default_security_group FILTER html %]">
+ <label for="default_security_group">
+ Many users could be harmed by this security problem:
+ it should be kept hidden from the public until it is resolved.
+ </label>
+ </td>
+</tr>
+
+<tr>
+ <td>
+ <input type="submit" id="commit" value="Submit Request">
+ </td>
+</tr>
+
+</table>
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-fxos-feature.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-fxos-feature.html.tmpl
new file mode 100644
index 000000000..faa0495a4
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-fxos-feature.html.tmpl
@@ -0,0 +1,181 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_style = BLOCK %]
+#feature_form {
+ padding: 10px;
+}
+#feature_form .required:after {
+ content: " *";
+ color: red;
+}
+#feature_form .field_label {
+ font-weight: bold;
+}
+#feature_form .field_desc {
+ padding-bottom: 3px;
+}
+#feature_form .field_desc,
+#feature_form .head_desc {
+ width: 600px;
+ word-wrap: normal;
+}
+#feature_form .head_desc {
+ padding-top: 5px;
+ padding-bottom: 12px;
+}
+#feature_form .form_section {
+ margin-bottom: 10px;
+}
+#feature_form textarea {
+ font-family: inherit;
+ font-size: inherit;
+}
+#feature_form #comp_description {
+ test-align: left;
+ color: green;
+ padding-left: 1em;
+}
+[% END %]
+
+[% inline_javascript = BLOCK %]
+var compdesc = new Array();
+compdesc[""] = 'Please select a component from the list above.';
+[% FOREACH comp = product.components %]
+ compdesc['[% comp.name FILTER js %]'] = '[% comp.description FILTER js %]';
+[% END %]
+function showCompDesc() {
+ var comp_select = document.getElementById('component');
+ document.getElementById('comp_description').innerHTML = compdesc[comp_select.value];
+}
+function validateAndSubmit() {
+ var alert_text = '';
+ if (!isFilledOut('component')) alert_text += 'Please select a value for product component.\n';
+ if (!isFilledOut('short_desc')) alert_text += 'Please enter a value for feature request title.\n';
+ if (alert_text != '') {
+ alert(alert_text);
+ return false;
+ }
+ return true;
+}
+YAHOO.util.Event.onDOMReady(showCompDesc);
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Firefox OS Feature Request Form"
+ style = inline_style
+ javascript = inline_javascript
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js' ]
+%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+<form id="feature_form" method="post" action="post_bug.cgi" enctype="multipart/form-data"
+ onSubmit="return validateAndSubmit();">
+ <input type="hidden" name="format" value="fxos-feature">
+ <input type="hidden" name="product" value="Firefox OS">
+ <input type="hidden" name="keywords" value="feature">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+<img title="Firefox OS Feature Form" src="extensions/BMO/web/producticons/firefox.png">
+
+<div class="form_section">
+ <label for="component" class="field_label required">Product Component</label>
+ <div class="field_desc">
+ Which product component is your feature request applicable to?
+ If you are not sure, choose "General".
+ </div>
+ <select name="component" id="component" onchange="showCompDesc(this);">
+ <option value="">Select One</option>
+ [%- FOREACH c = product.components %]
+ [% NEXT IF NOT c.is_active %]
+ <option value="[% c.name FILTER html %]"
+ id="v[% c.id FILTER html %]_component"
+ [% IF c.name == default.component_ %]
+ selected="selected"
+ [% END %]>
+ [% c.name FILTER html -%]
+ </option>
+ [%- END %]
+ </select>
+ <div id="comp_description"></div>
+</div>
+
+<div class="form_section">
+ <label for="short_desc" class="field_label required">Feature Request Title</label>
+ <div class="field_desc">
+ Please enter a title for your feature request that is brief and self explanatory.
+ (Example: "Memory dialing using keypad numbers")
+ </div>
+ <input type="text" name="short_desc" id="short_desc" size="80">
+</div>
+
+<div class="form_section">
+ <label for="description" class="field_label">Description of feature or problem to be solved</label>
+ <div class="field_desc">
+ Please describe the feature that you are requesting or the problem that you would like solved in detail
+ (Example, "Today, there is no way for the user to quickly dial user-defined numbers from the dial pad.
+ Instead the user must search for an find the contact in their contact list.").
+ If the described feature only applies to certain device types (eg. tablet vs. smartphone), please make note of it.
+ </div>
+ <textarea id="description" name="description" cols="80" rows="5"></textarea>
+</div>
+
+<div class="form_section">
+ <label for="implement_impact" class="field_label">Impact of implementing the feature/solution</label>
+ <div class="field_desc">
+ If this solution were to be implemented, what would the impact be?
+ (Example, "If this solution were to be implemented, it would save the users
+ significant time when dialing commonly used phone numbers.")
+ </div>
+ <textarea id="implement_impact" name="implement_impact" cols="80" rows="5"></textarea>
+</div>
+
+<div class="form_section">
+ <label for="not_implement_impact" class="field_label">Impact of NOT implementing the feature/solution</label>
+ <div class="field_desc">
+ If this solution were NOT to be implemented, what would the impact be?
+ (Example, "By not implementing this solution, we are unable to sell phones in
+ Iceland which has a certification requirement to have support for memory dialing.")
+ </div>
+ <textarea id="not_implement_impact" name="not_implement_impact" cols="80" rows="5"></textarea>
+</div>
+
+<div class="form_section">
+ <label for="date_required" class="field_label">Date required</label>
+ <div class="field_desc">
+ Is this solution required by a certain date? Why?
+ (Example: "March 2014. We plan to sell phones in Iceland in June 2014 using Firefox OS 1.4.
+ Completing the feature in March would allow the device to pass operator certification in time
+ for a June retail launch.")<br>
+ <strong>Note:</strong> completing this field does not imply that the feature will indeed be delivered in this timeframe.
+ </div>
+ <textarea id="date_required" name="date_required" cols="80" rows="5"></textarea>
+</div>
+
+<div class="head_desc">
+ Once your form has been submitted, a tracking [% terms.bug %] will be created. We will
+ then reach out for additional info and next steps. Thanks!
+</div>
+
+<input type="submit" id="commit" value="Submit">
+
+<p>
+ [ <span class="required_star">*</span> <span class="required_explanation">Required Field</span> ]
+</p>
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-fxos-mcts-waiver.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-fxos-mcts-waiver.html.tmpl
new file mode 100644
index 000000000..bfd624a8a
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-fxos-mcts-waiver.html.tmpl
@@ -0,0 +1,208 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_style = BLOCK %]
+#fxos_mcts_waiver_form {
+ padding: 10px;
+}
+#fxos_mcts_waiver_form .required:after {
+ content: " *";
+ color: red;
+}
+#fxos_mcts_waiver_form .field_label {
+ font-weight: bold;
+}
+#fxos_mcts_waiver_form .field_desc {
+ padding-bottom: 3px;
+}
+#fxos_mcts_waiver_form .field_desc,
+#fxos_mcts_waiver_form .head_desc {
+ width: 600px;
+ word-wrap: normal;
+}
+#fxos_mcts_waiver_form .head_desc {
+ padding-top: 5px;
+ padding-bottom: 12px;
+}
+#fxos_mcts_waiver_form .form_section {
+ margin-bottom: 10px;
+}
+#fxos_mcts_waiver_form textarea {
+ font-family: inherit;
+ font-size: inherit;
+}
+#fxos_mcts_waiver_form em {
+ font-size: 1em;
+}
+[% END %]
+
+[% inline_javascript = BLOCK %]
+function validateAndSubmit() {
+ 'use strict';
+ var alert_text = '';
+ var requiredLabels = YAHOO.util.Selector.query('label.required');
+ if (requiredLabels) {
+ requiredLabels.forEach(function (label) {
+ var name = label.getAttribute('for');
+ var ids = YAHOO.util.Selector.query(
+ '#fxos_mcts_waiver_form *[name="' + name + '"]'
+ ).map(function (e) {
+ return e.id
+ });
+
+ if (ids && ids[0]) {
+ if (!isFilledOut(ids[0])) {
+ var desc = label.textContent || name;
+ alert_text +=
+ "Please enter a value for " +
+ desc.replace(/[\r\n]+/, "").replace(/\s+/g, " ") +
+ "\n";
+ }
+ }
+ });
+ }
+
+ if (alert_text != '') {
+ alert(alert_text);
+ return false;
+ }
+
+ var short_desc = document.getElementById('short_desc');
+ var company_name = document.getElementById('company_name').value;
+ short_desc.value = "MCTS Waiver for " + company_name;
+
+ return true;
+}
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Firefox OS MCTS Waiver Form"
+ style = inline_style
+ javascript = inline_javascript
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/field.js', 'js/util.js' ]
+ yui = [ 'selector' ]
+%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+<form id="fxos_mcts_waiver_form" method="post" action="post_bug.cgi"
+ enctype="multipart/form-data" onSubmit="return validateAndSubmit();">
+ <input type="hidden" name="format" value="fxos-mcts-waiver">
+ <input type="hidden" name="product" value="Firefox OS">
+ <input type="hidden" name="component" value="MCTS Waiver Request">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+ <input type="hidden" name="groups" value="mozilla-employee-confidential">
+ <input type="hidden" id="short_desc" name="short_desc" value="">
+ <input type="hidden" name="cf_user_story" value="Engineering Analysis:
+
+
+Technical Account Manager Recommendation:
+
+
+">
+
+ <div class="head_desc">
+ Welcome to the [% title FILTER html %]!
+ </div>
+
+ <div class="form_section">
+ <label for="company_name" class="field_label required">Company Name</label>
+ <div class="field_desc">
+ Please enter the legal name of the company requesting the Waiver
+ </div>
+ <input type="text" name="company_name" id="company_name" size="80">
+ </div>
+
+ <div class="form_section">
+ <label for="device_desc" class="field_label required">Device Description</label>
+ <div class="field_desc">
+ Please enter the Make, Model, Chipset, screensize and type the device associated with the waiver request. For
+ example type may be mobile phone, tablet, dongle, tv, etc.
+ </div>
+ <textarea id="device_desc" name="device_desc" cols="80" rows="5"></textarea>
+ </div>
+
+ <div class="form_section">
+ <label for="ffos_release" class="field_label required">FFOS Release</label>
+ <div class="field_desc">
+ Please Enter the Release this Waiver applies to for this partner.
+ </div>
+ <input type="text" name="ffos_release" id="ffos_release" size="80">
+ </div>
+
+ <div class="form_section">
+ <label for="branding_tier" class="field_label required">Branding Tier</label>
+ <div class="field_desc">
+ Please Enter the Branding Tier associated with the Waiver Request (Powered by Firefox OS or Co-Branded).
+ </div>
+ <select name="branding_tier" id="branding_tier">
+ <option value="Firefox OS Inside">Firefox OS Inside</option>
+ <option value="Powered by Firefox OS">Powered by Firefox OS</option>
+ <option value="Firefox OS Co-branded">Firefox OS Co-branded</option>
+ </select>
+ </div>
+
+ <div class="form_section">
+ <label for="dist_countries" class="field_label required">Distribution Countries</label>
+ <div class="field_desc">
+ Please include list of countries where the device is planned to be distributed.
+ </div>
+ <textarea id="dist_countries" name="dist_countries" cols="80" rows="5"></textarea>
+ </div>
+
+ <div class="form_section">
+ <label for="dist_channel" class="field_label required">Distribution Channel</label>
+ <div class="field_desc">
+ Please identify how this device will be sold. For example, Operator, Retail.
+ </div>
+ <input type="text" name="dist_channel" id="dist_channel" size="80">
+ </div>
+
+ <div class="form_section">
+ <label for="reason" class="field_label required">Reason for Waiver Request</label>
+ <div class="field_desc">
+ Please describe which test cases, Branding Guidelines and/or Requirements the Partner is request waived.
+ </div>
+ <textarea id="reason" name="reason" cols="80" rows="5"></textarea>
+ </div>
+
+ <div class="form_section">
+ <label for="rationale" class="field_label required">Rationale for Granting Waiver Request</label>
+ <div class="field_desc">
+ Please document why the Partner thinks a waiver should be granted.
+ </div>
+ <textarea id="rationale" name="rationale" cols="80" rows="5"></textarea>
+ </div>
+
+ <div class="form_section">
+ <label for="impact" class="field_label required">Impact Analysis</label>
+ <div class="field_desc">
+ Please provide an assessment of the impact of granting this waiver in general business terms (this should include
+ broad perspective of potential issues such as brand consistency, impacts on reporting &amp; tracking capabilities,
+ help desk/support issues, etc.)
+ </div>
+ <textarea id="impact" name="impact" cols="80" rows="5"></textarea>
+ </div>
+
+ <input type="submit" id="commit" value="Submit">
+
+ <p>
+ [ <span class="required_star">*</span> <span class="required_explanation">
+ Required Field</span> ]
+ </p>
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-fxos-partner.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-fxos-partner.html.tmpl
new file mode 100644
index 000000000..3e910990d
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-fxos-partner.html.tmpl
@@ -0,0 +1,239 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_js = BLOCK %]
+ var compdesc = new Array();
+ compdesc[""] = 'Please select a component from the list above.';
+ [% FOREACH comp = product.components %]
+ compdesc['[% comp.name FILTER js %]'] = '[% comp.description FILTER js %]';
+ [% END %]
+ function showCompDesc(component) {
+ var value = component.value;
+ document.getElementById('comp_description').innerHTML = compdesc[value];
+ }
+ function onSubmit() {
+ var alert_text = '';
+ var status_whiteboard = '';
+
+ if (!isFilledOut('component'))
+ alert_text += "Please select a value for component.\n";
+ if (!isFilledOut('short_desc'))
+ alert_text += "Please enter a value for the summary.\n";
+ if (!isFilledOut('steps_to_reproduce'))
+ alert_text += "Please enter the steps to reproduce.\n";
+ if (!isFilledOut('actual_behavior'))
+ alert_text += "Please enter the actual behavior.\n";
+ if (!isFilledOut('expected_behavior'))
+ alert_text += "Please enter the expected behavior.\n";
+ if (!isFilledOut('build'))
+ alert_text += "Please enter a value for the build.\n";
+ if (!isFilledOut('requirements'))
+ alert_text += "Please enter a value for the requirements.\n";
+
+ var device_values = new Array();
+ var device_select = document.getElementById("b2g_device");
+ for (var i = 0, l = device_select.options.length; i < l; i++) {
+ if (device_select.options[i].selected)
+ device_values.push(device_select.options[i].value);
+ }
+
+ if (device_values.length == 0)
+ alert_text += "Please select one or more devices.\n";
+
+ if (alert_text != '') {
+ alert(alert_text);
+ return false;
+ }
+
+ for (var i = 0, l = device_values.length; i < l; i++)
+ status_whiteboard += '[device:' + device_values[i] + '] ';
+
+ if (document.getElementById('third_party_app').checked)
+ status_whiteboard += '[apps watch list]';
+
+ document.getElementById('status_whiteboard').value = status_whiteboard;
+
+ return true;
+ }
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Firefox OS Partner $terms.Bug Submission"
+ style_urls = [ 'skins/standard/enter_bug.css' ]
+ javascript = inline_js
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/attachment.js', 'js/field.js', 'js/util.js' ]
+ onload = "showCompDesc(document.getElementById('component'));"
+%]
+
+<h2>Firefox OS Partner [% terms.Bug %] Submission</h2>
+
+<p>All fields are mandatory</p>
+
+<form method="post" action="post_bug.cgi" id="bug_form" class="enter_bug_form"
+ enctype="multipart/form-data" onsubmit="return onSubmit();">
+<input type="hidden" name="format" value="fxos-partner">
+<input type="hidden" name="product" value="Firefox OS">
+<input type="hidden" name="rep_platform" value="ARM">
+<input type="hidden" name="op_sys" value="Gonk (Firefox OS)">
+<input type="hidden" name="priority" value="--">
+<input type="hidden" name="version" value="unspecified">
+<input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+<input type="hidden" name="comment" id="comment" value="">
+<input type="hidden" name="keywords" id="keywords" value="unagi">
+<input type="hidden" name="status_whiteboard" id="status_whiteboard" value="">
+<input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table>
+
+<tr>
+ <th>
+ <label for="short_desc">Summary:</label>
+ </th>
+ <td>
+ <input name="short_desc" id="short_desc" size="60"
+ value="[% short_desc FILTER html %]">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="component">Component:</label>
+ </th>
+ <td>
+ <select name="component" id="component" onchange="showCompDesc(this);">
+ <option value="">Select One</option>
+ [%- FOREACH c = product.components %]
+ [% NEXT IF NOT c.is_active %]
+ <option value="[% c.name FILTER html %]"
+ id="v[% c.id FILTER html %]_component"
+ [% IF c.name == default.component_ %]
+ selected="selected"
+ [% END %]>
+ [% c.name FILTER html -%]
+ </option>
+ [%- END %]
+ </select
+ </td>
+</tr>
+
+<tr>
+ <td></td>
+ <td id="comp_description" align="left" style="color: green; padding-left: 1em"></td>
+</tr>
+
+<tr>
+ <th>
+ <label for="b2g_device">B2G Device:</label>
+ </th>
+ <td>
+ <select name="b2g_device" id="b2g_device"
+ size="5" multiple="multiple">
+ <option name="Otoro">Otoro</option>
+ <option name="Unagi">Unagi</option>
+ <option name="Inari">Inari</option>
+ <option name="Ikura">Ikura</option>
+ <option name="Hamachi">Hamachi</option>
+ <option name="Buri">Buri</option>
+ <option name="Toro">Toro</option>
+ <option name="Leo">Leo</option>
+ <option name="Twist">Twist</option>
+ <option name="Zero">Zero</option>
+ <option name="Tara">Tara</option>
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="other_party">What are the steps to reproduce?:</label>
+ </th>
+ <td>
+ <textarea id="steps_to_reproduce" name="steps_to_reproduce" rows="5" cols="60">1.
+2.
+3.</textarea>
+ </td>
+<tr>
+
+<tr>
+ <th>
+ <label for="actual_behavior">What was the actual behavior?:</label>
+ </th>
+ <td>
+ <textarea id="actual_behavior" name="actual_behavior" rows="5" cols="60"></textarea>
+ </td>
+<tr>
+
+<tr>
+ <th>
+ <label for="expected_behavior">What was the expected behavior?:</label>
+ </th>
+ <td>
+ <textarea name="expected_behavior" id="expected_behavior" rows="5" cols="60"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="build">What build were you using?:</label>
+ </th>
+ <td>
+ <input type="text" name="build" id="build" value="" size="60">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="requirements">What are the requirements?:</label>
+ </th>
+ <td>
+ <input type="text" name="requirements" id="requirements" value="" size="60">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="requirements">Third party app content?:</label>
+ </th>
+ <td>
+ <input type="checkbox" name="third_party_app" id="third_party_app">
+ </td>
+</tr>
+
+<tr>
+ <th>Security:</th>
+ <td>
+ <input type="checkbox" name="groups" id="default_security_group"
+ value="[% product.default_security_group FILTER html %]"
+ [% FOREACH g = group %]
+ [% IF g.name == name %]
+ [% ' checked="checked"' IF g.checked %]
+ [% LAST %]
+ [% END %]
+ [% END %]
+ >
+ <label for="default_security_group">
+ Many users could be harmed by this security problem:
+ it should be kept hidden from the public until it is resolved.
+ </label>
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td>
+ <input type="submit" id="commit" value="Submit Request">
+ </td>
+</tr>
+</table>
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-fxos-preload-app.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-fxos-preload-app.html.tmpl
new file mode 100644
index 000000000..edf7fdbae
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-fxos-preload-app.html.tmpl
@@ -0,0 +1,185 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_style = BLOCK %]
+#fxos_preload_app_form {
+ padding: 10px;
+}
+#fxos_preload_app_form .required:after {
+ content: " *";
+ color: red;
+}
+#fxos_preload_app_form .field_label {
+ font-weight: bold;
+}
+#fxos_preload_app_form .field_desc {
+ padding-bottom: 3px;
+}
+#fxos_preload_app_form .field_desc,
+#fxos_preload_app_form .head_desc {
+ width: 600px;
+ word-wrap: normal;
+}
+#fxos_preload_app_form .head_desc {
+ padding-top: 5px;
+ padding-bottom: 12px;
+}
+#fxos_preload_app_form .form_section {
+ margin-bottom: 10px;
+}
+#fxos_preload_app_form textarea {
+ font-family: inherit;
+ font-size: inherit;
+}
+#fxos_preload_app_form em {
+ font-size: 1em;
+}
+.yui-calcontainer {
+ z-index: 2;
+}
+[% END %]
+
+[% inline_javascript = BLOCK %]
+function validateAndSubmit() {
+ 'use strict';
+ var alert_text = '';
+ var requiredLabels = YAHOO.util.Selector.query('label.required');
+ if (requiredLabels) {
+ requiredLabels.forEach(function (label) {
+ var name = label.getAttribute('for');
+ var ids = YAHOO.util.Selector.query(
+ '#fxos_preload_app_form *[name="' + name + '"]'
+ ).map(function (e) {
+ return e.id
+ });
+
+ if (ids && ids[0]) {
+ if (!isFilledOut(ids[0])) {
+ var desc = label.textContent || name;
+ alert_text +=
+ "Please enter a value for " +
+ desc.replace(/[\r\n]+/, "").replace(/\s+/g, " ") +
+ "\n";
+ }
+ }
+ });
+ }
+
+ if (alert_text != '') {
+ alert(alert_text);
+ return false;
+ }
+ return true;
+}
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Firefox OS Pre-load App"
+ style = inline_style
+ javascript = inline_javascript
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/field.js', 'js/util.js' ]
+ yui = [ "autocomplete", "calendar", "selector" ]
+%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+<form id="fxos_preload_app_form" method="post" action="post_bug.cgi"
+ enctype="multipart/form-data" onSubmit="return validateAndSubmit();">
+ <input type="hidden" name="format" value="fxos-preload-app">
+ <input type="hidden" name="product" value="Marketplace">
+ <input type="hidden" name="component" value="Pre-Installed Apps">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="version" value="1.0">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+ <input type="hidden" name="short_desc" value="Information Request: Pre-Installed Apps">
+ <input type="hidden" name="groups" value="mozilla-employee-confidential">
+
+ <div class="head_desc">
+ Welcome to the Firefox OS Pre-load App Info Request Form!
+ </div>
+
+ <div class="form_section">
+ <label for="company_name" class="field_label required">Company Name</label>
+ <div class="field_desc">
+ Please enter the legal name of your company
+ </div>
+ <input type="text" name="company_name" id="company_name" size="80">
+ </div>
+
+
+ <div class="form_section">
+ <label for="apps_business_dev_contact_name" class="field_label required">Apps Business Development Contact Name</label>
+ <div class="field_desc">Please enter your Name</div>
+ <input type="text" name="apps_business_dev_contact_name" id="apps_business_dev_contact_name" size="80">
+ </div>
+
+ <div class="form_section">
+ <label for="apps_business_dev_contact_email" class="field_label required">Apps Business Development Contact Email</label>
+ <div class="field_desc">Please enter your Email address.</div>
+ <input type="text" name="apps_business_dev_contact_email" id="apps_business_dev_contact_email"
+ value="[% user.email FILTER html %]" size="80">
+ </div>
+
+ <div class="form_section">
+ <label for="preload_apps" class="field_label required">Name of Firefox Marketplace apps of interest to you:</label>
+ <div class="field_desc">
+ Please provide the App Name and Marketplace URL for each app you wish to pre-load on your certified, branded
+ Firefox OS device. The Marketplace URL is an important identifier because there are many apps in Marketplace with
+ the same name.
+ </div>
+ <textarea id="preload_apps" name="preload_apps"
+ cols="80" rows="5"></textarea>
+ </div>
+
+ <div class="form_section">
+ <label for="countries" class="field_label required">Countries where your device will be distributed</label>
+ <div class="field_desc">
+ Please list the countries where your device will be distributed. This information is required because it
+ corresponds to the countries that the developers will evaluate for distribution rights.
+ </div>
+ <textarea id="countries" name="countries"
+ cols="80" rows="5"></textarea>
+ </div>
+
+ <div class="form_section">
+ <label for="release_info" class="field_label required">Release Information</label>
+ <div class="field_desc">
+ Please provide the Version of Firefox OS for your Branded, Certified device on which you plan to pre-load the
+ requested apps.
+ </div>
+ <input type="text" name="release_info" id="release_info" size="80">
+ </div>
+
+ <div class="form_section">
+ <label for="device_info" class="field_label required">Device Information</label>
+ <div class="field_desc">
+ Please include the device make and model, screen size, Chipset and RAM configuration for the Branded, Certified
+ device on which you plan to pre-load the requested apps.
+ </div>
+ <textarea id="device_info" name="device_info"
+ cols="80" rows="5"></textarea>
+ </div>
+
+ <p>When you press submit the information you've provided will be routed to Mozilla team members for follow up. The
+ system will also respond with a [% terms.Bugzilla %] tracking number that you may use for follow up.</p>
+
+ <input type="submit" id="commit" value="Submit">
+
+ <p>
+ [ <span class="required_star">*</span> <span class="required_explanation">
+ Required Field</span> ]
+ </p>
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-ipp.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-ipp.html.tmpl
new file mode 100644
index 000000000..fb59cfeb3
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-ipp.html.tmpl
@@ -0,0 +1,183 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_style = BLOCK %]
+#ipp_form th {
+ text-align: right;
+}
+
+#ipp_form input[type="text"], #ipp_form textarea {
+ width: 100%;
+}
+
+#ipp_form textarea {
+ font-family: inherit;
+ font-size: inherit;
+}
+
+#standard_link {
+ margin-top: 2em;
+}
+
+#standard_link img {
+ vertical-align: middle;
+}
+
+#standard_link a {
+ cursor: pointer;
+}
+
+[% END %]
+
+[% inline_javascript = BLOCK %]
+function validateAndSubmit() {
+ var alert_text = '';
+ if (!isFilledOut('component')) alert_text += 'Please select the "Area".\n';
+ if (!isFilledOut('short_desc')) alert_text += 'Please enter a "Summary".\n';
+ if (!isFilledOut('region')) alert_text += 'Please enter the "Region/Country".\n';
+ if (!isFilledOut('desc')) alert_text += 'Please provide a "Description".\n';
+ if (!isFilledOut('relevance')) alert_text += 'Please provide some "Relevance".\n';
+ if (!isFilledOut('goal')) alert_text += 'Please enter the "Goal".\n';
+ if (!isFilledOut('when')) alert_text += 'Please enter data for the "When" field.\n';
+ if (alert_text != '') {
+ alert(alert_text);
+ return false;
+ }
+ return true;
+}
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Internet Public Policy Issue"
+ style = inline_style
+ javascript = inline_javascript
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/field.js', 'js/util.js', 'js/bug.js' ]
+%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+<h1>Internet Public Policy Issue</h1>
+
+<form method="post" action="post_bug.cgi" enctype="multipart/form-data"
+ onSubmit="return validateAndSubmit();">
+ <input type="hidden" name="format" value="ipp">
+ <input type="hidden" name="product" value="Internet Public Policy">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table id="ipp_form">
+
+<tr>
+ <th class="required">Area</th>
+ <td>
+ <select name="component" id="component">
+ <option value="">Please select..</option>
+ [% FOREACH component = product.components %]
+ <option value="[% component.name FILTER html %]">
+ [% component.name FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <th class="required">Summary</th>
+ <td>
+ <input type="text" name="short_desc" id="short_desc" size="60"
+ placeholder="(Describe issue in one sentence)">
+ </td>
+</tr>
+
+<tr>
+ <th class="required">Region/Country</th>
+ <td>
+ <input type="text" name="region" id="region" size="60">
+ </td>
+</tr>
+
+<tr>
+ <th class="required">Description</th>
+ <td>
+ <textarea id="desc" name="desc" cols="50" rows="5"
+ placeholder="(Explain the legislative or policy activity which is happening)"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <th class="required">Relevance</th>
+ <td>
+ <textarea id="relevance" name="relevance" cols="50" rows="5"
+ placeholder="(Why should Mozilla care? What’s the impact on the open internet?)"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <th class="required">Goal</th>
+ <td>
+ <input type="text" name="goal" id="goal" size="60"
+ placeholder="(What would success look like for Mozilla?)">
+ </td>
+</tr>
+
+<tr>
+ <th class="required">When</th>
+ <td>
+ <input type="text" name="when" id="when" size="60"
+ placeholder="(Describe the timeline or due date)">
+ </td>
+</tr>
+
+<tr>
+ <th class="required">Urgency</th>
+ <td>
+ <select name="priority" id="priority">
+ <option value="P1">Urgent</option>
+ <option value="P3">Needs Attention Soon</option>
+ <option value="P5" selected>When You Get To It</option>
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <th>Additional Information</th>
+ <td>
+ <textarea id="additional" name="additional" cols="50" rows="5"
+ placeholder="(Please supply links to relevant articles/websites/organizations)"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td><input type="submit" id="commit" value="Submit Issue"></td>
+</tr>
+
+</table>
+</form>
+
+[ <span class="required_star">*</span> <span class="required_explanation">Required Field</span> ]
+
+<div id="standard_link">
+ <a href="enter_bug.cgi?format=__standard__&product=[% product.name FILTER uri %]">
+ <img src="extensions/BMO/web/images/advanced.png" width="16" height="16" border="0"></a>
+ <a href="enter_bug.cgi?format=__standard__&product=[% product.name FILTER uri %]">
+ Switch to the standard [% terms.bug %] entry form</a>
+</div>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-itrequest.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-itrequest.html.tmpl
new file mode 100644
index 000000000..57c11a08a
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-itrequest.html.tmpl
@@ -0,0 +1,238 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_javascript = BLOCK %]
+ function setsevdesc(theSelect) {
+ var theValue = theSelect.options[theSelect.selectedIndex].value;
+ if (theValue == 'blocker') {
+ document.getElementById('blockerdesc').style.display = 'block';
+ document.getElementById('critdesc').style.display = 'none';
+ } else if (theValue == 'critical') {
+ document.getElementById('blockerdesc').style.display = 'none';
+ document.getElementById('critdesc').style.display = 'block';
+ } else {
+ document.getElementById('blockerdesc').style.display = 'none';
+ document.getElementById('critdesc').style.display = 'none';
+ }
+ }
+
+ var compdesc = new Array();
+ [% FOREACH comp IN product.components %]
+ compdesc['[% comp.name FILTER js %]'] = '[% comp.description FILTER js %]';
+ [% END %]
+ compdesc['Server Operations'] = 'System administration for the mozilla.org servers. ' +
+ 'Requests for Server Ops that don\'t fit in any of the ' +
+ 'other Server Ops components can go here.';
+
+ var serviceNowText = 'Use <a href="https://mozilla.service-now.com/">Service Now</a> to:<br>' +
+ 'Request an LDAP/E-mail/etc. account<br>' +
+ 'Desktop/Laptop/Printer/Phone/Tablet/License problem/order/request';
+
+ function setcompdesc(theRadio) {
+ if (theRadio.id == 'component_service_desk') {
+ [%# helpdesk issue/request %]
+ document.getElementById('main_form').style.display = 'none';
+ document.getElementById('service_now_form').style.display = '';
+ document.getElementById('compdescription').innerHTML = serviceNowText;
+ } else {
+ document.getElementById('main_form').style.display = '';
+ document.getElementById('service_now_form').style.display = 'none';
+ var theValue = theRadio.value;
+ var compDescText = compdesc[theValue];
+ // If 'Server Operations', product must be changed to 'mozilla.org'
+ // otherwise set to 'Infrastructure & Operations'
+ if (theRadio.id == 'component_server_ops') {
+ compDescText = compDescText + '<br><br>' + serviceNowText;
+ document.getElementById('product').value = 'mozilla.org';
+ }
+ else {
+ document.getElementById('product').value = 'Infrastructure & Operations';
+ }
+ document.getElementById('compdescription').innerHTML = compDescText;
+ }
+ }
+
+ function on_submit() {
+ if (document.getElementById('componentsd').checked) {
+ [%# redirect desktop issues to service-now #%]
+ document.location.href = 'https://mozilla.service-now.com/';
+ return false;
+ }
+ return true;
+ }
+
+ YAHOO.util.Event.onDOMReady(function() {
+ var comps = document.getElementsByName('component');
+ for (var i = 0, l = comps.length; i < l; i++) {
+ if (comps[i].checked) {
+ setcompdesc(comps[i]);
+ break;
+ }
+ }
+ });
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Corporation/Foundation IT Requests"
+ javascript = inline_javascript
+ javascript_urls = [ 'js/field.js' ]
+ yui = [ 'autocomplete' ]
+%]
+
+[% USE Bugzilla %]
+
+<p><strong>Please use this form for IT requests only!</strong></p>
+<p>If you have a [% terms.bug %] to file, go <a href="enter_bug.cgi">here</a>.</p>
+
+<form method="post" action="post_bug.cgi" id="itRequestForm" enctype="multipart/form-data"
+ onsubmit="return on_submit()">
+ <input type="hidden" id="product" name="product" value="Infrastructure & Operations">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="priority" value="--">
+ <input type="hidden" name="version" value="other">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+ <table>
+ <tr>
+
+ <td align="right">
+ <strong>Urgency:</strong>
+ </td>
+
+ <td>
+ <select id="bug_severity" name="bug_severity" onchange="setsevdesc(this)">
+ <option value="blocker">All work for IT stops until this is done</option>
+ <option value="critical">IT should work on it soon as possible (urgent)</option>
+ <option value="major">IT should get to it within 24 hours</option>
+ <option value="normal">IT should get to it within the next week</option>
+ <option value="minor" selected="selected">No rush, but hopefully IT can get to it soon</option>
+ <option value="trivial">Whenever IT can get around to it</option>
+ <option value="enhancement">This is just an idea, filing it so we don't forget</option>
+ </select>
+ </td>
+ <td>
+ <div id="blockerdesc" style="color:red;display:none">This will page the on-call sysadmin if not handled within 30 minutes.</div>
+ <div id="critdesc" style="color:red;display:none">This will page the on-call sysadmin if not handled within 8 hours.</div>
+ </td>
+
+ </tr>
+ <tr>
+ <td align="right"><strong>Request Type:</strong></td>
+ <td style="white-space: nowrap;">
+ <input type="radio" name="component" id="component_service_desk" onclick="setcompdesc(this)" value="Desktop Issues">
+ <label for="component_service_desk">Service Desk issue/request</label><br>
+ <input type="radio" name="component" id="component_relops" onclick="setcompdesc(this)" value="RelOps">
+ <label for="component_relops">Report a problem with a tinderbox machine</label><br>
+ <input type="radio" name="component" id="component_webops_other" onclick="setcompdesc(this)" value="WebOps: Other">
+ <label for="component_webops_other">Report a problem with a Mozilla website, or to request a change or push</label><br>
+ <input type="radio" name="component" id="component_netops_acl" onclick="setcompdesc(this)" value="NetOps: DC Other">
+ <label for="component_netops_acl">Request a firewall change</label><br>
+ <input type="radio" name="component" id="component_server_ops" onclick="setcompdesc(this)" value="Server Operations">
+ <label for="component_server_ops">Any other issue</label><br>
+ Mailing list requests should be filed <a href="[% ulrbase FILTER none %]enter_bug.cgi?product=mozilla.org&amp;format=mozlist">here</a> instead.
+ </td>
+ <td id="compdescription" align="left" style="color: green; padding-left: 1em">
+ </td>
+ </tr>
+
+ <tbody id="main_form">
+
+ <tr>
+ <td align="right"><strong>Summary:</strong></td>
+ <td colspan="3">
+ <input name="short_desc" size="60" value="[% short_desc FILTER html %]">
+ </td>
+ </tr>
+
+ <tr>
+ <td align="right"><strong>CC&nbsp;(optional):</strong></td>
+ <td colspan="3">
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => cc
+ size => 60
+ multiple => 5
+ %]
+ </td>
+ </tr>
+
+ <tr><td align="right" valign="top"><strong>Description:</strong></td>
+ <td colspan="3">
+ <textarea name="comment" rows="10" cols="80">
+ [% comment FILTER html %]</textarea>
+ <br>
+ </td>
+ </tr>
+
+ <tr>
+ <td align="right"><strong>URL&nbsp;(optional):</strong></td>
+ <td colspan="3">
+ <input name="bug_file_loc" size="60"
+ value="[% bug_file_loc FILTER html %]">
+ </td>
+ </tr>
+
+ <tr><td colspan="4">&nbsp;</td></tr>
+
+ <tr>
+ <td colspan="4">
+ <strong>Attachment&nbsp;(optional):</strong>
+ </td>
+ </tr>
+
+ <tr>
+ <td align="right">File:</td>
+ <td colspan="3">
+ <em>Enter the path to the file on your computer.</em><br>
+ <input type="file" id="data" name="data" size="50">
+ <input type="hidden" name="contenttypemethod" value="autodetect" />
+ </td>
+ </tr>
+
+ <tr>
+ <td align="right">Description:</td>
+ <td colspan="3">
+ <em>Describe the attachment briefly.</em><br>
+ <input type="text" id="description" name="description" size="60" maxlength="200">
+ </td>
+ </tr>
+
+ <tr>
+ <td>&nbsp;</td>
+ <td>
+ <br>
+ <!-- infra -->
+ <input type="checkbox" name="groups" id="groups" value="infra" checked="checked">
+ <label for="groups"><strong>This is an internal issue which should not be publicly visible.</strong></label><br>
+ (please uncheck this box if it isn't)<br>
+ <br>
+ <input type="submit" id="commit" value="Submit Request"><br>
+ <br>
+ Thanks for contacting us. You will be notified by email of any progress made in resolving your request.
+ </td>
+ </tr>
+
+ </tbody>
+
+ <tbody id="service_now_form" style="display:none">
+ <tr>
+ <td>&nbsp;</td>
+ <td>
+ <br>
+ <input type="submit" value="Go to Service Now">
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</form>
+
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-legal.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-legal.html.tmpl
new file mode 100644
index 000000000..5abe79597
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-legal.html.tmpl
@@ -0,0 +1,226 @@
+[%# 1.0@bugzilla.org %]
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Mozilla Corporation.
+ # Portions created by Mozilla are Copyright (C) 2008 Mozilla
+ # Corporation. All Rights Reserved.
+ #
+ # Contributor(s): Mark Smith <mark@mozilla.com>
+ # Reed Loden <reed@mozilla.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Corporation Legal Requests"
+ style_urls = [ 'skins/standard/attachment.css' ]
+ javascript_urls = [ 'js/attachment.js', 'js/field.js' ]
+ yui = [ 'autocomplete' ]
+%]
+
+[% IF user.in_group("mozilla-employee-confidential")
+ OR user.in_group("mozilla-messaging-confidential")
+ OR user.in_group("mozilla-foundation-confidential") %]
+
+<div style='text-align: center; width: 98%; font-size: 2em; font-weight: bold; margin: 10px;'>MoLegal</div>
+
+<p><strong>Welcome to MoLegal.</strong> For legal help please fill in the form below completely.</p>
+
+<p>Legal [% terms.bugs %] are only visible to the reporter, members of the legal team, and those on the
+CC list. This is necessary to maintain attorney-client privilege. Please do not add non-
+employees to the cc list.</p>
+
+<p><strong>All Submissions, And Information Provided In Response To This Request,
+Are Confidential And Subject To The Attorney-Client Privilege And Work Product Doctrine.</strong></p>
+
+<p>If you are requesting legal review of a new product or service, a new feature of an existing product
+ or service, or any type of contract, please go
+ <a href="[% urlbase FILTER none %]enter_bug.cgi?product=mozilla.org&format=moz-project-review">here</a>
+ to kick-off review of your project. If you are requesting another type of legal action, e.g patent analysis,
+ trademark misuse investigation, HR issue, or standards work, please use this form.</p>
+
+<form method="post" action="post_bug.cgi" id="legalRequestForm" enctype="multipart/form-data">
+ <input type="hidden" name="product" value="Legal">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="priority" value="--">
+ <input type="hidden" name="bug_severity" value="normal">
+ <input type="hidden" name="format" value="legal">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+ [% IF user.in_group('canconfirm') %]
+ <input type="hidden" name="bug_status" value="NEW">
+ [% END %]
+
+<table>
+
+<tr>
+ <td align="right" width="170px"><strong>Request Type:</strong></td>
+ <td>
+ <select name="component">
+ [%- FOREACH c = product.components %]
+ [% NEXT IF NOT c.is_active %]
+ <option value="[% c.name FILTER html %]"
+ [% " selected=\"selected\"" IF c.name == "General" %]>
+ [% c.name FILTER html -%]
+ </option>
+ [%- END %]
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <td align="right" valign="top">
+ <strong>Goal:</strong>
+ </td>
+ <td colspan="3">
+ <em>Identify the company goal this request maps to.</em><br>
+ <input name="goal" id="goal" size="60" value="[% goal FILTER html %]">
+ </td>
+</tr>
+
+<tr>
+ <td align="right">
+ <strong>Priority to your Team:</strong>
+ </td>
+ <td>
+ <select id="teampriority" name="teampriority">
+ <option value="High">High</option>
+ <option value="Medium">Medium</option>
+ <option value="Low" selected="selected">Low</option>
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <td align="right">
+ <strong>Timeframe for Completion:</strong>
+ </td>
+ <td>
+ <select id="timeframe" name="timeframe">
+ <option value="2 days">2 days</option>
+ <option value="a week">a week</option>
+ <option value="2-4 weeks">2-4 weeks</option>
+ <option value="this will take a while, but please get started soon">
+ this will take a while, but please get started soon</option>
+ <option value="no rush" selected="selected">no rush</option>
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <td align="right" valign="top">
+ <strong>Summary:</strong>
+ </td>
+ <td colspan="3">
+ <em>Include the name of the vendor, partner, product, or other identifier.</em><br>
+ <input name="short_desc" size="60" value="[% short_desc FILTER html %]">
+ </td>
+</tr>
+
+<tr>
+ <td align="right">
+ <strong>CC&nbsp;(optional):</strong>
+ </td>
+ <td colspan="3">
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => cc
+ size => 60
+ multiple => 5
+ %]
+ </td>
+</tr>
+
+<tr>
+ <td align="right" valign="top">
+ <strong>Name of Other Party:</strong>
+ </td>
+ <td>
+ <em>If applicable, include full legal entity name, address, and any other relevant contact information.</em><br>
+ <textarea id="otherparty" name="otherparty" rows="3" cols="80"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <td align="right">
+ <strong>Business Objective:</strong>
+ </td>
+ <td>
+ <input type="text" name="busobj" id="busobj" value="" size="60" />
+ </td>
+</tr>
+
+<tr>
+ <td align="right" valign="top">
+ <strong>Description:</strong>
+ </td>
+ <td colspan="3">
+ <em>Describe your question, what you want and/or provide any relevant deal terms, restrictions,<br>
+ or provisions that are applicable. Also provide context and background.</em><br>
+ <textarea id="comment" name="comment" rows="10" cols="80">
+ [% comment FILTER html %]</textarea>
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><strong>URL&nbsp;(optional):</strong></td>
+ <td colspan="3">
+ <input name="bug_file_loc" size="60"
+ value="[% bug_file_loc FILTER html %]">
+ </td>
+</tr>
+
+<tr>
+ <td></td>
+ <td colspan=2><strong>Attachment (this is optional)</strong></td>
+</tr>
+
+<tr>
+ <td align="right" valign="top">
+ <strong><label for="data">File:</label></strong>
+ </td>
+ <td>
+ <em>Enter the path to the file on your computer.</em><br>
+ <input type="file" id="data" name="data" size="50">
+ <input type="hidden" name="contenttypemethod" value="autodetect" />
+ </td>
+</tr>
+
+<tr>
+ <td align="right" valign="top">
+ <strong><label for="description">Description:</label></strong>
+ </td>
+ <td>
+ <em>Describe the attachment briefly.</em><br>
+ <input type="text" id="description" name="description" size="60" maxlength="200">
+ </td>
+</tr>
+
+</table>
+
+<br>
+
+ <input type="submit" id="commit" value="Submit Request">
+</form>
+
+<p>Thanks for contacting us. You will be notified by email of any progress made in resolving your request.</p>
+
+[% ELSE %]
+
+<p>Sorry, you do not have access to this page.</p>
+
+[% END %]
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-mdn.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-mdn.html.tmpl
new file mode 100644
index 000000000..f79363c99
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-mdn.html.tmpl
@@ -0,0 +1,279 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_style = BLOCK %]
+strong.required:before {
+ content: "* ";
+ color: red;
+}
+#yui-history-iframe {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 1px;
+ height: 1px;
+ visibility: hidden;
+}
+#standard {
+ margin-top: 2em;
+}
+#standard img {
+ vertical-align: middle;
+}
+#standard a {
+ cursor: pointer;
+}
+[% END %]
+[% inline_javascript = BLOCK %]
+ var Dom = YAHOO.util.Dom;
+ var Event = YAHOO.util.Event;
+ var History = YAHOO.util.History;
+ var mdn = {
+ _initial_state: 'initial',
+ _current_state: 'initial',
+ _current_type: 'Bug',
+ _required_fields: {
+ 'Bug': {
+ 'bug_actions': 'Please enter some text for "What did you do?"',
+ 'bug_actual_results': 'Please enter some text for "What happened?"',
+ 'bug_expected_results': 'Please enter some text for "What should have happened?"',
+ },
+ 'Feature': {
+ 'feature_problem_solving': 'Please enter some text for "What problems would this solve?"',
+ 'feature_audience': 'Please enter some text for "Who would use this?"',
+ 'feature_interface': 'Please enter some text for "What would users see?"',
+ 'feature_process': 'Please enter some text for "What would users do? What would happen as a result?"',
+ },
+ 'Change': {
+ 'change_feature': 'Please enter some text for "What feature should be changed? Please provide the URL of the feature if possible"',
+ 'change_problem_solving': 'Please enter some text for "What problems would this solve?"',
+ 'change_audience': 'Please enter some text for "Who would use this?"',
+ 'change_interface': 'Please enter some text for "What would users see?"',
+ 'change_process': 'Please enter some text for "What would users do? What would happen as a result?"',
+ }
+ },
+ setState: function(state, request_type, no_set_history) {
+ if (state == 'detail') {
+ request_type = request_type || this._getRadioValueByClass('request_type');
+ request_type = request_type.toLowerCase();
+ if (request_type == 'bug') {
+ Dom.get('detail_header').innerHTML = '<h2>[% terms.Bug %] Report</h2>';
+ Dom.get('secure_type').innerHTML = 'report';
+ }
+ if (request_type == 'feature') {
+ Dom.get('detail_header').innerHTML = '<h2>Feature Request</h2>';
+ Dom.get('secure_type').innerHTML = 'request';
+ }
+ if (request_type == 'change') {
+ Dom.get('detail_header').innerHTML = '<h2>Change Request</h2>';
+ Dom.get('secure_type').innerHTML = 'request';
+ }
+ Dom.addClass('detail_' + this._current_type, 'bz_default_hidden');
+ Dom.removeClass('detail_' + request_type, 'bz_default_hidden');
+ this._current_type = request_type;
+ }
+ Dom.addClass(this._current_state + '_form', 'bz_default_hidden');
+ Dom.removeClass(state + '_form', 'bz_default_hidden');
+ this._current_state = state;
+ if (History && !no_set_history) {
+ History.navigate('h', state +
+ (request_type ? '|' + request_type : ''));
+ }
+ return true;
+ },
+ validateAndSubmit: function() {
+ var request_type = this._getRadioValueByClass('request_type');
+ var alert_text = '';
+ if (!isFilledOut('short_desc')) alert_text += 'Please enter a "Summary".\n';
+ for (require_type in this._required_fields) {
+ if (require_type == request_type) {
+ for (field in this._required_fields[require_type]) {
+ if (!isFilledOut(field))
+ alert_text += this._required_fields[require_type][field] + "\n";
+ }
+ }
+ }
+ if (alert_text != '') {
+ alert(alert_text);
+ return false;
+ }
+ var whiteboard = Dom.get('status_whiteboard');
+ whiteboard.value = "[specification][type:" + request_type.toLowerCase() + "]";
+ return true;
+ },
+ _getRadioValueByClass: function(class_name) {
+ var elements = Dom.getElementsByClassName(class_name);
+ for (var i = 0, l = elements.length; i < l; i++) {
+ if (elements[i].checked) return elements[i].value;
+ }
+ },
+ init: function() {
+ var bookmarked_state = History.getBookmarkedState('h');
+ this._initial_state = bookmarked_state || 'initial';
+ try {
+ History.register('h', this._initial_state, mdn.onStateChange);
+ History.initialize('yui-history-field', 'yui-history-iframe');
+ History.onReady(function () {
+ mdn.onStateChange(History.getCurrentState('h'), true);
+ });
+ }
+ catch(e) {
+ console.log('error initializing history: ' + e);
+ History = false;
+ }
+ },
+ onStateChange: function(state, no_set_history) {
+ var state_data = state.split('|');
+ mdn.setState(state_data[0], state_data[1], no_set_history);
+ }
+ };
+ Event.on('show_detail', 'click', function() { mdn.setState('detail'); });
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Developer Network Feedback"
+ style = inline_style
+ javascript = inline_javascript
+ yui = [ 'history' ]
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js' ]
+%]
+
+<iframe id="yui-history-iframe" src="extensions/BMO/web/yui-history-iframe.txt"></iframe>
+<input id="yui-history-field" type="hidden">
+
+<h1>Mozilla Developer Network Feedback</h1>
+
+<form method="post" action="post_bug.cgi" enctype="multipart/form-data"
+ onSubmit="return mdn.validateAndSubmit();">
+ <input type="hidden" name="format" value="mdn">
+ <input type="hidden" name="product" value="Mozilla Developer Network">
+ <input type="hidden" name="component" value="General">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+ <input type="hidden" name="status_whiteboard" id="status_whiteboard" value="">
+
+ <div id="initial_form">
+ <p>
+ <input type="radio" name="request_type" class="request_type"
+ id="request_type_bug" value="Bug" checked="checked">
+ <label for="request_type_bug">Report a [% terms.bug %]</label><br>
+ <input type="radio" name="request_type" class="request_type"
+ id="request_type_feature" value="Feature">
+ <label for="request_type_feature">Request a new feature</label><br>
+ <input type="radio" name="request_type" class="request_type"
+ id="request_type_change" value="Change">
+ <label for="request_type_change">Request a change to an existing feature</label><br>
+ <br>
+ <input id="show_detail" type="button" value="Next">
+ </p>
+ </div>
+
+ <div id="detail_form" class="bz_default_hidden">
+ <p id="detail_header"></p>
+
+ <p id="detail_summary">
+ <strong class="required">Summary</strong><br>
+ <input type="text" name="short_desc" id="short_desc" size="60">
+ </p>
+
+ <div id="detail_bug" class="bz_default_hidden">
+ <p>
+ <strong class="required">What did you do?</strong><br>
+ <textarea name="bug_actions" id="bug_actions" rows="5" cols="60">
+1.&nbsp;
+2.&nbsp;
+3.&nbsp;</textarea>
+ </p>
+ <p>
+ <strong class="required">What happened?</strong><br>
+ <textarea name="bug_actual_results" id="bug_actual_results" rows="5" cols="60"></textarea>
+ </p>
+ <p>
+ <strong class="required">What should have happened?</strong><br>
+ <textarea name="bug_expected_results" id="bug_expected_results" rows="5" cols="60"></textarea>
+ </p>
+ </div>
+
+ <div id="detail_feature" class="bz_default_hidden">
+ <p>
+ <strong class="required">What problems would this solve?</strong><br>
+ <textarea name="feature_problem_solving" id="feature_problem_solving" rows="5" cols="60"></textarea>
+ </p>
+ <p>
+ <strong class="required">Who would use this?</strong><br>
+ <textarea name="feature_audience" id="feature_audience" rows="5" cols="60"></textarea>
+ </p>
+ <p>
+ <strong class="required">What would users see?</strong><br>
+ <textarea name="feature_interface" id="feature_interface" rows="5" cols="60"></textarea>
+ </p>
+ <p>
+ <strong class="required">What would users do? What would happen as a result?</strong><br>
+ <textarea name="feature_process" id="feature_process" rows="5" cols="60"></textarea>
+ </p>
+ </div>
+
+ <div id="detail_change" class="bz_default_hidden">
+ <p>
+ <strong class="required">What feature should be changed? Please provide the URL of the feature if possible.</strong><br>
+ <textarea name="change_feature" id="change_feature" rows="5" cols="60"></textarea>
+ </p>
+ <p>
+ <strong class="required">What problems would this solve?</strong><br>
+ <textarea name="change_problem_solving" id="change_problem_solving" rows="5" cols="60"></textarea>
+ </p>
+ <p>
+ <strong class="required">Who would use this?</strong><br>
+ <textarea name="change_audience" id="change_audience" rows="5" cols="60"></textarea>
+ </p>
+ <p>
+ <strong class="required">What would users see?</strong><br>
+ <textarea name="change_interface" id="change_interface" rows="5" cols="60"></textarea>
+ </p>
+ <p>
+ <strong class="required">What would users do? What would happen as a result?</strong><br>
+ <textarea name="change_process" id="change_process" rows="5" cols="60"></textarea>
+ </p>
+ </div>
+
+ <p id="detail_description">
+ <strong>Is there anything else we should know?</strong><br>
+ <textarea name="description" id="description" rows="5" cols="60"></textarea>
+ </p>
+
+ <p id="detail_secure">
+ <input type="checkbox" name="groups" id="groups"
+ value="[% product.default_security_group FILTER html %]">
+ <label for="groups">
+ <strong>This <span id="secure_type">report</span> is about a problem
+ that is putting users at risk. It should be kept hidden from the public
+ until it is resolved.</strong>
+ </label>
+ </p>
+
+ <input type="submit" id="commit" value="Submit"></td>
+ </div>
+</form>
+
+<div id="standard">
+ <a href="enter_bug.cgi?format=__standard__&product=[% product.name FILTER uri %]">
+ <img src="extensions/BMO/web/images/advanced.png" width="16" height="16" border="0"></a>
+ <a href="enter_bug.cgi?format=__standard__&product=[% product.name FILTER uri %]">
+ Switch to the standard [% terms.bug %] entry form</a>
+</div>
+
+<script>
+ mdn.init();
+</script>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-mobile-compat.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-mobile-compat.html.tmpl
new file mode 100644
index 000000000..a9f0fd2cc
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-mobile-compat.html.tmpl
@@ -0,0 +1,201 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_style = BLOCK %]
+#bug_form th {
+ text-align: right;
+ vertical-align: middle;
+}
+
+#bug_form input[type="text"], #bug_form textarea {
+ width: 100%;
+}
+
+#bug_form textarea {
+ font-family: inherit;
+ font-size: inherit;
+}
+
+#standard_link {
+ margin-top: 2em;
+}
+
+#standard_link img {
+ vertical-align: middle;
+}
+
+#standard_link a {
+ cursor: pointer;
+}
+
+[% END %]
+
+[% inline_javascript = BLOCK %]
+function validateAndSubmit() {
+ var field_errors = {
+ 'op_sys': "Please tell us which product you are using.",
+ 'software_version': "Please tell us which version of the product you are using.",
+ 'bug_file_loc': "Please give the URL of the broken page.",
+ 'short_desc': "Please enter a summary of the problem.",
+ 'desc': "Please tell us how to reproduce the problem.",
+ 'expected_result': "Please tell us what you expected to happen.",
+ 'actual_result': "Please tell us what actually happened.",
+ };
+
+ var alert_text = '';
+
+ for (key in field_errors) {
+ if (!isFilledOut(key)) {
+ alert_text += field_errors[key] + '\n';
+ }
+ }
+
+ if (alert_text != '') {
+ alert(alert_text);
+ return false;
+ }
+
+ return true;
+}
+[% END %]
+
+[% title = "Mobile Web Compatibility Problem" %]
+
+[% PROCESS global/header.html.tmpl
+ title = title
+ style = inline_style
+ javascript = inline_javascript
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js']
+%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+<h1>[% title FILTER none %]</h1>
+
+<form method="post" action="post_bug.cgi" enctype="multipart/form-data"
+ onSubmit="return validateAndSubmit();">
+ <input type="hidden" name="format" value="mobile-compat">
+ <input type="hidden" name="product" value="Tech Evangelism">
+ <input type="hidden" name="component" value="Mobile">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_status" value="UNCONFIRMED">
+ <input type="hidden" name="rep_platform" value="Other">
+ <input type="hidden" name="bug_severity" value="normal">
+ <input type="hidden" name="status_whiteboard" value="[mobile-compat-form]">
+ <input type="hidden" name="user_agent" value="[% cgi.user_agent() FILTER html %]">
+
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+[% IF NOT cgi.user_agent("Mobile") %]
+<p>If possible, it's best to file [% terms.bugs %] using your device's browser. Visit and bookmark &lt;<a href="https://bugzilla.mozilla.org/form.mobile.compat">https://bugzilla.mozilla.org/form.mobile.compat</a>&gt;.</p>
+[% END %]
+
+<table id="bug_form">
+
+<tr>
+ <th class="required">Product</th>
+ <td>
+ <select name="op_sys" id="op_sys">
+ <option value="">Please select...</option>
+ <option value="Gonk (Firefox OS)">Firefox OS</option>
+ <option value="Android">Firefox for Android</option>
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <th class="required">Product Version</th>
+ <td>
+ <input type="text" name="software_version" id="software_version" size="60"
+ placeholder="Software version - see About box or Preferences">
+ </td>
+</tr>
+
+<tr>
+ <th class="required">Full Web Page Address</th>
+ <td>
+ <input type="text" name="bug_file_loc" id="bug_file_loc" size="60"
+ placeholder="e.g. http://www.example.com/page.html">
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+</tr>
+
+<tr>
+ <th class="required">Problem Summary</th>
+ <td>
+ <input type="text" name="short_desc" id="short_desc" size="60"
+ placeholder="Describe the specific problem with the page in one sentence">
+ </td>
+</tr>
+
+<tr>
+ <th class="required">Steps To Reproduce</th>
+ <td>
+ <textarea id="desc" name="desc" cols="50" rows="5">1.
+2.
+3.
+...</textarea>
+ </td>
+</tr>
+
+<tr>
+ <th class="required">Expected Result</th>
+ <td>
+ <input type="text" id="expected_result" name="expected_result" size="60"
+ placeholder="What were you expecting to happen?">
+ </td>
+</tr>
+
+<tr>
+ <th class="required">Actual Result</th>
+ <td>
+ <input type="text" name="actual_result" id="actual_result" size="60"
+ placeholder="What happened instead?">
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+</tr>
+
+<tr>
+ <th>Device Information</th>
+ <td>
+ <input type="text" name="device" id="device" size="60"
+ placeholder="Make and model">
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td><input type="submit" id="commit" value="Submit Issue"></td>
+</tr>
+
+</table>
+</form>
+
+[ <span class="required_star">*</span> <span class="required_explanation">Required Field</span> ]
+
+<div id="standard_link">
+ <a href="enter_bug.cgi?format=__standard__&amp;product=[% product.name FILTER uri %]">
+ <img src="extensions/BMO/web/images/advanced.png" width="16" height="16" border="0"></a>
+ <a href="enter_bug.cgi?format=__standard__&amp;product=[% product.name FILTER uri %]">
+ Switch to the standard [% terms.bug %] entry form</a>
+</div>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-mozlist.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-mozlist.html.tmpl
new file mode 100644
index 000000000..38c08c72f
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-mozlist.html.tmpl
@@ -0,0 +1,177 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Discussion Forum"
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/field.js' ]
+ yui = [ 'autocomplete' ]
+ style = ".mandatory{color:red;font-size:80%;}"
+%]
+
+<script type="text/javascript">
+<!--
+ function trySubmit() {
+ var alert_text = "";
+
+ if (!isFilledOut('listName')) {
+ alert_text += "Please enter the list name\n";
+ }
+
+ if (!isValidEmail(document.getElementById('listAdmin').value)) {
+ alert_text += "Please enter a valid email address for the list administrator\n";
+ }
+
+ if (alert_text) {
+ alert(alert_text);
+ return false;
+ }
+
+ var listName = document.getElementById('listName').value;
+ document.getElementById('short_desc').value = "Discussion Forum: " + listName;
+
+ return true;
+ }
+// -->
+</script>
+
+<p>
+ <b>Create a Mozilla Discussion Forum</b><br>
+ This option gives you a Mozilla <a
+ href="https://www.mozilla.org/about/forums/">Discussion Forum</a>.
+ These are the normal mechanism for public discussion in the Mozilla
+ project. They are made up of a mailing list on
+ <b>lists.mozilla.org</b>, a newsgroup on <b>news.mozilla.org</b> and
+ a <b>Google Group</b> (which maintains the list archives), all linked
+ together. Users can add and remove themselves.
+</p>
+
+<div id="message">
+ <b>Note:</b>
+ You must use <a href="https://mozilla.service-now.com/"><b>Service Now</b></a>
+ to request a distribution list or a standard mailing list.
+</div>
+<br>
+
+<form method="post" action="post_bug.cgi" id="mozListRequestForm"
+ enctype="multipart/form-data" onSubmit="return trySubmit();">
+ <input type="hidden" id="format" name="format" value="mozlist">
+ <input type="hidden" id="product" name="product" value="mozilla.org">
+ <input type="hidden" id="rep_platform" name="rep_platform" value="All">
+ <input type="hidden" id="op_sys" name="op_sys" value="Other">
+ <input type="hidden" id="priority" name="priority" value="--">
+ <input type="hidden" id="version" name="version" value="other">
+ <input type="hidden" id="short_desc" name="short_desc" value="">
+ <input type="hidden" id="component" name="component" value="Discussion Forums">
+ <input type="hidden" id="bug_severity" name="bug_severity" value="normal">
+ <input type="hidden" id="token" name="token" value="[% token FILTER html %]">
+
+ <table>
+ <tr>
+ <th class="field_label">
+ <span class="mandatory" title="Required">*</span> List Name:
+ </th>
+ <td>
+ The desired name for the newsgroup. Should start with 'mozilla.' and fit somewhere
+ in the hierarchy described <a href="https://www.mozilla.org/about/forums/">here</a>.<br>
+ <input name="listName" id="listName" size="60" value="[% listName FILTER html %]">
+ </td>
+ </tr>
+ <tr>
+ <th class="field_label">
+ <span class="mandatory" title="Required">*</span> List Administrator:
+ </th>
+ <td>
+ <b>Note:</b>The list administrator is also initially considered to be the list moderator
+ and will be responsible for moderation tasks unless delegated to someone else. For
+ convenience, [% terms.Bugzilla %] user accounts will autocomplete but it does not have
+ to be a [% terms.Bugzilla %] account.<br>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "listAdmin"
+ name => "listAdmin"
+ value => ""
+ size => 60
+ multiple => 5
+ %]
+ </td>
+ </tr>
+ <tr>
+ <td class="field_label">Short Description:</th>
+ <td>
+ This will be shown to users on the index of lists on the server.<br>
+ <input name="listShortDesc" id="listShortDesc" size="60" value="[% listShortDesc FILTER html %]">
+ </td>
+ </tr>
+ <tr>
+ <td class="field_label">Long Description:</th>
+ <td>
+ This will be shown at the top of the list's listinfo page.<br>
+ [% INCLUDE global/textarea.html.tmpl
+ name = 'listLongDesc'
+ id = 'listLongDesc'
+ minrows = 10
+ maxrows = 25
+ cols = constants.COMMENT_COLS
+ defaultcontent = listLongDesc
+ %]
+ </td>
+ </tr>
+ <tr>
+ <th class="field_label">Additional Comments:</th>
+ <td>
+ Justification for the list, special instructions, etc.<br>
+ [% INCLUDE global/textarea.html.tmpl
+ name = 'comment'
+ id = 'comment'
+ minrows = 10
+ maxrows = 25
+ cols = constants.COMMENT_COLS
+ defaultcontent = comment
+ %]
+ </td>
+ </tr>
+ <tr>
+ <th class="field_label">CC:</th>
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => cc
+ size => 60
+ multiple => 5
+ %]
+ </td>
+ </tr>
+ <tr>
+ <th class="field_label">URL:</th>
+ <td colspan="3">
+ <input name="bug_file_loc" size="60"
+ value="[% bug_file_loc FILTER html %]">
+ </td>
+ </tr>
+ <tr>
+ <td align="right">
+ <input type="checkbox" name="groups" id="group_35" value="infra">
+ </td>
+ <td>
+ <label for="group_35"><b>This is an internal issue which should not be publicly visible.</b></label>
+ </td>
+ </tr>
+ </table>
+
+ <input type="submit" id="commit" value="Submit Request">
+
+ <p>
+ Thanks for contacting us. You will be notified by email of any progress made
+ in resolving your request.
+ </p>
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-mozpr.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-mozpr.html.tmpl
new file mode 100644
index 000000000..f231ea3b9
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-mozpr.html.tmpl
@@ -0,0 +1,683 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_style = BLOCK %]
+#pr_form {
+ padding: 10px;
+ width: 600px;
+}
+
+#pr_form input[type="text"], #pr_form textarea {
+ width: 100%;
+ margin-bottom: 2px;
+}
+
+#pr_form .calendar {
+ width: 100px;
+}
+
+#pr_form .user {
+ width: 300px;
+}
+
+#pr_form select {
+ width: 200px;
+}
+
+#pr_form .required:after {
+ content: " *";
+ color: red;
+}
+
+#pr_form .missing {
+ box-shadow: 0px 0px 5px red;
+}
+
+#pr_form label {
+ font-weight: bold;
+ display: block;
+}
+
+#pr_form label.normal {
+ font-weight: normal;
+ display: inline;
+}
+
+#pr_form .calendar_button {
+ margin-top: 0.5em;
+}
+
+#pr_form .desc {
+ padding-bottom: 3px;
+}
+
+#pr_form .field {
+ margin-bottom: 10px;
+}
+
+#pr_form .indent {
+ margin-left: 30px;
+}
+
+#pr_form textarea {
+ font-family: inherit;
+ font-size: inherit;
+}
+
+#pr_form .head {
+ font-weight: bold;
+ border-top: 1px solid silver;
+ border-bottom: 1px solid silver;
+ padding: 5px;
+ margin: 1em 0;
+ background: #ddd;
+}
+
+#pr_form fieldset {
+ border: none;
+}
+
+#pr_form .extra {
+ font-style: italic;
+}
+
+#pr_form .extra a {
+ text-decoration: underline;
+}
+
+#pr_form #commit {
+ margin-top: 20px;
+}
+
+#pr_form .linked {
+ display: block;
+ margin-top: 2px;
+ width: 300px;
+}
+
+[% END %]
+
+[% inline_javascript = BLOCK %]
+var pr_inited = false;
+
+function init_listener(id, event, fn) {
+ YAHOO.util.Event.addListener(id, event, fn);
+ bz_fireEvent(document.getElementById(id), event);
+}
+
+function toggle_linked(id, value, suffix) {
+ var el = document.getElementById(id);
+ var show = el.type == 'checkbox' ? el.checked : el.value == value;
+ if (show) {
+ var linked = document.getElementById(id + suffix);
+ YAHOO.util.Dom.addClass(linked, 'linked');
+ YAHOO.util.Dom.removeClass(linked, 'bz_default_hidden');
+ if (pr_inited && linked.nodeName == "INPUT") {
+ linked.focus();
+ linked.select();
+ }
+ }
+ else {
+ YAHOO.util.Dom.addClass(id + suffix, 'bz_default_hidden');
+ }
+}
+
+function init_other(id) {
+ init_listener(id, 'change', function(o) {
+ toggle_linked(id, 'Other:', '_other');
+ });
+}
+
+YAHOO.util.Event.onDOMReady(function() {
+ init_listener('metrica', 'change', function(o) {
+ toggle_linked('metrica', 'Yes', '_extra');
+ });
+ init_listener('budget', 'change', function(o) {
+ toggle_linked('budget', 'Extra', '_extra');
+ });
+ init_listener('proj_mat_online', 'click', function(o) {
+ toggle_linked('proj_mat_online', 0, '_extra');
+ });
+ init_listener('proj_mat_file', 'click', function(o) {
+ toggle_linked('proj_mat_file', 0, '_extra');
+ });
+ init_listener('pr_mat_online', 'click', function(o) {
+ toggle_linked('pr_mat_online', 0, '_extra');
+ });
+ init_listener('pr_mat_file', 'click', function(o) {
+ toggle_linked('pr_mat_file', 0, '_extra');
+ });
+
+ init_other('pr_owner');
+ init_other('group_focus');
+ init_other('project_type');
+ init_other('region');
+ init_other('press_center');
+ init_other('internal_resources');
+ init_other('external_resources');
+ init_other('localization');
+ init_other('audience');
+
+ pr_inited = true;
+});
+
+function validate_other(id, value, suffix) {
+ var el = document.getElementById(id);
+ if (!value) value = 'Other:';
+ if (!suffix) suffix = '_other';
+ if (!el) {
+ console.error('Failed to find element: ' + elem_id);
+ return false;
+ }
+ if (el.type == 'checkbox') {
+ if (!el.checked) return true;
+ }
+ else if (el.value != value) {
+ return true;
+ }
+ return isFilledOut(id + suffix);
+}
+
+function validate_form() {
+ var Dom = YAHOO.util.Dom;
+
+ var old_missing = Dom.getElementsByClassName('missing');
+ for (var i = 0, il = old_missing.length; i < il; i++) {
+ Dom.removeClass(old_missing[i], 'missing');
+ }
+
+ var missing = [];
+ if (!isFilledOut('short_desc')) missing.push(['short_desc', 'Project Title']);
+ if (!isFilledOut('desc')) missing.push(['desc', 'Project Description and Scope']);
+
+ if (!isFilledOut('start_date')) missing.push(['start_date', 'Start Date']);
+ if (!isFilledOut('announce_date')) missing.push(['announce_date', 'Announcement Date']);
+ if (!isFilledOut('deadline')) missing.push(['deadline', 'Internal Deadline']);
+
+ if (!isFilledOut('pr_owner')) missing.push(['pr_owner', 'Project PR Owner']);
+ if (!isFilledOut('owner')) missing.push(['owner', 'Project Owner']);
+
+ if (!isFilledOut('rasci_a')) missing.push(['rasci_a', 'RASCI Approver']);
+
+ if (!isFilledOut('tier')) missing.push(['tier', 'Tier']);
+ if (!isFilledOut('project_type')) missing.push(['project_type', 'Project Type']);
+ if (!isFilledOut('pr_approach')) missing.push(['pr_approach', 'PR Approach']);
+ if (!isFilledOut('group_focus')) missing.push(['group_focus', 'Product Group Focus']);
+ if (!validate_other('group_focus')) missing.push(['group_focus', 'Product Group Focus - Other']);
+ if (!isFilledOut('region')) missing.push(['region', 'Region']);
+ if (!validate_other('region')) missing.push(['region', 'Region - Other']);
+
+ if (!isFilledOut('project_goals')) missing.push(['project_goals', 'Project Goals']);
+ if (!isFilledOut('pr_goals')) missing.push(['pr_goals', 'PR Goals']);
+ if (!isFilledOut('company_goal')) missing.push(['company_goal', 'Company Goal']);
+ if (!isOneChecked(document.forms.pr_form.audience))
+ missing.push(['audience_group', 'Audiences']);
+ if (!validate_other('audience')) missing.push(['audience', 'Audience - Other']);
+ if (!isFilledOut('key_messages')) missing.push(['key_messages', 'Key Messages']);
+
+ if (Dom.get('proj_mat_online').checked) {
+ if (!isFilledOut('proj_mat_online_desc')) missing.push(['proj_mat_online_desc', 'Project Materials - Online Description']);
+ if (!isFilledOut('proj_mat_online_link')) missing.push(['proj_mat_online_link', 'Project Materials - Online Link']);
+ }
+ if (Dom.get('proj_mat_file').checked) {
+ if (!isFilledOut('proj_mat_file_desc')) missing.push(['proj_mat_file_desc', 'Project Materials - Upload Description']);
+ if (!isFilledOut('proj_mat_file_attach')) missing.push(['proj_mat_file_attach', 'Project Materials - Upload File']);
+ }
+ if (Dom.get('pr_mat_online').checked) {
+ if (!isFilledOut('pr_mat_online_desc')) missing.push(['pr_mat_online_desc', 'PR Project Materials - Online Description']);
+ if (!isFilledOut('pr_mat_online_link')) missing.push(['pr_mat_online_link', 'PR Project Materials - Online Link']);
+ }
+ if (Dom.get('pr_mat_file').checked) {
+ if (!isFilledOut('pr_mat_file_desc')) missing.push(['pr_mat_file_desc', 'PR Project Materials - Upload Description']);
+ if (!isFilledOut('pr_mat_file_attach')) missing.push(['pr_mat_file_attach', 'PR Project Materials - Upload File']);
+ }
+
+ if (!validate_other('press_center')) missing.push(['press_center', 'Press Center Update - Other']);
+ if (!validate_other('internal_resources')) missing.push(['internal_resources', 'Internal Resources Needed - Other']);
+ if (!validate_other('external_resources')) missing.push(['external_resources', 'External Resources Needed - Other']);
+ if (!validate_other('localization')) missing.push(['localization', 'Localization Needed - Other']);
+
+ if (!isFilledOut('budget')) missing.push(['budget', 'Budget']);
+ if (!validate_other('budget', 'Extra', '_extra')) missing.push(['budget', 'Budget - Extra']);
+
+ if (missing.length) {
+ var missing_text = [];
+ for (var i = 0, il = missing.length; i < il; i++) {
+ Dom.addClass(missing[i][0], 'missing');
+ missing_text.push(missing[i][1]);
+ }
+ if (missing_text.length == 1) {
+ alert("The field '" + missing_text[0] + "' is required.");
+ }
+ else {
+ alert("The following fields are required:\n- " + missing_text.join("\n- "));
+ }
+ return false;
+ }
+
+ return true;
+}
+
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "PR Project Form"
+ style = inline_style
+ javascript = inline_javascript
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/field.js', 'js/util.js' ]
+ yui = [ "autocomplete", "calendar" ]
+%]
+
+[% UNLESS user.in_group('pr-private') %]
+ <div id="error_msg" class="throw_error">
+ This form is only available to members of the Mozilla PR team.
+ </div>
+ [% PROCESS global/footer.html.tmpl %]
+ [% RETURN %]
+[% END %]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+<form id="pr_form" name="pr_form" method="post" action="post_bug.cgi"
+ enctype="multipart/form-data" onSubmit="return validate_form()">
+<input type="hidden" name="format" value="mozpr">
+<input type="hidden" name="product" value="Mozilla PR">
+<input type="hidden" name="component" value="Projects">
+<input type="hidden" name="rep_platform" value="All">
+<input type="hidden" name="op_sys" value="Other">
+<input type="hidden" name="version" value="unspecified">
+<input type="hidden" name="bug_severity" value="normal">
+<input type="hidden" name="group" value="pr-private">
+<input type="hidden" name="assigned_to" id="assigned_to" value="nobody@mozilla.org">
+<input type="hidden" name="token" value="[% token FILTER html %]">
+
+<div class="head">
+ PR Project Form
+</div>
+
+<div class="field">
+ <label for="short_desc" class="required">Project Title</label>
+ <input type="text" name="short_desc" id="short_desc" placeholder="Your project's title">
+</div>
+
+<div class="field">
+ <label for="desc" class="required">Project Description and Scope</label>
+ <textarea name="desc" id="desc" placeholder="A short description of your PR project"></textarea>
+</div>
+
+<div class="head">
+ Timings
+</div>
+
+<div class="field">
+ <label for="start_date" class="required">Start Date</label>
+ <input name="start_date" id="start_date" value="" class="calendar"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button" id="button_calendar_start_date"
+ onclick="showCalendar('start_date')">
+ <span>Calendar</span>
+ </button>
+ <div id="con_calendar_start_date"></div>
+ <script type="text/javascript">
+ createCalendar('start_date')
+ </script>
+</div>
+
+<div class="field">
+ <label for="announce_date" class="required">Announcement Date</label>
+ <input name="announce_date" id="announce_date" value="" class="calendar"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button" id="button_calendar_announce_date"
+ onclick="showCalendar('announce_date')">
+ <span>Calendar</span>
+ </button>
+ <div id="con_calendar_announce_date"></div>
+ <script type="text/javascript">
+ createCalendar('announce_date')
+ </script>
+</div>
+
+<div class="field">
+ <label for="deadline" class="required">Internal Deadline</label>
+ <input name="deadline" id="deadline" value="" class="calendar"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button" id="button_calendar_deadline"
+ onclick="showCalendar('deadline')">
+ <span>Calendar</span>
+ </button>
+ <div id="con_calendar_deadline"></div>
+ <script type="text/javascript">
+ createCalendar('deadline')
+ </script>
+</div>
+
+<div class="head">
+ Owners
+</div>
+
+<div class="field">
+ <label for="pr_owner" class="required">Project PR Owner</label>
+ <select name="pr_owner" id="pr_owner">
+ <option value=""></option>
+ <option value="ahubert@mozilla.com">Aurelien Hubert</option>
+ <option value="bhueppe@mozilla.com">Barbara Hüppe</option>
+ <option value="ej@mozilla.com">Erica Jostedt</option>
+ <option value="jokelly@mozilla.com">Justin O'Kelly</option>
+ <option value="kshaw@mozilla.com">Karolina Shaw</option>
+ <option value="kshaw@mozilla.com">Mike Manning</option>
+ <option value="lnapoli@mozilla.com">Laura Napoli</option>
+ <option value="pjarratt@mozilla.com">Paul Jarratt</option>
+ <option value="tnitot@mozilla.com">Tristan Nitot</option>
+ <option value="vponell@mozilla.com">Valerie Ponell</option>
+ <option value="Other:">Other:</option>
+ </select>
+ <input name="pr_owner_other" id="pr_owner_other" class="bz_default_hidden">
+</div>
+
+<div class="field">
+ <label for="owner" class="required">Project Owner</label>
+ <input name="owner" id="owner" class="user">
+</div>
+
+<div class="head">
+ RASCI
+</div>
+
+<div class="field">
+ <label for="rasci_r">Responsible</label>
+ <input name="rasci_r" id="rasci_r" class="user">
+</div>
+
+<div class="field">
+ <label for="rasci_a" class="required">Approver</label>
+ <input name="rasci_a" id="rasci_a" class="user">
+</div>
+
+<div class="field">
+ <label for="rasci_s">Supporter</label>
+ <input name="rasci_s" id="rasci_s" class="user">
+</div>
+
+<div class="field">
+ <label for="rasci_c">Consultant</label>
+ <input name="rasci_c" id="rasci_c" class="user">
+</div>
+
+<div class="field">
+ <label for="rasci_i">Informed</label>
+ <input name="rasci_i" id="rasci_i" class="user">
+</div>
+
+<div class="head">
+ Details
+</div>
+
+<div class="field">
+ <label for="tier" class="required">Tier</label>
+ <select name="tier" id="tier">
+ <option value=""></option>
+ <option value="1">1</option>
+ <option value="2">2</option>
+ <option value="3">3</option>
+ </select>
+</div>
+
+<div class="field">
+ <label for="project_type" class="required">Project Type</label>
+ <select name="project_type" id="project_type">
+ <option value=""></option>
+ <option>Announcement</option>
+ <option>Speaking and Events</option>
+ <option>Planning</option>
+ <option>Messaging and Materials</option>
+ <option>Campaign</option>
+ <option>Other:</option>
+ </select>
+ <input name="project_type_other" id="project_type_other" class="bz_default_hidden">
+</div>
+
+<div class="field">
+ <label for="pr_approach" class="required">PR Approach</label>
+ <select name="pr_approach" id="pr_approach">
+ <option value=""></option>
+ <option value="Proactive">Proactive</option>
+ <option value="Reactive">Reactive</option>
+ </select>
+</div>
+
+<div class="field">
+ <label for="group_focus" class="required">Product Group Focus</label>
+ <select name="group_focus" id="group_focus">
+ <option value=""></option>
+ <option>Firefox Desktop</option>
+ <option>Firefox for Android</option>
+ <option>Marketplace</option>
+ <option>Developer Tools</option>
+ <option>Cloud</option>
+ <option>Firefox OS</option>
+ <option>Corporate / Business Support</option>
+ <option>Other:</option>
+ </select>
+ <input name="group_focus_other" id="group_focus_other" class="bz_default_hidden">
+</div>
+
+<div class="field">
+ <label for="region" class="required">Region</label>
+ <select name="region" id="region">
+ <option value=""></option>
+ <option>Global</option>
+ <option>US</option>
+ <option>LatAm</option>
+ <option>Europe</option>
+ <option>Africa</option>
+ <option>Asia</option>
+ <option>Other:</option>
+ </select>
+ <input name="region_other" id="region_other" class="bz_default_hidden">
+</div>
+
+<div class="head">
+ Goals, Audience, and Messages
+</div>
+
+<div class="field">
+ <label for="project_goals" class="required">Project Goals</label>
+ <textarea name="project_goals" id="project_goals"></textarea>
+</div>
+
+<div class="field">
+ <label for="pr_goals" class="required">PR Goals</label>
+ <textarea name="pr_goals" id="pr_goals"></textarea>
+</div>
+
+<div class="field">
+ <label for="company_goal" class="required">Company Goal</label>
+ <select name="company_goal" id="company_goal">
+ <option value=""></option>
+ <option>Scale Firefox OS</option>
+ <option>Add Services to our Product Lines</option>
+ <option>Get Firefox on a Growth Trajectory</option>
+ <option>Invest in Sustainability</option>
+ <option>Risk Mitigation</option>
+ </select>
+</div>
+
+<div class="field" id="audience_group">
+ <label class="required">Audiences</label>
+ <input type="checkbox" name="audience" id="audience_business" value="Business Press">
+ <label for="audience_business" class="normal">Business Press</label><br>
+ <input type="checkbox" name="audience" id="audience_con_tech" value="Consumer Tech">
+ <label for="audience_con_tech" class="normal">Consumer Tech</label><br>
+ <input type="checkbox" name="audience" id="audience_con" value="Consumer">
+ <label for="audience_con" class="normal">Consumer</label><br>
+ <input type="checkbox" name="audience" id="audience_dev" value="Developer">
+ <label for="audience_dev" class="normal">Developer</label><br>
+ <input type="checkbox" name="audience" id="audience" value="Other:">
+ <label for="audience" class="normal">Other:</label><br>
+ <input name="audience_other" id="audience_other" class="bz_default_hidden indent">
+</div>
+
+<div class="field">
+ <label for="key_messages" class="required">Key Messages</label>
+ <textarea name="key_messages" id="key_messages" placeholder="State (draft) key messages of what we would like to get across to
+ press for this project."></textarea>
+</div>
+
+<div class="head">
+ Materials
+</div>
+
+<div class="field">
+ <label>Project Materials</label>
+ <div>
+ <input type="checkbox" name="proj_mat_online" id="proj_mat_online">
+ <label class="normal" for="proj_mat_online">
+ Online Documentation (e.g. WAVE Dashboard)
+ </label>
+ <div id="proj_mat_online_extra" class="bz_default_hidden indent">
+ <label for="proj_mat_online_desc" class="required">Material Description</label>
+ <input type="text" name="proj_mat_online_desc" id="proj_mat_online_desc">
+ <label for="proj_mat_online_link" class="required">Material Link</label>
+ <input type="text" name="proj_mat_online_link" id="proj_mat_online_link">
+ </div>
+ </div>
+ <div>
+ <input type="checkbox" name="proj_mat_file" id="proj_mat_file">
+ <label class="normal" for="proj_mat_file">
+ Upload File
+ </label>
+ <div id="proj_mat_file_extra" class="bz_default_hidden indent">
+ <label for="proj_mat_file_desc" class="required">File Description</label>
+ <input type="text" name="proj_mat_file_desc" id="proj_mat_file_desc">
+ <label for="proj_mat_file_attach" class="required">File Upload</label>
+ <input type="file" name="proj_mat_file_attach" id="proj_mat_file_attach">
+ </div>
+ </div>
+</div>
+
+<div class="field">
+ <label>PR Project Materials</label>
+ <div>
+ <input type="checkbox" name="pr_mat_online" id="pr_mat_online">
+ <label class="normal" for="pr_mat_online">
+ Online Documentation (e.g. comms plan)
+ </label>
+ <div id="pr_mat_online_extra" class="bz_default_hidden indent">
+ <label for="pr_mat_online_desc" class="required">Material Description</label>
+ <input type="text" name="pr_mat_online_desc" id="pr_mat_online_desc">
+ <label for="pr_mat_online_link" class="required">Material Link</label>
+ <input type="text" name="pr_mat_online_link" id="pr_mat_online_link">
+ </div>
+ </div>
+ <div>
+ <input type="checkbox" name="pr_mat_file" id="pr_mat_file">
+ <label class="normal" for="pr_mat_file">
+ Upload File
+ </label>
+ <div id="pr_mat_file_extra" class="bz_default_hidden indent">
+ <label for="pr_mat_file_desc" class="required">File Description</label>
+ <input type="text" name="pr_mat_file_desc" id="pr_mat_file_desc">
+ <label for="pr_mat_file_attach" class="required">File Upload</label>
+ <input type="file" name="pr_mat_file_attach" id="pr_mat_file_attach">
+ </div>
+ </div>
+</div>
+
+<div class="head">
+ Requirements
+</div>
+
+<div class="field">
+ <label for="metrica">Metrica Coverage Reporting Scope</label>
+ <select name="metrica" id="metrica">
+ <option>No</option>
+ <option>Yes</option>
+ </select>
+ <div id="metrica_extra" class="bz_default_hidden extra">
+ Please fill out the
+ <a href="https://basecamp.com/2256351/projects/2980983/messages/12008835" target="_blank">Metrica form</a>
+ and submit to Metrica no later than a week before project starts.
+ </div>
+</div>
+
+<div class="field" id="press_center_group">
+ <label>Press Center Update</label>
+ <input type="checkbox" name="press_center" id="press_center_post" value="Post on press pages">
+ <label for="press_center_post" class="normal">Post on press pages</label><br>
+ <input type="checkbox" name="press_center" id="press_center_library" value="Media Library update">
+ <label for="press_center_library" class="normal">Media Library update</label><br>
+ <input type="checkbox" name="press_center" id="press_center" value="Other:">
+ <label for="press_center" class="normal">Other:</label><br>
+ <input name="press_center_other" id="press_center_other" class="bz_default_hidden indent">
+</div>
+
+<div class="field" id="internal_resources_group">
+ <label>Internal Resources Needed</label>
+ <input type="text" name="resources" id="resources">
+ <input type="checkbox" name="internal_resources" id="internal_resources_spokesperson" value="Spokesperson">
+ <label for="internal_resources_spokesperson" class="normal">Spokesperson</label><br>
+ <input type="checkbox" name="internal_resources" id="internal_resources_staff" value="Demo Staff">
+ <label for="internal_resources_staff" class="normal">Demo Staff</label><br>
+ <input type="checkbox" name="internal_resources" id="internal_resources_support" value="Creative Support">
+ <label for="internal_resources_support" class="normal">Creative Support</label><br>
+ <input type="checkbox" name="internal_resources" id="internal_resources" value="Other:">
+ <label for="internal_resources" class="normal">Other:</label><br>
+ <input name="internal_resources_other" id="internal_resources_other" class="bz_default_hidden indent">
+</div>
+
+<div class="field" id="external_resources_group">
+ <label>External Resources Needed</label>
+ <input type="checkbox" name="external_resources" id="external_resources_pr" value="PR Agency Support">
+ <label for="external_resources_pr" class="normal">PR Agency Support</label><br>
+ <input type="checkbox" name="external_resources" id="external_resources_design" value="Design Support">
+ <label for="external_resources_design" class="normal">Design Support</label><br>
+ <input type="checkbox" name="external_resources" id="external_resources_community" value="Community Support">
+ <label for="external_resources_community" class="normal">Community Support</label><br>
+ <input type="checkbox" name="external_resources" id="external_resources" value="Other:">
+ <label for="external_resources" class="normal">Other:</label><br>
+ <input name="external_resources_other" id="external_resources_other" class="bz_default_hidden indent">
+</div>
+
+<div class="field">
+ <label for="localization">Localization Needed</label>
+ <select name="localization" id="localization">
+ <option>None</option>
+ <option>Yes - Agency Localization</option>
+ <option>Yes - Community Localization</option>
+ <option>Other:</option>
+ </select>
+ <input name="localization_other" id="localization_other" class="bz_default_hidden">
+</div>
+
+<div class="head">
+ Budget
+</div>
+
+<div class="field">
+ <label for="budget" class="required">Budget</label>
+ <select name="budget" id="budget">
+ <option value=""></option>
+ <option value="Covered">Covered in budget</option>
+ <option value="Extra">Extra budget required:</option>
+ </select>
+ <input name="budget_extra" id="budget_extra" class="bz_default_hidden">
+</div>
+
+<input type="submit" id="commit" value="Submit">
+
+<p>
+ [ <span class="required_star">*</span> <span class="required_explanation">Required Field</span> ]
+</p>
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-poweredby.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-poweredby.html.tmpl
new file mode 100644
index 000000000..e231cd9d5
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-poweredby.html.tmpl
@@ -0,0 +1,87 @@
+[%# 1.0@bugzilla.org %]
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham <gerv@gerv.net>
+ # Ville SkyttŠ <ville.skytta@iki.fi>
+ # John Hoogstrate <hoogstrate@zeelandnet.nl>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Powered by Mozilla Logo Requests"
+%]
+
+[% USE Bugzilla %]
+
+<p>If you are interested in using the <a href="http://www.mozilla.org/poweredby">Powered by Mozilla logo</a>,
+please provide some information about your application or product.</p>
+
+<p><strong>Please use this form for Powered by Mozilla logo requests only.</strong></p>
+
+<form method="post" action="post_bug.cgi" id="tmRequestForm">
+
+ <input type="hidden" name="product" value="Marketing">
+ <input type="hidden" name="component" value="Trademark Permissions">
+ <input type="hidden" name="bug_severity" value="enhancement">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="priority" value="--">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="assigned_to" value="dboswell@mozilla.com">
+ <input type="hidden" name="cc" value="liz@mozilla.com">
+ <input type="hidden" name="groups" value="marketing-private">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+ <table>
+ <tr>
+ <td align="right"><strong>Application or Product Name:</strong></td>
+ <td colspan="3">
+ <input name="short_desc" size="60" value="Powered by Mozilla request for: [% short_desc FILTER html %]">
+ </td>
+ </tr>
+
+ <tr>
+ <td align="right"><strong>URL&nbsp;(optional):</strong></td>
+ <td colspan="3">
+ <input name="bug_file_loc" size="60"
+ value="[% bug_file_loc FILTER html %]">
+ </td>
+ </tr>
+
+ <tr><td align="right" valign="top"><strong>Comments&nbsp;(optional):</strong></td>
+ <td colspan="3">
+ <textarea name="comment" rows="10" cols="80">
+ [% comment FILTER html %]</textarea>
+ <br>
+ </td>
+ </tr>
+
+ </table>
+
+ <br>
+
+ <input type="submit" id="commit" value="Submit Request">
+</form>
+
+<p>Thanks for contacting us.
+ You will be notified by email of any progress made in resolving your
+ request.
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-presentation.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-presentation.html.tmpl
new file mode 100644
index 000000000..7819818b3
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-presentation.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/redirect.html.tmpl
+ url = "https://air.mozilla.org/requests"
+%]
diff --git a/extensions/BMO/template/en/default/bug/create/create-privacy-data.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-privacy-data.html.tmpl
new file mode 100644
index 000000000..b99953282
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-privacy-data.html.tmpl
@@ -0,0 +1,219 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_style = BLOCK %]
+ #bug_form input[type=text], #bug_form input[type=file], #cc_autocomplete, #bug_form textarea {
+ width: 100%;
+ }
+[% END %]
+
+[% inline_js = BLOCK %]
+ function onSubmit() {
+ var error = '';
+ if (!isFilledOut('short_desc')) error += 'Please enter a summary.\n';
+ if (!isFilledOut('attachment')) error += 'Please attach the data set/representative sample.\n';
+ if (!isFilledOut('source')) error += 'Please enter the data source.\n';
+ if (!isFilledOut('data_desc')) error += 'Please enter the data description.\n';
+ if (!isFilledOut('release')) error += 'Please enter the parts of data you want released.\n';
+ if (!isFilledOut('why')) error += 'Please enter why you want to release this data.\n';
+ if (!isFilledOut('when')) error += 'Please enter when you would like to release this data.\n';
+
+ if (error) {
+ alert(error);
+ return false;
+ }
+
+ return true;
+ }
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Privacy - Data Release Proposal"
+ style = inline_style
+ style_urls = [ 'skins/standard/enter_bug.css' ]
+ javascript = inline_js
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/attachment.js', 'js/field.js', 'js/util.js' ]
+ yui = [ 'autocomplete' ]
+%]
+
+<h2>Privacy - Data Release Proposal</h2>
+
+<p>
+ Before filling out this form, please look at the
+ <a href="https://wiki.mozilla.org/Privacy/How_To/Deidentify" target="_blank">guide</a>
+ for releasing info about people.
+</p>
+
+<p>
+ All fields except for CC are required.
+</p>
+
+<form method="post" action="post_bug.cgi" id="bug_form" class="enter_bug_form"
+ enctype="multipart/form-data" onSubmit="return onSubmit()">
+<input type="hidden" name="format" value="privacy-data">
+<input type="hidden" name="product" value="Privacy">
+<input type="hidden" name="component" value="Data Release Proposal">
+<input type="hidden" name="rep_platform" value="All">
+<input type="hidden" name="op_sys" value="Other">
+<input type="hidden" name="priority" value="--">
+<input type="hidden" name="version" value="unspecified">
+<input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+<input type="hidden" name="comment" id="comment" value="">
+<input type="hidden" name="groups" id="groups" value="privacy">
+<input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table>
+
+<tr>
+ <th>
+ <label for="short_desc">Summary:</label>
+ </th>
+ <td>
+ <input type="text" name="short_desc" id="short_desc" value="" size="60">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="cc">CC:</label>
+ </th>
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => cc
+ size => 60
+ multiple => 5
+ %]
+ </td>
+ <td>
+ <i>&nbsp;Optional</i>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="attachment">Data Set:</label>
+ </th>
+ <td>
+ <i>Please attach the data set, or a representative sample.</i>
+ <div>
+ <input type="file" id="attachment" name="data" size="50">
+ <input type="hidden" name="contenttypemethod" value="autodetect">
+ <input type="hidden" name="description" value="Data Set">
+ </div>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="source">Source:</label>
+ </th>
+ <td>
+ <i>Where does this data come from?</i>
+ <div>
+ <textarea name="source" id="source" rows="5" cols="40"></textarea>
+ </div>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="data_desc">Data Description:</label>
+ </th>
+ <td>
+ <i>What people and things does this data describe, and what fields does it contain?</i>
+ <div>
+ <textarea name="data_desc" id="data_desc" rows="5" cols="40"></textarea>
+ </div>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="release">Release:</label>
+ </th>
+ <td>
+ <i>What parts of this data do you want to release?</i>
+ <div>
+ <textarea name="release" id="release" rows="5" cols="40"></textarea>
+ </div>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="why">Why:</label>
+ </th>
+ <td>
+ <i>Why are we releasing this data, and what do we hope people will do with it?</i>
+ <div>
+ <textarea name="why" id="why" rows="5" cols="40"></textarea>
+ </div>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="when">Release Time:</label>
+ </th>
+ <td>
+ <i>Is there a particular time by which you would like to release this data?</i>
+ <div>
+ <input type="text" name="when" id="when" value="" size="60">
+ </div>
+ </td>
+</tr>
+
+<tr>
+ <td colspan="2">
+ Expect to discover that you've missed a few of things, so plan for a couple weeks to get them corrected.
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td>
+ <input type="submit" id="commit" value="Submit Request">
+ </td>
+</tr>
+</table>
+
+</form>
+
+<script type="text/javascript">
+function trySubmit() {
+ var topic = document.getElementById('topic').value;
+ var date = document.getElementById('date').value;
+ var time = document.getElementById('time_hour').value + ':' +
+ document.getElementById('time_minute').value +
+ document.getElementById('ampm').value + " " +
+ document.getElementById('time_zone').value;
+ var location = document.getElementById('location').value;
+ var shortdesc = 'Event - (' + date + ' ' + time + ') - ' + location + ' - ' + topic;
+ document.getElementById('short_desc').value = shortdesc;
+
+ // If intended audience is employees only, add mozilla-employee-confidential group
+ var audience = document.getElementById('audience').value;
+ if (audience == 'Employees Only') {
+ var brownbagRequestForm = document.getElementById('brownbagRequestForm');
+ var groups = document.createElement('input');
+ groups.type = 'hidden';
+ groups.name = 'groups';
+ groups.value = 'mozilla-employee-confidential';
+ brownbagRequestForm.appendChild(groups);
+ }
+
+ return true;
+}
+</script>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-recoverykey.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-recoverykey.html.tmpl
new file mode 100644
index 000000000..ffe9b3482
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-recoverykey.html.tmpl
@@ -0,0 +1,70 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the BMO Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Corporation/Foundation Encryption Recovery Key"
+%]
+
+<p>Please complete the following information as you are encrypting your laptop.</p>
+
+<ul>
+ <li>The Recovery Key will be displayed during the encryption process
+ (<a href="https://mana.mozilla.org/wiki/display/INFRASEC/Desktop+Security#DesktopSecurity-DiskencryptionFileVault">more info</a>)
+ </li>
+ <li>The asset tag number is located on a sticker typically on the bottom of the device.</li>
+</ul>
+
+<form method="post" action="post_bug.cgi" id="recoveryKeyForm" enctype="multipart/form-data">
+ <input type="hidden" name="product" value="mozilla.org">
+ <input type="hidden" name="component" value="Server Operations: Desktop Issues">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="All">
+ <input type="hidden" name="priority" value="--">
+ <input type="hidden" name="version" value="other">
+ <input type="hidden" name="bug_severity" value="normal">
+ <input type="hidden" name="groups" value="mozilla-employee-confidential">
+ <input type="hidden" name="groups" value="infra">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+ <input type="hidden" name="cc" value="tfairfield@mozilla.com, ghuerta@mozilla.com">
+ <input type="hidden" name="short_desc" value="Encryption Recovery Key for [% user.name || user.login FILTER html %]">
+ <input type="hidden" name="format" value="recoverykey">
+ <table>
+ <tr>
+ <td align="right"><strong>Recovery Key:</strong></td>
+ <td>
+ <input name="recoverykey" size="60" value="[% recoverykey FILTER html %]">
+ </td>
+ </tr>
+ <tr>
+ <td align="right"><strong>Asset Tag Number:</strong></td>
+ <td>
+ <input name="assettag" size="60" value="[% assettag FILTER html %]">
+ </td>
+ </tr>
+ <tr>
+ <td></td>
+ <td><input type="submit" id="commit" value="Submit"></td>
+ </tr>
+ </table>
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-swag.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-swag.html.tmpl
new file mode 100644
index 000000000..89f4c8987
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-swag.html.tmpl
@@ -0,0 +1,903 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[%
+items = [
+ { id => '' , name => 'Splendidest Gear' },
+ { id => '#155752' , name => 'Moleskine Notebook' },
+ { id => '#155749' , name => 'Rickshaw Messenger Bag' },
+ { id => '#155415S', name => 'Champion Hooded Sweatshirt S' },
+ { id => '#155415M', name => 'Champion Hooded Sweatshirt M' },
+ { id => '#155415L', name => 'Champion Hooded Sweatshirt L' },
+ { id => '#155415X', name => 'Champion Hooded Sweatshirt XL' },
+ { id => '#1554152', name => 'Champion Hooded Sweatshirt 2XL' },
+ { id => '#157454S', name => 'Very Splendid Package, Men\'s S' },
+ { id => '#157454M', name => 'Very Splendid Package, Men\'s M' },
+ { id => '#157454L', name => 'Very Splendid Package, Men\'s L' },
+ { id => '#157454X', name => 'Very Splendid Package, Men\'s XL' },
+ { id => '#157452S', name => 'Very Splendid Package, Ladies S' },
+ { id => '#157452M', name => 'Very Splendid Package, Ladies M' },
+ { id => '#157452L', name => 'Very Splendid Package, Ladies L' },
+ { id => '#157452X', name => 'Very Splendid Package, Ladies XL' },
+ { id => '#157451S', name => 'Most Splendid Package, S' },
+ { id => '#157451M', name => 'Most Splendid Package, M' },
+ { id => '#157451L', name => 'Most Splendid Package, L' },
+ { id => '#157451X', name => 'Most Splendid Package, XL' },
+ { id => '' , name => 'Splendider' },
+ { id => '#155341M', name => 'Unisex T-shirt Poppy M' },
+ { id => '#155341X', name => 'Unisex T-shirt Poppy XL' },
+ { id => '#1553412', name => 'Unisex T-shirt Poppy 2XL' },
+ { id => '#155344S', name => 'Ladies\' T-shirt Poppy S' },
+ { id => '#155344M', name => 'Ladies\' T-shirt Poppy M' },
+ { id => '#155344L', name => 'Ladies\' T-shirt Poppy L' },
+ { id => '#190928S', name => 'Unisex T-shirt Navy S' },
+ { id => '#190928M', name => 'Unisex T-shirt Navy M' },
+ { id => '#190928L', name => 'Unisex T-shirt Navy L' },
+ { id => '#190928X', name => 'Unisex T-shirt Navy XL' },
+ { id => '#190929L', name => 'Unisex T-shirt Lapis L' },
+ { id => '#1553422', name => 'Unisex T-shirt Navy 2XL' },
+ { id => '#1553423', name => 'Unisex T-shirt Navy 3XL' },
+ { id => '#155413S', name => 'Ladies\' T-shirt Navy S' },
+ { id => '#155413M', name => 'Ladies\' T-shirt Navy M' },
+ { id => '#155413L', name => 'Ladies\' T-shirt Navy L' },
+ { id => '#155413X', name => 'Ladies\' T-shirt Navy XL' },
+ { id => '#155343M', name => 'Unisex T-shirt Lapis M' },
+ { id => '#155343X', name => 'Unisex T-shirt Lapis XL' },
+ { id => '#155414S', name => 'Ladies\' T-shirt Lapis S' },
+ { id => '#155414M', name => 'Ladies\' T-shirt Lapis M' },
+ { id => '#155414L', name => 'Ladies\' T-shirt Lapis L' },
+ { id => '#155339' , name => 'Black Cap with Tote' },
+ { id => '#155340' , name => 'Beanie' },
+ { id => '#155751' , name => 'Drawstring Tote' },
+ { id => '#155758' , name => 'Glossy Finish Ceramic Mug' },
+ { id => '' , name => 'Splendid' },
+ { id => '#155754' , name => 'Mozilla Lanyard with Bulldog Clip' },
+ { id => '#155755' , name => 'Vertical Laminated Badge' },
+ { id => '#155756' , name => 'Silicone Wristband' },
+ { id => '#155757' , name => 'Custom Tattoos - Pkg50' },
+ { id => '#192150' , name => '1.25" Firefox Button-PKG25' },
+ { id => '' , name => 'Firefox OS items' },
+ { id => '#197158' , name => '3" Round Sticker, Firefox logo' },
+ { id => '#185686' , name => '3" Firefox OS Sticker Look Ahead' },
+ { id => '#189674' , name => '3" Firefox OS Mobilizer' },
+ { id => '#187062' , name => 'OS Lanyard w/ Bulldog Clip' },
+ { id => '#180589' , name => 'Sunglasses Firefox OS' },
+ { id => '#180595' , name => 'Rubber Grip Pens Firefox OS' },
+ { id => '#180593' , name => 'Firefox OS Moleskine Notebook' },
+]
+
+mozspaces = [
+ {
+ name => 'Beijing',
+ address1 => 'Mozilla Online Ltd, International Club Office Tower 800A',
+ address2 => '21 Jian Guo Men Wai Avenue',
+ city => 'Beijing',
+ state => 'Chaoyang District',
+ country => 'China',
+ postcode => '100020',
+ },
+ {
+ name => 'Berlin',
+ address1 => 'MZ Denmark ApS - Germany',
+ address2 => 'Rungestrasse 22 - 24',
+ city => 'Berlin',
+ state => 'Germany',
+ country => 'Germany',
+ postcode => '10179',
+ },
+ {
+ name => 'London',
+ address1 => 'Mozilla London',
+ address2 => '101 St. Martin\'s Lane, 3rd Floor',
+ city => 'London',
+ state => 'Greater London',
+ country => 'UK',
+ postcode => 'WC2N 4AZ',
+ },
+ {
+ name => 'Mountain View',
+ address1 => 'Mozilla',
+ address2 => '650 Castro St., Suite 300',
+ city => 'Mountain View',
+ state => 'CA',
+ country => 'USA',
+ postcode => '94041-2072',
+ },
+ {
+ name => 'Paris',
+ address1 => 'Mozilla',
+ address2 => '16 bis Boulevard Montmartre',
+ city => 'Paris',
+ state => 'Paris',
+ country => 'France',
+ postcode => '75009',
+ },
+ {
+ name => 'Portland',
+ address1 => 'Mozilla Portland',
+ address2 => 'Brewery Block 2, 1120 NW Couch St. Suite 320',
+ city => 'Portland',
+ state => 'OR',
+ country => 'USA',
+ postcode => '97209',
+ },
+ {
+ name => 'San Francisco',
+ address1 => 'Mozilla',
+ address2 => '2 Harrison Street, Suite 175',
+ city => 'San Francisco',
+ state => 'CA',
+ country => 'USA',
+ postcode => '94105',
+ },
+ {
+ name => 'Taipei',
+ address1 => '4F-A1, No. 106, Sec.5, Xinyi Rd',
+ address2 => '',
+ city => 'Taipei City',
+ state => 'Xinyi District',
+ country => 'Taiwan',
+ postcode => '11047',
+ },
+ {
+ name => 'Tokyo',
+ address1 => '7-5-6 Roppongi',
+ address2 => '',
+ city => 'Minato-ku',
+ state => 'Tokyo',
+ country => 'Japan',
+ postcode => '106-0032',
+ },
+ {
+ name => 'Toronto',
+ address1 => 'Mozilla Canada',
+ address2 => '366 Adelaide Street W, Suite 500',
+ city => 'Toronto',
+ state => 'Ontario',
+ country => 'Canada',
+ postcode => 'M5V 1R9',
+ },
+ {
+ name => 'Vancouver',
+ address1 => 'Mozilla Canada',
+ address2 => '163 West Hastings Street, Suite 209',
+ city => 'Vancouver',
+ state => 'BC',
+ country => 'Canada',
+ postcode => 'V6B 1H5',
+ },
+]
+
+cost_centers = [
+ 'Accounting (1210)',
+ 'Add Ons (7500)',
+ 'Advanced Techology Lab (6400)',
+ 'Brand Engagement (2400)',
+ 'Business Affairs (1100)',
+ 'Business Development (1150)',
+ 'Business Development Programs (7700)',
+ 'Business Support Services (1000)',
+ 'Cloud & Services (3000)',
+ 'Community Engagement (2300)',
+ 'Design (4400)',
+ 'Dev Infra (3130)',
+ 'Engagement (2000)',
+ 'Engineering Platform (8000)',
+ 'Engineering Program Management (8300)',
+ 'Facilities (1250)',
+ 'Finance Planning & Analysis (1211)',
+ 'Firefox (5000)',
+ 'Firefox Android Engineering (5310)',
+ 'Firefox Android Product Management (5330)',
+ 'Firefox Android UX (5320)',
+ 'Firefox Desktop (5200)',
+ 'Firefox Desktop Engineering (5210)',
+ 'Firefox Desktop Platform Integration (5240)',
+ 'Firefox Desktop Product Management (5230)',
+ 'Firefox Desktop UX (5220)',
+ 'Firefox Dev Tools (5400)',
+ 'Firefox Mobile (5300)',
+ 'Firefox OS Engineering I (6110)',
+ 'Firefox OS Engineering II (6120)',
+ 'Firefox OS Product Management (6200)',
+ 'Firefox OS UX (6300)',
+ 'Identity Eng (3210)',
+ 'Identity Infra (3110)',
+ 'Infrastructure (servers) (3100)',
+ 'Insights and Strategy (4000)',
+ 'IT & Network (1400)',
+ 'Labs (7600)',
+ 'Legal (1120)',
+ 'Localization (L10n) (5100)',
+ 'Location Engineering (3230)',
+ 'Location Infra (3150)',
+ 'Marketplace (7000)',
+ 'Marketplace Apps Engineering (7110)',
+ 'Marketplace Bus. Development (7400)',
+ 'Marketplace Engineering / AMO (7120)',
+ 'Marketplace Engineering /Dev Ecosystem (7130)',
+ 'Marketplace Product Management (7200)',
+ 'Marketplace UX (7300)',
+ 'Market Strategy (4200)',
+ 'Metrics (4300)',
+ 'Misc Infra (3160)',
+ 'Mobile (6000)',
+ 'Mobile Business Development (1130)',
+ 'Mobile Engineering (6130)',
+ 'Operations (3600)',
+ 'People (1340)',
+ 'People Ops (1320)',
+ 'Platform Accessibility (8490)',
+ 'Platform Content (8440)',
+ 'Platform Integration (8480)',
+ 'Platform Network (8410)',
+ 'Platform Network & Security (8400)',
+ 'Platform Performance (8470)',
+ 'Platform Rendering & Media (8450)',
+ 'Platform Security (8420)',
+ 'Platform Security Assurance (8430)',
+ 'Platform Stability & Plugin (8460)',
+ 'PR (2200)',
+ 'Product Marketing (2100)',
+ 'QA (8500)',
+ 'QA Android (8550)',
+ 'QA Automation (8520)',
+ 'QA Firefox Desktop (8510)',
+ 'QA FirefoxOS (8560)',
+ 'QA Mobile (8540)',
+ 'QA Services (8570)',
+ 'QA Web (8530)',
+ 'Release Engineering (8100)',
+ 'Release Management (5010)',
+ 'Research (6900)',
+ 'Services Engineering (3200)',
+ 'Services Product Management (3400)',
+ 'Services UX (3300)',
+ 'SUMO (2600)',
+ 'Sync Engineering (3220)',
+ 'Sync Infra (3120)',
+ 'UP (5500)',
+ 'User Research (4100)',
+ 'Web Engineering (8200)',
+ 'WebRTC (1160)',
+ 'WebRTC Infra (3140)',
+ 'Web Security and Security Automation (3500)',
+ 'Websites & Developer Engagement (2500)',
+]
+
+%]
+
+[% inline_style = BLOCK %]
+#gear_form th {
+ text-align: right;
+ font-weight: normal;
+}
+
+#gear_form .heading {
+ text-align: left;
+ font-weight: bold;
+ border-top: 2px dotted #969696;
+}
+
+.mandatory {
+ color: red;
+}
+[% END %]
+
+[% inline_javascript = BLOCK %]
+var Dom = YAHOO.util.Dom;
+var needed = {
+[% FOREACH item = items %]
+ [% NEXT UNLESS item.id %]
+ '[% item.id FILTER js %]': 0[% ',' UNLESS loop.last %]
+[% END %]
+};
+var needed_freeform = [];
+
+var gear = [
+[% FOREACH item = items %]
+ { id: '[% item.id FILTER js %]', name: '[% item.name FILTER js %]' }
+ [% ',' UNLESS loop.last %]
+[% END %]
+];
+
+var mozspaces = {
+[% FOREACH space = mozspaces %]
+ '[% space.name FILTER js %]': {
+ [% FOREACH key = space.keys.sort %]
+ '[% key FILTER js %]': '[% space.$key FILTER js %]'[% ',' UNLESS loop.last %]
+ [% END %]
+ }[% ',' UNLESS loop.last %]
+[% END %]
+};
+
+[%# implemented this way to allow for dynamic updating of mandatory fields #%]
+var fields = [
+ { id: 'firstname', mandatory: true },
+ { id: 'lastname', mandatory: true },
+ { id: 'email', mandatory: true },
+ { id: 'mozspace', mandatory: false },
+ { id: 'teamcode', mandatory: true },
+ { id: 'purpose', mandatory: true },
+ { id: 'purpose_other', mandatory: false },
+ { id: 'date_required', mandatory: false },
+ { id: 'items', mandatory: true },
+ { id: 'shiptofirstname', mandatory: true },
+ { id: 'shiptolastname', mandatory: true },
+ { id: 'shiptoemail', mandatory: true },
+ { id: 'shiptoaddress1', mandatory: true },
+ { id: 'shiptoaddress2', mandatory: false },
+ { id: 'shiptocity', mandatory: true },
+ { id: 'shiptostate', mandatory: true },
+ { id: 'shiptocountry', mandatory: true },
+ { id: 'shiptopostcode', mandatory: true },
+ { id: 'shiptophone', mandatory: true },
+ { id: 'shiptoidrut', mandatory: false },
+ { id: 'comment', mandatory: false }
+];
+
+function initFields() {
+ [%# find fields in the form, and update the fields array #%]
+ var rows = Dom.get('gear_form').getElementsByTagName('TR');
+ for (var i = 0, l = rows.length; i < l; i++) {
+ var row = rows[i];
+ var field = firstChild(row, 'INPUT') || firstChild(row, 'SELECT') || firstChild(row, 'TEXTAREA');
+ if (!field || field.type == 'submit') continue;
+ var id = field.id;
+ var label = firstChild(row, 'TH');
+ for (var j = 0, m = fields.length; j < m; j++) {
+ if (fields[j].id == id) {
+ fields[j].field = field;
+ fields[j].label = label;
+ fields[j].caption = label.textContent;
+ break;
+ }
+ }
+ }
+ createCalendar('date_required');
+}
+
+function tagMandatoryFields() {
+ [%# add or remove the "* mandatory" marker from fields #%]
+ for (var i = 0, l = fields.length; i < l; i++) {
+ var f = fields[i];
+ if (!f.label) continue;
+ var caption = f.caption;
+ if (f.mandatory)
+ caption = caption + '&nbsp;<span class="mandatory">*</span>';
+ f.label.innerHTML = caption;
+ }
+}
+
+function validateAndSubmit() {
+ var alert_text = '';
+ for(var i = 0, l = fields.length; i < l; i++) {
+ var f = fields[i];
+ if (f.mandatory && !isFilledOut(f.id))
+ if (f.field.nodeName == 'SELECT') {
+ alert_text += 'Please select the ' + f.caption + ".\n";
+ } else {
+ alert_text += 'Please enter the ' + f.caption + ".\n";
+ }
+ }
+ if (isFilledOut('email') && !isValidEmail(Dom.get('email').value))
+ alert_text += "Please enter a valid Email Address.\n";
+ if (isFilledOut('shiptoemail') && !isValidEmail(Dom.get('shiptoemail').value))
+ alert_text += "Please enter a valid Shipping Email Address.\n";
+
+ if (alert_text != '') {
+ alert(alert_text);
+ return false;
+ }
+
+ Dom.get('short_desc').value = 'Mozilla Gear - ' + Dom.get('firstname').value + ' ' + Dom.get('lastname').value;
+ return true;
+}
+
+function onPurposeChange() {
+ var value = Dom.get('purpose').value;
+ var other = Dom.get('purpose_other');
+
+ if (value == 'Event') {
+ getField('purpose_other').mandatory = true;
+ other.placeholder = 'link to wiki'
+ Dom.removeClass('purpose_other_row', 'bz_default_hidden');
+ Dom.addClass('recognition_row', 'bz_default_hidden');
+
+ } else if (value == 'Gear Space Stock') {
+ getField('purpose_other').mandatory = true;
+ other.placeholder = 'indicate space'
+ Dom.removeClass('purpose_other_row', 'bz_default_hidden');
+ Dom.addClass('recognition_row', 'bz_default_hidden');
+
+ } else if (value == 'Other') {
+ getField('purpose_other').mandatory = true;
+ other.placeholder = 'more information';
+ Dom.removeClass('purpose_other_row', 'bz_default_hidden');
+ Dom.addClass('recognition_row', 'bz_default_hidden');
+
+ } else if (value == 'Mozillian Recognition') {
+ getField('purpose_other').mandatory = false;
+ Dom.addClass('purpose_other_row', 'bz_default_hidden');
+ Dom.removeClass('recognition_row', 'bz_default_hidden');
+ onRecognitionChange();
+
+ } else {
+ getField('purpose_other').mandatory = false;
+ Dom.addClass('purpose_other_row', 'bz_default_hidden');
+ Dom.addClass('recognition_row', 'bz_default_hidden');
+ }
+
+ onRecognitionChange();
+}
+
+function onRecognitionChange() {
+ var mandatory = Dom.get('purpose').value != 'Mozillian Recognition'
+ || !Dom.get('recognition_shipping').checked;
+ getField('shiptoaddress1').mandatory = mandatory;
+ getField('shiptocity').mandatory = mandatory;
+ getField('shiptostate').mandatory = mandatory;
+ getField('shiptocountry').mandatory = mandatory;
+ getField('shiptopostcode').mandatory = mandatory;
+ getField('shiptophone').mandatory = mandatory;
+ tagMandatoryFields();
+}
+
+function onMozSpaceChange() {
+ if (Dom.get('mozspace').value) {
+ Dom.removeClass('shipto_mozspace_container', 'bz_default_hidden');
+ } else {
+ Dom.addClass('shipto_mozspace_container', 'bz_default_hidden');
+ }
+ onShipToMozSpaceClick();
+}
+
+function onShipToMozSpaceClick() {
+ var address1 = address2 = city = state = country = postcode = '';
+ if (Dom.get('shipto_mozspace').checked) {
+ var space = Dom.get('mozspace').value;
+ address1 = mozspaces[space].address1;
+ address2 = mozspaces[space].address2;
+ city = mozspaces[space].city;
+ state = mozspaces[space].state;
+ country = mozspaces[space].country;
+ postcode = mozspaces[space].postcode;
+ }
+ Dom.get('shiptoaddress1').value = address1;
+ Dom.get('shiptoaddress2').value = address2;
+ Dom.get('shiptocity').value = city;
+ Dom.get('shiptostate').value = state;
+ Dom.get('shiptocountry').value = country;
+ Dom.get('shiptopostcode').value = postcode;
+ Dom.get('shiptophone').value = '';
+ Dom.get('shiptoidrut').value = '';
+ onRecognitionChange();
+}
+
+function onAddGearChange(focusInput) {
+ var add_gear = Dom.get('add_gear').value;
+ var isFreeform = add_gear == 'custom' || add_gear == 'other';
+ if (isFreeform) {
+ Dom.addClass('quantity', 'bz_default_hidden');
+ resetFreeform();
+ Dom.get('freeform_quantity').value = Dom.get('quantity').value;
+ Dom.removeClass('freeform_quantity', 'bz_default_hidden');
+ Dom.removeClass('add_freeform', 'bz_default_hidden');
+ if (focusInput)
+ Dom.get('freeform_add').focus();
+ } else {
+ Dom.get('quantity').value = Dom.get('freeform_quantity').value;
+ Dom.removeClass('quantity', 'bz_default_hidden');
+ Dom.addClass('freeform_quantity', 'bz_default_hidden');
+ Dom.addClass('add_freeform', 'bz_default_hidden');
+ }
+}
+
+function firstChild(parent, name) {
+ var a = parent.getElementsByTagName(name);
+ return a.length == 0 ? false : a[0];
+}
+
+function getField(id) {
+ for(var i = 0, l = fields.length; i < l; i++) {
+ if (fields[i].id == id)
+ return fields[i];
+ }
+ return false;
+}
+
+function addGear() {
+ var id = Dom.get('add_gear').value;
+ if (id == 'custom' || id == 'other') {
+ var quantity = parseInt(Dom.get('freeform_quantity').value, 10);
+ var name = Dom.get('freeform_add').value;
+ if (!quantity || !name) return;
+ needed_freeform.push({ 'type': id, 'quantity': quantity, 'name': name });
+ Dom.get('add_gear').value = '';
+ resetFreeform();
+ onAddGearChange();
+ } else {
+ var quantity = parseInt(Dom.get('quantity').value, 10);
+ if (!quantity || !id) return;
+ needed[id] += quantity;
+ }
+ showGear();
+}
+
+function resetFreeform() {
+ Dom.get('freeform_quantity').value = '1';
+ Dom.get('freeform_add').value = '';
+}
+
+function removeGear(id) {
+ if (!id) return;
+ needed[id] = 0;
+ showGear();
+}
+
+function removeFreeform(index) {
+ needed_freeform.splice(index, 1);
+ showGear();
+}
+
+function showGear() {
+ var html = '<table border="0" cellpadding="2" cellspacing="0">';
+ var text = '';
+ var count = 0;
+ for (var i = 0, l = gear.length; i < l; i++) {
+ var item = gear[i];
+ var id = item.id;
+ if (!id) continue;
+ if (!needed[id]) continue;
+ count += needed[id];
+ html += '<tr>' +
+ '<td>' + needed[id] + ' x&nbsp;</td>' +
+ '<td>' + YAHOO.lang.escapeHTML(item.name) + '</td>' +
+ '<td><button onclick="removeGear(\'' + id + '\');return false">Remove</button></td>' +
+ '</tr>';
+ text += needed[id] + ' x ' + id + ' ' + item.name + "\n";
+ }
+ for (var i = 0, l = needed_freeform.length; i < l; i++) {
+ var item = needed_freeform[i];
+ count += item.quantity;
+ html += '<tr>' +
+ '<td>' + item.quantity + ' x&nbsp;</td>' +
+ '<td>(' + item.type + ') ' + YAHOO.lang.escapeHTML(item.name) + '</td>' +
+ '<td><button onclick="removeFreeform(\'' + i + '\');return false">Remove</button></td>' +
+ '</tr>';
+ text += item.quantity + ' x (' + item.type + ') ' + item.name + "\n";
+ }
+ if (!count)
+ html += '<tr><td><i>No gear selected.</i></td></tr>';
+ html += '</table>';
+ Dom.get('gear_container').innerHTML = html;
+ Dom.get('items').value = text;
+}
+
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Gear"
+ style = inline_style
+ javascript = inline_javascript
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/field.js', 'js/util.js' ]
+ yui = [ 'autocomplete', 'calendar' ]
+%]
+
+<h1>Mozilla Gear</h1>
+
+<p>
+ Want gear? Follow the steps below and click Submit Request.
+</p>
+<p>
+ Requests are reviewed and processed on Monday morning (US/Pacific). Any
+ requests received after 9am Monday will be processed the following week.
+</p>
+<ul>
+ <li>
+ If approved, your request will either be sent to our gear partner, Staples,
+ for shipment or it will be available for pick-up from your Mozilla space.
+ </li>
+ <li>
+ If your request is not approved, we will let you know why or possibly ask
+ for more information.
+ </li>
+</ul>
+
+<p>
+ Check <a href="https://wiki.mozilla.org/GearStore" target="_blank">the gear
+ wiki</a> for more information about gear, including approved uses and the
+ list of available gear.
+</p>
+
+<p>
+ Gear requests for Rep-driven events and campaigns should continue to be
+ submitted through <a href="https://wiki.mozilla.org/ReMo/Tools_and_Resources"
+ target="_blank">their existing process</a>.
+</p>
+
+<form method="post" action="post_bug.cgi" id="swagRequestForm" enctype="multipart/form-data"
+ onSubmit="return validateAndSubmit();">
+ <input type="hidden" name="format" value="swag">
+ <input type="hidden" name="product" value="Marketing">
+ <input type="hidden" name="component" value="Swag Requests">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="priority" value="--">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="short_desc" id="short_desc" value="">
+ <input type="hidden" name="groups" value="mozilla-engagement">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table id="gear_form">
+
+<tr>
+ <td>&nbsp;</td>
+</tr>
+<tr>
+ <th class="heading" colspan="2">Tell Us What You Want</th>
+</tr>
+
+<tr>
+ <th>Purpose of Gear</th>
+ <td>
+ <select name="purpose" id="purpose" onchange="onPurposeChange()">
+ <option value="">Please select..</option>
+ <option value="Campaign">Campaign</option>
+ <option value="Event">Event</option>
+ <option value="Gear Space Stock">Gear Space Stock</option>
+ <option value="Mozillian Recognition">Mozillian Recognition</option>
+ <option value="Onboarding">Onboarding</option>
+ <option value="Press">Press</option>
+ <option value="Recruiting">Recruiting</option>
+ <option value="VIP">VIP</option>
+ <option value="Other">Other</option>
+ </select>
+ </td>
+</tr>
+
+<tr id="purpose_other_row" class="bz_default_hidden">
+ <th>Purpose Text</th>
+ <td>
+ <input name="purpose_other" id="purpose_other" size="50">
+ </td>
+</tr>
+
+<tr id="recognition_row" class="bz_default_hidden">
+ <th>&nbsp;</th>
+ <td>
+ <input type="checkbox" name="recognition_shipping" id="recognition_shipping" value="Yes"
+ onclick="onRecognitionChange()">
+ <label for="recognition_shipping">
+ This [% terms.bug %] needs recipient shipping information
+ </label><br>
+ <input type="checkbox" name="recognition_sizing" id="recognition_sizing" value="Yes">
+ <label for="recognition_sizing">
+ This [% terms.bug %] needs recipient size information
+ </label><br>
+ </td>
+</tr>
+
+<tr>
+ <th>Date Required</th>
+ <td>
+ <input name="date_required" id="date_required" size="25"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button" id="button_calendar_date_required"
+ onclick="showCalendar('date_required')"><span>cal</span></button>
+ <div id="con_calendar_date_required"></div>
+ </td>
+</tr>
+
+<tr>
+ <th>Gear Needed</th>
+ <td>
+ <input type="hidden" name="items" id="items" value="">
+ <a href="https://wiki.mozilla.org/GearStore/Gearavailable" target="_blank">
+ View the current inventory</a>, then add your selection(s):<br>
+
+ <input type="text" size="2" id="quantity" value="1"
+ onblur="this.value = parseInt(this.value, 10) ? Math.floor(parseInt(this.value, 10)) : 1">
+ <select id="add_gear" onChange="onAddGearChange(true)">
+ <option value="">Please select..</option>
+ [% first_group = 1 %]
+ [% FOREACH item = items %]
+ [% IF item.id == "" %]
+ [% "</optgroup>" UNLESS first_group %]
+ [% first_group = 0 %]
+ <optgroup label="[% item.name FILTER html %]">
+ [% ELSE %]
+ <option value="[% item.id FILTER html %]">[% item.name FILTER html %]</option>
+ [% END %]
+ [% END %]
+ [% "</optgroup>" UNLESS first_group %]
+ <optgroup label="otherwise">
+ <option value="custom">custom</option>
+ <option value="other">other</option>
+ </optgroup>
+ </select>
+ <span id="add_freeform" class="bz_default_hidden">
+ <br>
+ Tell us how many and what you are looking for here. Add details in the
+ comments field below.
+ <br>
+ <input type="text" size="2" id="freeform_quantity" value="1"
+ onblur="this.value = parseInt(this.value, 10) ? Math.floor(parseInt(this.value, 10)) : 1">
+ <input type="text" id="freeform_add" size="40">
+ </span>
+ <button onclick="addGear();return false">Add</button>
+ <br>
+
+ <div id="gear_container"></div>
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+</tr>
+<tr>
+ <th class="heading" colspan="2">Tell Us About You</th>
+</tr>
+
+<tr>
+ <th>First Name</th>
+ <td><input name="firstname" id="firstname" size="50" maxlength="30"></td>
+</tr>
+
+<tr>
+ <th>Last Name</th>
+ <td><input name="lastname" id="lastname" size="50" maxlength="30"></td>
+</tr>
+
+<tr>
+ <th>Email Address</th>
+ <td><input name="email" id="email" size="50" maxlength="50"></td>
+</tr>
+
+<tr>
+ <th>My Mozilla Space</th>
+ <td>
+ <select name="mozspace" id="mozspace" onchange="onMozSpaceChange()">
+ <option value="">Please select..</option>
+ [% FOREACH space = mozspaces %]
+ <option value="[% space.name FILTER html %]">[% space.name FILTER html %]</option>
+ [% END %]
+ </select>
+ <i>(if applicable)</i>
+ <div id="shipto_mozspace_container" class="bz_default_hidden">
+ <input type="checkbox" id="shipto_mozspace" onclick="onShipToMozSpaceClick()">
+ <label for="shipto_mozspace">Ship to this address</label>
+ </div>
+</tr>
+
+<tr>
+ <th>Team + Department Code</th>
+ <td>
+ <select name="teamcode" id="teamcode">
+ <option value="">Please select..</option>
+ [% FOREACH cost IN cost_centers %]
+ <option value="[% cost FILTER html %]">[% cost FILTER html %]</option>
+ [% END %]
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+</tr>
+<tr>
+ <th class="heading" colspan="2">Tell Us Where To Send It</th>
+</tr>
+
+<tr>
+ <td colspan="2">
+ Please be aware that shipping can cost as much as, if not more than, your
+ item. And, items shipped internationally incur customs fees that can be
+ 100%+ the cost of the package. When possible, requests will be filled from
+ gear at your Mozilla space.
+ </td>
+</tr>
+
+<tr>
+ <th>First Name</th>
+ <td><input name="shiptofirstname" id="shiptofirstname" size="50" maxlength="50"></td>
+</tr>
+<tr>
+ <th>Last Name</th>
+ <td><input name="shiptolastname" id="shiptolastname" size="50" maxlength="50"></td>
+</tr>
+<tr>
+ <th>Email Address</th>
+ <td><input name="shiptoemail" id="shiptoemail" size="50" maxlength="50"></td>
+</tr>
+<tr>
+ <th>Address</th>
+ <td><input name="shiptoaddress1" id="shiptoaddress1" size="50" maxlength="50"></td>
+</tr>
+<tr>
+ <th>Address 2</th>
+ <td><input name="shiptoaddress2" id="shiptoaddress2" size="50" maxlength="50"></td>
+</tr>
+<tr>
+ <th>City</th>
+ <td><input name="shiptocity" id="shiptocity" size="50" maxlength="50"></td>
+</tr>
+<tr>
+ <th>State</th>
+ <td><input name="shiptostate" id="shiptostate" size="50" maxlength="50"></td>
+</tr>
+<tr>
+ <th>Country</th>
+ <td><input name="shiptocountry" id="shiptocountry" size="50" maxlength="50"></td>
+</tr>
+<tr>
+ <th>Postal Code</th>
+ <td><input name="shiptopostcode" id="shiptopostcode" size="50" maxlength="50"></td>
+</tr>
+<tr>
+ <th>Recipient Telephone</th>
+ <td>
+ <input name="shiptophone" id="shiptophone" size="50" maxlength="50">
+ <i>(include country code if outside of the US)</i>
+ </td>
+</tr>
+<tr>
+ <th>Personal ID/RUT</th>
+ <td>
+ <input name="shiptoidrut" id="shiptoidrut" size="50" maxlength="50">
+ <i>(if your country requires this)</i>
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+</tr>
+<tr>
+ <th class="heading" colspan="2">Tell Us Anything Else</th>
+</tr>
+
+<tr>
+ <th>Additional Comments</th>
+ <td><textarea id="comment" name="comment" rows="5" cols="50"></textarea></td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td><input type="submit" id="commit" value="Submit Request"></td>
+</tr>
+
+</table>
+</form>
+
+<p>
+ <span class="mandatory">*</span> Required Field
+</p>
+
+<p>
+ Requests will only be visible to the person who submitted it, authorized
+ members of the Mozilla Engagement team, and our Staples Customer Service rep.
+ We do this to help protect the personal identifying information in this [% terms.bugs %].
+</p>
+
+<script>
+ initFields();
+ onPurposeChange();
+ onAddGearChange();
+ tagMandatoryFields();
+ showGear();
+</script>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-trademark.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-trademark.html.tmpl
new file mode 100644
index 000000000..977ad00d4
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-trademark.html.tmpl
@@ -0,0 +1,87 @@
+[%# 1.0@bugzilla.org %]
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham <gerv@gerv.net>
+ # Ville Skyttä <ville.skytta@iki.fi>
+ # John Hoogstrate <hoogstrate@zeelandnet.nl>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Trademark Usage Requests"
+%]
+
+[% USE Bugzilla %]
+
+<p>
+ If, after reading
+ <a href="http://www.mozilla.org/foundation/trademarks/">the trademark policy
+ documents</a>, you know you need permission to use a certain trademark, this
+ is the place to be.
+</p>
+
+<p><strong>Please use this form for trademark requests only!</strong></p>
+
+<form method="post" action="post_bug.cgi" id="tmRequestForm">
+
+ <input type="hidden" name="product" value="Marketing">
+ <input type="hidden" name="component" value="Trademark Permissions">
+ <input type="hidden" name="bug_severity" value="enhancement">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="priority" value="P3">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="groups" value="marketing-private">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+ <table>
+ <tr>
+ <td align="right"><strong>Summary:</strong></td>
+ <td colspan="3">
+ <input name="short_desc" size="60" value="[% short_desc FILTER html %]">
+ </td>
+ </tr>
+
+ <tr><td align="right" valign="top"><strong>Description:</strong></td>
+ <td colspan="3">
+ <textarea name="comment" rows="10" cols="80">
+ [% comment FILTER html %]</textarea>
+ <br>
+ </td>
+ </tr>
+ <tr>
+ <td align="right"><strong>URL&nbsp;(optional):</strong></td>
+ <td colspan="3">
+ <input name="bug_file_loc" size="60"
+ value="[% bug_file_loc FILTER html %]">
+ </td>
+ </tr>
+ </table>
+
+ <br>
+
+ <input type="submit" id="commit" value="Submit Request">
+</form>
+
+<p>Thanks for contacting us.
+ You will be notified by email of any progress made in resolving your
+ request.
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-user-engagement.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-user-engagement.html.tmpl
new file mode 100644
index 000000000..f523b205b
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-user-engagement.html.tmpl
@@ -0,0 +1,219 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_style = BLOCK %]
+#engagement_form {
+ padding: 10px;
+}
+#engagement_form .required:after {
+ content: " *";
+ color: red;
+}
+#engagement_form .field_label {
+ font-weight: bold;
+}
+#engagement_form .field_desc {
+ padding-bottom: 3px;
+}
+#engagement_form .field_desc,
+#engagement_form .head_desc {
+ width: 600px;
+ word-wrap: normal;
+}
+#engagement_form .head_desc {
+ padding-top: 5px;
+ padding-bottom: 12px;
+}
+#engagement_form .form_section {
+ margin-bottom: 10px;
+}
+#engagement_form textarea {
+ font-family: inherit;
+ font-size: inherit;
+}
+#engagement_form em {
+ font-size: 1em;
+}
+.yui-calcontainer {
+ z-index: 2;
+}
+[% END %]
+
+[% inline_javascript = BLOCK %]
+function validateAndSubmit() {
+ var alert_text = '';
+ if (!isFilledOut('goals')) alert_text += 'Please enter a value for project goals.\n';
+ if (!isFilledOut('short_desc')) alert_text += 'Please enter a value for project title.\n';
+ if (!isFilledOut('audience')) alert_text += 'Please enter a value for who you are trying to reach.\n';
+ if (!isFilledOut('timing_date')) alert_text += 'Please enter a value for project timing.\n';
+ if (!isFilledOut('localization')) alert_text += 'Please enter a value for project localization.\n';
+ if (!isFilledOut('success')) alert_text += 'Please enter a value for project success\n';
+ if (!isFilledOut('bug_file_loc')) alert_text += 'Please enter a value for project destination url.\n';
+ if (!isFilledOut('mozilla_goal')) alert_text += 'Please enter a value for Mozilla goal.\n';
+ if (alert_text != '') {
+ alert(alert_text);
+ return false;
+ }
+ return true;
+}
+function toggleGoalOther() {
+ var goal_select = YAHOO.util.Dom.get('goal');
+ if (goal_select.options[goal_select.selectedIndex].value == 'Other') {
+ YAHOO.util.Dom.removeClass('goal_other','bz_default_hidden');
+ }
+ else {
+ YAHOO.util.Dom.addClass('goal_other','bz_default_hidden');
+ }
+}
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "User Engagement Form"
+ style = inline_style
+ javascript = inline_javascript
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/field.js', 'js/util.js' ]
+ yui = [ "autocomplete", "calendar" ]
+%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+<form id="engagement_form" method="post" action="post_bug.cgi" enctype="multipart/form-data"
+ onSubmit="return validateAndSubmit();">
+ <input type="hidden" name="format" value="user-engagement">
+ <input type="hidden" name="product" value="Marketing">
+ <input type="hidden" name="component" value="User Engagement">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+<img title="User Engagement Form" src="extensions/BMO/web/images/user-engagement.png">
+
+<div class="head_desc">
+ Have something that you think our users should know about? Is there a campaign that you
+ think may benefit from promotion on Mozilla’s User Engagement channels?<br>
+ <br>
+ Please use this form to help us understand the goals of your project or campaign.
+ We’ll use this data to recommend a promotional plan that will meet your needs.
+</div>
+
+<div class="form_section">
+ <label for="short_desc" class="field_label required">Project / Request Title</label>
+ <div class="field_desc">
+ Please tell us about your request in a few words
+ </div>
+ <input type="text" name="short_desc" id="short_desc" size="80">
+</div>
+
+<div class="form_section">
+ <label for="goals" class="field_label required">Project Goals</label>
+ <div class="field_desc">
+ Here’s where you tell us all the juicy details, especially your GOALS for this project.
+ Please tell us “I want to achieve this awesome goal†(ie. increase sign ups for this initiative,
+ get 1 million users to do X, etc.) rather than “I want a promotion on this specific channel.â€
+ </div>
+ <textarea id="goals" name="goals" cols="80" rows="5"></textarea>
+</div>
+
+<div class="form_section">
+ <label for="audience" class="field_label required">Who are you trying to reach?</label>
+ <div class="field_desc">
+ Use this section to explain the type of user you’re targeting. Who is the audience? Consumers?
+ Early adopters? Developers? Be specific.
+ </div>
+ <textarea id="audience" name="audience" cols="80" rows="5"></textarea>
+</div>
+
+<div class="form_section">
+ <label for="localization" class="field_label required">Localization</label>
+ <div class="field_desc">
+ Please tell us if your content needs to be localized, and in what languages.
+ Is the landing page localized?
+ </div>
+ <input type="text" name="localization" id="localization" size="80">
+</div>
+
+<div class="form_section">
+ <label for="bug_file_loc" class="field_label required">Destination URL</label>
+ <div class="field_desc">
+ Where would the user be sent when they click on the promotion?
+ </div>
+ <input type="text" name="bug_file_loc" id="bug_file_loc" size="80">
+</div>
+
+<div class="form_section">
+ <label for="timing_date" class="field_label required">Timing</label>
+ <div class="field_desc">
+ Here’s where you tell us when the initiative will launch. The content calendar
+ is determined at least 6 weeks in advance (to accommodate localization, etc.)
+ so the more notice we have, the better we’ll be able to help you meet your goals.
+ </div>
+ <input name="timing_date" size="20" id="timing_date" value=""
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_timing_date"
+ onclick="showCalendar('timing_date')">
+ <span>Calendar</span>
+ </button>
+ <div id="con_calendar_timing_date"></div>
+ <script type="text/javascript">
+ createCalendar('timing_date')
+ </script>
+</div>
+
+<div class="form_section">
+ <label for="success" class="field_label required">Success</label>
+ <div class="field_desc">
+ In a few words, tell us how you will define success from promotion to our consumers?
+ (example: Success is 1000 people clicking on this link.)
+ </div>
+ <textarea id="success" name="success" cols="80" rows="5"></textarea>
+</div>
+
+<div class="form_section">
+ <label for="mozilla_goal" class="field_label required">Mozilla Goal</label>
+ <div class="field_desc">
+ What high-level Mozilla goal does this achieve?
+ </div>
+ <input type="text" name="mozilla_goal" id="mozilla_goal" size="80">
+</div>
+
+<div class="form_section">
+ <label for="cc" class="field_label">Points of Contact</label>
+ <div class="field_desc">
+ Who should be cc’d on this [% terms.bug %] and kept informed of updates?
+ </div>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => ""
+ size => 80
+ classes => ["bz_userfield"]
+ multiple => 5
+ %]
+</div>
+
+<div class="head_desc">
+ Once your form has been submitted, a tracking [% terms.bug %] will be created. We will
+ then reach out for additional info and next steps. Thanks!
+</div>
+
+<input type="submit" id="commit" value="Submit">
+
+<p>
+ [ <span class="required_star">*</span> <span class="required_explanation">Required Field</span> ]
+</p>
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-web-bounty.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-web-bounty.html.tmpl
new file mode 100644
index 000000000..d76d57298
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-web-bounty.html.tmpl
@@ -0,0 +1,142 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_style = BLOCK %]
+#web_bounty_form {
+ padding: 10px;
+}
+#web_bounty_form .required:after {
+ content: " *";
+ color: red;
+}
+#web_bounty_form .field_label {
+ font-weight: bold;
+}
+#web_bounty_form .field_desc {
+ padding-bottom: 3px;
+}
+#web_bounty_form .field_desc,
+#web_bounty_form .head_desc {
+ width: 600px;
+ word-wrap: normal;
+}
+#web_bounty_form .head_desc {
+ padding-top: 5px;
+ padding-bottom: 12px;
+}
+#web_bounty_form .form_section {
+ margin-bottom: 10px;
+}
+#web_bounty_form textarea {
+ font-family: inherit;
+ font-size: inherit;
+ margin: 0 !important;
+}
+#web_bounty_form em {
+ font-size: 1em;
+}
+[% END %]
+
+[% inline_javascript = BLOCK %]
+function validateAndSubmit() {
+ var alert_text = '';
+ if (!isFilledOut('short_desc')) alert_text += 'Please enter a value for summary.\n';
+ if (!isFilledOut('comment')) alert_text += 'Please enter a value for comment.\n';
+ if (alert_text != '') {
+ alert(alert_text);
+ return false;
+ }
+ return true;
+}
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Web Bounty Form"
+ style = inline_style
+ javascript = inline_javascript
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/field.js', 'js/util.js' ]
+%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+<form id="web_bounty_form" method="post" action="post_bug.cgi" enctype="multipart/form-data"
+ onSubmit="return validateAndSubmit();">
+ <input type="hidden" name="product" value="Websites">
+ <input type="hidden" name="component" value="Other">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="All">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="priority" id="priority" value="--">
+ <input type="hidden" name="target_milestone" id="target_milestone" value="---">
+ <input type="hidden" name="status_whiteboard" id="status_whiteboard" value="[reporter-external] [web-bounty-form] [verif?]">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="groups" id="group_52" value="websites-security">
+ <input type="hidden" name="flag_type-803" id="flag_type-803" value="?">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+<div class="head_desc">
+ <a href="https://developer.mozilla.org/en-US/docs/Mozilla/QA/Bug_writing_guidelines?redirectlocale=en-US&redirectslug=Bug_writing_guidelines">
+ [% terms.Bug %] writing guidelines</a>
+</div>
+
+<div class="form_section">
+ <label for="short_desc" class="field_label required">Summary / Title</label>
+ <div class="field_desc">
+ A short description of the issue being reported including the host name
+ for the website on which it exists (example xss in blarg.foo.mozilla.org)
+ </div>
+ <input type="text" name="short_desc" id="short_desc" size="80">
+</div>
+
+<div class="form_section">
+ <label for="comment" class="field_label required">Comment</label>
+ <div class="field_desc">
+ How was this issue discovered, include the steps, tools or other information that
+ will help reproduce and diagnose the issue. A good primer on what to include can
+ be found <a href="https://developer.mozilla.org/en-US/docs/Mozilla/QA">here</a>.
+ </div>
+ <textarea id="comment" name="comment" cols="80" rows="5"></textarea>
+</div>
+
+<div class="form_section">
+ <label for="bug_file_loc" class="field_label">URL</label>
+ <div class="field_desc">
+ The full URL (hostname/subpage) where the issue exists (if the URL is especially long
+ please just include it in the comments)
+ </div>
+ <input type="text" name="bug_file_loc" id="bug_file_loc" size="80">
+</div>
+
+<div class="form_section">
+ <label for="data" class="field_label">Attachment</label>
+ <div class="field_desc">
+ A file that can add context to the report, such as a screen shot or code block for
+ reproduction purposes.
+ </div>
+ <input type="file" id="data" name="data" size="50">
+ <input type="hidden" name="contenttypemethod" value="autodetect" />
+ <div class="field_desc">
+ <label for="description">Description</label>
+ </div>
+ <input type="text" id="description" name="description" size="80">
+</div>
+
+<input type="submit" id="commit" value="Submit">
+
+<p>
+ [ <span class="required_star">*</span> <span class="required_explanation">Required Field</span> ]
+</p>
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-winqual.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-winqual.html.tmpl
new file mode 100644
index 000000000..fd21ed4ed
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-winqual.html.tmpl
@@ -0,0 +1,831 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham <gerv@gerv.net>
+ # Ville Skyttä <ville.skytta@iki.fi>
+ # Shane H. W. Travis <travis@sedsystems.ca>
+ # Marc Schumann <wurblzap@gmail.com>
+ # Akamai Technologies <bugzilla-dev@akamai.com>
+ # Max Kanat-Alexander <mkanat@bugzilla.org>
+ # Frédéric Buclin <LpSolit@gmail.com>
+ #%]
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+
+[% title = BLOCK %]Enter [% terms.Bug %]: [% product.name FILTER html %][% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = title
+ yui = [ 'autocomplete', 'calendar', 'datatable', 'button' ]
+ style_urls = [ 'skins/standard/attachment.css',
+ 'skins/standard/enter_bug.css',
+ 'skins/custom/create_bug.css' ]
+ javascript_urls = [ "js/attachment.js", "js/util.js",
+ "js/field.js", "js/TUI.js", "js/bug.js",
+ "js/create_bug.js" ]
+ onload = "init();"
+%]
+
+<script type="text/javascript">
+<!--
+
+function init() {
+ set_assign_to();
+ hideElementById('attachment_true');
+ showElementById('attachment_false');
+ showElementById('btn_no_attachment');
+ initCrashSignatureField();
+ init_take_handler('[% user.login FILTER js %]');
+}
+
+function initCrashSignatureField() {
+ var el = document.getElementById('cf_crash_signature');
+ if (!el) return;
+ [% IF cf_crash_signature.length %]
+ YAHOO.util.Dom.addClass('cf_crash_signature_container', 'bz_default_hidden');
+ [% ELSE %]
+ hideEditableField('cf_crash_signature_container','cf_crash_signature_input',
+ 'cf_crash_signature_action', 'cf_crash_signature');
+ [% END %]
+}
+
+var initialowners = new Array();
+var last_initialowner;
+var initialccs = new Array();
+var components = new Array();
+var comp_desc = new Array();
+var flags = new Array();
+[% IF Param("useqacontact") %]
+ var initialqacontacts = new Array([% product.components.size %]);
+ var last_initialqacontact;
+[% END %]
+[% count = 0 %]
+[%- FOREACH c = product.components %]
+ [% NEXT IF NOT c.is_active %]
+ [% NEXT IF c.name != 'WinQual Reports' %]
+ components[[% count %]] = "[% c.name FILTER js %]";
+ comp_desc[[% count %]] = "[% c.description FILTER html_light FILTER js %]";
+ initialowners[[% count %]] = "[% c.default_assignee.login FILTER js %]";
+ [% flag_list = [] %]
+ [% FOREACH f = c.flag_types(is_active=>1).bug %]
+ [% flag_list.push(f.id) %]
+ [% END %]
+ [% FOREACH f = c.flag_types(is_active=>1).attachment %]
+ [% flag_list.push(f.id) %]
+ [% END %]
+ flags[[% count %]] = [[% flag_list.join(",") FILTER js %]];
+ [% IF Param("useqacontact") %]
+ initialqacontacts[[% count %]] = "[% c.default_qa_contact.login FILTER js %]";
+ [% END %]
+
+ [% SET initial_cc_list = [] %]
+ [% FOREACH cc_user = c.initial_cc %]
+ [% initial_cc_list.push(cc_user.login) %]
+ [% END %]
+ initialccs[[% count %]] = "[% initial_cc_list.join(', ') FILTER js %]";
+
+ [% count = count + 1 %]
+[%- END %]
+
+function set_assign_to() {
+ // Based on the selected component, fill the "Assign To:" field
+ // with the default component owner, and the "QA Contact:" field
+ // with the default QA Contact. It also selectively enables flags.
+ var form = document.Create;
+ var assigned_to = form.assigned_to.value;
+
+[% IF Param("useqacontact") %]
+ var qa_contact = form.qa_contact.value;
+[% END %]
+
+ var index = -1;
+ if (form.component.type == 'select-one') {
+ index = form.component.selectedIndex;
+ } else if (form.component.type == 'hidden') {
+ // Assume there is only one component in the list
+ index = 0;
+ }
+ if (index != -1) {
+ var owner = initialowners[index];
+ var component = components[index];
+ if (assigned_to == last_initialowner
+ || assigned_to == owner
+ || assigned_to == '') {
+ form.assigned_to.value = owner;
+ last_initialowner = owner;
+ }
+
+ document.getElementById('initial_cc').innerHTML = initialccs[index];
+ document.getElementById('comp_desc').innerHTML = comp_desc[index];
+
+ if (initialccs[index]) {
+ showElementById('initial_cc_label');
+ showElementById('initial_cc');
+ } else {
+ hideElementById('initial_cc_label');
+ hideElementById('initial_cc');
+ }
+
+ [% IF Param("useqacontact") %]
+ var contact = initialqacontacts[index];
+ if (qa_contact == last_initialqacontact
+ || qa_contact == contact
+ || qa_contact == '') {
+ form.qa_contact.value = contact;
+ last_initialqacontact = contact;
+ }
+ [% END %]
+
+ // First, we disable all flags. Then we re-enable those
+ // which are available for the selected component.
+ var inputElements = document.getElementsByTagName("select");
+ var inputElement, flagField;
+ for ( var i=0 ; i<inputElements.length ; i++ ) {
+ inputElement = inputElements.item(i);
+ if (inputElement.name.search(/^flag_type-(\d+)$/) != -1) {
+ var id = inputElement.name.replace(/^flag_type-(\d+)$/, "$1");
+ inputElement.disabled = true;
+ // Also hide the requestee field, if it exists.
+ inputElement = document.getElementById("requestee_type-" + id);
+ if (inputElement)
+ YAHOO.util.Dom.addClass(inputElement.parentNode, 'bz_default_hidden');
+ }
+ }
+ // Now enable flags available for the selected component.
+ for (var i = 0; i < flags[index].length; i++) {
+ flagField = document.getElementById("flag_type-" + flags[index][i]);
+ // Do not enable flags the user cannot set nor request.
+ if (flagField && flagField.options.length > 1) {
+ flagField.disabled = false;
+ // Re-enabling the requestee field depends on the status
+ // of the flag.
+ toggleRequesteeField(flagField, 1);
+ }
+ }
+ }
+}
+
+var status_comment_required = new Array();
+[% FOREACH status = bug_status %]
+ status_comment_required['[% status.name FILTER js %]'] =
+ [% status.comment_required_on_change_from() ? 'true' : 'false' %]
+[% END %]
+
+TUI_alternates['expert_fields'] = 'Show Advanced Fields';
+// Hide the Advanced Fields by default, unless the user has a cookie
+// that specifies otherwise.
+TUI_hide_default('expert_fields');
+
+-->
+</script>
+
+<form name="Create" id="Create" method="post" action="post_bug.cgi"
+ class="enter_bug_form" enctype="multipart/form-data"
+ onsubmit="return validateEnterBug(this)">
+ <input type="hidden" name="product" value="Firefox">
+ <input type="hidden" name="component" value="WinQual Reports">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+ <input type="hidden" name="groups" value="winqual-data">
+
+<table>
+<tbody>
+ <tr>
+ <td colspan="4">
+ [%# Migration note: The following file corresponds to the old Param
+ # 'entryheaderhtml'
+ #%]
+ [% PROCESS 'bug/create/user-message.html.tmpl' %]
+ </td>
+ </tr>
+
+ <tr>
+ <td colspan="2">
+ <input type="button" id="expert_fields_controller"
+ value="Hide Advanced Fields" onClick="toggleAdvancedFields()">
+ [%# Show the link if the browser supports JS %]
+ <script type="text/javascript">
+ YAHOO.util.Dom.removeClass('expert_fields_controller',
+ 'bz_default_hidden');
+ </script>
+ </td>
+ <td colspan="2">
+ (<span class="required_star">*</span> =
+ <span class="required_explanation">Required Field</span>)
+ </td>
+ </tr>
+
+ <tr>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.product, editable = 0,
+ value = product.name %]
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.reporter, editable = 0,
+ value = user.login %]
+ </tr>
+
+ [%# We can't use the select block in these two cases for various reasons. %]
+ <tr>
+ [% component_desc_url = BLOCK -%]
+ describecomponents.cgi?product=[% product.name FILTER uri %]
+ [% END %]
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.component editable = 1
+ desc_url = component_desc_url
+ %]
+ <td id="field_container_component">
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.component, editable = 0,
+ value = "WinQual Reports", no_tds = 1 %]
+ <script type="text/javascript">
+ <!--
+ [%+ INCLUDE "bug/field-events.js.tmpl"
+ field = bug_fields.component %]
+ YAHOO.util.Event.onDOMReady(set_assign_to);
+ //-->
+ </script>
+ </td>
+
+ <td colspan="2" id="comp_desc_container">
+ [%# Enclose the fieldset in a nested table so that its width changes based
+ # on the length on the component description. %]
+ <table>
+ <tr>
+ <td>
+ <fieldset>
+ <legend>Component Description</legend>
+ <div id="comp_desc" class="comment"></div>
+ </fieldset>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.version editable = 1 rowspan = 3
+ %]
+ <td rowspan="3">
+ <select name="version" id="version" size="5">
+ [%- FOREACH v = version %]
+ [% NEXT IF NOT v.is_active %]
+ <option value="[% v.name FILTER html %]"
+ [% ' selected="selected"' IF v.name == default.version %]>[% v.name FILTER html -%]
+ </option>
+ [%- END %]
+ </select>
+ </td>
+
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.bug_severity, editable = 1,
+ value = default.bug_severity %]
+ </tr>
+
+ <tr>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.rep_platform, editable = 1,
+ value = default.rep_platform %]
+ </tr>
+
+ <tr>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.op_sys, editable = 1,
+ value = default.op_sys %]
+ </tr>
+ [% IF !Param('defaultplatform') || !Param('defaultopsys') %]
+ <tr>
+ <th colspan="3">&nbsp;</th>
+ <td id="os_guess_note" class="comment">
+ <div>We've made a guess at your
+ [% IF Param('defaultplatform') %]
+ operating system. Please check it
+ [% ELSIF Param('defaultopsys') %]
+ platform. Please check it
+ [% ELSE %]
+ operating system and platform. Please check them
+ [% END %]
+ and make any corrections if necessary.</div>
+ </td>
+ </tr>
+ [% END %]
+</tbody>
+
+<tbody class="expert_fields">
+ <tr>
+ [% IF Param('usetargetmilestone') && Param('letsubmitterchoosemilestone') %]
+ [% INCLUDE select field = bug_fields.target_milestone %]
+ [% ELSE %]
+ <td colspan="2">&nbsp;</td>
+ [% END %]
+
+ [% IF Param('letsubmitterchoosepriority') %]
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.priority, editable = 1,
+ value = default.priority %]
+ [% ELSE %]
+ <td colspan="2">&nbsp;</td>
+ [% END %]
+ </tr>
+</tbody>
+
+<tbody class="expert_fields">
+ <tr>
+ <td colspan="4">&nbsp;</td>
+ </tr>
+
+ <tr>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.bug_status,
+ editable = (bug_status.size > 1), value = default.bug_status
+ override_legal_values = bug_status %]
+ </tr>
+
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.assigned_to editable = 1
+ %]
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "assigned_to"
+ name => "assigned_to"
+ value => assigned_to
+ disabled => assigned_to_disabled
+ size => 30
+ emptyok => 1
+ custom_userlist => assignees_list
+ %]
+ [% UNLESS assigned_to_disabled %]
+ <span id="take_bug">
+ &nbsp;(<a title="Assign to yourself" href="#"
+ onclick="return take_bug('[% user.login FILTER js %]')">take</a>)
+ </span>
+ [% END %]
+ <noscript>(Leave blank to assign to component's default assignee)</noscript>
+ </td>
+
+[% IF Param("useqacontact") %]
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.qa_contact editable = 1
+ %]
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "qa_contact"
+ name => "qa_contact"
+ value => qa_contact
+ disabled => qa_contact_disabled
+ size => 30
+ emptyok => 1
+ custom_userlist => qa_contacts_list
+ %]
+ <noscript>(Leave blank to assign to default qa contact)</noscript>
+ </td>
+ </tr>
+[% END %]
+
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.cc editable = 1
+ %]
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => cc
+ disabled => cc_disabled
+ size => 30
+ multiple => 5
+ %]
+ </td>
+ <th>
+ <span id="initial_cc_label" class="bz_default_hidden">
+ Default [% field_descs.cc FILTER html %]:
+ </span>
+ </th>
+ <td>
+ <span id="initial_cc"></span>
+ </td>
+ </tr>
+
+ <tr>
+ <td colspan="3">&nbsp;</td>
+ </tr>
+
+[% IF Param("usebugaliases") %]
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.alias editable = 1
+ %]
+ <td colspan="2">
+ <input name="alias" size="20" value="[% alias FILTER html %]">
+ </td>
+ </tr>
+[% END %]
+</tbody>
+
+<tbody>
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.short_desc editable = 1
+ %]
+ <td colspan="3" class="field_value">
+ <input name="short_desc" size="70" value="[% short_desc FILTER html %]"
+ maxlength="255" spellcheck="true" aria-required="true"
+ class="required text_input" id="short_desc">
+ </td>
+ </tr>
+
+ [% IF feature_enabled('jsonrpc') AND !cloned_bug_id %]
+ <tr id="possible_duplicates_container" class="bz_default_hidden">
+ <th>Possible<br>Duplicates:</th>
+ <td colspan="3">
+ <div id="possible_duplicates"></div>
+ <script type="text/javascript">
+ var dt_columns = [
+ { key: "id", label: "[% field_descs.bug_id FILTER js %]",
+ formatter: YAHOO.bugzilla.dupTable.formatBugLink },
+ { key: "summary",
+ label: "[% field_descs.short_desc FILTER js %]",
+ formatter: "text" },
+ { key: "status",
+ label: "[% field_descs.bug_status FILTER js %]",
+ formatter: YAHOO.bugzilla.dupTable.formatStatus },
+ { key: "update_token", label: '',
+ formatter: YAHOO.bugzilla.dupTable.formatCcButton }
+ ];
+ YAHOO.bugzilla.dupTable.addCcMessage = "Add Me to the CC List";
+ YAHOO.bugzilla.dupTable.init({
+ container: 'possible_duplicates',
+ columns: dt_columns,
+ product_name: '[% product.name FILTER js %]',
+ summary_field: 'short_desc',
+ options: {
+ MSG_LOADING: 'Searching for possible duplicates...',
+ MSG_EMPTY: 'No possible duplicates found.',
+ SUMMARY: 'Possible Duplicates'
+ }
+ });
+ </script>
+ </td>
+ </tr>
+ [% END %]
+
+ <tr>
+ <th>Description:</th>
+ <td colspan="3">
+
+ [% defaultcontent = BLOCK %]
+ [% IF cloned_bug_id %]
++++ This [% terms.bug %] was initially created as a clone of [% terms.Bug %] #[% cloned_bug_id FILTER html %] +++
+
+
+ [% END %]
+ [%-# We are within a BLOCK. The comment will be correctly HTML-escaped
+ # by global/textarea.html.tmpl. So we must not escape the comment here. %]
+ [% comment FILTER none %]
+ [%- END %]
+ [% INCLUDE global/textarea.html.tmpl
+ name = 'comment'
+ id = 'comment'
+ minrows = 10
+ maxrows = 25
+ cols = constants.COMMENT_COLS
+ defaultcontent = defaultcontent
+ %]
+ <br>
+ </td>
+ </tr>
+
+<tbody class="expert_fields">
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.bug_file_loc editable = 1
+ %]
+ <td colspan="3" class="field_value">
+ <input name="bug_file_loc" id="bug_file_loc" class="text_input"
+ size="40" value="[% bug_file_loc FILTER html %]">
+ </td>
+ </tr>
+</tbody>
+
+<tbody>
+ [% IF Param("maxattachmentsize") %]
+ <tr>
+ <th>Attachment:</th>
+ <td colspan="3">
+ <div id="attachment_false" class="bz_default_hidden">
+ <input type="button" value="Add an attachment" onClick="handleWantsAttachment(true)">
+ </div>
+
+ <div id="attachment_true">
+ <input type="button" id="btn_no_attachment" value="Don't add an attachment"
+ class="bz_default_hidden" onClick="handleWantsAttachment(false)">
+ <fieldset>
+ <legend>Add an attachment</legend>
+ <table class="attachment_entry">
+ [% PROCESS attachment/createformcontents.html.tmpl
+ flag_types = product.flag_types(is_active=>1).attachment
+ any_flags_requesteeble = 1
+ flag_table_id ="attachment_flags" %]
+ </table>
+
+ [% IF user.is_insider %]
+ <input type="checkbox" id="comment_is_private" name="comment_is_private"
+ [% ' checked="checked"' IF comment_is_private %]
+ onClick="updateCommentTagControl(this, 'comment')">
+ <label for="comment_is_private">
+ Make this attachment and [% terms.bug %] description private (visible only
+ to members of the <strong>[% Param('insidergroup') FILTER html %]</strong> group)
+ </label>
+ [% END %]
+ </fieldset>
+ </div>
+ </td>
+ </tr>
+ [% END %]
+</tbody>
+
+<tbody class="expert_fields">
+ [% IF user.in_group('editbugs', product.id) %]
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.dependson editable = 1
+ %]
+ <td>
+ <input name="dependson" accesskey="d" value="[% dependson FILTER html %]" size="30">
+ </td>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.blocked editable = 1
+ %]
+ <td>
+ <input name="blocked" accesskey="b" value="[% blocked FILTER html %]" size="30">
+ </td>
+ </tr>
+
+ [% IF use_keywords %]
+ <tr>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.keywords, editable = 1,
+ value = keywords, desc_url = "describekeywords.cgi",
+ value_span = 3
+ %]
+ </tr>
+ [% END %]
+
+ <tr>
+ <th>Status Whiteboard:</th>
+ <td colspan="3" class="field_value">
+ <input id="status_whiteboard" name="status_whiteboard" size="70"
+ value="[% status_whiteboard FILTER html %]" class="text_input">
+ </td>
+ </tr>
+ [% END %]
+
+ [% IF user.is_timetracker %]
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.estimated_time editable = 1
+ %]
+ <td>
+ <input name="estimated_time" size="6" maxlength="6" value="[% estimated_time FILTER html %]">
+ </td>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.deadline, value = deadline, editable = 1
+ %]
+ </tr>
+ [% END %]
+</tbody>
+
+<tbody>
+[%# non-tracking flags custom fields %]
+[% FOREACH field = Bugzilla.active_custom_fields(product=>product,type=>1) %]
+ [% NEXT IF field.type == constants.FIELD_TYPE_EXTENSION %]
+ [% NEXT UNLESS field.enter_bug %]
+ [%# crash-signature gets custom handling %]
+ [% IF field.name == 'cf_crash_signature' %]
+ [% show_crash_signature = 1 %]
+ [% NEXT %]
+ [% END %]
+ [% SET value = ${field.name}.defined ? ${field.name} : "" %]
+ <tr [% 'class="expert_fields"' IF !field.is_mandatory %]>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = field, value = value, editable = 1,
+ value_span = 3 %]
+ </tr>
+[% END %]
+</tbody>
+
+[%# crash-signature handling %]
+[% IF show_crash_signature %]
+<tbody class="expert_fields">
+ <tr>
+ <th id="field_label_cf_crash_signature" class="field_label">
+ <label for="cf_crash_signature"> Crash Signature: </label>
+ </th>
+ <td colspan="3">
+ <span id="cf_crash_signature_container">
+ <span id="cf_crash_signature_nonedit_display"><i>None</i></span>
+ (<a id="cf_crash_signature_action" href="#">edit</a>)
+ </span>
+ <span id="cf_crash_signature_input">
+ <textarea id="cf_crash_signature" name="cf_crash_signature" rows="4" cols="60"
+ >[% cf_crash_signature FILTER html %]</textarea>
+ </span>
+ </td>
+ </tr>
+</tbody>
+[% END %]
+
+[% old_tracking_flags = [] %]
+[% old_project_flags = [] %]
+[% FOREACH field = Bugzilla.active_custom_fields(product=>product,type=>2) %]
+ [% NEXT IF field.type == constants.FIELD_TYPE_EXTENSION %]
+ [% NEXT UNLESS field.enter_bug %]
+ [% IF cf_is_project_flag(field.name) %]
+ [% old_project_flags.push(field) %]
+ [% ELSE %]
+ [% old_tracking_flags.push(field) %]
+ [% END %]
+[% END %]
+
+[% display_flags = 0 %]
+[% any_flags_requesteeble = 0 %]
+[% FOREACH flag_type = product.flag_types.bug %]
+ [% display_flags = 1 %]
+ [% SET any_flags_requesteeble = 1 IF flag_type.is_requestable && flag_type.is_requesteeble %]
+ [% LAST IF display_flags && any_flags_requesteeable %]
+[% END %]
+
+[% IF old_project_flags.size || old_tracking_flags.size || display_flags %]
+ <tbody class="expert_fields">
+ <tr>
+ <th>Flags:</th>
+ <td colspan="3">
+ <div id="bug_flags_false" class="bz_default_hidden">
+ <input type="button" value="Set [% terms.bug FILTER html %] flags" onClick="handleWantsBugFlags(true)">
+ </div>
+
+ <div id="bug_flags_true">
+ <input type="button" id="btn_no_bug_flags" value="Don't set [% terms.bug %] flags"
+ class="bz_default_hidden" onClick="handleWantsBugFlags(false)">
+
+ <fieldset>
+ <legend>Set [% terms.bug %] flags</legend>
+
+ <table cellpadding="0" cellspacing="0">
+ <tr>
+ [% IF old_tracking_flags.size %]
+ <td [% IF project_flags.size %]rowspan="2"[% END %]>
+ <table class="tracking_flags">
+ <tr>
+ <th colspan="2" style="text-align:left">Tracking Flags:</th>
+ </tr>
+ [% FOREACH field = old_tracking_flags %]
+ [% SET value = ${field.name}.defined ? ${field.name} : "" %]
+ <tr>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default
+ field = field
+ value = value
+ editable = 1
+ value_span = 3
+ %]
+ </tr>
+ [% END %]
+ [% Hook.process('tracking_flags_end') %]
+ </table>
+ </td>
+ [% END %]
+ [% IF old_project_flags.size %]
+ <td>
+ <table class="tracking_flags">
+ <tr>
+ <th colspan="2" style="text-align:left">Project Flags:</th>
+ </tr>
+ [% FOREACH field = old_project_flags %]
+ [% SET value = ${field.name}.defined ? ${field.name} : "" %]
+ <tr>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default
+ field = field
+ value = value
+ editable = 1
+ value_span = 3
+ %]
+ </tr>
+ [% END %]
+ [% Hook.process('project_flags_end') %]
+ </table>
+ </td>
+ </tr>
+ <tr>
+ [% END %]
+ [% IF display_flags %]
+ <td>
+ [% PROCESS "flag/list.html.tmpl" flag_types = product.flag_types.bug
+ any_flags_requesteeble = any_flags_requesteeble
+ flag_table_id = "bug_flags"
+ %]
+ </td>
+ [% END %]
+ </tr>
+ [% Hook.process('bug_flags_end') %]
+ </table>
+ </fieldset>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+[% END %]
+
+<tbody>
+ [%# Form controls for entering additional data about the bug being created. %]
+ [% Hook.process("form") %]
+
+ <tr>
+ <th>&nbsp;</th>
+ <td colspan="3">
+ <input type="submit" id="commit" value="Submit [% terms.Bug %]">
+ &nbsp;&nbsp;&nbsp;&nbsp;
+ <input type="submit" name="maketemplate" id="maketemplate"
+ value="Remember values as bookmarkable template"
+ onclick="bz_no_validate_enter_bug=true" class="expert_fields">
+ </td>
+ </tr>
+</tbody>
+ [%# "status whiteboard" and "qa contact" are the longest labels
+ # add them here to avoid shifting the page when toggling advanced fields %]
+ <tr>
+ <th class="hidden_text">Status Whiteboard:</th>
+ <td>&nbsp;</td>
+ <th class="hidden_text">QA Contact:</th>
+ </tr>
+ </table>
+ <input type="hidden" name="form_name" value="enter_bug">
+</form>
+
+[%# Links or content with more information about the bug being created. %]
+[% Hook.process("end") %]
+
+<div id="guided">
+ <a id="guided_img" href="enter_bug.cgi?format=guided&amp;product=[% product.name FILTER uri %]"><img
+ src="extensions/BMO/web/images/guided.png" width="16" height="16" border="0" align="absmiddle"></a>
+ <a id="guided_link" href="enter_bug.cgi?format=guided&amp;product=[% product.name FILTER uri %]"
+ >Switch to the [% terms.Bugzilla %] Helper</a>
+</div>
+
+[% PROCESS global/footer.html.tmpl %]
+
+[%############################################################################%]
+[%# Block for SELECT fields #%]
+[%############################################################################%]
+
+[% BLOCK select %]
+
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = field editable = 1
+ %]
+ <td>
+ <select name="[% field.name FILTER html %]"
+ id="[% field.name FILTER html %]">
+ [%- FOREACH x = ${field.name} %]
+ [% NEXT IF NOT x.is_active %]
+ <option value="[% x.name FILTER html %]"
+ [% " selected=\"selected\"" IF x.name == default.${field.name} %]>
+ [% display_value(field.name, x.name) FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ </td>
+[% END %]
+
+[% BLOCK build_userlist %]
+ [% user_found = 0 %]
+ [% default_login = default_user.login %]
+ [% RETURN UNLESS default_login %]
+
+ [% FOREACH user = userlist %]
+ [% IF user.login == default_login %]
+ [% user_found = 1 %]
+ [% LAST %]
+ [% END %]
+ [% END %]
+
+ [% userlist.push({login => default_login,
+ identity => default_user.identity,
+ visible => 1})
+ UNLESS user_found %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/bug/create/created-fxos-betaprogram.html.tmpl b/extensions/BMO/template/en/default/bug/create/created-fxos-betaprogram.html.tmpl
new file mode 100644
index 000000000..145c976cd
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/created-fxos-betaprogram.html.tmpl
@@ -0,0 +1,30 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Firefox OS Beta Bug Submission"
+%]
+
+<h1>Thank you!</h1>
+
+<p>
+ Thank you for submitting feedback about Firefox OS.
+</p>
+
+<p>
+ We'll link any [% terms.bugs %] we file (or are already filed) as a result of
+ this feedback to this report so you can be notified about their progress.
+</p>
+
+<p style="font-size: x-small">
+ Reference: <a href="show_bug.cgi?id=[% id FILTER uri %]">#[% id FILTER html %]</a>
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/custom_forms.none.tmpl b/extensions/BMO/template/en/default/bug/create/custom_forms.none.tmpl
new file mode 100644
index 000000000..25af4fa47
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/custom_forms.none.tmpl
@@ -0,0 +1,173 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%# link => url (can be relative to bugzilla.mozilla.org, or full url)
+ # title => visible title
+ # group => optional group name, if present the form won't be show to
+ # users not in this group
+ # hide => optional boolean, if true the form will not be shown on
+ # enter_bug (but will be visible on the custom forms list)
+ #%]
+
+[%
+custom_forms = {
+ "mozilla.org" => [
+ {
+ link => "form.moz.project.review",
+ title => "Mozilla Project Review",
+ group => "mozilla-employee-confidential",
+ },
+ {
+ link => "form.trademark",
+ title => "Trademark Usage Requests",
+ },
+ {
+ link => "form.gear",
+ title => "Mozilla Gear Request",
+ group => "mozilla-employee-confidential",
+ },
+ {
+ link => "form.poweredby",
+ title => "Powered by Mozilla Logo Requests",
+ },
+ {
+ link => "form.mozlist",
+ title => "Mozilla Discussion Forum Requests",
+ group => "mozilla-employee-confidential",
+ },
+ ],
+ "Marketing" => [
+ {
+ link => "form.user.engagement",
+ title => "User Engagement Initiation Form",
+ group => "mozilla-employee-confidential",
+ },
+ {
+ link => "form.gear",
+ title => "Mozilla Gear Request",
+ group => "mozilla-employee-confidential",
+ },
+ {
+ link => "form.creative",
+ title => "Brand Engagement Initiation Form",
+ group => "mozilla-employee-confidential",
+ },
+ {
+ link => "form.poweredby",
+ title => "Powered by Mozilla Logo Requests",
+ },
+ ],
+ "Finance" => [
+ {
+ link => "form.finance",
+ title => "Finance Request",
+ group => "mozilla-employee-confidential",
+ },
+ ],
+ "Privacy" => [
+ {
+ link => "form.privacy.data",
+ title => "Privacy - Data Release Proposal",
+ group => "mozilla-employee-confidential",
+ },
+ ],
+ "Mozilla PR" => [
+ {
+ link => "form.mozpr",
+ title => "PR Project Form",
+ group => "pr-private",
+ },
+ ],
+ "Infrastructure & Operations" => [
+ {
+ link => "form.itrequest",
+ title => "IT Request Form",
+ group => "mozilla-employee-confidential",
+ },
+ {
+ link => "form.mozlist",
+ title => "Mozilla Discussion Forum Requests",
+ group => "mozilla-employee-confidential",
+ },
+ ],
+ "Tech Evangelism" => [
+ {
+ link => "form.mobile.compat",
+ title => "Mobile Web Compatibility Problem",
+ },
+ ],
+ "Air Mozilla" => [
+ {
+ link => "https://air.mozilla.org/requests/",
+ title => "Air Mozilla/Brown Bag Request",
+ group => "mozilla-employee-confidential",
+ },
+ ],
+ "Websites" => [
+ {
+ link => "form.web.bounty",
+ title => "Web Bounty Form",
+ },
+ ],
+ "Firefox OS" => [
+ {
+ link => "form.fxos.feature",
+ title => "Firefox OS Feature Request Form",
+ },
+ {
+ link => "form.fxos.mcts.waiver",
+ title => "Firefox OS MCTS Waiver Form",
+ },
+ {
+ link => "form.fxos.partner",
+ title => "Firefox OS Partner Bug Submission",
+ hide => 1,
+ },
+ {
+ link => "form.fxos.preload.app",
+ title => "Firefox OS Pre-load App",
+ hide => 1,
+ },
+ {
+ link => "form.fxos.betaprogram",
+ title => "Firefox OS Beta Program Bug Submission",
+ hide => 1,
+ },
+ ],
+ "Testing" => [
+ {
+ link => "form.automative",
+ title => "Automation Request Form",
+ },
+ ],
+ "Developer Engagement" => [
+ {
+ link => "form.dev.engagement.event",
+ title => "Developer Events Request Form",
+ },
+ ],
+ "Mozilla Developer Network" => [
+ {
+ link => "form.mdn",
+ title => "Mozilla Developer Network Feedback",
+ },
+ ],
+ "Internet Public Policy" => [
+ {
+ link => "form.ipp",
+ title => "Internet Public Policy Issue",
+ },
+ ],
+ "Marketplace" => [
+ {
+ link => "form.fxos.preload.app",
+ title => "Firefox OS Pre-load App",
+ },
+ ],
+}
+%]
diff --git a/extensions/BMO/template/en/default/bug/create/user-message.html.tmpl b/extensions/BMO/template/en/default/bug/create/user-message.html.tmpl
new file mode 100644
index 000000000..52014ae15
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/user-message.html.tmpl
@@ -0,0 +1,49 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+<p>
+ [% UNLESS cloned_bug_id %]
+ Consider using the
+ <a href="enter_bug.cgi?product=[% product.name FILTER html %]&amp;format=guided"
+ ><img src="extensions/BMO/web/images/guided.png" width="16" height="16" align="absmiddle" border="0">
+ [%+ terms.Bugzilla %] Helper</a> instead of this form.
+ [% END +%]
+ Before reporting a [% terms.bug %], make sure you've read our
+ <a href="http://www.mozilla.org/quality/bug-writing-guidelines.html">
+ [% terms.bug %] writing guidelines</a> and double checked that your [% terms.bug %] hasn't already
+ been reported. Consult our list of <a href="https://bugzilla.mozilla.org/duplicates.cgi">
+ most frequently reported [% terms.bugs %]</a> and <a href="https://bugzilla.mozilla.org/query.cgi">
+ search through descriptions</a> of previously reported [% terms.bugs %].
+</p>
+
+[%
+ PROCESS bug/create/custom_forms.none.tmpl;
+ visible_forms = [];
+ FOREACH form = custom_forms.${product.name};
+ NEXT IF form.hide;
+ NEXT IF form.group && !user.in_group(form.group);
+ visible_forms.push(form);
+ END;
+ RETURN UNLESS visible_forms.size;
+%]
+
+<div id="custom_form_list">
+ <img src="extensions/BMO/web/images/notice.png" width="48" height="48" id="custom_form_list_image">
+ <div id="custom_form_list_text">
+ This product has task-specific [% terms.bug %] forms that should be used if
+ appropriate:
+
+ <ul>
+ [% FOREACH form = visible_forms.sort("title") %]
+ <li><a href="[% form.link FILTER none %]">[% form.title FILTER html %]</a></li>
+ [% END %]
+ </ul>
+ </div>
+</div>
diff --git a/extensions/BMO/template/en/default/email/bugmail.html.tmpl b/extensions/BMO/template/en/default/email/bugmail.html.tmpl
new file mode 100644
index 000000000..7a628ec7f
--- /dev/null
+++ b/extensions/BMO/template/en/default/email/bugmail.html.tmpl
@@ -0,0 +1,204 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+[% PROCESS "global/reason-descs.none.tmpl" %]
+
+[% isnew = bug.lastdiffed ? 0 : 1 %]
+<html>
+<head>
+ <base href="[% urlbase FILTER html %]">
+ <style>
+ .comment { font-size: 100% !important; }
+ </style>
+</head>
+<body style="font-family: sans-serif">
+
+ [% IF !to_user.in_group('editbugs') %]
+ <div id="noreply" style="font-size: 90%; color: #666666">
+ Do not reply to this email. You can add comments to this [% terms.bug %] at
+ [%# using the bug_link filter here causes a weird template error %]
+ <a href="[% urlbase FILTER html %]show_bug.cgi?id=[% bug.id FILTER none %]">
+ [% urlbase FILTER html %]show_bug.cgi?id=[% bug.id FILTER none %]</a>
+ </div>
+ <br>
+ [% END %]
+
+ [% IF isnew %]
+ [% PROCESS generate_new %]
+ [% ELSE %]
+ [% PROCESS generate_diffs %]
+ [% END %]
+
+ [% IF new_comments.size %]
+ <div id="comments">
+ [% FOREACH comment = new_comments.reverse %]
+ <div>
+ [% IF comment.count %]
+ <b>
+ [% "Comment # ${comment.count}"
+ FILTER bug_link(bug, { comment_num => comment.count, full_url => 1 }) FILTER none %]
+ on [% "$terms.Bug $bug.id" FILTER bug_link(bug, { full_url => 1 }) FILTER none %]
+ from [% INCLUDE global/user.html.tmpl user = to_user, who = comment.author %]
+ at [% comment.creation_ts FILTER time(undef, to_user.timezone) %]
+ </b>
+ [% END %]
+ <pre class="comment" style="font-size: 120%">[% comment.body_full({ wrap => 1 }) FILTER quoteUrls(bug, comment) %]</pre>
+ </div>
+ [% END %]
+ </div>
+ <br>
+ [% END %]
+
+ [% IF referenced_bugs.size %]
+ <div id="referenced">
+ <hr style="border: 1px dashed #969696">
+ <b>Referenced [% terms.Bugs %]:</b>
+ <ul>
+ [% FOREACH ref = referenced_bugs %]
+ <li>
+ [<a href="[% urlbase FILTER html %]show_bug.cgi?id=[% ref.id FILTER none %]">
+ [% terms.Bug %]&nbsp;[% ref.id FILTER none %]</a>] [% ref.short_desc FILTER html %]
+ </li>
+ [% END %]
+ </ul>
+ </div>
+ <br>
+ [% END %]
+
+ <div id="bug_details" style="font-size: 90%; color: #666666">
+ <hr style="border: 1px dashed #969696">
+ Product/Component: [% bug.product FILTER html %] :: [% bug.component FILTER html %]<br>
+ [% "You are mentoring this " _ terms.bug IF bug.is_mentor(to_user) %]
+ </div>
+
+ [% seen_header = 0 %]
+ [% FOREACH flag = tracking_flags %]
+ [% NEXT IF bug.${flag.name} == "---" %]
+ [% IF !seen_header %]
+ [% seen_header = 1 %]
+ <div id="tracking" style="font-size: 90%; color: #666666">
+ <hr style="border: 1px dashed #969696">
+ <b>Tracking Flags:</b>
+ <ul>
+ [% END %]
+ <li>[% flag.description FILTER html %]:[% bug.${flag.name} FILTER html %]</li>
+ [% END %]
+ [% IF seen_header %]
+ </ul>
+ </div>
+ [% END %]
+
+ <div id="reason" style="font-size: 90%; color: #666666">
+ <hr style="border: 1px dashed #969696">
+ <b>You are receiving this mail because:</b>
+ <ul>
+ [% FOREACH reason = reasons %]
+ [% IF reason_descs.$reason %]
+ <li>[% reason_descs.$reason FILTER html %]</li>
+ [% END %]
+ [% END %]
+ [% FOREACH reason = reasons_watch %]
+ [% IF watch_reason_descs.$reason %]
+ <li>[% watch_reason_descs.$reason FILTER html %]</li>
+ [% END %]
+ [% END %]
+ </ul>
+ </div>
+
+ @@body-headers@@
+</body>
+</html>
+
+[% BLOCK generate_new %]
+ <div class="new">
+ <table border="0" cellspacing="0" cellpadding="3">
+ [% FOREACH change = diffs %]
+ [% PROCESS "email/bugmail-common.txt.tmpl" %]
+ <tr>
+ <td class="c1" style="border-right: 1px solid #969696" nowrap><b>[% field_label FILTER html %]</b></td>
+ <td class="c2">
+ [% IF change.field_name == "bug_id" %]
+ [% new_value FILTER bug_link(bug, full_url => 1) FILTER none %]
+ [% ELSE %]
+ [% new_value FILTER html %]
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ </table>
+ </div>
+ <br>
+[% END %]
+
+[% BLOCK generate_diffs %]
+ [% SET in_table = 0 %]
+ [% last_changer = 0 %]
+ [% FOREACH change = diffs %]
+ [% PROCESS "email/bugmail-common.txt.tmpl" %]
+ [% IF changer.id != last_changer %]
+ [% last_changer = changer.id %]
+ [% IF in_table == 1 %]
+ </table>
+ </div>
+ <br>
+ [% SET in_table = 0 %]
+ [% END %]
+
+ <b>
+ [% IF change.blocker %]
+ [% "${terms.Bug} ${bug.id}" FILTER bug_link(bug, full_url => 1) FILTER none %]
+ depends on
+ <a href="[% urlbase FILTER html %]show_bug.cgi?id=[% change.blocker.id FILTER none %]">
+ [% terms.Bug %]&nbsp;[% change.blocker.id FILTER none %]</a>,
+ which changed state.<br>
+ [% ELSE %]
+ [% INCLUDE global/user.html.tmpl user = to_user, who = change.who %] changed
+ [%+ "${terms.Bug} ${bug.id}" FILTER bug_link(bug, full_url => 1) FILTER none %]
+ at [% change.bug_when FILTER time(undef, to_user.timezone) %]</b>:<br>
+ [% END %]
+ </b>
+
+ [% IF in_table == 0 %]
+ <br>
+ <div class="diffs">
+ <table border="0" cellspacing="0" cellpadding="5">
+ [% SET in_table = 1 %]
+ [% END %]
+ <tr class="head">
+ <td class="c1" style="border-bottom: 1px solid #969696; border-right: 1px solid #969696"><b>What</b></td>
+ <td class="c2" style="border-bottom: 1px solid #969696; border-right: 1px solid #969696"><b>Removed</b></td>
+ <td class="c3" style="border-bottom: 1px solid #969696"><b>Added</b></td>
+ </tr>
+ [% END %]
+
+ <tr>
+ <td class="c1" style="border-right: 1px solid #969696" nowrap>[% field_label FILTER html %]</td>
+ <td class="c2" style="border-right: 1px solid #969696">
+ [% IF old_value %]
+ [% old_value FILTER html %]
+ [% ELSE %]
+ &nbsp;
+ [% END %]
+ </td>
+ <td>
+ [% IF new_value %]
+ [% new_value FILTER html %]
+ [% ELSE %]
+ &nbsp;
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ [% IF in_table %]
+ </table>
+ </div>
+ <br>
+ [% END %]
+[% END %]
+
diff --git a/extensions/BMO/template/en/default/email/bugmail.txt.tmpl b/extensions/BMO/template/en/default/email/bugmail.txt.tmpl
new file mode 100644
index 000000000..9cb020b02
--- /dev/null
+++ b/extensions/BMO/template/en/default/email/bugmail.txt.tmpl
@@ -0,0 +1,92 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+[% PROCESS "global/reason-descs.none.tmpl" %]
+
+[% isnew = bug.lastdiffed ? 0 : 1 %]
+
+[% IF !to_user.in_group('editbugs') %]
+Do not reply to this email. You can add comments to this [% terms.bug %] at
+[% END %]
+[%+ PROCESS generate_diffs -%]
+
+[% FOREACH comment = new_comments %]
+
+[%- IF comment.count %]
+--- Comment #[% comment.count %] from [% comment.author.identity %] [%+ comment.creation_ts FILTER time(undef, to_user.timezone) %] ---
+[% END %]
+[%+ comment.body_full({ is_bugmail => 1, wrap => 1 }) %]
+[% END %]
+[% IF referenced_bugs.size %]
+
+Referenced [% terms.Bugs %]:
+
+[% FOREACH ref = referenced_bugs %]
+[%+ urlbase %]show_bug.cgi?id=[% ref.id %]
+[%+ "[" _ terms.Bug _ " " _ ref.id _ "] " _ ref.short_desc FILTER wrap_comment(76) %]
+[% END %]
+[% END %]
+
+-- [%# Protect the trailing space of the signature marker %]
+Configure [% terms.bug %]mail: [% urlbase %]userprefs.cgi?tab=email
+
+-------------------------------
+Product/Component: [%+ bug.product +%] :: [%+ bug.component %]
+[%+ "You are mentoring this " _ terms.bug IF bug.is_mentor(to_user) %]
+
+[% seen_header = 0 %]
+[% FOREACH flag = tracking_flags %]
+ [% NEXT IF bug.${flag.name} == "---" %]
+ [% IF !seen_header %]
+ [% seen_header = 1 %]
+------- Tracking Flags: -------
+ [% END %]
+[%+ flag.description %]:[% bug.${flag.name} %]
+[% END %]
+
+------- You are receiving this mail because: -------
+[% SET reason_lines = [] %]
+[% FOREACH reason = reasons %]
+ [% reason_lines.push(reason_descs.$reason) IF reason_descs.$reason %]
+[% END %]
+[% FOREACH reason = reasons_watch %]
+ [% reason_lines.push(watch_reason_descs.$reason)
+ IF watch_reason_descs.$reason %]
+[% END %]
+[%+ reason_lines.join("\n") %]
+
+@@body-headers@@
+
+[% BLOCK generate_diffs %]
+ [% urlbase %]show_bug.cgi?id=[% bug.id %]
+
+[%+ last_changer = 0 %]
+ [% FOREACH change = diffs %]
+ [% IF !isnew && changer.id != last_changer %]
+ [% last_changer = changer.id %]
+ [% IF change.blocker %]
+ [% terms.Bug %] [%+ bug.id %] depends on [% terms.bug %] [%+ change.blocker.id %], which changed state.
+
+[%+ terms.Bug %] [%+ change.blocker.id %] Summary: [% change.blocker.short_desc %]
+[%+ urlbase %]show_bug.cgi?id=[% change.blocker.id %]
+ [% ELSE %]
+ [%~ changer.identity %] changed:
+ [% END %]
+
+ What |Removed |Added
+----------------------------------------------------------------------------
+[%+ END %][%# End of IF. This indentation is intentional! ~%]
+ [% PROCESS "email/bugmail-common.txt.tmpl"%]
+ [%~ IF isnew %]
+ [% format_columns(2, field_label _ ":", new_value) -%]
+ [% ELSE %]
+ [% format_columns(3, field_label, old_value, new_value) -%]
+ [% END %]
+ [% END -%]
+[% END %]
diff --git a/extensions/BMO/template/en/default/global/choose-product.html.tmpl b/extensions/BMO/template/en/default/global/choose-product.html.tmpl
new file mode 100644
index 000000000..eb7581d4e
--- /dev/null
+++ b/extensions/BMO/template/en/default/global/choose-product.html.tmpl
@@ -0,0 +1,228 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham <gerv@gerv.net>
+ #%]
+
+[%# INTERFACE:
+ # classifications: array of hashes, with an 'object' key representing a
+ # classification object and 'products' the list of
+ # product objects the user can enter bugs into.
+ # target: the script that displays this template.
+ # cloned_bug_id: ID of the bug being cloned.
+ # format: the desired format to display the target.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% style_urls = [ "extensions/BMO/web/styles/choose_product.css" ] %]
+
+[% IF target == "enter_bug.cgi" %]
+ [% title = "Enter $terms.Bug" %]
+ [% h2 = "Which product is affected by the problem you would like to report?" %]
+[% ELSIF target == "describecomponents.cgi" %]
+ [% title = "Browse" %]
+ [% h2 = "Which product would you like to have described?" %]
+[% END %]
+
+[% javascript_urls = [ "js/yui3/yui/yui-min.js",
+ "extensions/ProdCompSearch/web/js/prod_comp_search.js" ]
+%]
+[% onload = "document.getElementById('prod_comp_search').focus();" %]
+[% style_urls.push("extensions/ProdCompSearch/web/styles/prod_comp_search.css") %]
+
+[% DEFAULT title = "Choose a Product" %]
+[% PROCESS global/header.html.tmpl %]
+
+<div id="choose_product">
+
+<hr>
+<p>
+ Looking for technical support or help getting your site to work with Mozilla?
+ <a href="http://www.mozilla.org/support/">Visit the mozilla.org support page</a>
+ before filing [% terms.bugs %].
+</p>
+<hr>
+
+<h2>[% h2 FILTER html %]</h2>
+
+<div id="prod_comp_search_main">
+ [% PROCESS prodcompsearch/form.html.tmpl
+ input_label = "Find product:"
+ format = format
+ cloned_bug_id = cloned_bug_id
+ script_name = target %]
+</div>
+
+<h2>or choose from the following selections</h2>
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+[% SET classification = cgi.param('classification') %]
+[% IF NOT ((cgi.param("full")) OR (user.settings.product_chooser.value == 'full_product_chooser')) %]
+
+<table align="center" border="0" width="600" cellpadding="5" cellspacing="0">
+[% INCLUDE easyproduct
+ name="Core"
+ icon="component.png"
+%]
+[% INCLUDE easyproduct
+ name="Firefox"
+ icon="firefox.png"
+%]
+[% INCLUDE easyproduct
+ name="Firefox OS"
+ icon="firefox_os.png"
+%]
+[% INCLUDE easyproduct
+ name="Firefox for Android"
+ icon="firefox_android.png"
+%]
+[% INCLUDE easyproduct
+ name="Marketplace"
+ icon="marketplace.png"
+%]
+[% INCLUDE easyproduct
+ name="Webmaker"
+ icon="webmaker.png"
+%]
+[% INCLUDE easyproduct
+ name="Toolkit"
+ icon="component.png"
+%]
+[% INCLUDE easyproduct
+ name="Thunderbird"
+ icon="thunderbird.png"
+%]
+[% INCLUDE easyproduct
+ name="SeaMonkey"
+ icon="seamonkey.png"
+%]
+[% INCLUDE easyproduct
+ name="Mozilla Localizations"
+ icon="localization.png"
+%]
+[% INCLUDE easyproduct
+ name="Mozilla Services"
+ icon="sync.png"
+%]
+<tr>
+ <td><a href="[% target FILTER uri %]?full=1
+ [%- IF cloned_bug_id %]&amp;cloned_bug_id=[% cloned_bug_id FILTER uri %][% END -%]
+ [%- IF classification %]&amp;classification=[% classification FILTER uri %][% END -%]
+ [%- IF format %]&amp;format=[% format FILTER uri %][% END %]">
+ <img src="extensions/BMO/web/producticons/other.png" height="64" width="64" border="0"></a></td>
+ <td><h2 align="left" style="margin-bottom: 0px;"><a href="[% target FILTER uri %]?full=1
+ [%- IF cloned_bug_id %]&amp;cloned_bug_id=[% cloned_bug_id FILTER uri %][% END -%]
+ [%- IF classification %]&amp;classification=[% classification FILTER uri %][% END -%]
+ [%- IF format %]&amp;format=[% format FILTER uri %][% END %]">
+ Other Products</a></h2>
+ <p style="margin-top: 0px;">Other Mozilla products which aren't listed here</p>
+ </td>
+</tr>
+</table>
+[% ELSE %]
+
+<table>
+
+[% FOREACH c = classifications %]
+ [% IF c.object %]
+ <tr>
+ <td align="right"><h2>[% c.object.name FILTER html %]</h2></td>
+ <td><strong>[%+ c.object.description FILTER html_light %]</strong></td>
+ </tr>
+ [% END %]
+
+ [% FOREACH p = c.products %]
+ [% class = "" %]
+ [% has_entry_groups = 0 %]
+ [% FOREACH gid = p.group_controls.keys %]
+ [% IF p.group_controls.$gid.entry %]
+ [% has_entry_groups = 1 %]
+ [% class = class _ " group_$gid" %]
+ [% END %]
+ [% END %]
+ <tr class="[% "group_secure" IF has_entry_groups +%] [% class FILTER html %]"
+ [%- IF has_entry_groups %] title="This product requires one or more
+ group memberships in order to enter [% terms.bugs %] in it. You have them, but be
+ aware not everyone else does."[% END %]>
+ <th align="right" valign="top">
+ [% IF p.name == "Mozilla PR" AND target == "enter_bug.cgi" AND NOT format AND NOT cgi.param("debug") %]
+ <a href="[% target FILTER uri %]?product=[% p.name FILTER uri -%]
+ [%- IF cloned_bug_id %]&amp;cloned_bug_id=[% cloned_bug_id FILTER uri %][% END %]&amp;format=mozpr">
+ [% p.name FILTER html FILTER no_break %]</a>:&nbsp;
+ [% ELSE %]
+ <a href="[% target FILTER uri %]?product=[% p.name FILTER uri -%]
+ [%- IF cloned_bug_id %]&amp;cloned_bug_id=[% cloned_bug_id FILTER uri %][% END -%]
+ [%- IF format %]&amp;format=[% format FILTER uri %][% END %]">
+ [% p.name FILTER html FILTER no_break %]</a>:&nbsp;
+ [% END %]
+ </th>
+ <td valign="top">[% p.description FILTER html_light %]</td>
+ </tr>
+ [% END %]
+[% END %]
+
+</table>
+
+<br>
+[% IF target == "enter_bug.cgi" AND user.settings.product_chooser.value != 'full_product_chooser' %]
+<p>You can choose to get this screen by default when you click "New [% terms.Bug %]"
+by changing your <a href="userprefs.cgi?tab=settings">preferences</a>.</p>
+[% END %]
+[% END %]
+<br>
+
+</div>
+
+<div id="guided">
+ <a id="guided_img" href="enter_bug.cgi?format=guided"><img
+ src="extensions/BMO/web/images/guided.png" width="16" height="16" border="0" align="absmiddle"></a>
+ <a id="guided_link" href="enter_bug.cgi?format=guided"
+ >Switch to the [% terms.Bugzilla %] Helper</a>
+ | <a href="page.cgi?id=custom_forms.html">Custom [% terms.bug %] entry forms</a>
+</div>
+
+[% PROCESS global/footer.html.tmpl %]
+
+[%###########################################################################%]
+[%# Block for "easy" product sections #%]
+[%###########################################################################%]
+
+[% BLOCK easyproduct %]
+ [% FOREACH c = classifications %]
+ [% FOREACH p = c.products %]
+ [% IF p.name == name %]
+ <tr>
+ <td><a href="[% target FILTER uri %]?product=[% p.name FILTER uri %]
+ [%- IF cloned_bug_id %]&amp;cloned_bug_id=[% cloned_bug_id FILTER uri %][% END -%]
+ [%- IF format %]&amp;format=[% format FILTER uri %][% END %]">
+ <img src="extensions/BMO/web/producticons/[% icon FILTER uri %]" height="64" width="64" border="0"></a></td>
+ <td><h2 align="left" style="margin-bottom: 0px"><a href="[% target FILTER uri %]?product=[% p.name FILTER uri %]
+ [%- IF cloned_bug_id %]&amp;cloned_bug_id=[% cloned_bug_id FILTER uri %][% END -%]
+ [%- IF format %]&amp;format=[% format FILTER uri %][% END %]">
+ [% caption || name FILTER html FILTER no_break %]</a>:</h2>
+ [% IF p.description %]
+ <p style="margin-top: 0px;">[% p.description FILTER html_light %]</p>
+ [% END %]
+ </td>
+ </tr>
+ [% LAST %]
+ [% END %]
+ [% END %]
+ [% END %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/global/prod-comp-search.html.tmpl b/extensions/BMO/template/en/default/global/prod-comp-search.html.tmpl
new file mode 100644
index 000000000..2f1d67bec
--- /dev/null
+++ b/extensions/BMO/template/en/default/global/prod-comp-search.html.tmpl
@@ -0,0 +1,43 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<div id="prod_comp_search_main">
+ <div id="prod_comp_search_autocomplete">
+ <div id="prod_comp_search_label">
+ Type to find product and component by name or description:
+ <img id="prod_comp_throbber" src="extensions/BMO/web/images/throbber.gif"
+ class="hidden" width="16" height="11">
+ </div>
+ <input id="prod_comp_search" type="text" size="60">
+ <div id="prod_comp_search_autocomplete_container"></div>
+ </div>
+</div>
+<script type="text/javascript">
+ if(typeof(YAHOO.bugzilla.prodCompSearch) !== 'undefined'
+ && YAHOO.bugzilla.prodCompSearch != null)
+ {
+ YAHOO.bugzilla.prodCompSearch.init(
+ "prod_comp_search",
+ "prod_comp_search_autocomplete_container",
+ "[% format FILTER js %]",
+ "[% cloned_bug_id FILTER js %]");
+ [% IF target == "describecomponents.cgi" %]
+ YAHOO.bugzilla.prodCompSearch.autoComplete.itemSelectEvent.subscribe(function (e, args) {
+ var oData = args[2];
+ var url = "describecomponents.cgi?product=" + encodeURIComponent(oData[0]) +
+ "&component=" + encodeURIComponent(oData[1]) +
+ "#" + encodeURIComponent(oData[1]);
+ var format = YAHOO.bugzilla.prodCompSearch.format;
+ if (format) {
+ url += "&format=" + encodeURIComponent(format);
+ }
+ window.location.href = url;
+ });
+ [% END %]
+ }
+</script>
diff --git a/extensions/BMO/template/en/default/global/redirect.html.tmpl b/extensions/BMO/template/en/default/global/redirect.html.tmpl
new file mode 100644
index 000000000..67561d8fa
--- /dev/null
+++ b/extensions/BMO/template/en/default/global/redirect.html.tmpl
@@ -0,0 +1,25 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<!doctype html>
+<html>
+<head>
+ <title>Moved</title>
+ <meta http-equiv="refresh" content="0;URL=[% url FILTER none %]">
+ <style>
+ body {
+ font-family: sans-serif;
+ font-size: small;
+ background: url('extensions/BMO/web/images/background.png') repeat-x;
+ }
+ </style>
+</head>
+<body>
+ Redirecting to <a href="[% url FILTER none %]">[% url FILTER html %]</a>
+</body>
+</html>
diff --git a/extensions/BMO/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl b/extensions/BMO/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl
new file mode 100644
index 000000000..39f063464
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF panel.name == "groupsecurity" %]
+ [% panel.param_descs.delete_comments_group =
+ 'The name of the group of users who can delete comments by using the "deleted" comment tag.'
+ %]
+[% END -%]
diff --git a/extensions/BMO/template/en/default/hook/attachment/createformcontents-mimetypes.html.tmpl b/extensions/BMO/template/en/default/hook/attachment/createformcontents-mimetypes.html.tmpl
new file mode 100644
index 000000000..3dc727b87
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/attachment/createformcontents-mimetypes.html.tmpl
@@ -0,0 +1,2 @@
+[% mimetypes.push({type => "image/svg+xml", desc => "SVG image"}) %]
+[% mimetypes.push({type => "application/vnd.mozilla.xul+xml", desc => "XUL"}) %] \ No newline at end of file
diff --git a/extensions/BMO/template/en/default/hook/attachment/createformcontents-patch_notes.html.tmpl b/extensions/BMO/template/en/default/hook/attachment/createformcontents-patch_notes.html.tmpl
new file mode 100644
index 000000000..ea80fdc5e
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/attachment/createformcontents-patch_notes.html.tmpl
@@ -0,0 +1 @@
+<em>You can <a href="http://developer.mozilla.org/en/docs/Getting_your_patch_in_the_tree">read about the patch submission and approval process</a>.</em><br>
diff --git a/extensions/BMO/template/en/default/hook/bug/comments-a_comment-end.html.tmpl b/extensions/BMO/template/en/default/hook/bug/comments-a_comment-end.html.tmpl
new file mode 100644
index 000000000..caf7acca7
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/comments-a_comment-end.html.tmpl
@@ -0,0 +1,19 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF user.id && comment.author.login_name == 'tbplbot@gmail.com' %]
+ [% has_tbpl_comment = 1 %]
+ <script>
+ var id = [% count FILTER none %];
+ tbpl_comment_ids.push(id);
+ collapse_comment(
+ document.getElementById('comment_link_' + id),
+ document.getElementById('comment_text_' + id)
+ );
+ </script>
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/bug/comments-aftercomments.html.tmpl b/extensions/BMO/template/en/default/hook/bug/comments-aftercomments.html.tmpl
new file mode 100644
index 000000000..65bf77967
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/comments-aftercomments.html.tmpl
@@ -0,0 +1,69 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF has_tbpl_comment %]
+ [% expand_caption = 'Expand TinderboxPushlog Comments' %]
+ [% collapse_caption = 'Collapse TinderboxPushlog Comments' %]
+ [% show_caption = 'Show TinderboxPushlog Comments' %]
+ [% hide_caption = 'Hide TinderboxPushlog Comments' %]
+ <script>
+ YAHOO.util.Event.onDOMReady(function () {
+ var ul = document.getElementsByClassName('bz_collapse_expand_comments');
+ if (ul.length == 0)
+ return;
+
+ var li = document.createElement('li');
+ var a = document.createElement('a');
+ Dom.setAttribute(a, 'href', 'javascript:void(0)');
+ Dom.setAttribute(a, 'id', 'tbpl_toggle_collapse');
+ a.innerHTML = '[% expand_caption FILTER js %]';
+ YAHOO.util.Event.on(a, 'click', function() {
+ var a = document.getElementById('tbpl_toggle_collapse');
+ var do_expand = a.innerHTML == '[% expand_caption FILTER js %]';
+ for (var i = 0, n = tbpl_comment_ids.length; i < n; i++) {
+ var id = tbpl_comment_ids[i];
+ var link = document.getElementById('comment_link_' + id);
+ var text = document.getElementById('comment_text_' + id);
+ if (do_expand) {
+ expand_comment(link, text);
+ } else {
+ collapse_comment(link, text);
+ }
+ }
+ a.innerHTML = do_expand
+ ? '[% collapse_caption FILTER js %]'
+ : '[% expand_caption FILTER js %]';
+ });
+ li.appendChild(a);
+ ul[0].appendChild(li);
+
+ li = document.createElement('li');
+ a = document.createElement('a');
+ Dom.setAttribute(a, 'href', 'javascript:void(0)');
+ Dom.setAttribute(a, 'id', 'tbpl_toggle_visible');
+ a.innerHTML = '[% hide_caption FILTER js %]';
+ YAHOO.util.Event.on(a, 'click', function() {
+ var a = document.getElementById('tbpl_toggle_visible');
+ var do_show = a.innerHTML == '[% show_caption FILTER js %]';
+ for (var i = 0, n = tbpl_comment_ids.length; i < n; i++) {
+ var id = tbpl_comment_ids[i];
+ if (do_show) {
+ Dom.removeClass('c' + id, 'bz_default_hidden');
+ } else {
+ Dom.addClass('c' + id, 'bz_default_hidden');
+ }
+ }
+ a.innerHTML = do_show
+ ? '[% hide_caption FILTER js %]'
+ : '[% show_caption FILTER js %]';
+ });
+ li.appendChild(a);
+ ul[0].appendChild(li);
+ });
+ </script>
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/bug/comments-comment_banner.html.tmpl b/extensions/BMO/template/en/default/hook/bug/comments-comment_banner.html.tmpl
new file mode 100644
index 000000000..2ae367456
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/comments-comment_banner.html.tmpl
@@ -0,0 +1,13 @@
+[%# *** Disclaimer for Legal bugs *** %]
+[% IF bug.product == "Legal" %]
+ <div id="legal_disclaimer">
+ The material and information contained herein is Confidential and
+ subject to Attorney-Client Privilege and Work Product Doctrine.
+ </div>
+[% END %]
+
+[%# Needed for collapsing TinderboxPushlog comments %]
+[% has_tbpl_comment = 0 %]
+<script>
+ var tbpl_comment_ids = new Array();
+</script>
diff --git a/extensions/BMO/template/en/default/hook/bug/create/create-after_custom_fields.html.tmpl b/extensions/BMO/template/en/default/hook/bug/create/create-after_custom_fields.html.tmpl
new file mode 100644
index 000000000..47d86bd58
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/create/create-after_custom_fields.html.tmpl
@@ -0,0 +1,30 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%# crash-signature handling %]
+[% IF show_crash_signature %]
+ <tbody class="expert_fields">
+ <tr>
+ <th id="field_label_cf_crash_signature" class="field_label">
+ <label for="cf_crash_signature"> Crash Signature: </label>
+ </th>
+ <td colspan="3">
+ <span id="cf_crash_signature_container">
+ <span id="cf_crash_signature_nonedit_display"><i>None</i></span>
+ (<a id="cf_crash_signature_action" href="#">edit</a>)
+ </span>
+ <span id="cf_crash_signature_input">
+ <textarea id="cf_crash_signature" name="cf_crash_signature" rows="4" cols="60"
+ >[% cf_crash_signature FILTER html %]</textarea>
+ </span>
+ </td>
+ </tr>
+ </tbody>
+[% END %]
+
+
diff --git a/extensions/BMO/template/en/default/hook/bug/create/create-custom_field.html.tmpl b/extensions/BMO/template/en/default/hook/bug/create/create-custom_field.html.tmpl
new file mode 100644
index 000000000..afbb2947c
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/create/create-custom_field.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%# crash-signature gets custom handling %]
+[% IF field.name == 'cf_crash_signature' %]
+ [% field.hidden = 1 %]
+ [% show_crash_signature = 1 %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/bug/create/create-end.html.tmpl b/extensions/BMO/template/en/default/hook/bug/create/create-end.html.tmpl
new file mode 100644
index 000000000..a152527ba
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/create/create-end.html.tmpl
@@ -0,0 +1,33 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS product.name == 'Bugzilla' &&
+ (user.in_group('mozilla-corporation') || user.in_group('mozilla-foundation')) %]
+
+<div id="bug_create_warning">
+ <div id="bug_create_warning_image">
+ <img src="extensions/BMO/web/images/sign_warning.png" width="32" height="32">
+ </div>
+ <div id="bug_create_warning_text">
+ <b>Mozilla employees</b><br>
+ This is <i>not</i> the place to request configuration, permission, or
+ account changes to this installation of [% terms.Bugzilla %] (bugzilla.mozilla.org).<br>
+ This includes, but is not limited to:
+ <ul>
+ <li>New or updates to products and components</li>
+ <li>Changes to the values of existing fields (versions, milestones, etc)</li>
+ </ul>
+ Instead, please file such changes under
+ <a href="enter_bug.cgi?product=bugzilla.mozilla.org;component=Administration">
+ <b>
+ the Administration component in the bugzilla.mozilla.org
+ </b>
+ </a>
+ product.
+ </div>
+</div>
diff --git a/extensions/BMO/template/en/default/hook/bug/create/create-form.html.tmpl b/extensions/BMO/template/en/default/hook/bug/create/create-form.html.tmpl
new file mode 100644
index 000000000..562ebfbdd
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/create/create-form.html.tmpl
@@ -0,0 +1,47 @@
+ <tr>
+ <th>Security:</th>
+ <td colspan="3">
+ [% IF user.in_group(product.default_security_group) %]
+ [% PROCESS group_checkbox
+ name = product.default_security_group
+ desc = "Restrict access to this " _ terms.bug _ " to members of " _
+ "the \"" _ product.default_security_group_obj.description _ "\" group."
+ %]
+ [% ELSE %]
+ [% PROCESS group_checkbox
+ name = product.default_security_group
+ desc = "Many users could be harmed by this security problem: " _
+ "it should be kept hidden from the public until it is resolved."
+ %]
+ [% END %]
+ [% IF user.in_group('partner-confidential-visible') %]
+ [% PROCESS group_checkbox
+ name = 'partner-confidential'
+ desc = "Restrict the visibility of this " _ terms.bug _ " to " _
+ "the assignee, QA contact, and CC list only."
+ %]
+ [% END %]
+ [% IF user.in_group('mozilla-employee-confidential-visible')
+ && !user.in_group('mozilla-employee-confidential') %]
+ [% PROCESS group_checkbox
+ name = 'mozilla-employee-confidential'
+ desc = "Restrict the visibility of this " _ terms.bug _ " to " _
+ "Mozilla Employees and Contractors only."
+ %]
+ [% END %]
+ <br>
+ </td>
+ </tr>
+
+[% BLOCK group_checkbox %]
+ <input type="checkbox" name="groups"
+ value="[% name FILTER none %]" id="group_[% name FILTER html %]"
+ [% FOREACH group = product.groups_available %]
+ [% IF group.name == name %]
+ [% ' checked="checked"' IF default.groups.contains(group.name) OR group.is_default %]
+ [% LAST %]
+ [% END %]
+ [% END %]
+ >
+ <label for="group_[% name FILTER html %]">[% desc FILTER html %]</label><br>
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/bug/edit-after_importance.html.tmpl b/extensions/BMO/template/en/default/hook/bug/edit-after_importance.html.tmpl
new file mode 100644
index 000000000..d7c0d58a8
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/edit-after_importance.html.tmpl
@@ -0,0 +1,76 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%# Display product and component descriptions after their respective fields %]
+<script type="text/javascript">
+ var Event = YAHOO.util.Event;
+ var Dom = YAHOO.util.Dom;
+ Event.onDOMReady(function() {
+ // Display product description if user requests it
+ var prod_desc = '[% bug.product_obj.description FILTER html_light FILTER js %]';
+ if (prod_desc) {
+ var field_container = Dom.get('field_container_product');
+ var toggle_container = document.createElement('span');
+ Dom.setAttribute(toggle_container, 'id', 'toggle_prod_desc');
+ toggle_container.appendChild(document.createTextNode(' ('));
+ var toggle_link = document.createElement('a');
+ Dom.setAttribute(toggle_link, 'id', 'toggle_prod_desc_link');
+ Dom.setAttribute(toggle_link, 'href', 'javascript:void(0);')
+ toggle_link.appendChild(document.createTextNode('show info'));
+ toggle_container.appendChild(toggle_link);
+ toggle_container.appendChild(document.createTextNode(')'));
+ field_container.appendChild(toggle_container);
+ var desc_container = document.createElement('div');
+ Dom.setAttribute(desc_container, 'id', 'prod_desc_container');
+ Dom.addClass(desc_container, 'bz_default_hidden');
+ desc_container.innerHTML = prod_desc;
+ field_container.appendChild(desc_container);
+ Event.addListener(toggle_link, 'click', function () {
+ if (Dom.hasClass('prod_desc_container', 'bz_default_hidden')) {
+ Dom.get('toggle_prod_desc_link').innerHTML = 'hide info';
+ Dom.removeClass('prod_desc_container', 'bz_default_hidden');
+ }
+ else {
+ Dom.get('toggle_prod_desc_link').innerHTML = 'show info';
+ Dom.addClass('prod_desc_container', 'bz_default_hidden');
+ }
+ });
+ }
+
+ // Display component description if user requests it
+ var comp_desc = '[% bug.component_obj.description FILTER html_light FILTER js %]';
+ if (comp_desc) {
+ var field_container = Dom.get('field_container_component');
+ var toggle_container = document.createElement('span');
+ Dom.setAttribute(toggle_container, 'id', 'toggle_comp_desc');
+ toggle_container.appendChild(document.createTextNode(' ('));
+ var toggle_link = document.createElement('a');
+ Dom.setAttribute(toggle_link, 'id', 'toggle_comp_desc_link');
+ Dom.setAttribute(toggle_link, 'href', 'javascript:void(0);')
+ toggle_link.appendChild(document.createTextNode('show info'));
+ toggle_container.appendChild(toggle_link);
+ toggle_container.appendChild(document.createTextNode(')'));
+ field_container.appendChild(toggle_container);
+ var desc_container = document.createElement('div');
+ Dom.setAttribute(desc_container, 'id', 'comp_desc_container');
+ Dom.addClass(desc_container, 'bz_default_hidden');
+ desc_container.innerHTML = comp_desc;
+ field_container.appendChild(desc_container);
+ Event.addListener(toggle_link, 'click', function () {
+ if (Dom.hasClass('comp_desc_container', 'bz_default_hidden')) {
+ Dom.get('toggle_comp_desc_link').innerHTML = 'hide info';
+ Dom.removeClass('comp_desc_container', 'bz_default_hidden');
+ }
+ else {
+ Dom.get('toggle_comp_desc_link').innerHTML = 'show info';
+ Dom.addClass('comp_desc_container', 'bz_default_hidden');
+ }
+ });
+ }
+ });
+</script>
diff --git a/extensions/BMO/template/en/default/hook/bug/edit-before_restrict_visibility.html.tmpl b/extensions/BMO/template/en/default/hook/bug/edit-before_restrict_visibility.html.tmpl
new file mode 100644
index 000000000..880ab58f7
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/edit-before_restrict_visibility.html.tmpl
@@ -0,0 +1,25 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN IF
+ bug.in_group(bug.product_obj.default_security_group_obj)
+ || user.in_group(bug.product_obj.default_security_group)
+ || (user.id != bug.reporter.id && !user.in_group('editbugs'))
+ %]
+
+<div class="bz_group_visibility_section">
+ <input type="checkbox" name="groups"
+ value="[% bug.product_obj.default_security_group FILTER none %]"
+ id="group_[% bug.product_obj.default_security_group_obj.id FILTER html %]"
+ onchange="if (this.checked) document.getElementById('addselfcc').checked = true"
+ >
+ <label for="group_[% bug.product_obj.default_security_group_obj.id FILTER html %]"
+ title="This [% terms.bug %] is security sensitive and should be hidden from the public until it is resolved">
+ Restrict access to this [% terms.bug %]
+ </label>
+</div><br>
diff --git a/extensions/BMO/template/en/default/hook/bug/field-help-end.none.tmpl b/extensions/BMO/template/en/default/hook/bug/field-help-end.none.tmpl
new file mode 100644
index 000000000..dda75a9c6
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/field-help-end.none.tmpl
@@ -0,0 +1,96 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the BMO Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Dave Lawrence <dkl@mozilla.com>
+ #%]
+
+[% USE Bugzilla %]
+[% IF Bugzilla.request_cache.bmo_fields_page %]
+ [%
+ vars.help_html.priority =
+ "This field describes the importance and order in which $terms.abug
+ should be fixed compared to other ${terms.bugs}. This field is utilized
+ by the programmers/engineers to prioritize their work to be done where
+ P1 is considered the highest and P5 is the lowest."
+
+ vars.help_html.bug_severity =
+ "This field describes the impact of ${terms.abug}.
+ <table>
+ <tr>
+ <th>blocker</th>
+ <td>Blocks development and/or testing work</td>
+ </tr>
+ <tr>
+ <th>critical</th>
+ <td>crashes, loss of data, severe memory leak</td>
+ </tr>
+ <tr>
+ <th>major</th>
+ <td>major loss of function</td>
+ </tr>
+ <tr>
+ <th>normal</th>
+ <td>regular issue, some loss of functionality under specific circumstances</td>
+ </tr>
+ <tr>
+ <th>minor</th>
+ <td>minor loss of function, or other problem where easy
+ workaround is present</td>
+ </tr>
+ <tr>
+ <th>trivial</th>
+ <td>cosmetic problem like misspelled words or misaligned
+ text</td>
+ </tr>
+ <tr>
+ <th>enhancement</th>
+ <td>Request for enhancement</td>
+ </table>"
+
+ vars.help_html.rep_platform =
+ "This is the hardware platform against which the $terms.bug was reported.
+ Legal platforms include:
+ <ul>
+ <li>All (happens on all platforms; cross-platform ${terms.bug})</li>
+ <li>x86_64</li>
+ <li>ARM</li>
+ </ul>
+ <b>Note:</b> When searching, selecting the option
+ <em>All</em> does not
+ select $terms.bugs assigned against any platform. It merely selects
+ $terms.bugs that are marked as occurring on all platforms, i.e. are
+ designated <em>All</em>.",
+
+ vars.help_html.op_sys =
+ "This is the operating system against which the $terms.bug was
+ reported. Legal operating systems include:
+ <ul>
+ <li>All (happens on all operating systems; cross-platform ${terms.bug})</li>
+ <li>Windows 7</li>
+ <li>Mac OS X</li>
+ <li>Linux</li>
+ </ul>
+ Sometimes the operating system implies the platform, but not
+ always. For example, Linux can run on x86_64, ARM, and others.",
+
+ vars.help_html.assigned_to =
+ "This is the person in charge of resolving the ${terms.bug}. Every time
+ this field changes, the status changes to
+ <b>NEW</b> to make it
+ easy to see which new $terms.bugs have appeared on a person's list.</p>",
+ %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/bug/process/header-title.html.tmpl b/extensions/BMO/template/en/default/hook/bug/process/header-title.html.tmpl
new file mode 100644
index 000000000..a99b4f9f6
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/process/header-title.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% title = title.replace('^' _ terms.Bug _ ' ', '') %]
diff --git a/extensions/BMO/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/BMO/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..5ab189045
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,18 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% style_urls.push('extensions/BMO/web/styles/edit_bug.css') %]
+[% javascript_urls.push('extensions/BMO/web/js/edit_bug.js') %]
+[% title = "$bug.bug_id &ndash; " %]
+[% IF bug.alias != '' %]
+ [% title = title _ "($bug.alias) " %]
+[% END %]
+[% title = title _ filtered_desc %]
+[% javascript = javascript _
+ "document.title = document.title.replace(/^" _ terms.Bug _ " /, '');"
+%]
diff --git a/extensions/BMO/template/en/default/hook/global/field-descs-end.none.tmpl b/extensions/BMO/template/en/default/hook/global/field-descs-end.none.tmpl
new file mode 100644
index 000000000..8c543b35d
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/field-descs-end.none.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF in_template_var %]
+ [% vars.field_descs.cc_count = "CC Count" %]
+ [% vars.field_descs.dupe_count = "Duplicate Count" %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/global/footer-end.html.tmpl b/extensions/BMO/template/en/default/hook/global/footer-end.html.tmpl
new file mode 100644
index 000000000..f14b34acb
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/footer-end.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<div id="privacy-policy">
+ <a href="https://www.mozilla.org/privacy/websites/" target="_blank">Privacy Policy</a>
+</div>
diff --git a/extensions/BMO/template/en/default/hook/global/header-additional_header.html.tmpl b/extensions/BMO/template/en/default/hook/global/header-additional_header.html.tmpl
new file mode 100644
index 000000000..f1896dccc
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/header-additional_header.html.tmpl
@@ -0,0 +1,54 @@
+[%#
+ # The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the BMOHeader Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is Reed Loden.
+ # Portions created by the Initial Developer are Copyright (C) 2010 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Reed Loden <reed@reedloden.com>
+ #%]
+
+<link rel="shortcut icon" href="extensions/BMO/web/images/favicon.ico">
+[% IF bug %]
+<link id="shorturl" rev="canonical" href="https://bugzil.la/[% bug.bug_id FILTER uri %]">
+[% END %]
+
+[%# *** Bug List Navigation *** %]
+[% IF bug %]
+ [% SET my_search = user.recent_search_for(bug) %]
+ [% IF my_search %]
+ [% SET last_bug_list = my_search.bug_list %]
+ [% SET this_bug_idx = lsearch(last_bug_list, bug.id) %]
+ <link rel="Up" href="buglist.cgi?regetlastlist=
+ [%- my_search.id FILTER uri %]">
+ <link rel="First" href="show_bug.cgi?id=
+ [%- last_bug_list.first FILTER uri %]&amp;list_id=
+ [%- my_search.id FILTER uri %]">
+ <link rel="Last" href="show_bug.cgi?id=
+ [%- last_bug_list.last FILTER uri %]&amp;list_id=
+ [%- my_search.id FILTER uri %]">
+ [% IF this_bug_idx > 0 %]
+ [% prev_bug = this_bug_idx - 1 %]
+ <link rel="Prev" href="show_bug.cgi?id=
+ [%- last_bug_list.$prev_bug FILTER uri %]&amp;list_id=
+ [%- my_search.id FILTER uri %]">
+ [% END %]
+ [% IF this_bug_idx + 1 < last_bug_list.size %]
+ [% next_bug = this_bug_idx + 1 %]
+ <link rel="Next" href="show_bug.cgi?id=
+ [%- last_bug_list.$next_bug FILTER uri %]&amp;list_id=
+ [%- my_search.id FILTER uri %]">
+ [% END %]
+ [% END %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/global/header-start.html.tmpl b/extensions/BMO/template/en/default/hook/global/header-start.html.tmpl
new file mode 100644
index 000000000..2b2642148
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/header-start.html.tmpl
@@ -0,0 +1,41 @@
+[% IF !javascript_urls %]
+ [% javascript_urls = [] %]
+[% END %]
+
+[% IF template.name == 'list/list.html.tmpl' %]
+ [% javascript_urls.push('extensions/BMO/web/js/sorttable.js') %]
+[% END %]
+
+[% IF !bodyclasses %]
+ [% bodyclasses = [] %]
+[% END %]
+
+[%# Change the background/border for bugs/attachments in certain bug groups %]
+[% IF template.name == 'attachment/edit.html.tmpl'
+ || template.name == 'attachment/create.html.tmpl'
+ || template.name == 'attachment/diff-header.html.tmpl' %]
+ [% style_urls.push("skins/custom/bug_groups.css") %]
+
+ [% IF template.name == 'attachment/edit.html.tmpl'
+ || template.name == 'attachment/diff-header.html.tmpl' %]
+ [% IF bodyclasses == 'no_javascript' %]
+ [% bodyclasses = ['no_javascript'] %]
+ [% END %]
+ [% FOREACH group = attachment.bug.groups_in %]
+ [% bodyclasses.push("bz_group_$group.name") %]
+ [% END %]
+ [% END %]
+
+ [% IF template.name == 'attachment/create.html.tmpl' %]
+ [% FOREACH group = bug.groups_in %]
+ [% bodyclasses.push("bz_group_$group.name") %]
+ [% END %]
+ [% END %]
+[% END %]
+
+[%# BMO - add user context menu %]
+[% IF user.id %]
+ [% yui.push('container', 'menu') %]
+ [% style_urls.push('js/yui/assets/skins/sam/menu.css') %]
+ [% javascript_urls.push('extensions/BMO/web/js/edituser_menu.js') %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/global/messages-messages.html.tmpl b/extensions/BMO/template/en/default/hook/global/messages-messages.html.tmpl
new file mode 100644
index 000000000..0c90b97b9
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/messages-messages.html.tmpl
@@ -0,0 +1,5 @@
+[% IF message_tag == "employee_incident_creation_failed" %]
+ The [% terms.bug %] was created successfully, but the dependent
+ Employee Incident [% terms.bug %] creation failed. The error has
+ been logged and no further action is required at this time.
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/global/setting-descs-settings.none.tmpl b/extensions/BMO/template/en/default/hook/global/setting-descs-settings.none.tmpl
new file mode 100644
index 000000000..57e20108d
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/setting-descs-settings.none.tmpl
@@ -0,0 +1,14 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%
+ setting_descs.product_chooser = "Product chooser to use when entering bugs",
+ setting_descs.pretty_product_chooser = "Pretty chooser with common products and icons",
+ setting_descs.full_product_chooser = "Full chooser with all products",
+ setting_descs.headers_in_body = "Include X-Bugzilla- headers in BugMail body",
+%]
diff --git a/extensions/BMO/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl b/extensions/BMO/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
new file mode 100644
index 000000000..bf46ed895
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
@@ -0,0 +1,17 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF object == 'group_admins' %]
+ the group administrators report
+[% ELSIF object == 'email_queue' %]
+ the email queue status report
+[% ELSIF object == 'product_security' %]
+ the product security report
+[% ELSIF object == 'comments' %]
+ comments
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/global/user-error-error_message.html.tmpl b/extensions/BMO/template/en/default/hook/global/user-error-error_message.html.tmpl
new file mode 100644
index 000000000..c7fb31009
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/user-error-error_message.html.tmpl
@@ -0,0 +1,26 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == 'illegal_change' || error == 'illegal_change_deps' %]
+ <p>
+ If you are attempting to confirm an unconfirmed [% terms.bug %] or edit the
+ fields of a [% terms.bug %], <a href="page.cgi?id=get_permissions.html">find
+ out how to get the necessary permissions</a>.
+ </p>
+[% END %]
+
+[% IF error == 'entry_access_denied' && product == 'Legal' %]
+ <p>
+ Unfortunately, we need to keep [% terms.bugs %] in the Legal product
+ restricted to employees, in order to preserve attorney-client privilege and
+ protect confidentiality. Due to the way [% terms.Bugzilla %] works, this
+ means we can't let you file such [% terms.abug %] yourself. However, you
+ can contact Mozilla Legal through either legal-notices@mozilla.com or
+ trademarks@mozilla.com, as appropriate.
+ </p>
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..aaf23fff5
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,40 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the BMO Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Byron Jones <bjones@mozilla.com>
+ #%]
+
+[% IF error == "user_activity_missing_username" %]
+ [% title = "Missing Username" %]
+ You must provide at least one email address to report on.
+
+[% ELSIF error == "report_invalid_date" %]
+ [% title = "Invalid Date" %]
+ The date '[% date FILTER html %]' is invalid.
+
+[% ELSIF error == "report_invalid_parameter" %]
+ [% title = "Invalid Parameter" %]
+ The value for parameter [% name FILTER html %] is invalid.
+
+[% ELSIF error == "invalid_object" %]
+ Invalid [% object FILTER html %]: "[% value FILTER html %]"
+
+[% ELSIF error == "report_too_many_bugs" %]
+ [% title = "Too Many Bugs" %]
+ Too many [% terms.bugs %] matched your selection criteria.
+
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/global/user-error.html.tmpl/auth_failure/permissions.html.tmpl b/extensions/BMO/template/en/default/hook/global/user-error.html.tmpl/auth_failure/permissions.html.tmpl
new file mode 100644
index 000000000..346e02373
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/user-error.html.tmpl/auth_failure/permissions.html.tmpl
@@ -0,0 +1,29 @@
+<!-- 1.0@bugzilla.org -->
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham <gerv@gerv.net>
+ # Reed Loden <reed@reedloden.com>
+ #%]
+
+[% IF (group == "canconfirm" OR group == "editbugs") AND !reason %]
+ <p>
+ If you are attempting to confirm an unconfirmed [% terms.bug %] or edit the fields of a [% terms.bug %],
+ <a href="http://www.gerv.net/hacking/before-you-mail-gerv.html#bugzilla-permissions">find
+ out how to get the necessary permissions</a>.
+ </p>
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/global/variables-end.none.tmpl b/extensions/BMO/template/en/default/hook/global/variables-end.none.tmpl
new file mode 100644
index 000000000..89eef6fc4
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/variables-end.none.tmpl
@@ -0,0 +1,3 @@
+[%
+ terms.BugzillaTitle = "Bugzilla@Mozilla"
+%]
diff --git a/extensions/BMO/template/en/default/hook/index-additional_links.html.tmpl b/extensions/BMO/template/en/default/hook/index-additional_links.html.tmpl
new file mode 100644
index 000000000..cfc224fcd
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/index-additional_links.html.tmpl
@@ -0,0 +1,18 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<li>
+|
+<a href="page.cgi?id=etiquette.html">
+ [%- terms.Bugzilla %] Etiquette</a>
+</li>
+<li>
+|
+<a href="https://developer.mozilla.org/en/Bug_writing_guidelines">
+ [%- terms.Bug %] Writing Guidelines</a>
+</li>
diff --git a/extensions/BMO/template/en/default/hook/index-intro.html.tmpl b/extensions/BMO/template/en/default/hook/index-intro.html.tmpl
new file mode 100644
index 000000000..d81d91491
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/index-intro.html.tmpl
@@ -0,0 +1,2 @@
+<a id="get_help" class="bz_common_actions"
+ href="page.cgi?id=get_help.html"><span>Get Help</span></a> \ No newline at end of file
diff --git a/extensions/BMO/template/en/default/hook/list/list-links.html.tmpl b/extensions/BMO/template/en/default/hook/list/list-links.html.tmpl
new file mode 100644
index 000000000..fda2b43a9
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/list/list-links.html.tmpl
@@ -0,0 +1,15 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<a href="rest/bug?include_fields=id,summary,status&amp;
+ [% IF quicksearch ~%]
+ quicksearch=[% quicksearch FILTER uri %]
+ [% ELSE %]
+ [% cgi.canonicalise_query('list_id', 'query_format') FILTER none %]"
+ [% END %]"
+ title="Query as a REST API request">REST</a> |
diff --git a/extensions/BMO/template/en/default/hook/list/table-before_table.html.tmpl b/extensions/BMO/template/en/default/hook/list/table-before_table.html.tmpl
new file mode 100644
index 000000000..35d7cd3f3
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/list/table-before_table.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+[% abbrev.product.maxlength = 20 %]
+[% abbrev.component.maxlength = 20 %]
diff --git a/extensions/BMO/template/en/default/hook/pages/fields-resolution.html.tmpl b/extensions/BMO/template/en/default/hook/pages/fields-resolution.html.tmpl
new file mode 100644
index 000000000..4d12ab345
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/pages/fields-resolution.html.tmpl
@@ -0,0 +1,13 @@
+<dt>
+ [% display_value("resolution", "INCOMPLETE") FILTER html %]
+</dt>
+<dd>
+ The problem is vaguely described with no steps to reproduce,
+ or is a support request. The reporter should be directed to the
+ product's support page for help diagnosing the issue. If there
+ are only a few comments in the [% terms.bug %], it may be reopened only if
+ the original reporter provides more info, or confirms someone
+ else's steps to reproduce. If the [% terms.bug %] is long, when enough info
+ is provided a new [% terms.bug %] should be filed and the original [% terms.bug %]
+ marked as a duplicate of it.
+</dd>
diff --git a/extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl b/extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl
new file mode 100644
index 000000000..93f04c4fa
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl
@@ -0,0 +1,59 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<h2>Other Reports</h2>
+
+<ul>
+ <li>
+ <strong>
+ <a href="[% urlbase FILTER none %]page.cgi?id=user_activity.html">User Changes</a>
+ </strong> - Show changes made by an individual user.
+ </li>
+ <li>
+ <strong>
+ <a href="[% urlbase FILTER none %]page.cgi?id=triage_reports.html">Triage Report</a>
+ </strong> - Report on UNCONFIRMED [% terms.bugs %] to assist triage.
+ </li>
+ <li>
+ <strong>
+ <a href="[% urlbase FILTER none %]page.cgi?id=release_tracking_report.html">Release Tracking Report</a>
+ </strong> - For triaging release-train flag information.
+ </li>
+ [% IF user.in_group('editusers') || user.in_group('infrasec') %]
+ <li>
+ <strong>
+ <a href="[% urlbase FILTER none %]page.cgi?id=group_admins.html">Group Admins</a>
+ </strong> - Lists the administrators of each group.
+ </li>
+ <li>
+ <strong>
+ <a href="[% urlbase FILTER none %]page.cgi?id=group_membership.html">Group Membership Report</a>
+ </strong> - Lists the groups a user is a member of.
+ </li>
+ <li>
+ <strong>
+ <a href="[% urlbase FILTER none %]page.cgi?id=group_members.html">Group Members Report</a>
+ </strong> - Lists the users of groups.
+ </li>
+ [% END %]
+ [% IF user.in_group('admin') || user.in_group('infrasec') %]
+ <li>
+ <strong>
+ <a href="[% urlbase FILTER none %]page.cgi?id=product_security_report.html">Product Security Report</a>
+ </strong> - Show each product's default security group and visibility.
+ </li>
+ [% END %]
+ [% IF user.in_group('admin') || user.in_group('infra') %]
+ <li>
+ <strong>
+ <a href="[% urlbase FILTER none %]page.cgi?id=email_queue.html">Email Queue</a>
+ </strong> - TheSchwartz queue
+ </li>
+ [% END %]
+</ul>
+
diff --git a/template/en/default/global/help.html.tmpl b/extensions/BMO/template/en/default/list/list.microsummary.tmpl
index c0ff819ce..8925db8dd 100644
--- a/template/en/default/global/help.html.tmpl
+++ b/extensions/BMO/template/en/default/list/list.microsummary.tmpl
@@ -1,3 +1,4 @@
+[%# 1.0@bugzilla.org %]
[%# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
@@ -15,19 +16,14 @@
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
- # Contributor(s): Gervase Markham <gerv@gerv.net>
+ # Contributor(s): Ronaldo Maia <rmaia@everythingsolved.com>
#%]
-[% USE Bugzilla %]
-[% cgi = Bugzilla.cgi %]
+[% PROCESS global/variables.none.tmpl %]
-[% IF cgi.param("help") %]
- <script type="text/javascript"> <!--
- [% FOREACH help_name = help_html.keys %]
- g_helpTexts["[% help_name FILTER js %]"] =
- "[%- help_html.$help_name FILTER js -%]";
- [% END %]
- // -->
- </script>
-[% END %]
+[% IF searchname %]
+ [% searchname FILTER html %] ([% bugs.size %])
+[% ELSE %]
+ [% terms.Bug %] List ([% bugs.size %])
+[% END %]
diff --git a/extensions/BMO/template/en/default/list/server-push.html.tmpl b/extensions/BMO/template/en/default/list/server-push.html.tmpl
new file mode 100644
index 000000000..1c1f3cf36
--- /dev/null
+++ b/extensions/BMO/template/en/default/list/server-push.html.tmpl
@@ -0,0 +1,52 @@
+[%# 1.0@bugzilla.org %]
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Myk Melez <myk@mozilla.org>
+ #%]
+
+[%# INTERFACE:
+ # debug: boolean. True if we want the search displayed while we wait.
+ # query: string. The SQL query which makes the buglist.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+<html>
+ <head>
+ <title>[% terms.Bugzilla %] is pondering your search</title>
+ </head>
+ <body>
+ <div style="margin-top: 15%; text-align: center;">
+ <center><img src="extensions/BMO/web/images/mozchomp.gif" alt=""
+ width="160" height="87"></center>
+ <h1>Please wait while your [% terms.bugs %] are retrieved.</h1>
+ </div>
+
+ [% IF debug %]
+ <p>
+ [% FOREACH debugline = debugdata %]
+ <code>[% debugline FILTER html %]</code><br>
+ [% END %]
+ </p>
+ <p>
+ <code>[% query FILTER html %]</code>
+ </p>
+ [% END %]
+
+ </body>
+</html>
diff --git a/extensions/BMO/template/en/default/pages/bug-writing.html.tmpl b/extensions/BMO/template/en/default/pages/bug-writing.html.tmpl
new file mode 100644
index 000000000..21ed3b040
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/bug-writing.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/redirect.html.tmpl
+ url = "https://developer.mozilla.org/en/Bug_writing_guidelines"
+%]
diff --git a/extensions/BMO/template/en/default/pages/custom_forms.html.tmpl b/extensions/BMO/template/en/default/pages/custom_forms.html.tmpl
new file mode 100644
index 000000000..d484d730c
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/custom_forms.html.tmpl
@@ -0,0 +1,40 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Custom Bug Entry Forms"
+%]
+
+[%
+ visible_forms = {};
+ PROCESS bug/create/custom_forms.none.tmpl;
+ FOREACH product = custom_forms.keys;
+ product_forms = [];
+ FOREACH form = custom_forms.$product;
+ NEXT IF form.group && !user.in_group(form.group);
+ product_forms.push(form);
+ END;
+ NEXT UNLESS product_forms.size;
+ visible_forms.$product = product_forms;
+ END;
+%]
+
+<h1>Custom [% terms.Bug %] Entry Forms</h1>
+
+[% FOREACH product = visible_forms.keys.sort %]
+ <h3>[% product FILTER html %]</h3>
+ <ul>
+ [% FOREACH form = visible_forms.$product.sort("title") %]
+ <li>
+ <a href="[% form.link FILTER none %]">[% form.title FILTER html %]</a>
+ </li>
+ [% END %]
+ </ul>
+[% END %]
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/email_queue.html.tmpl b/extensions/BMO/template/en/default/pages/email_queue.html.tmpl
new file mode 100644
index 000000000..f0c750129
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/email_queue.html.tmpl
@@ -0,0 +1,67 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE global/header.html.tmpl
+ title = "Job Queue Status"
+ style_urls = [ "extensions/BMO/web/styles/reports.css" ]
+%]
+
+[% IF jobs.size %]
+
+ <p><i>[% jobs.size FILTER none %] email(s) in the queue.</i></p>
+
+ <table id="report" class="hover" cellspacing="0" border="0" width="100%">
+ <tr id="report-header">
+ <th>Insert Time</th>
+ <th>Run Time</th>
+ <th>Age</th>
+ <th>Error Count</th>
+ <th>Last Error</th>
+ <th>Error Message</th>
+ </tr>
+ [% FOREACH job IN jobs %]
+ <tr class="report item [% loop.count % 2 == 1 ? "report_row_odd" : "report_row_even" %]">
+ <td nowrap>[% time2str("%Y-%m-%d %H:%M:%S %Z", job.insert_time) FILTER html %]</td>
+ <td nowrap>[% time2str("%Y-%m-%d %H:%M:%S %Z", job.run_time) FILTER html %]</td>
+ <td nowrap>
+ [% age = now - job.insert_time %]
+ [% IF age < 60 %]
+ [% age FILTER none %]s
+ [% ELSIF age < 60 * 60 %]
+ [% age / 60 FILTER format('%.0f') %]m
+ [% ELSE %]
+ [% age / (60 * 60) FILTER format('%.0f') %]h
+ [% END %]
+ </td>
+ <td nowrap>[% job.error_count FILTER html %]</td>
+ <td nowrap>
+ [% IF job.error_count %]
+ [% time2str("%Y-%m-%d %H:%M:%S %Z", job.error_time) FILTER html %]
+ [% ELSE %]
+ -
+ [% END %]
+ </td>
+ <td>
+ [% job.error_count ? job.error_message : '-' FILTER html %]
+ </td>
+ </tr>
+ [% IF job.subject %]
+ <tr class="report item [% loop.count % 2 == 1 ? "report_row_odd" : "report_row_even" %]">
+ <td colspan="6">&nbsp;&nbsp;&nbsp;[% job.subject FILTER html %]</td>
+ </tr>
+ [% END %]
+ [% END %]
+ </table>
+
+[% ELSE %]
+
+<p><i>The email queue is empty.</i></p>
+
+[% END %]
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/etiquette.html.tmpl b/extensions/BMO/template/en/default/pages/etiquette.html.tmpl
new file mode 100644
index 000000000..78cc0bad7
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/etiquette.html.tmpl
@@ -0,0 +1,146 @@
+<!-- 1.0@bugzilla.org -->
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Stefan Seifert <nine@detonation.org>
+ # Gervase Markham <gerv@gerv.net>
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Bugzilla Etiquette"
+ style = "li { margin: 5px } .heading { font-weight: bold }" %]
+
+<p>
+ There's a number of <i lang="fr">faux pas</i> you can commit when using
+ [%+ terms.Bugzilla %]. At the very
+ least, these will make Mozilla contributors upset at you; if committed enough
+ times they will cause those contributors to demand the disabling of your
+ [%+ terms.Bugzilla %] account. So, ignore this advice at your peril.
+</p>
+
+<p>
+ That said, Mozilla developers are generally a friendly bunch, and will be
+ friendly towards you as long as you follow these guidelines.
+</p>
+
+<h3>1. Commenting</h3>
+
+<p>
+ This is the most important section.
+</p>
+
+<ol>
+ <li>
+ <span class="heading">No pointless comments</span>.
+ Unless you have something constructive and helpful to say, do not add a
+ comment to a [% terms.bug %]. In [% terms.bugs %] where there is a heated debate going on, you
+ should be even more
+ inclined not to add a comment. Unless you have something new to contribute,
+ then the [% terms.bug %] owner is aware of all the issues, and will make a judgement
+ as to what to do. If you agree the [% terms.bug %] should be fixed, vote for it.
+ Additional "I see this too" or "It works for me" comments are unnecessary
+ unless they are on a different platform or a significantly different build.
+ Constructive and helpful thoughts unrelated to the topic of the [% terms.bug %]
+ should go in the appropriate
+ <a href="http://www.mozilla.org/about/forums/">newsgroup</a>.
+ </li>
+
+ <li>
+ <span class="heading">No obligation</span>.
+ "Open Source" is not the same as "the developers must do my bidding."
+ Everyone here wants to help, but no one else has any <i>obligation</i> to fix
+ the [% terms.bugs %] you want fixed. Therefore, you should not act as if you
+ expect someone to fix a [% terms.bug %] by a particular date or release.
+ Aggressive or repeated demands will not be received well and will almost
+ certainly diminish the impact and interest in your suggestions.
+ </li>
+
+ <li>
+ <span class="heading">No abusing people</span>.
+ Constant and intense critique is one of the reasons we build great products.
+ It's harder to fall into group-think if there is always a healthy amount of
+ dissent. We want to encourage vibrant debate inside of the Mozilla
+ community, we want you to disagree with us, and we want you to effectively
+ argue your case. However, we require that in the process, you attack
+ <i>things</i>, not <i>people</i>. Examples of things include: interfaces,
+ algorithms, and schedules. Examples of people include: developers,
+ designers and users. <b>Attacking a person may result in you being banned
+ from [% terms.Bugzilla %].</b>
+ </li>
+
+ <li>
+ <span class="heading">No private email</span>.
+ Unless the [% terms.bug %] owner or another respected project contributor has asked you
+ to email them with specific information, please place all information
+ relating to [% terms.bugs %]
+ in the [% terms.bug %] itself. Do not send them by private email; no-one else can read
+ them if you do that, and they'll probably just get ignored. If a file
+ is too big for [% terms.Bugzilla %], add a comment giving the file size and contents
+ and ask what to do.
+ </li>
+</ol>
+
+<h3>2. Changing Fields</h3>
+
+<ol>
+ <li>
+ <span class="heading">No messing with other people's [% terms.bugs %]</span>.
+ Unless you are the [% terms.bug %] assignee, or have some say over the use of their
+ time, never change the Priority or Target Milestone fields. If in doubt,
+ do not change the fields of [% terms.bugs %] you do not own - add a comment
+ instead, suggesting the change.
+ </li>
+
+ <li>
+ <span class="heading">No whining about decisions</span>.
+ If a respected project contributor has marked a [% terms.bug %] as INVALID, then it is
+ invalid. Someone filing another duplicate of it does not change this. Unless
+ you have further important evidence, do not post a comment arguing that an
+ INVALID or WONTFIX [% terms.bug %] should be reopened.
+ </li>
+
+</ol>
+
+<h3>3. Applicability</h3>
+
+<ol>
+ <li>
+ Some of these rules may not apply to you. If they do not, you will know
+ exactly which ones do not, and why they do not apply. If you are not
+ sure, then they definitely all apply to you.
+ </li>
+</ol>
+
+<p>
+ If you see someone not following these rules, the first step is, as an exception
+ to guideline 1.4, to make them aware of this document by <em>private</em> mail.
+ Flaming people publically in [% terms.bugs %] violates guidelines 1.1 and 1.3. In the case of
+ persistent offending you should ping an administrator on Mozilla IRC in channel #bmo and ask them
+ to look into it.
+</p>
+
+<p>
+ This entire document can be summed up in one sentence:
+ do unto others as you would have them do unto you.
+</p>
+
+<p>
+ Other useful documents:
+ <a href="page.cgi?id=bug-writing.html">The [% terms.Bug %] Writing Guidelines</a>.
+</p>
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/get_help.html.tmpl b/extensions/BMO/template/en/default/pages/get_help.html.tmpl
new file mode 100644
index 000000000..70ff0a12b
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/get_help.html.tmpl
@@ -0,0 +1,42 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): David Miller <justdave@bugzilla.org>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+[% INCLUDE global/header.html.tmpl title = "Get Help with Mozilla Products" %]
+
+<div id="steps">
+<h2>Got a problem?</h2>
+
+<ul>
+<li><a href="http://www.mozilla.org/support/">Get help with your mozilla.org product</a></li>
+<li><a href="http://hendrix.mozilla.org/">Leave quick feedback</a></li>
+<li><a href="http://input.mozilla.com/feedback">Report a broken website</a></li>
+<li><a href="enter_bug.cgi">Report a [% terms.bug %]</a> - latest release only
+ [% IF NOT user.id %]
+ (you'll need an
+ <a href="createaccount.cgi">account</a>)
+ [% END %]
+</li>
+</ul>
+</div>
+
+<br>
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/get_permissions.html.tmpl b/extensions/BMO/template/en/default/pages/get_permissions.html.tmpl
new file mode 100644
index 000000000..b70aa488f
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/get_permissions.html.tmpl
@@ -0,0 +1,44 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Upgrade Permissions"
+%]
+
+<h3>How to apply for upgraded permissions</h3>
+
+<p>
+ If you want <kbd>canconfirm</kbd>, email <a href="mailto:bmo-perms@mozilla.org">
+ bmo-perms@mozilla.org</a> the URLs of three good [% terms.bug %] reports you have filed.
+</p>
+
+<p>
+ If you want <kbd>editbugs</kbd>, email <a href="mailto:bmo-perms@mozilla.org">
+ bmo-perms@mozilla.org</a> either:
+ <ul>
+ <li>
+ The URLs of two [% terms.bugs %] to which you have attached patches
+ or testcases; or
+ </li>
+ <li>
+ The URLs of the relevant comment on three [% terms.bugs %] which you
+ wanted to change, but couldn't, and so added a comment instead.
+ </li>
+ </ul>
+</p>
+
+<p>
+ <kbd>editbugs</kbd> implies <kbd>canconfirm</kbd>; there's no need to apply for both.
+</p>
+
+<p>
+ Don't forget to include your [% terms.Bugzilla %] ID if it's not the email address
+ you are emailing from.
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/group_admins.html.tmpl b/extensions/BMO/template/en/default/pages/group_admins.html.tmpl
new file mode 100644
index 000000000..01bb744c4
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/group_admins.html.tmpl
@@ -0,0 +1,54 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the BMO Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+
+[% INCLUDE global/header.html.tmpl
+ title = "Group Admins Report"
+ style_urls = [ "extensions/BMO/web/styles/reports.css" ]
+ yui = [ "datasource" ]
+%]
+
+[% IF groups.size > 0 %]
+ <table border="0" cellspacing="0" id="report" class="hover" width="100%">
+ <tr id="report-header">
+ <th align="left">Name</th>
+ <th align="left">Admins</th>
+ </tr>
+
+ [% FOREACH group = groups %]
+ [% count = loop.count() %]
+ <tr class="report_item [% count % 2 == 1 ? "report_row_odd" : "report_row_even" %]">
+ <td>
+ [% group.name FILTER html %]
+ </td>
+ <td>
+ [% FOREACH admin = group.admins %]
+ [% INCLUDE global/user.html.tmpl who = admin %][% ", " UNLESS loop.last %]
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ </table>
+[% ELSE %]
+ <p>
+ No groups found.
+ </p>
+[% END %]
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/group_members.html.tmpl b/extensions/BMO/template/en/default/pages/group_members.html.tmpl
new file mode 100644
index 000000000..daf4d5b0d
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/group_members.html.tmpl
@@ -0,0 +1,97 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE global/header.html.tmpl
+ title = "Group Members Report"
+ style_urls = [ "extensions/BMO/web/styles/reports.css" ]
+%]
+
+<form method="GET" action="page.cgi">
+ <input type="hidden" name="id" value="group_members.html">
+
+ <table id="parameters">
+ <tr>
+ <th>Group</th>
+ <td>
+ <select name="group">
+ [% FOREACH group_name = groups %]
+ <option value="[% group_name FILTER html %]"
+ [% "selected" IF group_name == group %]>
+ [% group_name FILTER html %]</option>
+ [% END %]
+ </select>
+ <input type="checkbox" name="include_disabled" id="include_disabled"
+ value="1" [% "checked" IF include_disabled %]>
+ <label for="include_disabled">
+ Include disabled users
+ </label>
+ <input type="submit" value="Generate">
+ </td>
+ </tr>
+ </table>
+</form>
+
+[% IF group != '' %]
+
+ <p>
+ Members of the <b>[% group FILTER html %]</b> group:
+ </p>
+
+ [% IF types.size > 0 %]
+ <table border="0" cellspacing="0" id="report" class="nohover" width="100%">
+ <tr id="report-header">
+ <th>Type</th>
+ <th>Count</th>
+ <th>Members</th>
+ <th class="right">Last Seen (days ago)</th>
+ </tr>
+
+ [% FOREACH type = types %]
+ [% count = loop.count() %]
+ <tr class="report_item [% count % 2 == 1 ? "report_row_odd" : "report_row_even" %]">
+ <td valign="top">
+ [% "via&nbsp;" UNLESS type.name == 'direct' %]
+ [% type.name FILTER html %]
+ </td>
+ <td valign="top" align="right">
+ [% type.members.size FILTER html %]
+ </td>
+ <td valign="top" width="100%" colspan="2">
+ <table cellspacing="0" class="hoverrow">
+ [% FOREACH member = type.members %]
+ <tr>
+ <td width="100%">
+ <a href="editusers.cgi?action=edit&amp;userid=[% member.id FILTER none %]"
+ target="_blank">
+ <span [% 'class="bz_inactive"' UNLESS member.is_enabled %]>
+ [% member.name FILTER html %] &lt;[% member.email FILTER email FILTER html %]&gt;
+ </span>
+ </a>
+ </td>
+ <td align="right" nowrap>
+ [% member.lastseen FILTER html %]
+ </td>
+ </tr>
+ [% END %]
+ </table>
+ </td>
+ </tr>
+ [% END %]
+ </table>
+
+ <a href="page.cgi?id=group_members.json&amp;group=[% group FILTER uri %]
+ [% IF include_disabled %]&amp;include_disabled=1[% END %]">JSON</a>
+ [% ELSE %]
+ <p>
+ <i>This group is empty.</i>
+ </p>
+ [% END %]
+
+[% END %]
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/group_members.json.tmpl b/extensions/BMO/template/en/default/pages/group_members.json.tmpl
new file mode 100644
index 000000000..8cbb2a23a
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/group_members.json.tmpl
@@ -0,0 +1,32 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[
+ [% SET count = 0 %]
+ [% FOREACH type = types %]
+ [% SET count = count + type.members.size %]
+ [% END %]
+ [% SET i = 0 %]
+ [% FOREACH type = types %]
+ [% FOREACH member = type.members %]
+ [% SET i = i + 1 %]
+ { "login": "[% member.login FILTER email FILTER js %]",
+ [% IF type.name == "direct" %]
+ "membership": "direct",
+ [% ELSE %]
+ "membership": "indirect",
+ "group": "[% type.name FILTER js %]",
+ [% END %]
+ [% IF include_disabled %]
+ "disabled": "[% member.is_enabled ? "false" : "true" %]",
+ [% END %]
+ "lastseen": "[% member.lastseen FILTER js %]"
+ }[% "," UNLESS i == count %]
+ [% END %]
+ [% END %]
+]
diff --git a/extensions/BMO/template/en/default/pages/group_membership.html.tmpl b/extensions/BMO/template/en/default/pages/group_membership.html.tmpl
new file mode 100644
index 000000000..32484b13f
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/group_membership.html.tmpl
@@ -0,0 +1,75 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Group Membership Report"
+ yui = [ 'autocomplete' ]
+ style_urls = [ "extensions/BMO/web/styles/reports.css" ]
+ javascript_urls = [ "js/field.js" ]
+%]
+
+<form method="GET" action="page.cgi">
+<input type="hidden" name="id" id="id" value="group_membership.html">
+
+<table id="parameters">
+<tr>
+ <th>User(s):</th>
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "who"
+ name => "who"
+ value => who.join(', ')
+ size => 40
+ classes => ["bz_userfield"]
+ multiple => 5
+ field_title => "One or more email address (comma delimited)"
+ %]
+ </td>
+ <td>&nbsp;</td>
+ <td>
+ <select name="output"
+ onchange="document.getElementById('id').value = 'group_membership.' + this.value">
+ <option value="html" [% 'selected' IF output == 'html' %]>HTML</option>
+ <option value="txt" [% 'selected' IF output == 'txt' %]>Text</option>
+ </select>
+ </td>
+ <td>
+ <input type="submit" value="Generate">
+ </td>
+</tr>
+</table>
+
+</form>
+
+[% IF users.size %]
+
+ <table border="0" cellspacing="0" id="report" class="hover" width="100%">
+ [% FOREACH u = users %]
+ <tr>
+ <th colspan="3">[% u.user.identity FILTER html %]</th>
+ </tr>
+ [% FOREACH g = u.groups %]
+ <tr>
+ <td>&nbsp;</td>
+ <td>[% g.name FILTER html %]</td>
+ <td>[% g.desc FILTER html %]</td>
+ <td>
+ [% IF g.via == '' %]
+ direct
+ [% ELSE %]
+ <i>[% g.via FILTER html %]</i>
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ [% END %]
+ </table>
+
+[% END %]
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/group_membership.txt.tmpl b/extensions/BMO/template/en/default/pages/group_membership.txt.tmpl
new file mode 100644
index 000000000..9958f0877
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/group_membership.txt.tmpl
@@ -0,0 +1,16 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% FOREACH u = users %]
+[% u.user.login FILTER none%]:
+ [% FOREACH g = u.groups %]
+ [% g.name FILTER none %]
+ [% ',' UNLESS loop.last %]
+ [% END %]
+ [% "\n" %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/pages/product_security_report.html.tmpl b/extensions/BMO/template/en/default/pages/product_security_report.html.tmpl
new file mode 100644
index 000000000..f5e1c05c8
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/product_security_report.html.tmpl
@@ -0,0 +1,60 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE global/header.html.tmpl
+ title = "Product Security Report"
+ style_urls = [ "extensions/BMO/web/styles/reports.css" ]
+%]
+
+<table border="0" cellspacing="0" id="report" class="nohover" width="100%">
+ <tr id="report-header">
+ <th>Product</th>
+ <th>Default Security Group</th>
+ <th>Default Group Visibility</th>
+ <th>mozilla-employee-confidential Enabled</th>
+ </tr>
+
+ [% count = 0 %]
+ [% FOREACH product = products %]
+ [% count = count + 1 %]
+ <tr class="report_item [% count % 2 == 1 ? "report_row_odd" : "report_row_even" %]">
+ <td>
+ <a href="editproducts.cgi?action=editgroupcontrols&product=[% product.name FILTER uri %]" target="_blank">
+ [% product.name FILTER html %]
+ </a>
+ </td>
+ [% IF product.group_problem %]
+ <td class="problem">
+ <span title="[% product.group_problem FILTER html %]">
+ [% product.default_security_group FILTER html %]
+ </span>
+ </td>
+ [% ELSE %]
+ <td>
+ [% product.default_security_group FILTER html %]
+ </td>
+ [% END %]
+ [% IF product.visibility_problem %]
+ <td class="problem">
+ <span title="[% product.visibility_problem FILTER html %]">
+ [% product.group_visibility FILTER html %]
+ </span>
+ </td>
+ [% ELSE %]
+ <td>
+ [% product.group_visibility FILTER html %]
+ </td>
+ [% END %]
+ <td>
+ [% product.moco ? 'Yes' : 'No' FILTER none %]
+ </td>
+ </tr>
+ [% END %]
+</table>
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/query_database.html.tmpl b/extensions/BMO/template/en/default/pages/query_database.html.tmpl
new file mode 100644
index 000000000..97f5c0a25
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/query_database.html.tmpl
@@ -0,0 +1,47 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE global/header.html.tmpl
+ title = "Query Database"
+ style_urls = [ "extensions/BMO/web/styles/reports.css" ]
+%]
+
+<form method="post" action="page.cgi">
+<input type="hidden" name="id" value="query_database.html">
+<textarea cols="80" rows="10" name="query">[% query FILTER html %]</textarea><br>
+<input type="submit" value="Execute">
+</form>
+
+[% IF executed %]
+ <hr>
+
+ [% IF sql_error %]
+ <b>[% sql_error FILTER html %]</b>
+ [% ELSIF rows.size %]
+ <table border="0" cellspacing="0" id="report">
+ <tr>
+ [% FOREACH column = columns %]
+ <th>[% column FILTER html %]</th>
+ [% END %]
+ </tr>
+ [% FOREACH row = rows %]
+ [% tr_class = loop.count % 2 ? 'report_row_even' : 'report_row_odd' %]
+ <tr class="[% tr_class FILTER html %]">
+ [% FOREACH field = row %]
+ <td>[% field FILTER html %]</td>
+ [% END %]
+ </tr>
+ [% END %]
+ </table>
+ [% ELSE %]
+ <i>no results</i>
+ [% END %]
+
+[% END %]
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/release_tracking_report.html.tmpl b/extensions/BMO/template/en/default/pages/release_tracking_report.html.tmpl
new file mode 100644
index 000000000..71228014a
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/release_tracking_report.html.tmpl
@@ -0,0 +1,103 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE global/header.html.tmpl
+ title = "Release Tracking Report"
+ style_urls = [ "extensions/BMO/web/styles/reports.css" ]
+ javascript_urls = [ "extensions/BMO/web/js/release_tracking_report.js" ]
+%]
+
+<noscript>
+<h1>JavaScript is required to use this report.</h1>
+</noscript>
+
+<script>
+var flags_data = [% flags_json FILTER none %];
+var products_data = [% products_json FILTER none %];
+var fields_data = [% fields_json FILTER none %];
+var default_query = '[% default_query FILTER js %]';
+</script>
+
+<form action="page.cgi" method="get" onSubmit="return onFormSubmit()">
+<input type="hidden" name="id" value="release_tracking_report.html">
+<input type="hidden" name="q" id="q" value="">
+<table>
+
+<tr>
+ <th>Approval:</th>
+ <td>
+ Show [% terms.bugs %] where
+ <select id="flag" onChange="onFlagChange()">
+ [% FOREACH flag_name = flag_names %]
+ <option value="[% flag_name FILTER html %]">[% flag_name FILTER html %]</option>
+ [% END %]
+ </select>
+
+ was changed to (and is currently)
+ <select id="flag_value">
+ <option value="?">?</option>
+ <option value="-">-</option>
+ <option value="+">+</option>
+ </select>
+
+ between
+ <select id="range" onChange="serialiseForm()">
+ [% FOREACH range = ranges %]
+ <option value="[% range.value FILTER html %]">
+ [% range.label FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <th>Status:</th>
+ <td>
+ for the product
+ <select id="product" onChange="onProductChange()">
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td>
+ <select id="op" onChange="serialiseForm()">
+ <option value="and">All selected tracking fields (AND)</option>
+ <option value="or">Any selected tracking fields (OR)</option>
+ </select>
+ [
+ <a href="javascript:void(0)" onClick="selectAllFields()">All</a> |
+ <a href="javascript:void(0)" onClick="selectNoFields()">None</a>
+ ]
+ [
+ <a href="javascript:void(0)" onClick="invertFields()">Invert</a>
+ ]
+ <br>
+ <span id="tracking_span">
+ </span>
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td colspan="2">
+ <input type="submit" value="Search">
+ <input type="submit" value="Reset" onClick="onFormReset(); return false">
+ <a href="?" id="bookmark">Bookmarkable Link</a>
+ </td>
+</tr>
+</table>
+</form>
+
+<p>
+ <i>"fixed" in the status field checks for the "verified" status as well as "fixed".</i>
+</p>
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/triage_reports.html.tmpl b/extensions/BMO/template/en/default/pages/triage_reports.html.tmpl
new file mode 100644
index 000000000..a7f26e86d
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/triage_reports.html.tmpl
@@ -0,0 +1,199 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the BMO Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Byron Jones <bjones@mozilla.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% js_data = BLOCK %]
+var useclassification = false;
+var first_load = true;
+var last_sel = [];
+var cpts = new Array();
+[% n = 1 %]
+[% FOREACH p = user.get_selectable_products %]
+ cpts['[% n FILTER js %]'] = [
+ [%- FOREACH c = p.components %]'[% c.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
+ [% n = n+1 %]
+[% END %]
+
+var selected_components = [
+ [%- FOREACH c = input.component %]'[% c FILTER js %]'
+ [%- ',' UNLESS loop.last %] [%- END ~%] ];
+
+[% END %]
+
+[% INCLUDE global/header.html.tmpl
+ title = "Triage Reports"
+ yui = [ 'autocomplete', 'calendar' ]
+ javascript = js_data
+ javascript_urls = [ "js/util.js", "js/field.js", "js/productform.js",
+ "extensions/BMO/web/js/triage_reports.js" ]
+ style_urls = [ "skins/standard/buglist.css",
+ "extensions/BMO/web/styles/triage_reports.css" ]
+%]
+
+<noscript>
+<h2>Javascript is required to use this report.</h2>
+</noscript>
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+
+<form id="activity_form" name="activity_form" action="page.cgi" method="get"
+ onSubmit="return onGenerateReport()">
+<input type="hidden" name="id" value="triage_reports.html">
+<input type="hidden" name="action" value="run">
+
+Show UNCONFIRMED [% terms.bugs %] with:
+<table id="triage_form">
+
+<tr>
+ <th>Product:</th>
+ <td>
+ <select name="product" id="product" onChange="onSelectProduct()">
+ <option value=""></option>
+ [% FOREACH p = user.get_selectable_products %]
+ <option value="[% p.name FILTER html %]"
+ [% " selected" IF input.product == p.name %]>
+ [% p.name FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ </td>
+ <td rowspan="2" valign="top">
+ <b>Comment:</b><br>
+
+ <input type="checkbox" name="filter_commenter" id="filter_commenter" value="1"
+ [% 'checked' IF input.filter_commenter %]>
+ <label for="filter_commenter">where the last commenter</label>
+ <select name="commenter" id="commenter" onChange="onCommenterChange()">
+ <option value="reporter" [% 'selected' IF input.commenter == 'reporter' %]>is the reporter</option>
+ <option value="noconfirm" [% 'selected' IF input.commenter == 'noconfirm' %]>does not have canconfirm</option>
+ <option value="is" [% 'selected' IF input.commenter == 'is' %]>is</option>
+ </select>
+ [%+ INCLUDE global/userselect.html.tmpl
+ id => "commenter_is"
+ name => "commenter_is"
+ value => input.commenter_is
+ size => 20
+ emptyok => 0
+ classes = input.commenter == "is" ? "" : "hidden"
+ %]
+ <br>
+
+ <input type="checkbox" name="filter_last" id="filter_last" value="1"
+ [% 'checked' IF input.filter_last %]>
+ <label for="filter_last">where the last comment is older than</label>
+ <select name="last" id="last" onChange="onLastChange()">
+ <option value="30" [% 'selected' IF input.last == '30' %]>30 days</option>
+ <option value="60" [% 'selected' IF input.last == '60' %]>60 days</option>
+ <option value="90" [% 'selected' IF input.last == '90' %]>90 days</option>
+ <option value="365" [% 'selected' IF input.last == '365' %]>one year</option>
+ <option value="is" [% 'selected' IF input.last == 'is' %]>the date</option>
+ </select>
+ <span id="last_is_span" class="[% 'hidden' IF input.last != 'is' %]">
+ <input type="text" id="last_is" name="last_is" size="11" maxlength="10"
+ value="[% input.last_is FILTER html %]"
+ onChange="updateCalendarLastIs(this)">
+ <button type="button" class="calendar_button" id="button_calendar_last_is"
+ onClick="showCalendar('last_is')"><span>Calendar</span>
+ </button>
+ <div id="con_calendar_last_is"></div>
+ </span>
+ <br>
+ </td>
+</tr>
+
+<tr>
+ <th>Component:</th>
+ <td>
+ <select name="component" id="component" multiple size="5">
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td>
+ <input type="submit" value="Generate Report">
+ </td>
+</tr>
+
+</table>
+
+</form>
+<script>
+ createCalendar('last_is');
+</script>
+
+[% IF input.action == 'run' %]
+<hr>
+[% IF bugs.size > 0 %]
+ <p>
+ Found [% bugs.size %] [%+ terms.bug %][% 's' IF bugs.size != 1 %]:
+ </p>
+ <table border="0" cellspacing="0" id="report" width="100%">
+ <tr id="report-header">
+ <th>[% terms.Bug %] / Date</th>
+ <th>Summary</th>
+ <th>Reporter / Commenter</th>
+ <th>Comment Date</th>
+ <th>Last Comment</th>
+ </tr>
+
+ [% FOREACH bug = bugs %]
+ [% count = loop.count() %]
+ <tr class="bz_bugitem [% count % 2 == 1 ? "bz_row_odd" : "bz_row_even" %]">
+ <td>
+ [% bug.id FILTER bug_link(bug.id) FILTER none %]<br>
+ [% bug.creation_ts.replace(' .*' '') FILTER html FILTER no_break %]
+ </td>
+ <td>
+ [% bug.summary FILTER html %]
+ </td>
+ <td>
+ [% INCLUDE global/user.html.tmpl who = bug.reporter %]
+ [% IF bug.commenter.id != bug.reporter.id %]
+ <br>[% INCLUDE global/user.html.tmpl who = bug.commenter %]
+ [% END %]
+ </td>
+ <td>
+ [% bug.comment_ts FILTER html FILTER no_break %]
+ </td>
+ <td>
+ [% bug.comment FILTER html %]
+ </td>
+ </tr>
+ [% END %]
+ </table>
+
+ <p>
+ <a href="buglist.cgi?bug_id=
+ [%- FOREACH bug = bugs %][% bug.id FILTER uri %],[% END -%]
+ ">Show as a [% terms.Bug %] List</a>
+ </p>
+
+[% ELSE %]
+ <p>
+ No [% terms.bugs %] found.
+ </p>
+[% END %]
+
+[% END %]
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/upgrade-3.6.html.tmpl b/extensions/BMO/template/en/default/pages/upgrade-3.6.html.tmpl
new file mode 100644
index 000000000..8fa944ae6
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/upgrade-3.6.html.tmpl
@@ -0,0 +1,304 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): David Miller <justdave@bugzilla.org>
+ # Reed Loden <reed@reedloden.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+[% INCLUDE global/header.html.tmpl
+ title = "Bugzilla 3.6 Upgrade"
+%]
+[% USE date %]
+
+<p><b>Last Updated:</b> [% date.format(template.modtime, "%d-%b-%Y %H:%M %Z") %]</p>
+
+<p>On Friday, July 9, 2010, at 11:40pm PDT (0640 UTC), bugzilla.mozilla.org was
+ <a href="show_bug.cgi?id=558044">upgraded</a> to Bugzilla 3.6.1+. Please
+ <a href="enter_bug.cgi?product=mozilla.org&amp;component=Bugzilla:+Other+b.m.o+Issues&amp;blocked=bmo-regressions">file
+ any regressions</a> for tracking purposes.</p>
+
+<h3>Known Issues</h3>
+
+<p>The following is a list of issues which are known to be broken or incomplete with this upgrade so far.</p>
+
+<ul>
+
+<li>The <a href="https://bugzilla.mozilla.org/showdependencytree.cgi?id=577801&hide_resolved=1">stuff filed in Bugzilla</a>.</li>
+
+</ul>
+
+<h3>What's New</h3>
+
+<h4>Custom bugzilla.mozilla.org Changes</h4>
+
+<ul>
+ <li>Addition of autocomplete support for all user-related fields (assignee,
+ QA contact, and CC list) and the keywords field.</li>
+ <li>New attachment details UI.</li>
+ <li>New icons for the front page.</li>
+ <li>Removal of unused "Patches" column from buglist.</li>
+ <li>Initial support for <a href="http://en.wikipedia.org/wiki/Strict_Transport_Security">Strict-Transport-Security</a> (STS) header.</li>
+</ul>
+
+<h4>General Usability Improvements</h4>
+
+<p>A <a href="https://wiki.mozilla.org/Bugzilla:CMU_HCI_Research_2008">scientific
+ usability study</a> was done on [% terms.Bugzilla %] by researchers
+ from Carnegie-Mellon University. As a result of this study,
+ <a href="https://bugzilla.mozilla.org/showdependencytree.cgi?id=490786&amp;hide_resolved=0">several
+ usability issues</a> were prioritized to be fixed, based on specific data
+ from the study.</p>
+
+<p>As a result, you will see many small improvements in [% terms.Bugzilla %]'s
+ usability, such as using Javascript to validate certain forms before
+ they are submitted, standardizing the words that we use in the user interface,
+ being clearer about what [% terms.Bugzilla %] needs from the user,
+ and other changes, all of which are also listed individually in this New
+ Features section.</p>
+
+<p>Work continues on improving usability for the next release of
+ [%+ terms.Bugzilla %], but the results of the research have already
+ had an impact on this 3.6 release.</p>
+
+<h4>Improved Quicksearch</h4>
+
+<p>The "quicksearch" box that appears on the front page of
+ [%+ terms.Bugzilla %] and in the header/footer of every page
+ is now simplified and made more powerful. There is a
+ <kbd>[?]</kbd> link next to the box that will take you to
+ the simplified <a href="page.cgi?id=quicksearch.html">Quicksearch Help</a>,
+ which describes every single feature of the system in a simple layout,
+ including new features such as the ability to use partial field names
+ when searching.</p>
+
+<p>Quicksearch should also be much faster than it was before, particularly
+ on large installations.</p>
+
+<p>Note that in order to implement the new quicksearch, certain old
+ and rarely-used features had to be removed:
+
+<ul>
+ <li><b>+</b> as a prefix to mean "search additional resolutions", and
+ <b>+</b> as a prefix to mean "search just the summary". You can
+ instead use <kbd>summary:</kbd> to explicitly search summaries.</li>
+ <li>Searching the Severity field if you type something that matches
+ the first few characters of a severity. You can explicitly search
+ the Severity field if you want to find [% terms.bugs %] by severity.</li>
+ <li>Searching the Priority field if you typed something that exactly
+ matched the name of a priority. You can explicitly search the
+ Priority field if you want to find [% terms.bugs %] by priority.</li>
+ <li>Searching the Platform and OS fields if you typed in one of a
+ certain hard-coded list of strings (like "pc", "windows", etc.).
+ You can explicitly search these fields, instead, if you want to
+ find [% terms.bugs %] with a specific Platform or OS set.</li>
+</ul>
+
+<h4>Simple "Browse" Interface</h4>
+
+<p>There is now a "Browse" link in the header of each [% terms.Bugzilla %]
+ page that presents a very basic interface that allows users to simply
+ browse through all open [% terms.bugs %] in particular components.</p>
+
+<h4>JSON-RPC Interface</h4>
+
+<p>[% terms.Bugzilla %] now has support for the
+ <a href="http://json-rpc.org/">JSON-RPC</a> WebServices protocol via
+ <a href="[% docs_urlbase FILTER html %]api/Bugzilla/WebService/Server/JSONRPC.html">jsonrpc.cgi</a>.
+ The JSON-RPC interface is experimental in this release--if you want any
+ fundamental changes in how it works,
+ <a href="http://www.bugzilla.org/developers/reporting_bugs.html">let us
+ know</a>, for the next release of [% terms.Bugzilla %].</p>
+
+<h3>New Features</h3>
+
+<h4>Enhancements for Users</h4>
+
+<ul>
+ <li><b>[% terms.Bug %] Filing:</b> When filing [% terms.abug %],
+ [%+ terms.Bugzilla %] now visually indicates which fields are
+ mandatory.</li>
+ <li><b>[% terms.Bug %] Filing:</b> "Bookmarkable templates" now
+ support the "alias" and "estimated hours" fields.</li>
+
+ <li><b>[% terms.Bug %] Editing:</b> In previous versions of
+ [%+ terms.Bugzilla %], if you added a private comment to [% terms.abug %],
+ then <em>none</em> of the changes that you made at that time were
+ sent to users who couldn't see the private comment. Now, for users
+ who can't see private comments, public changes are sent, but the private
+ comment is excluded from their email notification.</li>
+ <li><b>[% terms.Bug %] Editing:</b> The controls for groups now
+ appear to the right of the attachment and time-tracking tables,
+ when editing [% terms.abug %].</li>
+ <li><b>[% terms.Bug %] Editing:</b> The "Collapse All Comments"
+ and "Expand All Comments" links now appear to the right of the
+ comment list instead of above it.</li>
+ <li><b>[% terms.Bug %] Editing:</b> The See Also field now supports
+ URLs for Google Code Issues and the Debian B[% %]ug-Tracking System.</li>
+ <li><b>[% terms.Bug %] Editing:</b> There have been significant performance
+ improvements in <kbd>show_bug.cgi</kbd> (the script that displays the
+ [% terms.bug %]-editing form), particularly for [% terms.bugs %] that
+ have lots of comments or attachments.</li>
+
+ <li><b>Attachments:</b> The "Details" page of an attachment
+ now displays itself as uneditable if you can't edit the fields
+ there.</li>
+ <li><b>Attachments:</b> We now make sure that there is
+ a Description specified for an attachment, using JavaScript, before
+ the form is submitted.</li>
+ <li><b>Attachments:</b> There is now a link back to the [% terms.bug %]
+ at the bottom of the "Details" page for an attachment.</li>
+ <li><b>Attachments:</b> When you click on an "attachment 12345" link
+ in a comment, if the attachment is a patch, you will now see the
+ formatted "Diff" view instead of the raw patch.</li>
+ <li><b>Attachments</b>: For text attachments, we now let the browser
+ auto-detect the character encoding, instead of forcing the browser to
+ always assume the attachment is in UTF-8.</li>
+
+ <li><b>Search:</b> You can now display [% terms.bug %] flags as a column
+ in search results.</li>
+ <li><b>Search:</b> When viewing search results, you can see which columns are
+ being sorted on, and which direction the sort is on, as indicated
+ by arrows next to the column headers.</li>
+ <li><b>Search:</b> You can now search the Deadline field using relative
+ dates (like "1d", "2w", etc.).</li>
+ <li><b>Search:</b> The iCalendar format of search results now includes
+ a PRIORITY field.</li>
+ <li><b>Search:</b> It is no longer an error to enter an invalid search
+ order in a search URL--[% terms.Bugzilla %] will simply warn you that
+ some of your order options are invalid.</li>
+ <li><b>Search:</b> When there are no search results, some helpful
+ links are displayed, offering actions you might want to take.</li>
+ <li><b>Search:</b> For those who like to make their own
+ <kbd>buglist.cgi</kbd> URLs (and for people working on customizations),
+ <kbd>buglist.cgi</kbd> now accepts nearly every valid field in
+ [%+ terms.Bugzilla %] as a direct URL parameter, like
+ <kbd>&amp;field=value</kbd>.</li>
+
+ <li><b>Requests:</b> When viewing the "My Requests" page, you can now
+ see the lists as a normal search result by clicking a link at the
+ bottom of each table.</li>
+ <li><b>Requests:</b> When viewing the "My Requests" page, if you are
+ using Classifications, the Product drop-down will be grouped by
+ Classification.</li>
+
+ <li>If there are multiple languages available for your
+ [%+ terms.Bugzilla %], you can now select what language you want
+ [%+ terms.Bugzilla %] displayed in using links at the top of every
+ page.</li>
+ <li>When creating a new account, you will be automatically logged in
+ after setting your password.</li>
+ <li>There is no longer a maximum password length for accounts.</li>
+ <li>In the Dusk skin, it's now easier to see links.</li>
+ <li>In the Whining system, you can now choose to receive emails even
+ if there are no [% terms.bugs %] that match your searches.</li>
+ <li>The arrows in dependency graphs now point the other way, so that
+ [%+ terms.bugs %] point at their dependencies.</li>
+
+ <li><b>New Charts:</b> You can now convert an existing Saved Search
+ into a data series for New Charts.</li>
+ <li><b>New Charts:</b> There is now an interface that allows you to
+ delete data series.</li>
+ <li><b>New Charts:</b> When deleting a product, you now have the option
+ to delete the data series that are associated with that product.</li>
+</ul>
+
+<h4>Enhancements for Administrators and Developers</h4>
+
+<ul>
+ <li>Depending on how your workflow is set up, it is now possible to
+ have both UNCONFIRMED and REOPENED show up as status choices for
+ a closed [% terms.bug %]. If you only want one or the other to
+ show up, you should edit your status workflow appropriately
+ (possibly by removing or disabling the REOPENED status).</li>
+ <li>You can now "disable" field values so that they don't show
+ up as choices on [% terms.abug %] unless they are already set as
+ the value for that [% terms.bug %]. This doesn't work for the
+ per-product field values (component, target_milestone, and version)
+ yet, though.</li>
+ <li>Users are now locked out of their accounts for 30 minutes after
+ trying five bad passwords in a row during login. Every time a
+ user is locked out like this, the user in the "maintainer" parameter
+ will get an email.</li>
+ <li>The minimum length allowed for a password is now 6 characters.</li>
+ <li>The <kbd>UNCONFIRMED</kbd> status being enabled in a product
+ is now unrelated to the voting parameters. Instead, there is a checkbox
+ to enable the <kbd>UNCONFIRMED</kbd> status in a product.</li>
+ <li>Information about duplicates is now stored in the database instead
+ of being stored in the <kbd>data/</kbd> directory. On large installations
+ this could save several hundred megabytes of disk space.</li>
+
+ <li>When editing a group, you can now specify that members of a group
+ are allowed to grant others membership in that group itself.</li>
+ <li>The ability to compress BMP attachments to PNGs is now an Extension.
+ To enable the feature, remove the file
+ <kbd>extensions/BmpConvert/disabled</kbd> and then run checksetup.pl.</li>
+ <li>The default list of values for the Priority field are now clear English
+ words instead of P1, P2, etc.</li>
+ <li><kbd>config.cgi</kbd> now returns an ETag header and understands
+ the If-None-Match header in HTTP requests.</li>
+ <li>The XML format of <kbd>show_bug.cgi</kbd> now returns more information:
+ the numeric id of each comment, whether an attachment is a URL,
+ the modification time of an attachment, the numeric id of a flag,
+ and the numeric id of a flag's type.</li>
+</ul>
+
+<h4>WebService Changes</h4>
+
+<ul>
+ <li>The WebService now returns all dates and times in the UTC timezone.
+ <kbd>B[% %]ugzilla.time</kbd> now acts as though the [% terms.Bugzilla %]
+ server were in the UTC timezone, always. If you want to write clients
+ that are compatible across all [% terms.Bugzilla %] versions,
+ check the timezone from <kbd>B[% %]ugzilla.timezone</kbd> or
+ <kbd>B[% %]ugzilla.time</kbd>, and always input times in that timezone
+ and expect times to be returned in that format.</li>
+ <li>You can now log in by passing <kbd>Bugzilla_login</kbd> and
+ <kbd>Bugzilla_password</kbd> as arguments to any WebService function.
+ See the
+ <a href="[% docs_urlbase FILTER html %]api/Bugzilla/WebService.html#LOGGING_IN">Bugzilla::WebService</a>
+ documentation for details.</li>
+ <li>New Method:
+ <a href="[% docs_urlbase FILTER html %]api/Bugzilla/WebService/Bug.html#attachments">B[% %]ug.attachments</a>
+ which allows getting information about attachments.</li>
+ <li>New Method:
+ <a href="[% docs_urlbase FILTER html %]api/Bugzilla/WebService/Bug.html#fields">B[% %]ug.fields</a>,
+ which gets information about all the fields that [% terms.abug %] can have
+ in [% terms.Bugzilla %], include custom fields and legal values for
+ all fields. The <kbd>B[% %]ug.legal_values</kbd> method is now deprecated.</li>
+ <li>In the <kbd>B[% %]ug.add_comment</kbd> method, the "private" parameter
+ has been renamed to "is_private" (for consistency with other methods).
+ You can still use "private", though, for backwards-compatibility.</li>
+ <li>The WebService now has Perl's "taint mode" turned on. This means that
+ it validates all data passed in before sending it to the database.
+ Also, all parameter names are validated, and if you pass in a parameter
+ whose name contains anything other than letters, numbers, or underscores,
+ that parameter will be ignored. Mostly this just affects
+ customizers--[% terms.Bugzilla %]'s WebService is not functionally
+ affected by these changes.</li>
+ <li>In previous versions of [% terms.Bugzilla %], error messages were
+ sent word-wrapped to the client, from the WebService. Error messages
+ are now sent as one unbroken line.</li>
+</ul>
+
+<h3>Last Ten Commits</h3>
+
+<pre>[% bzr_history.join('') FILTER html %]</pre>
+
+<br>
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/user_activity.html.tmpl b/extensions/BMO/template/en/default/pages/user_activity.html.tmpl
new file mode 100644
index 000000000..c9b46b2eb
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/user_activity.html.tmpl
@@ -0,0 +1,230 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF who %]
+[% who_title = ' (' _ who _ ')' %]
+[% ELSE %]
+[% who_title = '' %]
+[% END %]
+
+[% INCLUDE global/header.html.tmpl
+ title = "User Activity Report" _ who_title
+ yui = [ 'autocomplete', 'calendar' ]
+ javascript_urls = [ "js/util.js", "js/field.js" ]
+ style_urls = [ "extensions/BMO/web/styles/reports.css" ]
+
+%]
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+[% PROCESS bug/time.html.tmpl %]
+
+<form id="activity_form" name="activity_form" action="page.cgi" method="get">
+<input type="hidden" name="id" value="user_activity.html">
+<input type="hidden" name="action" value="run">
+<table id="parameters">
+
+<tr>
+ <th>
+ Who:
+ </th>
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "who"
+ name => "who"
+ value => who
+ size => 40
+ emptyok => 0
+ title => "One or more email address (comma delimited)"
+ %]
+ &nbsp;
+ </td>
+ <th>
+ Period:
+ </th>
+ <td>
+ <input type="text" id="from" name="from" size="11"
+ align="right" value="[% from FILTER html %]" maxlength="10"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button" id="button_calendar_from"
+ onclick="showCalendar('from')"><span>Calendar</span>
+ </button>
+ <div id="con_calendar_from"></div>
+ to
+ <input type="text" name="to" size="11" id="to"
+ align="right" value ="[% to FILTER html %]" maxlength="10"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button" id="button_calendar_to"
+ onclick="showCalendar('to')"><span>Calendar</span>
+ </button>
+ <div id="con_calendar_to"></div>
+ </td>
+ <th>
+ Group by:
+ </th>
+ <td>
+ <select name="group">
+ <option value="when" [% 'selected' IF group == 'when' %]>When</option>
+ <option value="bug" [% 'selected' IF group == 'bug' %]>[% terms.Bug %]</option>
+ </select>
+ </td>
+ <td>
+ <input type="submit" id="run" value="Generate Report">
+ </td>
+</tr>
+
+</table>
+[% IF debug_sql %]
+ <input type="hidden" name="debug" value="1">
+[% END %]
+</form>
+
+<script type="text/javascript">
+ createCalendar('from');
+ createCalendar('to');
+</script>
+
+[% IF action == 'run' %]
+
+[% IF debug_sql %]
+ <pre>[% debug_sql FILTER html %]</pre>
+[% END %]
+
+[% IF incomplete_data %]
+ <p>
+ There used to be an issue in <a href="http://www.bugzilla.org/">Bugzilla</a>
+ which caused activity data to be lost if there were a large number of cc's
+ or dependencies. That has been fixed, but some data was already lost in
+ your activity table that could not be regenerated. The changes that
+ could not reliably determine are prefixed by '?'.
+ </p>
+[% END %]
+
+[% IF operations.size > 0 %]
+ <br>
+ <table border="1" cellpadding="4" cellspacing="0" id="report" class="hover">
+ <tr id="report-header">
+ [% IF who_count > 1 %]
+ <th>Who</th>
+ [% END %]
+ [% IF group == 'when' %]
+ <th class="sorted">[% INCLUDE group_when_link %]</th>
+ <th>[% INCLUDE group_bug_link %]</th>
+ [% ELSE %]
+ <th class="sorted">[% INCLUDE group_bug_link %]</th>
+ <th>[% INCLUDE group_when_link %]</th>
+ [% END %]
+ <th>What</th>
+ <th>Removed</th>
+ <th>Added</th>
+ </tr>
+
+ [% FOREACH operation = operations %]
+ [% tr_class = loop.count % 2 ? 'report_row_even' : 'report_row_odd' %]
+ [% FOREACH change = operation.changes %]
+ <tr class="[% tr_class FILTER none %]">
+ [% IF loop.count == 1 %]
+ [% IF who_count > 1 %]
+ <td>[% operation.who FILTER email FILTER html %]</td>
+ [% END %]
+ [% IF group == 'when' %]
+ <td>[% change.when FILTER time FILTER no_break %]</td>
+ <td>[% operation.bug FILTER bug_link(operation.bug) FILTER none %]</td>
+ [% ELSE %]
+ <td>[% operation.bug FILTER bug_link(operation.bug) FILTER none %]</td>
+ <td>[% change.when FILTER time FILTER no_break %]</td>
+ [% END %]
+ [% ELSE %]
+ [% IF who_count > 1 %]
+ <td>&nbsp;</td>
+ [% END %]
+ <td>&nbsp;</td>
+ [% IF group == 'when' %]
+ <td>&nbsp;</td>
+ [% ELSE %]
+ <td>[% change.when FILTER time FILTER no_break %]</td>
+ [% END %]
+ [% END %]
+ <td>
+ [% IF change.attachid %]
+ <a href="attachment.cgi?id=[% change.attachid FILTER uri %]"
+ title="[% change.attach.description FILTER html %]
+ [%- %] - [% change.attach.filename FILTER html %]"
+ >Attachment #[% change.attachid FILTER html %]</a>
+ [% END %]
+ [%IF change.comment.defined && change.fieldname == 'longdesc' %]
+ [% "Comment $change.comment.count"
+ FILTER bug_link(operation.bug, comment_num => change.comment.count)
+ FILTER none %]
+ [% ELSIF change.comment.defined && change.fieldname == 'comment_tag' %]
+ [% "Comment $change.comment.count Tagged"
+ FILTER bug_link(operation.bug, comment_num => change.comment.count)
+ FILTER none %]
+ [% ELSE %]
+ [%+ field_descs.${change.fieldname} FILTER html %]
+ [% END %]
+ </td>
+ [% PROCESS change_column change_type = change.removed %]
+ [% PROCESS change_column change_type = change.added %]
+ </tr>
+ [% END %]
+ [% END %]
+ </table>
+ <p>
+ <a href="buglist.cgi?bug_id=[% bug_ids.join(',') FILTER uri %]">
+ Show as a [% terms.Bug %] List</a>
+ </p>
+
+[% ELSE %]
+ <p>
+ No changes.
+ </p>
+[% END %]
+
+[% BLOCK change_column %]
+ <td>
+ [% IF change_type.defined %]
+ [% IF change.fieldname == 'estimated_time' ||
+ change.fieldname == 'remaining_time' ||
+ change.fieldname == 'work_time' %]
+ [% PROCESS formattimeunit time_unit=change_type %]
+ [% ELSIF change.fieldname == 'blocked' ||
+ change.fieldname == 'dependson' %]
+ [% change_type FILTER bug_list_link FILTER none %]
+ [% ELSIF change.fieldname == 'assigned_to' ||
+ change.fieldname == 'reporter' ||
+ change.fieldname == 'qa_contact' ||
+ change.fieldname == 'cc' ||
+ change.fieldname == 'flagtypes.name' %]
+ [% display_value(change.fieldname, change_type) FILTER email FILTER html %]
+ [% ELSE %]
+ [% display_value(change.fieldname, change_type) FILTER html %]
+ [% END %]
+ [% ELSE %]
+ &nbsp;
+ [% END %]
+ </td>
+[% END %]
+[% END %]
+
+[% INCLUDE global/footer.html.tmpl %]
+
+[% BLOCK group_when_link %]
+ <a href="page.cgi?id=user_activity.html&amp;action=run&amp;
+ [%~%]who=[% who FILTER uri %]&amp;
+ [%~%]from=[% from FILTER uri %]&amp;
+ [%~%]to=[% to FILTER uri %]&amp;
+ [%~%]group=when">When</a>
+[% END %]
+
+[% BLOCK group_bug_link %]
+ <a href="page.cgi?id=user_activity.html&amp;action=run&amp;
+ [%~%]who=[% who FILTER uri %]&amp;
+ [%~%]from=[% from FILTER uri %]&amp;
+ [%~%]to=[% to FILTER uri %]&amp;
+ [%~%]group=bug">[% terms.Bug %]</a>
+[% END %]
diff --git a/extensions/BMO/template/en/default/search/search-plugin.xml.tmpl b/extensions/BMO/template/en/default/search/search-plugin.xml.tmpl
new file mode 100644
index 000000000..0c52c1a58
--- /dev/null
+++ b/extensions/BMO/template/en/default/search/search-plugin.xml.tmpl
@@ -0,0 +1,17 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
+<ShortName>[% terms.BugzillaTitle %]</ShortName>
+<Description>[% terms.BugzillaTitle %] Quick Search</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image height="16" width="16" type="image/vnd.microsoft.icon">https://bugzilla.mozilla.org/extensions/BMO/web/images/favicon.ico</Image>
+<Url type="text/html" method="GET" template="[% urlbase FILTER xml %]buglist.cgi?quicksearch={searchTerms}"/>
+</OpenSearchDescription>
diff --git a/extensions/BMO/web/core.png b/extensions/BMO/web/core.png
new file mode 100644
index 000000000..b9c5053f6
--- /dev/null
+++ b/extensions/BMO/web/core.png
Binary files differ
diff --git a/extensions/BMO/web/images/advanced.png b/extensions/BMO/web/images/advanced.png
new file mode 100644
index 000000000..71a3fcb78
--- /dev/null
+++ b/extensions/BMO/web/images/advanced.png
Binary files differ
diff --git a/extensions/BMO/web/images/background.png b/extensions/BMO/web/images/background.png
new file mode 100644
index 000000000..eb254aab9
--- /dev/null
+++ b/extensions/BMO/web/images/background.png
Binary files differ
diff --git a/extensions/BMO/web/images/bugzilla.png b/extensions/BMO/web/images/bugzilla.png
new file mode 100644
index 000000000..4b7c10284
--- /dev/null
+++ b/extensions/BMO/web/images/bugzilla.png
Binary files differ
diff --git a/extensions/BMO/web/images/creative.png b/extensions/BMO/web/images/creative.png
new file mode 100644
index 000000000..2aeec79ae
--- /dev/null
+++ b/extensions/BMO/web/images/creative.png
Binary files differ
diff --git a/extensions/BMO/web/images/favicon.ico b/extensions/BMO/web/images/favicon.ico
new file mode 100644
index 000000000..c14fec40a
--- /dev/null
+++ b/extensions/BMO/web/images/favicon.ico
Binary files differ
diff --git a/extensions/BMO/web/images/groups/bugzilla-approvers.png b/extensions/BMO/web/images/groups/bugzilla-approvers.png
new file mode 100644
index 000000000..d2414e041
--- /dev/null
+++ b/extensions/BMO/web/images/groups/bugzilla-approvers.png
Binary files differ
diff --git a/extensions/BMO/web/images/groups/calendar-drivers.png b/extensions/BMO/web/images/groups/calendar-drivers.png
new file mode 100644
index 000000000..fc2c1d1e5
--- /dev/null
+++ b/extensions/BMO/web/images/groups/calendar-drivers.png
Binary files differ
diff --git a/extensions/BMO/web/images/guided.png b/extensions/BMO/web/images/guided.png
new file mode 100644
index 000000000..46ba060f8
--- /dev/null
+++ b/extensions/BMO/web/images/guided.png
Binary files differ
diff --git a/extensions/BMO/web/images/mozchomp.gif b/extensions/BMO/web/images/mozchomp.gif
new file mode 100644
index 000000000..ac6549527
--- /dev/null
+++ b/extensions/BMO/web/images/mozchomp.gif
Binary files differ
diff --git a/extensions/BMO/web/images/mozilla-tab.png b/extensions/BMO/web/images/mozilla-tab.png
new file mode 100644
index 000000000..417f6a5c6
--- /dev/null
+++ b/extensions/BMO/web/images/mozilla-tab.png
Binary files differ
diff --git a/extensions/BMO/web/images/notice.png b/extensions/BMO/web/images/notice.png
new file mode 100644
index 000000000..e436c22ae
--- /dev/null
+++ b/extensions/BMO/web/images/notice.png
Binary files differ
diff --git a/extensions/BMO/web/images/presshat.png b/extensions/BMO/web/images/presshat.png
new file mode 100644
index 000000000..a61de59e5
--- /dev/null
+++ b/extensions/BMO/web/images/presshat.png
Binary files differ
diff --git a/extensions/BMO/web/images/sign_warning.png b/extensions/BMO/web/images/sign_warning.png
new file mode 100644
index 000000000..30963f47d
--- /dev/null
+++ b/extensions/BMO/web/images/sign_warning.png
Binary files differ
diff --git a/extensions/BMO/web/images/stop-sign.gif b/extensions/BMO/web/images/stop-sign.gif
new file mode 100644
index 000000000..9b420ec6c
--- /dev/null
+++ b/extensions/BMO/web/images/stop-sign.gif
Binary files differ
diff --git a/extensions/BMO/web/images/throbber.gif b/extensions/BMO/web/images/throbber.gif
new file mode 100644
index 000000000..bc4fa6561
--- /dev/null
+++ b/extensions/BMO/web/images/throbber.gif
Binary files differ
diff --git a/extensions/BMO/web/images/user-engagement.png b/extensions/BMO/web/images/user-engagement.png
new file mode 100644
index 000000000..11fdbc000
--- /dev/null
+++ b/extensions/BMO/web/images/user-engagement.png
Binary files differ
diff --git a/extensions/BMO/web/js/edit_bug.js b/extensions/BMO/web/js/edit_bug.js
new file mode 100644
index 000000000..41a71935e
--- /dev/null
+++ b/extensions/BMO/web/js/edit_bug.js
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+function init_clone_bug_menu(el, bug_id, product, component) {
+ var diff_url = 'enter_bug.cgi?format=__default__&cloned_bug_id=' + bug_id;
+ var cur_url = diff_url +
+ '&product=' + encodeURIComponent(product) +
+ '&component=' + encodeURIComponent(component);
+ var menu = new YAHOO.widget.Menu('clone_bug_menu', { position : 'dynamic' });
+ menu.addItems([
+ { text: 'Clone to the current product', url: cur_url },
+ { text: 'Clone to a different product', url: diff_url }
+ ]);
+ menu.render(document.body);
+ YAHOO.util.Event.addListener(el, 'click', show_clone_bug_menu, menu);
+}
+
+function show_clone_bug_menu(event, menu) {
+ menu.cfg.setProperty('xy', YAHOO.util.Event.getXY(event));
+ menu.show();
+ event.preventDefault();
+}
+
+// -- make attachment table, comments, new comment textarea equal widths
+
+YAHOO.util.Event.onDOMReady(function() {
+ var comment_tables = Dom.getElementsByClassName('bz_comment_table', 'table', 'comments');
+ if (comment_tables.length) {
+ var comment_width = comment_tables[0].getElementsByTagName('td')[0].clientWidth + 'px';
+ var attachment_table = Dom.get('attachment_table');
+ if (attachment_table)
+ attachment_table.style.width = comment_width;
+ var new_comment = Dom.get('comment');
+ if (new_comment)
+ new_comment.style.width = comment_width;
+ }
+});
diff --git a/extensions/BMO/web/js/edituser_menu.js b/extensions/BMO/web/js/edituser_menu.js
new file mode 100644
index 000000000..383418430
--- /dev/null
+++ b/extensions/BMO/web/js/edituser_menu.js
@@ -0,0 +1,33 @@
+var usermenu_widget;
+
+YAHOO.util.Event.onDOMReady(function() {
+ usermenu_widget = new YAHOO.widget.Menu('usermenu_widget', { position : 'dynamic' });
+ usermenu_widget.addItems([
+ { text: 'Profile', url: '#', target: '_blank' },
+ { text: 'Activity', url: '#', target: '_blank' },
+ { text: 'Mail', url: '#', target: '_blank' },
+ { text: 'Edit', url: '#', target: '_blank' }
+ ]);
+ usermenu_widget.render(document.body);
+});
+
+function show_usermenu(event, id, email, show_edit) {
+ if (!usermenu_widget)
+ return true;
+ if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey)
+ return true;
+ usermenu_widget.getItem(0).cfg.setProperty('url',
+ 'user_profile?login=' + encodeURIComponent(email));
+ usermenu_widget.getItem(1).cfg.setProperty('url',
+ 'page.cgi?id=user_activity.html&action=run&from=-14d&who=' + encodeURIComponent(email));
+ usermenu_widget.getItem(2).cfg.setProperty('url', 'mailto:' + encodeURIComponent(email));
+ if (show_edit) {
+ usermenu_widget.getItem(3).cfg.setProperty('url', 'editusers.cgi?action=edit&userid=' + id);
+ } else {
+ usermenu_widget.removeItem(3);
+ }
+ usermenu_widget.cfg.setProperty('xy', YAHOO.util.Event.getXY(event));
+ usermenu_widget.show();
+ return false;
+}
+
diff --git a/extensions/BMO/web/js/form_validate.js b/extensions/BMO/web/js/form_validate.js
new file mode 100644
index 000000000..7e9746a5c
--- /dev/null
+++ b/extensions/BMO/web/js/form_validate.js
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+/**
+ * Form Validation and Interaction
+ **/
+
+//Makes sure that there is an '@' in the address with a '.'
+//somewhere after it (and at least one character in between them)
+function isValidEmail(email) {
+ var at_index = email.indexOf("@");
+ var last_dot = email.lastIndexOf(".");
+ return at_index > 0 && last_dot > (at_index + 1);
+}
+
+//Takes a DOM element id and makes sure that it is filled out
+function isFilledOut(elem_id) {
+ var el = document.getElementById(elem_id);
+ if (!el) {
+ console.error('Failed to find element: ' + elem_id);
+ return false;
+ }
+ var str = el.value;
+ return str.length > 0 && str != "noneselected";
+}
+
+function isChecked(elem_id) {
+ return document.getElementById(elem_id).checked;
+}
+
+function isOneChecked(form_nodelist) {
+ for (var i = 0, il = form_nodelist.length; i < il; i++) {
+ if (form_nodelist[i].checked)
+ return true;
+ }
+ return false;
+}
diff --git a/extensions/BMO/web/js/release_tracking_report.js b/extensions/BMO/web/js/release_tracking_report.js
new file mode 100644
index 000000000..840b57df1
--- /dev/null
+++ b/extensions/BMO/web/js/release_tracking_report.js
@@ -0,0 +1,203 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+var Dom = YAHOO.util.Dom;
+var flagEl;
+var productEl;
+var trackingEl;
+var selectedFields;
+
+// events
+
+function onFieldToggle(cbEl, id) {
+ if (cbEl.checked) {
+ Dom.removeClass('field_' + id + '_td', 'disabled');
+ selectedFields['field_' + id] = id;
+ } else {
+ Dom.addClass('field_' + id + '_td', 'disabled');
+ selectedFields['field_' + id] = false;
+ }
+ Dom.get('field_' + id + '_select').disabled = !cbEl.checked;
+ serialiseForm();
+}
+
+function onProductChange() {
+ var product = productEl.value;
+ var productData = product == '0' ? getFlagByName(flagEl.value) : getProductById(product);
+ var html = '';
+ selectedFields = new Array();
+
+ if (productData) {
+ // update status fields
+ html = '<table>';
+ for(var i = 0, l = productData.fields.length; i < l; i++) {
+ var field = getFieldById(productData.fields[i]);
+ selectedFields['field_' + field.id] = false;
+ html += '<tr>' +
+ '<td>' +
+ '<input type="checkbox" id="field_' + field.id + '_cb" ' +
+ 'onClick="onFieldToggle(this,' + field.id + ')">' +
+ '</td>' +
+ '<td class="disabled" id="field_' + field.id + '_td">' +
+ '<label for="field_' + field.id + '_cb">' +
+ YAHOO.lang.escapeHTML(field.name) + ':</label>' +
+ '</td>' +
+ '<td>' +
+ '<select disabled id="field_' + field.id + '_select">' +
+ '<option value="+">fixed</option>' +
+ '<option value="-">not fixed</option>' +
+ '</select>' +
+ '</td>' +
+ '</tr>';
+ }
+ html += '</table>';
+ }
+ trackingEl.innerHTML = html;
+ serialiseForm();
+}
+
+function onFlagChange() {
+ var flag = flagEl.value;
+ var flagData = getFlagByName(flag);
+ productEl.options.length = 0;
+
+ if (flagData) {
+ // update product select
+ var currentProduct = productEl.value;
+ productEl.options[0] = new Option('(Any Product)', '0');
+ for(var i = 0, l = flagData.products.length; i < l; i++) {
+ var product = getProductById(flagData.products[i]);
+ var n = productEl.length;
+ productEl.options[n] = new Option(product.name, product.id);
+ productEl.options[n].selected = product.id == currentProduct;
+ }
+ }
+ onProductChange();
+}
+
+// form
+
+function selectAllFields() {
+ for(var i = 0, l = fields_data.length; i < l; i++) {
+ var cb = Dom.get('field_' + fields_data[i].id + '_cb');
+ cb.checked = true;
+ onFieldToggle(cb, fields_data[i].id);
+ }
+ serialiseForm();
+}
+
+function selectNoFields() {
+ for(var i = 0, l = fields_data.length; i < l; i++) {
+ var cb = Dom.get('field_' + fields_data[i].id + '_cb');
+ cb.checked = false;
+ onFieldToggle(cb, fields_data[i].id);
+ }
+ serialiseForm();
+}
+
+function invertFields() {
+ for(var i = 0, l = fields_data.length; i < l; i++) {
+ var el = Dom.get('field_' + fields_data[i].id + '_select');
+ if (el.value == '+') {
+ el.options[1].selected = true;
+ } else {
+ el.options[0].selected = true;
+ }
+ }
+ serialiseForm();
+}
+
+function onFormSubmit() {
+ serialiseForm();
+ return true;
+}
+
+function onFormReset() {
+ deserialiseForm('');
+}
+
+function serialiseForm() {
+ var q = flagEl.value + ':' +
+ Dom.get('flag_value').value + ':' +
+ Dom.get('range').value + ':' +
+ productEl.value + ':' +
+ Dom.get('op').value + ':';
+
+ for(var id in selectedFields) {
+ if (selectedFields[id]) {
+ q += selectedFields[id] + Dom.get(id + '_select').value + ':';
+ }
+ }
+
+ Dom.get('q').value = q;
+ Dom.get('bookmark').href = 'page.cgi?id=release_tracking_report.html&q=' +
+ encodeURIComponent(q);
+}
+
+function deserialiseForm(q) {
+ var parts = q.split(/:/);
+ selectValue(flagEl, parts[0]);
+ onFlagChange();
+ selectValue(Dom.get('flag_value'), parts[1]);
+ selectValue(Dom.get('range'), parts[2]);
+ selectValue(productEl, parts[3]);
+ onProductChange();
+ selectValue(Dom.get('op'), parts[4]);
+ for(var i = 5, l = parts.length; i < l; i++) {
+ var part = parts[i];
+ if (part.length) {
+ var value = part.substr(part.length - 1, 1);
+ var id = part.substr(0, part.length - 1);
+ var cb = Dom.get('field_' + id + '_cb');
+ cb.checked = true;
+ onFieldToggle(cb, id);
+ selectValue(Dom.get('field_' + id + '_select'), value);
+ }
+ }
+ serialiseForm();
+}
+
+// utils
+
+YAHOO.util.Event.onDOMReady(function() {
+ flagEl = Dom.get('flag');
+ productEl = Dom.get('product');
+ trackingEl = Dom.get('tracking_span');
+ onFlagChange();
+ deserialiseForm(default_query);
+});
+
+function getFlagByName(name) {
+ for(var i = 0, l = flags_data.length; i < l; i++) {
+ if (flags_data[i].name == name)
+ return flags_data[i];
+ }
+}
+
+function getProductById(id) {
+ for(var i = 0, l = products_data.length; i < l; i++) {
+ if (products_data[i].id == id)
+ return products_data[i];
+ }
+}
+
+function getFieldById(id) {
+ for(var i = 0, l = fields_data.length; i < l; i++) {
+ if (fields_data[i].id == id)
+ return fields_data[i];
+ }
+}
+
+function selectValue(el, value) {
+ for(var i = 0, l = el.options.length; i < l; i++) {
+ if (el.options[i].value == value) {
+ el.options[i].selected = true;
+ return;
+ }
+ }
+ el.options[0].selected = true;
+}
diff --git a/extensions/BMO/web/js/sorttable.js b/extensions/BMO/web/js/sorttable.js
new file mode 100644
index 000000000..0873dc20a
--- /dev/null
+++ b/extensions/BMO/web/js/sorttable.js
@@ -0,0 +1,709 @@
+/*
+ SortTable
+ version 2
+ 7th April 2007
+ Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/
+
+ Instructions:
+ Download this file
+ Add <script src="sorttable.js"></script> to your HTML
+ Add class="sortable" to any table you'd like to make sortable
+ Click on the headers to sort
+
+ Thanks to many, many people for contributions and suggestions.
+ Licenced as X11: http://www.kryogenix.org/code/browser/licence.html
+ This basically means: do what you want with it.
+*/
+
+var stIsIE = /*@cc_on!@*/false;
+
+sorttable = {
+ init: function() {
+ // quit if this function has already been called
+ if (arguments.callee.done) return;
+ // flag this function so we don't do the same thing twice
+ arguments.callee.done = true;
+ // kill the timer
+ if (_timer) clearInterval(_timer);
+
+ if (!document.createElement || !document.getElementsByTagName) return;
+
+ sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/;
+
+ forEach(document.getElementsByTagName('table'), function(table) {
+ if (table.className.search(/\bsortable\b/) != -1) {
+ sorttable.makeSortable(table);
+ }
+ });
+
+ },
+
+ /*
+ * Prepares the table so that it can be sorted
+ *
+ */
+ makeSortable: function(table) {
+
+ if (table.getElementsByTagName('thead').length == 0) {
+ // table doesn't have a tHead. Since it should have, create one and
+ // put the first table row in it.
+ the = document.createElement('thead');
+ the.appendChild(table.rows[0]);
+ table.insertBefore(the,table.firstChild);
+ }
+ // Safari doesn't support table.tHead, sigh
+ if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0];
+
+ //if (table.tHead.rows.length != 1) return; // can't cope with two header rows
+
+ // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as
+ // "total" rows, for example). This is B&R, since what you're supposed
+ // to do is put them in a tfoot. So, if there are sortbottom rows,
+ // for backwards compatibility, move them to tfoot (creating it if needed).
+ sortbottomrows = [];
+ for (var i=0; i<table.rows.length; i++) {
+ if (table.rows[i].className.search(/\bsortbottom\b/) != -1) {
+ sortbottomrows[sortbottomrows.length] = table.rows[i];
+ }
+ }
+
+ if (sortbottomrows) {
+ if (table.tFoot == null) {
+ // table doesn't have a tfoot. Create one.
+ tfo = document.createElement('tfoot');
+ table.appendChild(tfo);
+ }
+ for (var i=0; i<sortbottomrows.length; i++) {
+ tfo.appendChild(sortbottomrows[i]);
+ }
+ delete sortbottomrows;
+ }
+
+ sorttable._walk_through_headers(table);
+ },
+
+ /*
+ * Helper function for preparing the table
+ *
+ */
+ _walk_through_headers: function(table) {
+ // First, gather some information we need to sort the table.
+ var bodies = [];
+ var table_rows = [];
+ var body_size = table.tBodies[0].rows.length;
+
+ // We need to get all the rows
+ for (var i=0; i<table.tBodies.length; i++) {
+ if (!table.tBodies[i].className.match(/\bsorttable_body\b/))
+ continue;
+
+ bodies[bodies.length] = table.tBodies[i];
+ for (j=0; j<table.tBodies[i].rows.length; j++) {
+ table_rows[table_rows.length] = table.tBodies[i].rows[j];
+ }
+ }
+
+ table.sorttable_rows = table_rows;
+ table.sorttable_body_size = body_size;
+ table.sorttable_bodies = bodies;
+
+
+ // work through each column and calculate its type
+
+ // For each row in the header..
+ for (var row_index=0; row_index < table.tHead.rows.length; row_index++) {
+
+ headrow = table.tHead.rows[row_index].cells;
+ // ... Walk through each column and calculate the type.
+ for (var i=0; i<headrow.length; i++) {
+ // Don't sort this column, please
+ if (headrow[i].className.match(/\bsorttable_nosort\b/)) continue;
+
+ // Override sort column index.
+ column_index = i;
+ mtch = headrow[i].className.match(/\bsortable_column_([a-z0-9]+)\b/);
+ if (mtch) column_index = mtch[1];
+
+
+ // Manually override the type with a sorttable_type attribute
+ // Override sort function
+ mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/);
+ if (mtch) override = mtch[1];
+
+ if (mtch && typeof sorttable["sort_"+override] == 'function') {
+ headrow[i].sorttable_sortfunction = sorttable["sort_"+override];
+ } else {
+ headrow[i].sorttable_sortfunction = sorttable.guessType(table, column_index);
+ }
+
+ // make it clickable to sort
+ headrow[i].sorttable_columnindex = column_index;
+ headrow[i].table = table;
+
+ // If the header contains a link, clear the href.
+ for (var k=0; k<headrow[i].childNodes.length; k++) {
+ if (headrow[i].childNodes[k].tagName == 'A') {
+ headrow[i].childNodes[k].href = "javascript:void(0);";
+ }
+ }
+
+ dean_addEvent(headrow[i], "click", sorttable._on_column_header_clicked);
+
+ } // inner for (var i=0; i<headrow.length; i++)
+ } // outer for
+ },
+
+
+
+
+ /*
+ * Helper function for the _on_column_header_clicked handler
+ *
+ */
+
+ _remove_sorted_classes: function(header) {
+ // For each row in the header..
+ for (var j=0; j< header.rows.length; j++) {
+ // ... Walk through each column and calculate the type.
+ row = header.rows[j].cells;
+
+ for (var i=0; i<row.length; i++) {
+ cell = row[i];
+ if (cell.nodeType != 1) return; // an element
+
+ mtch = cell.className.match(/\bsorted_([0-9]+)\b/);
+ if (mtch) {
+ cell.className = cell.className.replace('sorted_'+mtch[1],
+ 'sorted_'+(parseInt(mtch[1])+1));
+ }
+
+ cell.className = cell.className.replace('sorttable_sorted_reverse','');
+ cell.className = cell.className.replace('sorttable_sorted','');
+ }
+ }
+ },
+
+ _check_already_sorted: function(cell) {
+ if (cell.className.search(/\bsorttable_sorted\b/) != -1) {
+ // if we're already sorted by this column, just
+ // reverse the table, which is quicker
+ sorttable.reverse_table(cell);
+
+ sorttable._mark_column_as_sorted(cell, '&#x25BC;', 1);
+ return 1;
+ }
+
+ if (cell.className.search(/\bsorttable_sorted_reverse\b/) != -1) {
+ // if we're already sorted by this column in reverse, just
+ // re-reverse the table, which is quicker
+ sorttable.reverse_table(cell);
+
+ sorttable._mark_column_as_sorted(cell, '&#x25B2;', 0);
+
+ return 1;
+ }
+
+ return 0;
+ },
+
+ /* Visualy mark the cell as sorted.
+ *
+ * @param cell: the cell being marked
+ * @param text: the text being used to mark. you can use html
+ * @param reversed: whether the column is reversed or not.
+ *
+ */
+ _mark_column_as_sorted: function(cell, text, reversed) {
+ // remove eventual class
+ cell.className = cell.className.replace('sorttable_sorted', '');
+ cell.className = cell.className.replace('sorttable_sorted_reverse', '');
+
+ // the column is reversed
+ if (reversed) {
+ cell.className += ' sorttable_sorted_reverse';
+ }
+ else {
+ // remove eventual class
+ cell.className += ' sorttable_sorted';
+ }
+
+ sorttable._remove_sorting_marker();
+
+ marker = document.createElement('span');
+ marker.id = "sorttable_sort_mark";
+ marker.className = "bz_sort_order_primary";
+ marker.innerHTML = text;
+ cell.appendChild(marker);
+ },
+
+ _remove_sorting_marker: function() {
+ mark = document.getElementById('sorttable_sort_mark');
+ if (mark) { mark.parentNode.removeChild(mark); }
+ els = sorttable._getElementsByClassName('bz_sort_order_primary');
+ for(var i=0,j=els.length; i<j; i++) {
+ els[i].parentNode.removeChild(els[i]);
+ }
+ els = sorttable._getElementsByClassName('bz_sort_order_secondary');
+ for(var i=0,j=els.length; i<j; i++) {
+ els[i].parentNode.removeChild(els[i]);
+ }
+ },
+
+ _getElementsByClassName: function(classname, node) {
+ if(!node) node = document.getElementsByTagName("body")[0];
+ var a = [];
+ var re = new RegExp('\\b' + classname + '\\b');
+ var els = node.getElementsByTagName("*");
+ for(var i=0,j=els.length; i<j; i++)
+ if(re.test(els[i].className))a.push(els[i]);
+ return a;
+ },
+
+ /*
+ * This is the callback for when the table header is clicked.
+ *
+ * @param evt: the event that triggered this callback
+ */
+ _on_column_header_clicked: function(evt) {
+
+ // The table is already sorted by this column. Just reverse it.
+ if (sorttable._check_already_sorted(this))
+ return;
+
+
+ // First, remove sorttable_sorted classes from the other header
+ // that is currently sorted and its marker (the simbol indicating
+ // that its sorted.
+ sorttable._remove_sorted_classes(this.table.tHead);
+ mtch = this.className.match(/\bsorted_([0-9]+)\b/);
+ if (mtch) {
+ this.className = this.className.replace('sorted_'+mtch[1], '');
+ }
+ this.className += ' sorted_0 ';
+
+ // This is the text that indicates that the column is sorted.
+ sorttable._mark_column_as_sorted(this, '&#x25BC;', 0);
+
+ sorttable.sort_table(this);
+
+ },
+
+ sort_table: function(cell) {
+ // build an array to sort. This is a Schwartzian transform thing,
+ // i.e., we "decorate" each row with the actual sort key,
+ // sort based on the sort keys, and then put the rows back in order
+ // which is a lot faster because you only do getInnerText once per row
+ col = cell.sorttable_columnindex;
+ rows = cell.table.sorttable_rows;
+
+ var BUGLIST = '';
+
+ for (var j = 0; j < cell.table.sorttable_rows.length; j++) {
+ rows[j].sort_data = sorttable.getInnerText(rows[j].cells[col]);
+ }
+
+ /* If you want a stable sort, uncomment the following line */
+ sorttable.shaker_sort(rows, cell.sorttable_sortfunction);
+ /* and comment out this one */
+ //rows.sort(cell.sorttable_sortfunction);
+
+ // Rebuild the table, using he sorted rows.
+ tb = cell.table.sorttable_bodies[0];
+ body_size = cell.table.sorttable_body_size;
+ body_index = 0;
+
+ for (var j=0; j<rows.length; j++) {
+ if (j % 2)
+ rows[j].className = rows[j].className.replace('bz_row_even',
+ 'bz_row_odd');
+ else
+ rows[j].className = rows[j].className.replace('bz_row_odd',
+ 'bz_row_even');
+
+ tb.appendChild(rows[j]);
+ var bug_id = sorttable.getInnerText(rows[j].cells[0].childNodes[1]);
+ BUGLIST = BUGLIST ? BUGLIST+':'+bug_id : bug_id;
+
+ if (j % body_size == body_size-1) {
+ body_index++;
+ if (body_index < cell.table.sorttable_bodies.length) {
+ tb = cell.table.sorttable_bodies[body_index];
+ }
+ }
+ }
+
+ document.cookie = 'BUGLIST='+BUGLIST;
+
+ cell.table.sorttable_rows = rows;
+ },
+
+ reverse_table: function(cell) {
+ oldrows = cell.table.sorttable_rows;
+ newrows = [];
+
+ for (var i=0; i < oldrows.length; i++) {
+ newrows[newrows.length] = oldrows[i];
+ }
+
+ tb = cell.table.sorttable_bodies[0];
+ body_size = cell.table.sorttable_body_size;
+ body_index = 0;
+
+ var BUGLIST = '';
+
+ cell.table.sorttable_rows = [];
+ for (var i = newrows.length-1; i >= 0; i--) {
+ if (i % 2)
+ newrows[i].className = newrows[i].className.replace('bz_row_even',
+ 'bz_row_odd');
+ else
+ newrows[i].className = newrows[i].className.replace('bz_row_odd',
+ 'bz_row_even');
+
+ tb.appendChild(newrows[i]);
+ cell.table.sorttable_rows.push(newrows[i]);
+
+ var bug_id = sorttable.getInnerText(newrows[i].cells[0].childNodes[1]);
+ BUGLIST = BUGLIST ? BUGLIST+':'+bug_id : bug_id;
+
+ if ((newrows.length-1-i) % body_size == body_size-1) {
+ body_index++;
+ if (body_index < cell.table.sorttable_bodies.length) {
+ tb = cell.table.sorttable_bodies[body_index];
+ }
+ }
+
+ }
+
+ document.cookie = 'BUGLIST='+BUGLIST;
+
+ delete newrows;
+ },
+
+ guessType: function(table, column) {
+ // guess the type of a column based on its first non-blank row
+ sortfn = sorttable.sort_alpha;
+ for (var i=0; i<table.sorttable_bodies[0].rows.length; i++) {
+ text = sorttable.getInnerText(table.sorttable_bodies[0].rows[i].cells[column]);
+ if (text != '') {
+ if (text.match(/^-?[£$¤]?[\d,.]+%?$/)) {
+ return sorttable.sort_numeric;
+ }
+ // check for a date: dd/mm/yyyy or dd/mm/yy
+ // can have / or . or - as separator
+ // can be mm/dd as well
+ possdate = text.match(sorttable.DATE_RE)
+ if (possdate) {
+ // looks like a date
+ first = parseInt(possdate[1]);
+ second = parseInt(possdate[2]);
+ if (first > 12) {
+ // definitely dd/mm
+ return sorttable.sort_ddmm;
+ } else if (second > 12) {
+ return sorttable.sort_mmdd;
+ } else {
+ // looks like a date, but we can't tell which, so assume
+ // that it's dd/mm (English imperialism!) and keep looking
+ sortfn = sorttable.sort_ddmm;
+ }
+ }
+ }
+ }
+ return sortfn;
+ },
+
+ getInnerText: function(node) {
+ // gets the text we want to use for sorting for a cell.
+ // strips leading and trailing whitespace.
+ // this is *not* a generic getInnerText function; it's special to sorttable.
+ // for example, you can override the cell text with a customkey attribute.
+ // it also gets .value for <input> fields.
+
+ hasInputs = (typeof node.getElementsByTagName == 'function') &&
+ node.getElementsByTagName('input').length;
+
+ if (typeof node.getAttribute != 'undefined' && node.getAttribute("sorttable_customkey") != null) {
+ return node.getAttribute("sorttable_customkey");
+ }
+ else if (typeof node.textContent != 'undefined' && !hasInputs) {
+ return node.textContent.replace(/^\s+|\s+$/g, '');
+ }
+ else if (typeof node.innerText != 'undefined' && !hasInputs) {
+ return node.innerText.replace(/^\s+|\s+$/g, '');
+ }
+ else if (typeof node.text != 'undefined' && !hasInputs) {
+ return node.text.replace(/^\s+|\s+$/g, '');
+ }
+ else {
+ switch (node.nodeType) {
+ case 3:
+ if (node.nodeName.toLowerCase() == 'input') {
+ return node.value.replace(/^\s+|\s+$/g, '');
+ }
+ case 4:
+ return node.nodeValue.replace(/^\s+|\s+$/g, '');
+ break;
+ case 1:
+ case 11:
+ var innerText = '';
+ for (var i = 0; i < node.childNodes.length; i++) {
+ innerText += sorttable.getInnerText(node.childNodes[i]);
+ }
+ return innerText.replace(/^\s+|\s+$/g, '');
+ break;
+ default:
+ return '';
+ }
+ }
+ },
+
+ /* sort functions
+ each sort function takes two parameters, a and b
+ you are comparing a.sort_data and b.sort_data */
+ sort_numeric: function(a,b) {
+ aa = parseFloat(a.sort_data.replace(/[^0-9.-]/g,''));
+ if (isNaN(aa)) aa = 0;
+ bb = parseFloat(b.sort_data.replace(/[^0-9.-]/g,''));
+ if (isNaN(bb)) bb = 0;
+ return aa-bb;
+ },
+
+ sort_alpha: function(a,b) {
+ if (a.sort_data.toLowerCase()==b.sort_data.toLowerCase()) return 0;
+ if (a.sort_data.toLowerCase()<b.sort_data.toLowerCase()) return -1;
+ return 1;
+ },
+
+ sort_ddmm: function(a,b) {
+ mtch = a.sort_data.match(sorttable.DATE_RE);
+ y = mtch[3]; m = mtch[2]; d = mtch[1];
+ if (m.length == 1) m = '0'+m;
+ if (d.length == 1) d = '0'+d;
+ dt1 = y+m+d;
+ mtch = b.sort_data.match(sorttable.DATE_RE);
+ y = mtch[3]; m = mtch[2]; d = mtch[1];
+ if (m.length == 1) m = '0'+m;
+ if (d.length == 1) d = '0'+d;
+ dt2 = y+m+d;
+ if (dt1==dt2) return 0;
+ if (dt1<dt2) return -1;
+ return 1;
+ },
+
+ sort_mmdd: function(a,b) {
+ mtch = a.sort_data.match(sorttable.DATE_RE);
+ y = mtch[3]; d = mtch[2]; m = mtch[1];
+ if (m.length == 1) m = '0'+m;
+ if (d.length == 1) d = '0'+d;
+ dt1 = y+m+d;
+ mtch = b.sort_data.match(sorttable.DATE_RE);
+ y = mtch[3]; d = mtch[2]; m = mtch[1];
+ if (m.length == 1) m = '0'+m;
+ if (d.length == 1) d = '0'+d;
+ dt2 = y+m+d;
+ if (dt1==dt2) return 0;
+ if (dt1<dt2) return -1;
+ return 1;
+ },
+
+ shaker_sort: function(list, comp_func) {
+ // A stable sort function to allow multi-level sorting of data
+ // see: http://en.wikipedia.org/wiki/Cocktail_sort
+ // thanks to Joseph Nahmias
+ var b = 0;
+ var t = list.length - 1;
+ var swap = true;
+
+ while(swap) {
+ swap = false;
+ for(var i = b; i < t; ++i) {
+ if ( comp_func(list[i], list[i+1]) > 0 ) {
+ var q = list[i]; list[i] = list[i+1]; list[i+1] = q;
+ swap = true;
+ }
+ } // for
+ t--;
+
+ if (!swap) break;
+
+ for(var i = t; i > b; --i) {
+ if ( comp_func(list[i], list[i-1]) < 0 ) {
+ var q = list[i]; list[i] = list[i-1]; list[i-1] = q;
+ swap = true;
+ }
+ } // for
+ b++;
+
+ } // while(swap)
+ }
+}
+
+/* ******************************************************************
+ Supporting functions: bundled here to avoid depending on a library
+ ****************************************************************** */
+
+// Dean Edwards/Matthias Miller/John Resig
+
+/* for Mozilla/Opera9 */
+if (document.addEventListener) {
+ document.addEventListener("DOMContentLoaded", sorttable.init, false);
+}
+
+/* for Internet Explorer */
+/*@cc_on @*/
+/*@if (@_win32)
+ // IE doesn't have a way to test if the DOM is loaded
+ // doing a deferred script load with onReadyStateChange checks is
+ // problematic, so poll the document until it is scrollable
+ // http://blogs.atlassian.com/developer/2008/03/when_ie_says_dom_is_ready_but.html
+ var loadTestTimer = function() {
+ try {
+ if (document.readyState != "loaded" && document.readyState != "complete") {
+ document.documentElement.doScroll("left");
+ }
+ sorttable.init(); // call the onload handler
+ } catch(error) {
+ setTimeout(loadTestTimer, 100);
+ }
+ };
+ loadTestTimer();
+/*@end @*/
+
+/* for Safari */
+if (/WebKit/i.test(navigator.userAgent)) { // sniff
+ var _timer = setInterval(function() {
+ if (/loaded|complete/.test(document.readyState)) {
+ sorttable.init(); // call the onload handler
+ }
+ }, 10);
+}
+
+/* for other browsers */
+window.onload = sorttable.init;
+
+// written by Dean Edwards, 2005
+// with input from Tino Zijdel, Matthias Miller, Diego Perini
+
+// http://dean.edwards.name/weblog/2005/10/add-event/
+
+function dean_addEvent(element, type, handler) {
+ if (element.addEventListener) {
+ element.addEventListener(type, handler, false);
+ } else {
+ // assign each event handler a unique ID
+ if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++;
+ // create a hash table of event types for the element
+ if (!element.events) element.events = {};
+ // create a hash table of event handlers for each element/event pair
+ var handlers = element.events[type];
+ if (!handlers) {
+ handlers = element.events[type] = {};
+ // store the existing event handler (if there is one)
+ if (element["on" + type]) {
+ handlers[0] = element["on" + type];
+ }
+ }
+ // store the event handler in the hash table
+ handlers[handler.$$guid] = handler;
+ // assign a global event handler to do all the work
+ element["on" + type] = handleEvent;
+ }
+};
+// a counter used to create unique IDs
+dean_addEvent.guid = 1;
+
+function removeEvent(element, type, handler) {
+ if (element.removeEventListener) {
+ element.removeEventListener(type, handler, false);
+ } else {
+ // delete the event handler from the hash table
+ if (element.events && element.events[type]) {
+ delete element.events[type][handler.$$guid];
+ }
+ }
+};
+
+function handleEvent(event) {
+ var returnValue = true;
+ // grab the event object (IE uses a global event object)
+ event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
+ // get a reference to the hash table of event handlers
+ var handlers = this.events[event.type];
+ // execute each event handler
+ for (var i in handlers) {
+ this.$$handleEvent = handlers[i];
+ if (this.$$handleEvent(event) === false) {
+ returnValue = false;
+ }
+ }
+ return returnValue;
+};
+
+function fixEvent(event) {
+ // add W3C standard event methods
+ event.preventDefault = fixEvent.preventDefault;
+ event.stopPropagation = fixEvent.stopPropagation;
+ return event;
+};
+fixEvent.preventDefault = function() {
+ this.returnValue = false;
+};
+fixEvent.stopPropagation = function() {
+ this.cancelBubble = true;
+}
+
+// Dean's forEach: http://dean.edwards.name/base/forEach.js
+/*
+ forEach, version 1.0
+ Copyright 2006, Dean Edwards
+ License: http://www.opensource.org/licenses/mit-license.php
+*/
+
+// array-like enumeration
+if (!Array.forEach) { // mozilla already supports this
+ Array.forEach = function(array, block, context) {
+ for (var i = 0; i < array.length; i++) {
+ block.call(context, array[i], i, array);
+ }
+ };
+}
+
+// generic enumeration
+Function.prototype.forEach = function(object, block, context) {
+ for (var key in object) {
+ if (typeof this.prototype[key] == "undefined") {
+ block.call(context, object[key], key, object);
+ }
+ }
+};
+
+// character enumeration
+String.forEach = function(string, block, context) {
+ Array.forEach(string.split(""), function(chr, index) {
+ block.call(context, chr, index, string);
+ });
+};
+
+// globally resolve forEach enumeration
+var forEach = function(object, block, context) {
+ if (object) {
+ var resolve = Object; // default
+ if (object instanceof Function) {
+ // functions have a "length" property
+ resolve = Function;
+ } else if (object.forEach instanceof Function) {
+ // the object implements a custom forEach method so use that
+ object.forEach(block, context);
+ return;
+ } else if (typeof object == "string") {
+ // the object is a string
+ resolve = String;
+ } else if (typeof object.length == "number") {
+ // the object is array-like
+ resolve = Array;
+ }
+ resolve.forEach(object, block, context);
+ }
+};
+
diff --git a/extensions/BMO/web/js/swag.js b/extensions/BMO/web/js/swag.js
new file mode 100644
index 000000000..cd9561b54
--- /dev/null
+++ b/extensions/BMO/web/js/swag.js
@@ -0,0 +1,60 @@
+/**
+ * Swag Request Form Functions
+ * Form Interal Swag Request Form
+ * dtran
+ * 7/6/09
+ **/
+
+
+function evalToNumber(numberString) {
+ if(numberString=='') return 0;
+ return parseInt(numberString);
+}
+
+function evalToNumberString(numberString) {
+ if(numberString=='') return '0';
+ return numberString;
+}
+//item_array should be an array of DOM element ids
+function getTotal(item_array) {
+ var total = 0;
+ for(var i in item_array) {
+ total += evalToNumber(document.getElementById(item_array[i]).value);
+ }
+ return total;
+}
+
+function calculateTotalSwag() {
+ document.getElementById('Totalswag').value =
+ getTotal( new Array('Lanyards',
+ 'Stickers',
+ 'Bracelets',
+ 'Tattoos',
+ 'Buttons',
+ 'Posters'));
+
+}
+
+
+function calculateTotalMensShirts() {
+ document.getElementById('mens_total').value =
+ getTotal( new Array('mens_s',
+ 'mens_m',
+ 'mens_l',
+ 'mens_xl',
+ 'mens_xxl',
+ 'mens_xxxl'));
+
+}
+
+
+function calculateTotalWomensShirts() {
+ document.getElementById('womens_total').value =
+ getTotal( new Array('womens_s',
+ 'womens_m',
+ 'womens_l',
+ 'womens_xl',
+ 'womens_xxl',
+ 'womens_xxxl'));
+
+}
diff --git a/extensions/BMO/web/js/triage_reports.js b/extensions/BMO/web/js/triage_reports.js
new file mode 100644
index 000000000..855b577d7
--- /dev/null
+++ b/extensions/BMO/web/js/triage_reports.js
@@ -0,0 +1,83 @@
+var Dom = YAHOO.util.Dom;
+
+function onSelectProduct() {
+ var component = Dom.get('component');
+ if (Dom.get('product').value == '') {
+ bz_clearOptions(component);
+ return;
+ }
+ selectProduct(Dom.get('product'), component);
+ // selectProduct only supports __Any__ on both elements
+ // we only want it on component, so add it back in
+ try {
+ component.add(new Option('__Any__', ''), component.options[0]);
+ } catch(e) {
+ // support IE
+ component.add(new Option('__Any__', ''), 0);
+ }
+ component.value = '';
+}
+
+function onCommenterChange() {
+ var commenter_is = Dom.get('commenter_is');
+ if (Dom.get('commenter').value == 'is') {
+ Dom.removeClass(commenter_is, 'hidden');
+ } else {
+ Dom.addClass(commenter_is, 'hidden');
+ }
+}
+
+function onLastChange() {
+ var last_is_span = Dom.get('last_is_span');
+ if (Dom.get('last').value == 'is') {
+ Dom.removeClass(last_is_span, 'hidden');
+ } else {
+ Dom.addClass(last_is_span, 'hidden');
+ }
+}
+
+function onGenerateReport() {
+ if (Dom.get('product').value == '') {
+ alert('You must select a product.');
+ return false;
+ }
+ if (Dom.get('component').value == '' && !Dom.get('component').options[0].selected) {
+ alert('You must select at least one component.');
+ return false;
+ }
+ if (!(Dom.get('filter_commenter').checked || Dom.get('filter_last').checked)) {
+ alert('You must select at least one comment filter.');
+ return false;
+ }
+ if (Dom.get('filter_commenter').checked
+ && Dom.get('commenter').value == 'is'
+ && Dom.get('commenter_is').value == '')
+ {
+ alert('You must specify the last commenter\'s email address.');
+ return false;
+ }
+ if (Dom.get('filter_last').checked
+ && Dom.get('last').value == 'is'
+ && Dom.get('last_is').value == '')
+ {
+ alert('You must specify the "comment is older than" date.');
+ return false;
+ }
+ return true;
+}
+
+YAHOO.util.Event.onDOMReady(function() {
+ onSelectProduct();
+ onCommenterChange();
+ onLastChange();
+
+ var component = Dom.get('component');
+ if (selected_components.length == 0)
+ return;
+ component.options[0].selected = false;
+ for (var i = 0, n = selected_components.length; i < n; i++) {
+ var index = bz_optionIndex(component, selected_components[i]);
+ if (index != -1)
+ component.options[index].selected = true;
+ }
+});
diff --git a/extensions/BMO/web/js/webtrends.js b/extensions/BMO/web/js/webtrends.js
new file mode 100644
index 000000000..fd0aca29e
--- /dev/null
+++ b/extensions/BMO/web/js/webtrends.js
@@ -0,0 +1,213 @@
+function WebTrends(options){var that=this;this.dcsid="dcsis0ifv10000gg3ag82u4rf_7b1e";this.rate=100;this.fpcdom=".mozilla.org";this.trackevents=false;if(typeof(options)!="undefined")
+{if(typeof(options.dcsid)!="undefined")this.dcsid=options.dcsid;if(typeof(options.rate)!="undefined")this.rate=options.rate;if(typeof(options.fpcdom)!="undefined")this.fpcdom=options.fpcdom;if(typeof(this.fpcdom)!="undefined"&&this.fpcdom.substring(0,1)!='.')this.fpcdom='.'+this.fpcdom;if(typeof(options.trackevents)!="undefined")this.trackevents=options.trackevents;}
+this.domain="statse.webtrendslive.com";this.timezone=0;this.onsitedoms="";this.downloadtypes="xls,doc,pdf,txt,csv,zip,dmg,exe";this.navigationtag="div,table";this.enabled=true;this.i18n=false;this.fpc="WT_FPC";this.paidsearchparams="gclid";this.splitvalue="";this.preserve=true;this.DCSdir={};this.DCS={};this.WT={};this.DCSext={};this.images=[];this.index=0;this.exre=(function()
+{return(window.RegExp?new RegExp("dcs(uri)|(ref)|(aut)|(met)|(sta)|(sip)|(pro)|(byt)|(dat)|(p3p)|(cfg)|(redirect)|(cip)","i"):"");})();this.re=(function()
+{return(window.RegExp?(that.i18n?{"%25":/\%/g,"%26":/\&/g}:{"%09":/\t/g,"%20":/ /g,"%23":/\#/g,"%26":/\&/g,"%2B":/\+/g,"%3F":/\?/g,"%5C":/\\/g,"%22":/\"/g,"%7F":/\x7F/g,"%A0":/\xA0/g}):"");})();}
+WebTrends.prototype.dcsGetId=function(){if(this.enabled&&(document.cookie.indexOf(this.fpc+"=")==-1)&&(document.cookie.indexOf("WTLOPTOUT=")==-1)){document.write("<scr"+"ipt type='text/javascript' src='"+"http"+(window.location.protocol.indexOf('https:')==0?'s':'')+"://"+this.domain+"/"+this.dcsid+"/wtid.js"+"'><\/scr"+"ipt>");}}
+WebTrends.prototype.dcsGetCookie=function(name)
+{var cookies=document.cookie.split("; ");var cmatch=[];var idx=0;var i=0;var namelen=name.length;var clen=cookies.length;for(i=0;i<clen;i++)
+{var c=cookies[i];if((c.substring(0,namelen+1))==(name+"=")){cmatch[idx++]=c;}}
+var cmatchCount=cmatch.length;if(cmatchCount>0)
+{idx=0;if((cmatchCount>1)&&(name==this.fpc))
+{var dLatest=new Date(0);for(i=0;i<cmatchCount;i++)
+{var lv=parseInt(this.dcsGetCrumb(cmatch[i],"lv"));var dLst=new Date(lv);if(dLst>dLatest)
+{dLatest.setTime(dLst.getTime());idx=i;}}}
+return unescape(cmatch[idx].substring(namelen+1));}
+else
+{return null;}}
+WebTrends.prototype.dcsGetCrumb=function(cval,crumb,sep){var aCookie=cval.split(sep||":");for(var i=0;i<aCookie.length;i++){var aCrumb=aCookie[i].split("=");if(crumb==aCrumb[0]){return aCrumb[1];}}
+return null;}
+WebTrends.prototype.dcsGetIdCrumb=function(cval,crumb){var id=cval.substring(0,cval.indexOf(":lv="));var aCrumb=id.split("=");for(var i=0;i<aCrumb.length;i++){if(crumb==aCrumb[0]){return aCrumb[1];}}
+return null;}
+WebTrends.prototype.dcsIsFpcSet=function(name,id,lv,ss){var c=this.dcsGetCookie(name);if(c){return((id==this.dcsGetIdCrumb(c,"id"))&&(lv==this.dcsGetCrumb(c,"lv"))&&(ss==this.dcsGetCrumb(c,"ss")))?0:3;}
+return 2;}
+WebTrends.prototype.dcsFPC=function(){if(document.cookie.indexOf("WTLOPTOUT=")!=-1){return;}
+var WT=this.WT;var name=this.fpc;var dCur=new Date();var adj=(dCur.getTimezoneOffset()*60000)+(this.timezone*3600000);dCur.setTime(dCur.getTime()+adj);var dExp=new Date(dCur.getTime()+315360000000);var dSes=new Date(dCur.getTime());WT.co_f=WT.vtid=WT.vtvs=WT.vt_f=WT.vt_f_a=WT.vt_f_s=WT.vt_f_d=WT.vt_f_tlh=WT.vt_f_tlv="";if(document.cookie.indexOf(name+"=")==-1){if((typeof(gWtId)!="undefined")&&(gWtId!="")){WT.co_f=gWtId;}
+else if((typeof(gTempWtId)!="undefined")&&(gTempWtId!="")){WT.co_f=gTempWtId;WT.vt_f="1";}
+else{WT.co_f="2";var curt=dCur.getTime().toString();for(var i=2;i<=(32-curt.length);i++){WT.co_f+=Math.floor(Math.random()*16.0).toString(16);}
+WT.co_f+=curt;WT.vt_f="1";}
+if(typeof(gWtAccountRollup)=="undefined"){WT.vt_f_a="1";}
+WT.vt_f_s=WT.vt_f_d="1";WT.vt_f_tlh=WT.vt_f_tlv="0";}
+else{var c=this.dcsGetCookie(name);var id=this.dcsGetIdCrumb(c,"id");var lv=parseInt(this.dcsGetCrumb(c,"lv"));var ss=parseInt(this.dcsGetCrumb(c,"ss"));if((id==null)||(id=="null")||isNaN(lv)||isNaN(ss)){return;}
+WT.co_f=id;var dLst=new Date(lv);WT.vt_f_tlh=Math.floor((dLst.getTime()-adj)/1000);dSes.setTime(ss);if((dCur.getTime()>(dLst.getTime()+1800000))||(dCur.getTime()>(dSes.getTime()+28800000))){WT.vt_f_tlv=Math.floor((dSes.getTime()-adj)/1000);dSes.setTime(dCur.getTime());WT.vt_f_s="1";}
+if((dCur.getDay()!=dLst.getDay())||(dCur.getMonth()!=dLst.getMonth())||(dCur.getYear()!=dLst.getYear())){WT.vt_f_d="1";}}
+WT.co_f=escape(WT.co_f);WT.vtid=(typeof(this.vtid)=="undefined")?WT.co_f:(this.vtid||"");WT.vtvs=(dSes.getTime()-adj).toString();var expiry="; expires="+dExp.toGMTString();var cur=dCur.getTime().toString();var ses=dSes.getTime().toString();document.cookie=name+"="+"id="+WT.co_f+":lv="+cur+":ss="+ses+expiry+"; path=/"+(((this.fpcdom!=""))?("; domain="+this.fpcdom):(""));var rc=this.dcsIsFpcSet(name,WT.co_f,cur,ses);if(rc!=0){WT.co_f=WT.vtvs=WT.vt_f_s=WT.vt_f_d=WT.vt_f_tlh=WT.vt_f_tlv="";if(typeof(this.vtid)=="undefined"){WT.vtid="";}
+WT.vt_f=WT.vt_f_a=rc;}}
+WebTrends.prototype.dcsIsOnsite=function(host){if(host.length>0){host=host.toLowerCase();if(host==window.location.hostname.toLowerCase()){return true;}
+if(typeof(this.onsitedoms.test)=="function"){return this.onsitedoms.test(host);}
+else if(this.onsitedoms.length>0){var doms=this.dcsSplit(this.onsitedoms);var len=doms.length;for(var i=0;i<len;i++){if(host==doms[i]){return true;}}}}
+return false;}
+WebTrends.prototype.dcsTypeMatch=function(pth,typelist){var type=pth.toLowerCase().substring(pth.lastIndexOf(".")+1,pth.length);var types=this.dcsSplit(typelist);var tlen=types.length;for(var i=0;i<tlen;i++){if(type==types[i]){return true;}}
+return false;}
+WebTrends.prototype.dcsEvt=function(evt,tag){var e=evt.target||evt.srcElement;while(e.tagName&&(e.tagName.toLowerCase()!=tag.toLowerCase())){e=e.parentElement||e.parentNode;}
+return e;}
+WebTrends.prototype.dcsNavigation=function(evt){var id="";var cname="";var elems=this.dcsSplit(this.navigationtag);var elen=elems.length;var i,e,elem;for(i=0;i<elen;i++)
+{elem=elems[i];if(elem.length)
+{e=this.dcsEvt(evt,elem);id=(e.getAttribute&&e.getAttribute("id"))?e.getAttribute("id"):"";cname=e.className||"";if(id.length||cname.length){break;}}}
+return id.length?id:cname;}
+WebTrends.prototype.dcsBind=function(event,func){if((typeof(func)=="function")&&document.body){if(document.body.addEventListener){document.body.addEventListener(event,func.wtbind(this),true);}
+else if(document.body.attachEvent){document.body.attachEvent("on"+event,func.wtbind(this));}}}
+WebTrends.prototype.dcsET=function(){var e=(navigator.appVersion.indexOf("MSIE")!=-1)?"click":"mousedown";this.dcsBind(e,this.dcsDownload);this.dcsBind("contextmenu",this.dcsRightClick);this.dcsBind(e,this.dcsLinkTrack);}
+WebTrends.prototype.dcsMultiTrack=function(){var args=dcsMultiTrack.arguments?dcsMultiTrack.arguments:arguments;if(args.length%2==0){this.dcsSaveProps(args);this.dcsSetProps(args);var dCurrent=new Date();this.DCS.dcsdat=dCurrent.getTime();this.dcsFPC();this.dcsTag();this.dcsRestoreProps();}}
+WebTrends.prototype.dcsLinkTrack=function(evt)
+{evt=evt||(window.event||"");if(evt&&((typeof(evt.which)!="number")||(evt.which==1)))
+{var e=this.dcsEvt(evt,"A");var f=this.dcsEvt(evt,"IMG");if(e.href&&e.protocol&&e.protocol.indexOf("http")!=-1&&!this.dcsLinkTrackException(e))
+{if((navigator.appVersion.indexOf("MSIE")==-1)&&((e.onclick)||(e.onmousedown)))
+{this.dcsSetVarCap(e);}
+var hn=e.hostname?(e.hostname.split(":")[0]):"";var qry=e.search?e.search.substring(e.search.indexOf("?")+1,e.search.length):"";var pth=e.pathname?((e.pathname.indexOf("/")!=0)?"/"+e.pathname:e.pathname):"/";var ti='';if(f.alt)
+{ti=f.alt;}
+else
+{if(document.all)
+{ti=e.title||e.innerText||e.innerHTML||"";}
+else
+{ti=e.title||e.text||e.innerHTML||"";}}
+hn=this.DCS.setvar_dcssip||hn;pth=this.DCS.setvar_dcsuri||pth;qry=this.DCS.setvar_dcsqry||qry;ti=this.WT.setvar_ti||ti;ti=this.dcsTrim(ti);this.WT.mc_id=this.WT.setvar_mc_id||"";this.WT.sp=this.WT.ad=this.DCS.setvar_dcsuri=this.DCS.setvar_dcssip=this.DCS.setvar_dcsqry=this.WT.setvar_ti=this.WT.setvar_mc_id="";this.dcsMultiTrack("DCS.dcssip",hn,"DCS.dcsuri",pth,"DCS.dcsqry",this.trimoffsiteparams?"":qry,"DCS.dcsref",window.location,"WT.ti","Link:"+ti,"WT.dl","1","WT.nv",this.dcsNavigation(evt),"WT.sp","","WT.ad","","WT.AutoLinkTrack","1");this.DCS.dcssip=this.DCS.dcsuri=this.DCS.dcsqry=this.DCS.dcsref=this.WT.ti=this.WT.dl=this.WT.nv="";}}}
+WebTrends.prototype.dcsTrim=function(sString)
+{while(sString.substring(0,1)==' ')
+{sString=sString.substring(1,sString.length);}
+while(sString.substring(sString.length-1,sString.length)==' ')
+{sString=sString.substring(0,sString.length-1);}
+return sString;}
+WebTrends.prototype.dcsSetVarCap=function(e)
+{if(e.onclick)
+var gCap=e.onclick.toString();else if(e.onmousedown)
+var gCap=e.onmousedown.toString();var gStart=gCap.substring(gCap.indexOf("dcsSetVar(")+10,gCap.length)||gCap.substring(gCap.indexOf("_tag.dcsSetVar(")+16,gCap.length);var gEnd=gStart.substring(0,gStart.indexOf(");")).replace(/\s"/gi,"").replace(/"/gi,"");var gSplit=gEnd.split(",");if(gSplit.length!=-1)
+{for(var i=0;i<gSplit.length;i+=2)
+{if(gSplit[i].indexOf('WT.')==0)
+{if(this.dcsSetVarValidate(gSplit[i]))
+{this.WT["setvar_"+gSplit[i].substring(3)]=gSplit[i+1];}
+else
+{this.WT[gSplit[i].substring(3)]=gSplit[i+1];}}
+else if(gSplit[i].indexOf('DCS.')==0)
+{if(this.dcsSetVarValidate(gSplit[i]))
+{this.DCS["setvar_"+gSplit[i].substring(4)]=gSplit[i+1];}
+else
+{this.DCS[gSplit[i].substring(4)]=gSplit[i+1];}}
+else if(gSplit[i].indexOf('DCSext.')==0)
+{if(this.dcsSetVarValidate(gSplit[i]))
+{this.DCSext["setvar_"+gSplit[i].substring(7)]=gSplit[i+1];}
+else
+{this.DCSext[gSplit[i].substring(7)]=gSplit[i+1];}}
+else if(gSplit[i].indexOf('DCSdir.')==0)
+{if(this.dcsSetVarValidate(gSplit[i]))
+{this.DCSdir["setvar_"+gSplit[i].substring(7)]=gSplit[i+1];}
+else
+{this.DCSdir[gSplit[i].substring(7)]=gSplit[i+1];}}}}}
+WebTrends.prototype.dcsSetVarValidate=function(validate)
+{var wtParamList="DCS.dcssip,DCS.dcsuri,DCS.dcsqry,WT.ti,WT.mc_id".split(",");for(var i=0;i<wtParamList.length;i++)
+{if(wtParamList[i]==validate)
+{return 1;}}
+return 0;}
+WebTrends.prototype.dcsSetVar=function()
+{var args=dcsSetVar.arguments?dcsSetVar.arguments:arguments;if((args.length%2==0)&&(navigator.appVersion.indexOf("MSIE")!=-1)){for(var i=0;i<args.length;i+=2){if(args[i].indexOf('WT.')==0){if(this.dcsSetVarValidate(args[i])){this.WT["setvar_"+args[i].substring(3)]=args[i+1];}
+else{this.WT[args[i].substring(3)]=args[i+1];}}
+else if(args[i].indexOf('DCS.')==0){if(this.dcsSetVarValidate(args[i])){this.DCS["setvar_"+args[i].substring(4)]=args[i+1];}
+else{this.DCS[args[i].substring(4)]=args[i+1];}}
+else if(args[i].indexOf('DCSext.')==0){if(this.dcsSetVarValidate(args[i])){this.DCSext["setvar_"+args[i].substring(7)]=args[i+1];}
+else{this.DCSext[args[i].substring(7)]=args[i+1];}}
+else if(args[i].indexOf('DCSdir.')==0){if(this.dcsSetVarValidate(args[i])){this.DCSdir["setvar_"+args[i].substring(7)]=args[i+1];}
+else{this.DCSdir[args[i].substring(7)]=args[i+1];}}}}}
+WebTrends.prototype.dcsLinkTrackException=function(n)
+{try
+{var b=0;if(this.DCSdir.gTrackExceptions)
+{var e=this.DCSdir.gTrackExceptions.split(",");while(b!=1)
+{if(n.tagName&&n.tagName=="body")
+{b=1;return false}
+else
+{if(n.className)
+{var f=String(n.className).split(" ");for(var c=0;c<e.length;c++)for(var d=0;d<f.length;d++)
+{if(f[d]==e[c])
+{b=1;return true}}}}
+n=n.parentNode}}
+else
+{return false;}}
+catch(g){}}
+WebTrends.prototype.dcsCleanUp=function(){this.DCS={};this.WT={};this.DCSext={};if(arguments.length%2==0){this.dcsSetProps(arguments);}}
+WebTrends.prototype.dcsSetProps=function(args){for(var i=0;i<args.length;i+=2){if(args[i].indexOf('WT.')==0){this.WT[args[i].substring(3)]=args[i+1];}
+else if(args[i].indexOf('DCS.')==0){this.DCS[args[i].substring(4)]=args[i+1];}
+else if(args[i].indexOf('DCSext.')==0){this.DCSext[args[i].substring(7)]=args[i+1];}}}
+WebTrends.prototype.dcsSaveProps=function(args){var i,key,param;if(this.preserve){this.args=[];for(i=0;i<args.length;i+=2){param=args[i];if(param.indexOf('WT.')==0){key=param.substring(3);this.args[i]=param;this.args[i+1]=this.WT[key]||"";}
+else if(param.indexOf('DCS.')==0){key=param.substring(4);this.args[i]=param;this.args[i+1]=this.DCS[key]||"";}
+else if(param.indexOf('DCSext.')==0){key=param.substring(7);this.args[i]=param;this.args[i+1]=this.DCSext[key]||"";}}}}
+WebTrends.prototype.dcsRestoreProps=function(){if(this.preserve){this.dcsSetProps(this.args);this.args=[];}}
+WebTrends.prototype.dcsSplit=function(list){var items=list.toLowerCase().split(",");var len=items.length;for(var i=0;i<len;i++){items[i]=items[i].replace(/^\s*/,"").replace(/\s*$/,"");}
+return items;}
+WebTrends.prototype.dcsDownload=function(evt){evt=evt||(window.event||"");if(evt&&((typeof(evt.which)!="number")||(evt.which==1))){var e=this.dcsEvt(evt,"A");if(e.href){var hn=e.hostname?(e.hostname.split(":")[0]):"";if(this.dcsIsOnsite(hn)&&this.dcsTypeMatch(e.pathname,this.downloadtypes)){var qry=e.search?e.search.substring(e.search.indexOf("?")+1,e.search.length):"";var pth=e.pathname?((e.pathname.indexOf("/")!=0)?"/"+e.pathname:e.pathname):"/";var ttl="";var text=document.all?e.innerText:e.text;var img=this.dcsEvt(evt,"IMG");if(img.alt){ttl=img.alt;}
+else if(text){ttl=text;}
+else if(e.innerHTML){ttl=e.innerHTML;}
+this.dcsMultiTrack("DCS.dcssip",hn,"DCS.dcsuri",pth,"DCS.dcsqry",e.search||"","WT.ti","Download:"+ttl,"WT.dl","20","WT.nv",this.dcsNavigation(evt));}}}}
+WebTrends.prototype.dcsRightClick=function(evt){evt=evt||(window.event||"");if(evt){var btn=evt.which||evt.button;if((btn!=1)||(navigator.userAgent.indexOf("Safari")!=-1)){var e=this.dcsEvt(evt,"A");if((typeof(e.href)!="undefined")&&e.href){if((typeof(e.protocol)!="undefined")&&e.protocol&&(e.protocol.indexOf("http")!=-1)){if((typeof(e.pathname)!="undefined")&&this.dcsTypeMatch(e.pathname,this.downloadtypes)){var pth=e.pathname?((e.pathname.indexOf("/")!=0)?"/"+e.pathname:e.pathname):"/";var hn=e.hostname?(e.hostname.split(":")[0]):"";this.dcsMultiTrack("DCS.dcssip",hn,"DCS.dcsuri",pth,"DCS.dcsqry","","WT.ti","RightClick:"+pth,"WT.dl","25");}}}}}}
+WebTrends.prototype.dcsAdv=function(){if(this.trackevents&&(typeof(this.dcsET)=="function")){if(window.addEventListener){window.addEventListener("load",this.dcsET.wtbind(this),false);}
+else if(window.attachEvent){window.attachEvent("onload",this.dcsET.wtbind(this));}}
+this.dcsFPC();}
+WebTrends.prototype.dcsVar=function(){var dCurrent=new Date();var WT=this.WT;var DCS=this.DCS;WT.tz=parseInt(dCurrent.getTimezoneOffset()/60*-1)||"0";WT.bh=dCurrent.getHours()||"0";WT.ul=navigator.appName=="Netscape"?navigator.language:navigator.userLanguage;if(typeof(screen)=="object"){WT.cd=navigator.appName=="Netscape"?screen.pixelDepth:screen.colorDepth;WT.sr=screen.width+"x"+screen.height;}
+if(typeof(navigator.javaEnabled())=="boolean"){WT.jo=navigator.javaEnabled()?"Yes":"No";}
+if(document.title){if(window.RegExp){var tire=new RegExp("^"+window.location.protocol+"//"+window.location.hostname+"\\s-\\s");WT.ti=document.title.replace(tire,"");}
+else{WT.ti=document.title;}}
+WT.js="Yes";WT.jv=(function(){var agt=navigator.userAgent.toLowerCase();var major=parseInt(navigator.appVersion);var mac=(agt.indexOf("mac")!=-1);var ff=(agt.indexOf("firefox")!=-1);var ff0=(agt.indexOf("firefox/0.")!=-1);var ff10=(agt.indexOf("firefox/1.0")!=-1);var ff15=(agt.indexOf("firefox/1.5")!=-1);var ff20=(agt.indexOf("firefox/2.0")!=-1);var ff3up=(ff&&!ff0&&!ff10&!ff15&!ff20);var nn=(!ff&&(agt.indexOf("mozilla")!=-1)&&(agt.indexOf("compatible")==-1));var nn4=(nn&&(major==4));var nn6up=(nn&&(major>=5));var ie=((agt.indexOf("msie")!=-1)&&(agt.indexOf("opera")==-1));var ie4=(ie&&(major==4)&&(agt.indexOf("msie 4")!=-1));var ie5up=(ie&&!ie4);var op=(agt.indexOf("opera")!=-1);var op5=(agt.indexOf("opera 5")!=-1||agt.indexOf("opera/5")!=-1);var op6=(agt.indexOf("opera 6")!=-1||agt.indexOf("opera/6")!=-1);var op7up=(op&&!op5&&!op6);var jv="1.1";if(ff3up){jv="1.8";}
+else if(ff20){jv="1.7";}
+else if(ff15){jv="1.6";}
+else if(ff0||ff10||nn6up||op7up){jv="1.5";}
+else if((mac&&ie5up)||op6){jv="1.4";}
+else if(ie5up||nn4||op5){jv="1.3";}
+else if(ie4){jv="1.2";}
+return jv;})();WT.ct="unknown";if(document.body&&document.body.addBehavior){try{document.body.addBehavior("#default#clientCaps");WT.ct=document.body.connectionType||"unknown";document.body.addBehavior("#default#homePage");WT.hp=document.body.isHomePage(location.href)?"1":"0";}
+catch(e){}}
+if(document.all){WT.bs=document.body?document.body.offsetWidth+"x"+document.body.offsetHeight:"unknown";}
+else{WT.bs=window.innerWidth+"x"+window.innerHeight;}
+WT.fv=(function(){var i,flash;if(window.ActiveXObject){for(i=15;i>0;i--){try{flash=new ActiveXObject("ShockwaveFlash.ShockwaveFlash."+i);return i+".0";}
+catch(e){}}}
+else if(navigator.plugins&&navigator.plugins.length){for(i=0;i<navigator.plugins.length;i++){if(navigator.plugins[i].name.indexOf('Shockwave Flash')!=-1){return navigator.plugins[i].description.split(" ")[2];}}}
+return"Not enabled";})();WT.slv=(function(){var slv="Not enabled";try{if(navigator.userAgent.indexOf('MSIE')!=-1){var sli=new ActiveXObject('AgControl.AgControl');if(sli){slv="Unknown";}}
+else if(navigator.plugins["Silverlight Plug-In"]){slv="Unknown";}}
+catch(e){}
+if(slv!="Not enabled"){var i,m,M,F;if((typeof(Silverlight)=="object")&&(typeof(Silverlight.isInstalled)=="function")){for(i=9;i>0;i--){M=i;if(Silverlight.isInstalled(M+".0")){break;}
+if(slv==M){break;}}
+for(m=9;m>=0;m--){F=M+"."+m;if(Silverlight.isInstalled(F)){slv=F;break;}
+if(slv==F){break;}}}}
+return slv;})();if(this.i18n){if(typeof(document.defaultCharset)=="string"){WT.le=document.defaultCharset;}
+else if(typeof(document.characterSet)=="string"){WT.le=document.characterSet;}
+else{WT.le="unknown";}}
+WT.tv="9.3.0";WT.sp=this.splitvalue;WT.dl="0";WT.ssl=(window.location.protocol.indexOf('https:')==0)?"1":"0";DCS.dcsdat=dCurrent.getTime();DCS.dcssip=window.location.hostname;DCS.dcsuri=window.location.pathname;WT.es=DCS.dcssip+DCS.dcsuri;if(window.location.search){DCS.dcsqry=window.location.search;}
+if(DCS.dcsqry){var dcsqry=DCS.dcsqry.toLowerCase();var params=this.paidsearchparams.length?this.paidsearchparams.toLowerCase().split(","):[];for(var i=0;i<params.length;i++){if(dcsqry.indexOf(params[i]+"=")!=-1){WT.srch="1";break;}}}
+if((window.document.referrer!="")&&(window.document.referrer!="-")){if(!(navigator.appName=="Microsoft Internet Explorer"&&parseInt(navigator.appVersion)<4)){DCS.dcsref=window.document.referrer;}}}
+WebTrends.prototype.dcsEscape=function(S,REL){if(REL!=""){S=S.toString();for(var R in REL){if(REL[R]instanceof RegExp){S=S.replace(REL[R],R);}}
+return S;}
+else{return escape(S);}}
+WebTrends.prototype.dcsA=function(N,V){if(this.i18n&&(this.exre!="")&&!this.exre.test(N)){if(N=="dcsqry"){var newV="";var params=V.substring(1).split("&");for(var i=0;i<params.length;i++){var pair=params[i];var pos=pair.indexOf("=");if(pos!=-1){var key=pair.substring(0,pos);var val=pair.substring(pos+1);if(i!=0){newV+="&";}
+newV+=key+"="+this.dcsEncode(val);}}
+V=V.substring(0,1)+newV;}
+else{V=this.dcsEncode(V);}}
+return"&"+N+"="+this.dcsEscape(V,this.re);}
+WebTrends.prototype.dcsEncode=function(S){return(typeof(encodeURIComponent)=="function")?encodeURIComponent(S):escape(S);}
+WebTrends.prototype.dcsCreateImage=function(dcsSrc){if(document.images){this.images[this.index]=new Image();this.images[this.index].src=dcsSrc;this.index++;}
+else{document.write('<img alt="" border="0" name="DCSIMG" width="1" height="1" src="'+dcsSrc+'">');}}
+WebTrends.prototype.dcsMeta=function(){var elems;if(document.documentElement){elems=document.getElementsByTagName("meta");}
+else if(document.all){elems=document.all.tags("meta");}
+if(typeof(elems)!="undefined"){var length=elems.length;for(var i=0;i<length;i++){var name=elems.item(i).name;var content=elems.item(i).content;var equiv=elems.item(i).httpEquiv;if(name.length>0){if(name.toUpperCase().indexOf("WT.")==0){this.WT[name.substring(3)]=content;}
+else if(name.toUpperCase().indexOf("DCSEXT.")==0){this.DCSext[name.substring(7)]=content;}
+else if(name.toUpperCase().indexOf("DCSDIR.")==0){this.DCSdir[name.substring(7)]=content;}
+else if(name.toUpperCase().indexOf("DCS.")==0){this.DCS[name.substring(4)]=content;}}}}}
+WebTrends.prototype.dcsTag=function(){if(document.cookie.indexOf("WTLOPTOUT=")!=-1||!this.dcsChk()){return;}
+var WT=this.WT;var DCS=this.DCS;var DCSext=this.DCSext;var i18n=this.i18n;var P="http"+(window.location.protocol.indexOf('https:')==0?'s':'')+"://"+this.domain+(this.dcsid==""?'':'/'+this.dcsid)+"/dcs.gif?";if(i18n){WT.dep="";}
+for(var N in DCS){if(DCS[N]&&(typeof DCS[N]!="function")){P+=this.dcsA(N,DCS[N]);}}
+for(N in WT){if(WT[N]&&(typeof WT[N]!="function")){P+=this.dcsA("WT."+N,WT[N]);}}
+for(N in DCSext){if(DCSext[N]&&(typeof DCSext[N]!="function")){if(i18n){WT.dep=(WT.dep.length==0)?N:(WT.dep+";"+N);}
+P+=this.dcsA(N,DCSext[N]);}}
+if(i18n&&(WT.dep.length>0)){P+=this.dcsA("WT.dep",WT.dep);}
+if(P.length>2048&&navigator.userAgent.indexOf('MSIE')>=0){P=P.substring(0,2040)+"&WT.tu=1";}
+this.dcsCreateImage(P);this.WT.ad="";}
+WebTrends.prototype.dcsDebug=function(){var t=this;var i=t.images[0].src;var q=i.indexOf("?");var r=i.substring(0,q).split("/");var m="<b>Protocol</b><br><code>"+r[0]+"<br></code>";m+="<b>Domain</b><br><code>"+r[2]+"<br></code>";m+="<b>Path</b><br><code>/"+r[3]+"/"+r[4]+"<br></code>";m+="<b>Query Params</b><code>"+i.substring(q+1).replace(/\&/g,"<br>")+"</code>";m+="<br><b>Cookies</b><br><code>"+document.cookie.replace(/\;/g,"<br>")+"</code>";if(t.w&&!t.w.closed){t.w.close();}
+t.w=window.open("","dcsDebug","width=500,height=650,scrollbars=yes,resizable=yes");t.w.document.write(m);t.w.focus();}
+WebTrends.prototype.dcsCollect=function(){if(this.enabled){this.dcsVar();this.dcsMeta();this.dcsAdv();this.dcsBounce();if(typeof(this.dcsCustom)=="function"){this.dcsCustom();}
+this.dcsTag();}}
+function dcsMultiTrack(){if(typeof(_tag)!="undefined"){return(_tag.dcsMultiTrack());}}
+function dcsSetVar(){if(typeof(_tag)!="undefined"){return(_tag.dcsSetVar());}}
+function dcsDebug(){if(typeof(_tag)!="undefined"){return(_tag.dcsDebug());}}
+Function.prototype.wtbind=function(obj){var method=this;var temp=function(){return method.apply(obj,arguments);};return temp;}
+WebTrends.prototype.dcsBounce=function(){if(typeof(this.WT.vt_f_s)!="undefined"&&this.WT.vt_f_s==1){this.WT.z_bounce="1";}else{this.WT.z_bounce="0";}}
+WebTrends.prototype.dcsChk=function()
+{if(this.rate==100){return"true";}
+var cname='wtspl';cval=this.dcsGetCookie(cname);if(cval==null)
+{cval=Math.floor(Math.random()*1000000);var date=new Date();date.setTime(date.getTime()+(30*24*60*60*1000));document.cookie=cname+"="+cval+"; expires="+date.toGMTString()+"; path=/; domain="+this.fpcdom+";";}
+return((cval%1000)<(this.rate*10));} \ No newline at end of file
diff --git a/extensions/BMO/web/producticons/component.png b/extensions/BMO/web/producticons/component.png
new file mode 100644
index 000000000..b9c5053f6
--- /dev/null
+++ b/extensions/BMO/web/producticons/component.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/dino.png b/extensions/BMO/web/producticons/dino.png
new file mode 100644
index 000000000..9e0470a07
--- /dev/null
+++ b/extensions/BMO/web/producticons/dino.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/firefox.png b/extensions/BMO/web/producticons/firefox.png
new file mode 100644
index 000000000..3ba536ed2
--- /dev/null
+++ b/extensions/BMO/web/producticons/firefox.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/firefox_android.png b/extensions/BMO/web/producticons/firefox_android.png
new file mode 100644
index 000000000..7f9329082
--- /dev/null
+++ b/extensions/BMO/web/producticons/firefox_android.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/firefox_os.png b/extensions/BMO/web/producticons/firefox_os.png
new file mode 100644
index 000000000..5f08dc4f9
--- /dev/null
+++ b/extensions/BMO/web/producticons/firefox_os.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/input.png b/extensions/BMO/web/producticons/input.png
new file mode 100644
index 000000000..81f355d85
--- /dev/null
+++ b/extensions/BMO/web/producticons/input.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/localization.png b/extensions/BMO/web/producticons/localization.png
new file mode 100644
index 000000000..df3eac2d0
--- /dev/null
+++ b/extensions/BMO/web/producticons/localization.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/marketplace.png b/extensions/BMO/web/producticons/marketplace.png
new file mode 100644
index 000000000..62025a2a8
--- /dev/null
+++ b/extensions/BMO/web/producticons/marketplace.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/other.png b/extensions/BMO/web/producticons/other.png
new file mode 100644
index 000000000..e436c22ae
--- /dev/null
+++ b/extensions/BMO/web/producticons/other.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/seamonkey.png b/extensions/BMO/web/producticons/seamonkey.png
new file mode 100644
index 000000000..fcb261ae1
--- /dev/null
+++ b/extensions/BMO/web/producticons/seamonkey.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/sync.png b/extensions/BMO/web/producticons/sync.png
new file mode 100644
index 000000000..b42125ef6
--- /dev/null
+++ b/extensions/BMO/web/producticons/sync.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/thunderbird.png b/extensions/BMO/web/producticons/thunderbird.png
new file mode 100644
index 000000000..f3523183a
--- /dev/null
+++ b/extensions/BMO/web/producticons/thunderbird.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/webmaker.png b/extensions/BMO/web/producticons/webmaker.png
new file mode 100644
index 000000000..d576a5f01
--- /dev/null
+++ b/extensions/BMO/web/producticons/webmaker.png
Binary files differ
diff --git a/extensions/BMO/web/styles/choose_product.css b/extensions/BMO/web/styles/choose_product.css
new file mode 100644
index 000000000..053af542f
--- /dev/null
+++ b/extensions/BMO/web/styles/choose_product.css
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#choose_product h2,
+#choose_product p {
+ text-align: center;
+}
+
+#choose_product td h2,
+#choose_product td p {
+ text-align: left;
+}
diff --git a/extensions/BMO/web/styles/create_account.css b/extensions/BMO/web/styles/create_account.css
new file mode 100644
index 000000000..0ab527629
--- /dev/null
+++ b/extensions/BMO/web/styles/create_account.css
@@ -0,0 +1,62 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Bugzilla Bug Tracking System.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Byron Jones <glob@mozilla.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#create-account h2 {
+ margin: 0px;
+}
+
+.column-header {
+ padding: 20px 20px 20px 0px;
+}
+
+#create-account-left {
+ border-right: 2px solid #888888;
+ padding-right: 10px;
+}
+
+#product-list td {
+ padding-top: 10px;
+}
+
+#product-list img {
+ padding-right: 10px;
+}
+
+#create-account-right {
+ padding-left: 10px;
+}
+
+#right-blurb {
+ font-size: large;
+}
+
+#right-blurb li {
+ padding-bottom: 1em;
+}
+
+#create-account-right {
+ padding-bottom: 5em;
+}
+
diff --git a/extensions/BMO/web/styles/edit_bug.css b/extensions/BMO/web/styles/edit_bug.css
new file mode 100644
index 000000000..24212270d
--- /dev/null
+++ b/extensions/BMO/web/styles/edit_bug.css
@@ -0,0 +1,49 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the BMO Bugzilla Extension;
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011 the
+ * Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Byron Jones <glob@mozilla.com>
+ *
+ * ***** END LICENSE BLOCK *****
+ */
+
+#project-flags,
+#custom-flags {
+ width: auto;
+}
+
+.bz_hidden {
+ display: none;
+}
+
+.bz_collapse_comment {
+ font-family: monospace;
+}
+
+#prod_desc_container,
+#comp_desc_container {
+ overflow: auto;
+ color: green;
+ padding: 2px;
+}
+
+#toggle_prod_desc,
+#toggle_comp_desc {
+ white-space: nowrap;
+}
diff --git a/extensions/BMO/web/styles/reports.css b/extensions/BMO/web/styles/reports.css
new file mode 100644
index 000000000..06ae52d68
--- /dev/null
+++ b/extensions/BMO/web/styles/reports.css
@@ -0,0 +1,75 @@
+.hidden {
+ display: none;
+}
+
+#product, #component {
+ width: 20em;
+}
+
+#parameters th {
+ text-align: left;
+ vertical-align: middle !important;
+}
+
+#report tr.bugitem:hover {
+ background: #ccccff;
+}
+
+#report td, #report th {
+ padding: 3px 10px 3px 3px;
+}
+
+#report th {
+ text-align: left;
+}
+
+#report th.right {
+ text-align: right;
+}
+
+#report th.sorted {
+ text-decoration: underline;
+}
+
+#report-header {
+ background-color: #cccccc;
+}
+
+.report_subheader {
+ background-color: #dddddd;
+}
+
+.report_row_odd {
+ background-color: #eeeeee;
+ color: #000000;
+}
+
+.report_row_even {
+ background-color: #ffffff;
+ color: #000000;
+}
+
+#report.hover tr:hover {
+ background-color: #ccccff;
+}
+
+#report {
+ border: 1px solid #888888;
+}
+
+#report th, #report td {
+ border: 0px;
+}
+
+.disabled {
+ color: #888888;
+}
+
+.hoverrow tr:hover {
+ background-color: #ccccff;
+}
+
+.problem {
+ color: #aa2222;
+}
+
diff --git a/extensions/BMO/web/styles/triage_reports.css b/extensions/BMO/web/styles/triage_reports.css
new file mode 100644
index 000000000..6190fd32c
--- /dev/null
+++ b/extensions/BMO/web/styles/triage_reports.css
@@ -0,0 +1,23 @@
+.hidden {
+ display: none;
+}
+
+#triage_form th {
+ text-align: left;
+}
+
+#product, #component {
+ width: 20em;
+}
+
+#report tr.bugitem:hover {
+ background: #ccccff;
+}
+
+#report td {
+ padding: 1px 10px 1px 10px;
+}
+
+#report-header {
+ background: #dddddd;
+}
diff --git a/extensions/BmpConvert/disabled b/extensions/BMO/web/yui-history-iframe.txt
index e69de29bb..e69de29bb 100644
--- a/extensions/BmpConvert/disabled
+++ b/extensions/BMO/web/yui-history-iframe.txt
diff --git a/extensions/Bitly/Config.pm b/extensions/Bitly/Config.pm
new file mode 100644
index 000000000..aff9d4b4b
--- /dev/null
+++ b/extensions/Bitly/Config.pm
@@ -0,0 +1,45 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Bitly;
+use strict;
+
+use Bugzilla::Install::Util qw(vers_cmp);
+
+use constant NAME => 'Bitly';
+
+sub REQUIRED_MODULES {
+ my @required;
+ push @required, {
+ package => 'LWP',
+ module => 'LWP',
+ version => 5,
+ };
+ # LWP 6 split https support into a separate package
+ if (Bugzilla::Install::Requirements::have_vers({
+ package => 'LWP',
+ module => 'LWP',
+ version => 6,
+ })) {
+ push @required, {
+ package => 'LWP-Protocol-https',
+ module => 'LWP::Protocol::https',
+ version => 0
+ };
+ }
+ return \@required;
+}
+
+use constant OPTIONAL_MODULES => [
+ {
+ package => 'Mozilla-CA',
+ module => 'Mozilla::CA',
+ version => 0
+ },
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/Bitly/Extension.pm b/extensions/Bitly/Extension.pm
new file mode 100644
index 000000000..a368b20fe
--- /dev/null
+++ b/extensions/Bitly/Extension.pm
@@ -0,0 +1,31 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Bitly;
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+our $VERSION = '1';
+
+use Bugzilla;
+
+sub webservice {
+ my ($self, $args) = @_;
+ $args->{dispatch}->{Bitly} = "Bugzilla::Extension::Bitly::WebService";
+}
+
+sub config_modify_panels {
+ my ($self, $args) = @_;
+ push @{ $args->{panels}->{advanced}->{params} }, {
+ name => 'bitly_token',
+ type => 't',
+ default => '',
+ };
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/Bitly/lib/WebService.pm b/extensions/Bitly/lib/WebService.pm
new file mode 100644
index 000000000..e721103b0
--- /dev/null
+++ b/extensions/Bitly/lib/WebService.pm
@@ -0,0 +1,141 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Bitly::WebService;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::WebService);
+
+use Bugzilla::CGI;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Search;
+use Bugzilla::Search::Quicksearch;
+use Bugzilla::Util 'correct_urlbase';
+use Bugzilla::WebService::Util 'validate';
+use JSON;
+use LWP::UserAgent;
+use URI;
+use URI::Escape;
+use URI::QueryParam;
+
+sub _validate_uri {
+ my ($self, $params) = @_;
+
+ # extract url from params
+ if (!defined $params->{url}) {
+ ThrowCodeError(
+ 'param_required',
+ { function => 'Bitly.shorten', param => 'url' }
+ );
+ }
+ my $url = ref($params->{url}) ? $params->{url}->[0] : $params->{url};
+
+ # only allow buglist queries for this bugzilla install
+ my $uri = URI->new($url);
+ $uri->query(undef);
+ $uri->fragment(undef);
+ if ($uri->as_string ne correct_urlbase() . 'buglist.cgi') {
+ ThrowUserError('bitly_unsupported');
+ }
+
+ return URI->new($url);
+}
+
+sub shorten {
+ my ($self) = shift;
+ my $uri = $self->_validate_uri(@_);
+
+ # the list_id is user-specific, remove it
+ $uri->query_param_delete('list_id');
+
+ return $self->_bitly($uri);
+}
+
+sub list {
+ my ($self) = shift;
+ my $uri = $self->_validate_uri(@_);
+
+ # map params to cgi vars, converting quicksearch if required
+ my $params = $uri->query_param('quicksearch')
+ ? Bugzilla::CGI->new(quicksearch($uri->query_param('quicksearch')))->Vars
+ : Bugzilla::CGI->new($uri->query)->Vars;
+
+ # execute the search
+ my $search = Bugzilla::Search->new(
+ params => $params,
+ fields => ['bug_id'],
+ limit => Bugzilla->params->{max_search_results},
+ );
+ my $data = $search->data;
+
+ # form a bug_id only url, sanity check the length
+ $uri = URI->new(correct_urlbase() . 'buglist.cgi?bug_id=' . join(',', map { $_->[0] } @$data));
+ if (length($uri->as_string) > CGI_URI_LIMIT) {
+ ThrowUserError('bitly_failure', { message => "Too many bugs returned by search" });
+ }
+
+ # shorten
+ return $self->_bitly($uri);
+}
+
+sub _bitly {
+ my ($self, $uri) = @_;
+
+ # form request url
+ # http://dev.bitly.com/links.html#v3_shorten
+ my $bitly_url = sprintf(
+ 'https://api-ssl.bitly.com/v3/shorten?access_token=%s&longUrl=%s',
+ Bugzilla->params->{bitly_token},
+ uri_escape($uri->as_string)
+ );
+
+ # is Mozilla::CA isn't installed, skip certificate verification
+ eval { require Mozilla::CA };
+ $ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = $@ ? 0 : 1;
+
+ # request
+ my $ua = LWP::UserAgent->new(agent => 'Bugzilla');
+ $ua->timeout(10);
+ $ua->protocols_allowed(['http', 'https']);
+ if (my $proxy_url = Bugzilla->params->{proxy_url}) {
+ $ua->proxy(['http', 'https'], $proxy_url);
+ }
+ else {
+ $ua->env_proxy();
+ }
+ my $response = $ua->get($bitly_url);
+ if ($response->is_error) {
+ ThrowUserError('bitly_failure', { message => $response->message });
+ }
+ my $result = decode_json($response->decoded_content);
+ if ($result->{status_code} != 200) {
+ ThrowUserError('bitly_failure', { message => $result->{status_txt} });
+ }
+
+ # return just the short url
+ return { url => $result->{data}->{url} };
+}
+
+sub rest_resources {
+ return [
+ qr{^/bitly/shorten$}, {
+ GET => {
+ method => 'shorten',
+ },
+ },
+ qr{^/bitly/list$}, {
+ GET => {
+ method => 'list',
+ },
+ },
+ ]
+}
+
+1;
diff --git a/extensions/Bitly/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl b/extensions/Bitly/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl
new file mode 100644
index 000000000..2e0f58bc4
--- /dev/null
+++ b/extensions/Bitly/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF panel.name == "advanced" %]
+ [% panel.param_descs.bitly_token =
+ 'Bitly Generic Access Token'
+ %]
+[% END -%]
diff --git a/extensions/Bitly/template/en/default/hook/global/header-start.html.tmpl b/extensions/Bitly/template/en/default/hook/global/header-start.html.tmpl
new file mode 100644
index 000000000..12ab7b20f
--- /dev/null
+++ b/extensions/Bitly/template/en/default/hook/global/header-start.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS template.name == 'list/list.html.tmpl' %]
+
+[% style_urls.push('extensions/Bitly/web/styles/bitly.css') %]
+[% javascript_urls.push('extensions/Bitly/web/js/bitly.js') %]
+[% yui.push('container') %]
diff --git a/extensions/Bitly/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/Bitly/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..edf0b0724
--- /dev/null
+++ b/extensions/Bitly/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,17 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "bitly_failure" %]
+ [% title = "Failed to generate short URL" %]
+ [% message FILTER html %]
+
+[% ELSIF error == "bitly_unsupported" %]
+ [% title = "Unsupported URL" %]
+ The requested URL is not supported.
+
+[% END %]
diff --git a/extensions/Bitly/template/en/default/hook/list/list-links.html.tmpl b/extensions/Bitly/template/en/default/hook/list/list-links.html.tmpl
new file mode 100644
index 000000000..836c017ed
--- /dev/null
+++ b/extensions/Bitly/template/en/default/hook/list/list-links.html.tmpl
@@ -0,0 +1,24 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS user.id && Bugzilla.params.bitly_token %]
+
+<div id="bitly_overlay">
+ <div class="bd">
+ <select id="bitly_type" onchange="YAHOO.bitly.execute()">
+ <option value="shorten">Share a link to this search</option>
+ <option value="list">Share a link to this list of [% terms.bugs %]</option>
+ </select>
+ <input id="bitly_url" readonly placeholder="Generating short link...">
+ </div>
+ <div class="ft">
+ <button id="bitly_close" class="notransition">Close</button>
+ </div>
+</div>
+<a id="bitly_shorten" href="#" onclick="YAHOO.bitly.toggle();return false">Short URL</a>
+|&nbsp; [%# using nbsp because tt always trims trailing whitespace from templates %]
diff --git a/extensions/Bitly/web/js/bitly.js b/extensions/Bitly/web/js/bitly.js
new file mode 100644
index 000000000..62c49b650
--- /dev/null
+++ b/extensions/Bitly/web/js/bitly.js
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+(function() {
+ 'use strict';
+ var Dom = YAHOO.util.Dom;
+ YAHOO.namespace('bitly');
+ var bitly = YAHOO.bitly;
+
+ bitly.dialog = false;
+ bitly.url = { shorten: '', list: '' };
+
+ bitly.shorten = function() {
+ if (this.dialog) {
+ this.dialog.show();
+ var el = Dom.get('bitly_url');
+ el.select();
+ el.focus();
+ return;
+ }
+ this.dialog = new YAHOO.widget.Overlay('bitly_overlay', {
+ visible: true,
+ close: false,
+ underlay: 'shadow',
+ width: '400px',
+ context: [ 'bitly_shorten', 'bl', 'tl', ['windowResize'], [0, -10] ]
+ });
+ this.dialog.render(document.body);
+
+ YAHOO.util.Event.addListener('bitly_close', 'click', function() {
+ YAHOO.bitly.dialog.hide();
+ });
+ YAHOO.util.Event.addListener('bitly_url', 'keypress', function(o) {
+ if (o.keyCode == 27 || o.keyCode == 13)
+ YAHOO.bitly.dialog.hide();
+ });
+ this.execute();
+ Dom.get('bitly_url').focus();
+ };
+
+ bitly.execute = function() {
+ Dom.get('bitly_url').value = '';
+
+ var type = Dom.get('bitly_type').value;
+ if (this.url[type]) {
+ this.set(this.url[type]);
+ return;
+ }
+
+ var url = 'rest/bitly/' + type + '?url=' + encodeURIComponent(document.location);
+ YAHOO.util.Connect.initHeader("Accept", "application/json");
+ YAHOO.util.Connect.asyncRequest('GET', url, {
+ success: function(o) {
+ var response = YAHOO.lang.JSON.parse(o.responseText);
+ if (response.error) {
+ bitly.set(response.message);
+ }
+ else {
+ bitly.url[type] = response.url;
+ bitly.set(response.url);
+ }
+ },
+ failure: function(o) {
+ try {
+ var response = YAHOO.lang.JSON.parse(o.responseText);
+ if (response.error) {
+ bitly.set(response.message);
+ }
+ else {
+ bitly.set(o.statusText);
+ }
+ } catch (ex) {
+ bitly.set(o.statusText);
+ }
+ }
+ });
+ };
+
+ bitly.set = function(value) {
+ var el = Dom.get('bitly_url');
+ el.value = value;
+ el.select();
+ el.focus();
+ };
+
+ bitly.toggle = function() {
+ if (this.dialog
+ && YAHOO.util.Dom.get('bitly_overlay').style.visibility == 'visible')
+ {
+ this.dialog.hide();
+ }
+ else {
+ this.shorten();
+ }
+ };
+})();
diff --git a/extensions/Bitly/web/styles/bitly.css b/extensions/Bitly/web/styles/bitly.css
new file mode 100644
index 000000000..110a6bef4
--- /dev/null
+++ b/extensions/Bitly/web/styles/bitly.css
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#bitly_overlay {
+ position: absolute;
+ background: #eee;
+ border: 1px solid black;
+ padding: 5px;
+ margin: 10px;
+ visibility: collapse;
+ box-shadow: 3px 3px 6px #888;
+ -moz-box-shadow: 3px 3px 6px #888;
+}
+
+#bitly_url {
+ margin: 2px 0;
+ display: block;
+ width: 100%;
+}
diff --git a/extensions/BugmailFilter/Config.pm b/extensions/BugmailFilter/Config.pm
new file mode 100644
index 000000000..9932afb40
--- /dev/null
+++ b/extensions/BugmailFilter/Config.pm
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BugmailFilter;
+use strict;
+
+use constant NAME => 'BugmailFilter';
+use constant REQUIRED_MODULES => [];
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/BugmailFilter/Extension.pm b/extensions/BugmailFilter/Extension.pm
new file mode 100644
index 000000000..ebfc1f851
--- /dev/null
+++ b/extensions/BugmailFilter/Extension.pm
@@ -0,0 +1,478 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BugmailFilter;
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+our $VERSION = '1';
+
+use Bugzilla::BugMail;
+use Bugzilla::Component;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Extension::BugmailFilter::Constants;
+use Bugzilla::Extension::BugmailFilter::FakeField;
+use Bugzilla::Extension::BugmailFilter::Filter;
+use Bugzilla::Field;
+use Bugzilla::Product;
+use Bugzilla::User;
+use Bugzilla::Util qw(template_var);
+use Encode;
+use Sys::Syslog qw(:DEFAULT);
+
+#
+# preferences
+#
+
+sub user_preferences {
+ my ($self, $args) = @_;
+ return unless $args->{current_tab} eq 'bugmail_filter';
+
+ if ($args->{save_changes}) {
+ my $input = Bugzilla->input_params;
+
+ if ($input->{add_filter}) {
+
+ # add a new filter
+
+ my $params = {
+ user_id => Bugzilla->user->id,
+ };
+ $params->{field_name} = $input->{field} || IS_NULL;
+ if ($params->{field_name} eq '~') {
+ $params->{field_name} = '~' . $input->{field_contains};
+ }
+ $params->{relationship} = $input->{relationship} || IS_NULL;
+ if ($input->{changer}) {
+ Bugzilla::User::match_field({ changer => { type => 'single'} });
+ $params->{changer_id} = Bugzilla::User->check({
+ name => $input->{changer},
+ cache => 1,
+ })->id;
+ }
+ else {
+ $params->{changer_id} = IS_NULL;
+ }
+ if (my $product_name = $input->{product}) {
+ my $product = Bugzilla::Product->check({
+ name => $product_name, cache => 1
+ });
+ $params->{product_id} = $product->id;
+
+ if (my $component_name = $input->{component}) {
+ $params->{component_id} = Bugzilla::Component->check({
+ name => $component_name, product => $product,
+ cache => 1
+ })->id;
+ }
+ else {
+ $params->{component_id} = IS_NULL;
+ }
+ }
+ else {
+ $params->{product_id} = IS_NULL;
+ $params->{component_id} = IS_NULL;
+ }
+
+ if (@{ Bugzilla::Extension::BugmailFilter::Filter->match($params) }) {
+ ThrowUserError('bugmail_filter_exists');
+ }
+ $params->{action} = $input->{action} eq 'Exclude' ? 1 : 0;
+ foreach my $name (keys %$params) {
+ $params->{$name} = undef
+ if $params->{$name} eq IS_NULL;
+ }
+ Bugzilla::Extension::BugmailFilter::Filter->create($params);
+ }
+
+ elsif ($input->{remove_filter}) {
+
+ # remove filter(s)
+
+ my $ids = ref($input->{remove}) ? $input->{remove} : [ $input->{remove} ];
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction;
+ foreach my $id (@$ids) {
+ if (my $filter = Bugzilla::Extension::BugmailFilter::Filter->new($id)) {
+ $filter->remove_from_db();
+ }
+ }
+ $dbh->bz_commit_transaction;
+ }
+ }
+
+ my $vars = $args->{vars};
+ my $field_descs = template_var('field_descs');
+
+ # load all fields into a hash for easy manipulation
+ my %fields =
+ map { $_->name => $field_descs->{$_->name} }
+ @{ Bugzilla->fields({ obsolete => 0 }) };
+
+ # remove time trackinger fields
+ if (!Bugzilla->user->is_timetracker) {
+ foreach my $field (TIMETRACKING_FIELDS) {
+ delete $fields{$field};
+ }
+ }
+
+ # remove fields which don't make any sense to filter on
+ foreach my $field (IGNORE_FIELDS) {
+ delete $fields{$field};
+ }
+
+ # remove all tracking flag fields. these change too frequently to be of
+ # value, so they only add noise to the list.
+ foreach my $field (Bugzilla->tracking_flag_names) {
+ delete $fields{$field};
+ }
+
+ # add tracking flag types instead
+ foreach my $field (
+ @{ Bugzilla::Extension::BugmailFilter::FakeField->tracking_flag_fields() }
+ ) {
+ $fields{$field->name} = $field->description;
+ }
+
+ # adjust the description for selected fields
+ foreach my $field (keys %{ FIELD_DESCRIPTION_OVERRIDE() }) {
+ $fields{$field} = FIELD_DESCRIPTION_OVERRIDE->{$field};
+ }
+
+ # some fields are present in the changed-fields x-header but are not real
+ # bugzilla fields
+ foreach my $field (
+ @{ Bugzilla::Extension::BugmailFilter::FakeField->fake_fields() }
+ ) {
+ $fields{$field->name} = $field->description;
+ }
+
+ $vars->{fields} = \%fields;
+ $vars->{field_list} = [
+ sort { lc($a->{description}) cmp lc($b->{description}) }
+ map { { name => $_, description => $fields{$_} } }
+ keys %fields
+ ];
+
+ $vars->{relationships} = FILTER_RELATIONSHIPS();
+
+ $vars->{filters} = [
+ sort {
+ $a->product_name cmp $b->product_name
+ || $a->component_name cmp $b->component_name
+ || $a->field_name cmp $b->field_name
+ }
+ @{ Bugzilla::Extension::BugmailFilter::Filter->match({
+ user_id => Bugzilla->user->id,
+ }) }
+ ];
+
+ # set field_description
+ foreach my $filter (@{ $vars->{filters} }) {
+ my $field_name = $filter->field_name;
+ if (!$field_name) {
+ $filter->field_description('Any');
+ }
+ elsif (substr($field_name, 0, 1) eq '~') {
+ $filter->field_description('~ ' . substr($field_name, 1));
+ }
+ else {
+ $filter->field_description($fields{$field_name} || $filter->field->description);
+ }
+ }
+
+ # build a list of tracking-flags, grouped by type
+ require Bugzilla::Extension::TrackingFlags::Constants;
+ require Bugzilla::Extension::TrackingFlags::Flag;
+ my %flag_types =
+ map { $_->{name} => $_->{description} }
+ @{ Bugzilla::Extension::TrackingFlags::Constants::FLAG_TYPES() };
+ my %tracking_flags_by_type;
+ foreach my $flag (Bugzilla::Extension::TrackingFlags::Flag->get_all) {
+ my $type = $flag_types{$flag->flag_type};
+ $tracking_flags_by_type{$type} //= [];
+ push @{ $tracking_flags_by_type{$type} }, $flag;
+ }
+ my @tracking_flags_by_type;
+ foreach my $type (sort keys %tracking_flags_by_type) {
+ push @tracking_flags_by_type, {
+ name => $type,
+ flags => $tracking_flags_by_type{$type},
+ };
+ }
+ $vars->{tracking_flags_by_type} = \@tracking_flags_by_type;
+
+ ${ $args->{handled} } = 1;
+}
+
+#
+# hooks
+#
+
+sub user_wants_mail {
+ my ($self, $args) = @_;
+
+ my ($user, $wants_mail, $diffs, $comments)
+ = @$args{qw( user wants_mail fieldDiffs comments )};
+
+ return unless $$wants_mail;
+
+ my $cache = Bugzilla->request_cache->{bugmail_filters} //= {};
+ my $filters = $cache->{$user->id} //=
+ Bugzilla::Extension::BugmailFilter::Filter->match({
+ user_id => $user->id
+ });
+ return unless @$filters;
+
+ my $fields = [
+ map { {
+ filter_field => $_->{field_name}, # filter's field_name
+ field_name => $_->{field_name}, # raw bugzilla field_name
+ } }
+ @$diffs
+ ];
+
+ # insert fake fields for new attachments and comments
+ if (@$comments) {
+ if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) {
+ push @$fields, { filter_field => 'attachment.created' };
+ }
+ if (grep { $_->type != CMT_ATTACHMENT_CREATED } @$comments) {
+ push @$fields, { filter_field => 'comment.created' };
+ }
+ }
+
+ # set filter_field on tracking flags to tracking.$type
+ require Bugzilla::Extension::TrackingFlags::Flag;
+ my @tracking_flags = Bugzilla->tracking_flags;
+ foreach my $field (@$fields) {
+ next unless my $field_name = $field->{field_name};
+ foreach my $tracking_flag (@tracking_flags) {
+ if ($field_name eq $tracking_flag->name) {
+ $field->{filter_field} = 'tracking.'. $tracking_flag->flag_type;
+ }
+ }
+ }
+
+ if (_should_drop($fields, $filters, $args)) {
+ $$wants_mail = 0;
+ openlog('apache', 'cons,pid', 'local4');
+ syslog('notice', encode_utf8(sprintf(
+ '[bugmail] %s (filtered) bug-%s %s',
+ $args->{user}->login,
+ $args->{bug}->id,
+ $args->{bug}->short_desc,
+ )));
+ closelog();
+ }
+}
+
+sub _should_drop {
+ my ($fields, $filters, $args) = @_;
+
+ # calculate relationships
+
+ my ($user, $bug, $relationship, $changer) = @$args{qw( user bug relationship changer )};
+ my ($user_id, $login) = ($user->id, $user->login);
+ my $bit_direct = Bugzilla::BugMail::BIT_DIRECT;
+ my $bit_watching = Bugzilla::BugMail::BIT_WATCHING;
+ my $bit_compwatch = 15; # from Bugzilla::Extension::ComponentWatching
+
+ # the index of $rel_map corresponds to the values in FILTER_RELATIONSHIPS
+ my @rel_map;
+ $rel_map[1] = $bug->assigned_to->id == $user_id;
+ $rel_map[2] = !$rel_map[1];
+ $rel_map[3] = $bug->reporter->id == $user_id;
+ $rel_map[4] = !$rel_map[3];
+ if ($bug->qa_contact) {
+ $rel_map[5] = $bug->qa_contact->id == $user_id;
+ $rel_map[6] = !$rel_map[6];
+ }
+ $rel_map[7] = $bug->cc
+ ? grep { $_ eq $login } @{ $bug->cc }
+ : 0;
+ $rel_map[8] = !$rel_map[8];
+ $rel_map[9] = (
+ $relationship & $bit_watching
+ or $relationship & $bit_compwatch
+ );
+ $rel_map[10] = !$rel_map[9];
+ $rel_map[11] = $bug->is_mentor($user);
+ $rel_map[12] = !$rel_map[11];
+ foreach my $bool (@rel_map) {
+ $bool = $bool ? 1 : 0;
+ }
+
+ # exclusions
+ # drop email where we are excluding all changed fields
+
+ my $params = {
+ product_id => $bug->product_id,
+ component_id => $bug->component_id,
+ rel_map => \@rel_map,
+ changer_id => $changer->id,
+ };
+
+ foreach my $field (@$fields) {
+ $params->{field} = $field;
+ foreach my $filter (grep { $_->is_exclude } @$filters) {
+ if ($filter->matches($params)) {
+ $field->{exclude} = 1;
+ last;
+ }
+ }
+ }
+
+ # no need to process includes if nothing was excluded
+ if (!grep { $_->{exclude} } @$fields) {
+ return 0;
+ }
+
+ # inclusions
+ # flip the bit for fields that should be included
+
+ foreach my $field (@$fields) {
+ $params->{field} = $field;
+ foreach my $filter (grep { $_->is_include } @$filters) {
+ if ($filter->matches($params)) {
+ $field->{exclude} = 0;
+ last;
+ }
+ }
+ }
+
+ # drop if all fields are still excluded
+ return !(grep { !$_->{exclude} } @$fields);
+}
+
+# catch when fields are renamed, and update the field_name entires
+sub object_end_of_update {
+ my ($self, $args) = @_;
+ my $object = $args->{object};
+
+ return unless $object->isa('Bugzilla::Field')
+ || $object->isa('Bugzilla::Extension::TrackingFlags::Flag');
+
+ return unless exists $args->{changes}->{name};
+
+ my $old_name = $args->{changes}->{name}->[0];
+ my $new_name = $args->{changes}->{name}->[1];
+
+ Bugzilla->dbh->do(
+ "UPDATE bugmail_filters SET field_name=? WHERE field_name=?",
+ undef,
+ $new_name, $old_name);
+}
+
+sub reorg_move_component {
+ my ($self, $args) = @_;
+ my $new_product = $args->{new_product};
+ my $component = $args->{component};
+
+ Bugzilla->dbh->do(
+ "UPDATE bugmail_filters SET product_id=? WHERE component_id=?",
+ undef,
+ $new_product->id, $component->id,
+ );
+}
+
+#
+# schema / install
+#
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{schema}->{bugmail_filters} = {
+ FIELDS => [
+ id => {
+ TYPE => 'INTSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'
+ },
+ },
+ field_name => {
+ # due to fake fields, this can't be field_id
+ TYPE => 'VARCHAR(64)',
+ NOTNULL => 0,
+ },
+ product_id => {
+ TYPE => 'INT2',
+ NOTNULL => 0,
+ REFERENCES => {
+ TABLE => 'products',
+ COLUMN => 'id',
+ DELETE => 'CASCADE'
+ },
+ },
+ component_id => {
+ TYPE => 'INT2',
+ NOTNULL => 0,
+ REFERENCES => {
+ TABLE => 'components',
+ COLUMN => 'id',
+ DELETE => 'CASCADE'
+ },
+ },
+ changer_id => {
+ TYPE => 'INT3',
+ NOTNULL => 0,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'
+ },
+ },
+ relationship => {
+ TYPE => 'INT2',
+ NOTNULL => 0,
+ },
+ action => {
+ TYPE => 'INT1',
+ NOTNULL => 1,
+ },
+ ],
+ INDEXES => [
+ bugmail_filters_unique_idx => {
+ FIELDS => [ qw( user_id field_name product_id component_id
+ relationship ) ],
+ TYPE => 'UNIQUE',
+ },
+ bugmail_filters_user_idx => [
+ 'user_id',
+ ],
+ ],
+ };
+}
+
+sub install_update_db {
+ Bugzilla->dbh->bz_add_column(
+ 'bugmail_filters',
+ 'changer_id',
+ {
+ TYPE => 'INT3',
+ NOTNULL => 0,
+ }
+ );
+}
+
+sub db_sanitize {
+ my $dbh = Bugzilla->dbh;
+ print "Deleting bugmail filters...\n";
+ $dbh->do("DELETE FROM bugmail_filters");
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/BugmailFilter/lib/Constants.pm b/extensions/BugmailFilter/lib/Constants.pm
new file mode 100644
index 000000000..20e5480d0
--- /dev/null
+++ b/extensions/BugmailFilter/lib/Constants.pm
@@ -0,0 +1,120 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BugmailFilter::Constants;
+use strict;
+
+use base qw(Exporter);
+
+our @EXPORT = qw(
+ FAKE_FIELD_NAMES
+ IGNORE_FIELDS
+ FIELD_DESCRIPTION_OVERRIDE
+ FILTER_RELATIONSHIPS
+);
+
+use Bugzilla::Constants;
+
+# these are field names which are inserted into X-Bugzilla-Changed-Field-Names
+# header but are not real fields
+
+use constant FAKE_FIELD_NAMES => [
+ {
+ name => 'comment.created',
+ description => 'Comment created',
+ },
+ {
+ name => 'attachment.created',
+ description => 'Attachment created',
+ },
+];
+
+# these fields don't make any sense to filter on
+
+use constant IGNORE_FIELDS => qw(
+ assignee_last_login
+ attach_data.thedata
+ attachments.submitter
+ cf_last_resolved
+ commenter
+ comment_tag
+ creation_ts
+ days_elapsed
+ delta_ts
+ everconfirmed
+ last_visit_ts
+ longdesc
+ longdescs.count
+ owner_idle_time
+ reporter
+ reporter_accessible
+ setters.login_name
+ tag
+ votes
+);
+
+# override the description of some fields
+
+use constant FIELD_DESCRIPTION_OVERRIDE => {
+ bug_id => 'Bug Created',
+};
+
+# relationship / int mappings
+# _should_drop() also needs updating when this const is changed
+
+use constant FILTER_RELATIONSHIPS => [
+ {
+ name => 'Assignee',
+ value => 1,
+ },
+ {
+ name => 'Not Assignee',
+ value => 2,
+ },
+ {
+ name => 'Reporter',
+ value => 3,
+ },
+ {
+ name => 'Not Reporter',
+ value => 4,
+ },
+ {
+ name => 'QA Contact',
+ value => 5,
+ },
+ {
+ name => 'Not QA Contact',
+ value => 6,
+ },
+ {
+ name => "CC'ed",
+ value => 7,
+ },
+ {
+ name => "Not CC'ed",
+ value => 8,
+ },
+ {
+ name => 'Watching',
+ value => 9,
+ },
+ {
+ name => 'Not Watching',
+ value => 10,
+ },
+ {
+ name => 'Mentoring',
+ value => 11,
+ },
+ {
+ name => 'Not Mentoring',
+ value => 12,
+ },
+];
+
+1;
diff --git a/extensions/BugmailFilter/lib/FakeField.pm b/extensions/BugmailFilter/lib/FakeField.pm
new file mode 100644
index 000000000..88e4ac1ca
--- /dev/null
+++ b/extensions/BugmailFilter/lib/FakeField.pm
@@ -0,0 +1,57 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BugmailFilter::FakeField;
+
+use strict;
+use warnings;
+
+use Bugzilla::Extension::BugmailFilter::Constants;
+
+# object
+
+sub new {
+ my ($class, $params) = @_;
+ return bless($params, $class);
+}
+
+sub name { $_[0]->{name} }
+sub description { $_[0]->{description} }
+
+# static methods
+
+sub fake_fields {
+ my $cache = Bugzilla->request_cache->{bugmail_filter};
+ if (!$cache->{fake_fields}) {
+ my @fields;
+ foreach my $rh (@{ FAKE_FIELD_NAMES() }) {
+ push @fields, Bugzilla::Extension::BugmailFilter::FakeField->new($rh);
+ }
+ $cache->{fake_fields} = \@fields;
+ }
+ return $cache->{fake_fields};
+}
+
+sub tracking_flag_fields {
+ my $cache = Bugzilla->request_cache->{bugmail_filter};
+ if (!$cache->{tracking_flag_fields}) {
+ require Bugzilla::Extension::TrackingFlags::Constants;
+ my @fields;
+ my $tracking_types = Bugzilla::Extension::TrackingFlags::Constants::FLAG_TYPES();
+ foreach my $tracking_type (@$tracking_types) {
+ push @fields, Bugzilla::Extension::BugmailFilter::FakeField->new({
+ name => 'tracking.' . $tracking_type->{name},
+ description => $tracking_type->{description},
+ sortkey => $tracking_type->{sortkey},
+ });
+ }
+ $cache->{tracking_flag_fields} = \@fields;
+ }
+ return $cache->{tracking_flag_fields};
+}
+
+1;
diff --git a/extensions/BugmailFilter/lib/Filter.pm b/extensions/BugmailFilter/lib/Filter.pm
new file mode 100644
index 000000000..6246f51d9
--- /dev/null
+++ b/extensions/BugmailFilter/lib/Filter.pm
@@ -0,0 +1,212 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BugmailFilter::Filter;
+
+use base qw(Bugzilla::Object);
+
+use strict;
+use warnings;
+
+use Bugzilla::Component;
+use Bugzilla::Error;
+use Bugzilla::Extension::BugmailFilter::Constants;
+use Bugzilla::Extension::BugmailFilter::FakeField;
+use Bugzilla::Field;
+use Bugzilla::Product;
+use Bugzilla::User;
+use Bugzilla::Util qw(trim);
+
+use constant DB_TABLE => 'bugmail_filters';
+
+use constant DB_COLUMNS => qw(
+ id
+ user_id
+ product_id
+ component_id
+ field_name
+ relationship
+ changer_id
+ action
+);
+
+use constant LIST_ORDER => 'id';
+
+use constant UPDATE_COLUMNS => ();
+
+use constant VALIDATORS => {
+ user_id => \&_check_user,
+ field_name => \&_check_field_name,
+ action => \&Bugzilla::Object::check_boolean,
+};
+use constant VALIDATOR_DEPENDENCIES => {
+ component_id => [ 'product_id' ],
+};
+
+use constant AUDIT_CREATES => 0;
+use constant AUDIT_UPDATES => 0;
+use constant AUDIT_REMOVES => 0;
+use constant USE_MEMCACHED => 0;
+
+# getters
+
+sub user {
+ my ($self) = @_;
+ return Bugzilla::User->new({ id => $self->{user_id}, cache => 1 });
+}
+
+sub product {
+ my ($self) = @_;
+ return $self->{product_id}
+ ? Bugzilla::Product->new({ id => $self->{product_id}, cache => 1 })
+ : undef;
+}
+
+sub product_name {
+ my ($self) = @_;
+ return $self->{product_name} //= $self->{product_id} ? $self->product->name : '';
+}
+
+sub component {
+ my ($self) = @_;
+ return $self->{component_id}
+ ? Bugzilla::Component->new({ id => $self->{component_id}, cache => 1 })
+ : undef;
+}
+
+sub component_name {
+ my ($self) = @_;
+ return $self->{component_name} //= $self->{component_id} ? $self->component->name : '';
+}
+
+sub field_name {
+ return $_[0]->{field_name} //= '';
+}
+
+sub field_description {
+ my ($self, $value) = @_;
+ $self->{field_description} = $value if defined($value);
+ return $self->{field_description};
+}
+
+sub field {
+ my ($self) = @_;
+ return unless $self->{field_name};
+ if (!$self->{field}) {
+ if (substr($self->{field_name}, 0, 1) eq '~') {
+ # this should never happen
+ die "not implemented";
+ }
+ foreach my $field (
+ @{ Bugzilla::Extension::BugmailFilter::FakeField->fake_fields() },
+ @{ Bugzilla::Extension::BugmailFilter::FakeField->tracking_flag_fields() },
+ ) {
+ if ($field->{name} eq $self->{field_name}) {
+ return $self->{field} = $field;
+ }
+ }
+ $self->{field} = Bugzilla::Field->new({ name => $self->{field_name}, cache => 1 });
+ }
+ return $self->{field};
+}
+
+sub relationship {
+ return $_[0]->{relationship};
+}
+
+sub changer_id {
+ return $_[0]->{changer_id};
+}
+
+sub changer {
+ my ($self) = @_;
+ return $self->{changer_id}
+ ? Bugzilla::User->new({ id => $self->{changer_id}, cache => 1 })
+ : undef;
+}
+
+sub relationship_name {
+ my ($self) = @_;
+ foreach my $rel (@{ FILTER_RELATIONSHIPS() }) {
+ return $rel->{name}
+ if $rel->{value} == $self->{relationship};
+ }
+ return '?';
+}
+
+sub is_exclude {
+ return $_[0]->{action} == 1;
+}
+
+sub is_include {
+ return $_[0]->{action} == 0;
+}
+
+# validators
+
+sub _check_user {
+ my ($class, $user) = @_;
+ $user || ThrowCodeError('param_required', { param => 'user' });
+}
+
+sub _check_field_name {
+ my ($class, $field_name) = @_;
+ return undef unless $field_name;
+ if (substr($field_name, 0, 1) eq '~') {
+ $field_name = lc(trim($field_name));
+ $field_name =~ /^~[a-z0-9_\.\-]+$/
+ || ThrowUserError('bugmail_filter_invalid');
+ length($field_name) <= 64
+ || ThrowUserError('bugmail_filter_too_long');
+ return $field_name;
+ }
+ foreach my $rh (@{ FAKE_FIELD_NAMES() }) {
+ return $field_name if $rh->{name} eq $field_name;
+ }
+ return $field_name
+ if $field_name =~ /^tracking\./;
+ Bugzilla::Field->check({ name => $field_name, cache => 1});
+ return $field_name;
+}
+
+# methods
+
+sub matches {
+ my ($self, $args) = @_;
+
+ if (my $field_name = $self->{field_name}) {
+ if ($args->{field}->{field_name} && substr($field_name, 0, 1) eq '~') {
+ my $substring = quotemeta(substr($field_name, 1));
+ if ($args->{field}->{field_name} !~ /$substring/i) {
+ return 0;
+ }
+ }
+ elsif ($field_name ne $args->{field}->{filter_field}) {
+ return 0;
+ }
+ }
+
+ if ($self->{product_id} && $self->{product_id} != $args->{product_id}) {
+ return 0;
+ }
+
+ if ($self->{component_id} && $self->{component_id} != $args->{component_id}) {
+ return 0;
+ }
+
+ if ($self->{relationship} && !$args->{rel_map}->[$self->{relationship}]) {
+ return 0;
+ }
+
+ if ($self->{changer_id} && $self->{changer_id} != $args->{changer_id}) {
+ return 0;
+ }
+
+ return 1;
+}
+
+1;
diff --git a/extensions/BugmailFilter/template/en/default/account/prefs/bugmail_filter.html.tmpl b/extensions/BugmailFilter/template/en/default/account/prefs/bugmail_filter.html.tmpl
new file mode 100644
index 000000000..e7e0ed749
--- /dev/null
+++ b/extensions/BugmailFilter/template/en/default/account/prefs/bugmail_filter.html.tmpl
@@ -0,0 +1,392 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<link href="[% "extensions/BugmailFilter/web/style/bugmail-filter.css" FILTER mtime %]"
+ rel="stylesheet" type="text/css">
+<script type="text/javascript"
+ src="[% "extensions/BugmailFilter/web/js/bugmail-filter.js" FILTER mtime %]"></script>
+
+[% SET selectable_products = user.get_selectable_products %]
+[% SET dont_show_button = 1 %]
+
+<script>
+var useclassification = false;
+var first_load = true;
+var last_sel = [];
+var cpts = new Array();
+[% n = 1 %]
+[% FOREACH prod = selectable_products %]
+ cpts['[% n %]'] = [
+ [%- FOREACH comp = prod.components %]'[% comp.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
+ [% n = n + 1 %]
+[% END %]
+</script>
+<script type="text/javascript" src="[% 'js/productform.js' FILTER mtime FILTER html %]">
+</script>
+
+<hr>
+<b>Bugmail Filtering</b>
+
+<p>
+ You can instruct [% terms.Bugzilla %] to filter bugmail based on the field
+ that was changed.
+</p>
+
+<table id="add_filter_table">
+<tr>
+ <th>Field:</th>
+ <td>
+ <select name="field" id="field">
+ <option value="">__Any__</option>
+ [% FOREACH field = field_list %]
+ <option value="[% field.name FILTER html %]">
+ [% field.description FILTER html %]
+ </option>
+ [% END %]
+ <option value="~">Contains:</option>
+ </select>
+ </td>
+ <td class="blurb">
+ the field that was changed
+ </td>
+</tr>
+<tr id="field_contains_row" class="bz_default_hidden">
+ <td>&nbsp;</td>
+ <td>
+ <input name="field_contains" id="field_contains"
+ placeholder="field name" maxlength="63">
+ </td>
+</tr>
+<tr>
+ <th>Product:</th>
+ <td>
+ <select name="product" id="product">
+ <option value="">__Any__</option>
+ [% FOREACH product IN selectable_products %]
+ <option>[% product.name FILTER html %]</option>
+ [% END %]
+ </select>
+ </td>
+ <td class="blurb">
+ the [% terms.bug %]'s current product
+ </td>
+</tr>
+<tr>
+ <th>Component:</th>
+ <td>
+ <select name="component" id="component">
+ <option value="">__Any__</option>
+ [% FOREACH product IN selectable_products %]
+ [% FOREACH component IN product.components %]
+ <option>[% component.name FILTER html %]</option>
+ [% END %]
+ [% END %]
+ </select>
+ </td>
+ <td class="blurb">
+ the [% terms.bug %]'s current component
+ </td>
+</tr>
+<tr>
+ <th>Relationship:</th>
+ <td>
+ <select name="relationship" id="relationship">
+ <option value="">__Any__</option>
+ [% FOREACH rel IN relationships %]
+ <option value="[% rel.value FILTER html %]">
+ [% rel.name FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ </td>
+ <td class="blurb">
+ your relationship with the [% terms.bug %]
+ </td>
+</tr>
+<tr>
+ <th>Changer:</th>
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "changer"
+ name => "changer"
+ size => 32
+ emptyok => 1
+ %]
+ </td>
+ <td class="blurb">
+ the person who made the change (leave empty for "anyone")
+ </td>
+</tr>
+<tr>
+ <th>Action:</th>
+ <td>
+ <select name="action" id="action">
+ <option></option>
+ <option>Exclude</option>
+ <option>Include</option>
+ </select>
+ </td>
+ <td class="blurb">
+ action to take when all conditions match
+ </td>
+</tr>
+<tr>
+ <td></td>
+ <td><input type="submit" id="add_filter" name="add_filter" value="Add"></td>
+</tr>
+</table>
+
+<hr>
+<p>
+ You are currently filtering on:
+</p>
+
+[% IF filters.size %]
+
+ <table id="filters_table">
+ <tr>
+ <td></td>
+ <th>Product</th>
+ <th>Component</th>
+ <th>Field</th>
+ <th>Relationship</th>
+ <th>Changer</th>
+ <th>Action</th>
+ </tr>
+ [% FOREACH filter = filters %]
+ <tr class="[% "row_odd" UNLESS loop.count % 2 %]">
+ <td>
+ <input type="checkbox" name="remove" value="[% filter.id FILTER none %]"
+ onChange="onFilterRemoveChange()">
+ </td>
+ <td>[% filter.product ? filter.product.name : 'Any' FILTER html %]</td>
+ <td>[% filter.component ? filter.component.name : 'Any' FILTER html %]</td>
+ <td>[% filter.field_description FILTER html %]</td>
+ <td>[% filter.relationship ? filter.relationship_name : 'Any' FILTER html %]</td>
+ <td>
+ [% IF filter.changer %]
+ <span title="[% filter.changer.name FILTER html %]">
+ [% filter.changer.login FILTER html %]
+ </span>
+ [% ELSE %]
+ Anyone
+ [% END %]
+ </td>
+ <td>[% filter.action ? 'Exclude' : 'Include' %]</td>
+ </tr>
+ [% END %]
+ <tr>
+ <td></td>
+ <td><input id="remove" name="remove_filter" type="submit" value="Remove Selected"></td>
+ </tr>
+ </table>
+
+[% ELSE %]
+
+ <p>
+ <i>You do not have any filters configured.</i>
+ </p>
+
+[% END %]
+
+<hr>
+<p>
+ This feature provides fine-grained control over what changes to [% terms.bugs
+ %] will result in an email notification. These filters are applied
+ <b>after</b> the rules configured on the
+ <a href="userprefs.cgi?tab=email">Email Preferences</a> tab.
+</p>
+<p>
+ If multiple filters are applicable to the same [% terms.bug %] change,
+ <b>include</b> filters override <b>exclude</b> filters.
+</p>
+
+<hr>
+<h4>Field Groups</h4>
+
+<p>
+ Some fields are grouped into a single entry in the "field" list.
+ Following is a list of the groups and all the fields they contain:
+</p>
+
+[% FOREACH type = tracking_flags_by_type %]
+ [% type.name FILTER html %]:
+ <blockquote>
+ [% flag_count = type.flags.size %]
+ [% FOREACH flag = type.flags %]
+ [% IF flag_count > 10 && loop.count == 10 %]
+ <span id="show_all">
+ &hellip;
+ (<a href="#" onclick="showAllFlags(); return false">show all</a>)
+ </span>
+ <span id="all_flags" class="bz_default_hidden">
+ [% END %]
+ [% flag.description FILTER html FILTER no_break %]
+ [% ", " UNLESS loop.last %]
+ [% IF loop.last && flag_count > 10 %]
+ </span>
+ [% END %]
+ [% END %]
+ </blockquote>
+[% END %]
+
+<hr>
+<h4>Examples</h4>
+<p>
+ To never receive changes made to the "QA Whiteboard" field for [% terms.bugs %]
+ where you are not the assignee:<br>
+</p>
+<table class="example_filter_table">
+ <tr>
+ <th>Field:</th>
+ <td>QA Whiteboard</td>
+ </tr>
+ <tr>
+ <th>Product:</th>
+ <td>__Any__</td>
+ </tr>
+ <tr>
+ <th>Component:</th>
+ <td>__Any__</td>
+ </tr>
+ <tr>
+ <th>Relationship:</th>
+ <td>Not Assignee</td>
+ </tr>
+ <tr>
+ <th>Changer:</th>
+ <td>(empty)</td>
+ </tr>
+ <tr>
+ <th>Action:</th>
+ <td>Exclude</td>
+ </tr>
+</table>
+
+<p>
+ To never receive email for any change made by webops-kanban@mozilla.bugs:
+</p>
+<table class="example_filter_table">
+ <tr>
+ <th>Field:</th>
+ <td>__Any__</td>
+ </tr>
+ <tr>
+ <th>Product:</th>
+ <td>__Any__</td>
+ </tr>
+ <tr>
+ <th>Component:</th>
+ <td>__Any__</td>
+ </tr>
+ <tr>
+ <th>Relationship:</th>
+ <td>__Any__</td>
+ </tr>
+ <tr>
+ <th>Changer:</th>
+ <td>webops-kanban@mozilla.bugs</td>
+ </tr>
+ <tr>
+ <th>Action:</th>
+ <td>Exclude</td>
+ </tr>
+</table>
+
+<p>
+ To receive notifications of new [% terms.bugs %] in Firefox's "New Tab Page"
+ component, and no other changes, you require three filters. First an
+ <b>exclude</b> filter to drop all changes made to [% terms.bugs %] in that
+ component:<br>
+</p>
+<table class="example_filter_table">
+ <tr>
+ <th>Field:</th>
+ <td>__Any__</td>
+ </tr>
+ <tr>
+ <th>Product:</th>
+ <td>Firefox</td>
+ </tr>
+ <tr>
+ <th>Component:</th>
+ <td>New Tab Page</td>
+ </tr>
+ <tr>
+ <th>Relationship:</th>
+ <td>__Any__</td>
+ </tr>
+ <tr>
+ <th>Changer:</th>
+ <td>(empty)</td>
+ </tr>
+ <tr>
+ <th>Action:</th>
+ <td>Exclude</td>
+ </tr>
+</table>
+<p>
+ Then an <b>include</b> filter to indicate that you want to receive
+ notifications when a [% terms.bug %] is created:
+</p>
+<table class="example_filter_table">
+ <tr>
+ <th>Field:</th>
+ <td>[% terms.Bug %] Created</td>
+ </tr>
+ <tr>
+ <th>Product:</th>
+ <td>Firefox</td>
+ </tr>
+ <tr>
+ <th>Component:</th>
+ <td>New Tab Page</td>
+ </tr>
+ <tr>
+ <th>Relationship:</th>
+ <td>__Any__</td>
+ </tr>
+ <tr>
+ <th>Changer:</th>
+ <td>(empty)</td>
+ </tr>
+ <tr>
+ <th>Action:</th>
+ <td>Include</td>
+ </tr>
+</table>
+<p>
+ And finally another <b>include</b> filter to catch when a [% terms.bug %] is
+ moved into the "New Tab Page" component.
+</p>
+<table class="example_filter_table">
+ <tr>
+ <th>Field:</th>
+ <td>Component</td>
+ </tr>
+ <tr>
+ <th>Product:</th>
+ <td>Firefox</td>
+ </tr>
+ <tr>
+ <th>Component:</th>
+ <td>New Tab Page</td>
+ </tr>
+ <tr>
+ <th>Relationship:</th>
+ <td>__Any__</td>
+ </tr>
+ <tr>
+ <th>Changer:</th>
+ <td>(empty)</td>
+ </tr>
+ <tr>
+ <th>Action:</th>
+ <td>Include</td>
+ </tr>
+</table>
diff --git a/extensions/BugmailFilter/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl b/extensions/BugmailFilter/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
new file mode 100644
index 000000000..95ffdee99
--- /dev/null
+++ b/extensions/BugmailFilter/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
@@ -0,0 +1,14 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% tabs = tabs.import([{
+ name => "bugmail_filter",
+ label => "Bugmail Filtering",
+ link => "userprefs.cgi?tab=bugmail_filter",
+ saveable => 1
+ }]) %]
diff --git a/extensions/BugmailFilter/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/BugmailFilter/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..a0ec2125f
--- /dev/null
+++ b/extensions/BugmailFilter/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,22 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "bugmail_filter_exists" %]
+ [% title = "Filter Already Exists" %]
+ A filter already exists with the selected criteria.
+
+[% ELSIF error == "bugmail_filter_too_long" %]
+ [% title = "Invalid Field Name" %]
+ The field name to filter on is too long (must be 63 character or fewer).
+
+[% ELSIF error == "bugmail_filter_invalid" %]
+ [% title = "Invalid Field Name" %]
+ The field name contains invalid characters (alpha-numeric, underscore,
+ hyphen, and period only).
+
+[% END %]
diff --git a/extensions/BugmailFilter/web/js/bugmail-filter.js b/extensions/BugmailFilter/web/js/bugmail-filter.js
new file mode 100644
index 000000000..c24528861
--- /dev/null
+++ b/extensions/BugmailFilter/web/js/bugmail-filter.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+var Dom = YAHOO.util.Dom;
+
+function onFilterFieldChange() {
+ if (Dom.get('field').value == '~') {
+ Dom.removeClass('field_contains_row', 'bz_default_hidden');
+ Dom.get('field_contains').focus();
+ Dom.get('field_contains').select();
+ }
+ else {
+ Dom.addClass('field_contains_row', 'bz_default_hidden');
+ }
+}
+
+function onFilterProductChange() {
+ selectProduct(Dom.get('product'), Dom.get('component'), null, null, '__Any__');
+ Dom.get('component').disabled = Dom.get('product').value == '';
+}
+
+function setFilterAddEnabled() {
+ Dom.get('add_filter').disabled =
+ (
+ Dom.get('field').value == '~'
+ && Dom.get('field_contains').value == ''
+ )
+ || Dom.get('action').value == '';
+}
+
+function onFilterRemoveChange() {
+ var cbs = Dom.get('filters_table').getElementsByTagName('input');
+ for (var i = 0, l = cbs.length; i < l; i++) {
+ if (cbs[i].checked) {
+ Dom.get('remove').disabled = false;
+ return;
+ }
+ }
+ Dom.get('remove').disabled = true;
+}
+
+function showAllFlags() {
+ Dom.addClass('show_all', 'bz_default_hidden');
+ Dom.removeClass('all_flags', 'bz_default_hidden');
+}
+
+YAHOO.util.Event.onDOMReady(function() {
+ YAHOO.util.Event.on('field', 'change', onFilterFieldChange);
+ YAHOO.util.Event.on('field_contains', 'keyup', setFilterAddEnabled);
+ YAHOO.util.Event.on('product', 'change', onFilterProductChange);
+ YAHOO.util.Event.on('action', 'change', setFilterAddEnabled);
+ onFilterFieldChange();
+ onFilterProductChange();
+ onFilterRemoveChange();
+ setFilterAddEnabled();
+});
diff --git a/extensions/BugmailFilter/web/style/bugmail-filter.css b/extensions/BugmailFilter/web/style/bugmail-filter.css
new file mode 100644
index 000000000..193cf4469
--- /dev/null
+++ b/extensions/BugmailFilter/web/style/bugmail-filter.css
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#add_filter_table th {
+ text-align: right;
+}
+
+.example_filter_table {
+ margin-left: 2em;
+}
+
+.example_filter_table th {
+ text-align: right;
+ font-weight: normal;
+}
+
+.example_filter_table td {
+ padding-left: 1em;
+}
+
+#add_filter_table .blurb {
+ font-style: italic;
+ padding-left: 2em;
+}
+
+#filters_table {
+ margin-bottom: 1em;
+ border-spacing: 0;
+}
+
+#filters_table th, #filters_table td {
+ text-align: left;
+ padding-right: 1em;
+ padding: 2px;
+}
+
+#filters_table .row_odd {
+ background-color: #eeeeee;
+ color: #000000;
+}
diff --git a/extensions/BzAPI/Config.pm b/extensions/BzAPI/Config.pm
new file mode 100644
index 000000000..89b8c1e02
--- /dev/null
+++ b/extensions/BzAPI/Config.pm
@@ -0,0 +1,16 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BzAPI;
+
+use strict;
+
+use constant NAME => 'BzAPI';
+
+use constant REQUIRED_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/BzAPI/Extension.pm b/extensions/BzAPI/Extension.pm
new file mode 100644
index 000000000..cd08369b0
--- /dev/null
+++ b/extensions/BzAPI/Extension.pm
@@ -0,0 +1,267 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BzAPI;
+
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Extension::BzAPI::Constants;
+use Bugzilla::Extension::BzAPI::Util qw(fix_credentials filter_wants_nocache);
+
+use Bugzilla::Error;
+use Bugzilla::Util qw(trick_taint datetime_from);
+use Bugzilla::Constants;
+use Bugzilla::Install::Filesystem;
+
+use File::Basename;
+
+our $VERSION = '0.1';
+
+################
+# Installation #
+################
+
+sub install_filesystem {
+ my ($self, $args) = @_;
+ my $files = $args->{'files'};
+
+ my $extensionsdir = bz_locations()->{'extensionsdir'};
+ my $scriptname = $extensionsdir . "/" . __PACKAGE__->NAME . "/bin/rest.cgi";
+
+ $files->{$scriptname} = {
+ perms => Bugzilla::Install::Filesystem::WS_EXECUTE
+ };
+}
+
+##################
+# Template Hooks #
+##################
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my $vars = $args->{'vars'};
+ my $file = $args->{'file'};
+
+ if ($file =~ /config\.json\.tmpl$/) {
+ $vars->{'initial_status'} = Bugzilla::Status->can_change_to;
+ $vars->{'status_objects'} = [ Bugzilla::Status->get_all ];
+ }
+}
+
+##############
+# Code Hooks #
+##############
+
+sub bug_start_of_update {
+ my ($self, $args) = @_;
+ my $old_bug = $args->{old_bug};
+ my $params = Bugzilla->input_params;
+
+ return if !Bugzilla->request_cache->{bzapi};
+
+ # Check for a mid-air collision. Currently this only works when updating
+ # an individual bug and if last_changed_time is provided. Otherwise it
+ # allows the changes.
+ my $delta_ts = $params->{last_change_time} || '';
+
+ if ($delta_ts && exists $params->{ids} && @{ $params->{ids} } == 1) {
+ _midair_check($delta_ts, $old_bug->delta_ts);
+ }
+}
+
+sub object_end_of_set_all {
+ my ($self, $args) = @_;
+ my $object = $args->{object};
+ my $params = Bugzilla->input_params;
+
+ return if !Bugzilla->request_cache->{bzapi};
+ return if !$object->isa('Bugzilla::Attachment');
+
+ # Check for a mid-air collision. Currently this only works when updating
+ # an individual attachment and if last_changed_time is provided. Otherwise it
+ # allows the changes.
+ my $stash = Bugzilla->request_cache->{bzapi_stash} ||= {};
+ my $delta_ts = $stash->{last_change_time};
+
+ _midair_check($delta_ts, $object->modification_time) if $delta_ts;
+}
+
+sub _midair_check {
+ my ($delta_ts, $old_delta_ts) = @_;
+ my $delta_ts_z = datetime_from($delta_ts)
+ || ThrowCodeError('invalid_timestamp', { timestamp => $delta_ts });
+ my $old_delta_tz_z = datetime_from($old_delta_ts);
+ if ($old_delta_tz_z ne $delta_ts_z) {
+ ThrowUserError('bzapi_midair_collision');
+ }
+}
+
+sub webservice_error_codes {
+ my ($self, $args) = @_;
+ my $error_map = $args->{error_map};
+ $error_map->{'bzapi_midair_collision'} = 400;
+}
+
+sub webservice_fix_credentials {
+ my ($self, $args) = @_;
+ my $rpc = $args->{rpc};
+ my $params = $args->{params};
+ return if !Bugzilla->request_cache->{bzapi};
+ fix_credentials($params);
+}
+
+sub webservice_rest_request {
+ my ($self, $args) = @_;
+ my $rpc = $args->{rpc};
+ my $params = $args->{params};
+ my $cache = Bugzilla->request_cache;
+
+ return if !$cache->{bzapi};
+
+ # Stash certain values for later use
+ $cache->{bzapi_rpc} = $rpc;
+
+ # Internal websevice method being used
+ $cache->{bzapi_rpc_method} = $rpc->path_info . "." . $rpc->bz_method_name;
+
+ # Load the appropriate request handler based on path and type
+ if (my $handler = _find_handler($rpc, 'request')) {
+ &$handler($params);
+ }
+}
+
+sub webservice_rest_response {
+ my ($self, $args) = @_;
+ my $rpc = $args->{rpc};
+ my $result = $args->{result};
+ my $response = $args->{response};
+ my $cache = Bugzilla->request_cache;
+
+ # Stash certain values for later use
+ $cache->{bzapi_rpc} ||= $rpc;
+
+ return if !Bugzilla->request_cache->{bzapi}
+ || ref $$result ne 'HASH';
+
+ if (exists $$result->{error}) {
+ $$result->{documentation} = BZAPI_DOC;
+ return;
+ }
+
+ # Load the appropriate response handler based on path and type
+ if (my $handler = _find_handler($rpc, 'response')) {
+ &$handler($result, $response);
+ }
+}
+
+sub webservice_rest_resources {
+ my ($self, $args) = @_;
+ my $rpc = $args->{rpc};
+ my $resources = $args->{resources};
+
+ return if !Bugzilla->request_cache->{bzapi};
+
+ _add_resources($rpc, $resources);
+}
+
+#####################
+# Utility Functions #
+#####################
+
+sub _find_handler {
+ my ($rpc, $type) = @_;
+
+ my $path_info = $rpc->cgi->path_info;
+ my $request_method = $rpc->request->method;
+
+ my $module = $rpc->bz_class_name || '';
+ $module =~ s/^Bugzilla::WebService:://;
+
+ my $cache = _preload_handlers();
+
+ return undef if !exists $cache->{$module};
+
+ # Make a copy of the handler array so
+ # as to not alter the actual cached data.
+ my @handlers = @{ $cache->{$module} };
+
+ while (my $regex = shift @handlers) {
+ my $data = shift @handlers;
+ next if ref $data ne 'HASH';
+ if ($path_info =~ $regex
+ && exists $data->{$request_method}
+ && exists $data->{$request_method}->{$type})
+ {
+ return $data->{$request_method}->{$type};
+ }
+ }
+
+ return undef;
+}
+
+sub _add_resources {
+ my ($rpc, $native_resources) = @_;
+
+ my $cache = _preload_handlers();
+
+ foreach my $module (keys %$cache) {
+ my $native_module = "Bugzilla::WebService::$module";
+ next if !$native_resources->{$native_module};
+
+ # Make a copy of the handler array so
+ # as to not alter the actual cached data.
+ my @handlers = @{ $cache->{$module} };
+
+ my @ext_resources = ();
+ while (my $regex = shift @handlers) {
+ my $data = shift @handlers;
+ next if ref $data ne 'HASH';
+ my $new_data = {};
+ foreach my $request_method (keys %$data) {
+ next if !exists $data->{$request_method}->{resource};
+ $new_data->{$request_method} = $data->{$request_method}->{resource};
+ }
+ push(@ext_resources, $regex, $new_data);
+ }
+
+ # Places the new resources at the beginning of the list
+ # so we can capture specific paths before the native resources
+ unshift(@{$native_resources->{$native_module}}, @ext_resources);
+ }
+}
+
+sub _resource_modules {
+ my $extdir = bz_locations()->{extensionsdir};
+ return map { basename($_, '.pm') } glob("$extdir/" . __PACKAGE__->NAME . "/lib/Resources/*.pm");
+}
+
+# preload all handlers into cache
+# since we don't want to parse all
+# this multiple times
+sub _preload_handlers {
+ my $cache = Bugzilla->request_cache;
+
+ if (!exists $cache->{rest_handlers}) {
+ my $all_handlers = {};
+ foreach my $module (_resource_modules()) {
+ my $resource_class = "Bugzilla::Extension::BzAPI::Resources::$module";
+ trick_taint($resource_class);
+ eval("require $resource_class");
+ warn $@ if $@;
+ next if ($@ || !$resource_class->can('rest_handlers'));
+ my $handlers = $resource_class->rest_handlers;
+ next if (ref $handlers ne 'ARRAY' || scalar @$handlers % 2 != 0);
+ $all_handlers->{$module} = $handlers;
+ }
+ $cache->{rest_handlers} = $all_handlers;
+ }
+
+ return $cache->{rest_handlers};
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/BzAPI/bin/rest.cgi b/extensions/BzAPI/bin/rest.cgi
new file mode 100755
index 000000000..37cbab437
--- /dev/null
+++ b/extensions/BzAPI/bin/rest.cgi
@@ -0,0 +1,34 @@
+#!/usr/bin/perl -wT
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use lib qw(../../.. ../../../lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::WebService::Constants;
+BEGIN {
+ if (!Bugzilla->feature('rest')
+ || !Bugzilla->feature('jsonrpc'))
+ {
+ ThrowUserError('feature_disabled', { feature => 'rest' });
+ }
+}
+
+# Set request_cache bzapi value to true in order to enable the
+# BzAPI extension functionality
+Bugzilla->request_cache->{bzapi} = 1;
+
+use Bugzilla::WebService::Server::REST;
+Bugzilla->usage_mode(USAGE_MODE_REST);
+local @INC = (bz_locations()->{extensionsdir}, @INC);
+my $server = new Bugzilla::WebService::Server::REST;
+$server->version('1.1');
+$server->handle();
diff --git a/extensions/BzAPI/lib/Constants.pm b/extensions/BzAPI/lib/Constants.pm
new file mode 100644
index 000000000..65ae00480
--- /dev/null
+++ b/extensions/BzAPI/lib/Constants.pm
@@ -0,0 +1,155 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BzAPI::Constants;
+
+use strict;
+
+use base qw(Exporter);
+our @EXPORT = qw(
+ USER_FIELDS
+ BUG_FIELD_MAP
+ BOOLEAN_TYPE_MAP
+ ATTACHMENT_FIELD_MAP
+ DEFAULT_BUG_FIELDS
+ DEFAULT_ATTACHMENT_FIELDS
+
+ BZAPI_DOC
+);
+
+# These are fields that are normally exported as a single value such
+# as the user's email. BzAPI needs to convert them to user objects
+# where possible.
+use constant USER_FIELDS => (qw(
+ assigned_to
+ cc
+ creator
+ qa_contact
+ reporter
+));
+
+# Convert old field names from old to new
+use constant BUG_FIELD_MAP => {
+ 'opendate' => 'creation_time', # query
+ 'creation_ts' => 'creation_time',
+ 'changeddate' => 'last_change_time', # query
+ 'delta_ts' => 'last_change_time',
+ 'bug_id' => 'id',
+ 'rep_platform' => 'platform',
+ 'bug_severity' => 'severity',
+ 'bug_status' => 'status',
+ 'short_desc' => 'summary',
+ 'bug_file_loc' => 'url',
+ 'status_whiteboard' => 'whiteboard',
+ 'reporter' => 'creator',
+ 'reporter_realname' => 'creator_realname',
+ 'cclist_accessible' => 'is_cc_accessible',
+ 'reporter_accessible' => 'is_creator_accessible',
+ 'everconfirmed' => 'is_confirmed',
+ 'dependson' => 'depends_on',
+ 'blocked' => 'blocks',
+ 'attachment' => 'attachments',
+ 'flag' => 'flags',
+ 'flagtypes.name' => 'flag',
+ 'bug_group' => 'group',
+ 'group' => 'groups',
+ 'longdesc' => 'comment',
+ 'bug_file_loc_type' => 'url_type',
+ 'bugidtype' => 'id_mode',
+ 'longdesc_type' => 'comment_type',
+ 'short_desc_type' => 'summary_type',
+ 'status_whiteboard_type' => 'whiteboard_type',
+ 'emailassigned_to1' => 'email1_assigned_to',
+ 'emailassigned_to2' => 'email2_assigned_to',
+ 'emailcc1' => 'email1_cc',
+ 'emailcc2' => 'email2_cc',
+ 'emailqa_contact1' => 'email1_qa_contact',
+ 'emailqa_contact2' => 'email2_qa_contact',
+ 'emailreporter1' => 'email1_creator',
+ 'emailreporter2' => 'email2_creator',
+ 'emaillongdesc1' => 'email1_comment_creator',
+ 'emaillongdesc2' => 'email2_comment_creator',
+ 'emailtype1' => 'email1_type',
+ 'emailtype2' => 'email2_type',
+ 'chfieldfrom' => 'changed_after',
+ 'chfieldto' => 'changed_before',
+ 'chfield' => 'changed_field',
+ 'chfieldvalue' => 'changed_field_to',
+ 'deadlinefrom' => 'deadline_after',
+ 'deadlineto' => 'deadline_before',
+ 'attach_data.thedata' => 'attachment.data',
+ 'longdescs.isprivate' => 'comment.is_private',
+ 'commenter' => 'comment.creator',
+ 'requestees.login_name' => 'flag.requestee',
+ 'setters.login_name' => 'flag.setter',
+ 'days_elapsed' => 'idle',
+ 'owner_idle_time' => 'assignee_idle',
+ 'dup_id' => 'dupe_of',
+ 'isopened' => 'is_open',
+ 'flag_type' => 'flag_types',
+ 'attachments.submitter' => 'attachment.attacher',
+ 'attachments.filename' => 'attachment.file_name',
+ 'attachments.description' => 'attachment.description',
+ 'attachments.delta_ts' => 'attachment.last_change_time',
+ 'attachments.isobsolete' => 'attachment.is_obsolete',
+ 'attachments.ispatch' => 'attachment.is_patch',
+ 'attachments.isprivate' => 'attachment.is_private',
+ 'attachments.mimetype' => 'attachment.content_type',
+ 'attachments.date' => 'attachment.creation_time',
+ 'attachments.attachid' => 'attachment.id',
+ 'attachments.flag' => 'attachment.flags',
+ 'attachments.token' => 'attachment.update_token'
+};
+
+# Convert from old boolean chart type names to new names
+use constant BOOLEAN_TYPE_MAP => {
+ 'equals' => 'equals',
+ 'not_equals' => 'notequals',
+ 'equals_any' => 'anyexact',
+ 'contains' => 'substring',
+ 'not_contains' => 'notsubstring',
+ 'case_contains' => 'casesubstring',
+ 'contains_any' => 'anywordssubstr',
+ 'not_contains_any' => 'nowordssubstr',
+ 'contains_all' => 'allwordssubstr',
+ 'contains_any_words' => 'anywords',
+ 'not_contains_any_words' => 'nowords',
+ 'contains_all_words' => 'allwords',
+ 'regex' => 'regexp',
+ 'not_regex' => 'notregexp',
+ 'less_than' => 'lessthan',
+ 'greater_than' => 'greaterthan',
+ 'changed_before' => 'changedbefore',
+ 'changed_after' => 'changedafter',
+ 'changed_from' => 'changedfrom',
+ 'changed_to' => 'changedto',
+ 'changed_by' => 'changedby',
+ 'matches' => 'matches'
+};
+
+# Convert old attachment field names from old to new
+use constant ATTACHMENT_FIELD_MAP => {
+ 'submitter' => 'attacher',
+ 'description' => 'description',
+ 'filename' => 'file_name',
+ 'delta_ts' => 'last_change_time',
+ 'isobsolete' => 'is_obsolete',
+ 'ispatch' => 'is_patch',
+ 'isprivate' => 'is_private',
+ 'mimetype' => 'content_type',
+ 'contenttypeentry' => 'content_type',
+ 'date' => 'creation_time',
+ 'attachid' => 'id',
+ 'desc' => 'description',
+ 'flag' => 'flags',
+ 'type' => 'content_type',
+};
+
+# A base link to the current BzAPI Documentation.
+use constant BZAPI_DOC => 'https://wiki.mozilla.org/Bugzilla:BzAPI';
+
+1;
diff --git a/extensions/BzAPI/lib/Resources/Bug.pm b/extensions/BzAPI/lib/Resources/Bug.pm
new file mode 100644
index 000000000..77b567421
--- /dev/null
+++ b/extensions/BzAPI/lib/Resources/Bug.pm
@@ -0,0 +1,867 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BzAPI::Resources::Bug;
+
+use 5.10.1;
+use strict;
+
+use Bugzilla::Bug;
+use Bugzilla::Error;
+use Bugzilla::Token qw(issue_hash_token);
+use Bugzilla::Util qw(trick_taint diff_arrays);
+use Bugzilla::WebService::Constants;
+
+use Bugzilla::Extension::BzAPI::Util;
+use Bugzilla::Extension::BzAPI::Constants;
+
+use List::MoreUtils qw(uniq);
+use List::Util qw(max);
+
+#################
+# REST Handlers #
+#################
+
+BEGIN {
+ require Bugzilla::WebService::Bug;
+ *Bugzilla::WebService::Bug::get_bug_count = \&get_bug_count_resource;
+}
+
+sub rest_handlers {
+ my $rest_handlers = [
+ qr{^/bug$}, {
+ GET => {
+ request => \&search_bugs_request,
+ response => \&search_bugs_response
+ },
+ POST => {
+ request => \&create_bug_request,
+ response => \&create_bug_response
+ }
+ },
+ qr{^/bug/([^/]+)$}, {
+ GET => {
+ response => \&get_bug_response
+ },
+ PUT => {
+ request => \&update_bug_request,
+ response => \&update_bug_response
+ }
+ },
+ qr{^/bug/([^/]+)/comment$}, {
+ GET => {
+ response => \&get_comments_response
+ },
+ POST => {
+ request => \&add_comment_request,
+ response => \&add_comment_response
+ }
+ },
+ qr{^/bug/([^/]+)/history$}, {
+ GET => {
+ response => \&get_history_response
+ }
+ },
+ qr{^/bug/([^/]+)/attachment$}, {
+ GET => {
+ response => \&get_attachments_response
+ },
+ POST => {
+ request => \&add_attachment_request,
+ response => \&add_attachment_response
+ }
+ },
+ qr{^/bug/attachment/([^/]+)$}, {
+ GET => {
+ response => \&get_attachment_response
+ },
+ PUT => {
+ request => \&update_attachment_request,
+ response => \&update_attachment_response
+ }
+ },
+ qr{^/attachment/([^/]+)$}, {
+ GET => {
+ response => \&get_attachment_response
+ },
+ PUT => {
+ request => \&update_attachment_request,
+ response => \&update_attachment_response
+ }
+ },
+ qr{^/bug/([^/]+)/flag$}, {
+ GET => {
+ resource => {
+ method => 'get',
+ params => sub {
+ return { ids => [ $_[0] ],
+ include_fields => ['flags'] };
+ }
+ },
+ response => \&get_bug_flags_response,
+ }
+ },
+ qr{^/count$}, {
+ GET => {
+ resource => {
+ method => 'get_bug_count'
+ }
+ }
+ },
+ qr{^/attachment/([^/]+)$}, {
+ GET => {
+ resource => {
+ method => 'attachments',
+ params => sub {
+ return { attachment_ids => [ $_[0] ] };
+ }
+ }
+ },
+ PUT => {
+ resource => {
+ method => 'update_attachment',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ }
+ }
+ }
+ }
+ ];
+ return $rest_handlers;
+}
+
+#########################
+# REST Resource Methods #
+#########################
+
+# Return bug counts based on row/col/table fields
+# FIXME Borrowed a lot of code from report.cgi, eventually
+# this should be broken into it's own module so that report.cgi
+# and here can share the same code.
+sub get_bug_count_resource {
+ my ($self, $params) = @_;
+
+ Bugzilla->switch_to_shadow_db();
+
+ my $col_field = $params->{x_axis_field} || '';
+ my $row_field = $params->{y_axis_field} || '';
+ my $tbl_field = $params->{z_axis_field} || '';
+
+ my $dimensions = $col_field ?
+ $row_field ?
+ $tbl_field ? 3 : 2 : 1 : 0;
+
+ if ($dimensions == 0) {
+ $col_field = "bug_status";
+ $params->{x_axis_field} = "bug_status";
+ }
+
+ # Valid bug fields that can be reported on.
+ my $valid_columns = Bugzilla::Search::REPORT_COLUMNS;
+
+ # Convert external names to internal if necessary
+ $params = Bugzilla::Bug::map_fields($params);
+ $row_field = Bugzilla::Bug::FIELD_MAP->{$row_field} || $row_field;
+ $col_field = Bugzilla::Bug::FIELD_MAP->{$col_field} || $col_field;
+ $tbl_field = Bugzilla::Bug::FIELD_MAP->{$tbl_field} || $tbl_field;
+
+ # Validate the values in the axis fields or throw an error.
+ !$row_field
+ || ($valid_columns->{$row_field} && trick_taint($row_field))
+ || ThrowCodeError("report_axis_invalid", { fld => "x", val => $row_field });
+ !$col_field
+ || ($valid_columns->{$col_field} && trick_taint($col_field))
+ || ThrowCodeError("report_axis_invalid", { fld => "y", val => $col_field });
+ !$tbl_field
+ || ($valid_columns->{$tbl_field} && trick_taint($tbl_field))
+ || ThrowCodeError("report_axis_invalid", { fld => "z", val => $tbl_field });
+
+ my @axis_fields = grep { $_ } ($row_field, $col_field, $tbl_field);
+
+ my $search = new Bugzilla::Search(
+ fields => \@axis_fields,
+ params => $params,
+ allow_unlimited => 1,
+ );
+
+ my ($results, $extra_data) = $search->data;
+
+ # We have a hash of hashes for the data itself, and a hash to hold the
+ # row/col/table names.
+ my %data;
+ my %names;
+
+ # Read the bug data and count the bugs for each possible value of row, column
+ # and table.
+ #
+ # We detect a numerical field, and sort appropriately, if all the values are
+ # numeric.
+ my $col_isnumeric = 1;
+ my $row_isnumeric = 1;
+ my $tbl_isnumeric = 1;
+
+ foreach my $result (@$results) {
+ # handle empty dimension member names
+ my $row = check_value($row_field, $result);
+ my $col = check_value($col_field, $result);
+ my $tbl = check_value($tbl_field, $result);
+
+ $data{$tbl}{$col}{$row}++;
+ $names{"col"}{$col}++;
+ $names{"row"}{$row}++;
+ $names{"tbl"}{$tbl}++;
+
+ $col_isnumeric &&= ($col =~ /^-?\d+(\.\d+)?$/o);
+ $row_isnumeric &&= ($row =~ /^-?\d+(\.\d+)?$/o);
+ $tbl_isnumeric &&= ($tbl =~ /^-?\d+(\.\d+)?$/o);
+ }
+
+ my @col_names = get_names($names{"col"}, $col_isnumeric, $col_field);
+ my @row_names = get_names($names{"row"}, $row_isnumeric, $row_field);
+ my @tbl_names = get_names($names{"tbl"}, $tbl_isnumeric, $tbl_field);
+
+ push(@tbl_names, "-total-") if (scalar(@tbl_names) > 1);
+
+ my @data;
+ foreach my $tbl (@tbl_names) {
+ my @tbl_data;
+ foreach my $row (@row_names) {
+ my @col_data;
+ foreach my $col (@col_names) {
+ $data{$tbl}{$col}{$row} = $data{$tbl}{$col}{$row} || 0;
+ push(@col_data, $data{$tbl}{$col}{$row});
+ if ($tbl ne "-total-") {
+ # This is a bit sneaky. We spend every loop except the last
+ # building up the -total- data, and then last time round,
+ # we process it as another tbl, and push() the total values
+ # into the image_data array.
+ $data{"-total-"}{$col}{$row} += $data{$tbl}{$col}{$row};
+ }
+ }
+ push(@tbl_data, \@col_data);
+ }
+ push(@data, \@tbl_data);
+ }
+
+ my $result = {};
+ if ($dimensions == 0) {
+ my $sum = 0;
+
+ # If the search returns no results, we just get an 0-byte file back
+ # and so there is no data at all.
+ if (@data) {
+ foreach my $value (@{ $data[0][0] }) {
+ $sum += $value;
+ }
+ }
+
+ $result = {
+ 'data' => $sum
+ };
+ }
+ elsif ($dimensions == 1) {
+ $result = {
+ 'x_labels' => \@col_names,
+ 'data' => $data[0][0] || []
+ };
+ }
+ elsif ($dimensions == 2) {
+ $result = {
+ 'x_labels' => \@col_names,
+ 'y_labels' => \@row_names,
+ 'data' => $data[0] || [[]]
+ };
+ }
+ elsif ($dimensions == 3) {
+ if (@data > 1 && $tbl_names[-1] eq "-total-") {
+ # Last table is a total, which we discard
+ pop(@data);
+ pop(@tbl_names);
+ }
+
+ $result = {
+ 'x_labels' => \@col_names,
+ 'y_labels' => \@row_names,
+ 'z_labels' => \@tbl_names,
+ 'data' => @data ? \@data : [[[]]]
+ };
+ }
+
+ return $result;
+}
+
+sub get_names {
+ my ($names, $isnumeric, $field_name) = @_;
+ my ($field, @sorted);
+ # XXX - This is a hack to handle the actual_time/work_time field,
+ # because it's named 'actual_time' in Search.pm but 'work_time' in Field.pm.
+ $_[2] = $field_name = 'work_time' if $field_name eq 'actual_time';
+
+ # _realname fields aren't real Bugzilla::Field objects, but they are a
+ # valid axis, so we don't vailidate them as Bugzilla::Field objects.
+ $field = Bugzilla::Field->check($field_name)
+ if ($field_name && $field_name !~ /_realname$/);
+
+ if ($field && $field->is_select) {
+ foreach my $value (@{$field->legal_values}) {
+ push(@sorted, $value->name) if $names->{$value->name};
+ }
+ unshift(@sorted, '---') if $field_name eq 'resolution';
+ @sorted = uniq @sorted;
+ }
+ elsif ($isnumeric) {
+ # It's not a field we are preserving the order of, so sort it
+ # numerically...
+ @sorted = sort { $a <=> $b } keys %$names;
+ }
+ else {
+ # ...or alphabetically, as appropriate.
+ @sorted = sort keys %$names;
+ }
+
+ return @sorted;
+}
+
+sub check_value {
+ my ($field, $result) = @_;
+
+ my $value;
+ if (!defined $field) {
+ $value = '';
+ }
+ elsif ($field eq '') {
+ $value = ' ';
+ }
+ else {
+ $value = shift @$result;
+ $value = ' ' if (!defined $value || $value eq '');
+ $value = '---' if ($field eq 'resolution' && $value eq ' ');
+ }
+ return $value;
+}
+
+########################
+# REST Request Methods #
+########################
+
+sub search_bugs_request {
+ my ($params) = @_;
+
+ if (defined $params->{changed_field}
+ && $params->{changed_field} eq "creation_time")
+ {
+ $params->{changed_field} = "[Bug creation]";
+ }
+
+ my $FIELD_NEW_TO_OLD = { reverse %{ BUG_FIELD_MAP() } };
+
+ # Update values of various forms.
+ foreach my $key (keys %$params) {
+ # First, search types. These are found in the value of any field ending
+ # _type, and the value of any field matching type\d-\d-\d.
+ if ($key =~ /^type(\d+)-(\d+)-(\d+)$|_type$/) {
+ $params->{$key}
+ = BOOLEAN_TYPE_MAP->{$params->{$key}} || $params->{$key};
+ }
+
+ # Field names hiding in values instead of keys: changed_field, boolean
+ # charts and axis names.
+ if ($key =~ /^(field\d+-\d+-\d+|
+ changed_field|
+ (x|y|z)_axis_field)$
+ /x) {
+ $params->{$key}
+ = $FIELD_NEW_TO_OLD->{$params->{$key}} || $params->{$key};
+ }
+ }
+
+ # Update field names
+ foreach my $field (keys %$FIELD_NEW_TO_OLD) {
+ if (defined $params->{$field}) {
+ $params->{$FIELD_NEW_TO_OLD->{$field}} = delete $params->{$field};
+ }
+ }
+
+ if (exists $params->{bug_id_type}) {
+ $params->{bug_id_type}
+ = BOOLEAN_TYPE_MAP->{$params->{bug_id_type}} || $params->{bug_id_type};
+ }
+
+ # Time field names are screwy, and got reused. We can't put this mapping
+ # in NEW2OLD as everything will go haywire. actual_time has to be queried
+ # as work_time even though work_time is the submit-only field for _adding_
+ # to actual_time, which can't be arbitrarily manipulated.
+ if (defined $params->{work_time}) {
+ $params->{actual_time} = delete $params->{work_time};
+ }
+
+ # Other convenience search ariables used by BzAPI
+ my @field_ids = grep(/^f(\d+)$/, keys %$params);
+ my $last_field_id = @field_ids ? max @field_ids + 1 : 1;
+ foreach my $field (qw(setters.login_name requestees.login_name)) {
+ if (my $value = delete $params->{$field}) {
+ $params->{"f${last_field_id}"} = $FIELD_NEW_TO_OLD->{$field} || $field;
+ $params->{"o${last_field_id}"} = 'equals';
+ $params->{"v${last_field_id}"} = $value;
+ $last_field_id++;
+ }
+ }
+}
+
+sub create_bug_request {
+ my ($params) = @_;
+
+ # User roles such as assigned_to and qa_contact should be just the
+ # email (login) of the user you want to set to.
+ foreach my $field (qw(assigned_to qa_contact)) {
+ if (exists $params->{$field}) {
+ $params->{$field} = $params->{$field}->{name};
+ }
+ }
+
+ # CC should just be a list of bugzilla logins
+ if (exists $params->{cc}) {
+ $params->{cc} = [ map { $_->{name} } @{ $params->{cc} } ];
+ }
+
+ # Comment
+ if (exists $params->{comments}) {
+ $params->{comment_is_private} = $params->{comments}->[0]->{is_private};
+ $params->{description} = $params->{comments}->[0]->{text};
+ delete $params->{comments};
+ }
+
+ # Some fields are not supported by Bugzilla::Bug->create but are supported
+ # by Bugzilla::Bug->update :(
+ my $cache = Bugzilla->request_cache->{bzapi_bug_create_extra} ||= {};
+ foreach my $field (qw(remaining_time)) {
+ next if !exists $params->{$field};
+ $cache->{$field} = delete $params->{$field};
+ }
+
+ # remove username/password
+ delete $params->{username};
+ delete $params->{password};
+}
+
+sub update_bug_request {
+ my ($params) = @_;
+
+ my $bug_id = ref $params->{ids} ? $params->{ids}->[0] : $params->{ids};
+ my $bug = Bugzilla::Bug->check($bug_id);
+
+ # Convert groups to proper add/remove lists
+ if (exists $params->{groups}) {
+ my @new_groups = map { $_->{name} } @{ $params->{groups} };
+ my @old_groups = map { $_->name } @{ $bug->groups_in };
+ my ($removed, $added) = diff_arrays(\@old_groups, \@new_groups);
+ if (@$added || @$removed) {
+ my $groups_data = {};
+ $groups_data->{add} = $added if @$added;
+ $groups_data->{remove} = $removed if @$removed;
+ $params->{groups} = $groups_data;
+ }
+ else {
+ delete $params->{groups};
+ }
+ }
+
+ # Other fields such as keywords, blocks depends_on
+ # support 'set' which will make the list exactly what
+ # the user passes in.
+ foreach my $field (qw(blocks depends_on dependson keywords)) {
+ if (exists $params->{$field}) {
+ $params->{$field} = { set => $params->{$field} };
+ }
+ }
+
+ # User roles such as assigned_to and qa_contact should be just the
+ # email (login) of the user you want to change to. Also if defined
+ # but set to NULL then we reset them to default
+ foreach my $field (qw(assigned_to qa_contact)) {
+ if (exists $params->{$field}) {
+ if (!$params->{$field}) {
+ $params->{"reset_$field"} = 1;
+ delete $params->{$field};
+ }
+ else {
+ $params->{$field} = $params->{$field}->{name};
+ }
+ }
+ }
+
+ # CC is treated like groups in that we need 'add' and 'remove' keys
+ if (exists $params->{cc}) {
+ my $new_cc = [ map { $_->{name} } @{ $params->{cc} } ];
+ my ($removed, $added) = diff_arrays($bug->cc, $new_cc);
+ if (@$added || @$removed) {
+ my $cc_data = {};
+ $cc_data->{add} = $added if @$added;
+ $cc_data->{remove} = $removed if @$removed;
+ $params->{cc} = $cc_data;
+ }
+ else {
+ delete $params->{cc};
+ }
+ }
+
+ # see_also is treated like groups in that we need 'add' and 'remove' keys
+ if (exists $params->{see_also}) {
+ my $old_see_also = [ map { $_->name } @{ $bug->see_also } ];
+ my ($removed, $added) = diff_arrays($old_see_also, $params->{see_also});
+ if (@$added || @$removed) {
+ my $data = {};
+ $data->{add} = $added if @$added;
+ $data->{remove} = $removed if @$removed;
+ $params->{see_also} = $data;
+ }
+ else {
+ delete $params->{see_also};
+ }
+ }
+
+ # BzAPI allows for adding comments by appending to the list of current
+ # comments and passing the whole list back.
+ # 1. If a comment id is specified, the user can update the comment privacy
+ # 2. If no id is specified it is considered a new comment but only the last
+ # one will be accepted.
+ my %comment_is_private;
+ foreach my $comment (@{ $params->{'comments'} }) {
+ if (my $id = $comment->{'id'}) {
+ # Existing comment; tweak privacy flags if necessary
+ $comment_is_private{$id}
+ = ($comment->{'is_private'} && $comment->{'is_private'} eq "true") ? 1 : 0;
+ }
+ else {
+ # New comment to be added
+ # If multiple new comments are specified, only the last one will be
+ # added.
+ $params->{comment} = {
+ body => $comment->{text},
+ is_private => ($comment->{'is_private'} &&
+ $comment->{'is_private'} eq "true") ? 1 : 0
+ };
+ }
+ }
+ $params->{comment_is_private} = \%comment_is_private if %comment_is_private;
+
+ # Remove setter and convert requestee to just name
+ if (exists $params->{flags}) {
+ foreach my $flag (@{ $params->{flags} }) {
+ delete $flag->{setter}; # Always use logged in user
+ if (exists $flag->{requestee} && ref $flag->{requestee}) {
+ $flag->{requestee} = $flag->{requestee}->{name};
+ }
+ # If no flag id provided, assume it is new
+ if (!exists $flag->{id}) {
+ $flag->{new} = 1;
+ }
+ }
+ }
+}
+
+sub add_comment_request {
+ my ($params) = @_;
+ $params->{comment} = delete $params->{text} if $params->{text};
+}
+
+sub add_attachment_request {
+ my ($params) = @_;
+
+ # Bug.add_attachment uses 'summary' for description.
+ if ($params->{description}) {
+ $params->{summary} = $params->{description};
+ delete $params->{description};
+ }
+
+ # Remove setter and convert requestee to just name
+ if (exists $params->{flags}) {
+ foreach my $flag (@{ $params->{flags} }) {
+ delete $flag->{setter}; # Always use logged in user
+ if (exists $flag->{requestee} && ref $flag->{requestee}) {
+ $flag->{requestee} = $flag->{requestee}->{name};
+ }
+ }
+ }
+
+ # Add comment if one is provided
+ if (exists $params->{comments} && scalar @{ $params->{comments} }) {
+ $params->{comment} = $params->{comments}->[0]->{text};
+ delete $params->{comments};
+ }
+}
+
+sub update_attachment_request {
+ my ($params) = @_;
+
+ # Stash away for midair checking later
+ if ($params->{last_change_time}) {
+ my $stash = Bugzilla->request_cache->{bzapi_stash} ||= {};
+ $stash->{last_change_time} = delete $params->{last_change_time};
+ }
+
+ # Immutable values
+ foreach my $key (qw(attacher bug_id bug_ref creation_time
+ encoding id ref size update_token)) {
+ delete $params->{$key};
+ }
+
+ # Convert setter and requestee to standard values
+ if (exists $params->{flags}) {
+ foreach my $flag (@{ $params->{flags} }) {
+ delete $flag->{setter}; # Always use logged in user
+ if (exists $flag->{requestee} && ref $flag->{requestee}) {
+ $flag->{requestee} = $flag->{requestee}->{name};
+ }
+ }
+ }
+
+ # Add comment if one is provided
+ if (exists $params->{comments} && scalar @{ $params->{comments} }) {
+ $params->{comment} = $params->{comments}->[0]->{text};
+ delete $params->{comments};
+ }
+}
+
+#########################
+# REST Response Methods #
+#########################
+
+sub search_bugs_response {
+ my ($result, $response) = @_;
+ my $cache = Bugzilla->request_cache;
+ my $params = Bugzilla->input_params;
+
+ return if !exists $$result->{bugs};
+
+ my $bug_objs = $cache->{bzapi_search_bugs};
+
+ my @fixed_bugs;
+ foreach my $bug_data (@{$$result->{bugs}}) {
+ my $bug_obj = shift @$bug_objs;
+ my $fixed = fix_bug($bug_data, $bug_obj);
+
+ # CC count and Dupe count
+ if (filter_wants_nocache($params, 'cc_count')) {
+ $fixed->{cc_count} = scalar @{ $bug_obj->cc }
+ if $bug_obj->cc;
+ }
+ if (filter_wants_nocache($params, 'dupe_count')) {
+ $fixed->{dupe_count} = scalar @{ $bug_obj->duplicate_ids }
+ if $bug_obj->duplicate_ids;
+ }
+
+ push(@fixed_bugs, $fixed);
+ }
+
+ $$result->{bugs} = \@fixed_bugs;
+}
+
+sub create_bug_response {
+ my ($result, $response) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+
+ return if !exists $$result->{id};
+ my $bug_id = $$result->{id};
+
+ $$result->{ref} = $rpc->type('string', ref_urlbase() . "/bug/$bug_id");
+ $response->code(STATUS_CREATED);
+}
+
+sub get_bug_response {
+ my ($result) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+
+ return if !exists $$result->{bugs};
+ my $bug_data = $$result->{bugs}->[0];
+
+ my $bug_id = $rpc->bz_rest_params->{ids}->[0];
+ my $bug_obj = Bugzilla::Bug->check($bug_id);
+ my $fixed = fix_bug($bug_data, $bug_obj);
+
+ $$result = $fixed;
+}
+
+sub update_bug_response {
+ my ($result) = @_;
+ return if !exists $$result->{bugs}
+ || !scalar @{$$result->{bugs}};
+ $$result = { ok => 1 };
+}
+
+# Get all comments for a bug
+sub get_comments_response {
+ my ($result) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ my $params = Bugzilla->input_params;
+
+ return if !exists $$result->{bugs};
+
+ my $bug_id = $rpc->bz_rest_params->{ids}->[0];
+ my $bug = Bugzilla::Bug->check($bug_id);
+
+ my $comment_objs = $bug->comments({ order => 'oldest_to_newest',
+ after => $params->{new_since} });
+ my @filtered_comment_objs;
+ foreach my $comment (@$comment_objs) {
+ next if $comment->is_private && !Bugzilla->user->is_insider;
+ push(@filtered_comment_objs, $comment);
+ }
+
+ my $comments_data = $$result->{bugs}->{$bug_id}->{comments};
+
+ my @fixed_comments;
+ foreach my $comment_data (@$comments_data) {
+ my $comment_obj = shift @filtered_comment_objs;
+ my $fixed = fix_comment($comment_data, $comment_obj);
+
+ if (exists $fixed->{creator}) {
+ # /bug/<ID>/comment returns full login for creator but not for /bug/<ID>?include_fields=comments :(
+ $fixed->{creator}->{name} = $rpc->type('string', $comment_obj->author->login);
+ # /bug/<ID>/comment does not return real_name for creator but returns ref
+ $fixed->{creator}->{'ref'} = $rpc->type('string', ref_urlbase() . "/user/" . $comment_obj->author->login);
+ delete $fixed->{creator}->{real_name};
+ }
+
+ push(@fixed_comments, filter($params, $fixed));
+ }
+
+ $$result = { comments => \@fixed_comments };
+}
+
+# Format the return response on successful comment creation
+sub add_comment_response {
+ my ($result, $response) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+
+ return if !exists $$result->{id};
+ my $bug_id = $rpc->bz_rest_params->{id};
+
+ $$result = { ref => $rpc->type('string', ref_urlbase() . "/bug/$bug_id/comment") };
+ $response->code(STATUS_CREATED);
+}
+
+# Get the history for a bug
+sub get_history_response {
+ my ($result) = @_;
+ my $params = Bugzilla->input_params;
+
+ return if !exists $$result->{bugs};
+ my $history = $$result->{bugs}->[0]->{history};
+
+ my @new_history;
+ foreach my $changeset (@$history) {
+ $changeset = fix_changeset($changeset);
+ push(@new_history, filter($params, $changeset));
+ }
+
+ $$result = { history => \@new_history };
+}
+
+# Get all attachments for a bug
+sub get_attachments_response {
+ my ($result) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ my $params = Bugzilla->input_params;
+
+ return if !exists $$result->{bugs};
+ my $bug_id = $rpc->bz_rest_params->{ids}->[0];
+ my $bug = Bugzilla::Bug->check($bug_id);
+ my $attachment_objs = $bug->attachments;
+
+ my $attachments_data = $$result->{bugs}->{$bug_id};
+
+ my @fixed_attachments;
+ foreach my $attachment (@$attachments_data) {
+ my $attachment_obj = shift @$attachment_objs;
+ my $fixed = fix_attachment($attachment, $attachment_obj);
+
+ if ((filter_wants_nocache($params, 'data', 'extra')
+ || filter_wants_nocache($params, 'encoding', 'extra')
+ || $params->{attachmentdata}))
+ {
+ if (!$fixed->{data}) {
+ $fixed->{data} = $rpc->type('base64', $attachment_obj->data);
+ $fixed->{encoding} = $rpc->type('string', 'base64');
+ }
+ }
+ else {
+ delete $fixed->{data};
+ delete $fixed->{encoding};
+ }
+
+ push(@fixed_attachments, filter($params, $fixed));
+ }
+
+ $$result = { attachments => \@fixed_attachments };
+}
+
+# Format the return response on successful attachment creation
+sub add_attachment_response {
+ my ($result, $response) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+
+ my ($attach_id) = keys %{ $$result->{attachments} };
+
+ $$result = { ref => $rpc->type('string', ref_urlbase() . "/attachment/$attach_id"), id => $attach_id };
+ $response->code(STATUS_CREATED);
+}
+
+# Update an attachment's metadata
+sub update_attachment_response {
+ my ($result) = @_;
+ $$result = { ok => 1 };
+}
+
+# Get a single attachment by attachment_id
+sub get_attachment_response {
+ my ($result) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ my $params = Bugzilla->input_params;
+
+ return if !exists $$result->{attachments};
+ my $attach_id = $rpc->bz_rest_params->{attachment_ids}->[0];
+ my $attachment_data = $$result->{attachments}->{$attach_id};
+ my $attachment_obj = Bugzilla::Attachment->new($attach_id);
+ my $fixed = fix_attachment($attachment_data, $attachment_obj);
+
+ if ((filter_wants_nocache($params, 'data', 'extra')
+ || filter_wants_nocache($params, 'encoding', 'extra')
+ || $params->{attachmentdata}))
+ {
+ if (!$fixed->{data}) {
+ $fixed->{data} = $rpc->type('base64', $attachment_obj->data);
+ $fixed->{encoding} = $rpc->type('string', 'base64');
+ }
+ }
+ else {
+ delete $fixed->{data};
+ delete $fixed->{encoding};
+ }
+
+ $fixed = filter($params, $fixed);
+
+ $$result = $fixed;
+}
+
+# Get a list of flags for a bug
+sub get_bug_flags_response {
+ my ($result) = @_;
+ my $params = Bugzilla->input_params;
+
+ return if !exists $$result->{bugs};
+ my $flags = $$result->{bugs}->[0]->{flags};
+
+ my @new_flags;
+ foreach my $flag (@$flags) {
+ push(@new_flags, fix_flag($flag));
+ }
+
+ $$result = { flags => \@new_flags };
+}
+
+1;
diff --git a/extensions/BzAPI/lib/Resources/Bugzilla.pm b/extensions/BzAPI/lib/Resources/Bugzilla.pm
new file mode 100644
index 000000000..96b07297e
--- /dev/null
+++ b/extensions/BzAPI/lib/Resources/Bugzilla.pm
@@ -0,0 +1,138 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BzAPI::Resources::Bugzilla;
+
+use 5.10.1;
+use strict;
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Keyword;
+use Bugzilla::Product;
+use Bugzilla::Status;
+use Bugzilla::Field;
+use Bugzilla::Util qw(correct_urlbase);
+
+use Bugzilla::Extension::BzAPI::Constants;
+
+use Digest::MD5 qw(md5_base64);
+
+#########################
+# REST Resource Methods #
+#########################
+
+BEGIN {
+ require Bugzilla::WebService::Bugzilla;
+ *Bugzilla::WebService::Bugzilla::get_configuration = \&get_configuration;
+ *Bugzilla::WebService::Bugzilla::get_empty = \&get_empty;
+}
+
+sub rest_handlers {
+ my $rest_handlers = [
+ qr{^/$}, {
+ GET => {
+ resource => {
+ method => 'get_empty'
+ }
+ }
+ },
+ qr{^/configuration$}, {
+ GET => {
+ resource => {
+ method => 'get_configuration'
+ }
+ }
+ }
+ ];
+ return $rest_handlers;
+}
+
+sub get_configuration {
+ my ($self) = @_;
+ my $user = Bugzilla->user;
+ my $params = Bugzilla->input_params;
+
+ # Get data from the shadow DB as they don't change very often.
+ Bugzilla->switch_to_shadow_db;
+
+ # Pass a bunch of Bugzilla configuration to the templates.
+ my $vars = {};
+ $vars->{'priority'} = get_legal_field_values('priority');
+ $vars->{'severity'} = get_legal_field_values('bug_severity');
+ $vars->{'platform'} = get_legal_field_values('rep_platform');
+ $vars->{'op_sys'} = get_legal_field_values('op_sys');
+ $vars->{'keyword'} = [ map($_->name, Bugzilla::Keyword->get_all) ];
+ $vars->{'resolution'} = get_legal_field_values('resolution');
+ $vars->{'status'} = get_legal_field_values('bug_status');
+ $vars->{'custom_fields'} =
+ [ grep {$_->is_select} Bugzilla->active_custom_fields ];
+
+ # Include a list of product objects.
+ if ($params->{'product'}) {
+ my @products = $params->{'product'};
+ foreach my $product_name (@products) {
+ my $product = new Bugzilla::Product({ name => $product_name });
+ if ($product && $user->can_see_product($product->name)) {
+ push (@{$vars->{'products'}}, $product);
+ }
+ }
+ } else {
+ $vars->{'products'} = $user->get_selectable_products;
+ }
+
+ # We set the 2nd argument to 1 to also preload flag types.
+ Bugzilla::Product::preload($vars->{'products'}, 1, { is_active => 1 });
+
+ # Allow consumers to specify whether or not they want flag data.
+ if (defined $params->{'flags'}) {
+ $vars->{'show_flags'} = $params->{'flags'};
+ }
+ else {
+ # We default to sending flag data.
+ $vars->{'show_flags'} = 1;
+ }
+
+ # Create separate lists of open versus resolved statuses. This should really
+ # be made part of the configuration.
+ my @open_status;
+ my @closed_status;
+ foreach my $status (@{$vars->{'status'}}) {
+ is_open_state($status) ? push(@open_status, $status)
+ : push(@closed_status, $status);
+ }
+ $vars->{'open_status'} = \@open_status;
+ $vars->{'closed_status'} = \@closed_status;
+
+ # Generate a list of fields that can be queried.
+ my @fields = @{Bugzilla::Field->match({obsolete => 0})};
+ # Exclude fields the user cannot query.
+ if (!Bugzilla->user->is_timetracker) {
+ @fields = grep { $_->name !~ /^(estimated_time|remaining_time|work_time|percentage_complete|deadline)$/ } @fields;
+ }
+ $vars->{'field'} = \@fields;
+
+ my $json;
+ Bugzilla->template->process('config.json.tmpl', $vars, \$json);
+ my $result = {};
+ if ($json) {
+ $result = $self->json->decode($json);
+ }
+ return $result;
+}
+
+sub get_empty {
+ my ($self) = @_;
+ return {
+ ref => $self->type('string', correct_urlbase() . "bzapi/"),
+ documentation => $self->type('string', BZAPI_DOC),
+ version => $self->type('string', BUGZILLA_VERSION)
+ };
+}
+
+1;
diff --git a/extensions/BzAPI/lib/Resources/User.pm b/extensions/BzAPI/lib/Resources/User.pm
new file mode 100644
index 000000000..7fbcdb871
--- /dev/null
+++ b/extensions/BzAPI/lib/Resources/User.pm
@@ -0,0 +1,79 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BzAPI::Resources::User;
+
+use 5.10.1;
+use strict;
+
+use Bugzilla::Extension::BzAPI::Util;
+
+sub rest_handlers {
+ my $rest_handlers = [
+ qr{/user$}, {
+ GET => {
+ response => \&get_users,
+ },
+ },
+ qr{/user/([^/]+)$}, {
+ GET => {
+ response => \&get_user,
+ },
+ }
+ ];
+ return $rest_handlers;
+}
+
+sub get_users {
+ my ($result) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ my $params = Bugzilla->input_params;
+
+ return if !exists $$result->{users};
+
+ my @users;
+ foreach my $user (@{$$result->{users}}) {
+ my $object = Bugzilla::User->new(
+ { id => $user->{id}, cache => 1 });
+
+ $user = fix_user($user, $object);
+
+ # Use userid instead of email for 'ref' for /user calls
+ $user->{'ref'} = $rpc->type('string', ref_urlbase . "/user/" . $object->id);
+
+ # Emails are not filtered even if user is not logged in
+ $user->{name} = $rpc->type('string', $object->login);
+
+ push(@users, filter($params, $user));
+ }
+
+ $$result->{users} = \@users;
+}
+
+sub get_user {
+ my ($result) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ my $params = Bugzilla->input_params;
+
+ return if !exists $$result->{users};
+ my $user = $$result->{users}->[0] || return;
+ my $object = Bugzilla::User->new({ id => $user->{id}, cache => 1 });
+
+ $user = fix_user($user, $object);
+
+ # Use userid instead of email for 'ref' for /user calls
+ $user->{'ref'} = $rpc->type('string', ref_urlbase . "/user/" . $object->id);
+
+ # Emails are not filtered even if user is not logged in
+ $user->{name} = $rpc->type('string', $object->login);
+
+ $user = filter($params, $user);
+
+ $$result = $user;
+}
+
+1;
diff --git a/extensions/BzAPI/lib/Util.pm b/extensions/BzAPI/lib/Util.pm
new file mode 100644
index 000000000..e783a1584
--- /dev/null
+++ b/extensions/BzAPI/lib/Util.pm
@@ -0,0 +1,459 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BzAPI::Util;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Bug;
+use Bugzilla::Constants;
+use Bugzilla::Extension::BzAPI::Constants;
+use Bugzilla::Token;
+use Bugzilla::Util qw(correct_urlbase email_filter);
+use Bugzilla::WebService::Util qw(filter_wants);
+
+use MIME::Base64;
+
+use base qw(Exporter);
+our @EXPORT = qw(
+ ref_urlbase
+ fix_bug
+ fix_user
+ fix_flag
+ fix_comment
+ fix_changeset
+ fix_attachment
+ fix_group
+ filter_wants_nocache
+ filter
+ fix_credentials
+ filter_email
+);
+
+# Return an URL base appropriate for constructing a ref link
+# normally required by REST API calls.
+sub ref_urlbase {
+ return correct_urlbase() . "bzapi";
+}
+
+# convert certain fields within a bug object
+# from a simple scalar value to their respective objects
+sub fix_bug {
+ my ($data, $bug) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $params = Bugzilla->input_params;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ my $method = Bugzilla->request_cache->{bzapi_rpc_method};
+
+ $bug = ref $bug ? $bug : Bugzilla::Bug->check($bug || $data->{id});
+
+ # Add REST API reference to the individual bug
+ if (filter_wants_nocache($params, 'ref')) {
+ $data->{'ref'} = ref_urlbase() . "/bug/" . $bug->id;
+ }
+
+ # User fields
+ foreach my $field (USER_FIELDS) {
+ next if !exists $data->{$field};
+ if ($field eq 'cc') {
+ my @new_cc;
+ foreach my $cc (@{ $bug->cc_users }) {
+ my $cc_data = { name => filter_email($cc->email) };
+ push(@new_cc, fix_user($cc_data, $cc));
+ }
+ $data->{$field} = \@new_cc;
+ }
+ else {
+ my $field_name = $field;
+ if ($field eq 'creator') {
+ $field_name = 'reporter';
+ }
+ $data->{$field}
+ = fix_user($data->{"${field}_detail"}, $bug->$field_name);
+ delete $data->{$field}->{id};
+ delete $data->{$field}->{email};
+ $data->{$field} = filter($params, $data->{$field}, undef, $field);
+ }
+
+ # Get rid of extra detail hash if exists since redundant
+ delete $data->{"${field}_detail"} if exists $data->{"${field}_detail"};
+ }
+
+ # Groups
+ if (filter_wants_nocache($params, 'groups')) {
+ my @new_groups;
+ foreach my $group (@{ $data->{groups} }) {
+ push(@new_groups, fix_group($group));
+ }
+ $data->{groups} = \@new_groups;
+ }
+
+ # Flags
+ if (exists $data->{flags}) {
+ my @new_flags;
+ foreach my $flag (@{ $data->{flags} }) {
+ push(@new_flags, fix_flag($flag));
+ }
+ $data->{flags} = \@new_flags;
+ }
+
+ # Attachment metadata is included by default but not data
+ if (filter_wants_nocache($params, 'attachments')) {
+ my $attachment_params = { ids => $bug->id };
+ if (!filter_wants_nocache($params, 'data', 'extra', 'attachments')
+ && !$params->{attachmentdata})
+ {
+ $attachment_params->{exclude_fields} = ['data'];
+ }
+
+ my $attachments = $rpc->attachments($attachment_params);
+
+ my @fixed_attachments;
+ foreach my $attachment (@{ $attachments->{bugs}->{$bug->id} }) {
+ my $fixed = fix_attachment($attachment);
+ push(@fixed_attachments, filter($params, $fixed, undef, 'attachments'));
+ }
+
+ $data->{attachments} = \@fixed_attachments;
+ }
+
+ # Comments and history are not part of _default and have to be requested
+
+ # Comments
+ if (filter_wants_nocache($params, 'comments', 'extra', 'comments')) {
+ my $comments = $rpc->comments({ ids => $bug->id });
+ $comments = $comments->{bugs}->{$bug->id}->{comments};
+ my @new_comments;
+ foreach my $comment (@$comments) {
+ $comment = fix_comment($comment);
+ push(@new_comments, filter($params, $comment, 'extra', 'comments'));
+ }
+ $data->{comments} = \@new_comments;
+ }
+
+ # History
+ if (filter_wants_nocache($params, 'history', 'extra', 'history')) {
+ my $history = $rpc->history({ ids => [ $bug->id ] });
+ my @new_history;
+ foreach my $changeset (@{ $history->{bugs}->[0]->{history} }) {
+ push(@new_history, fix_changeset($changeset, $bug));
+ }
+ $data->{history} = \@new_history;
+ }
+
+ # Add in all custom fields even if not set or visible on this bug
+ my $custom_fields = Bugzilla->fields({ custom => 1, obsolete => 0 });
+ foreach my $field (@$custom_fields) {
+ my $name = $field->name;
+ next if !filter_wants_nocache($params, $name, ['default','custom']);
+ if ($field->type == FIELD_TYPE_BUG_ID) {
+ $data->{$name} = $rpc->type('int', $bug->$name);
+ }
+ elsif ($field->type == FIELD_TYPE_DATETIME
+ || $field->type == FIELD_TYPE_DATE)
+ {
+ $data->{$name} = $rpc->type('dateTime', $bug->$name);
+ }
+ elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
+ # Bug.search, when include_fields=_all, returns array, otherwise return as comma delimited string :(
+ if ($method eq 'Bug.search' && !grep($_ eq '_all', @{ $params->{include_fields} })) {
+ $data->{$name} = $rpc->type('string', join(', ', @{ $bug->$name }));
+ }
+ else {
+ my @values = map { $rpc->type('string', $_) } @{ $bug->$name };
+ $data->{$name} = \@values;
+ }
+ }
+ else {
+ $data->{$name} = $rpc->type('string', $bug->$name);
+ }
+ }
+
+ # Remove empty values in some cases
+ foreach my $key (keys %$data) {
+ # QA Contact is null if single bug or "" if doing search
+ if ($key eq 'qa_contact' && !$data->{$key}->{name}) {
+ if ($method eq 'Bug.search') {
+ $data->{$key}->{name} = $rpc->type('string', '');
+ }
+ next;
+ }
+
+ next if $method eq 'Bug.search' && $key eq 'url'; # Return url even if empty
+ next if $method eq 'Bug.search' && $key eq 'keywords'; # Return keywords even if empty
+ next if $method eq 'Bug.search' && $key eq 'whiteboard'; # Return whiteboard even if empty
+ next if $method eq 'Bug.get' && grep($_ eq $key, TIMETRACKING_FIELDS);
+
+ next if ($method eq 'Bug.search'
+ && $key =~ /^(resolution|cc_count|dupe_count)$/
+ && !grep($_ eq '_all', @{ $params->{include_fields} }));
+
+ if (!ref $data->{$key}) {
+ delete $data->{$key} if !$data->{$key};
+ }
+ else {
+ if (ref $data->{$key} eq 'ARRAY' && !@{$data->{$key}}) {
+ # Return empty string if blocks or depends_on is empty
+ if ($method eq 'Bug.search' && ($key eq 'depends_on' || $key eq 'blocks')) {
+ $data->{$key} = '';
+ }
+ else {
+ delete $data->{$key};
+ }
+ }
+ elsif (ref $data->{$key} eq 'HASH' && !%{$data->{$key}}) {
+ delete $data->{$key};
+ }
+ }
+ }
+
+ return $data;
+}
+
+# convert a user related field from being just login
+# names to user objects
+sub fix_user {
+ my ($data, $object) = @_;
+ my $user = Bugzilla->user;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+
+ return { name => undef } if !$data;
+
+ if (!ref $data) {
+ $data = {
+ name => filter_email($object->login)
+ };
+ $data->{real_name} = $rpc->type('string', $object->name);
+ }
+ else {
+ $data->{name} = filter_email($data->{name});
+ }
+
+ if ($user->id) {
+ $data->{ref} = $rpc->type('string', ref_urlbase . "/user/" . $object->login);
+ }
+
+ return $data;
+}
+
+# convert certain attributes of a comment to objects
+# and also remove other unwanted key/values.
+sub fix_comment {
+ my ($data, $object) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ my $method = Bugzilla->request_cache->{bzapi_rpc_method};
+
+ $object ||= Bugzilla::Comment->new({ id => $data->{id}, cache => 1 });
+
+ if (exists $data->{creator}) {
+ $data->{creator} = fix_user($data->{creator}, $object->author);
+ }
+
+ if ($data->{attachment_id} && $method ne 'Bug.search') {
+ $data->{attachment_ref} = $rpc->type('string', ref_urlbase() .
+ "/attachment/" . $object->extra_data);
+ }
+ else {
+ delete $data->{attachment_id};
+ }
+
+ delete $data->{author};
+ delete $data->{time};
+ delete $data->{raw_text};
+
+ return $data;
+}
+
+# convert certain attributes of a changeset object from
+# scalar values to related objects. Also remove other unwanted
+# key/values.
+sub fix_changeset {
+ my ($data, $object) = @_;
+ my $user = Bugzilla->user;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+
+ if ($data->{who}) {
+ $data->{changer} = {
+ name => $rpc->type('string', $data->{who}),
+ ref => $rpc->type('string', ref_urlbase() . "/user/" . $data->{who})
+ };
+ delete $data->{who};
+ }
+
+ if ($data->{when}) {
+ $data->{change_time} = $rpc->type('dateTime', $data->{when});
+ delete $data->{when};
+ }
+
+ foreach my $change (@{ $data->{changes} }) {
+ $change->{field_name} = 'flag' if $change->{field_name} eq 'flagtypes.name';
+ }
+
+ return $data;
+}
+
+# convert certain attributes of an attachment object from
+# scalar values to related objects. Also add in additional
+# key/values.
+sub fix_attachment {
+ my ($data, $object) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ my $method = Bugzilla->request_cache->{bzapi_rpc_method};
+ my $params = Bugzilla->input_params;
+ my $user = Bugzilla->user;
+
+ $object ||= Bugzilla::Attachment->new({ id => $data->{id}, cache => 1 });
+
+ if (exists $data->{attacher}) {
+ $data->{attacher} = fix_user($data->{attacher}, $object->attacher);
+ if ($method eq 'Bug.search') {
+ delete $data->{attacher}->{real_name};
+ }
+ else {
+ $data->{attacher}->{real_name} = $rpc->type('string', $object->attacher->name);
+ }
+ }
+
+ if (exists $data->{data}) {
+ $data->{encoding} = $rpc->type('string', 'base64');
+ if ($params->{attachmentdata}
+ || filter_wants_nocache($params, 'attachments.data'))
+ {
+ $data->{encoding} = $rpc->type('string', 'base64');
+ }
+ else {
+ delete $data->{data};
+ }
+ }
+
+ if (exists $data->{bug_id}) {
+ $data->{bug_ref} = $rpc->type('string', ref_urlbase() . "/bug/" . $object->bug_id);
+ }
+
+ # Upstream API returns these as integers where bzapi returns as booleans
+ if (exists $data->{is_patch}) {
+ $data->{is_patch} = $rpc->type('boolean', $data->{is_patch});
+ }
+ if (exists $data->{is_obsolete}) {
+ $data->{is_obsolete} = $rpc->type('boolean', $data->{is_obsolete});
+ }
+ if (exists $data->{is_private}) {
+ $data->{is_private} = $rpc->type('boolean', $data->{is_private});
+ }
+
+ if (exists $data->{flags} && @{ $data->{flags} }) {
+ my @new_flags;
+ foreach my $flag (@{ $data->{flags} }) {
+ push(@new_flags, fix_flag($flag));
+ }
+ $data->{flags} = \@new_flags;
+ }
+ else {
+ delete $data->{flags};
+ }
+
+ $data->{ref} = $rpc->type('string', ref_urlbase() . "/attachment/" . $object->id);
+
+ # Add update token if we are getting an attachment outside of Bug.get and user is logged in
+ if ($user->id && ($method eq 'Bug.attachments'|| $method eq 'Bug.search')) {
+ $data->{update_token} = issue_hash_token([ $object->id, $object->modification_time ]);
+ }
+
+ delete $data->{creator};
+ delete $data->{summary};
+
+ return $data;
+}
+
+# convert certain attributes of a flag object from
+# scalar values to related objects. Also remove other unwanted
+# key/values.
+sub fix_flag {
+ my ($data, $object) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+
+ $object ||= Bugzilla::Flag->new({ id => $data->{id}, cache => 1 });
+
+ if (exists $data->{setter}) {
+ $data->{setter} = fix_user($data->{setter}, $object->setter);
+ delete $data->{setter}->{real_name};
+ }
+
+ if (exists $data->{requestee}) {
+ $data->{requestee} = fix_user($data->{requestee}, $object->requestee);
+ delete $data->{requestee}->{real_name};
+ }
+
+ return $data;
+}
+
+# convert certain attributes of a group object from scalar
+# values to related objects
+sub fix_group {
+ my ($group, $object) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+
+ $object ||= Bugzilla::Group->new({ name => $group });
+
+ if ($object) {
+ $group = {
+ id => $rpc->type('int', $object->id),
+ name => $rpc->type('string', $object->name),
+ };
+ }
+
+ return $group;
+}
+
+# Calls Bugzilla::WebService::Util::filter_wants but disables caching
+# as we make several webservice calls in a single REST call and the
+# caching can cause unexpected results.
+sub filter_wants_nocache {
+ my ($params, $field, $types, $prefix) = @_;
+ delete Bugzilla->request_cache->{filter_wants};
+ return filter_wants($params, $field, $types, $prefix);
+}
+
+sub filter {
+ my ($params, $hash, $types, $prefix) = @_;
+ my %newhash = %$hash;
+ foreach my $key (keys %$hash) {
+ delete $newhash{$key} if !filter_wants_nocache($params, $key, $types, $prefix);
+ }
+ return \%newhash;
+}
+
+sub fix_credentials {
+ my ($params) = @_;
+ # Allow user to pass in username=foo&password=bar to be compatible
+ $params->{'Bugzilla_login'} = $params->{'login'} = delete $params->{'username'}
+ if exists $params->{'username'};
+ $params->{'Bugzilla_password'} = $params->{'password'} if exists $params->{'password'};
+
+ # Allow user to pass userid=1&cookie=3iYGuKZdyz for compatibility with BzAPI
+ if (exists $params->{'userid'} && exists $params->{'cookie'}) {
+ my $userid = delete $params->{'userid'};
+ my $cookie = delete $params->{'cookie'};
+ $params->{'Bugzilla_token'} = "${userid}-${cookie}";
+ }
+}
+
+# Filter email addresses by default ignoring the system
+# webservice_email_filter setting
+sub filter_email {
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ return $rpc->type('string', email_filter($_[0]));
+}
+
+1;
diff --git a/extensions/BzAPI/template/en/default/config.json.tmpl b/extensions/BzAPI/template/en/default/config.json.tmpl
new file mode 100644
index 000000000..993b34915
--- /dev/null
+++ b/extensions/BzAPI/template/en/default/config.json.tmpl
@@ -0,0 +1,302 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+[%
+ # Pinched from Bugzilla/API/Model/Utils.pm in BzAPI - need to keep in sync
+OLD2NEW = {
+ 'opendate' => 'creation_time', # query
+ 'creation_ts' => 'creation_time',
+ 'changeddate' => 'last_change_time', # query
+ 'delta_ts' => 'last_change_time',
+ 'bug_id' => 'id',
+ 'rep_platform' => 'platform',
+ 'bug_severity' => 'severity',
+ 'bug_status' => 'status',
+ 'short_desc' => 'summary',
+ 'short_short_desc' => 'summary',
+ 'bug_file_loc' => 'url',
+ 'status_whiteboard' => 'whiteboard',
+ 'reporter' => 'creator',
+ 'reporter_realname' => 'creator_realname',
+ 'cclist_accessible' => 'is_cc_accessible',
+ 'reporter_accessible' => 'is_creator_accessible',
+ 'everconfirmed' => 'is_confirmed',
+ 'dependson' => 'depends_on',
+ 'blocked' => 'blocks',
+ 'attachment' => 'attachments',
+ 'flag' => 'flags',
+ 'flagtypes.name' => 'flag',
+ 'bug_group' => 'group',
+ 'group' => 'groups',
+ 'longdesc' => 'comment',
+ 'bug_file_loc_type' => 'url_type',
+ 'bugidtype' => 'id_mode',
+ 'longdesc_type' => 'comment_type',
+ 'short_desc_type' => 'summary_type',
+ 'status_whiteboard_type' => 'whiteboard_type',
+ 'emailassigned_to1' => 'email1_assigned_to',
+ 'emailassigned_to2' => 'email2_assigned_to',
+ 'emailcc1' => 'email1_cc',
+ 'emailcc2' => 'email2_cc',
+ 'emailqa_contact1' => 'email1_qa_contact',
+ 'emailqa_contact2' => 'email2_qa_contact',
+ 'emailreporter1' => 'email1_creator',
+ 'emailreporter2' => 'email2_creator',
+ 'emaillongdesc1' => 'email1_comment_creator',
+ 'emaillongdesc2' => 'email2_comment_creator',
+ 'emailtype1' => 'email1_type',
+ 'emailtype2' => 'email2_type',
+ 'chfieldfrom' => 'changed_after',
+ 'chfieldto' => 'changed_before',
+ 'chfield' => 'changed_field',
+ 'chfieldvalue' => 'changed_field_to',
+ 'deadlinefrom' => 'deadline_after',
+ 'deadlineto' => 'deadline_before',
+ 'attach_data.thedata' => 'attachment.data',
+ 'longdescs.isprivate' => 'comment.is_private',
+ 'commenter' => 'comment.creator',
+ 'flagtypes.name' => 'flag',
+ 'requestees.login_name' => 'flag.requestee',
+ 'setters.login_name' => 'flag.setter',
+ 'days_elapsed' => 'idle',
+ 'owner_idle_time' => 'assignee_idle',
+ 'dup_id' => 'dupe_of',
+ 'isopened' => 'is_open',
+ 'flag_type' => 'flag_types',
+ 'token' => 'update_token'
+};
+
+OLDATTACH2NEW = {
+ 'submitter' => 'attacher',
+ 'description' => 'description',
+ 'filename' => 'file_name',
+ 'delta_ts' => 'last_change_time',
+ 'isobsolete' => 'is_obsolete',
+ 'ispatch' => 'is_patch',
+ 'isprivate' => 'is_private',
+ 'mimetype' => 'content_type',
+ 'contenttypeentry' => 'content_type',
+ 'date' => 'creation_time',
+ 'attachid' => 'id',
+ 'desc' => 'description',
+ 'flag' => 'flags',
+ 'type' => 'content_type',
+ 'token' => 'update_token'
+};
+
+%]
+
+[%# Add attachment stuff to the main hash - but with right prefix. (This is
+ # the way the code is structured in BzAPI, and changing it makes it harder
+ # to keep the two in sync.)
+ #%]
+[% FOREACH entry IN OLDATTACH2NEW %]
+ [% newkey = 'attachments.' _ entry.key %]
+ [% OLD2NEW.${newkey} = 'attachment.' _ OLDATTACH2NEW.${entry.key} %]
+[% END %]
+
+[% all_visible_flag_types = {} %]
+
+{
+ "version": "[% constants.BUGZILLA_VERSION FILTER json %]",
+ "maintainer": "[% Param('maintainer') FILTER json %]",
+ "announcement": "[% Param('announcehtml') FILTER json %]",
+ "max_attachment_size": [% (Param('maxattachmentsize') * 1000) FILTER json %],
+
+[% IF Param('useclassification') %]
+ [% cl_name_for = {} %]
+ "classification": {
+ [% FOREACH cl IN user.get_selectable_classifications() %]
+ [% cl_name_for.${cl.id} = cl.name %]
+ "[% cl.name FILTER json %]": {
+ "id": [% cl.id FILTER json %],
+ "description": "[% cl.description FILTER json %]",
+ "products": [
+ [% FOREACH product IN user.get_selectable_products(cl.id) %]
+ "[% product.name FILTER json %]"[% ',' UNLESS loop.last() %]
+ [% END %]
+ ]
+ }[% ',' UNLESS loop.last() %]
+ [% END %]
+ },
+[% END %]
+
+ "product": {
+ [% FOREACH product = products %]
+ "[% product.name FILTER json %]": {
+ "id": [% product.id FILTER json %],
+ "description": "[% product.description FILTER json %]",
+ "is_active": [% product.isactive ? "true" : "false" %],
+ "is_permitting_unconfirmed": [% product.allows_unconfirmed ? "true" : "false" %],
+[% IF Param('useclassification') %]
+ "classification": "[% cl_name_for.${product.classification_id} FILTER json %]",
+[% END %]
+ "component": {
+ [% FOREACH component = product.components %]
+ "[% component.name FILTER json %]": {
+ "id": [% component.id FILTER json %],
+[% IF show_flags %]
+ "flag_type": [
+ [% flag_types =
+ component.flag_types(is_active=>1).bug.merge(component.flag_types(is_active=>1).attachment) %]
+ [%-# "first" flag used to get commas right; can't use loop.last() in case
+ # last flag is inactive %]
+ [% first = 1 %]
+ [% FOREACH flag_type = flag_types %]
+ [% all_visible_flag_types.${flag_type.id} = flag_type %]
+ [% ',' UNLESS first %][% flag_type.id FILTER json %][% first = 0 %]
+ [% END %]],
+[% END %]
+ "description": "[% component.description FILTER json %]"
+ } [% ',' UNLESS loop.last() %]
+ [% END %]
+ },
+ "version": [
+ [% FOREACH version = product.versions %]
+ "[% version.name FILTER json %]"[% ',' UNLESS loop.last() %]
+ [% END %]
+ ],
+
+[% IF Param('usetargetmilestone') %]
+ "default_target_milestone": "[% product.defaultmilestone FILTER json %]",
+ "target_milestone": [
+ [% FOREACH milestone = product.milestones %]
+ "[% milestone.name FILTER json %]"[% ',' UNLESS loop.last() %]
+ [% END %]
+ ],
+[% END %]
+
+ "group": [
+ [% FOREACH group = product.groups_valid %]
+ [% group.id FILTER json %][% ',' UNLESS loop.last() %]
+ [% END %]
+ ]
+ }[% ',' UNLESS loop.last() %]
+ [% END %]
+ },
+
+ "group": {
+ [% FOREACH group = product.groups_valid %]
+ "[% group.id FILTER json %]": {
+ "name": "[% group.name FILTER json %]",
+ "description": "[% group.description FILTER json %]",
+ "is_accepting_bugs": [% group.is_bug_group ? 'true' : 'false' %],
+ "is_active": [% group.is_active ? 'true' : 'false' %]
+ }[% ',' UNLESS loop.last() %]
+ [% END %]
+ },
+
+[% IF show_flags %]
+ "flag_type": {
+ [% FOREACH flag_type = all_visible_flag_types.values.sort('name') %]
+ "[%+ flag_type.id FILTER json %]": {
+ "name": "[% flag_type.name FILTER json %]",
+ "description": "[% flag_type.description FILTER json %]",
+ [% IF user.in_group("editcomponents") %]
+ [% IF flag_type.request_group_id %]
+ "request_group": [% flag_type.request_group_id FILTER json %],
+ [% END %]
+ [% IF flag_type.grant_group_id %]
+ "grant_group": [% flag_type.grant_group_id FILTER json %],
+ [% END %]
+ [% END %]
+ "is_for_bugs": [% flag_type.target_type == "bug" ? 'true' : 'false' %],
+ "is_requestable": [% flag_type.is_requestable ? 'true' : 'false' %],
+ "is_specifically_requestable": [% flag_type.is_requesteeble ? 'true' : 'false' %],
+ "is_multiplicable": [% flag_type.is_multiplicable ? 'true' : 'false' %]
+ }[% ',' UNLESS loop.last() %]
+ [% END %]
+ },
+[% END %]
+
+ [% PROCESS "global/field-descs.none.tmpl" %]
+
+ [%# Put custom field value data where below loop expects to find it %]
+ [% FOREACH cf = custom_fields %]
+ [% ${cf.name} = [] %]
+ [% FOREACH value = cf.legal_values %]
+ [% ${cf.name}.push(value.name) %]
+ [% END %]
+ [% END %]
+
+ [%# Built-in fields do not have type IDs. There aren't ID values for all
+ # the types of the built-in fields, but we do what we can, and leave the
+ # rest as "0" (unknown).
+ #%]
+ [% type_id_for = {
+ "id" => 6,
+ "summary" => 1,
+ "classification" => 2,
+ "version" => 2,
+ "url" => 1,
+ "whiteboard" => 1,
+ "keywords" => 3,
+ "component" => 2,
+ "attachment.description" => 1,
+ "attachment.file_name" => 1,
+ "attachment.content_type" => 1,
+ "target_milestone" => 2,
+ "comment" => 4,
+ "alias" => 1,
+ "deadline" => 5,
+ } %]
+
+ "field": {
+ [% FOREACH item = field %]
+ [% newname = OLD2NEW.${item.name} || item.name %]
+ "[% newname FILTER json %]": {
+ "description": "[% (field_descs.${item.name} OR
+ item.description) FILTER json %]",
+ "is_active": [% field.obsolete ? "false" : "true" %],
+ [% blacklist = ["version", "group", "product", "component"] %]
+ [% IF ${newname} AND NOT blacklist.contains(newname) %]
+ "values": [
+ [% FOREACH value = ${newname} %]
+ "[% value FILTER json %]"[% ',' UNLESS loop.last() %]
+ [% END %]
+ ],
+ [% END %]
+ [% paramname = newname.replace("_", "") %] [%# For op_sys... %]
+ [% IF paramname != "query" AND Param('default' _ paramname) %]
+ "default": "[% Param('default' _ paramname) %]",
+ [% END %]
+ [%-# The 'status' hash has a lot of extra stuff %]
+ [% IF newname == "status" %]
+ "open": [
+ [% FOREACH value = open_status %]
+ "[% value FILTER json %]"[% ',' UNLESS loop.last() %]
+ [% END %]
+ ],
+ "closed": [
+ [% FOREACH value = closed_status %]
+ "[% value FILTER json %]"[% ',' UNLESS loop.last() %]
+ [% END %]
+ ],
+ "transitions": {
+ "{Start}": [
+ [% FOREACH target = initial_status %]
+ "[% target.name FILTER json %]"[% ',' UNLESS loop.last() %]
+ [% END %]
+ ],
+ [% FOREACH status = status_objects %]
+ [% targets = status.can_change_to() %]
+ "[% status.name FILTER json %]": [
+ [% FOREACH target = targets %]
+ "[% target.name FILTER json %]"[% ',' UNLESS loop.last() %]
+ [% END %]
+ ][% ',' UNLESS loop.last() %]
+ [% END %]
+ },
+ [% END %]
+ [% IF newname.match("^cf_") %]
+ "is_on_bug_entry": [% item.enter_bug ? 'true' : 'false' %],
+ [% END %]
+ "type": [% item.type || type_id_for.$newname || 0 FILTER json %]
+ }[% ',' UNLESS loop.last() %]
+ [% END %]
+ }
+}
diff --git a/extensions/BzAPI/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/BzAPI/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..1ffe03d22
--- /dev/null
+++ b/extensions/BzAPI/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "bzapi_midair_collision" %]
+ Mid-air collision
+[% END %]
diff --git a/extensions/ComponentWatching/Config.pm b/extensions/ComponentWatching/Config.pm
new file mode 100644
index 000000000..560b5c3c5
--- /dev/null
+++ b/extensions/ComponentWatching/Config.pm
@@ -0,0 +1,12 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ComponentWatching;
+use strict;
+use constant NAME => 'ComponentWatching';
+
+__PACKAGE__->NAME;
diff --git a/extensions/ComponentWatching/Extension.pm b/extensions/ComponentWatching/Extension.pm
new file mode 100644
index 000000000..627fb12fd
--- /dev/null
+++ b/extensions/ComponentWatching/Extension.pm
@@ -0,0 +1,626 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ComponentWatching;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Group;
+use Bugzilla::User;
+use Bugzilla::User::Setting;
+use Bugzilla::Util qw(trim trick_taint);
+
+our $VERSION = '2';
+
+use constant REL_COMPONENT_WATCHER => 15;
+
+#
+# installation
+#
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'component_watch'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE',
+ }
+ },
+ component_id => {
+ TYPE => 'INT2',
+ NOTNULL => 0,
+ REFERENCES => {
+ TABLE => 'components',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ }
+ },
+ product_id => {
+ TYPE => 'INT2',
+ NOTNULL => 0,
+ REFERENCES => {
+ TABLE => 'products',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ }
+ },
+ component_prefix => {
+ TYPE => 'VARCHAR(64)',
+ NOTNULL => 0,
+ },
+ ],
+ };
+}
+
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_add_column(
+ 'components',
+ 'watch_user',
+ {
+ TYPE => 'INT3',
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'SET NULL',
+ }
+ }
+ );
+ $dbh->bz_add_column(
+ 'component_watch',
+ 'id',
+ {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ );
+ $dbh->bz_add_column(
+ 'component_watch',
+ 'component_prefix',
+ {
+ TYPE => 'VARCHAR(64)',
+ NOTNULL => 0,
+ }
+ );
+}
+
+#
+# templates
+#
+
+sub template_before_create {
+ my ($self, $args) = @_;
+ my $config = $args->{config};
+ my $constants = $config->{CONSTANTS};
+ $constants->{REL_COMPONENT_WATCHER} = REL_COMPONENT_WATCHER;
+}
+
+#
+# user-watch
+#
+
+BEGIN {
+ *Bugzilla::Component::watch_user = \&_component_watch_user;
+}
+
+sub _component_watch_user {
+ my ($self) = @_;
+ return unless $self->{watch_user};
+ $self->{watch_user_object} ||= Bugzilla::User->new($self->{watch_user});
+ return $self->{watch_user_object};
+}
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my $class = $args->{class};
+ my $columns = $args->{columns};
+ return unless $class->isa('Bugzilla::Component');
+
+ push(@$columns, 'watch_user');
+}
+
+sub object_update_columns {
+ my ($self, $args) = @_;
+ my $object = $args->{object};
+ my $columns = $args->{columns};
+ return unless $object->isa('Bugzilla::Component');
+
+ push(@$columns, 'watch_user');
+
+ # editcomponents.cgi doesn't call set_all, so we have to do this here
+ my $input = Bugzilla->input_params;
+ $object->set('watch_user', $input->{watch_user});
+}
+
+sub object_validators {
+ my ($self, $args) = @_;
+ my $class = $args->{class};
+ my $validators = $args->{validators};
+ return unless $class->isa('Bugzilla::Component');
+
+ $validators->{watch_user} = \&_check_watch_user;
+}
+
+sub object_before_create {
+ my ($self, $args) = @_;
+ my $class = $args->{class};
+ my $params = $args->{params};
+ return unless $class->isa('Bugzilla::Component');
+
+ my $input = Bugzilla->input_params;
+ $params->{watch_user} = $input->{watch_user};
+}
+
+sub object_end_of_update {
+ my ($self, $args) = @_;
+ my $object = $args->{object};
+ my $old_object = $args->{old_object};
+ my $changes = $args->{changes};
+ return unless $object->isa('Bugzilla::Component');
+
+ my $old_id = $old_object->watch_user ? $old_object->watch_user->id : 0;
+ my $new_id = $object->watch_user ? $object->watch_user->id : 0;
+ return if $old_id == $new_id;
+
+ $changes->{watch_user} = [ $old_id ? $old_id : undef, $new_id ? $new_id : undef ];
+}
+
+sub _check_watch_user {
+ my ($self, $value, $field) = @_;
+ $value = trim($value || '');
+ if ($value eq '') {
+ ThrowUserError('component_watch_missing_watch_user');
+ }
+ if ($value !~ /\.bugs$/i) {
+ ThrowUserError('component_watch_invalid_watch_user');
+ }
+ return Bugzilla::User->check($value)->id;
+}
+
+#
+# preferences
+#
+
+sub user_preferences {
+ my ($self, $args) = @_;
+ my $tab = $args->{'current_tab'};
+ return unless $tab eq 'component_watch';
+
+ my $save = $args->{'save_changes'};
+ my $handled = $args->{'handled'};
+ my $vars = $args->{'vars'};
+ my $user = Bugzilla->user;
+ my $input = Bugzilla->input_params;
+
+ if ($save) {
+ if ($input->{'add'} && $input->{'add_product'}) {
+ # add watch
+
+ # load product and verify access
+ my $productName = $input->{'add_product'};
+ my $product = Bugzilla::Product->new({ name => $productName, cache => 1 });
+ unless ($product && $user->can_access_product($product)) {
+ ThrowUserError('product_access_denied', { product => $productName });
+ }
+
+ # starting-with
+ if (my $prefix = $input->{add_starting}) {
+ _addPrefixWatch($user, $product, $prefix);
+
+ } else {
+ my $ra_componentNames = $input->{'add_component'};
+ $ra_componentNames = [$ra_componentNames || ''] unless ref($ra_componentNames);
+
+ if (grep { $_ eq '' } @$ra_componentNames) {
+ # watching a product
+ _addProductWatch($user, $product);
+
+ } else {
+ # watching specific components
+ foreach my $componentName (@$ra_componentNames) {
+ my $component = Bugzilla::Component->new({
+ name => $componentName, product => $product, cache => 1
+ });
+ unless ($component) {
+ ThrowUserError('product_access_denied', { product => $productName });
+ }
+ _addComponentWatch($user, $component);
+ }
+ }
+ }
+
+ _addDefaultSettings($user);
+
+ } else {
+ # remove watch(s)
+
+ my $delete = ref $input->{del_watch}
+ ? $input->{del_watch}
+ : [ $input->{del_watch} ];
+ foreach my $id (@$delete) {
+ _deleteWatch($user, $id);
+ }
+ }
+
+ }
+
+ $vars->{'add_product'} = $input->{'product'};
+ $vars->{'add_component'} = $input->{'component'};
+ $vars->{'watches'} = _getWatches($user);
+ $vars->{'user_watches'} = _getUserWatches($user);
+
+ $$handled = 1;
+}
+
+#
+# bugmail
+#
+
+sub bugmail_recipients {
+ my ($self, $args) = @_;
+ my $bug = $args->{'bug'};
+ my $recipients = $args->{'recipients'};
+ my $diffs = $args->{'diffs'};
+
+ my ($oldProductId, $newProductId) = ($bug->product_id, $bug->product_id);
+ my ($oldComponentId, $newComponentId) = ($bug->component_id, $bug->component_id);
+
+ # notify when the product/component is switch from one being watched
+ if (@$diffs) {
+ # we need the product to process the component, so scan for that first
+ my $product;
+ foreach my $ra (@$diffs) {
+ next if !(exists $ra->{'old'}
+ && exists $ra->{'field_name'});
+ if ($ra->{'field_name'} eq 'product') {
+ $product = Bugzilla::Product->new({ name => $ra->{'old'}, cache => 1 });
+ $oldProductId = $product->id;
+ }
+ }
+ if (!$product) {
+ $product = Bugzilla::Product->new({ id => $oldProductId, cache => 1 });
+ }
+ foreach my $ra (@$diffs) {
+ next if !(exists $ra->{'old'}
+ && exists $ra->{'field_name'});
+ if ($ra->{'field_name'} eq 'component') {
+ my $component = Bugzilla::Component->new({
+ name => $ra->{'old'}, product => $product, cache => 1
+ });
+ $oldComponentId = $component->id;
+ }
+ }
+ }
+
+ # add component watchers
+ my $dbh = Bugzilla->dbh;
+ my $sth = $dbh->prepare("
+ SELECT user_id
+ FROM component_watch
+ WHERE ((product_id = ? OR product_id = ?) AND component_id IS NULL)
+ OR (component_id = ? OR component_id = ?)
+ UNION
+ SELECT user_id
+ FROM component_watch
+ INNER JOIN components ON components.product_id = component_watch.product_id
+ WHERE component_prefix IS NOT NULL
+ AND (component_watch.product_id = ? OR component_watch.product_id = ?)
+ AND components.name LIKE CONCAT(component_prefix, '%')
+ ");
+ $sth->execute(
+ $oldProductId, $newProductId,
+ $oldComponentId, $newComponentId,
+ $oldProductId, $newProductId
+ );
+ while (my ($uid) = $sth->fetchrow_array) {
+ if (!exists $recipients->{$uid}) {
+ $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_WATCHING();
+ }
+ }
+
+ # add component watchers from watch-users
+ my $uidList = join(',', keys %$recipients);
+ $sth = $dbh->prepare("
+ SELECT component_watch.user_id
+ FROM components
+ INNER JOIN component_watch ON component_watch.component_id = components.id
+ WHERE components.watch_user in ($uidList)
+ ");
+ $sth->execute();
+ while (my ($uid) = $sth->fetchrow_array) {
+ if (!exists $recipients->{$uid}) {
+ $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_WATCHING();
+ }
+ }
+
+ # add watch-users from component watchers
+ $sth = $dbh->prepare("
+ SELECT watch_user
+ FROM components
+ WHERE (id = ? OR id = ?)
+ AND (watch_user IS NOT NULL)
+ ");
+ $sth->execute($oldComponentId, $newComponentId);
+ while (my ($uid) = $sth->fetchrow_array) {
+ if (!exists $recipients->{$uid}) {
+ $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_DIRECT();
+ }
+ }
+}
+
+sub bugmail_relationships {
+ my ($self, $args) = @_;
+ my $relationships = $args->{relationships};
+ $relationships->{+REL_COMPONENT_WATCHER} = 'Component-Watcher';
+}
+
+#
+# db
+#
+
+sub _getWatches {
+ my ($user) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $sth = $dbh->prepare("
+ SELECT id, product_id, component_id, component_prefix
+ FROM component_watch
+ WHERE user_id = ?
+ ");
+ $sth->execute($user->id);
+ my @watches;
+ while (my ($id, $productId, $componentId, $prefix) = $sth->fetchrow_array) {
+ my $product = Bugzilla::Product->new({ id => $productId, cache => 1 });
+ next unless $product && $user->can_access_product($product);
+
+ my %watch = (
+ id => $id,
+ product => $product,
+ product_name => $product->name,
+ component_name => '',
+ component_prefix => $prefix,
+ );
+ if ($componentId) {
+ my $component = Bugzilla::Component->new({ id => $componentId, cache => 1 });
+ next unless $component;
+ $watch{'component'} = $component;
+ $watch{'component_name'} = $component->name;
+ }
+
+ push @watches, \%watch;
+ }
+
+ @watches = sort {
+ $a->{'product_name'} cmp $b->{'product_name'}
+ || $a->{'component_name'} cmp $b->{'component_name'}
+ || $a->{'component_prefix'} cmp $b->{'component_prefix'}
+ } @watches;
+
+ return \@watches;
+}
+
+sub _getUserWatches {
+ my ($user) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $sth = $dbh->prepare("
+ SELECT components.product_id, components.id as component, profiles.login_name
+ FROM watch
+ INNER JOIN components ON components.watch_user = watched
+ INNER JOIN profiles ON profiles.userid = watched
+ WHERE watcher = ?
+ ");
+ $sth->execute($user->id);
+ my @watches;
+ while (my ($productId, $componentId, $login) = $sth->fetchrow_array) {
+ my $product = Bugzilla::Product->new({ id => $productId, cache => 1 });
+ next unless $product && $user->can_access_product($product);
+
+ my %watch = (
+ product => $product,
+ component => Bugzilla::Component->new({ id => $componentId, cache => 1 }),
+ user => Bugzilla::User->check($login),
+ );
+ push @watches, \%watch;
+ }
+
+ @watches = sort {
+ $a->{'product'}->name cmp $b->{'product'}->name
+ || $a->{'component'}->name cmp $b->{'component'}->name
+ } @watches;
+
+ return \@watches;
+}
+
+sub _addProductWatch {
+ my ($user, $product) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $sth = $dbh->prepare("
+ SELECT 1
+ FROM component_watch
+ WHERE user_id = ? AND product_id = ? AND component_id IS NULL
+ ");
+ $sth->execute($user->id, $product->id);
+ return if $sth->fetchrow_array;
+
+ $sth = $dbh->prepare("
+ DELETE FROM component_watch
+ WHERE user_id = ? AND product_id = ?
+ ");
+ $sth->execute($user->id, $product->id);
+
+ $sth = $dbh->prepare("
+ INSERT INTO component_watch(user_id, product_id)
+ VALUES (?, ?)
+ ");
+ $sth->execute($user->id, $product->id);
+}
+
+sub _addComponentWatch {
+ my ($user, $component) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $sth = $dbh->prepare("
+ SELECT 1
+ FROM component_watch
+ WHERE user_id = ?
+ AND (component_id = ? OR (product_id = ? AND component_id IS NULL))
+ ");
+ $sth->execute($user->id, $component->id, $component->product_id);
+ return if $sth->fetchrow_array;
+
+ $sth = $dbh->prepare("
+ INSERT INTO component_watch(user_id, product_id, component_id)
+ VALUES (?, ?, ?)
+ ");
+ $sth->execute($user->id, $component->product_id, $component->id);
+}
+
+sub _addPrefixWatch {
+ my ($user, $product, $prefix) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ trick_taint($prefix);
+ my $sth = $dbh->prepare("
+ SELECT 1
+ FROM component_watch
+ WHERE user_id = ?
+ AND (
+ (product_id = ? AND component_prefix = ?)
+ OR (product_id = ? AND component_id IS NULL)
+ )
+ ");
+ $sth->execute(
+ $user->id,
+ $product->id, $prefix,
+ $product->id
+ );
+ return if $sth->fetchrow_array;
+
+ $sth = $dbh->prepare("
+ INSERT INTO component_watch(user_id, product_id, component_prefix)
+ VALUES (?, ?, ?)
+ ");
+ $sth->execute($user->id, $product->id, $prefix);
+}
+
+sub _deleteWatch {
+ my ($user, $id) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ trick_taint($id);
+ $dbh->do("DELETE FROM component_watch WHERE id=?", undef, $id);
+}
+
+sub _addDefaultSettings {
+ my ($user) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $sth = $dbh->prepare("
+ SELECT 1
+ FROM email_setting
+ WHERE user_id = ? AND relationship = ?
+ ");
+ $sth->execute($user->id, REL_COMPONENT_WATCHER);
+ return if $sth->fetchrow_array;
+
+ my @defaultEvents = (
+ EVT_OTHER,
+ EVT_COMMENT,
+ EVT_ATTACHMENT,
+ EVT_ATTACHMENT_DATA,
+ EVT_PROJ_MANAGEMENT,
+ EVT_OPENED_CLOSED,
+ EVT_KEYWORD,
+ EVT_DEPEND_BLOCK,
+ EVT_BUG_CREATED,
+ );
+ foreach my $event (@defaultEvents) {
+ $dbh->do(
+ "INSERT INTO email_setting(user_id,relationship,event) VALUES (?,?,?)",
+ undef,
+ $user->id, REL_COMPONENT_WATCHER, $event
+ );
+ }
+}
+
+sub reorg_move_component {
+ my ($self, $args) = @_;
+ my $new_product = $args->{new_product};
+ my $component = $args->{component};
+
+ Bugzilla->dbh->do(
+ "UPDATE component_watch SET product_id=? WHERE component_id=?",
+ undef,
+ $new_product->id, $component->id,
+ );
+}
+
+sub sanitycheck_check {
+ my ($self, $args) = @_;
+ my $status = $args->{status};
+
+ $status->('component_watching_check');
+
+ my ($count) = Bugzilla->dbh->selectrow_array("
+ SELECT COUNT(*)
+ FROM component_watch
+ INNER JOIN components ON components.id = component_watch.component_id
+ WHERE component_watch.product_id <> components.product_id
+ ");
+ if ($count) {
+ $status->('component_watching_alert', undef, 'alert');
+ $status->('component_watching_repair');
+ }
+}
+
+sub sanitycheck_repair {
+ my ($self, $args) = @_;
+ return unless Bugzilla->cgi->param('component_watching_repair');
+
+ my $status = $args->{'status'};
+ my $dbh = Bugzilla->dbh;
+ $status->('component_watching_repairing');
+
+ my $rows = $dbh->selectall_arrayref("
+ SELECT DISTINCT component_watch.product_id AS bad_product_id,
+ components.product_id AS good_product_id,
+ component_watch.component_id
+ FROM component_watch
+ INNER JOIN components ON components.id = component_watch.component_id
+ WHERE component_watch.product_id <> components.product_id
+ ",
+ { Slice => {} }
+ );
+ foreach my $row (@$rows) {
+ $dbh->do("
+ UPDATE component_watch
+ SET product_id=?
+ WHERE product_id=? AND component_id=?
+ ", undef,
+ $row->{good_product_id},
+ $row->{bad_product_id},
+ $row->{component_id},
+ );
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/ComponentWatching/template/en/default/account/prefs/component_watch.html.tmpl b/extensions/ComponentWatching/template/en/default/account/prefs/component_watch.html.tmpl
new file mode 100644
index 000000000..5e27c1247
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/account/prefs/component_watch.html.tmpl
@@ -0,0 +1,261 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%# initialise product to component mapping #%]
+
+[% SET selectable_products = user.get_selectable_products %]
+[% SET dont_show_button = 1 %]
+
+<style>
+#add_compwatch input[type=text],
+#add_compwatch select
+{
+ width: 30em;
+}
+
+#component[disabled] {
+ color: silver;
+}
+</style>
+
+<script>
+var Dom = YAHOO.util.Dom;
+var useclassification = false;
+var first_load = true;
+var last_sel = [];
+var cpts = new Array();
+var watch_users = new Array();
+[% n = 0 %]
+[% FOREACH prod = selectable_products %]
+ cpts['[% n %]'] = [
+ [%- FOREACH comp = prod.components %]'[% comp.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
+ [% n = n + 1 %]
+ [% FOREACH comp = prod.components %]
+ [% IF comp.watch_user %]
+ if (!watch_users['[% prod.name FILTER js %]'])
+ watch_users['[% prod.name FILTER js %]'] = new Array();
+ watch_users['[% prod.name FILTER js %]']['[% comp.name FILTER js %]'] = '[% comp.watch_user.login FILTER js %]';
+ [% END %]
+ [% END %]
+[% END %]
+</script>
+<script type="text/javascript" src="[% 'js/productform.js' FILTER mtime FILTER html %]">
+</script>
+
+<script>
+function onSelectProduct() {
+ var component = Dom.get('component');
+ selectProduct(Dom.get('product'), component);
+ // selectProduct only supports __Any__ on both elements
+ // we only want it on component, so add it back in
+ try {
+ component.add(new Option('__Any__', ''), component.options[0]);
+ } catch(e) {
+ // support IE
+ component.add(new Option('__Any__', ''), 0);
+ }
+ if ('[% add_component FILTER js %]' != ''
+ && bz_valueSelected(Dom.get('product'), '[% add_product FILTER js %]')
+ ) {
+ var index = bz_optionIndex(Dom.get('component'), '[% add_component FILTER js %]');
+ if (index != -1)
+ Dom.get('component').options[index].selected = true;
+ }
+ onSelectComponent();
+}
+
+function onSelectComponent() {
+ var product_select = Dom.get('product');
+ var product = product_select.options[product_select.selectedIndex].value;
+ var component = Dom.get('component').value;
+ if (component && watch_users[product] && watch_users[product][component]) {
+ Dom.get('watch-user-email').innerHTML = watch_users[product][component];
+ Dom.get('watch-user-div').style.display = '';
+ } else {
+ Dom.get('watch-user-div').style.display = 'none';
+ }
+ Dom.get('add').disabled = Dom.get('component').selectedIndex == -1;
+}
+
+function onStartingWith(el) {
+ var value = el.value.replace(/(^\s*|\s*$)/g, '');
+ if (value == '') {
+ Dom.get('component').disabled = false;
+ onSelectProduct();
+ } else {
+ Dom.get('component').selectedIndex = -1;
+ Dom.get('watch-user-div').style.display = 'none';
+ Dom.get('component').disabled = true;
+ Dom.get('add').disabled = false;
+ }
+}
+
+YAHOO.util.Event.onDOMReady(onSelectProduct);
+
+function onRemoveChange() {
+ var cbs = Dom.get('remove_table').getElementsByTagName('input');
+ for (var i = 0, l = cbs.length; i < l; i++) {
+ if (cbs[i].checked) {
+ Dom.get('remove').disabled = false;
+ return;
+ }
+ }
+ Dom.get('remove').disabled = true;
+}
+
+YAHOO.util.Event.onDOMReady(onRemoveChange);
+
+</script>
+
+<p>
+ Select the components you want to watch.<br>
+ To watch all components in a product, watch "__Any__".<br>
+ Watching components starting with "Developer Tools" is the same as watching
+ "Developer Tools", "Developer Tools: Console", "Developer Tools: Debugger",
+ etc. If a new component is added which starts with "Developer Tools", you'll
+ automatically start watching that too.
+ <br>
+ Use <a href="userprefs.cgi?tab=email">Email Preferences</a> to filter which
+ notification emails you receive.
+</p>
+
+<table border="0" cellpadding="3" cellspacing="0" id="add_compwatch">
+<tr>
+ <th align="right">Product:</th>
+ <td colspan="2">
+ <select name="add_product" id="product" onChange="onSelectProduct()">
+ [% FOREACH product IN selectable_products %]
+ <option [% 'selected' IF add_product == product.name %]>
+ [%~ product.name FILTER html %]</option>
+ [% END %]
+ </select>
+ </td>
+</tr>
+<tr>
+ <th align="right" valign="top">Component:</th>
+ <td>Select the component(s) to add to your watch list:</td>
+</tr>
+<tr>
+ <td></td>
+ <td>
+ <select name="add_component" id="component" multiple size="5" onChange="onSelectComponent()">
+ <option value="">__Any__</option>
+ [% FOREACH product IN selectable_products %]
+ [% FOREACH component IN product.components %]
+ <option [% 'selected' IF add_component == component.name %]>
+ [%~ component.name FILTER html %]</option>
+ [% END %]
+ [% END %]
+ </select><br>
+ Or watch components starting with:<br>
+ <input type="text" name="add_starting" id="add_starting" maxlength="64"
+ onKeyUp="onStartingWith(this)" onBlur="onStartingWith(this)">
+ </td>
+ <td valign="top">
+ <div id="watch-user-div"
+ title="You can also watch a component by following this user. [% ~%]
+ CC'ing this user on a [% terms.bug %] will trigger notifications to all watchers of this component."
+ style="cursor:help">
+ Watch User: <span id="watch-user-email"></span>
+ </div>
+ </td>
+</tr>
+<tr>
+ <td>&nbsp;</td>
+ <td><input type="submit" id="add" name="add" value="Add"></td>
+</tr>
+</table>
+
+<hr>
+<p>
+ You are currently watching:
+</p>
+
+[% IF watches.size %]
+
+ <table border="0" cellpadding="3" cellspacing="0" id="remove_table">
+ <tr>
+ <td>&nbsp;</td>
+ <td><b>Product</b></td>
+ <td>&nbsp;<b>Component</b></td>
+ </tr>
+ [% FOREACH watch IN watches %]
+ <tr>
+ <td>
+ <input type="checkbox" onChange="onRemoveChange()"
+ id="cwdel_[% watch.id FILTER none %]"
+ name="del_watch" value="[% watch.id FILTER none %]">
+ </td>
+ <td>
+ <label for="cwdel_[% watch.id FILTER none %]">
+ [% watch.product.name FILTER html %]
+ </label>
+ </td>
+ <td>&nbsp;
+ [% IF (watch.component) %]
+ <a href="buglist.cgi?product=[% watch.product.name FILTER uri ~%]
+ &component=[% watch.component.name FILTER uri %]&resolution=---">
+ [% watch.component.name FILTER html %]
+ </a>
+ [% ELSIF watch.component_prefix %]
+ <i>starts with:</i> [% watch.component_prefix FILTER html %]
+ [% ELSE %]
+ <a href="describecomponents.cgi?product=[% watch.product.name FILTER uri %]">
+ __Any__
+ </a>
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ </table>
+
+ <input id="remove" type="submit" value="Remove Selected">
+
+[% ELSE %]
+
+ <p>
+ <i>You are not watching any components directly.</i>
+ </p>
+
+[% END %]
+
+[% IF user_watches.size %]
+
+ <hr>
+ <p>
+ [% watches.size ? "In addition," : "However," %]
+ you are watching the following components by watching users:
+ </p>
+
+ <table border="0" cellpadding="3" cellspacing="0">
+ <tr>
+ <td><b>User</b></td>
+ <td>&nbsp;<b>Product</b></td>
+ <td>&nbsp;<b>Component</b></td>
+ </tr>
+ [% FOREACH watch IN user_watches %]
+ <tr>
+ <td>[% watch.user.login FILTER html %]</td>
+ <td>&nbsp;[% watch.component.product.name FILTER html %]</td>
+ <td>&nbsp;
+ <a href="buglist.cgi?product=[% watch.product.name FILTER uri ~%]
+ &component=[% watch.component.name FILTER uri %]&resolution=---">
+ [% watch.component.name FILTER html %]
+ </a>
+ </td>
+ </tr>
+ [% END %]
+ </table>
+
+ <p>
+ Use <a href="userprefs.cgi?tab=email#new_watched_by_you">Email Preferences</a>
+ to manage this list.
+ </p>
+
+[% END %]
+
diff --git a/extensions/ComponentWatching/template/en/default/hook/account/prefs/email-relationships.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/account/prefs/email-relationships.html.tmpl
new file mode 100644
index 000000000..69ab53751
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/account/prefs/email-relationships.html.tmpl
@@ -0,0 +1,10 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% relationships.push({ id = constants.REL_COMPONENT_WATCHER, description = "Component" }) %]
+[% no_added_removed.push(constants.REL_COMPONENT_WATCHER) %]
diff --git a/extensions/ComponentWatching/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
new file mode 100644
index 000000000..9af22ed39
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
@@ -0,0 +1,14 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% tabs = tabs.import([{
+ name => "component_watch",
+ label => "Component Watching",
+ link => "userprefs.cgi?tab=component_watch",
+ saveable => 1
+ }]) %]
diff --git a/extensions/ComponentWatching/template/en/default/hook/admin/components/edit-common-rows.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/admin/components/edit-common-rows.html.tmpl
new file mode 100644
index 000000000..4f92097ff
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/admin/components/edit-common-rows.html.tmpl
@@ -0,0 +1,20 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<tr>
+ <th class="field_label"><label for="watch_user">Watch User:</label></th>
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ name => "watch_user"
+ id => "watch_user"
+ value => comp.watch_user.login
+ size => 64
+ emptyok => 1
+ %]
+ </td>
+</tr>
diff --git a/extensions/ComponentWatching/template/en/default/hook/admin/components/list-before_table.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/admin/components/list-before_table.html.tmpl
new file mode 100644
index 000000000..ed8d6e350
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/admin/components/list-before_table.html.tmpl
@@ -0,0 +1,17 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% CALL columns.splice(5, 0, { name => 'watch_user', heading => 'Watch User' }) %]
+
+[% FOREACH my_component = product.components %]
+ [% overrides.watch_user.name.${my_component.name} = {
+ override_content => 1
+ content => my_component.watch_user.login
+ }
+ %]
+[% END %]
diff --git a/extensions/ComponentWatching/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl
new file mode 100644
index 000000000..c22ffacaa
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl
@@ -0,0 +1,23 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF san_tag == "component_watching_repair" %]
+ <a href="sanitycheck.cgi?component_watching_repair=1&amp;token=
+ [%- issue_hash_token(['sanitycheck']) FILTER uri %]"
+ >Repair invalid product_id values in the component_watch table</a>
+
+[% ELSIF san_tag == "component_watching_check" %]
+ Checking component_watch table for bad values of product_id.
+
+[% ELSIF san_tag == "component_watching_alert" %]
+ Bad values for product_id found in the component_watch table.
+
+[% ELSIF san_tag == "component_watching_repairing" %]
+ OK, now fixing bad product_id values in the component_watch table.
+
+[% END %]
diff --git a/extensions/ComponentWatching/template/en/default/hook/global/messages-component_updated_fields.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/global/messages-component_updated_fields.html.tmpl
new file mode 100644
index 000000000..38c7e8c8a
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/global/messages-component_updated_fields.html.tmpl
@@ -0,0 +1,15 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF changes.watch_user.defined %]
+ [% IF comp.watch_user %]
+ <li>Watch User updated to '[% comp.watch_user.login FILTER html %]'</li>
+ [% ELSE %]
+ <li>Watch User deleted</li>
+ [% END %]
+[% END %]
diff --git a/extensions/ComponentWatching/template/en/default/hook/global/reason-descs-end.none.tmpl b/extensions/ComponentWatching/template/en/default/hook/global/reason-descs-end.none.tmpl
new file mode 100644
index 000000000..8cd67bdff
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/global/reason-descs-end.none.tmpl
@@ -0,0 +1,10 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% watch_reason_descs.${constants.REL_COMPONENT_WATCHER} =
+ "You are watching the component for the ${terms.bug}." %]
diff --git a/extensions/ComponentWatching/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..01dbb5114
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,17 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "component_watch_invalid_watch_user" %]
+ [% title = "Invalid Watch User" %]
+ The "Watch User" must be a <b>.bugs</b> email address.<br>
+ For example: <i>accessibility-apis@core.bugs</i>
+[% ELSIF error == "component_watch_missing_watch_user" %]
+ [% title = "Missing Watch User" %]
+ You must provide a <b>.bugs</b> email address for the "Watch User".<br>
+ For example: <i>accessibility-apis@core.bugs</i>
+[% END %]
diff --git a/extensions/ContributorEngagement/Config.pm b/extensions/ContributorEngagement/Config.pm
new file mode 100644
index 000000000..3984dd60e
--- /dev/null
+++ b/extensions/ContributorEngagement/Config.pm
@@ -0,0 +1,19 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ContributorEngagement;
+use strict;
+
+use constant NAME => 'ContributorEngagement';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/ContributorEngagement/Extension.pm b/extensions/ContributorEngagement/Extension.pm
new file mode 100644
index 000000000..def41b6ea
--- /dev/null
+++ b/extensions/ContributorEngagement/Extension.pm
@@ -0,0 +1,123 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ContributorEngagement;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::User;
+use Bugzilla::Util qw(format_time);
+use Bugzilla::Mailer;
+use Bugzilla::Install::Util qw(indicate_progress);
+
+use Bugzilla::Extension::ContributorEngagement::Constants;
+
+our $VERSION = '2.0';
+
+BEGIN {
+ *Bugzilla::User::first_patch_reviewed_id = \&_first_patch_reviewed_id;
+}
+
+sub _first_patch_reviewed_id { return $_[0]->{'first_patch_reviewed_id'}; }
+
+sub install_update_db {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ if ($dbh->bz_column_info('profiles', 'first_patch_approved_id')) {
+ $dbh->bz_drop_column('profiles', 'first_patch_approved_id');
+ }
+ if (!$dbh->bz_column_info('profiles', 'first_patch_reviewed_id')) {
+ $dbh->bz_add_column('profiles', 'first_patch_reviewed_id', { TYPE => 'INT3' });
+ _populate_first_reviewed_ids();
+ }
+}
+
+sub _populate_first_reviewed_ids {
+ my $dbh = Bugzilla->dbh;
+
+ my $sth = $dbh->prepare('UPDATE profiles SET first_patch_reviewed_id = ? WHERE userid = ?');
+ my $ra = $dbh->selectall_arrayref("SELECT attachments.submitter_id,
+ attachments.attach_id
+ FROM attachments
+ INNER JOIN flags ON attachments.attach_id = flags.attach_id
+ INNER JOIN flagtypes ON flags.type_id = flagtypes.id
+ WHERE flagtypes.name LIKE 'review%' AND flags.status = '+'
+ ORDER BY flags.modification_date");
+ my $count = 1;
+ my $total = scalar @$ra;
+ my %user_seen;
+ foreach my $ra_row (@$ra) {
+ my ($user_id, $attach_id) = @$ra_row;
+ indicate_progress({ current => $count++, total => $total, every => 25 });
+ next if $user_seen{$user_id};
+ $sth->execute($attach_id, $user_id);
+ $user_seen{$user_id} = 1;
+ }
+
+ print "done\n";
+}
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my ($class, $columns) = @$args{qw(class columns)};
+ if ($class->isa('Bugzilla::User')) {
+ push(@$columns, 'first_patch_reviewed_id');
+ }
+}
+
+sub flag_end_of_update {
+ my ($self, $args) = @_;
+ my ($object, $timestamp, $new_flags) = @$args{qw(object timestamp new_flags)};
+
+ if ($object->isa('Bugzilla::Attachment')
+ && @$new_flags
+ && !$object->attacher->first_patch_reviewed_id
+ && grep($_ eq $object->bug->product, ENABLED_PRODUCTS))
+ {
+ my $attachment = $object;
+
+ foreach my $orig_change (@$new_flags) {
+ my $change = $orig_change;
+ $change =~ s/^[^:]+://; # get rid of setter
+ $change =~ s/\([^\)]+\)$//; # get rid of requestee
+ my ($name, $value) = $change =~ /^(.+)(.)$/;
+
+ # Only interested in review flags set to +
+ next unless $name =~ /^review/ && $value eq '+';
+
+ _send_mail($attachment, $timestamp);
+
+ Bugzilla->dbh->do("UPDATE profiles SET first_patch_reviewed_id = ? WHERE userid = ?",
+ undef, $attachment->id, $attachment->attacher->id);
+ Bugzilla->memcached->clear({ table => 'profiles', id => $attachment->attacher->id });
+ last;
+ }
+ }
+}
+
+sub _send_mail {
+ my ($attachment, $timestamp) = @_;
+
+ my $vars = {
+ date => format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC'),
+ attachment => $attachment,
+ from_user => EMAIL_FROM,
+ };
+
+ my $msg;
+ my $template = Bugzilla->template_inner($attachment->attacher->setting('lang'));
+ $template->process("contributor/email.txt.tmpl", $vars, \$msg)
+ || ThrowTemplateError($template->error());
+
+ MessageToMTA($msg);
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/ContributorEngagement/lib/Constants.pm b/extensions/ContributorEngagement/lib/Constants.pm
new file mode 100644
index 000000000..346e00c35
--- /dev/null
+++ b/extensions/ContributorEngagement/lib/Constants.pm
@@ -0,0 +1,31 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ContributorEngagement::Constants;
+
+use strict;
+
+use base qw(Exporter);
+
+our @EXPORT = qw(
+ EMAIL_FROM
+ ENABLED_PRODUCTS
+);
+
+use constant EMAIL_FROM => 'bugzilla-daemon@mozilla.org';
+
+use constant ENABLED_PRODUCTS => (
+ "Core",
+ "Firefox",
+ "Firefox for Android",
+ "Firefox for Metro",
+ "Mozilla Services",
+ "Testing",
+ "Toolkit",
+);
+
+1;
diff --git a/extensions/ContributorEngagement/template/en/default/contributor/email.txt.tmpl b/extensions/ContributorEngagement/template/en/default/contributor/email.txt.tmpl
new file mode 100644
index 000000000..915ebd912
--- /dev/null
+++ b/extensions/ContributorEngagement/template/en/default/contributor/email.txt.tmpl
@@ -0,0 +1,49 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+[% PROCESS "global/variables.none.tmpl" %]
+From: [% from_user FILTER none %]
+To: [% attachment.attacher.email FILTER none %]
+Subject: Congratulations on having your first patch approved
+Date: [% date FILTER none %]
+X-Bugzilla-Type: contributor-engagement
+
+Congratulations on having your first patch approved, and thank you for your
+contribution to Mozilla.
+
+[%+ urlbase %]attachment.cgi?id=[% attachment.id FILTER uri %]&action=edit
+
+The next step is to get the patch actually checked in to our repository. For
+more information about how to make that happen, check out this post:
+
+https://developer.mozilla.org/en-US/docs/Mercurial_FAQ#How_can_I_generate_a_patch_for_somebody_else_to_check-in_for_me.3F
+
+While you are going through those final steps, if you're looking for a new
+project to take on, have a look at our list of 'mentored' [% terms.bugs %] ([% terms.bugs %] where
+someone is specifically available to help you):
+
+https://bugzil.la/sw:mentor
+
+Alternatively, you could join us on our IRC chat server in the #introduction
+channel and ask for suggestions about what would be a good [% terms.bugs %] to work on.
+There's more about using our chat server at:
+
+http://irc.mozilla.org/
+
+If you haven't done so already, this is also a good time to sign up to the
+Mozilla Contributor Directory and create a profile for yourself. Doing this
+will give you access to community members' profiles so you can reach out and
+connect with other Mozillians. You will need someone to 'vouch for' your
+profile; if you don't know any other Mozillians well, why not contact the
+person who approved your patch?
+
+The directory is here:
+
+https://mozillians.org/
+
+Thanks again for your help :-)
+Josh, Kyle, Dietrich and Brian; Coding Stewards
diff --git a/extensions/EditComments/Config.pm b/extensions/EditComments/Config.pm
new file mode 100644
index 000000000..dae675001
--- /dev/null
+++ b/extensions/EditComments/Config.pm
@@ -0,0 +1,19 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::EditComments;
+
+use 5.10.1;
+use strict;
+
+use constant NAME => 'EditComments';
+
+use constant REQUIRED_MODULES => [];
+
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/EditComments/Extension.pm b/extensions/EditComments/Extension.pm
new file mode 100644
index 000000000..fef1b7693
--- /dev/null
+++ b/extensions/EditComments/Extension.pm
@@ -0,0 +1,270 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::EditComments;
+
+use 5.10.1;
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Bug;
+use Bugzilla::Util;
+use Bugzilla::Error;
+use Bugzilla::Config::Common;
+use Bugzilla::Config::GroupSecurity;
+use Bugzilla::WebService::Bug;
+use Bugzilla::WebService::Util qw(filter_wants);
+
+our $VERSION = '0.01';
+
+################
+# Installation #
+################
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ my $schema = $args->{schema};
+
+ $schema->{'longdescs_activity'} = {
+ FIELDS => [
+ comment_id => {TYPE => 'INT', NOTNULL => 1,
+ REFERENCES => {TABLE => 'longdescs',
+ COLUMN => 'comment_id',
+ DELETE => 'CASCADE'}},
+ who => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
+ change_when => {TYPE => 'DATETIME', NOTNULL => 1},
+ old_comment => {TYPE => 'LONGTEXT', NOTNULL => 1},
+ ],
+ INDEXES => [
+ longdescs_activity_comment_id_idx => ['comment_id'],
+ longdescs_activity_change_when_idx => ['change_when'],
+ longdescs_activity_comment_id_change_when_idx => [qw(comment_id change_when)],
+ ],
+ };
+}
+
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_add_column('longdescs', 'edit_count', { TYPE => 'INT3', DEFAULT => 0 });
+}
+
+####################
+# Template Methods #
+####################
+
+sub page_before_template {
+ my ($self, $args) = @_;
+
+ return if $args->{'page_id'} ne 'editcomments.html';
+
+ my $vars = $args->{'vars'};
+ my $user = Bugzilla->user;
+ my $params = Bugzilla->input_params;
+
+ # validate group membership
+ my $edit_comments_group = Bugzilla->params->{edit_comments_group};
+ if (!$edit_comments_group || !$user->in_group($edit_comments_group)) {
+ ThrowUserError('auth_failure', { group => $edit_comments_group,
+ action => 'view',
+ object => 'editcomments' });
+ }
+
+ my $bug_id = $params->{bug_id};
+ my $bug = Bugzilla::Bug->check($bug_id);
+
+ my $comment_id = $params->{comment_id};
+
+ my ($comment) = grep($_->id == $comment_id, @{ $bug->comments });
+ if (!$comment
+ || ($comment->is_private && !$user->is_insider))
+ {
+ ThrowUserError("edit_comment_invalid_comment_id", { comment_id => $comment_id });
+ }
+
+ $vars->{'bug'} = $bug;
+ $vars->{'comment'} = $comment;
+}
+
+##################
+# Object Methods #
+##################
+
+BEGIN {
+ no warnings 'redefine';
+ *Bugzilla::Comment::activity = \&_get_activity;
+ *Bugzilla::Comment::edit_count = \&_edit_count;
+ *Bugzilla::WebService::Bug::_super_translate_comment = \&Bugzilla::WebService::Bug::_translate_comment;
+ *Bugzilla::WebService::Bug::_translate_comment = \&_new_translate_comment;
+}
+
+sub _new_translate_comment {
+ my ($self, $comment, $filters) = @_;
+
+ my $comment_hash = $self->_super_translate_comment($comment, $filters);
+
+ if (filter_wants $filters, 'raw_text') {
+ $comment_hash->{raw_text} = $self->type('string', $comment->body);
+ }
+
+ return $comment_hash;
+}
+
+sub _edit_count { return $_[0]->{'edit_count'}; }
+
+sub _get_activity {
+ my ($self, $activity_sort_order) = @_;
+
+ return $self->{'activity'} if $self->{'activity'};
+
+ my $dbh = Bugzilla->dbh;
+ my $query = 'SELECT longdescs_activity.comment_id AS id, profiles.userid, ' .
+ $dbh->sql_date_format('longdescs_activity.change_when', '%Y.%m.%d %H:%i:%s') . '
+ AS time, longdescs_activity.old_comment AS old
+ FROM longdescs_activity
+ INNER JOIN profiles
+ ON profiles.userid = longdescs_activity.who
+ WHERE longdescs_activity.comment_id = ?';
+ $query .= " ORDER BY longdescs_activity.change_when DESC";
+ my $sth = $dbh->prepare($query);
+ $sth->execute($self->id);
+
+ # We are shifting each comment activity body 1 back. The reason this
+ # has to be done is that the longdescs_activity table stores the comment
+ # body that the comment was before the edit, not the actual new version
+ # of the comment.
+ my @activity;
+ my $new_comment;
+ my $last_old_comment;
+ my $count = 0;
+ while (my $change_ref = $sth->fetchrow_hashref()) {
+ my %change = %$change_ref;
+ $change{'author'} = Bugzilla::User->new({ id => $change{'userid'}, cache => 1 });
+ if ($count == 0) {
+ $change{new} = $self->body;
+ }
+ else {
+ $change{new} = $new_comment;
+ }
+ $new_comment = $change{old};
+ $last_old_comment = $change{old};
+ push (@activity, \%change);
+ $count++;
+ }
+
+ return [] if !@activity;
+
+ # Store the original comment as the first or last entry
+ # depending on sort order
+ push(@activity, {
+ author => $self->author,
+ body => $last_old_comment,
+ time => $self->creation_ts,
+ original => 1
+ });
+
+ $activity_sort_order
+ ||= Bugzilla->user->settings->{'comment_sort_order'}->{'value'};
+
+ if ($activity_sort_order eq "oldest_to_newest") {
+ @activity = reverse @activity;
+ }
+
+ $self->{'activity'} = \@activity;
+
+ return $self->{'activity'};
+}
+
+#########
+# Hooks #
+#########
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my ($class, $columns) = @$args{qw(class columns)};
+ if ($class->isa('Bugzilla::Comment')) {
+ push(@$columns, 'edit_count');
+ }
+}
+
+sub bug_end_of_update {
+ my ($self, $args) = @_;
+
+ # Silently return if not in the proper group
+ # or if editing comments is disabled
+ my $user = Bugzilla->user;
+ my $edit_comments_group = Bugzilla->params->{edit_comments_group};
+ return if (!$edit_comments_group || !$user->in_group($edit_comments_group));
+
+ my $bug = $args->{bug};
+ my $timestamp = $args->{timestamp};
+ my $params = Bugzilla->input_params;
+ my $dbh = Bugzilla->dbh;
+
+ my $updated = 0;
+ foreach my $param (grep(/^edit_comment_textarea_/, keys %$params)) {
+ my ($comment_id) = $param =~ /edit_comment_textarea_(\d+)$/;
+ next if !detaint_natural($comment_id);
+
+ # The comment ID must belong to this bug.
+ my ($comment_obj) = grep($_->id == $comment_id, @{ $bug->comments});
+ next if (!$comment_obj || ($comment_obj->is_private && !$user->is_insider));
+
+ my $new_comment = $comment_obj->_check_thetext($params->{$param});
+
+ my $old_comment = $comment_obj->body;
+ next if $old_comment eq $new_comment;
+
+ trick_taint($new_comment);
+ $dbh->do("UPDATE longdescs SET thetext = ?, edit_count = edit_count + 1
+ WHERE comment_id = ?",
+ undef, $new_comment, $comment_id);
+ Bugzilla->memcached->clear({ table => 'longdescs', id => $comment_id });
+
+ # Log old comment to the longdescs activity table
+ $timestamp ||= $dbh->selectrow_array("SELECT NOW()");
+ $dbh->do("INSERT INTO longdescs_activity " .
+ "(comment_id, who, change_when, old_comment) " .
+ "VALUES (?, ?, ?, ?)",
+ undef, ($comment_id, $user->id, $timestamp, $old_comment));
+
+ $comment_obj->{thetext} = $new_comment;
+
+ $updated = 1;
+ }
+
+ $bug->_sync_fulltext( update_comments => 1 ) if $updated;
+}
+
+sub config_modify_panels {
+ my ($self, $args) = @_;
+ push @{ $args->{panels}->{groupsecurity}->{params} }, {
+ name => 'edit_comments_group',
+ type => 's',
+ choices => \&Bugzilla::Config::GroupSecurity::_get_all_group_names,
+ default => 'admin',
+ checker => \&check_group
+ };
+}
+
+sub webservice {
+ my ($self, $args) = @_;
+ my $dispatch = $args->{dispatch};
+ $dispatch->{EditComments} = "Bugzilla::Extension::EditComments::WebService";
+}
+
+sub db_sanitize {
+ my $dbh = Bugzilla->dbh;
+ print "Deleting edited comment histories...\n";
+ $dbh->do("DELETE FROM longdescs_activity");
+ $dbh->do("UPDATE longdescs SET edit_count=0");
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/EditComments/lib/WebService.pm b/extensions/EditComments/lib/WebService.pm
new file mode 100644
index 000000000..9213f0407
--- /dev/null
+++ b/extensions/EditComments/lib/WebService.pm
@@ -0,0 +1,170 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::EditComments::WebService;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::WebService);
+
+use Bugzilla::Error;
+use Bugzilla::Util qw(trim);
+use Bugzilla::WebService::Util qw(validate);
+
+sub comments {
+ my ($self, $params) = validate(@_, 'comment_ids');
+ my $dbh = Bugzilla->switch_to_shadow_db();
+ my $user = Bugzilla->user;
+
+ if (!defined $params->{comment_ids}) {
+ ThrowCodeError('param_required',
+ { function => 'Bug.comments',
+ param => 'comment_ids' });
+ }
+
+ my @ids = map { trim($_) } @{ $params->{comment_ids} || [] };
+ my $comment_data = Bugzilla::Comment->new_from_list(\@ids);
+
+ # See if we were passed any invalid comment ids.
+ my %got_ids = map { $_->id => 1 } @$comment_data;
+ foreach my $comment_id (@ids) {
+ if (!$got_ids{$comment_id}) {
+ ThrowUserError('comment_id_invalid', { id => $comment_id });
+ }
+ }
+
+ # Now make sure that we can see all the associated bugs.
+ my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data;
+ $user->visible_bugs([ keys %got_bug_ids ]); # preload cache for visibility check
+ Bugzilla::Bug->check($_) foreach (keys %got_bug_ids);
+
+ my %comments;
+ foreach my $comment (@$comment_data) {
+ if ($comment->is_private && !$user->is_insider) {
+ ThrowUserError('comment_is_private', { id => $comment->id });
+ }
+ $comments{$comment->id} = $comment->body;
+ }
+
+ return { comments => \%comments };
+}
+
+sub rest_resources {
+ return [
+ qr{^/editcomments/comment/(\d+)$}, {
+ GET => {
+ method => 'comments',
+ params => sub {
+ return { comment_ids => $_[0] };
+ },
+ },
+ },
+ qr{^/editcomments/comment$}, {
+ GET => {
+ method => 'comments',
+ },
+ },
+ ];
+};
+
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Extension::EditComments::Webservice - The EditComments WebServices API
+
+=head1 DESCRIPTION
+
+This module contains API methods that are useful to user's of bugzilla.mozilla.org.
+
+=head1 METHODS
+
+=head2 comments
+
+B<EXPERIMENTAL>
+
+=over
+
+=item B<Description>
+
+This allows you to get the raw comment text about comments, given a list of comment ids.
+
+=item B<REST>
+
+To get all comment text for a list of comment ids:
+
+GET /bug/editcomments/comment?comment_ids=1234&comment_ids=5678...
+
+To get comment text for a specific comment based on the comment ID:
+
+GET /bug/editcomments/comment/<comment_id>
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+=over
+
+=item C<comment_ids> (required)
+
+C<array> An array of integer comment_ids. These comments will be
+returned individually, separate from any other comments in their
+respective bugs.
+
+=item B<Returns>
+
+1 item is returned:
+
+=over
+
+=item C<comments>
+
+Each individual comment requested in C<comment_ids> is returned here,
+in a hash where the numeric comment id is the key, and the value
+is the comment's raw text.
+
+=back
+
+=item B<Errors>
+
+In addition to standard Bug.get type errors, this method can throw the
+following additional errors:
+
+=over
+
+=item 110 (Comment Is Private)
+
+You specified the id of a private comment in the C<comment_ids>
+argument, and you are not in the "insider group" that can see
+private comments.
+
+=item 111 (Invalid Comment ID)
+
+You specified an id in the C<comment_ids> argument that is invalid--either
+you specified something that wasn't a number, or there is no comment with
+that id.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in BMO Bugzilla B<4.2>.
+
+=back
+
+=back
+
+=back
+
+See L<Bugzilla::WebService> for a description of how parameters are passed,
+and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
diff --git a/extensions/EditComments/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl b/extensions/EditComments/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl
new file mode 100644
index 000000000..01ca7bbb7
--- /dev/null
+++ b/extensions/EditComments/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF panel.name == "groupsecurity" %]
+ [% panel.param_descs.edit_comments_group =
+ 'The name of the group of users who can edit comments. Leave blank to disable comment editing.'
+ %]
+[% END -%]
diff --git a/extensions/EditComments/template/en/default/hook/bug/comments-a_comment-end.html.tmpl b/extensions/EditComments/template/en/default/hook/bug/comments-a_comment-end.html.tmpl
new file mode 100644
index 000000000..89249efdf
--- /dev/null
+++ b/extensions/EditComments/template/en/default/hook/bug/comments-a_comment-end.html.tmpl
@@ -0,0 +1,49 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF Param('edit_comments_group')
+ && user.in_group(Param('edit_comments_group'))
+ && (comment.type == 0 || comment.type == 5)
+ && comment.body != ''
+%]
+ <span id="edit_comment_link_[% comment.count FILTER html %]">
+ [<a href="javascript:void(0);" id="edit_comment_edit_link_[% comment.count FILTER html %]"
+ onclick="editComment('[% comment.count FILTER js %]','[% comment.id FILTER js %]');">edit</a>
+ [% IF comment.edit_count %]
+ | <a href="page.cgi?id=editcomments.html&bug_id=[% bug.id FILTER uri %]&comment_id=[% comment.id FILTER uri %]">history</a>
+ ([% comment.edit_count FILTER html %])
+ [% END %]]
+ </span>
+ <div id="edit_comment_[% comment.count FILTER html %]">
+ <div class="bz_comment_text bz_default_hidden" id="edit_comment_loading_[% comment.count FILTER html %]">Loading...</div>
+ [% INCLUDE global/textarea.html.tmpl
+ name = "edit_comment_textarea_${comment.id}"
+ id = "edit_comment_textarea_${comment.count}"
+ minrows = 10
+ maxrows = 25
+ classes = "edit_comment_textarea bz_default_hidden"
+ cols = constants.COMMENT_COLS
+ disabled = 1
+ %]
+ </div>
+ <script>
+ YAHOO.util.Event.onDOMReady(function() {
+ // Insert edit links near other comment actions such as reply
+ var comment_div = YAHOO.util.Dom.get('c[% comment.count FILTER js %]');
+ var bz_comment_actions = YAHOO.util.Dom.getElementsByClassName('bz_comment_actions', 'span', comment_div)[0];
+ var edit_comment_link = YAHOO.util.Dom.get('edit_comment_link_[% comment.count FILTER js %]');
+ bz_comment_actions.insertBefore(edit_comment_link, bz_comment_actions.firstChild);
+
+ // Insert blank textarea right below formatted comment
+ var comment_div = YAHOO.util.Dom.get('c[% comment.count FILTER js %]');
+ var comment_pre = YAHOO.util.Dom.get('comment_text_[% comment.count FILTER js %]');
+ var edit_comment_div = YAHOO.util.Dom.get('edit_comment_[% comment.count FILTER js %]');
+ comment_div.insertBefore(edit_comment_div, comment_pre);
+ });
+ </script>
+[% END %]
diff --git a/extensions/EditComments/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/EditComments/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..331d7e6df
--- /dev/null
+++ b/extensions/EditComments/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF Param('edit_comments_group') && user.in_group(Param('edit_comments_group')) %]
+ [% style_urls.push('extensions/EditComments/web/styles/editcomments.css') %]
+ [% javascript_urls.push('extensions/EditComments/web/js/editcomments.js') %]
+[% END %]
diff --git a/extensions/EditComments/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl b/extensions/EditComments/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
new file mode 100644
index 000000000..4325aab30
--- /dev/null
+++ b/extensions/EditComments/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF object == 'editcomments' %]
+ edit comments
+[% END %]
diff --git a/extensions/EditComments/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/EditComments/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..bc02b52f0
--- /dev/null
+++ b/extensions/EditComments/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "edit_comment_invalid_comment_id" %]
+ [% title = "Invalid Comment ID" %]
+ The comment id '[% comment_id FILTER html %]' is invalid.
+[% END %]
diff --git a/extensions/EditComments/template/en/default/pages/editcomments.html.tmpl b/extensions/EditComments/template/en/default/pages/editcomments.html.tmpl
new file mode 100644
index 000000000..8b3b90c9e
--- /dev/null
+++ b/extensions/EditComments/template/en/default/pages/editcomments.html.tmpl
@@ -0,0 +1,122 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Comment changes made to $terms.bug $bug.id, comment $comment.id"
+ header = "Activity log for $terms.bug $bug.id, comment $comment.id"
+ %]
+
+<script type="text/javascript">
+/* The functions below expand and collapse comments */
+function toggle_comment_display(link, comment_id) {
+ if (YAHOO.util.Dom.hasClass('comment_text_' + comment_id, 'collapsed')) {
+ expand_comment(link, comment);
+ }
+ else {
+ collapse_comment(link, comment);
+ }
+}
+
+function toggle_all_comments(action) {
+ var num_comments = [% comment.activity.size FILTER html %];
+
+ // If for some given ID the comment doesn't exist, this doesn't mean
+ // there are no more comments, but that the comment is private and
+ // the user is not allowed to view it.
+
+ for (var id = 0; id < num_comments; id++) {
+ var comment = document.getElementById('comment_text_' + id);
+ if (!comment) {
+ continue;
+ }
+
+ var link = document.getElementById('comment_link_' + id);
+ if (action == 'collapse') {
+ collapse_comment(link, comment);
+ }
+ else {
+ expand_comment(link, comment);
+ }
+ }
+}
+
+function collapse_comment(link, comment) {
+ link.innerHTML = "[+]";
+ link.title = "Expand the comment.";
+ YAHOO.util.Dom.addClass(comment, 'collapsed');
+}
+
+function expand_comment(link, comment) {
+ link.innerHTML = "[-]";
+ link.title = "Collapse the comment";
+ YAHOO.util.Dom.removeClass(comment, 'collapsed');
+}
+</script>
+
+<p>
+ [% "Back to $terms.bug $bug.id" FILTER bug_link(bug.id) FILTER none %]
+</p>
+
+<p>
+ <strong>Note</strong>: The actual edited comment in the [% terms.bug %] view page will always show the original commentor's name and original timestamp.
+</p>
+
+<p>
+ <a href="#" onclick="toggle_all_comments('collapse'); return false;">Collapse All Changes</a> -
+ <a href="#" onclick="toggle_all_comments('expand'); return false;">Expand All Changes</a>
+</p>
+
+[% count = 0 %]
+[% FOREACH a = comment.activity %]
+ <div class="bz_comment">
+ <div class="bz_comment_head">
+ <i>
+ [% IF a.original %]
+ Original comment by [% (a.author.name || "Need Real Name") FILTER html %]
+ <span class="vcard">
+ (<a class="fn email" href="mailto:[% a.author.email FILTER html %]">
+ [%- a.author.email FILTER html -%]</a>)
+ </span>
+ on [%+ a.time FILTER time %]
+ [% ELSE %]
+ Revision by [% (a.author.name || "Need Real Name") FILTER html %]
+ <span class="vcard">
+ (<a class="fn email" href="mailto:[% a.author.email FILTER html %]">
+ [%- a.author.email FILTER html -%]</a>)
+ </span>
+ on [%+ a.time FILTER time %]
+ [% END %]
+ </i>
+ <a href="#" id="comment_link_[% count FILTER html %]"
+ onclick="toggle_comment_display(this, '[% count FILTER html FILTER js %]'); return false;"
+ title="Collapse the comment.">[-]</a>
+ </div>
+ [% IF a.original %]
+ [% wrapped_comment = a.body FILTER wrap_comment %]
+ [% ELSE %]
+ [% wrapped_comment = a.new FILTER wrap_comment %]
+ [% END %]
+[%# Don't indent the <pre> block, since then the spaces are displayed in the
+ # generated HTML %]
+<pre class="bz_comment_text" id="comment_text_[% count FILTER html %]">
+ [%- wrapped_comment FILTER quoteUrls(bug) -%]
+</pre>
+ </div>
+ [% count = count + 1 %]
+[% END %]
+
+[% IF comment.activity.size > 0 %]
+ <p>
+ [% "Back to $terms.bug $bug.id" FILTER bug_link(bug.id) FILTER none %]
+ </p>
+[% END %]
+
+[% PROCESS global/footer.html.tmpl %]
+
diff --git a/extensions/EditComments/web/js/editcomments.js b/extensions/EditComments/web/js/editcomments.js
new file mode 100644
index 000000000..91763fa62
--- /dev/null
+++ b/extensions/EditComments/web/js/editcomments.js
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+function editComment(comment_count, comment_id) {
+ if (!comment_count || !comment_id) return;
+
+ var edit_comment_textarea = YAHOO.util.Dom.get('edit_comment_textarea_' + comment_count);
+ if (!YAHOO.util.Dom.hasClass(edit_comment_textarea, 'bz_default_hidden')) {
+ hideEditCommentField(comment_count);
+ return;
+ }
+
+ // Show the loading indicator
+ toggleCommentLoading(comment_count);
+
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ YAHOO.util.Connect.asyncRequest(
+ 'POST',
+ 'jsonrpc.cgi',
+ {
+ success: function(res) {
+ // Hide the loading indicator
+ toggleCommentLoading(comment_count);
+ data = YAHOO.lang.JSON.parse(res.responseText);
+ if (data.error) {
+ alert("Get [% comment failed: " + data.error.message);
+ }
+ else if (data.result.comments[comment_id]) {
+ var comment_text = data.result.comments[comment_id];
+ showEditCommentField(comment_count, comment_text);
+ }
+ },
+ failure: function(res) {
+ // Hide the loading indicator
+ toggleCommentLoading(comment_count);
+ if (res.responseText) {
+ alert("Get comment failed: " + res.responseText);
+ }
+ }
+ },
+ YAHOO.lang.JSON.stringify({
+ version: "1.1",
+ method: "EditComments.comments",
+ id: comment_id,
+ params: { comment_ids: [ comment_id ] }
+ })
+ );
+}
+
+function hideEditCommentField(comment_count) {
+ var comment_text_pre = YAHOO.util.Dom.get('comment_text_' + comment_count);
+ YAHOO.util.Dom.removeClass(comment_text_pre, 'bz_default_hidden');
+
+ var edit_comment_textarea = YAHOO.util.Dom.get('edit_comment_textarea_' + comment_count);
+ YAHOO.util.Dom.addClass(edit_comment_textarea, 'bz_default_hidden');
+ edit_comment_textarea.disabled = true;
+
+ YAHOO.util.Dom.get("edit_comment_edit_link_" + comment_count).innerHTML = "edit";
+}
+
+function showEditCommentField(comment_count, comment_text) {
+ var comment_text_pre = YAHOO.util.Dom.get('comment_text_' + comment_count);
+ YAHOO.util.Dom.addClass(comment_text_pre, 'bz_default_hidden');
+
+ var edit_comment_textarea = YAHOO.util.Dom.get('edit_comment_textarea_' + comment_count);
+ YAHOO.util.Dom.removeClass(edit_comment_textarea, 'bz_default_hidden');
+ edit_comment_textarea.disabled = false;
+ edit_comment_textarea.value = comment_text;
+
+ YAHOO.util.Dom.get("edit_comment_edit_link_" + comment_count).innerHTML = "unedit";
+}
+
+function toggleCommentLoading(comment_count, hide) {
+ var comment_div = 'comment_text_' + comment_count;
+ var loading_div = 'edit_comment_loading_' + comment_count;
+ if (YAHOO.util.Dom.hasClass(loading_div, 'bz_default_hidden')) {
+ YAHOO.util.Dom.addClass(comment_div, 'bz_default_hidden');
+ YAHOO.util.Dom.removeClass(loading_div, 'bz_default_hidden');
+ }
+ else {
+ YAHOO.util.Dom.removeClass(comment_div, 'bz_default_hidden');
+ YAHOO.util.Dom.addClass(loading_div, 'bz_default_hidden');
+ }
+}
+
diff --git a/extensions/EditComments/web/styles/editcomments.css b/extensions/EditComments/web/styles/editcomments.css
new file mode 100644
index 000000000..911896ac8
--- /dev/null
+++ b/extensions/EditComments/web/styles/editcomments.css
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+.edit_comment_textarea {
+ width: 845px;
+}
diff --git a/extensions/EditTable/Config.pm b/extensions/EditTable/Config.pm
new file mode 100644
index 000000000..d601951a4
--- /dev/null
+++ b/extensions/EditTable/Config.pm
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::EditTable;
+use strict;
+
+use constant NAME => 'EditTable';
+use constant REQUIRED_MODULES => [];
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/EditTable/Extension.pm b/extensions/EditTable/Extension.pm
new file mode 100644
index 000000000..a10a30e57
--- /dev/null
+++ b/extensions/EditTable/Extension.pm
@@ -0,0 +1,180 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+
+# this is a quick and dirty table editor, designed to allow admins to quickly
+# maintain tables.
+#
+# each table must be defined via the editable_tables hook
+#
+# this extension doesn't currently provide any ability to modify or validate
+# values. use with caution!
+
+package Bugzilla::Extension::EditTable;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Error;
+use Bugzilla::Hook;
+use Bugzilla::Util qw(trick_taint);
+use JSON;
+use Storable qw(dclone);
+
+our $VERSION = '1';
+
+# definitions for tables which we can edit with the quick-and-dirty editor
+#
+# $table_name => {
+# id_field => name of the "id" field
+# order_by => the field to sort rows by (optional, defaults to the id_field)
+# blurb => text which describes the table
+# group => group required to edit this table (optional, defaults to "admin")
+# }
+#
+# example:
+# 'antispam_domain_blocklist' => {
+# id_field => 'id',
+# order_by => 'domain',
+# blurb => 'List of fully qualified domain names to block at account creation time.',
+# group => 'can_configure_antispam',
+# },
+
+sub EDITABLE_TABLES {
+ my $tables = {};
+ Bugzilla::Hook::process("editable_tables", { tables => $tables });
+ return $tables;
+}
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my ($vars, $page) = @$args{qw(vars page_id)};
+ return unless $page eq 'edit_table.html';
+ my $input = Bugzilla->input_params;
+
+ # we only support editing a particular set of tables
+ my $table_name = $input->{table};
+ exists $self->EDITABLE_TABLES()->{$table_name}
+ || ThrowUserError('edittable_unsupported', { table => $table_name } );
+ my $table = $self->EDITABLE_TABLES()->{$table_name};
+ my $id_field = $table->{id_field};
+ my $order_by = $table->{order_by} || $id_field;
+ my $group = $table->{group} || 'admin';
+ trick_taint($table_name);
+
+ Bugzilla->user->in_group($group)
+ || ThrowUserError('auth_failure', { group => $group,
+ action => 'edit',
+ object => 'tables' });
+
+ # load columns
+ my $dbh = Bugzilla->dbh;
+ my @fields = sort
+ grep { $_ ne $id_field && $_ ne $order_by; }
+ $dbh->bz_table_columns($table_name);
+ if ($order_by ne $id_field) {
+ unshift @fields, $order_by;
+ }
+
+ # update table
+ my $data = $input->{table_data};
+ my $edits = [];
+ if ($data) {
+ $data = from_json($data)->{data};
+ $edits = dclone($data);
+ eval {
+ $dbh->bz_start_transaction;
+
+ foreach my $row (@$data) {
+ map { trick_taint($_) } @$row;
+ if ($row->[0] eq '-') {
+ # add
+ shift @$row;
+ next unless grep { $_ ne '' } @$row;
+ my $placeholders = join(',', split(//, '?' x scalar(@fields)));
+ $dbh->do(
+ "INSERT INTO $table_name(" . join(',', @fields) . ") " .
+ "VALUES ($placeholders)",
+ undef,
+ @$row
+ );
+ }
+ elsif ($row->[0] < 0) {
+ # delete
+ $dbh->do(
+ "DELETE FROM $table_name WHERE $id_field=?",
+ undef,
+ -$row->[0]
+ );
+ }
+ else {
+ # update
+ my $id = shift @$row;
+ $dbh->do(
+ "UPDATE $table_name " .
+ "SET " . join(',', map { "$_ = ?" } @fields) . " " .
+ "WHERE $id_field = ?",
+ undef,
+ @$row, $id
+ );
+ }
+ }
+
+ $dbh->bz_commit_transaction;
+ $vars->{updated} = 1;
+ $edits = [];
+ };
+ if ($@) {
+ my $error = $@;
+ $error =~ s/^DBD::[^:]+::db do failed: //;
+ $error =~ s/^(.+) \[for Statement ".+$/$1/s;
+ $vars->{error} = $error;
+ $dbh->bz_rollback_transaction;
+ }
+ }
+
+ # load data from table
+ unshift @fields, $id_field;
+ $data = $dbh->selectall_arrayref(
+ "SELECT " . join(',', @fields) . " FROM $table_name ORDER BY $order_by"
+ );
+
+ # we don't support nulls currently
+ foreach my $row (@$data) {
+ if (grep { !defined($_) } @$row) {
+ ThrowUserError('edittable_nulls', { table => $table_name } );
+ }
+ }
+
+ # apply failed edits
+ foreach my $edit (@$edits) {
+ if ($edit->[0] eq '-') {
+ push @$data, $edit;
+ }
+ else {
+ my $id = $edit->[0];
+ foreach my $row (@$data) {
+ if ($row->[0] == $id) {
+ @$row = @$edit;
+ last;
+ }
+ }
+ }
+ }
+
+ $vars->{table_name} = $table_name;
+ $vars->{blurb} = $table->{blurb};
+ $vars->{table_data} = to_json({
+ fields => \@fields,
+ id_field => $id_field,
+ data => $data,
+ });
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/EditTable/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl b/extensions/EditTable/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
new file mode 100644
index 000000000..f86fb4c86
--- /dev/null
+++ b/extensions/EditTable/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF object == 'tables' %]
+ tables
+[% END %]
diff --git a/extensions/EditTable/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/EditTable/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..d87270b98
--- /dev/null
+++ b/extensions/EditTable/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,17 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "edittable_unsupported" %]
+ [% title = "Unsupported Table" %]
+ You cannot edit the table '[% table FILTER html %]'.
+
+[% ELSIF error == "edittable_nulls" %]
+ [% title = "Table Contains NULLs" %]
+ EditTable cannot edit the table '[% table FILTER html %]' as it contains NULL
+ values.
+[% END %]
diff --git a/extensions/EditTable/template/en/default/pages/edit_table.html.tmpl b/extensions/EditTable/template/en/default/pages/edit_table.html.tmpl
new file mode 100644
index 000000000..d81291640
--- /dev/null
+++ b/extensions/EditTable/template/en/default/pages/edit_table.html.tmpl
@@ -0,0 +1,43 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Edit Table"
+ javascript_urls = [ 'extensions/EditTable/web/js/edit_table.js' ]
+ style_urls = [ "extensions/EditTable/web/styles/edit_table.css" ]
+%]
+
+<h2>[% table_name FILTER html %]</h2>
+
+[% IF updated %]
+ <p id="message">Table [% table_name FILTER html %] updated.</p>
+[% ELSIF error %]
+ <p class="throw_error">[% error FILTER html FILTER html_line_break %]</p>
+[% END %]
+
+<p>[% blurb FILTER html FILTER html_line_break %]</p>
+
+<div id="edit_table"></div>
+<br>
+<form method="post" action="page.cgi" enctype="multipart/form-data"
+ onsubmit="editTable.to_json('table_data')">
+<input type="hidden" name="id" value="edit_table.html">
+<input type="hidden" name="table" value="[% table_name FILTER html %]">
+<input type="hidden" name="table_data" id="table_data">
+<input type="submit" value="Commit Changes" id="commit_btn" class="bz_default_hidden">
+</form>
+
+<script>
+ var table_data = [% table_data FILTER none %];
+ var editTable = new EditTable('edit_table', table_data);
+ editTable.render();
+</script>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/EditTable/web/js/edit_table.js b/extensions/EditTable/web/js/edit_table.js
new file mode 100644
index 000000000..ae239759b
--- /dev/null
+++ b/extensions/EditTable/web/js/edit_table.js
@@ -0,0 +1,131 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+
+function EditTable(parent_el, table_data) {
+ this.parent_el = YAHOO.util.Dom.get(parent_el);
+ this.table_data = table_data;
+ this.field_count = table_data.fields.length;
+ if (!JSON) JSON = YAHOO.lang.JSON;
+
+ this.render = function() {
+ // create table
+ this.parent_el.innerHTML = '';
+ var table = document.createElement('table');
+
+ // header
+ var tr = document.createElement('tr');
+ for (var i = 0; i < this.field_count; i++) {
+ var th = document.createElement('th');
+ th.appendChild(document.createTextNode(this.table_data.fields[i]));
+ tr.appendChild(th);
+ }
+ var td = document.createElement('td');
+ td.innerHTML = '&nbsp;&nbsp;';
+ tr.appendChild(td);
+ table.appendChild(tr);
+
+ // rows
+ for (var i = 0; i < table_data.data.length; i++) {
+ // skip deleted rows
+ if (this.table_data.data[i][0] < 0)
+ continue;
+ var tr = document.createElement('tr');
+ for (var j = 0; j < this.field_count; j++) {
+ var td = document.createElement('td');
+ td.appendChild(document.createTextNode(this.table_data.data[i][j]));
+ tr.appendChild(td);
+
+ if (this.table_data.fields[j] != this.table_data.id_field) {
+ td.className = 'editable';
+ td.contentEditable = true;
+ YAHOO.util.Event.addListener(td, 'keydown', this._edit_keydown, this);
+ YAHOO.util.Event.addListener(td, 'blur', this._save, this);
+ d = td;
+ }
+ }
+ var td = document.createElement('td');
+ var a = document.createElement('a');
+ a.href = '#';
+ a.innerHTML = 'x';
+ YAHOO.util.Event.addListener(a, 'click', this._remove_row, this);
+ td.appendChild(a);
+ td.className = 'action';
+ tr.appendChild(td);
+ table.appendChild(tr);
+ }
+
+ this.parent_el.appendChild(table);
+
+ var add_btn = document.createElement('button');
+ add_btn.innerHTML = 'Add';
+ YAHOO.util.Event.addListener(add_btn, 'click', this._add_row, this);
+ this.parent_el.appendChild(add_btn);
+ },
+
+ this.to_json = function(target) {
+ YAHOO.util.Dom.get(target).value = JSON.stringify(this.table_data);
+ },
+
+ this._add_row = function(event, obj) {
+ var row = [];
+ for (var i = 0; i < obj.field_count; i++) {
+ row.push(obj.table_data.fields[i] == obj.table_data.id_field ? '-' : '');
+ }
+ obj.table_data.data.push(row);
+ obj.render();
+ YAHOO.util.Dom.removeClass('commit_btn', 'bz_default_hidden');
+ event.preventDefault();
+ },
+
+ this._remove_row = function(event, obj) {
+ var row = event.target.parentElement.parentElement.rowIndex - 1;
+ if (obj.table_data.data[row][0] == '-') {
+ // removing a newly added row
+ obj.table_data.data.splice(row, 1);
+ }
+ else {
+ // to remove a db row we set its id to negative
+ // it'll be skipped by render, and the update script knows which id to delete
+ obj.table_data.data[row][0] = -obj.table_data.data[row][0];
+ }
+ obj.render();
+ YAHOO.util.Dom.removeClass('commit_btn', 'bz_default_hidden');
+ event.preventDefault();
+ },
+
+ this._save = function(event, obj) {
+ var row = event.target.parentElement.rowIndex - 1;
+ var col = event.target.cellIndex;
+ var value = event.target.textContent;
+ if (obj.table_data.data[row][col] != event.target.textContent) {
+ obj.table_data.data[row][col] = event.target.textContent;
+ YAHOO.util.Dom.removeClass('commit_btn', 'bz_default_hidden');
+ }
+ },
+
+ this._revert = function(event, obj) {
+ var row = event.target.parentElement.rowIndex - 1;
+ var col = event.target.cellIndex;
+ event.target.replaceChild(
+ document.createTextNode(obj.table_data.data[row][col]),
+ event.target.firstChild
+ );
+ },
+
+ this._edit_keydown = function(event, obj) {
+ if (event.keyCode == 13) {
+ event.preventDefault();
+ obj._save(event, obj);
+ document.activeElement.blur(event.target);
+ }
+ else if (event.keyCode == 27) {
+ event.preventDefault();
+ obj._revert(event, obj);
+ }
+ }
+};
diff --git a/extensions/EditTable/web/styles/edit_table.css b/extensions/EditTable/web/styles/edit_table.css
new file mode 100644
index 000000000..0b1c72db6
--- /dev/null
+++ b/extensions/EditTable/web/styles/edit_table.css
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+
+#edit_table table {
+ border-spacing: 0;
+ border-collapse: collapse;
+ margin-bottom: 1em;
+}
+
+#edit_table td, #edit_table th {
+ padding: 5px;
+}
+
+#edit_table th {
+ background: #ccc;
+ text-align: left;
+}
+
+#edit_table .editable {
+ background: #fff;
+}
+
+#edit_table tr:hover {
+ background: #eee;
+}
+
+#edit_table .action {
+ display: none;
+}
+
+#edit_table tr:hover .action {
+ display: block;
+}
+
diff --git a/extensions/Ember/Config.pm b/extensions/Ember/Config.pm
new file mode 100644
index 000000000..e3405146d
--- /dev/null
+++ b/extensions/Ember/Config.pm
@@ -0,0 +1,19 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Ember;
+
+use 5.10.1;
+use strict;
+
+use constant NAME => 'Ember';
+
+use constant REQUIRED_MODULES => [];
+
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/Ember/Extension.pm b/extensions/Ember/Extension.pm
new file mode 100644
index 000000000..1c8b8b4e9
--- /dev/null
+++ b/extensions/Ember/Extension.pm
@@ -0,0 +1,22 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Ember;
+
+use 5.10.1;
+use strict;
+use parent qw(Bugzilla::Extension);
+
+our $VERSION = '0.01';
+
+sub webservice {
+ my ($self, $args) = @_;
+ my $dispatch = $args->{dispatch};
+ $dispatch->{Ember} = "Bugzilla::Extension::Ember::WebService";
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/Voting/disabled b/extensions/Ember/disabled
index e69de29bb..e69de29bb 100644
--- a/extensions/Voting/disabled
+++ b/extensions/Ember/disabled
diff --git a/extensions/Ember/lib/FakeBug.pm b/extensions/Ember/lib/FakeBug.pm
new file mode 100644
index 000000000..46fef4ea7
--- /dev/null
+++ b/extensions/Ember/lib/FakeBug.pm
@@ -0,0 +1,78 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Ember::FakeBug;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla::Bug;
+
+our $AUTOLOAD;
+
+sub new {
+ my $class = shift;
+ my $self = shift;
+ bless $self, $class;
+ return $self;
+}
+
+sub AUTOLOAD {
+ my $self = shift;
+ my $name = $AUTOLOAD;
+ $name =~ s/.*://;
+ return exists $self->{$name} ? $self->{$name} : undef;
+}
+
+sub check_can_change_field {
+ return Bugzilla::Bug::check_can_change_field(@_);
+}
+
+sub id { return undef; }
+sub product_obj { return $_[0]->{product_obj}; }
+sub reporter { return Bugzilla->user; }
+
+sub choices {
+ my $self = shift;
+ return $self->{'choices'} if exists $self->{'choices'};
+ return {} if $self->{'error'};
+ my $user = Bugzilla->user;
+
+ my @products = @{ $user->get_enterable_products };
+ # The current product is part of the popup, even if new bugs are no longer
+ # allowed for that product
+ if (!grep($_->name eq $self->product_obj->name, @products)) {
+ unshift(@products, $self->product_obj);
+ }
+
+ my @statuses = @{ Bugzilla::Status->can_change_to };
+
+ # UNCONFIRMED is only a valid status if it is enabled in this product.
+ if (!$self->product_obj->allows_unconfirmed) {
+ @statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses;
+ }
+
+ my %choices = (
+ bug_status => \@statuses,
+ product => \@products,
+ component => $self->product_obj->components,
+ version => $self->product_obj->versions,
+ target_milestone => $self->product_obj->milestones,
+ );
+
+ my $resolution_field = new Bugzilla::Field({ name => 'resolution' });
+ # Don't include the empty resolution in drop-downs.
+ my @resolutions = grep($_->name, @{ $resolution_field->legal_values });
+ $choices{'resolution'} = \@resolutions;
+
+ $self->{'choices'} = \%choices;
+ return $self->{'choices'};
+}
+
+1;
+
diff --git a/extensions/Ember/lib/WebService.pm b/extensions/Ember/lib/WebService.pm
new file mode 100644
index 000000000..7a037e654
--- /dev/null
+++ b/extensions/Ember/lib/WebService.pm
@@ -0,0 +1,995 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Ember::WebService;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use parent qw(Bugzilla::WebService
+ Bugzilla::WebService::Bug
+ Bugzilla::WebService::Product);
+
+use Bugzilla::Bug;
+use Bugzilla::Component;
+use Bugzilla::Product;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Field;
+use Bugzilla::Util qw(trick_taint);
+
+use Bugzilla::Extension::Ember::FakeBug;
+
+use Scalar::Util qw(blessed);
+use Storable qw(dclone);
+
+use constant DATE_FIELDS => {
+ show => ['last_updated'],
+};
+
+use constant FIELD_TYPE_MAP => {
+ 0 => 'unknown',
+ 1 => 'freetext',
+ 2 => 'single_select',
+ 3 => 'multiple_select',
+ 4 => 'textarea',
+ 5 => 'datetime',
+ 6 => 'date',
+ 7 => 'bug_id',
+ 8 => 'bug_urls',
+ 9 => 'keywords',
+ 99 => 'extension'
+};
+
+use constant NON_EDIT_FIELDS => qw(
+ assignee_accessible
+ bug_group
+ bug_id
+ commenter
+ cclist_accessible
+ content
+ creation_ts
+ days_elapsed
+ everconfirmed
+ qacontact_accessible
+ reporter
+ reporter_accessible
+ restrict_comments
+ tag
+ votes
+);
+
+use constant BUG_CHOICE_FIELDS => qw(
+ bug_status
+ component
+ product
+ resolution
+ target_milestone
+ version
+);
+
+use constant DEFAULT_VALUE_MAP => {
+ op_sys => 'defaultopsys',
+ rep_platform => 'defaultplatform',
+ priority => 'defaultpriority',
+ bug_severity => 'defaultseverity'
+};
+
+sub API_NAMES {
+ # Internal field names converted to the API equivalents
+ my %api_names = reverse %{ Bugzilla::Bug::FIELD_MAP() };
+ return \%api_names;
+}
+
+###############
+# API Methods #
+###############
+
+sub create {
+ my ($self, $params) = @_;
+
+ Bugzilla->login(LOGIN_REQUIRED);
+ Bugzilla->switch_to_shadow_db();
+
+ my $product = delete $params->{product};
+ $product || ThrowCodeError('params_required',
+ { function => 'Ember.create', params => ['product'] });
+
+ my $product_obj = Bugzilla::Product->check($product);
+
+ my $fake_bug = Bugzilla::Extension::Ember::FakeBug->new(
+ { product_obj => $product_obj, reporter_id => Bugzilla->user->id });
+
+ my @fields = $self->_get_fields($fake_bug);
+
+ return {
+ fields => \@fields
+ };
+}
+
+sub show {
+ my ($self, $params) = @_;
+ my (@fields, $attachments, $comments, $data);
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+
+ Bugzilla->switch_to_shadow_db();
+
+ # Throw error if token was provided and user is not logged
+ # in meaning token was invalid/expired.
+ if (exists $params->{token} && !$user->id) {
+ ThrowUserError('invalid_token');
+ }
+
+ my $bug_id = delete $params->{id};
+ $bug_id || ThrowCodeError('params_required',
+ { function => 'Ember.show', params => ['id'] });
+
+ my $bug = Bugzilla::Bug->check($bug_id);
+
+ my $bug_hash = $self->_bug_to_hash($bug, $params);
+
+ # Only return changes since last_updated if provided
+ my $last_updated = delete $params->{last_updated};
+ if ($last_updated) {
+ trick_taint($last_updated);
+
+ my $updated_fields =
+ $dbh->selectcol_arrayref('SELECT fieldid FROM bugs_activity
+ WHERE bug_when > ? AND bug_id = ?',
+ undef, ($last_updated, $bug->id));
+
+ if (@$updated_fields) {
+ # Also add in the delta_ts value which is in the bugs_activity
+ # entries
+ push(@$updated_fields, get_field_id('delta_ts'));
+ @fields = $self->_get_fields($bug, $updated_fields);
+ }
+ }
+ # Return all the things
+ else {
+ @fields = $self->_get_fields($bug);
+ }
+
+ # Place the fields current value along with the field definition
+ foreach my $field (@fields) {
+ if (($field->{name} eq 'depends_on'
+ || $field->{name} eq 'blocks')
+ && scalar @{ $bug_hash->{$field->{name}} })
+ {
+ my $bug_ids = delete $bug_hash->{$field->{name}};
+ $user->visible_bugs($bug_ids);
+ my $bug_objs = Bugzilla::Bug->new_from_list($bug_ids);
+
+ my @new_list;
+ foreach my $bug (@$bug_objs) {
+ my $data;
+ if ($user->can_see_bug($bug)) {
+ $data = {
+ id => $bug->id,
+ status => $bug->bug_status,
+ summary => $bug->short_desc
+ };
+ }
+ else {
+ $data = { id => $bug->id };
+ }
+ push(@new_list, $data);
+ }
+ $field->{current_value} = \@new_list;
+ }
+ else {
+ $field->{current_value} = delete $bug_hash->{$field->{name}} || '';
+ }
+ }
+
+ # Any left over bug values will be added to the field list
+ # These are extra fields that do not have a corresponding
+ # Field.pm object
+ if (!$last_updated) {
+ foreach my $key (keys %$bug_hash) {
+ my $field = {
+ name => $key,
+ current_value => $bug_hash->{$key}
+ };
+ my $name = Bugzilla::Bug::FIELD_MAP()->{$key} || $key;
+ $field->{can_edit} = $self->_can_change_field($name, $bug);
+ push(@fields, $field);
+ }
+ }
+
+ # Complete the return data
+ my $data = { id => $bug->id, fields => \@fields };
+
+ return $data;
+}
+
+sub search {
+ my ($self, $params) = @_;
+
+ my $total;
+ if (exists $params->{offset} && exists $params->{limit}) {
+ my $count_params = dclone($params);
+ delete $count_params->{offset};
+ delete $count_params->{limit};
+ $count_params->{count_only} = 1;
+ $total = $self->SUPER::search($count_params);
+ }
+
+ my $result = $self->SUPER::search($params);
+ $result->{total} = defined $total ? $total : scalar(@{ $result->{bugs} });
+ return $result;
+}
+
+sub bug {
+ my ($self, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $bug_id = delete $params->{id};
+ $bug_id || ThrowCodeError('param_required',
+ { function => 'Ember.bug', param => 'id' });
+
+ my ($comments, $attachments) = ([], []);
+ my $bug = $self->get({ ids => [ $bug_id ] });
+ $bug = $bug->{bugs}->[0];
+
+ # Only return changes since last_updated if provided
+ my $last_updated = delete $params->{last_updated};
+ if ($last_updated) {
+ trick_taint($last_updated);
+ my $updated_fields = $dbh->selectcol_arrayref('SELECT fielddefs.name
+ FROM fielddefs INNER JOIN bugs_activity
+ ON fielddefs.id = bugs_activity.fieldid
+ WHERE bugs_activity.bug_when > ?
+ AND bugs_activity.bug_id = ?',
+ undef, ($last_updated, $bug->{id}));
+
+ my %field_map = reverse %{ Bugzilla::Bug::FIELD_MAP() };
+ $field_map{'flagtypes.name'} = 'flags';
+
+ my $changed_bug = {};
+ foreach my $field (@$updated_fields) {
+ my $field_name = $field_map{$field} || $field;
+ if ($bug->{$field_name}) {
+ $changed_bug->{$field_name} = $bug->{$field_name};
+ }
+ }
+ $bug = $changed_bug;
+
+ # Find any comments created since the last_updated date
+ $comments = $self->comments({ ids => $bug_id, new_since => $last_updated });
+
+ # Find any new attachments or modified attachments since the
+ # last_updated date
+ my $updated_attachments =
+ $dbh->selectcol_arrayref('SELECT attach_id FROM attachments
+ WHERE (creation_ts > ? OR modification_time > ?)
+ AND bug_id = ?',
+ undef, ($last_updated, $last_updated, $bug->{id}));
+ if ($updated_attachments) {
+ $attachments = $self->_get_attachments({ attachment_ids => $updated_attachments,
+ exclude_fields => ['data'] });
+ }
+ }
+ else {
+ $comments = $self->comments({ ids => [ $bug_id ] });
+ $attachments = $self->_get_attachments({ ids => [ $bug_id ],
+ exclude_fields => ['data'] });
+
+ }
+
+ $comments = $comments->{bugs}->{$bug_id}->{comments};
+
+ return {
+ bug => $bug,
+ comments => $comments,
+ attachments => $attachments,
+ };
+}
+
+sub get_attachments {
+ my ($self, $params) = @_;
+ my $attachments = $self->_get_attachments($params);
+ my $flag_types = [];
+ my $bug;
+ if ($params->{ids}) {
+ $bug = Bugzilla::Bug->check($params->{ids}->[0]);
+ $flag_types = $self->_get_flag_types_bug($bug, 'attachment');
+ }
+ elsif ($params->{attachment_ids} && @$attachments) {
+ $bug = Bugzilla::Bug->check($attachments->[0]->{bug_id});
+ $flag_types = $self->_get_flag_types_all($bug, 'attachment')->{attachment};
+ }
+ if (@$flag_types) {
+ @$flag_types = map { $self->_flagtype_to_hash($_, $bug) } @$flag_types;
+ }
+ return {
+ attachments => $attachments,
+ flag_types => $flag_types
+ };
+}
+
+###################
+# Private Methods #
+###################
+
+sub _get_attachments {
+ my ($self, $params) = @_;
+ my $user = Bugzilla->user;
+
+ my $attachments = $self->attachments($params);
+
+ if ($params->{ids}) {
+ $attachments = [ map { @{ $attachments->{bugs}->{$_} } }
+ keys %{ $attachments->{bugs} } ];
+ }
+ elsif ($params->{attachment_ids}) {
+ $attachments = [ map { $attachments->{attachments}->{$_} }
+ keys %{ $attachments->{attachments} } ];
+ }
+
+ foreach my $attachment (@$attachments) {
+ $attachment->{can_edit}
+ = ($user->login eq $attachment->{creator} || $user->in_group('editbugs')) ? 1 : 0;
+ }
+
+ return $attachments;
+}
+
+sub _get_fields {
+ my ($self, $bug, $field_ids) = @_;
+ my $user = Bugzilla->user;
+
+ # Load the field objects we need
+ my @field_objs;
+ if ($field_ids) {
+ # Load just the fields that match the ids provided
+ @field_objs = @{ Bugzilla::Field->match({ id => $field_ids }) };
+
+ }
+ else {
+ # load up standard fields
+ @field_objs = @{ Bugzilla->fields({ custom => 0 }) };
+
+ # Load custom fields
+ my $cf_params = { product => $bug->product_obj };
+ $cf_params->{component} = $bug->component_obj if $bug->can('component_obj');
+ $cf_params->{bug_id} = $bug->id if $bug->id;
+ push(@field_objs, Bugzilla->active_custom_fields($cf_params));
+ }
+
+ my $return_groups = my $return_flags = $field_ids ? 0 : 1;
+ my @fields;
+ foreach my $field (@field_objs) {
+ $return_groups = 1 if $field->name eq 'bug_group';
+ $return_flags = 1 if $field->name eq 'flagtypes.name';
+
+ # Skip any special fields containing . in the name such as
+ # for attachments.*, etc.
+ next if $field->name =~ /\./;
+
+ # Remove time tracking fields if the user is privileged
+ next if (grep($field->name eq $_, TIMETRACKING_FIELDS)
+ && !Bugzilla->user->is_timetracker);
+
+ # These fields should never be set by the user
+ next if grep($field->name eq $_, NON_EDIT_FIELDS);
+
+ # We already selected a product so no need to display all choices
+ # Might as well skip classification for new bugs as well.
+ next if (!$bug->id && ($field->name eq 'product' || $field->name eq 'classification'));
+
+ # Skip assigned_to and qa_contact for new bugs if user not in
+ # editbugs group
+ next if (!$bug->id
+ && ($field->name eq 'assigned_to' || $field->name eq 'qa_contact')
+ && !$user->in_group('editbugs', $bug->product_obj->id));
+
+ # Do not display obsolete fields or fields that should be displayed for create bug form
+ next if (!$bug->id && $field->custom
+ && ($field->obsolete || !$field->enter_bug));
+
+ push(@fields, $self->_field_to_hash($field, $bug));
+ }
+
+ # Add group information as separate field
+ if ($return_groups) {
+ push(@fields, {
+ description => $self->type('string', 'Groups'),
+ is_custom => $self->type('boolean', 0),
+ is_mandatory => $self->type('boolean', 0),
+ name => $self->type('string', 'groups'),
+ values => [ map { $self->_group_to_hash($_, $bug) }
+ @{ $bug->product_obj->groups_available } ]
+ });
+ }
+
+ # Add flag information as separate field
+ if ($return_flags) {
+ my $flag_hash;
+ if ($bug->id) {
+ foreach my $flag_type ('bug', 'attachment') {
+ $flag_hash->{$flag_type} = $self->_get_flag_types_bug($bug, $flag_type);
+ }
+ }
+ else {
+ $flag_hash = $self->_get_flag_types_all($bug);
+ }
+ my @flag_values;
+ foreach my $flag_type ('bug', 'attachment') {
+ foreach my $flag (@{ $flag_hash->{$flag_type} }) {
+ push(@flag_values, $self->_flagtype_to_hash($flag, $bug));
+ }
+ }
+
+ push(@fields, {
+ description => $self->type('string', 'Flags'),
+ is_custom => $self->type('boolean', 0),
+ is_mandatory => $self->type('boolean', 0),
+ name => $self->type('string', 'flags'),
+ values => \@flag_values
+ });
+ }
+
+ return @fields;
+}
+
+sub _get_flag_types_all {
+ my ($self, $bug, $type) = @_;
+ my $params = { is_active => 1 };
+ $params->{target_type} = $type if $type;
+ return $bug->product_obj->flag_types($params);
+}
+
+sub _get_flag_types_bug {
+ my ($self, $bug, $type) = @_;
+ my $params = {
+ target_type => $type,
+ product_id => $bug->product_obj->id,
+ component_id => $bug->component_obj->id,
+ bug_id => $bug->id,
+ active_or_has_flags => $bug->id,
+ };
+ return Bugzilla::Flag->_flag_types($params);
+}
+
+sub _group_to_hash {
+ my ($self, $group, $bug) = @_;
+
+ my $data = {
+ description => $self->type('string', $group->description),
+ name => $self->type('string', $group->name)
+ };
+
+ if ($group->name eq $bug->product_obj->default_security_group) {
+ $data->{security_default} = $self->type('boolean', 1);
+ }
+
+ return $data;
+}
+
+sub _field_to_hash {
+ my ($self, $field, $bug) = @_;
+
+ my $data = {
+ is_custom => $self->type('boolean', $field->custom),
+ description => $self->type('string', $field->description),
+ is_mandatory => $self->type('boolean', $field->is_mandatory),
+ };
+
+ if ($field->custom) {
+ $data->{type} = $self->type('string', FIELD_TYPE_MAP->{$field->type});
+ }
+
+ # Use the API name if one is present instead of the internal field name
+ my $field_name = $field->name;
+ $field_name = API_NAMES->{$field_name} || $field_name;
+
+ if ($field_name eq 'longdesc') {
+ $field_name = $bug->id ? 'comment' : 'description';
+ }
+
+ $data->{name} = $self->type('string', $field_name);
+
+ # Set can_edit true or false if we are editing a current bug
+ if ($bug->id) {
+ # 'delta_ts's can_edit is incorrectly set in fielddefs
+ $data->{can_edit} = $field->name eq 'delta_ts'
+ ? $self->type('boolean', 0)
+ : $self->_can_change_field($field, $bug);
+ }
+
+ # description for creating a new bug, otherwise comment
+
+ # FIXME 'version' and 'target_milestone' types are incorrectly set in fielddefs
+ if ($field->is_select || $field->name eq 'version' || $field->name eq 'target_milestone') {
+ $data->{values} = [ $self->_get_field_values($field, $bug) ];
+ }
+
+ # Add default values for specific fields if new bug
+ if (!$bug->id && DEFAULT_VALUE_MAP->{$field->name}) {
+ my $default_value = Bugzilla->params->{DEFAULT_VALUE_MAP->{$field->name}};
+ $data->{default_value} = $default_value;
+ }
+
+ return $data;
+}
+
+sub _value_to_hash {
+ my ($self, $value, $bug) = @_;
+
+ my $data = { name=> $self->type('string', $value->name) };
+
+ if ($bug->{bug_id}) {
+ $data->{is_active} = $self->type('boolean', $value->is_active);
+ }
+
+ if ($value->can('sortkey')) {
+ $data->{sort_key} = $self->type('int', $value->sortkey || 0);
+ }
+
+ if ($value->isa('Bugzilla::Component')) {
+ $data->{default_assignee} = $self->_user_to_hash($value->default_assignee);
+ $data->{initial_cc} = [ map { $self->_user_to_hash($_) } @{ $value->initial_cc } ];
+ if (Bugzilla->params->{useqacontact} && $value->default_qa_contact) {
+ $data->{default_qa_contact} = $self->_user_to_hash($value->default_qa_contact);
+ }
+ }
+
+ if ($value->can('description')) {
+ $data->{description} = $self->type('string', $value->description);
+ }
+
+ return $data;
+}
+
+sub _user_to_hash {
+ my ($self, $user) = @_;
+
+ my $data = {
+ real_name => $self->type('string', $user->name)
+ };
+
+ if (Bugzilla->user->id) {
+ $data->{email} = $self->type('string', $user->email);
+ }
+
+ return $data;
+}
+
+sub _get_field_values {
+ my ($self, $field, $bug) = @_;
+
+ # Certain fields are special and should use $bug->choices
+ # to determine editability and not $bug->check_can_change_field
+ my @values;
+ if (grep($field->name eq $_, BUG_CHOICE_FIELDS)) {
+ @values = @{ $bug->choices->{$field->name} };
+ }
+ else {
+ # We need to get the values from the product for
+ # component, version, and milestones.
+ if ($field->name eq 'component') {
+ @values = @{ $bug->product_obj->components };
+ }
+ elsif ($field->name eq 'target_milestone') {
+ @values = @{ $bug->product_obj->milestones };
+ }
+ elsif ($field->name eq 'version') {
+ @values = @{ $bug->product_obj->versions };
+ }
+ else {
+ @values = @{ $field->legal_values };
+ }
+ }
+
+ my @filtered_values;
+ foreach my $value (@values) {
+ next if !$bug->id && !$value->is_active;
+ next if $bug->id && !$self->_can_change_field($field, $bug, $value->name);
+ push(@filtered_values, $value);
+ }
+
+ return map { $self->_value_to_hash($_, $bug) } @filtered_values;
+}
+
+sub _can_change_field {
+ my ($self, $field, $bug, $value) = @_;
+ my $user = Bugzilla->user;
+ my $field_name = blessed $field ? $field->name : $field;
+
+ # Cannot set resolution on bug creation
+ return $self->type('boolean', 0) if ($field_name eq 'resolution' && !$bug->{bug_id});
+
+ # Cannot edit an obsolete or inactive custom field
+ return $self->type('boolean', 0) if (blessed $field && $field->custom && $field->obsolete);
+
+ # If not a multi-select or single-select, value is not provided
+ # and we just check if the field itself is editable by the user.
+ if (!defined $value) {
+ return $self->type('boolean', $bug->check_can_change_field($field_name, 0, 1));
+ }
+
+ return $self->type('boolean', $bug->check_can_change_field($field_name, '', $value));
+}
+
+sub _flag_to_hash {
+ my ($self, $flag) = @_;
+
+ my $data = {
+ id => $self->type('int', $flag->id),
+ name => $self->type('string', $flag->name),
+ type_id => $self->type('int', $flag->type_id),
+ creation_date => $self->type('dateTime', $flag->creation_date),
+ modification_date => $self->type('dateTime', $flag->modification_date),
+ status => $self->type('string', $flag->status)
+ };
+
+ foreach my $field (qw(setter requestee)) {
+ my $field_id = $field . "_id";
+ $data->{$field} = $self->_user_to_hash($flag->$field) if $flag->$field_id;
+ }
+
+ $data->{type} = $flag->attach_id ? 'attachment' : 'bug';
+ $data->{attach_id} = $flag->attach_id if $flag->attach_id;
+
+ return $data;
+}
+
+sub _flagtype_to_hash {
+ my ($self, $flagtype, $bug) = @_;
+ my $user = Bugzilla->user;
+
+ my $cansetflag = $user->can_set_flag($flagtype);
+ my $canrequestflag = $user->can_request_flag($flagtype);
+
+ my $data = {
+ id => $self->type('int' , $flagtype->id),
+ name => $self->type('string' , $flagtype->name),
+ description => $self->type('string' , $flagtype->description),
+ type => $self->type('string' , $flagtype->target_type),
+ is_requestable => $self->type('boolean', $flagtype->is_requestable),
+ is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble),
+ is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable),
+ can_set_flag => $self->type('boolean', $cansetflag),
+ can_request_flag => $self->type('boolean', $canrequestflag)
+ };
+
+ my @values;
+ foreach my $value ('?','+','-') {
+ push(@values, $self->type('string', $value));
+ }
+ $data->{values} = \@values;
+
+ # if we're creating a bug, we need to return all valid flags for
+ # this product, as well as inclusions & exclusions so ember can
+ # display relevant flags once the component is selected
+ if (!$bug->id) {
+ my $inclusions = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $bug->product_obj->id);
+ my $exclusions = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $bug->product_obj->id);
+ # if we have both inclusions and exclusions, the exclusions are redundant
+ $exclusions = [] if @$inclusions && @$exclusions;
+ # no need to return anything if there's just "any component"
+ $data->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne '';
+ $data->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne '';
+ }
+
+ return $data;
+}
+
+sub _flagtype_clusions_to_hash {
+ my ($self, $clusions, $product_id) = @_;
+ my $result = [];
+ foreach my $key (keys %$clusions) {
+ my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2);
+ if ($prod_id == 0 || $prod_id == $product_id) {
+ if ($comp_id) {
+ my $component = Bugzilla::Component->new({ id => $comp_id, cache => 1 });
+ push @$result, $component->name;
+ }
+ else {
+ return [ '' ];
+ }
+ }
+ }
+ return $result;
+}
+
+sub rest_resources {
+ return [
+ # create page - single product name
+ qr{^/ember/create/(.*)$}, {
+ GET => {
+ method => 'create',
+ params => sub {
+ return { product => $_[0] };
+ }
+ }
+ },
+ # create page - one or more products
+ qr{^/ember/create$}, {
+ GET => {
+ method => 'create'
+ }
+ },
+ # show bug page - single bug id
+ qr{^/ember/show/(\d+)$}, {
+ GET => {
+ method => 'show',
+ params => sub {
+ return { id => $_[0] };
+ }
+ }
+ },
+ # search - wrapper around SUPER::search which also includes the total
+ # number of bugs when using pagination
+ qr{^/ember/search$}, {
+ GET => {
+ method => 'search',
+ },
+ },
+ # get current bug attributes without field information - single bug id
+ qr{^/ember/bug/(\d+)$}, {
+ GET => {
+ method => 'bug',
+ params => sub {
+ return { id => $_[0] };
+ }
+ }
+ },
+ # attachments - wrapper around SUPER::attachments that also includes
+ # can_edit attribute
+ qr{^/ember/bug/(\d+)/attachments$}, {
+ GET => {
+ method => 'get_attachments',
+ params => sub {
+ return { ids => $_[0] };
+ }
+ }
+ },
+ qr{^/ember/bug/attachments/(\d+)$}, {
+ GET => {
+ method => 'get_attachments',
+ params => sub {
+ return { attachment_ids => $_[0] };
+ }
+ }
+ }
+ ];
+};
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Extension::Ember::Webservice - The BMO Ember WebServices API
+
+=head1 DESCRIPTION
+
+This module contains API methods that are useful to user's of the Bugzilla Ember
+based UI.
+
+=head1 METHODS
+
+See L<Bugzilla::WebService> for a description of how parameters are passed,
+and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
+
+=head2 create
+
+B<UNSTABLE>
+
+=over
+
+=item B<Description>
+
+This method returns the necessary information for the Bugzilla Ember UI to generate a
+bug creation page.
+
+=item B<Params>
+
+You pass a field called C<product> that must be a valid Bugzilla product name.
+
+=over
+
+=item C<product> (string) - The Bugzilla product name.
+
+=back
+
+=item B<Returns>
+
+=over
+
+=back
+
+=item B<Errors>
+
+=over
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in BMO Bugzilla B<4.2>.
+
+=back
+
+=back
+
+=head2 show
+
+B<UNSTABLE>
+
+=over
+
+=item B<Description>
+
+This method returns the necessary information for the Bugzilla Ember UI to properly
+generate a page to edit current bugs.
+
+=item B<Params>
+
+You pass a field called C<id> that is the current bug id.
+
+=over
+
+=item C<id> (int) - A bug id.
+
+=back
+
+=item B<Returns>
+
+=over
+
+=back
+
+=item B<Errors>
+
+=over
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in BMO Bugzilla B<4.0>.
+
+=back
+
+=back
+
+=head2 search
+
+B<UNSTABLE>
+
+=over
+
+=item B<Description>
+
+A wrapper around Bugzilla's C<search> method which also returns the total of
+bugs matching a query, even if the limit and offset parameters are supplied.
+
+=item B<Params>
+
+As per Bugzilla::WebService::Bug::search()
+
+=item B<Returns>
+
+=over
+
+=back
+
+=item B<Errors>
+
+=over
+
+=back
+
+=item B<History>
+
+=over
+
+=back
+
+=back
+
+=head2 bug
+
+B<UNSTABLE>
+
+=over
+
+=item B<Description>
+
+This method returns just the current bug values, comments, and attachments without
+all of the field information.
+
+=item B<Params>
+
+You pass a field called C<id> that is a valid bug ids.
+
+=over
+
+=item C<id> (integer) - A valid bug id
+
+=item C<last_updated> - (dateTime) An optional timestamp that includes only fields,
+attachments, or comments that have been changed or added since.
+
+=back
+
+=item B<Returns>
+
+=over
+
+=back
+
+=item B<Errors>
+
+=over
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in BMO Bugzilla B<4.2>.
+
+=back
+
+=back
+
+=head2 get_attachments
+
+B<UNSTABLE>
+
+=over
+
+=item B<Description>
+
+This method returns the current attachment data and flag types for a given
+bug id or attachment id.
+
+=item B<Params>
+
+You pass a field called C<id> that is a valid bug id or an C<attachment_id> which
+is a valid attachment id.
+
+=over
+
+=item C<id> (integer) - A valid bug id.
+
+=item C<attachment_id> (integer) - A valid attachment id.
+
+=back
+
+=item B<Returns>
+
+=over
+
+=back
+
+=item B<Errors>
+
+=over
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in BMO Bugzilla B<4.2>.
+
+=back
+
+=back
diff --git a/extensions/Ember/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/Ember/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..c438af283
--- /dev/null
+++ b/extensions/Ember/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,4 @@
+[% IF error == "invalid_token" %]
+ [% title = "Invalid Token Provided" %]
+ The token provided is either invalid or expired. You must log in again.
+[% END %]
diff --git a/extensions/Example/Extension.pm b/extensions/Example/Extension.pm
index c0b3c6210..a42f87b9e 100644
--- a/extensions/Example/Extension.pm
+++ b/extensions/Example/Extension.pm
@@ -44,6 +44,20 @@ use constant REL_EXAMPLE => -127;
our $VERSION = '1.0';
+sub admin_editusers_action {
+ my ($self, $args) = @_;
+ my ($vars, $action, $user) = @$args{qw(vars action user)};
+ my $template = Bugzilla->template;
+
+ if ($action eq 'my_action') {
+ # Allow to restrict the search to any group the user is allowed to bless.
+ $vars->{'restrictablegroups'} = $user->bless_groups();
+ $template->process('admin/users/search.html.tmpl', $vars)
+ || ThrowTemplateError($template->error());
+ exit;
+ }
+}
+
sub attachment_process_data {
my ($self, $args) = @_;
my $type = $args->{attributes}->{mimetype};
@@ -80,6 +94,44 @@ sub auth_verify_methods {
}
}
+sub bug_check_can_change_field {
+ my ($self, $args) = @_;
+
+ my ($bug, $field, $new_value, $old_value, $priv_results)
+ = @$args{qw(bug field new_value old_value priv_results)};
+
+ my $user = Bugzilla->user;
+
+ # Disallow a bug from being reopened if currently closed unless user
+ # is in 'admin' group
+ if ($field eq 'bug_status' && $bug->product_obj->name eq 'Example') {
+ if (!is_open_state($old_value) && is_open_state($new_value)
+ && !$user->in_group('admin'))
+ {
+ push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ return;
+ }
+ }
+
+ # Disallow a bug's keywords from being edited unless user is the
+ # reporter of the bug
+ if ($field eq 'keywords' && $bug->product_obj->name eq 'Example'
+ && $user->login ne $bug->reporter->login)
+ {
+ push(@$priv_results, PRIVILEGES_REQUIRED_REPORTER);
+ return;
+ }
+
+ # Allow updating of priority even if user cannot normally edit the bug
+ # and they are in group 'engineering'
+ if ($field eq 'priority' && $bug->product_obj->name eq 'Example'
+ && $user->in_group('engineering'))
+ {
+ push(@$priv_results, PRIVILEGES_REQUIRED_NONE);
+ return;
+ }
+}
+
sub bug_columns {
my ($self, $args) = @_;
my $columns = $args->{'columns'};
@@ -116,6 +168,42 @@ sub bug_end_of_create_validators {
# $bug_params->{cc} = [];
}
+sub bug_start_of_update {
+ my ($self, $args) = @_;
+
+ # This code doesn't actually *do* anything, it's just here to show you
+ # how to use this hook.
+ my ($bug, $old_bug, $timestamp, $changes) =
+ @$args{qw(bug old_bug timestamp changes)};
+
+ foreach my $field (keys %$changes) {
+ my $used_to_be = $changes->{$field}->[0];
+ my $now_it_is = $changes->{$field}->[1];
+ }
+
+ my $old_summary = $old_bug->short_desc;
+
+ my $status_message;
+ if (my $status_change = $changes->{'bug_status'}) {
+ my $old_status = new Bugzilla::Status({ name => $status_change->[0] });
+ my $new_status = new Bugzilla::Status({ name => $status_change->[1] });
+ if ($new_status->is_open && !$old_status->is_open) {
+ $status_message = "Bug re-opened!";
+ }
+ if (!$new_status->is_open && $old_status->is_open) {
+ $status_message = "Bug closed!";
+ }
+ }
+
+ my $bug_id = $bug->id;
+ my $num_changes = scalar keys %$changes;
+ my $result = "There were $num_changes changes to fields on bug $bug_id"
+ . " at $timestamp.";
+ # Uncomment this line to see $result in your webserver's error log whenever
+ # you update a bug.
+ # warn $result;
+}
+
sub bug_end_of_update {
my ($self, $args) = @_;
@@ -678,10 +766,12 @@ sub _check_short_desc {
my $invocant = shift;
my $value = $invocant->$original(@_);
if ($value !~ /example/i) {
- # Uncomment this line to make Bugzilla throw an error every time
+ # Use this line to make Bugzilla throw an error every time
# you try to file a bug or update a bug without the word "example"
# in the summary.
- #ThrowUserError('example_short_desc_invalid');
+ if (0) {
+ ThrowUserError('example_short_desc_invalid');
+ }
}
return $value;
}
@@ -697,6 +787,12 @@ sub page_before_template {
}
}
+sub path_info_whitelist {
+ my ($self, $args) = @_;
+ my $whitelist = $args->{whitelist};
+ push(@$whitelist, "page.cgi");
+}
+
sub post_bug_after_creation {
my ($self, $args) = @_;
@@ -825,58 +921,6 @@ sub template_before_process {
}
}
-sub bug_check_can_change_field {
- my ($self, $args) = @_;
-
- my ($bug, $field, $new_value, $old_value, $priv_results)
- = @$args{qw(bug field new_value old_value priv_results)};
-
- my $user = Bugzilla->user;
-
- # Disallow a bug from being reopened if currently closed unless user
- # is in 'admin' group
- if ($field eq 'bug_status' && $bug->product_obj->name eq 'Example') {
- if (!is_open_state($old_value) && is_open_state($new_value)
- && !$user->in_group('admin'))
- {
- push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
- return;
- }
- }
-
- # Disallow a bug's keywords from being edited unless user is the
- # reporter of the bug
- if ($field eq 'keywords' && $bug->product_obj->name eq 'Example'
- && $user->login ne $bug->reporter->login)
- {
- push(@$priv_results, PRIVILEGES_REQUIRED_REPORTER);
- return;
- }
-
- # Allow updating of priority even if user cannot normally edit the bug
- # and they are in group 'engineering'
- if ($field eq 'priority' && $bug->product_obj->name eq 'Example'
- && $user->in_group('engineering'))
- {
- push(@$priv_results, PRIVILEGES_REQUIRED_NONE);
- return;
- }
-}
-
-sub admin_editusers_action {
- my ($self, $args) = @_;
- my ($vars, $action, $user) = @$args{qw(vars action user)};
- my $template = Bugzilla->template;
-
- if ($action eq 'my_action') {
- # Allow to restrict the search to any group the user is allowed to bless.
- $vars->{'restrictablegroups'} = $user->bless_groups();
- $template->process('admin/users/search.html.tmpl', $vars)
- || ThrowTemplateError($template->error());
- exit;
- }
-}
-
sub user_preferences {
my ($self, $args) = @_;
my $tab = $args->{current_tab};
@@ -911,5 +955,70 @@ sub webservice_error_codes {
$error_map->{'example_my_error'} = 10001;
}
+sub webservice_before_call {
+ my ($self, $args) = @_;
+
+ # This code doesn't actually *do* anything, it's just here to show you
+ # how to use this hook.
+ my $method = $args->{method};
+ my $full_method = $args->{full_method};
+
+ # Uncomment this line to see a line in your webserver's error log whenever
+ # a webservice call is made
+ #warn "RPC call $full_method made by ", Bugzilla->user->login, "\n";
+}
+
+sub webservice_fix_credentials {
+ my ($self, $args) = @_;
+ my $rpc = $args->{'rpc'};
+ my $params = $args->{'params'};
+ # Allow user to pass in username=foo&password=bar
+ if (exists $params->{'username'} && exists $params->{'password'}) {
+ $params->{'Bugzilla_login'} = $params->{'username'};
+ $params->{'Bugzilla_password'} = $params->{'password'};
+ }
+}
+
+sub webservice_rest_request {
+ my ($self, $args) = @_;
+ my $rpc = $args->{'rpc'};
+ my $params = $args->{'params'};
+ # Internally we may have a field called 'cf_test_field' but we allow users
+ # to use the shorter 'test_field' name.
+ if (exists $params->{'test_field'}) {
+ $params->{'test_field'} = delete $params->{'cf_test_field'};
+ }
+}
+
+sub webservice_rest_resources {
+ my ($self, $args) = @_;
+ my $rpc = $args->{'rpc'};
+ my $resources = $args->{'resources'};
+ # Add a new resource that allows for /rest/example/hello
+ # to call Example.hello
+ $resources->{'Bugzilla::Extension::Example::WebService'} = [
+ qr{^/example/hello$}, {
+ GET => {
+ method => 'hello',
+ }
+ }
+ ];
+}
+
+sub webservice_rest_response {
+ my ($self, $args) = @_;
+ my $rpc = $args->{'rpc'};
+ my $result = $args->{'result'};
+ my $response = $args->{'response'};
+ # Convert a list of bug hashes to a single bug hash if only one is
+ # being returned.
+ if (ref $$result eq 'HASH'
+ && exists $$result->{'bugs'}
+ && scalar @{ $$result->{'bugs'} } == 1)
+ {
+ $$result = $$result->{'bugs'}->[0];
+ }
+}
+
# This must be the last line of your extension.
__PACKAGE__->NAME;
diff --git a/extensions/FlagDefaultRequestee/Config.pm b/extensions/FlagDefaultRequestee/Config.pm
new file mode 100644
index 000000000..70c5ca33a
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/Config.pm
@@ -0,0 +1,17 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::FlagDefaultRequestee;
+
+use strict;
+
+use constant NAME => 'FlagDefaultRequestee';
+
+use constant REQUIRED_MODULES => [];
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/FlagDefaultRequestee/Extension.pm b/extensions/FlagDefaultRequestee/Extension.pm
new file mode 100644
index 000000000..958a1bb85
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/Extension.pm
@@ -0,0 +1,173 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::FlagDefaultRequestee;
+
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Error;
+use Bugzilla::FlagType;
+use Bugzilla::User;
+use Bugzilla::Util 'trim';
+
+use Bugzilla::Extension::FlagDefaultRequestee::Constants;
+
+our $VERSION = '1';
+
+################
+# Installation #
+################
+
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_add_column('flagtypes', 'default_requestee', {
+ TYPE => 'INT3',
+ NOTNULL => 0,
+ REFERENCES => { TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'SET NULL' }
+ });
+}
+
+#############
+# Templates #
+#############
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ return unless Bugzilla->user->id;
+ my ($vars, $file) = @$args{qw(vars file)};
+ return unless grep { $_ eq $file } FLAGTYPE_TEMPLATES;
+
+ my $flag_types = [];
+ if (exists $vars->{bug} || exists $vars->{attachment}) {
+ my $bug;
+ if (exists $vars->{bug}) {
+ $bug = $vars->{'bug'};
+ } elsif (exists $vars->{'attachment'}) {
+ $bug = $vars->{'attachment'}->{bug};
+ }
+
+ $flag_types = Bugzilla::FlagType::match({
+ 'target_type' => ($file =~ /^bug/ ? 'bug' : 'attachment'),
+ 'product_id' => $bug->product_id,
+ 'component_id' => $bug->component_id,
+ 'bug_id' => $bug->id,
+ 'active_or_has_flags' => $bug->id,
+ });
+
+ $vars->{flag_currently_requested} ||= {};
+ foreach my $type (@$flag_types) {
+ my $flags = Bugzilla::Flag->match({
+ type_id => $type->id,
+ bug_id => $bug->id,
+ status => '?'
+ });
+ map { $vars->{flag_currently_requested}->{$_->id} = 1 } @$flags;
+ }
+ }
+ elsif ($file =~ /^bug\/create/ && exists $vars->{product}) {
+ my $bug_flags = $vars->{product}->flag_types->{bug};
+ my $attachment_flags = $vars->{product}->flag_types->{attachment};
+ $flag_types = [ map { $_ } (@$bug_flags, @$attachment_flags) ];
+ }
+
+ return if !@$flag_types;
+
+ $vars->{flag_default_requestees} ||= {};
+ foreach my $type (@$flag_types) {
+ next if !$type->default_requestee;
+ $vars->{flag_default_requestees}->{$type->id} = $type->default_requestee->login;
+ }
+}
+
+##################
+# Object Methods #
+##################
+
+BEGIN {
+ *Bugzilla::FlagType::default_requestee = \&_default_requestee;
+}
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my ($class, $columns) = @$args{qw(class columns)};
+ if ($class->isa('Bugzilla::FlagType')) {
+ push(@$columns, 'default_requestee');
+ }
+}
+
+sub object_update_columns {
+ my ($self, $args) = @_;
+ my $object = $args->{object};
+ return unless $object->isa('Bugzilla::FlagType');
+
+ my $columns = $args->{columns};
+ push(@$columns, 'default_requestee');
+
+ # editflagtypes.cgi doesn't call set_all, so we have to do this here
+ my $input = Bugzilla->input_params;
+ $object->set('default_requestee', $input->{default_requestee})
+ if exists $input->{default_requestee};
+}
+
+sub object_validators {
+ my ($self, $args) = @_;
+ my $class = $args->{class};
+ return unless $class->isa('Bugzilla::FlagType');
+
+ my $validators = $args->{validators};
+ $validators->{default_requestee} = \&_check_default_requestee;
+}
+
+sub object_before_create {
+ my ($self, $args) = @_;
+ my $class = $args->{class};
+ return unless $class->isa('Bugzilla::FlagType');
+
+ my $params = $args->{params};
+ my $input = Bugzilla->input_params;
+ $params->{default_requestee} = $input->{default_requestee}
+ if exists $params->{default_requestee};
+}
+
+sub object_end_of_update {
+ my ($self, $args) = @_;
+ my $object = $args->{object};
+ return unless $object->isa('Bugzilla::FlagType');
+
+ my $old_object = $args->{old_object};
+ my $changes = $args->{changes};
+ my $old_id = $old_object->default_requestee
+ ? $old_object->default_requestee->id
+ : 0;
+ my $new_id = $object->default_requestee
+ ? $object->default_requestee->id
+ : 0;
+ return if $old_id == $new_id;
+
+ $changes->{default_requestee} = [ $old_id, $new_id ];
+}
+
+sub _check_default_requestee {
+ my ($self, $value, $field) = @_;
+ $value = trim($value // '');
+ return undef if $value eq '';
+ ThrowUserError("flag_default_requestee_review")
+ if $self->name eq 'review';
+ return Bugzilla::User->check($value)->id;
+}
+
+sub _default_requestee {
+ my ($self) = @_;
+ return $self->{default_requestee}
+ ? Bugzilla::User->new({ id => $self->{default_requestee}, cache => 1 })
+ : undef;
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/FlagDefaultRequestee/lib/Constants.pm b/extensions/FlagDefaultRequestee/lib/Constants.pm
new file mode 100644
index 000000000..467028423
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/lib/Constants.pm
@@ -0,0 +1,25 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::FlagDefaultRequestee::Constants;
+
+use strict;
+
+use base qw(Exporter);
+
+our @EXPORT = qw(
+ FLAGTYPE_TEMPLATES
+);
+
+use constant FLAGTYPE_TEMPLATES => (
+ "attachment/edit.html.tmpl",
+ "attachment/createformcontents.html.tmpl",
+ "bug/edit.html.tmpl",
+ "bug/create/create.html.tmpl"
+);
+
+1;
diff --git a/extensions/FlagDefaultRequestee/template/en/default/flag/default_requestees.html.tmpl b/extensions/FlagDefaultRequestee/template/en/default/flag/default_requestees.html.tmpl
new file mode 100644
index 000000000..db728c168
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/template/en/default/flag/default_requestees.html.tmpl
@@ -0,0 +1,105 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF flag_default_requestees.keys.size %]
+ <script type="text/javascript">
+ var currently_requested = new Array();
+ var default_requestees = new Array();
+ [% FOREACH id = flag_currently_requested.keys %]
+ currently_requested.push('[% id FILTER js %]');
+ [% END %]
+ [% FOREACH id = flag_default_requestees.keys %]
+ default_requestees['id_[% id FILTER js %]'] = '[% flag_default_requestees.$id FILTER js %]';
+ [% END %]
+
+ function fdrSetDefaultRequestee(field, default_requestee) {
+ field.value = default_requestee;
+ field.focus();
+ field.select();
+ }
+
+ function fdrOnChange(ev) {
+ var parts = ev.target.id.split('-');
+ var flag = parts[0];
+ var id = parts[1];
+ var state = ev.target.value;
+ var requestee_field;
+
+ if (flag.search(/_type/) == -1) {
+ for (var i = 0; i < currently_requested.length; i++) {
+ if (id == currently_requested[i]) {
+ return;
+ }
+ }
+ requestee_field = YAHOO.util.Dom.get('requestee-' + id);
+ parts = ev.target.className.split('-');
+ id = parts[1];
+ }
+ else {
+ requestee_field = YAHOO.util.Dom.get('requestee_type-' + id);
+ }
+ if (!requestee_field) return;
+
+ var current_requestee = requestee_field.value;
+ var default_requestee = default_requestees['id_' + id];
+ if (!default_requestee) return;
+
+ if (state == '?' && !current_requestee && default_requestee) {
+ fdrSetDefaultRequestee(requestee_field, default_requestees['id_' + id]);
+ }
+ else if (state == '?' && current_requestee != default_requestee) {
+ fdrShowDefaultLink(requestee_field, id);
+ }
+ }
+
+ YAHOO.util.Event.onDOMReady(function() {
+ var selects = YAHOO.util.Dom.getElementsByClassName('flag_select');
+ for (var i = 0; i < selects.length; i++) {
+ YAHOO.util.Event.on(selects[i], 'change', fdrOnChange);
+ }
+
+ for (var i = 0; i < currently_requested.length; i++) {
+ var flag_id = currently_requested[i];
+ var flag_field = YAHOO.util.Dom.get('flag-' + flag_id);
+ var requestee_field = YAHOO.util.Dom.get('requestee-' + flag_id);
+ if (!requestee_field) continue;
+ var parts = flag_field.className.split('-');
+ var type_id = parts[1];
+ var current_requestee = requestee_field.value;
+ var default_requestee = default_requestees['id_' + type_id];
+ if (!default_requestee) continue;
+ if (current_requestee != default_requestee) {
+ fdrShowDefaultLink(requestee_field, type_id, flag_id);
+ }
+ }
+ });
+
+ function fdrHideDefaultLink (flag_id) {
+ YAHOO.util.Dom.addClass('default_requestee_' + flag_id, 'bz_default_hidden');
+ }
+
+ function fdrShowDefaultLink (requestee_field, type_id, flag_id) {
+ var default_requestee = default_requestees['id_' + type_id];
+
+ var default_link = document.createElement('a');
+ YAHOO.util.Dom.setAttribute(default_link, 'href', 'javascript:void(0)');
+ default_link.appendChild(document.createTextNode('default requestee'));
+ YAHOO.util.Event.addListener(default_link, 'click', function() {
+ fdrSetDefaultRequestee(requestee_field, default_requestee);
+ fdrHideDefaultLink(flag_id);
+ });
+
+ var default_span = document.createElement('span');
+ YAHOO.util.Dom.setAttribute(default_span, 'id', 'default_requestee_' + flag_id);
+ default_span.appendChild(document.createTextNode("\u00a0("));
+ default_span.appendChild(default_link);
+ default_span.appendChild(document.createTextNode(')'));
+ requestee_field.parentNode.parentNode.appendChild(default_span);
+ }
+ </script>
+[% END %]
diff --git a/extensions/FlagDefaultRequestee/template/en/default/hook/admin/flag-type/edit-rows.html.tmpl b/extensions/FlagDefaultRequestee/template/en/default/hook/admin/flag-type/edit-rows.html.tmpl
new file mode 100644
index 000000000..edefca370
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/template/en/default/hook/admin/flag-type/edit-rows.html.tmpl
@@ -0,0 +1,21 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<tr>
+ <th>Default Requestee:</th>
+ <td>
+ If flag is specifically requestable, this user will be entered in the
+ requestee field by default unless the user changes it.<br>
+ [% INCLUDE global/userselect.html.tmpl
+ name => 'default_requestee'
+ id => 'default_requestee'
+ value => type.default_requestee.login
+ classes => ['requestee']
+ %]
+ </td>
+</tr>
diff --git a/extensions/FlagDefaultRequestee/template/en/default/hook/attachment/create-end.html.tmpl b/extensions/FlagDefaultRequestee/template/en/default/hook/attachment/create-end.html.tmpl
new file mode 100644
index 000000000..20b2526d0
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/template/en/default/hook/attachment/create-end.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE flag/default_requestees.html.tmpl %]
diff --git a/extensions/FlagDefaultRequestee/template/en/default/hook/attachment/edit-end.html.tmpl b/extensions/FlagDefaultRequestee/template/en/default/hook/attachment/edit-end.html.tmpl
new file mode 100644
index 000000000..20b2526d0
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/template/en/default/hook/attachment/edit-end.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE flag/default_requestees.html.tmpl %]
diff --git a/extensions/FlagDefaultRequestee/template/en/default/hook/bug/create/create-form.html.tmpl b/extensions/FlagDefaultRequestee/template/en/default/hook/bug/create/create-form.html.tmpl
new file mode 100644
index 000000000..20b2526d0
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/template/en/default/hook/bug/create/create-form.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE flag/default_requestees.html.tmpl %]
diff --git a/extensions/FlagDefaultRequestee/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl b/extensions/FlagDefaultRequestee/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
new file mode 100644
index 000000000..20b2526d0
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE flag/default_requestees.html.tmpl %]
diff --git a/extensions/FlagDefaultRequestee/template/en/default/hook/global/messages-flag_type_updated_fields.html.tmpl b/extensions/FlagDefaultRequestee/template/en/default/hook/global/messages-flag_type_updated_fields.html.tmpl
new file mode 100644
index 000000000..478718d3c
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/template/en/default/hook/global/messages-flag_type_updated_fields.html.tmpl
@@ -0,0 +1,15 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF changes.default_requestee.defined %]
+ [% IF flagtype.default_requestee %]
+ <li>Default requestee updated to '[% flagtype.default_requestee.login FILTER html %]'</li>
+ [% ELSE %]
+ <li>Default requestee deleted</li>
+ [% END %]
+[% END %]
diff --git a/extensions/FlagDefaultRequestee/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/FlagDefaultRequestee/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..3fbd0458c
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "flag_default_requestee_review" %]
+ [% title = "Review flag not supported" %]
+ You cannot use the 'Default Requestee' field for the review flag.
+ Instead set the 'Suggested Reviewers' on the Product or Component edit forms.
+[% END %]
diff --git a/extensions/FlagTypeComment/Config.pm b/extensions/FlagTypeComment/Config.pm
new file mode 100644
index 000000000..e20be10e3
--- /dev/null
+++ b/extensions/FlagTypeComment/Config.pm
@@ -0,0 +1,29 @@
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the FlagTypeComment Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Alex Keybl
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Alex Keybl <akeybl@mozilla.com>
+# byron jones <glob@mozilla.com>
+
+package Bugzilla::Extension::FlagTypeComment;
+use strict;
+
+use constant NAME => 'FlagTypeComment';
+
+use constant REQUIRED_MODULES => [];
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/FlagTypeComment/Extension.pm b/extensions/FlagTypeComment/Extension.pm
new file mode 100644
index 000000000..8da6101ad
--- /dev/null
+++ b/extensions/FlagTypeComment/Extension.pm
@@ -0,0 +1,200 @@
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the FlagTypeComment Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Alex Keybl
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Alex Keybl <akeybl@mozilla.com>
+# byron jones <glob@mozilla.com>
+
+package Bugzilla::Extension::FlagTypeComment;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Extension::FlagTypeComment::Constants;
+
+use Bugzilla::FlagType;
+use Bugzilla::Util qw(trick_taint);
+use Scalar::Util qw(blessed);
+
+our $VERSION = '1';
+
+################
+# Installation #
+################
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'flagtype_comments'} = {
+ FIELDS => [
+ type_id => {
+ TYPE => 'SMALLINT(6)',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'flagtypes',
+ COLUMN => 'id',
+ DELETE => 'CASCADE'
+ }
+ },
+ on_status => {
+ TYPE => 'CHAR(1)',
+ NOTNULL => 1
+ },
+ comment => {
+ TYPE => 'MEDIUMTEXT',
+ NOTNULL => 1
+ },
+ ],
+ INDEXES => [
+ flagtype_comments_idx => ['type_id'],
+ ],
+ };
+}
+
+#############
+# Templates #
+#############
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my ($vars, $file) = @$args{qw(vars file)};
+
+ return unless Bugzilla->user->id;
+ if (grep { $_ eq $file } FLAGTYPE_COMMENT_TEMPLATES) {
+ _set_ftc_states($file, $vars);
+ }
+}
+
+sub _set_ftc_states {
+ my ($file, $vars) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $ftc_flags;
+ my $db_result;
+ if ($file =~ /^admin\//) {
+ # admin
+ my $type = $vars->{'type'} || return;
+ my ($target_type, $id);
+ if (blessed($type)) {
+ ($target_type, $id) = ($type->target_type, $type->id);
+ } else {
+ ($target_type, $id) = ($type->{target_type}, $type->{id});
+ trick_taint($id) if $id;
+ }
+ if ($target_type eq 'bug') {
+ return unless FLAGTYPE_COMMENT_BUG_FLAGS;
+ } else {
+ return unless FLAGTYPE_COMMENT_ATTACHMENT_FLAGS;
+ }
+ if ($id) {
+ $db_result = $dbh->selectall_arrayref(
+ "SELECT type_id AS flagtype, on_status AS state, comment AS text
+ FROM flagtype_comments
+ WHERE type_id = ?",
+ { Slice => {} }, $id);
+ }
+ } else {
+ # creating/editing attachment / viewing bug
+ my $bug;
+ if (exists $vars->{'bug'}) {
+ $bug = $vars->{'bug'};
+ } elsif (exists $vars->{'attachment'}) {
+ $bug = $vars->{'attachment'}->{bug};
+ } else {
+ return;
+ }
+
+ my $flag_types = Bugzilla::FlagType::match({
+ 'target_type' => ($file =~ /^bug/ ? 'bug' : 'attachment'),
+ 'product_id' => $bug->product_id,
+ 'component_id' => $bug->component_id,
+ 'bug_id' => $bug->id,
+ 'active_or_has_flags' => $bug->id,
+ });
+
+ my $types = join(',', map { $_->id } @$flag_types);
+ my $states = "'" . join("','", FLAGTYPE_COMMENT_STATES) . "'";
+ $db_result = $dbh->selectall_arrayref(
+ "SELECT type_id AS flagtype, on_status AS state, comment AS text
+ FROM flagtype_comments
+ WHERE type_id IN ($types) AND on_status IN ($states)",
+ { Slice => {} });
+ }
+
+ foreach my $row (@$db_result) {
+ $ftc_flags->{$row->{'flagtype'}} ||= {};
+ $ftc_flags->{$row->{'flagtype'}}{$row->{'state'}} = $row->{text};
+ }
+
+ $vars->{'ftc_states'} = [ FLAGTYPE_COMMENT_STATES ];
+ $vars->{'ftc_flags'} = $ftc_flags;
+}
+
+#########
+# Admin #
+#########
+
+sub flagtype_end_of_create {
+ my ($self, $args) = @_;
+ _set_flagtypes($args->{type});
+}
+
+sub flagtype_end_of_update {
+ my ($self, $args) = @_;
+ _set_flagtypes($args->{type});
+}
+
+sub _set_flagtypes {
+ my $flag_type = shift;
+ my $flagtype_id = $flag_type->id;
+ my $input = Bugzilla->input_params;
+ my $dbh = Bugzilla->dbh;
+
+ foreach my $state (FLAGTYPE_COMMENT_STATES) {
+ next if (!defined $input->{"ftc_${flagtype_id}_$state"}
+ && !defined $input->{"ftc_new_$state"});
+
+ my $text = $input->{"ftc_${flagtype_id}_$state"} || $input->{"ftc_new_$state"} || '';
+ $text =~ s/\r\n/\n/g;
+ trick_taint($text);
+
+ if ($text ne '') {
+ if ($dbh->selectrow_array(
+ "SELECT 1 FROM flagtype_comments WHERE type_id=? AND on_status=?",
+ undef,
+ $flagtype_id, $state)
+ ) {
+ $dbh->do(
+ "UPDATE flagtype_comments SET comment=?
+ WHERE type_id=? AND on_status=?",
+ undef,
+ $text, $flagtype_id, $state);
+ } else {
+ $dbh->do(
+ "INSERT INTO flagtype_comments(type_id, on_status, comment)
+ VALUES (?, ?, ?)",
+ undef,
+ $flagtype_id, $state, $text);
+ }
+
+ } else {
+ $dbh->do(
+ "DELETE FROM flagtype_comments WHERE type_id=? AND on_status=?",
+ undef,
+ $flagtype_id, $state);
+ }
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/FlagTypeComment/lib/Constants.pm b/extensions/FlagTypeComment/lib/Constants.pm
new file mode 100644
index 000000000..e1a99e5b3
--- /dev/null
+++ b/extensions/FlagTypeComment/lib/Constants.pm
@@ -0,0 +1,50 @@
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the FlagTypeComment Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Alex Keybl
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Alex Keybl <akeybl@mozilla.com>
+# byron jones <glob@mozilla.com>
+
+package Bugzilla::Extension::FlagTypeComment::Constants;
+use strict;
+
+use base qw(Exporter);
+our @EXPORT = qw(
+ FLAGTYPE_COMMENT_TEMPLATES
+ FLAGTYPE_COMMENT_STATES
+ FLAGTYPE_COMMENT_BUG_FLAGS
+ FLAGTYPE_COMMENT_ATTACHMENT_FLAGS
+);
+
+use constant FLAGTYPE_COMMENT_STATES => ("?", "+", "-");
+use constant FLAGTYPE_COMMENT_BUG_FLAGS => 0;
+use constant FLAGTYPE_COMMENT_ATTACHMENT_FLAGS => 1;
+
+sub FLAGTYPE_COMMENT_TEMPLATES {
+ my @result = ("admin/flag-type/edit.html.tmpl");
+ if (FLAGTYPE_COMMENT_BUG_FLAGS) {
+ push @result, ("bug/comments.html.tmpl");
+ }
+ if (FLAGTYPE_COMMENT_ATTACHMENT_FLAGS) {
+ push @result, (
+ "attachment/edit.html.tmpl",
+ "attachment/createformcontents.html.tmpl",
+ );
+ }
+ return @result;
+}
+
+1;
diff --git a/extensions/FlagTypeComment/template/en/default/flag/type_comment.html.tmpl b/extensions/FlagTypeComment/template/en/default/flag/type_comment.html.tmpl
new file mode 100644
index 000000000..95c0cb283
--- /dev/null
+++ b/extensions/FlagTypeComment/template/en/default/flag/type_comment.html.tmpl
@@ -0,0 +1,54 @@
+[%# The contents of this file are subject to the Mozilla Public License Version
+ # 1.1 (the "License"); you may not use this file except in compliance with
+ # the License. You may obtain a copy of the License at
+ # http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS IS" basis,
+ # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ # for the specific language governing rights and limitations under the
+ # License.
+ #
+ # The Original Code is FlagTypeComment Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is
+ # the Mozilla Foundation.
+ # Portions created by the Initial Developer are Copyright (C) 2011
+ # the Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Alex Keybl <akeybl@mozilla.com>
+ # byron jones <glob@mozilla.com>
+ #%]
+
+[% IF ftc_flags.keys.size %]
+ <script type="text/javascript">
+ YAHOO.util.Event.onDOMReady(function() {
+ var selects = YAHOO.util.Dom.getElementsByClassName('flag_select');
+ for (var i = 0; i < selects.length; i++) {
+ YAHOO.util.Event.on(selects[i], 'change', ftc_on_change);
+ }
+ });
+
+ function ftc_on_change(ev) {
+ var id = ev.target.id.split('-')[1];
+ var state = ev.target.value;
+ var commentEl = document.getElementById('comment');
+ if (!commentEl) return;
+ [% FOREACH type_id = ftc_flags.keys %]
+ [% FOREACH state = ftc_states %]
+ if ([% type_id FILTER none %] == id && '[% state FILTER js %]' == state) {
+ var text = '[% ftc_flags.$type_id.$state FILTER js %]';
+ var value = commentEl.value;
+ if (value == text) {
+ return;
+ } else if (value == '') {
+ commentEl.value = text;
+ } else {
+ commentEl.value = text + "\n\n" + value;
+ }
+ }
+ [% END %]
+ [% END %]
+ }
+ </script>
+[% END %]
diff --git a/extensions/FlagTypeComment/template/en/default/hook/admin/flag-type/edit-rows.html.tmpl b/extensions/FlagTypeComment/template/en/default/hook/admin/flag-type/edit-rows.html.tmpl
new file mode 100644
index 000000000..3ca5e8aa7
--- /dev/null
+++ b/extensions/FlagTypeComment/template/en/default/hook/admin/flag-type/edit-rows.html.tmpl
@@ -0,0 +1,45 @@
+[%# The contents of this file are subject to the Mozilla Public License Version
+ # 1.1 (the "License"); you may not use this file except in compliance with
+ # the License. You may obtain a copy of the License at
+ # http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS IS" basis,
+ # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ # for the specific language governing rights and limitations under the
+ # License.
+ #
+ # The Original Code is FlagTypeComment Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is
+ # the Mozilla Foundation.
+ # Portions created by the Initial Developer are Copyright (C) 2011
+ # the Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Alex Keybl <akeybl@mozilla.com>
+ # byron jones <glob@mozilla.com>
+ #%]
+
+[% IF ftc_states %]
+ <tr>
+ <th>Flag Comments:</th>
+ <td>add text into the comment box when flag is changed to a state</td>
+ </tr>
+
+ [% FOREACH state = ftc_states %]
+ [% ftc_type_id = "ftc_${type.id}_$state" %]
+ [% IF action == 'insert' %]
+ [% ftc_type_id = "ftc_new_$state" %]
+ [% END %]
+ <tr>
+ <td>&nbsp;</td>
+ <td>
+ for [% state FILTER html %]<br>
+ <textarea
+ id="[% ftc_type_id FILTER html %]"
+ name="[% ftc_type_id FILTER html %]"
+ cols="50" rows="2">[% ftc_flags.${type.id}.$state FILTER html %]</textarea>
+ </td>
+ </tr>
+ [% END %]
+[% END %]
diff --git a/extensions/FlagTypeComment/template/en/default/hook/attachment/create-end.html.tmpl b/extensions/FlagTypeComment/template/en/default/hook/attachment/create-end.html.tmpl
new file mode 100644
index 000000000..dfa010d7c
--- /dev/null
+++ b/extensions/FlagTypeComment/template/en/default/hook/attachment/create-end.html.tmpl
@@ -0,0 +1,23 @@
+[%# The contents of this file are subject to the Mozilla Public License Version
+ # 1.1 (the "License"); you may not use this file except in compliance with
+ # the License. You may obtain a copy of the License at
+ # http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS IS" basis,
+ # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ # for the specific language governing rights and limitations under the
+ # License.
+ #
+ # The Original Code is FlagTypeComment Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is
+ # the Mozilla Foundation.
+ # Portions created by the Initial Developer are Copyright (C) 2011
+ # the Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Alex Keybl <akeybl@mozilla.com>
+ # byron jones <glob@mozilla.com>
+ #%]
+
+[% INCLUDE flag/type_comment.html.tmpl %]
diff --git a/extensions/FlagTypeComment/template/en/default/hook/attachment/edit-end.html.tmpl b/extensions/FlagTypeComment/template/en/default/hook/attachment/edit-end.html.tmpl
new file mode 100644
index 000000000..dfa010d7c
--- /dev/null
+++ b/extensions/FlagTypeComment/template/en/default/hook/attachment/edit-end.html.tmpl
@@ -0,0 +1,23 @@
+[%# The contents of this file are subject to the Mozilla Public License Version
+ # 1.1 (the "License"); you may not use this file except in compliance with
+ # the License. You may obtain a copy of the License at
+ # http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS IS" basis,
+ # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ # for the specific language governing rights and limitations under the
+ # License.
+ #
+ # The Original Code is FlagTypeComment Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is
+ # the Mozilla Foundation.
+ # Portions created by the Initial Developer are Copyright (C) 2011
+ # the Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Alex Keybl <akeybl@mozilla.com>
+ # byron jones <glob@mozilla.com>
+ #%]
+
+[% INCLUDE flag/type_comment.html.tmpl %]
diff --git a/extensions/FlagTypeComment/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl b/extensions/FlagTypeComment/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
new file mode 100644
index 000000000..dfa010d7c
--- /dev/null
+++ b/extensions/FlagTypeComment/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
@@ -0,0 +1,23 @@
+[%# The contents of this file are subject to the Mozilla Public License Version
+ # 1.1 (the "License"); you may not use this file except in compliance with
+ # the License. You may obtain a copy of the License at
+ # http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS IS" basis,
+ # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ # for the specific language governing rights and limitations under the
+ # License.
+ #
+ # The Original Code is FlagTypeComment Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is
+ # the Mozilla Foundation.
+ # Portions created by the Initial Developer are Copyright (C) 2011
+ # the Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Alex Keybl <akeybl@mozilla.com>
+ # byron jones <glob@mozilla.com>
+ #%]
+
+[% INCLUDE flag/type_comment.html.tmpl %]
diff --git a/extensions/Gravatar/Config.pm b/extensions/Gravatar/Config.pm
new file mode 100644
index 000000000..e15a41ee8
--- /dev/null
+++ b/extensions/Gravatar/Config.pm
@@ -0,0 +1,16 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Gravatar;
+
+use strict;
+
+use constant NAME => 'Gravatar';
+use constant REQUIRED_MODULES => [];
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/Gravatar/Extension.pm b/extensions/Gravatar/Extension.pm
new file mode 100644
index 000000000..050a0c27d
--- /dev/null
+++ b/extensions/Gravatar/Extension.pm
@@ -0,0 +1,43 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Gravatar;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::User::Setting;
+use Digest::MD5 qw(md5_hex);
+
+use constant DEFAULT_URL => 'extensions/Gravatar/web/default.jpg';
+
+BEGIN {
+ *Bugzilla::User::gravatar = \&_user_gravatar;
+}
+
+sub _user_gravatar {
+ my ($self, $size) = @_;
+ if ($self->setting('show_my_gravatar') eq 'Off') {
+ return DEFAULT_URL;
+ }
+ if (!$self->{gravatar}) {
+ $self->{gravatar} = 'https://secure.gravatar.com/avatar/' .
+ md5_hex(lc($self->email)) . '?d=mm';
+ }
+ $size ||= 64;
+ return $self->{gravatar} . "&amp;size=$size";
+}
+
+sub install_before_final_checks {
+ my ($self, $args) = @_;
+ add_setting('show_gravatars', ['On', 'Off'], 'Off');
+ add_setting('show_my_gravatar', ['On', 'Off'], 'On');
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/Gravatar/template/en/default/hook/bug/comments-user-image.html.tmpl b/extensions/Gravatar/template/en/default/hook/bug/comments-user-image.html.tmpl
new file mode 100644
index 000000000..66714748b
--- /dev/null
+++ b/extensions/Gravatar/template/en/default/hook/bug/comments-user-image.html.tmpl
@@ -0,0 +1,19 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF user.settings.show_gravatars.value == 'On' %]
+ [% IF who.last_activity_ts %]
+ [% IF user.id %]
+ <a href="user_profile?login=[% who.login FILTER uri %]">
+ [% ELSE %]
+ <a href="user_profile?user_id=[% who.id FILTER uri %]">
+ [% END %]
+ [% END %]
+ <img align="middle" src="[% who.gravatar FILTER none %]" width="32" height="32" border="0">
+ [% "</a>" IF who.last_activity_ts %]
+[% END %]
diff --git a/extensions/Gravatar/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/Gravatar/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..0f1130976
--- /dev/null
+++ b/extensions/Gravatar/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF user.settings.show_gravatars.value == 'On' %]
+ [% bodyclasses.push('bz_gravatar') %]
+[% END %]
diff --git a/extensions/Gravatar/template/en/default/hook/global/setting-descs-settings.none.tmpl b/extensions/Gravatar/template/en/default/hook/global/setting-descs-settings.none.tmpl
new file mode 100644
index 000000000..697cfef99
--- /dev/null
+++ b/extensions/Gravatar/template/en/default/hook/global/setting-descs-settings.none.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%
+ setting_descs.show_gravatars = "Show gravatar images when viewing $terms.bugs"
+ setting_descs.show_my_gravatar = "Show my gravatar image to other users"
+%]
diff --git a/extensions/Gravatar/web/default.jpg b/extensions/Gravatar/web/default.jpg
new file mode 100644
index 000000000..98dc1fa87
--- /dev/null
+++ b/extensions/Gravatar/web/default.jpg
Binary files differ
diff --git a/extensions/GuidedBugEntry/Config.pm b/extensions/GuidedBugEntry/Config.pm
new file mode 100644
index 000000000..e4bc9c70b
--- /dev/null
+++ b/extensions/GuidedBugEntry/Config.pm
@@ -0,0 +1,19 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::GuidedBugEntry;
+use strict;
+
+use constant NAME => 'GuidedBugEntry';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/GuidedBugEntry/Extension.pm b/extensions/GuidedBugEntry/Extension.pm
new file mode 100644
index 000000000..127a93a8e
--- /dev/null
+++ b/extensions/GuidedBugEntry/Extension.pm
@@ -0,0 +1,118 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::GuidedBugEntry;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Token;
+use Bugzilla::Error;
+use Bugzilla::Status;
+use Bugzilla::Util 'url_quote';
+use Bugzilla::UserAgent;
+use Bugzilla::Extension::BMO::Data;
+
+our $VERSION = '1';
+
+sub enter_bug_start {
+ my ($self, $args) = @_;
+ my $vars = $args->{vars};
+ my $template = Bugzilla->template;
+ my $cgi = Bugzilla->cgi;
+ my $user = Bugzilla->user;
+
+ # hack for skipping old guided code when enabled
+ $vars->{'disable_guided'} = 1;
+
+ # force guided format for new users
+ my $format = $cgi->param('format') || '';
+ if (
+ $format eq 'guided' ||
+ (
+ $format eq '' &&
+ !$user->in_group('canconfirm')
+ )
+ ) {
+ # skip the first step if a product is provided
+ if ($cgi->param('product')) {
+ print $cgi->redirect('enter_bug.cgi?format=guided#h=dupes' .
+ '|' . url_quote($cgi->param('product')) .
+ '|' . url_quote($cgi->param('component') || '')
+ );
+ exit;
+ }
+
+ $self->_init_vars($vars);
+ print $cgi->header();
+ $template->process('guided/guided.html.tmpl', $vars)
+ || ThrowTemplateError($template->error());
+ exit;
+ }
+
+ # we use the __default__ format to bypass the guided entry
+ # it isn't understood upstream, so remove it once a product
+ # has been selected.
+ if (
+ ($cgi->param('format') && $cgi->param('format') eq "__default__")
+ && ($cgi->param('product') && $cgi->param('product') ne '')
+ ) {
+ $cgi->delete('format');
+ }
+}
+
+sub _init_vars {
+ my ($self, $vars) = @_;
+ my $user = Bugzilla->user;
+
+ my @enterable_products = @{$user->get_enterable_products};
+ ThrowUserError('no_products') unless scalar(@enterable_products);
+
+ my @classifications = ({object => undef, products => \@enterable_products});
+
+ my $class;
+ foreach my $product (@enterable_products) {
+ $class->{$product->classification_id}->{'object'} ||=
+ new Bugzilla::Classification($product->classification_id);
+ push(@{$class->{$product->classification_id}->{'products'}}, $product);
+ }
+ @classifications =
+ sort {
+ $a->{'object'}->sortkey <=> $b->{'object'}->sortkey
+ || lc($a->{'object'}->name) cmp lc($b->{'object'}->name)
+ } (values %$class);
+ $vars->{'classifications'} = \@classifications;
+
+ my @open_states = BUG_STATE_OPEN();
+ $vars->{'open_states'} = \@open_states;
+
+ $vars->{'token'} = issue_session_token('create_bug');
+
+ $vars->{'platform'} = detect_platform();
+ $vars->{'op_sys'} = detect_op_sys();
+}
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my $page = $args->{'page_id'};
+ my $vars = $args->{'vars'};
+
+ return unless $page eq 'guided_products.js';
+
+ # import data from the BMO ext
+
+ $vars->{'product_sec_groups'} = \%product_sec_groups;
+
+ my %bug_formats;
+ foreach my $product (keys %create_bug_formats) {
+ if (my $format = Bugzilla::Extension::BMO::forced_format($product)) {
+ $bug_formats{$product} = $format;
+ }
+ }
+ $vars->{'create_bug_formats'} = \%bug_formats;
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/GuidedBugEntry/template/en/default/bug/create/comment-guided.txt.tmpl b/extensions/GuidedBugEntry/template/en/default/bug/create/comment-guided.txt.tmpl
new file mode 100644
index 000000000..6b0de9466
--- /dev/null
+++ b/extensions/GuidedBugEntry/template/en/default/bug/create/comment-guided.txt.tmpl
@@ -0,0 +1,25 @@
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+User Agent: [% cgi.param('user_agent') %]
+[% IF cgi.param('build_id') %]
+Build ID: [% cgi.param('build_id') %][% END %]
+
+[% IF cgi.param('bug_steps') %]
+Steps to reproduce:
+
+[%+ cgi.param('bug_steps') %]
+[% END %]
+
+[% IF cgi.param('actual') %]
+
+Actual results:
+
+[%+ cgi.param('actual') %]
+[% END %]
+
+[% IF cgi.param('expected') %]
+
+Expected results:
+
+[%+ cgi.param('expected') %]
+[% END %]
diff --git a/extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl b/extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl
new file mode 100644
index 000000000..5b57a0900
--- /dev/null
+++ b/extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl
@@ -0,0 +1,529 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% js_urls = [ 'js/yui3/yui/yui-min.js',
+ 'extensions/GuidedBugEntry/web/js/products.js',
+ 'extensions/GuidedBugEntry/web/js/guided.js',
+ 'extensions/ProdCompSearch/web/js/prod_comp_search.js',
+ 'js/field.js', 'js/TUI.js', 'js/bug.js' ] %]
+
+[% yui_modules = [ 'history', 'datatable', 'container' ] %]
+[% yui_modules.push('autocomplete') %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Enter A Bug"
+ javascript_urls = js_urls
+ style_urls = [ 'extensions/GuidedBugEntry/web/style/guided.css',
+ 'js/yui/assets/skins/sam/container.css' ]
+ yui = yui_modules
+%]
+
+<iframe id="yui-history-iframe" src="extensions/GuidedBugEntry/web/yui-history-iframe.txt"></iframe>
+<input id="yui-history-field" type="hidden">
+
+<noscript>
+You require JavaScript to use this [% terms.bug %] entry form.<br><br>
+Please use the <a href="enter_bug.cgi?format=__default__">advanced [% terms.bug %] entry form</a>.
+</noscript>
+
+<div id="loading" class="hidden">
+Please wait...
+</div>
+<script type="text/javascript">
+YAHOO.util.Dom.removeClass('loading', 'hidden');
+</script>
+
+<div id="steps">
+[% INCLUDE product_step %]
+[% INCLUDE otherProducts_step %]
+[% INCLUDE dupes_step %]
+[% INCLUDE bugForm_step %]
+</div>
+
+<div id="advanced">
+ <a id="advanced_img" href="enter_bug.cgi?format=__default__"><img
+ src="extensions/GuidedBugEntry/web/images/advanced.png" width="16" height="16" border="0"></a>
+ <a id="advanced_link" href="enter_bug.cgi?format=__default__">Switch to the advanced [% terms.bug %] entry form</a>
+</div>
+
+<script type="text/javascript">
+YAHOO.util.Dom.addClass('loading', 'hidden');
+guided.init();
+guided.detectedPlatform = '[% platform FILTER js %]';
+guided.detectedOpSys = '[% op_sys FILTER js %]';
+guided.currentUser = '[% user.login FILTER js %]';
+guided.openStates = [
+[% FOREACH state = open_states %]
+ '[% state FILTER js%]'
+ [%- "," UNLESS loop.last %]
+[% END %]
+];
+dupes.setLabels(
+ {
+ id: "[% field_descs.bug_id FILTER js %]",
+ summary: "[% field_descs.short_desc FILTER js %]",
+ component: "[% field_descs.component FILTER js %]",
+ status: "[% field_descs.bug_status FILTER js %]"
+ }
+);
+</script>
+<script type="text/javascript" src="page.cgi?id=guided_products.js"></script>
+[% PROCESS global/footer.html.tmpl %]
+
+[%############################################################################%]
+[%# page title #%]
+[%############################################################################%]
+
+[% BLOCK page_title %]
+ <div id="page_title">
+ <h2>Enter A [% terms.Bug %]</h2>
+ <h3>Step [% step_number FILTER html %] of 3</h3>
+ </div>
+[% END %]
+
+[%############################################################################%]
+[%# product step #%]
+[%############################################################################%]
+
+[% BLOCK product_step %]
+<div id="product_step" class="step hidden">
+
+[% INCLUDE page_title
+ step_number = "1"
+%]
+
+[% INCLUDE exits
+ show = "all"
+%]
+
+<table id="products">
+[% INCLUDE 'guided/products.html.tmpl' %]
+[% INCLUDE product_block
+ name="Other Products"
+ icon="other.png"
+ desc="Other Mozilla products which aren't listed here"
+ onclick="guided.setStep('otherProducts')"
+%]
+</table>
+
+<h3>
+ Or search for a Product:
+</h3>
+
+<div id="prod_comp_search_main">
+ [% PROCESS prodcompsearch/form.html.tmpl
+ input_label = "Find product:"
+ format = "guided"
+ script_name = "enter_bug.cgi" %]
+</div>
+
+</div>
+[% END %]
+
+[% BLOCK product_block %]
+ [% IF !caption %]
+ [% caption = name %]
+ [% END %]
+ [% IF !desc %]
+ [% FOREACH c = classifications %]
+ [% FOREACH p = c.products %]
+ [% IF p.name == name %]
+ [% desc = p.description %]
+ [% LAST %]
+ [% END %]
+ [% END %]
+ [% END %]
+ [% END %]
+ <tr>
+ <td class="product_img">
+ <a href="javascript:void(0)"
+ [% IF onclick %]
+ onclick="[% onclick FILTER html %]"
+ [% ELSE %]
+ onclick="product.select('[% name FILTER js %]')"
+ [% END %]
+ ><img src="extensions/BMO/web/producticons/[% icon FILTER uri %]" width="64" height="64"
+ ></a>
+ </td>
+ <td>
+ <h2>
+ <a href="javascript:void(0)"
+ [% IF onclick %]
+ onclick="[% onclick FILTER html %]"
+ [% ELSE %]
+ onclick="product.select('[% name FILTER js %]')"
+ [% END %]
+ >[% caption FILTER html %]</a>
+ </h2>
+ <p>
+ [% desc FILTER html_light %]
+ </p>
+ </td>
+ </tr>
+[% END %]
+
+[%############################################################################%]
+[%# other products step #%]
+[%############################################################################%]
+
+[% BLOCK otherProducts_step %]
+<div id="otherProducts_step" class="step hidden">
+
+[% INCLUDE page_title
+ step_number = "1"
+%]
+
+[% INCLUDE exits
+ show = "all"
+%]
+
+<table id="other_products">
+[% FOREACH c = classifications %]
+ [% IF c.object %]
+ <tr class="classification">
+ <th align="right" valign="top">
+ [% c.object.name FILTER html %]:&nbsp;
+ </th>
+ <td>
+ [% c.object.description FILTER html_light %]
+ </td>
+ </tr>
+ [% END %]
+ [% FOREACH p = c.products %]
+ <tr>
+ <th align="right" valign="top">
+ <a href="javascript:void(0)" onclick="product.select('[% p.name FILTER js %]')">
+ [% p.name FILTER html FILTER no_break %]</a>:&nbsp;
+ </th>
+
+ <td valign="top">[% p.description FILTER html_light %]</td>
+ </tr>
+ [% END %]
+ <tr>
+ <td>&nbsp;</td>
+ </tr>
+[% END %]
+</table>
+
+</div>
+[% END %]
+
+[%############################################################################%]
+[%# exits (support/input) #%]
+[%############################################################################%]
+
+[% BLOCK exits %]
+<table class="exits">
+ <tr>
+ <td>
+ <div class="exit_img">
+ <a href="http://www.mozilla.org/support/"
+ ><img src="extensions/GuidedBugEntry/web/images/support.png" width="32" height="32"
+ ></a>
+ </div>
+ </td>
+ <td class="exit_text">
+ <a href="http://www.mozilla.org/support/">I need technical support</a><br>
+ For technical support or help getting your site to work with Mozilla.
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <div class="exit_img">
+ <a href="http://input.mozilla.org/feedback/"
+ ><img src="extensions/GuidedBugEntry/web/images/input.png" width="32" height="32"
+ ></a>
+ </div>
+ </td>
+ <td class="exit_text">
+ <a href="http://input.mozilla.org/feedback/#sad">Offer us ideas on how to make Firefox better</a><br>
+ <a href="http://input.mozilla.org/feedback/">Provide feedback about Firefox</a><br>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <div class="exit_img">
+ <img src="extensions/GuidedBugEntry/web/images/webbug.png" width="32" height="32">
+ </div>
+ </td>
+ <td class="exit_text_last">
+ <a href="enter_bug.cgi?format=guided&amp;product=Core">Report an issue with Firefox on a site that I've developed</a><br>
+ <a href="http://input.mozilla.org/feedback/#sad">Report an issue with a web site that I use</a><br>
+ </td>
+ </tr>
+</table>
+
+<h3>
+ None of the above; my [% terms.bug %] is in:
+</h3>
+[% END %]
+
+[% BLOCK exit_block %]
+ <tr>
+ <td>
+ <div class="exit_img">
+ <a href="[% href FILTER none %]"
+ ><img src="extensions/GuidedBugEntry/web/images/[% icon FILTER uri %]" width="32" height="32"
+ ></a>
+ </div>
+ </td>
+ <td width="100%">
+ <h2>
+ <a href="[% href FILTER none %]">[% name FILTER html %]</a>
+ </h2>
+ [% desc FILTER html %]
+ </td>
+ </tr>
+[% END %]
+
+[%############################################################################%]
+[%# duplicates step #%]
+[%############################################################################%]
+
+[% BLOCK dupes_step %]
+<div id="dupes_step" class="step hidden">
+
+[% INCLUDE page_title
+ step_number = "2"
+%]
+
+<p>
+Product: <b><span id="dupes_product_name">?</span></b>:
+(<a href="javascript:void(0)" onclick="guided.setStep('product')">Change</a>)
+</p>
+
+<table border="0" cellpadding="5" cellspacing="0" id="product_support" class="hidden">
+<tr>
+<td>
+ <img src="extensions/GuidedBugEntry/web/images/message.png" width="24" height="24">
+</td>
+<td id="product_support_message">&nbsp;</td>
+</table>
+
+<div id="dupe_form">
+ <p>
+ Please summarise your issue or request in one sentence:
+ </p>
+ <input id="dupes_summary" value="Short summary of issue" spellcheck="true" placeholder="Short summary of issue">
+ <button id="dupes_search">Find similar issues</button>
+ <button id="dupes_continue_button_top" onclick="guided.setStep('bugForm')">My issue is not listed</button>
+</div>
+
+<div id="dupes_list"></div>
+<div id="dupes_continue">
+<button id="dupes_continue_button_bottom" onclick="guided.setStep('bugForm')">My issue is not listed</button>
+</div>
+
+</div>
+[% END %]
+
+[%############################################################################%]
+[%# bug form step #%]
+[%############################################################################%]
+
+[% BLOCK bugForm_step %]
+<div id="bugForm_step" class="step hidden">
+
+[% INCLUDE page_title
+ step_number = "3"
+%]
+
+<form method="post" action="post_bug.cgi" enctype="multipart/form-data" onsubmit="return bugForm.validate()">
+<input type="hidden" name="token" value="[% token FILTER html %]">
+<input type="hidden" name="product" id="product" value="">
+<input type="hidden" name="component" id="component" value="">
+<input type="hidden" name="bug_severity" value="normal">
+<input type="hidden" name="rep_platform" id="rep_platform" value="All">
+<input type="hidden" name="priority" value="--">
+<input type="hidden" name="op_sys" id="op_sys" value="All">
+<input type="hidden" name="version" id="version" value="">
+<input type="hidden" name="comment" id="comment" value="">
+<input type="hidden" name="format" value="guided">
+<input type="hidden" name="user_agent" id="user_agent" value="">
+<input type="hidden" name="build_id" id="build_id" value="">
+
+<ul>
+<li>Please fill out this form clearly, precisely and in as much detail as you can manage.</li>
+<li>Please report only a single problem at a time.</li>
+<li><a href="https://developer.mozilla.org/en/Bug_writing_guidelines" target="_blank">These guidelines</a>
+explain how to write effective [% terms.bug %] reports.</li>
+</ul>
+
+<table id="bugForm" cellspacing="0">
+
+<tr class="odd">
+ <td class="label">Summary:</td>
+ <td width="100%" colspan="2">
+ <input name="short_desc" id="short_desc" class="textInput" spellcheck="true">
+ </td>
+ <td valign="top">
+ [% PROCESS help id="summary_help" %]
+ <div id="summary_help" class="hidden help">
+ A sentence which summarises the problem. Please be descriptive and use lots of keywords.<br>
+ <br>
+ <span class="help-bad">Bad example</span>: mail crashed<br>
+ <span class="help-good">Good example</span>: crash if I close the mail window while checking for new POP mail
+ </div>
+ </td>
+</tr>
+
+<tr class="even">
+ <td class="label">Product:</td>
+ <td id="productTD">
+ <span id="product_label"></span>
+ (<a href="javascript:void(0)" onclick="guided.setStep('product')">Change</a>)
+ </td>
+ <td id="versionTD" class="hidden">
+ <span class="label">Version:
+ <select id="version_select" onchange="bugForm.onVersionChange(this.value)">
+ </select>
+ </td>
+ <td valign="top">
+ [% PROCESS help id="product_help" %]
+ <div id="product_help" class="hidden help">
+ The Product and Version you are reporting the issue with.
+ </div>
+</tr>
+
+<tr class="odd" id="componentTR">
+ <td valign="top">
+ <div class="label">
+ Component:
+ </div>
+ (<a id="list_comp" href="describecomponents.cgi" target="_blank"
+ title="Show a list of all components and descriptions (in a new window)."
+ >List</a>)
+ </td>
+ <td valign="top" colspan="2">
+ <select id="component_select" onchange="bugForm.onComponentChange(this.value)" class="mandatory">
+ </select>
+ <div id="component_description"></div>
+ </td>
+ <td valign="top">
+ [% PROCESS help id="component_help" %]
+ <div id="component_help" class="hidden help">
+ The area where the problem occurs.<br>
+ <br>
+ If you are unsure which component to use, select a 'General' component.
+ </div>
+</tr>
+
+<tr class="even">
+ <td class="label" colspan="3">What did you do? (steps to reproduce)</td>
+ <td valign="top">
+ [% PROCESS help id="steps_help" %]
+ <div id="steps_help" class="hidden help">
+ Please be as specific as possible about what what you did
+ to cause the problem. Providing step-by-step instructions
+ would be ideal.<br>
+ <br>
+ Include any relevant URLs and special setup steps.<br>
+ <br>
+ <span class="help-bad">Bad example</span>: Mozilla crashed. You suck!<br>
+ <span class="help-good">Good example</span>: After a crash which happened
+ when I was sorting in the Bookmark Manager, all of my top-level bookmark
+ folders beginning with the letters Q to Z are no longer present.
+ </div>
+ </td>
+</tr>
+<tr class="even">
+ <td colspan="3"><textarea id="bug_steps" name="bug_steps" rows="5"></textarea></td>
+ <td>&nbsp;</td>
+</tr>
+
+<tr class="odd">
+ <td class="label" colspan="3">What happened? (actual results)</td>
+ <td valign="top">
+ [% PROCESS help id="actual_help" %]
+ <div id="actual_help" class="hidden help">
+ What happened after you performed the steps above?
+ </div>
+</tr>
+<tr class="odd">
+ <td colspan="3"><textarea id="actual" name="actual" rows="5"></textarea></td>
+ <td>&nbsp;</td>
+</tr>
+
+<tr class="even">
+ <td class="label" colspan="3">What should have happened? (expected results)</td>
+ <td valign="top">
+ [% PROCESS help id="expected_help" %]
+ <div id="expected_help" class="hidden help">
+ What should the software have done instead?
+ </div>
+</tr>
+<tr class="even">
+ <td colspan="3"><textarea id="expected" name="expected" rows="5"></textarea></td>
+ <td>&nbsp;</td>
+</tr>
+
+<tr class="odd">
+ <td class="label">Attach a file:</td>
+ <td colspan="2">
+ <input type="file" name="data" id="data" size="50" onchange="bugForm.onFileChange()">
+ <input type="hidden" name="contenttypemethod" value="autodetect">
+ <button id="reset_data" onclick="return bugForm.onFileClear()" disabled>Clear</button>
+ </td>
+ <td valign="top">
+ [% PROCESS help id="file_help" %]
+ <div id="file_help" class="hidden help">
+ If a file helps explain the issue better, such as a screenshot, please
+ attach one here.
+ </div>
+ </td>
+</tr>
+<tr class="odd">
+ <td class="label">File Description:</td>
+ <td colspan="2"><input type="text" name="description" id="data_description" class="textInput" disabled></td>
+ <td>&nbsp;</td>
+</tr>
+
+<tr class="even">
+ <td class="label">Security:</td>
+ <td colspan="2">
+ <table border="0" cellpadding="0" cellspacing="0">
+ <tr>
+ <td>
+ <input type="checkbox" name="groups" value="core-security" id="groups">
+ </td>
+ <td>
+ <label for="groups">Many users could be harmed by this security problem:
+ it should be kept hidden from the public until it is resolved.</label>
+ </td>
+ </tr>
+ </table>
+ </td>
+ <td>&nbsp;</td>
+</tr>
+
+<tr class="odd">
+ <td>&nbsp;</td>
+ <td colspan="2" id="submitTD">
+ <input type="submit" id="submit" value="Submit [% terms.Bug %]">
+ </td>
+ <td>&nbsp;</td>
+</tr>
+
+</table>
+
+</form>
+
+</div>
+[% END %]
+
+[%############################################################################%]
+[%# help block #%]
+[%############################################################################%]
+
+[% BLOCK help %]
+<img src="extensions/GuidedBugEntry/web/images/help.png" width="16" height="16" class="help_image"
+ helpid="[% id FILTER html %]" onMouseOver="bugForm.showHelp(this)" onMouseOut="bugForm.hideHelp(this)"
+ >
+[% END %]
diff --git a/extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl b/extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl
new file mode 100644
index 000000000..f4e7b81ff
--- /dev/null
+++ b/extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl
@@ -0,0 +1,50 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE product_block
+ name="Firefox"
+ icon="firefox.png"
+%]
+[% INCLUDE product_block
+ name="Firefox OS"
+ icon="firefox_os.png"
+%]
+[% INCLUDE product_block
+ name="Firefox for Android"
+ icon="firefox_android.png"
+%]
+[% INCLUDE product_block
+ name="Marketplace"
+ icon="marketplace.png"
+%]
+[% INCLUDE product_block
+ name="Webmaker"
+ icon="webmaker.png"
+%]
+[% INCLUDE product_block
+ name="Thunderbird"
+ icon="thunderbird.png"
+%]
+[% INCLUDE product_block
+ name="SeaMonkey"
+ icon="seamonkey.png"
+%]
+[% INCLUDE product_block
+ name="Core"
+ icon="component.png"
+%]
+[% INCLUDE product_block
+ name="Mozilla Localizations"
+ icon="localization.png"
+ caption="Localizations"
+%]
+[% INCLUDE product_block
+ name="Mozilla Services"
+ icon="sync.png"
+ caption="Services"
+%]
diff --git a/extensions/GuidedBugEntry/template/en/default/pages/guided_products.js.tmpl b/extensions/GuidedBugEntry/template/en/default/pages/guided_products.js.tmpl
new file mode 100644
index 000000000..b58df8298
--- /dev/null
+++ b/extensions/GuidedBugEntry/template/en/default/pages/guided_products.js.tmpl
@@ -0,0 +1,26 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%# this file allows us to pull in data defined in the BMO ext %]
+
+[% IF create_bug_formats %]
+ [% FOREACH product = create_bug_formats %]
+ if (!products['[% product.key FILTER js %]']) [% ~%]
+ products['[% product.key FILTER js %]'] = {};
+ products['[% product.key FILTER js %]'].format = '[% product.value FILTER js %]';
+ [% END %]
+[% END %]
+
+[% IF product_sec_groups %]
+ [% FOREACH product = product_sec_groups %]
+ if (!products['[% product.key FILTER js %]']) [% ~%]
+ products['[% product.key FILTER js %]'] = {};
+ products['[% product.key FILTER js %]'].secgroup = '[% product.value FILTER js %]';
+ [% END %]
+[% END %]
+
diff --git a/extensions/GuidedBugEntry/web/images/advanced.png b/extensions/GuidedBugEntry/web/images/advanced.png
new file mode 100644
index 000000000..71a3fcb78
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/advanced.png
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/images/help.png b/extensions/GuidedBugEntry/web/images/help.png
new file mode 100644
index 000000000..5c870176d
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/help.png
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/images/input.png b/extensions/GuidedBugEntry/web/images/input.png
new file mode 100644
index 000000000..34c10e989
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/input.png
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/images/message.png b/extensions/GuidedBugEntry/web/images/message.png
new file mode 100644
index 000000000..55b6add19
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/message.png
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/images/sumo.png b/extensions/GuidedBugEntry/web/images/sumo.png
new file mode 100644
index 000000000..d5773647c
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/sumo.png
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/images/support.png b/extensions/GuidedBugEntry/web/images/support.png
new file mode 100644
index 000000000..2320ea74a
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/support.png
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/images/throbber.gif b/extensions/GuidedBugEntry/web/images/throbber.gif
new file mode 100644
index 000000000..bc4fa6561
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/throbber.gif
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/images/warning.png b/extensions/GuidedBugEntry/web/images/warning.png
new file mode 100644
index 000000000..86bed170d
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/warning.png
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/images/webbug.png b/extensions/GuidedBugEntry/web/images/webbug.png
new file mode 100644
index 000000000..949cfbc59
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/webbug.png
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/js/guided.js b/extensions/GuidedBugEntry/web/js/guided.js
new file mode 100644
index 000000000..a3888783b
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/js/guided.js
@@ -0,0 +1,927 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+// global
+
+var Dom = YAHOO.util.Dom;
+var Event = YAHOO.util.Event;
+var History = YAHOO.util.History;
+
+var guided = {
+ _currentStep: '',
+ detectedPlatform: '',
+ detectedOpSys: '',
+ currentUser: '',
+ openStates: [],
+ updateStep: true,
+
+ setStep: function(newStep, noSetHistory) {
+ // initialise new step
+ this.updateStep = true;
+ switch(newStep) {
+ case 'product':
+ product.onShow();
+ break;
+ case 'otherProducts':
+ otherProducts.onShow();
+ break;
+ case 'dupes':
+ dupes.onShow();
+ break;
+ case 'bugForm':
+ bugForm.onShow();
+ break;
+ default:
+ guided.setStep('product');
+ return;
+ }
+
+ if (!this.updateStep)
+ return;
+
+ // change visibility of _step div
+ if (this._currentStep)
+ Dom.addClass(this._currentStep + '_step', 'hidden');
+ this._currentStep = newStep;
+ Dom.removeClass(this._currentStep + '_step', 'hidden');
+
+ // scroll to top of page to mimic real navigation
+ scroll(0,0);
+
+ // update history
+ if (History && !noSetHistory) {
+ History.navigate('h', newStep + '|' + product.getName() +
+ (product.getPreselectedComponent() ? '|' + product.getPreselectedComponent() : '')
+ );
+ }
+ },
+
+ init: function() {
+ // init history manager
+ try {
+ History.register('h', History.getBookmarkedState('h') || 'product',
+ this._onStateChange);
+ History.initialize("yui-history-field", "yui-history-iframe");
+ History.onReady(function () {
+ guided._onStateChange(History.getCurrentState('h'), true);
+ });
+ } catch(err) {
+ History = false;
+ }
+
+ // init steps
+ product.onInit();
+ dupes.onInit();
+ bugForm.onInit();
+ },
+
+ _onStateChange: function(state, noSetHistory) {
+ state = state.split('|');
+ product.setName(state[1] || '');
+ product.setPreselectedComponent(state[2] || '');
+ guided.setStep(state[0], noSetHistory);
+ },
+
+ setAdvancedLink: function() {
+ href = 'enter_bug.cgi?format=__default__' +
+ '&product=' + encodeURIComponent(product.getName()) +
+ '&short_desc=' + encodeURIComponent(dupes.getSummary());
+ Dom.get('advanced_img').href = href;
+ Dom.get('advanced_link').href = href;
+ }
+};
+
+// product step
+
+var product = {
+ details: false,
+ _counter: 0,
+ _loaded: '',
+ _preselectedComponent: '',
+
+ onInit: function() { },
+
+ onShow: function() {
+ Dom.removeClass('advanced', 'hidden');
+ },
+
+ select: function(productName) {
+ // called when a product is selected
+ this.setName(productName);
+ dupes.reset();
+ guided.setStep('dupes');
+ },
+
+ getName: function() {
+ return Dom.get('product').value;
+ },
+
+ getPreselectedComponent: function() {
+ return this._preselectedComponent;
+ },
+
+ setPreselectedComponent: function(value) {
+ this._preselectedComponent = value;
+ },
+
+ _getNameAndRelated: function() {
+ var result = [];
+
+ var name = this.getName();
+ result.push(name);
+
+ if (products[name] && products[name].related) {
+ for (var i = 0, n = products[name].related.length; i < n; i++) {
+ result.push(products[name].related[i]);
+ }
+ }
+
+ return result;
+ },
+
+ setName: function(productName) {
+ if (productName == this.getName() && this.details)
+ return;
+
+ // display the product name
+ Dom.get('product').value = productName;
+ Dom.get('product_label').innerHTML = YAHOO.lang.escapeHTML(productName);
+ Dom.get('dupes_product_name').innerHTML = YAHOO.lang.escapeHTML(productName);
+ Dom.get('list_comp').href = 'describecomponents.cgi?product=' + encodeURIComponent(productName);
+ guided.setAdvancedLink();
+
+ if (productName == '') {
+ Dom.addClass("product_support", "hidden");
+ return;
+ }
+
+ // use the correct security group
+ if (products[productName] && products[productName].secgroup) {
+ Dom.get('groups').value = products[productName].secgroup;
+ } else {
+ Dom.get('groups').value = products['_default'].secgroup;
+ }
+
+ // use the correct platform & op_sys
+ if (products[productName] && products[productName].detectPlatform) {
+ Dom.get('rep_platform').value = guided.detectedPlatform;
+ Dom.get('op_sys').value = guided.detectedOpSys;
+ } else {
+ Dom.get('rep_platform').value = 'All';
+ Dom.get('op_sys').value = 'All';
+ }
+
+ // show support message
+ if (products[productName] && products[productName].support) {
+ Dom.get("product_support_message").innerHTML = products[productName].support;
+ Dom.removeClass("product_support", "hidden");
+ } else {
+ Dom.addClass("product_support", "hidden");
+ }
+
+ // show/hide component selection row
+ if (products[productName] && products[productName].noComponentSelection) {
+ if (!Dom.hasClass('componentTR', 'hidden')) {
+ Dom.addClass('componentTR', 'hidden');
+ bugForm.toggleOddEven();
+ }
+ } else {
+ if (Dom.hasClass('componentTR', 'hidden')) {
+ Dom.removeClass('componentTR', 'hidden');
+ bugForm.toggleOddEven();
+ }
+ }
+
+ if (this._loaded == productName)
+ return;
+
+ // grab the product information
+ this.details = false;
+ this._loaded = productName;
+ YAHOO.util.Connect.setDefaultPostHeader('application/json; charset=UTF-8');
+ YAHOO.util.Connect.asyncRequest(
+ 'POST',
+ 'jsonrpc.cgi',
+ {
+ success: function(res) {
+ try {
+ data = YAHOO.lang.JSON.parse(res.responseText);
+ if (data.error)
+ throw(data.error.message);
+ if (data.result.products.length == 0)
+ document.location.href = 'enter_bug.cgi?format=guided';
+ product.details = data.result.products[0];
+ bugForm.onProductUpdated();
+ } catch (err) {
+ product.details = false;
+ bugForm.onProductUpdated();
+ if (err) {
+ alert('Failed to retreive components for product "' +
+ productName + '":' + "\n\n" + err);
+ if (console)
+ console.error(err);
+ }
+ }
+ },
+ failure: function(res) {
+ this._loaded = '';
+ product.details = false;
+ bugForm.onProductUpdated();
+ if (res.responseText) {
+ alert('Failed to retreive components for product "' +
+ productName + '":' + "\n\n" + res.responseText);
+ if (console)
+ console.error(res);
+ }
+ }
+ },
+ YAHOO.lang.JSON.stringify({
+ version: "1.1",
+ method: "Product.get",
+ id: ++this._counter,
+ params: {
+ names: [productName],
+ exclude_fields: ['internals', 'milestones']
+ }
+ }
+ )
+ );
+ }
+};
+
+// other products step
+
+var otherProducts = {
+ onInit: function() { },
+
+ onShow: function() {
+ Dom.removeClass('advanced', 'hidden');
+ }
+};
+
+// duplicates step
+
+var dupes = {
+ _counter: 0,
+ _dataTable: null,
+ _dataTableColumns: null,
+ _elSummary: null,
+ _elSearch: null,
+ _elList: null,
+ _currentSearchQuery: '',
+
+ onInit: function() {
+ this._elSummary = Dom.get('dupes_summary');
+ this._elSearch = Dom.get('dupes_search');
+ this._elList = Dom.get('dupes_list');
+
+ Event.onBlur(this._elSummary, this._onSummaryBlur);
+ Event.addListener(this._elSummary, 'input', this._onSummaryBlur);
+ Event.addListener(this._elSummary, 'keydown', this._onSummaryKeyDown);
+ Event.addListener(this._elSummary, 'keyup', this._onSummaryKeyUp);
+ Event.addListener(this._elSearch, 'click', this._doSearch);
+ },
+
+ setLabels: function(labels) {
+ this._dataTableColumns = [
+ { key: "id", label: labels.id, formatter: this._formatId },
+ { key: "summary", label: labels.summary, formatter: "text" },
+ { key: "component", label: labels.component, formatter: "text" },
+ { key: "status", label: labels.status, formatter: this._formatStatus },
+ { key: "update_token", label: '', formatter: this._formatCc }
+ ];
+ },
+
+ _initDataTable: function() {
+ var dataSource = new YAHOO.util.XHRDataSource("jsonrpc.cgi");
+ dataSource.connTimeout = 15000;
+ dataSource.connMethodPost = true;
+ dataSource.connXhrMode = "cancelStaleRequests";
+ dataSource.maxCacheEntries = 3;
+ dataSource.responseSchema = {
+ resultsList : "result.bugs",
+ metaFields : { error: "error", jsonRpcId: "id" }
+ };
+ // DataSource can't understand a JSON-RPC error response, so
+ // we have to modify the result data if we get one.
+ dataSource.doBeforeParseData =
+ function(oRequest, oFullResponse, oCallback) {
+ if (oFullResponse.error) {
+ oFullResponse.result = {};
+ oFullResponse.result.bugs = [];
+ if (console)
+ console.error("JSON-RPC error:", oFullResponse.error);
+ }
+ return oFullResponse;
+ };
+ dataSource.subscribe('dataErrorEvent',
+ function() {
+ dupes._currentSearchQuery = '';
+ }
+ );
+
+ this._dataTable = new YAHOO.widget.DataTable(
+ 'dupes_list',
+ this._dataTableColumns,
+ dataSource,
+ {
+ initialLoad: false,
+ MSG_EMPTY: 'No similar issues found.',
+ MSG_ERROR: 'An error occurred while searching for similar issues,' +
+ ' please try again.'
+ }
+ );
+ },
+
+ _formatId: function(el, oRecord, oColumn, oData) {
+ el.innerHTML = '<a href="show_bug.cgi?id=' + oData +
+ '" target="_blank">' + oData + '</a>';
+ },
+
+ _formatStatus: function(el, oRecord, oColumn, oData) {
+ var resolution = oRecord.getData('resolution');
+ var bug_status = display_value('bug_status', oData);
+ if (resolution) {
+ el.innerHTML = bug_status + ' ' +
+ display_value('resolution', resolution);
+ } else {
+ el.innerHTML = bug_status;
+ }
+ },
+
+ _formatCc: function(el, oRecord, oColumn, oData) {
+ var cc = oRecord.getData('cc');
+ var isCCed = false;
+ for (var i = 0, n = cc.length; i < n; i++) {
+ if (cc[i] == guided.currentUser) {
+ isCCed = true;
+ break;
+ }
+ }
+ dupes._buildCcHTML(el, oRecord.getData('id'), oRecord.getData('status'),
+ isCCed);
+ },
+
+ _buildCcHTML: function(el, id, bugStatus, isCCed) {
+ while (el.childNodes.length > 0)
+ el.removeChild(el.firstChild);
+
+ var isOpen = false;
+ for (var i = 0, n = guided.openStates.length; i < n; i++) {
+ if (guided.openStates[i] == bugStatus) {
+ isOpen = true;
+ break;
+ }
+ }
+
+ if (!isOpen && !isCCed) {
+ // you can't cc yourself to a closed bug here
+ return;
+ }
+
+ var button = document.createElement('button');
+ button.setAttribute('type', 'button');
+ if (isCCed) {
+ button.innerHTML = 'Stop&nbsp;following';
+ button.onclick = function() {
+ dupes.updateFollowing(el, id, bugStatus, button, false); return false;
+ };
+ } else {
+ button.innerHTML = 'Follow&nbsp;bug';
+ button.onclick = function() {
+ dupes.updateFollowing(el, id, bugStatus, button, true); return false;
+ };
+ }
+ el.appendChild(button);
+ },
+
+ updateFollowing: function(el, bugID, bugStatus, button, follow) {
+ button.disabled = true;
+ button.innerHTML = 'Updating...';
+
+ var ccObject;
+ if (follow) {
+ ccObject = { add: [ guided.currentUser ] };
+ } else {
+ ccObject = { remove: [ guided.currentUser ] };
+ }
+
+ YAHOO.util.Connect.setDefaultPostHeader('application/json; charset=UTF-8');
+ YAHOO.util.Connect.asyncRequest(
+ 'POST',
+ 'jsonrpc.cgi',
+ {
+ success: function(res) {
+ data = YAHOO.lang.JSON.parse(res.responseText);
+ if (data.error)
+ throw(data.error.message);
+ dupes._buildCcHTML(el, bugID, bugStatus, follow);
+ },
+ failure: function(res) {
+ dupes._buildCcHTML(el, bugID, bugStatus, !follow);
+ if (res.responseText)
+ alert("Update failed:\n\n" + res.responseText);
+ }
+ },
+ YAHOO.lang.JSON.stringify({
+ version: "1.1",
+ method: "Bug.update",
+ id: ++this._counter,
+ params: {
+ ids: [ bugID ],
+ cc : ccObject
+ }
+ })
+ );
+ },
+
+ reset: function() {
+ this._elSummary.value = '';
+ Dom.addClass(this._elList, 'hidden');
+ Dom.addClass('dupes_continue', 'hidden');
+ this._elList.innerHTML = '';
+ this._showProductSupport();
+ this._currentSearchQuery = '';
+ },
+
+ _showProductSupport: function() {
+ var elSupport = Dom.get('product_support_' +
+ product.getName().replace(' ', '_').toLowerCase());
+ var supportElements = Dom.getElementsByClassName('product_support');
+ for (var i = 0, n = supportElements.length; i < n; i++) {
+ if (supportElements[i] == elSupport) {
+ Dom.removeClass(elSupport, 'hidden');
+ } else {
+ Dom.addClass(supportElements[i], 'hidden');
+ }
+ }
+ },
+
+ onShow: function() {
+ this._showProductSupport();
+ this._onSummaryBlur();
+
+ // hide the advanced form and top continue button entry until
+ // a search has happened
+ Dom.addClass('advanced', 'hidden');
+ Dom.addClass('dupes_continue_button_top', 'hidden');
+
+ if (!this._elSearch.disabled && this.getSummary().length >= 4) {
+ // do an immediate search after a page refresh if there's a query
+ this._doSearch();
+
+ } else {
+ // prepare for a search
+ this.reset();
+ }
+ },
+
+ _onSummaryBlur: function() {
+ dupes._elSearch.disabled = dupes._elSummary.value == '';
+ guided.setAdvancedLink();
+ },
+
+ _onSummaryKeyDown: function(e) {
+ // map <enter> to doSearch()
+ if (e && (e.keyCode == 13)) {
+ dupes._doSearch();
+ Event.stopPropagation(e);
+ }
+ },
+
+ _onSummaryKeyUp: function(e) {
+ // disable search button until there's a query
+ dupes._elSearch.disabled = YAHOO.lang.trim(dupes._elSummary.value) == '';
+ },
+
+ _doSearch: function() {
+ if (dupes.getSummary().length < 4) {
+ alert('The summary must be at least 4 characters long.');
+ return;
+ }
+ dupes._elSummary.blur();
+
+ // don't query if we already have the results (or they are pending)
+ if (dupes._currentSearchQuery == dupes.getSummary())
+ return;
+ dupes._currentSearchQuery = dupes.getSummary();
+
+ // initialise the datatable as late as possible
+ dupes._initDataTable();
+
+ try {
+ // run the search
+ Dom.removeClass(dupes._elList, 'hidden');
+
+ dupes._dataTable.showTableMessage(
+ 'Searching for similar issues...&nbsp;&nbsp;&nbsp;' +
+ '<img src="extensions/GuidedBugEntry/web/images/throbber.gif"' +
+ ' width="16" height="11">',
+ YAHOO.widget.DataTable.CLASS_LOADING
+ );
+ var json_object = {
+ version: "1.1",
+ method: "Bug.possible_duplicates",
+ id: ++dupes._counter,
+ params: {
+ product: product._getNameAndRelated(),
+ summary: dupes.getSummary(),
+ limit: 12,
+ include_fields: [ "id", "summary", "status", "resolution",
+ "update_token", "cc", "component" ]
+ }
+ };
+
+ dupes._dataTable.getDataSource().sendRequest(
+ YAHOO.lang.JSON.stringify(json_object),
+ {
+ success: dupes._onDupeResults,
+ failure: dupes._onDupeResults,
+ scope: dupes._dataTable,
+ argument: dupes._dataTable.getState()
+ }
+ );
+
+ Dom.get('dupes_continue_button_top').disabled = true;
+ Dom.get('dupes_continue_button_bottom').disabled = true;
+ Dom.removeClass('dupes_continue', 'hidden');
+ } catch(err) {
+ if (console)
+ console.error(err.message);
+ }
+ },
+
+ _onDupeResults: function(sRequest, oResponse, oPayload) {
+ Dom.removeClass('advanced', 'hidden');
+ Dom.removeClass('dupes_continue_button_top', 'hidden');
+ Dom.get('dupes_continue_button_top').disabled = false;
+ Dom.get('dupes_continue_button_bottom').disabled = false;
+ dupes._dataTable.onDataReturnInitializeTable(sRequest, oResponse,
+ oPayload);
+ },
+
+ getSummary: function() {
+ var summary = YAHOO.lang.trim(this._elSummary.value);
+ // work around chrome bug
+ if (summary == dupes._elSummary.getAttribute('placeholder')) {
+ return '';
+ } else {
+ return summary;
+ }
+ }
+};
+
+// bug form step
+
+var bugForm = {
+ _visibleHelpPanel: null,
+ _mandatoryFields: [],
+
+ onInit: function() {
+ var user_agent = navigator.userAgent;
+ Dom.get('user_agent').value = navigator.userAgent;
+ if (navigator.buildID && navigator.buildID != navigator.userAgent) {
+ Dom.get('build_id').value = navigator.buildID;
+ }
+ Event.addListener(Dom.get('short_desc'), 'blur', function() {
+ Dom.get('dupes_summary').value = Dom.get('short_desc').value;
+ guided.setAdvancedLink();
+ });
+ },
+
+ onShow: function() {
+ // check for a forced format
+ var productName = product.getName();
+ if (products[productName] && products[productName].format) {
+ Dom.addClass('advanced', 'hidden');
+ document.location.href = 'enter_bug.cgi?format=' + encodeURIComponent(products[productName].format) +
+ '&product=' + encodeURIComponent(productName) +
+ '&short_desc=' + encodeURIComponent(dupes.getSummary());
+ guided.updateStep = false;
+ return;
+ }
+ Dom.removeClass('advanced', 'hidden');
+ // default the summary to the dupes query
+ Dom.get('short_desc').value = dupes.getSummary();
+ this.resetSubmitButton();
+ if (Dom.get('component_select').length == 0)
+ this.onProductUpdated();
+ this.onFileChange();
+ for (var i = 0, n = this._mandatoryFields.length; i < n; i++) {
+ Dom.removeClass(this._mandatoryFields[i], 'missing');
+ }
+ },
+
+ resetSubmitButton: function() {
+ Dom.get('submit').disabled = false;
+ Dom.get('submit').value = 'Submit Bug';
+ },
+
+ onProductUpdated: function() {
+ var productName = product.getName();
+
+ // init
+ var elComponents = Dom.get('component_select');
+ Dom.addClass('component_description', 'hidden');
+ elComponents.options.length = 0;
+
+ var elVersions = Dom.get('version_select');
+ elVersions.length = 0;
+
+ // product not loaded yet, bail out
+ if (!product.details) {
+ Dom.addClass('versionTH', 'hidden');
+ Dom.addClass('versionTD', 'hidden');
+ Dom.get('productTD').colSpan = 2;
+ Dom.get('submit').disabled = true;
+ return;
+ }
+ Dom.get('submit').disabled = false;
+
+ // filter components
+ if (products[productName] && products[productName].componentFilter) {
+ product.details.components = products[productName].componentFilter(product.details.components);
+ }
+
+ // build components
+
+ var elComponent = Dom.get('component');
+ if (products[productName] && products[productName].noComponentSelection) {
+
+ elComponent.value = products[productName].defaultComponent;
+ bugForm._mandatoryFields = [ 'short_desc', 'version_select' ];
+
+ } else {
+
+ bugForm._mandatoryFields = [ 'short_desc', 'component_select', 'version_select' ];
+
+ // check for the default component
+ var defaultRegex;
+ if (product.getPreselectedComponent()) {
+ defaultRegex = new RegExp('^' + quoteMeta(product.getPreselectedComponent()) + '$', 'i')
+ } else if(products[productName] && products[productName].defaultComponent) {
+ defaultRegex = new RegExp('^' + quoteMeta(products[productName].defaultComponent) + '$', 'i')
+ } else {
+ defaultRegex = new RegExp('General', 'i');
+ }
+
+ var preselectedComponent = false;
+ for (var i = 0, n = product.details.components.length; i < n; i++) {
+ var component = product.details.components[i];
+ if (component.is_active == '1') {
+ if (defaultRegex.test(component.name)) {
+ preselectedComponent = component.name;
+ break;
+ }
+ }
+ }
+
+ // if there isn't a default component, default to blank
+ if (!preselectedComponent) {
+ elComponents.options[elComponents.options.length] = new Option('', '');
+ }
+
+ // build component select
+ for (var i = 0, n = product.details.components.length; i < n; i++) {
+ var component = product.details.components[i];
+ if (component.is_active == '1') {
+ elComponents.options[elComponents.options.length] =
+ new Option(component.name, component.name);
+ }
+ }
+
+ var validComponent = false;
+ for (var i = 0, n = elComponents.options.length; i < n && !validComponent; i++) {
+ if (elComponents.options[i].value == elComponent.value)
+ validComponent = true;
+ }
+ if (!validComponent)
+ elComponent.value = '';
+ if (elComponent.value == '' && preselectedComponent)
+ elComponent.value = preselectedComponent;
+ if (elComponent.value != '') {
+ elComponents.value = elComponent.value;
+ this.onComponentChange(elComponent.value);
+ }
+
+ }
+
+ // build versions
+ var defaultVersion = '';
+ var currentVersion = Dom.get('version').value;
+ for (var i = 0, n = product.details.versions.length; i < n; i++) {
+ var version = product.details.versions[i];
+ if (version.is_active == '1') {
+ elVersions.options[elVersions.options.length] =
+ new Option(version.name, version.name);
+ if (currentVersion == version.name)
+ defaultVersion = version.name;
+ }
+ }
+
+ if (!defaultVersion) {
+ // try to detect version on a per-product basis
+ if (products[productName] && products[productName].version) {
+ var detectedVersion = products[productName].version();
+ var options = elVersions.options;
+ for (var i = 0, n = options.length; i < n; i++) {
+ if (options[i].value == detectedVersion) {
+ defaultVersion = detectedVersion;
+ break;
+ }
+ }
+ }
+ }
+ if (!defaultVersion) {
+ // load last selected version
+ defaultVersion = YAHOO.util.Cookie.get('VERSION-' + productName);
+ }
+
+ if (elVersions.length > 1) {
+ // more than one version, show select
+ Dom.get('productTD').colSpan = 1;
+ Dom.removeClass('versionTH', 'hidden');
+ Dom.removeClass('versionTD', 'hidden');
+
+ } else {
+ // if there's only one version, we don't need to ask the user
+ Dom.addClass('versionTH', 'hidden');
+ Dom.addClass('versionTD', 'hidden');
+ Dom.get('productTD').colSpan = 2;
+ defaultVersion = elVersions.options[0].value;
+ }
+
+ if (defaultVersion) {
+ elVersions.value = defaultVersion;
+
+ } else {
+ // no default version, select an empty value to force a decision
+ var opt = new Option('', '');
+ try {
+ // standards
+ elVersions.add(opt, elVersions.options[0]);
+ } catch(ex) {
+ // ie only
+ elVersions.add(opt, 0);
+ }
+ elVersions.value = '';
+ }
+ bugForm.onVersionChange(elVersions.value);
+ },
+
+ onComponentChange: function(componentName) {
+ // show the component description
+ Dom.get('component').value = componentName;
+ var elComponentDesc = Dom.get('component_description');
+ elComponentDesc.innerHTML = '';
+ for (var i = 0, n = product.details.components.length; i < n; i++) {
+ var component = product.details.components[i];
+ if (component.name == componentName) {
+ elComponentDesc.innerHTML = component.description;
+ break;
+ }
+ }
+ Dom.removeClass(elComponentDesc, 'hidden');
+ },
+
+ onVersionChange: function(version) {
+ Dom.get('version').value = version;
+ },
+
+ onFileChange: function() {
+ // toggle ui enabled when a file is uploaded or cleared
+ var elFile = Dom.get('data');
+ var elReset = Dom.get('reset_data');
+ var elDescription = Dom.get('data_description');
+ var filename = bugForm._getFilename();
+ if (filename) {
+ elReset.disabled = false;
+ elDescription.value = filename;
+ elDescription.disabled = false;
+ } else {
+ elReset.disabled = true;
+ elDescription.value = '';
+ elDescription.disabled = true;
+ }
+ },
+
+ onFileClear: function() {
+ Dom.get('data').value = '';
+ this.onFileChange();
+ return false;
+ },
+
+ toggleOddEven: function() {
+ var rows = Dom.get('bugForm').getElementsByTagName('TR');
+ var doToggle = false;
+ for (var i = 0, n = rows.length; i < n; i++) {
+ if (doToggle) {
+ rows[i].className = rows[i].className == 'odd' ? 'even' : 'odd';
+ } else {
+ doToggle = rows[i].id == 'componentTR';
+ }
+ }
+ },
+
+ _getFilename: function() {
+ var filename = Dom.get('data').value;
+ if (!filename)
+ return '';
+ filename = filename.replace(/^.+[\\\/]/, '');
+ return filename;
+ },
+
+ _mandatoryMissing: function() {
+ var result = new Array();
+ for (var i = 0, n = this._mandatoryFields.length; i < n; i++ ) {
+ id = this._mandatoryFields[i];
+ el = Dom.get(id);
+
+ if (el.type.toString() == "checkbox") {
+ value = el.checked;
+ } else {
+ value = el.value.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
+ el.value = value;
+ }
+
+ if (value == '') {
+ Dom.addClass(id, 'missing');
+ result.push(id);
+ } else {
+ Dom.removeClass(id, 'missing');
+ }
+ }
+ return result;
+ },
+
+ validate: function() {
+
+ // check mandatory fields
+
+ var missing = bugForm._mandatoryMissing();
+ if (missing.length) {
+ var message = 'The following field' +
+ (missing.length == 1 ? ' is' : 's are') + ' required:\n\n';
+ for (var i = 0, n = missing.length; i < n; i++ ) {
+ var id = missing[i];
+ if (id == 'short_desc') message += ' Summary\n';
+ if (id == 'component_select') message += ' Component\n';
+ if (id == 'version_select') message += ' Version\n';
+ }
+ alert(message);
+ return false;
+ }
+
+ if (Dom.get('data').value && !Dom.get('data_description').value)
+ Dom.get('data_description').value = bugForm._getFilename();
+
+ Dom.get('submit').disabled = true;
+ Dom.get('submit').value = 'Submitting Bug...';
+
+ return true;
+ },
+
+ _initHelp: function(el) {
+ var help_id = el.getAttribute('helpid');
+ if (!el.panel) {
+ if (!el.id)
+ el.id = help_id + '_parent';
+ el.panel = new YAHOO.widget.Panel(
+ help_id,
+ {
+ width: "320px",
+ visible: false,
+ close: false,
+ context: [el.id, 'tl', 'tr', null, [5, 0]]
+ }
+ );
+ el.panel.render();
+ Dom.removeClass(help_id, 'hidden');
+ }
+ },
+
+ showHelp: function(el) {
+ this._initHelp(el);
+ if (this._visibleHelpPanel)
+ this._visibleHelpPanel.hide();
+ el.panel.show();
+ this._visibleHelpPanel = el.panel;
+ },
+
+ hideHelp: function(el) {
+ if (!el.panel)
+ return;
+ if (this._visibleHelpPanel)
+ this._visibleHelpPanel.hide();
+ el.panel.hide();
+ this._visibleHelpPanel = null;
+ }
+}
+
+function quoteMeta(value) {
+ return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
+}
diff --git a/extensions/GuidedBugEntry/web/js/products.js b/extensions/GuidedBugEntry/web/js/products.js
new file mode 100644
index 000000000..dfc830d0f
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/js/products.js
@@ -0,0 +1,118 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+/* Product-specifc configuration for guided bug entry
+ *
+ * related: array of product names which will also be searched for duplicates
+ * version: function which returns a version (eg. detected from UserAgent)
+ * support: string which is displayed at the top of the duplicates page
+ * secgroup: the group to place confidential bugs into
+ * defaultComponent: the default compoent to select. Defaults to 'General'
+ * noComponentSelection: when true, the default component will always be
+ * used. Defaults to 'false';
+ * detectPlatform: when true the platform and op_sys will be set from the
+ * browser's user agent. when false, these will be set to All
+ */
+
+var products = {
+
+ "Firefox": {
+ related: [ "Core", "Toolkit" ],
+ version: function() {
+ var re = /Firefox\/(\d+)\.(\d+)/i;
+ var match = re.exec(navigator.userAgent);
+ if (match) {
+ var maj = match[1];
+ var min = match[2];
+ if (maj * 1 >= 5) {
+ return maj + " Branch";
+ } else {
+ return maj + "." + min + " Branch";
+ }
+ } else {
+ return false;
+ }
+ },
+ defaultComponent: "Untriaged",
+ noComponentSelection: true,
+ detectPlatform: true,
+ support:
+ 'If you are new to Firefox or Bugzilla, please consider checking ' +
+ '<a href="http://support.mozilla.com/">' +
+ '<img src="extensions/GuidedBugEntry/web/images/sumo.png" width="16" height="16" align="absmiddle">' +
+ ' <b>Firefox Help</b></a> instead of creating a bug.'
+ },
+
+ "Firefox for Android": {
+ related: [ "Core", "Toolkit" ],
+ detectPlatform: true,
+ support:
+ 'If you are new to Firefox or Bugzilla, please consider checking ' +
+ '<a href="http://support.mozilla.com/">' +
+ '<img src="extensions/GuidedBugEntry/web/images/sumo.png" width="16" height="16" align="absmiddle">' +
+ ' <b>Firefox Help</b></a> instead of creating a bug.'
+ },
+
+ "SeaMonkey": {
+ related: [ "Core", "Toolkit", "MailNews Core" ],
+ detectPlatform: true,
+ version: function() {
+ var re = /SeaMonkey\/(\d+)\.(\d+)/i;
+ var match = re.exec(navigator.userAgent);
+ if (match) {
+ var maj = match[1];
+ var min = match[2];
+ return "SeaMonkey " + maj + "." + min + " Branch";
+ } else {
+ return false;
+ }
+ }
+ },
+
+ "Camino": {
+ related: [ "Core", "Toolkit" ],
+ detectPlatform: true
+ },
+
+ "Core": {
+ detectPlatform: true
+ },
+
+ "Thunderbird": {
+ related: [ "Core", "Toolkit", "MailNews Core" ],
+ detectPlatform: true,
+ defaultComponent: "Untriaged",
+ componentFilter : function(components) {
+ var index = -1;
+ for (var i = 0, l = components.length; i < l; i++) {
+ if (components[i].name == 'General') {
+ index = i;
+ break;
+ }
+ }
+ if (index != -1) {
+ components.splice(index, 1);
+ }
+ return components;
+ }
+ },
+
+ "Penelope": {
+ related: [ "Core", "Toolkit", "MailNews Core" ]
+ },
+
+ "Bugzilla": {
+ support:
+ 'Please use <a href="http://landfill.bugzilla.org/">Bugzilla Landfill</a> to file "test bugs".'
+ },
+
+ "bugzilla.mozilla.org": {
+ related: [ "Bugzilla" ],
+ support:
+ 'Please use <a href="http://landfill.bugzilla.org/">Bugzilla Landfill</a> to file "test bugs".'
+ }
+}
diff --git a/extensions/GuidedBugEntry/web/style/guided.css b/extensions/GuidedBugEntry/web/style/guided.css
new file mode 100644
index 000000000..f06715eab
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/style/guided.css
@@ -0,0 +1,237 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+/* global */
+
+#page_title {
+}
+
+#page_title h2 {
+ margin-bottom: 0px;
+}
+
+#page_title h3 {
+ margin-top: 0px;
+}
+
+.hidden {
+ display: none;
+}
+
+#yui-history-iframe {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 1px;
+ height: 1px;
+ visibility: hidden;
+}
+
+.step {
+ margin-left: 20px;
+ margin-bottom: 25px;
+}
+
+#steps a img {
+ border: none;
+}
+
+#advanced {
+ margin-top: 50px;
+}
+
+#advanced img {
+ vertical-align: middle;
+}
+
+#advanced a {
+ cursor: pointer;
+}
+
+/* remove the shaded background from data_table header
+ it looks out of place */
+.yui-skin-sam .yui-dt th {
+ background: #f0f0f0;
+}
+
+/* products and other_products step */
+
+.exits {
+ width: 600px;
+ margin-bottom: 10px;
+ border: 1px solid #aaa;
+ border-radius: 5px;
+}
+
+.exits td {
+ padding: 5px;
+}
+
+.exits h2 {
+ margin: 0px;
+ font-size: 90%;
+}
+
+.exit_img {
+ width: 64px;
+ text-align: right;
+}
+
+.exit_text, .exit_text_last {
+ width: 100%;
+}
+
+.exit_text {
+ border-bottom: 1px dotted silver;
+}
+
+#prod_comp_search_main {
+ width: 400px;
+}
+
+#prod_comp_search_label {
+ margin-bottom: 1px;
+}
+
+#prod_comp_search_main li.yui-ac-highlight a {
+ text-decoration: none;
+ color: #FFFFFF;
+ display: block;
+}
+
+#products {
+ width: 600px;
+}
+
+#products td {
+ padding: 5px;
+ padding-bottom: 10px;
+}
+
+#products h2 {
+ margin-bottom: 0px;
+}
+
+#products p {
+ margin-top: 0px;
+}
+
+.product_img {
+ width: 64px;
+}
+
+#other_products .classification {
+ font-weight: bold;
+}
+
+#other_products .classification th {
+ font-size: large;
+}
+
+/* duplicates step */
+
+#dupes_summary {
+ width: 500px;
+}
+
+#dupes_list {
+ margin-top: 1em;
+ margin-bottom: 1em;
+}
+
+#product_support {
+ border: 1px solid #dddddd;
+}
+
+/* bug form step */
+
+#bugForm {
+ width: 600px;
+ border: 4px solid #e0e0e0;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+#bugForm th, #bugForm td {
+ padding: 5px;
+}
+
+#bugForm .even th, #bugForm .even td {
+ background: #e0e0e0;
+}
+
+#bugForm .label {
+ text-align: left;
+ font-weight: bold;
+ white-space: nowrap
+}
+
+#bugzilla-body #bugForm th {
+ vertical-align: middle;
+}
+
+#bugForm .textInput {
+ width: 450px;
+}
+
+#bugForm textarea {
+ font-family: Verdana, sans-serif;
+ font-size: small;
+ width: 590px;
+}
+
+#bugForm .mandatory_mark {
+ color: red;
+ font-size: 80%;
+}
+
+#bugForm .mandatory {
+}
+
+#bugForm .textInput[disabled] {
+ background: transparent;
+ border: 1px solid #dddddd;
+}
+
+#versionTD {
+ text-align: right;
+ white-space: nowrap
+}
+
+#component_select {
+ width: 450px;
+}
+
+#component_description {
+ padding: 5px;
+}
+
+#bugForm .missing {
+ border: 1px solid red;
+ box-shadow: 0px 0px 4px #ff0000;
+ -webkit-box-shadow: 0px 0px 4px #ff0000;
+ -moz-box-shadow: 0px 0px 4px #ff0000;
+}
+
+#submitTD {
+ text-align: right;
+}
+
+.help {
+ position: absolute;
+ background: #ffffff;
+ padding: 2px;
+ cursor: default;
+}
+
+.help-bad {
+ color: #990000;
+}
+
+.help-good {
+ color: #009900;
+}
diff --git a/extensions/GuidedBugEntry/web/yui-history-iframe.txt b/extensions/GuidedBugEntry/web/yui-history-iframe.txt
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/yui-history-iframe.txt
diff --git a/extensions/InlineHistory/Config.pm b/extensions/InlineHistory/Config.pm
new file mode 100644
index 000000000..3834bd81d
--- /dev/null
+++ b/extensions/InlineHistory/Config.pm
@@ -0,0 +1,13 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::InlineHistory;
+use strict;
+
+use constant NAME => 'InlineHistory';
+
+__PACKAGE__->NAME;
diff --git a/extensions/InlineHistory/Extension.pm b/extensions/InlineHistory/Extension.pm
new file mode 100644
index 000000000..803262517
--- /dev/null
+++ b/extensions/InlineHistory/Extension.pm
@@ -0,0 +1,215 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::InlineHistory;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::User::Setting;
+use Bugzilla::Constants;
+use Bugzilla::Attachment;
+
+our $VERSION = '1.5';
+
+# don't show inline history for bugs with lots of changes
+use constant MAXIMUM_ACTIVITY_COUNT => 500;
+
+# don't show really long values
+use constant MAXIMUM_VALUE_LENGTH => 256;
+
+sub template_before_create {
+ my ($self, $args) = @_;
+ $args->{config}->{FILTERS}->{ih_short_value} = sub {
+ my ($str) = @_;
+ return length($str) <= MAXIMUM_VALUE_LENGTH
+ ? $str
+ : substr($str, 0, MAXIMUM_VALUE_LENGTH - 3) . '...';
+ };
+}
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my $file = $args->{'file'};
+ my $vars = $args->{'vars'};
+
+ return if $file ne 'bug/edit.html.tmpl';
+
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+ return unless $user->id && $user->settings->{'inline_history'}->{'value'} eq 'on';
+
+ # note: bug/edit.html.tmpl doesn't support multiple bugs
+ my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'};
+ my $bug_id = $bug->id;
+
+ # build bug activity
+ my ($activity) = $bug->can('get_activity')
+ ? $bug->get_activity()
+ : Bugzilla::Bug::GetBugActivity($bug_id);
+ $activity = _add_duplicates($bug_id, $activity);
+
+ if (scalar @$activity > MAXIMUM_ACTIVITY_COUNT) {
+ $activity = [];
+ $vars->{'ih_activity'} = 0;
+ $vars->{'ih_activity_max'} = 1;
+ return;
+ }
+
+ # allow other extensions to alter history
+ Bugzilla::Hook::process('inline_history_activtiy', { activity => $activity });
+
+ my %attachment_cache;
+ foreach my $attachment (@{$bug->attachments}) {
+ $attachment_cache{$attachment->id} = $attachment;
+ }
+
+ # build a list of bugs we need to check visibility of, so we can check with a single query
+ my %visible_bug_ids;
+
+ # augment and tweak
+ foreach my $operation (@$activity) {
+ # make operation.who an object
+ $operation->{who} =
+ Bugzilla::User->new({ name => $operation->{who}, cache => 1 });
+
+ for (my $i = 0; $i < scalar(@{$operation->{changes}}); $i++) {
+ my $change = $operation->{changes}->[$i];
+
+ # make an attachment object
+ if ($change->{attachid}) {
+ $change->{attach} = $attachment_cache{$change->{attachid}};
+ }
+
+ # empty resolutions are displayed as --- by default
+ # make it explicit here to enable correct display of the change
+ if ($change->{fieldname} eq 'resolution') {
+ $change->{removed} = '---' if $change->{removed} eq '';
+ $change->{added} = '---' if $change->{added} eq '';
+ }
+
+ # make boolean fields true/false instead of 1/0
+ my ($table, $field) = ('bugs', $change->{fieldname});
+ if ($field =~ /^([^\.]+)\.(.+)$/) {
+ ($table, $field) = ($1, $2);
+ }
+ my $column = $dbh->bz_column_info($table, $field);
+ if ($column && $column->{TYPE} eq 'BOOLEAN') {
+ $change->{removed} = '';
+ $change->{added} = $change->{added} ? 'true' : 'false';
+ }
+
+ my $field_obj;
+ if ($change->{fieldname} =~ /^cf_/) {
+ $field_obj = Bugzilla::Field->new({ name => $change->{fieldname}, cache => 1 });
+ }
+
+ # identify buglist changes
+ if ($change->{fieldname} eq 'blocked' ||
+ $change->{fieldname} eq 'dependson' ||
+ $change->{fieldname} eq 'dupe' ||
+ ($field_obj && $field_obj->type == FIELD_TYPE_BUG_ID)
+ ) {
+ $change->{buglist} = 1;
+ foreach my $what (qw(removed added)) {
+ my @buglist = split(/[\s,]+/, $change->{$what});
+ foreach my $id (@buglist) {
+ if ($id && $id =~ /^\d+$/) {
+ $visible_bug_ids{$id} = 1;
+ }
+ }
+ }
+ }
+
+ # split multiple flag changes (must be processed last)
+ if ($change->{fieldname} eq 'flagtypes.name') {
+ my @added = split(/, /, $change->{added});
+ my @removed = split(/, /, $change->{removed});
+ next if scalar(@added) <= 1 && scalar(@removed) <= 1;
+ # remove current change
+ splice(@{$operation->{changes}}, $i, 1);
+ # restructure into added/removed for each flag
+ my %flags;
+ foreach my $added (@added) {
+ my ($value, $name) = $added =~ /^((.+).)$/;
+ $flags{$name}{added} = $value;
+ $flags{$name}{removed} |= '';
+ }
+ foreach my $removed (@removed) {
+ my ($value, $name) = $removed =~ /^((.+).)$/;
+ $flags{$name}{added} |= '';
+ $flags{$name}{removed} = $value;
+ }
+ # clone current change, modify and insert
+ foreach my $flag (sort keys %flags) {
+ my $flag_change = {};
+ foreach my $key (keys %$change) {
+ $flag_change->{$key} = $change->{$key};
+ }
+ $flag_change->{removed} = $flags{$flag}{removed};
+ $flag_change->{added} = $flags{$flag}{added};
+ splice(@{$operation->{changes}}, $i, 0, $flag_change);
+ }
+ $i--;
+ }
+ }
+ }
+
+ $user->visible_bugs([keys %visible_bug_ids]);
+
+ $vars->{'ih_activity'} = $activity;
+}
+
+sub _add_duplicates {
+ # insert 'is a dupe of this bug' comment to allow js to display
+ # as activity
+
+ my ($bug_id, $activity) = @_;
+
+ # we're ignoring pre-bugzilla 3.0 ".. has been marked as a duplicate .."
+ # comments because searching each comment's text is expensive. these
+ # legacy comments will not be visible at all in the bug's comment/activity
+ # stream. bug 928786 deals with migrating those comments to be stored as
+ # CMT_HAS_DUPE instead.
+
+ my $dbh = Bugzilla->dbh;
+ my $sth = $dbh->prepare("
+ SELECT profiles.login_name, " .
+ $dbh->sql_date_format('bug_when', '%Y.%m.%d %H:%i:%s') . ",
+ extra_data
+ FROM longdescs
+ INNER JOIN profiles ON profiles.userid = longdescs.who
+ WHERE bug_id = ? AND type = ?
+ ORDER BY bug_when
+ ");
+ $sth->execute($bug_id, CMT_HAS_DUPE);
+
+ while (my($who, $when, $dupe_id) = $sth->fetchrow_array) {
+ my $entry = {
+ 'when' => $when,
+ 'who' => $who,
+ 'changes' => [
+ {
+ 'removed' => '',
+ 'added' => $dupe_id,
+ 'attachid' => undef,
+ 'fieldname' => 'dupe',
+ 'dupe' => 1,
+ }
+ ],
+ };
+ push @$activity, $entry;
+ }
+
+ return [ sort { $a->{when} cmp $b->{when} } @$activity ];
+}
+
+sub install_before_final_checks {
+ my ($self, $args) = @_;
+ add_setting('inline_history', ['on', 'off'], 'off');
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/InlineHistory/README b/extensions/InlineHistory/README
new file mode 100644
index 000000000..f5aaf163f
--- /dev/null
+++ b/extensions/InlineHistory/README
@@ -0,0 +1,10 @@
+InlineHistory inserts bug activity inline with the comments when viewing a bug.
+It was derived from the Bugzilla Tweaks Addon by Ehasn Akhgari.
+
+For technical and performance reasons it is only available to logged in users,
+and is enabled by a User Preference.
+
+It works with an unmodified install of Bugzilla 4.0 and 4.2.
+
+If you have modified your show_bug template, the javascript in
+web/inline-history.js may need to be updated to suit your installation.
diff --git a/extensions/InlineHistory/template/en/default/hook/bug/comments-aftercomments.html.tmpl b/extensions/InlineHistory/template/en/default/hook/bug/comments-aftercomments.html.tmpl
new file mode 100644
index 000000000..3c4d4a202
--- /dev/null
+++ b/extensions/InlineHistory/template/en/default/hook/bug/comments-aftercomments.html.tmpl
@@ -0,0 +1,160 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS ih_activity %]
+[%# this div exists to allow bugzilla-tweaks to detect when we're active %]
+<div id="inline-history-ext"></div>
+
+<script>
+ var ih_activity = new Array();
+ var ih_activity_flags = new Array();
+ var ih_activity_sort_order = '[% user.settings.comment_sort_order.value FILTER js %]';
+ [% FOREACH operation = ih_activity %]
+ var html = '';
+ [% has_cc = 0 %]
+ [% has_flag = 0 %]
+ [% changer_identity = operation.who.identity %]
+ [% changer_login = operation.who.login %]
+ [% change_date = operation.when FILTER time %]
+
+ [% FOREACH change = operation.changes %]
+ [%# track flag changes %]
+ [% IF change.fieldname == 'flagtypes.name' && change.added != '' %]
+ [% new_flags = change.added.split('[ ,]+') %]
+ [% FOREACH new_flag IN new_flags %]
+ var item = new Array(5);
+ item[0] = '[% changer_login FILTER js %]';
+ item[1] = '[% change_date FILTER js %]';
+ item[2] = '[% change.attachid FILTER js %]';
+ item[3] = '[% new_flag FILTER js %]';
+ item[4] = '[% changer_identity FILTER js %]';
+ ih_activity_flags.push(item);
+ [% has_flag = 1 %]
+ [% END %]
+ [% END %]
+
+ [%# wrap CC changes in a span for toggling visibility %]
+ [% IF change.fieldname == 'cc' %]
+ html += '<span class="ih_cc">';
+ [% has_cc = 1 %]
+ [% END %]
+
+ [%# make attachment changes better %]
+ [% IF change.attachid %]
+ html += '<a '
+ + 'href="attachment.cgi?id=[% change.attachid FILTER none %]&amp;action=edit" '
+ + 'title="[% change.attach.description FILTER html FILTER js %]" '
+ + 'class="[% "bz_obsolete" IF change.attach.isobsolete %]"'
+ + '>Attachment #[% change.attachid FILTER none %]</a> - ';
+ [% END %]
+
+ [%# buglists need to be displayed differently, as we shouldn't use strike-out %]
+ [% IF change.buglist %]
+ [% IF change.dupe %]
+ [% label = 'Duplicate of this ' _ terms.bug %]
+ [% ELSE %]
+ [% label = field_descs.${change.fieldname} %]
+ [% END %]
+ [% IF change.added != '' %]
+ html += '[% label FILTER js %]: ';
+ [% PROCESS add_change value = change.added %]
+ [% END %]
+ [% IF change.removed != '' %]
+ [% "html += '<br>';" IF change.added != '' %]
+ html += 'No longer [% label FILTER lcfirst FILTER js %]: ';
+ [% PROCESS add_change value = change.removed %]
+ [% END %]
+ [% ELSE %]
+ [% IF change.fieldname == 'longdescs.isprivate' %]
+ [%# reference the comment that was made private/public in the field label %]
+ html += '<a href="#c[% change.comment.count FILTER js %]">'
+ + 'Comment [% change.comment.count FILTER js %]</a> is private: ';
+ [% ELSE %]
+ [%# normal label %]
+ html += '[% field_descs.${change.fieldname} FILTER js %]: ';
+ [% END %]
+ [% IF change.removed != '' %]
+ [% IF change.added == '' %]
+ html += '<span class="ih_deleted">';
+ [% END %]
+ [% PROCESS add_change value = change.removed %]
+ [% IF change.added == '' %]
+ html += '</span>';
+ [% ELSE %]
+ html += ' &rarr; ';
+ [% END %]
+ [% END %]
+ [% PROCESS add_change value = change.added %]
+ [% END %]
+ [% "html += '<br>';" UNLESS loop.last %]
+
+ [% IF change.fieldname == 'cc' %]
+ html += '</span>';
+ [% END %]
+ [% END %]
+
+ [% changer_id = operation.who.id %]
+ [% UNLESS user_cache.$changer_id %]
+ [% user_cache.$changer_id = BLOCK %]
+ [% INCLUDE global/user.html.tmpl who = operation.who %]
+ [% END %]
+ [% END %]
+
+ var user_image = '
+ [%~ who = operation.who %]
+ [% Hook.process('user-image', 'bug/comments.html.tmpl') FILTER js %]';
+
+ var item = new Array(7);
+ item[0] = '[% changer_login FILTER js %]';
+ item[1] = '[% change_date FILTER js %]';
+ item[2] = html;
+ item[3] = '<div class="bz_comment_head">'
+ + '<span class="bz_comment_user">'
+ + user_image
+ + ' [% user_cache.$changer_id FILTER js %]'
+ + '</span>'
+ + '<span class="bz_comment_time"> ' + item[1] + ' </span>'
+ + '</div>';
+ item[4] = [% IF has_cc && (operation.changes.size == 1) %]true[% ELSE %]false[% END %];
+ item[5] = [% IF change.dupe %][% change.added FILTER js %][% ELSE %]0[% END %];
+ item[6] = [% IF has_flag %]true[% ELSE %]false[% END %];
+ ih_activity[[% loop.index %]] = item;
+ [% END %]
+ inline_history.init();
+</script>
+
+[% BLOCK add_change %]
+ html += '[%~%]
+ [% IF change.fieldname == 'estimated_time' ||
+ change.fieldname == 'remaining_time' ||
+ change.fieldname == 'work_time' %]
+ [% PROCESS formattimeunit time_unit = value FILTER html FILTER js %]
+ [% ELSIF change.buglist %]
+ [% value FILTER bug_list_link FILTER js %]
+ [% ELSIF change.fieldname == 'bug_file_loc' %]
+ [%~%]<a href="[% value FILTER html FILTER js %]" target="_blank"
+ [%~ ' onclick="return inline_history.confirmUnsafeUrl(this.href)"'
+ UNLESS is_safe_url(value) %]>
+ [%~%][% value FILTER ih_short_value FILTER html FILTER js %]</a>
+ [% ELSIF change.fieldname == 'see_also' %]
+ [% FOREACH see_also = value.split(', ') %]
+ [%~%]<a href="[% see_also FILTER html FILTER js %]" target="_blank">
+ [%~%][% see_also FILTER html FILTER js %]</a>
+ [%- ", " IF NOT loop.last %]
+ [% END %]
+ [% ELSIF change.fieldname == 'assigned_to' ||
+ change.fieldname == 'reporter' ||
+ change.fieldname == 'qa_contact' ||
+ change.fieldname == 'cc' ||
+ change.fieldname == 'flagtypes.name' %]
+ [% value FILTER email FILTER js %]
+ [% ELSE %]
+ [% value FILTER ih_short_value FILTER html FILTER js %]
+ [% END %]
+ [%~ %]';
+[% END %]
diff --git a/extensions/InlineHistory/template/en/default/hook/bug/comments-comment_banner.html.tmpl b/extensions/InlineHistory/template/en/default/hook/bug/comments-comment_banner.html.tmpl
new file mode 100644
index 000000000..133005f4f
--- /dev/null
+++ b/extensions/InlineHistory/template/en/default/hook/bug/comments-comment_banner.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF ih_activity_max %]
+<p>
+ <i>This [% terms.bug %] contains too many changes to be displayed inline.</i>
+</p>
+[% END %]
diff --git a/extensions/InlineHistory/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/InlineHistory/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..7e54b8380
--- /dev/null
+++ b/extensions/InlineHistory/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF user.id && user.settings.inline_history.value == "on" %]
+ [% style_urls.push('extensions/InlineHistory/web/style.css') %]
+ [% javascript_urls.push('extensions/InlineHistory/web/inline-history.js') %]
+[% END %]
diff --git a/extensions/InlineHistory/template/en/default/hook/global/setting-descs-settings.none.tmpl b/extensions/InlineHistory/template/en/default/hook/global/setting-descs-settings.none.tmpl
new file mode 100644
index 000000000..e1ff4c0f6
--- /dev/null
+++ b/extensions/InlineHistory/template/en/default/hook/global/setting-descs-settings.none.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%
+ setting_descs.inline_history = "When viewing a $terms.bug, show all $terms.bug activity",
+%]
diff --git a/extensions/InlineHistory/web/inline-history.js b/extensions/InlineHistory/web/inline-history.js
new file mode 100644
index 000000000..a1bfeca23
--- /dev/null
+++ b/extensions/InlineHistory/web/inline-history.js
@@ -0,0 +1,417 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+var inline_history = {
+ _ccDivs: null,
+ _hasAttachmentFlags: false,
+ _hasBugFlags: false,
+
+ init: function() {
+ Dom = YAHOO.util.Dom;
+
+ var reDuplicate = /^\*\*\* \S+ \d+ has been marked as a duplicate of this/;
+ var reBugId = /show_bug\.cgi\?id=(\d+)/;
+ var reHours = /Additional hours worked: \d+\.\d+/;
+
+ var comments = Dom.getElementsByClassName("bz_comment", 'div', 'comments');
+ for (var i = 1, il = comments.length; i < il; i++) {
+ // remove 'has been marked as a duplicate of this bug' comments
+ var textDiv = Dom.getElementsByClassName('bz_comment_text', 'pre', comments[i]);
+ if (textDiv) {
+ var match = reDuplicate.exec(textDiv[0].textContent || textDiv[0].innerText);
+ if (match) {
+ // grab the comment and bug number from the element
+ var comment = comments[i];
+ var number = comment.id.substr(1);
+ var time = this.trim(Dom.getElementsByClassName('bz_comment_time', 'span', comment)[0].innerHTML);
+ var dupeId = 0;
+ match = reBugId.exec(Dom.get('comment_text_' + number).innerHTML);
+ if (match)
+ dupeId = match[1];
+ // remove the element
+ comment.parentNode.removeChild(comment);
+ comments[i] = false;
+ // update the html for the history item to include the comment number
+ if (dupeId == 0)
+ continue;
+ for (var j = 0, jl = ih_activity.length; j < jl; j++) {
+ var item = ih_activity[j];
+ if (item[5] == dupeId && item[1] == time) {
+ // insert comment number and link into the header
+ item[3] = item[3].substr(0, item[3].length - 6) // remove trailing </div>
+ // add comment number
+ + '<span class="bz_comment_number" id="c' + number + '">'
+ + '<a href="#c' + number + '">Comment ' + number + '</a>'
+ + '</span>'
+ + '</div>';
+ break;
+ }
+ }
+ }
+ }
+ if (!comments[i])
+ continue;
+
+ // remove 'Additional hours worked: ' comments
+ var commentNodes = comments[i].childNodes;
+ for (var j = 0, jl = commentNodes.length; j < jl; j++) {
+ if (reHours.exec(commentNodes[j].textContent)) {
+ comments[i].removeChild(commentNodes[j]);
+ // Remove the <br> before it
+ comments[i].removeChild(commentNodes[j-1]);
+ break;
+ }
+ }
+ }
+
+ // remove deleted comments
+ for (var i = 0; i < comments.length; i++) {
+ if (!comments[i]) {
+ comments.splice(i, 1);
+ i--;
+ }
+ }
+
+ // ensure new items are placed immediately after the last comment
+ if (!comments.length) return;
+ var lastCommentDiv = comments[comments.length - 1];
+
+ // insert activity into the correct location
+ var commentTimes = Dom.getElementsByClassName('bz_comment_time', 'span', 'comments');
+ for (var i = 0, il = ih_activity.length; i < il; i++) {
+ var item = ih_activity[i];
+ // item[0] : who
+ // item[1] : when
+ // item[2] : change html
+ // item[3] : header html
+ // item[4] : bool; cc-only
+ // item[5] : int; dupe bug id (or 0)
+ // item[6] : bool; is flag
+ var user = item[0];
+ var time = item[1];
+
+ var reachedEnd = false;
+ var start_index = ih_activity_sort_order == 'newest_to_oldest_desc_first' ? 1 : 0;
+ for (var j = start_index, jl = commentTimes.length; j < jl; j++) {
+ var commentHead = commentTimes[j].parentNode;
+ var mainUser = Dom.getElementsByClassName('email', 'a', commentHead)[0].href.substr(7);
+ var text = commentTimes[j].textContent || commentTimes[j].innerText;
+ var mainTime = this.trim(text);
+
+ if (ih_activity_sort_order == 'oldest_to_newest' ? time > mainTime : time < mainTime) {
+ if (j < commentTimes.length - 1) {
+ continue;
+ } else {
+ reachedEnd = true;
+ }
+ }
+
+ var inline = (mainUser == user && time == mainTime);
+ var currentDiv = document.createElement("div");
+
+ // place ih_cc class on parent container if it's the only child
+ var containerClass = '';
+ if (item[4]) {
+ item[2] = item[2].replace('"ih_cc"', '""');
+ containerClass = 'ih_cc';
+ }
+
+ if (inline) {
+ // assume that the change was made by the same user
+ commentHead.parentNode.appendChild(currentDiv);
+ currentDiv.innerHTML = item[2];
+ Dom.addClass(currentDiv, 'ih_inlinehistory');
+ Dom.addClass(currentDiv, containerClass);
+ if (item[6])
+ this.setFlagChangeID(item, commentHead.parentNode.id);
+
+ } else {
+ // the change was made by another user
+ if (!reachedEnd) {
+ var parentDiv = commentHead.parentNode;
+ var previous = this.previousElementSibling(parentDiv);
+ if (previous && previous.className.indexOf("ih_history") >= 0) {
+ currentDiv = this.previousElementSibling(parentDiv);
+ } else {
+ parentDiv.parentNode.insertBefore(currentDiv, parentDiv);
+ }
+ } else {
+ var parentDiv = commentHead.parentNode;
+ var next = this.nextElementSibling(parentDiv);
+ if (next && next.className.indexOf("ih_history") >= 0) {
+ currentDiv = this.nextElementSibling(parentDiv);
+ } else {
+ lastCommentDiv.parentNode.insertBefore(currentDiv, lastCommentDiv.nextSibling);
+ }
+ }
+
+ var itemContainer = document.createElement('div');
+ itemContainer.className = 'ih_history_item ' + containerClass;
+ itemContainer.id = 'h' + i;
+ itemContainer.innerHTML = item[3] + '<div class="ih_history_change">' + item[2] + '</div>';
+
+ if (ih_activity_sort_order == 'oldest_to_newest') {
+ currentDiv.appendChild(itemContainer);
+ } else {
+ currentDiv.insertBefore(itemContainer, currentDiv.firstChild);
+ }
+ currentDiv.setAttribute("class", "bz_comment ih_history");
+ if (item[6])
+ this.setFlagChangeID(item, 'h' + i);
+ }
+ break;
+ }
+ }
+
+ // find comment blocks which only contain cc changes, shift the ih_cc
+ var historyDivs = Dom.getElementsByClassName('ih_history', 'div', 'comments');
+ for (var i = 0, il = historyDivs.length; i < il; i++) {
+ var historyDiv = historyDivs[i];
+ var itemDivs = Dom.getElementsByClassName('ih_history_item', 'div', historyDiv);
+ var ccOnly = true;
+ for (var j = 0, jl = itemDivs.length; j < jl; j++) {
+ if (!Dom.hasClass(itemDivs[j], 'ih_cc')) {
+ ccOnly = false;
+ break;
+ }
+ }
+ if (ccOnly) {
+ for (var j = 0, jl = itemDivs.length; j < jl; j++) {
+ Dom.removeClass(itemDivs[j], 'ih_cc');
+ }
+ Dom.addClass(historyDiv, 'ih_cc');
+ }
+ }
+
+ if (this._hasAttachmentFlags)
+ this.linkAttachmentFlags();
+ if (this._hasBugFlags)
+ this.linkBugFlags();
+
+ ih_activity = undefined;
+ ih_activity_flags = undefined;
+
+ this._ccDivs = Dom.getElementsByClassName('ih_cc', '', 'comments');
+ this.hideCC();
+ YAHOO.util.Event.onDOMReady(this.addCCtoggler);
+ },
+
+ setFlagChangeID: function(changeItem, id) {
+ // put the ID for the change into ih_activity_flags
+ for (var i = 0, il = ih_activity_flags.length; i < il; i++) {
+ var flagItem = ih_activity_flags[i];
+ // flagItem[0] : who.login
+ // flagItem[1] : when
+ // flagItem[2] : attach id
+ // flagItem[3] : flag
+ // flagItem[4] : who.identity
+ // flagItem[5] : change div id
+ if (flagItem[0] == changeItem[0] && flagItem[1] == changeItem[1]) {
+ // store the div
+ flagItem[5] = id;
+ // tag that we have flags to process
+ if (flagItem[2]) {
+ this._hasAttachmentFlags = true;
+ } else {
+ this._hasBugFlags = true;
+ }
+ if (flagItem[3].match(/^needinfo\?/)) {
+ this.lastNeedinfo = flagItem;
+ }
+ // don't break as there may be multiple flag changes at once
+ }
+ }
+ },
+
+ linkAttachmentFlags: function() {
+ var rows = Dom.get('attachment_table').getElementsByTagName('tr');
+ for (var i = 0, il = rows.length; i < il; i++) {
+
+ // deal with attachments with flags only
+ var tr = rows[i];
+ if (!tr.id || tr.id == 'a0')
+ continue;
+ var attachFlagTd = Dom.getElementsByClassName('bz_attach_flags', 'td', tr);
+ if (attachFlagTd.length == 0)
+ continue;
+ attachFlagTd = attachFlagTd[0];
+
+ // get the attachment id
+ var attachId = 0;
+ var anchors = tr.getElementsByTagName('a');
+ for (var j = 0, jl = anchors.length; j < jl; j++) {
+ var match = anchors[j].href.match(/attachment\.cgi\?id=(\d+)/);
+ if (match) {
+ attachId = match[1];
+ break;
+ }
+ }
+ if (!attachId)
+ continue;
+
+ var html = '';
+
+ // there may be multiple flags, split by <br>
+ var attachFlags = attachFlagTd.innerHTML.split('<br>');
+ for (var j = 0, jl = attachFlags.length; j < jl; j++) {
+ var match = attachFlags[j].match(/^\s*(<span.+\/span>):([^\?\-\+]+[\?\-\+])([\s\S]*)/);
+ if (!match) continue;
+ var setterSpan = match[1];
+ var flag = this.trim(match[2].replace('\u2011', '-', 'g'));
+ var requestee = this.trim(match[3]);
+ var requesteeLogin = '';
+
+ match = setterSpan.match(/title="([^"]+)"/);
+ if (!match) continue;
+ var setterIdentity = this.htmlDecode(match[1]);
+
+ if (requestee) {
+ match = requestee.match(/title="([^"]+)"/);
+ if (!match) continue;
+ requesteeLogin = this.htmlDecode(match[1]);
+ match = requesteeLogin.match(/<([^>]+)>/);
+ if (match)
+ requesteeLogin = match[1];
+ }
+
+ var flagValue = requestee ? flag + '(' + requesteeLogin + ')' : flag;
+ // find the id for this change
+ var found = false;
+ for (var k = 0, kl = ih_activity_flags.length; k < kl; k++) {
+ flagItem = ih_activity_flags[k];
+ if (
+ flagItem[2] == attachId
+ && flagItem[3] == flagValue
+ && flagItem[4] == setterIdentity
+ ) {
+ html +=
+ setterSpan + ': '
+ + '<a href="#' + flagItem[5] + '">' + flag + '</a> '
+ + requestee + '<br>';
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ // something went wrong, insert the flag unlinked
+ html += attachFlags[j] + '<br>';
+ }
+ }
+
+ if (html)
+ attachFlagTd.innerHTML = html;
+ }
+ },
+
+ linkBugFlags: function() {
+ var flags = Dom.get('flags');
+ if (!flags) return;
+ var rows = flags.getElementsByTagName('tr');
+ for (var i = 0, il = rows.length; i < il; i++) {
+ var cells = rows[i].getElementsByTagName('td');
+ if (!cells[1]) continue;
+
+ var match = cells[0].innerHTML.match(/title="([^"]+)"/);
+ if (!match) continue;
+ var setterIdentity = this.htmlDecode(match[1]);
+
+ var flagValue = cells[2].getElementsByTagName('select');
+ if (!flagValue.length) continue;
+ flagValue = flagValue[0].value;
+
+ var flagLabel = cells[1].getElementsByTagName('label');
+ if (!flagLabel.length) continue;
+ flagLabel = flagLabel[0];
+ var flagName = this.trim(flagLabel.innerHTML).replace('\u2011', '-', 'g');
+
+ for (var j = 0, jl = ih_activity_flags.length; j < jl; j++) {
+ flagItem = ih_activity_flags[j];
+ if (
+ !flagItem[2]
+ && flagItem[3] == flagName + flagValue
+ && flagItem[4] == setterIdentity
+ ) {
+ flagLabel.innerHTML =
+ '<a href="#' + flagItem[5] + '">' + flagName + '</a>';
+ break;
+ }
+ }
+ }
+ },
+
+ hideCC: function() {
+ Dom.addClass(this._ccDivs, 'ih_hidden');
+ },
+
+ showCC: function() {
+ Dom.removeClass(this._ccDivs, 'ih_hidden');
+ },
+
+ addCCtoggler: function() {
+ var ul = Dom.getElementsByClassName('bz_collapse_expand_comments');
+ if (ul.length == 0)
+ return;
+ ul = ul[0];
+ var a = document.createElement('a');
+ a.href = 'javascript:void(0)';
+ a.id = 'ih_toggle_cc';
+ YAHOO.util.Event.addListener(a, 'click', function(e) {
+ if (Dom.get('ih_toggle_cc').innerHTML == 'Show CC Changes') {
+ a.innerHTML = 'Hide CC Changes';
+ inline_history.showCC();
+ } else {
+ a.innerHTML = 'Show CC Changes';
+ inline_history.hideCC();
+ }
+ });
+ a.innerHTML = 'Show CC Changes';
+ var li = document.createElement('li');
+ li.appendChild(a);
+ ul.appendChild(li);
+ },
+
+ confirmUnsafeUrl: function(url) {
+ return confirm(
+ 'This is considered an unsafe URL and could possibly be harmful.\n'
+ + 'The full URL is:\n\n' + url + '\n\nContinue?');
+ },
+
+ previousElementSibling: function(el) {
+ if (el.previousElementSibling)
+ return el.previousElementSibling;
+ while (el = el.previousSibling) {
+ if (el.nodeType == 1)
+ return el;
+ }
+ },
+
+ nextElementSibling: function(el) {
+ if (el.nextElementSibling)
+ return el.nextElementSibling;
+ while (el = el.nextSibling) {
+ if (el.nodeType == 1)
+ return el;
+ }
+ },
+
+ getNeedinfoDiv: function () {
+ if (this.lastNeedinfo && this.lastNeedinfo[5]) {
+ return this.lastNeedinfo[5];
+ }
+ },
+
+ htmlDecode: function(v) {
+ if (!v.match(/&/)) return v;
+ var e = document.createElement('textarea');
+ e.innerHTML = v;
+ return e.value;
+ },
+
+ trim: function(s) {
+ return s.replace(/^\s+|\s+$/g, '');
+ }
+};
diff --git a/extensions/InlineHistory/web/style.css b/extensions/InlineHistory/web/style.css
new file mode 100644
index 000000000..af76eba82
--- /dev/null
+++ b/extensions/InlineHistory/web/style.css
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+.ih_history {
+ background: none !important;
+ color: #444;
+}
+
+.ih_inlinehistory {
+ font-weight: normal;
+ font-size: small;
+ color: #444;
+ border-top: 1px dotted #C8C8BA;
+ padding-top: 5px;
+}
+
+.bz_comment.ih_history {
+ padding: 5px 5px 0px 5px
+}
+
+.ih_history_item {
+ margin-bottom: 5px;
+}
+
+.ih_hidden {
+ display: none;
+}
+
+.ih_deleted {
+ text-decoration: line-through;
+}
diff --git a/extensions/LastResolved/Config.pm b/extensions/LastResolved/Config.pm
new file mode 100644
index 000000000..f763167e2
--- /dev/null
+++ b/extensions/LastResolved/Config.pm
@@ -0,0 +1,20 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::LastResolved;
+
+use strict;
+
+use constant NAME => 'LastResolved';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/LastResolved/Extension.pm b/extensions/LastResolved/Extension.pm
new file mode 100644
index 000000000..ad0519387
--- /dev/null
+++ b/extensions/LastResolved/Extension.pm
@@ -0,0 +1,113 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::LastResolved;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Bug qw(LogActivityEntry);
+use Bugzilla::Util qw(format_time);
+use Bugzilla::Constants;
+use Bugzilla::Field;
+use Bugzilla::Install::Util qw(indicate_progress);
+
+our $VERSION = '0.01';
+
+sub install_update_db {
+ my ($self, $args) = @_;
+ my $last_resolved = Bugzilla::Field->new({'name' => 'cf_last_resolved'});
+ if (!$last_resolved) {
+ Bugzilla::Field->create({
+ name => 'cf_last_resolved',
+ description => 'Last Resolved',
+ type => FIELD_TYPE_DATETIME,
+ mailhead => 0,
+ enter_bug => 0,
+ obsolete => 0,
+ custom => 1,
+ buglist => 1,
+ });
+ _migrate_last_resolved();
+ }
+}
+
+sub _migrate_last_resolved {
+ my $dbh = Bugzilla->dbh;
+ my $field_id = get_field_id('bug_status');
+ my $resolved_activity = $dbh->selectall_arrayref(
+ "SELECT bugs_activity.bug_id, bugs_activity.bug_when, bugs_activity.who
+ FROM bugs_activity
+ WHERE bugs_activity.fieldid = ?
+ AND bugs_activity.added = 'RESOLVED'
+ ORDER BY bugs_activity.bug_when",
+ undef, $field_id);
+
+ my $count = 1;
+ my $total = scalar @$resolved_activity;
+ my %current_last_resolved;
+ foreach my $activity (@$resolved_activity) {
+ indicate_progress({ current => $count++, total => $total, every => 25 });
+ my ($id, $new, $who) = @$activity;
+ my $old = $current_last_resolved{$id} ? $current_last_resolved{$id} : "";
+ $dbh->do("UPDATE bugs SET cf_last_resolved = ? WHERE bug_id = ?", undef, $new, $id);
+ LogActivityEntry($id, 'cf_last_resolved', $old, $new, $who, $new);
+ $current_last_resolved{$id} = $new;
+ }
+}
+
+sub bug_check_can_change_field {
+ my ($self, $args) = @_;
+ my ($field, $priv_results) = @$args{qw(field priv_results)};
+ if ($field eq 'cf_last_resolved') {
+ push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ }
+}
+
+sub bug_end_of_update {
+ my ($self, $args) = @_;
+ my $dbh = Bugzilla->dbh;
+ my ($bug, $old_bug, $timestamp, $changes) =
+ @$args{qw(bug old_bug timestamp changes)};
+ if ($changes->{'bug_status'}) {
+ # If the bug has been resolved then update the cf_last_resolved
+ # value to the current timestamp if cf_last_resolved exists
+ if ($bug->bug_status eq 'RESOLVED') {
+ $dbh->do("UPDATE bugs SET cf_last_resolved = ? WHERE bug_id = ?",
+ undef, $timestamp, $bug->id);
+ my $old_value = $bug->cf_last_resolved || '';
+ LogActivityEntry($bug->id, 'cf_last_resolved', $old_value,
+ $timestamp, Bugzilla->user->id, $timestamp);
+ }
+ }
+}
+
+sub bug_fields {
+ my ($self, $args) = @_;
+ my $fields = $args->{'fields'};
+ push (@$fields, 'cf_last_resolved')
+}
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my ($class, $columns) = @$args{qw(class columns)};
+ if ($class->isa('Bugzilla::Bug')) {
+ push(@$columns, 'cf_last_resolved');
+ }
+}
+
+sub buglist_columns {
+ my ($self, $args) = @_;
+ my $columns = $args->{columns};
+ $columns->{'cf_last_resolved'} = {
+ name => 'bugs.cf_last_resolved',
+ title => 'Last Resolved',
+ };
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/LastResolved/template/en/default/hook/bug/edit-custom_field.html.tmpl b/extensions/LastResolved/template/en/default/hook/bug/edit-custom_field.html.tmpl
new file mode 100644
index 000000000..27366b01f
--- /dev/null
+++ b/extensions/LastResolved/template/en/default/hook/bug/edit-custom_field.html.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%# Do not display the last resolved value in the UI %]
+[% IF field.name == 'cf_last_resolved' %]
+ [% field.hidden = 1 %]
+[% END %]
diff --git a/extensions/LastResolved/template/en/default/hook/global/field-descs-end.none.tmpl b/extensions/LastResolved/template/en/default/hook/global/field-descs-end.none.tmpl
new file mode 100644
index 000000000..4457ccd9b
--- /dev/null
+++ b/extensions/LastResolved/template/en/default/hook/global/field-descs-end.none.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF in_template_var %]
+ [% vars.field_descs.cf_last_resolved = "Last Resolved" %]
+[% END %]
diff --git a/extensions/LastResolved/template/en/default/hook/list/edit-multiple-custom_field.html.tmpl b/extensions/LastResolved/template/en/default/hook/list/edit-multiple-custom_field.html.tmpl
new file mode 100644
index 000000000..31e645c32
--- /dev/null
+++ b/extensions/LastResolved/template/en/default/hook/list/edit-multiple-custom_field.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF field.name == "cf_last_resolved" %]
+ [% field.hidden = 1 %]
+[% END %]
diff --git a/extensions/LimitedEmail/Config.pm b/extensions/LimitedEmail/Config.pm
new file mode 100644
index 000000000..ea05f363c
--- /dev/null
+++ b/extensions/LimitedEmail/Config.pm
@@ -0,0 +1,22 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::LimitedEmail;
+
+use strict;
+use constant NAME => 'LimitedEmail';
+use constant REQUIRED_MODULES => [ ];
+use constant OPTIONAL_MODULES => [ ];
+
+use constant FILTERS => [
+ qr/^(?:glob|dkl|justdave|shyam)\@mozilla\.com$/i,
+ qr/^byron\.jones\@gmail\.com$/i,
+ qr/^gerv\@mozilla\.org$/i,
+ qr/^reed\@reedloden\.com$/i,
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/LimitedEmail/Extension.pm b/extensions/LimitedEmail/Extension.pm
new file mode 100644
index 000000000..35cc83567
--- /dev/null
+++ b/extensions/LimitedEmail/Extension.pm
@@ -0,0 +1,62 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::LimitedEmail;
+use strict;
+use base qw(Bugzilla::Extension);
+
+our $VERSION = '2';
+
+use FileHandle;
+use Date::Format;
+use Encode qw(encode_utf8);
+use Bugzilla::Constants qw(bz_locations);
+
+sub mailer_before_send {
+ my ($self, $args) = @_;
+ my $email = $args->{email};
+ my $header = $email->{header};
+ return if $header->header('to') eq '';
+
+ my $blocked = '';
+ if (!deliver_to($header->header('to'))) {
+ $blocked = $header->header('to');
+ $header->header_set(to => '');
+ }
+
+ my $log_filename = bz_locations->{'datadir'} . '/mail.log';
+ my $fh = FileHandle->new(">>$log_filename");
+ if ($fh) {
+ print $fh encode_utf8(sprintf(
+ "[%s] %s%s %s : %s\n",
+ time2str('%D %T', time),
+ ($blocked eq '' ? '' : '(blocked) '),
+ ($blocked eq '' ? $header->header('to') : $blocked),
+ $header->header('X-Bugzilla-Reason') || '-',
+ $header->header('subject')
+ ));
+ $fh->close();
+ }
+}
+
+sub deliver_to {
+ my $email = address_of(shift);
+ my $ra_filters = Bugzilla::Extension::LimitedEmail::FILTERS;
+ foreach my $re (@$ra_filters) {
+ if ($email =~ $re) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+sub address_of {
+ my $email = shift;
+ return $email =~ /<([^>]+)>/ ? $1 : $email;
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/LimitedEmail/disabled b/extensions/LimitedEmail/disabled
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/extensions/LimitedEmail/disabled
diff --git a/extensions/MozProjectReview/Config.pm b/extensions/MozProjectReview/Config.pm
new file mode 100644
index 000000000..5a9d2b730
--- /dev/null
+++ b/extensions/MozProjectReview/Config.pm
@@ -0,0 +1,19 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Extension::MozProjectReview;
+
+use strict;
+
+use constant NAME => 'MozProjectReview';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/MozProjectReview/Extension.pm b/extensions/MozProjectReview/Extension.pm
new file mode 100644
index 000000000..4f132af21
--- /dev/null
+++ b/extensions/MozProjectReview/Extension.pm
@@ -0,0 +1,284 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Extension::MozProjectReview;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::User;
+use Bugzilla::Group;
+use Bugzilla::Error;
+use Bugzilla::Constants;
+
+our $VERSION = '0.01';
+
+# these users will be cc'd to the parent bug when the corresponding child bug
+# is created, as well as the child bug.
+our %auto_cc = (
+ 'legal' => ['liz@mozilla.com'],
+ 'sec-review' => ['curtisk@mozilla.com'],
+ 'finance' => ['waoieong@mozilla.com', 'mcristobal@mozilla.com', 'echoe@mozilla.com'],
+ 'privacy-vendor' => ['smartin@mozilla.com'],
+ 'privacy-project' => ['ahua@mozilla.com'],
+ 'privacy-tech' => ['ahua@mozilla.com'],
+ 'policy-business-partner' => ['smartin@mozilla.com']
+);
+
+sub post_bug_after_creation {
+ my ($self, $args) = @_;
+ my $vars = $args->{'vars'};
+ my $bug = $vars->{'bug'};
+ my $timestamp = $args->{'timestamp'};
+ my $user = Bugzilla->user;
+ my $params = Bugzilla->input_params;
+ my $template = Bugzilla->template;
+
+ return if !($params->{format}
+ && $params->{format} eq 'moz-project-review'
+ && $bug->component eq 'Project Review');
+
+ # do a match if applicable
+ Bugzilla::User::match_field({
+ 'legal_cc' => { 'type' => 'multi' }
+ });
+
+ my ($do_sec_review, $do_legal, $do_finance, $do_privacy_vendor,
+ $do_privacy_tech, $do_privacy_policy);
+
+ if ($params->{'mozilla_data'} eq 'Yes') {
+ $do_legal = 1;
+ $do_privacy_policy = 1;
+ $do_privacy_tech = 1;
+ $do_sec_review = 1;
+ }
+
+ if ($params->{'separate_party'} eq 'Yes') {
+ if ($params->{'relationship_type'} ne 'Hardware Purchase') {
+ $do_legal = 1;
+ }
+
+ if ($params->{'data_access'} eq 'Yes') {
+ $do_privacy_policy = 1;
+ $do_legal = 1;
+ $do_sec_review = 1;
+ }
+
+ if ($params->{'data_access'} eq 'Yes'
+ && $params->{'privacy_policy_vendor_user_data'} eq 'Yes')
+ {
+ $do_privacy_vendor = 1;
+ }
+
+ if ($params->{'vendor_cost'} eq '> $25,000'
+ || ($params->{'vendor_cost'} eq '<= $25,000'
+ && $params->{'po_needed'} eq 'Yes'))
+ {
+ $do_finance = 1;
+ }
+ }
+
+ my ($sec_review_bug, $legal_bug, $finance_bug, $privacy_vendor_bug,
+ $privacy_tech_bug, $privacy_policy_bug, $error, @dep_comment,
+ @dep_errors, @send_mail);
+
+ # Common parameters always passed to _file_child_bug
+ # bug_data and template_suffix will be different for each bug
+ my $child_params = {
+ parent_bug => $bug,
+ template_vars => $vars,
+ dep_comment => \@dep_comment,
+ dep_errors => \@dep_errors,
+ send_mail => \@send_mail,
+ };
+
+ if ($do_sec_review) {
+ $child_params->{'bug_data'} = {
+ short_desc => 'Security Review: ' . $bug->short_desc,
+ product => 'mozilla.org',
+ component => 'Security Assurance: Review Request',
+ bug_severity => 'normal',
+ groups => [ 'mozilla-employee-confidential' ],
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'other',
+ blocked => $bug->bug_id,
+ };
+ $child_params->{'template_suffix'} = 'sec-review';
+ _file_child_bug($child_params);
+ }
+
+ if ($do_legal) {
+ my $component = 'General';
+
+ if ($params->{separate_party} eq 'Yes'
+ && $params->{relationship_type})
+ {
+ $component = ($params->{relationship_type} eq 'Other'
+ || $params->{relationship_type} eq 'Hardware Purchase')
+ ? 'General'
+ : $params->{relationship_type};
+ }
+
+ my $legal_summary = "Legal Review: ";
+ $legal_summary .= $params->{legal_other_party} . " - " if $params->{legal_other_party};
+ $legal_summary .= $bug->short_desc;
+
+ $child_params->{'bug_data'} = {
+ short_desc => $legal_summary,
+ product => 'Legal',
+ component => $component,
+ bug_severity => 'normal',
+ priority => '--',
+ groups => [ 'legal' ],
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'unspecified',
+ blocked => $bug->bug_id,
+ cc => $params->{'legal_cc'},
+ };
+ $child_params->{'template_suffix'} = 'legal';
+ _file_child_bug($child_params);
+ }
+
+ if ($do_finance) {
+ $child_params->{'bug_data'} = {
+ short_desc => 'Finance Review: ' . $bug->short_desc,
+ product => 'Finance',
+ component => 'Purchase Request Form',
+ bug_severity => 'normal',
+ priority => '--',
+ groups => [ 'finance' ],
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'unspecified',
+ blocked => $bug->bug_id,
+ };
+ $child_params->{'template_suffix'} = 'finance';
+ _file_child_bug($child_params);
+ }
+
+ if ($do_privacy_tech) {
+ $child_params->{'bug_data'} = {
+ short_desc => 'Privacy-Technical Review: ' . $bug->short_desc,
+ product => 'mozilla.org',
+ component => 'Security Assurance: Review Request',
+ bug_severity => 'normal',
+ priority => '--',
+ keywords => 'privacy-review-needed',
+ groups => [ 'mozilla-employee-confidential' ],
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'other',
+ blocked => $bug->bug_id,
+ };
+ $child_params->{'template_suffix'} = 'privacy-tech';
+ _file_child_bug($child_params);
+ }
+
+ if ($do_privacy_policy) {
+ $child_params->{'bug_data'} = {
+ short_desc => 'Privacy-Policy Review: ' . $bug->short_desc,
+ product => 'Privacy',
+ component => 'Product Review',
+ bug_severity => 'normal',
+ priority => '--',
+ groups => [ 'mozilla-employee-confidential' ],
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'unspecified',
+ blocked => $bug->bug_id,
+ };
+ $child_params->{'template_suffix'} = 'privacy-policy';
+ _file_child_bug($child_params);
+ }
+
+ if ($do_privacy_vendor) {
+ $child_params->{'bug_data'} = {
+ short_desc => 'Privacy / Vendor Review: ' . $bug->short_desc,
+ product => 'Privacy',
+ component => 'Vendor Review',
+ bug_severity => 'normal',
+ priority => '--',
+ groups => [ 'mozilla-employee-confidential' ],
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'unspecified',
+ blocked => $bug->bug_id,
+ };
+ $child_params->{'template_suffix'} = 'privacy-vendor';
+ _file_child_bug($child_params);
+ }
+
+ if (scalar @dep_errors) {
+ warn "[Bug " . $bug->id . "] Failed to create additional moz-project-review bugs:\n" .
+ join("\n", @dep_errors);
+ $vars->{'message'} = 'moz_project_review_creation_failed';
+ }
+
+ if (scalar @dep_comment) {
+ my $comment = join("\n", @dep_comment);
+ if (scalar @dep_errors) {
+ $comment .= "\n\nSome errors occurred creating dependent bugs and have been recorded";
+ }
+ $bug->add_comment($comment);
+ $bug->update($bug->creation_ts);
+ }
+
+ foreach my $bug_id (@send_mail) {
+ Bugzilla::BugMail::Send($bug_id, { changer => Bugzilla->user });
+ }
+}
+
+sub _file_child_bug {
+ my ($params) = @_;
+ my ($parent_bug, $template_vars, $template_suffix, $bug_data, $dep_comment, $dep_errors, $send_mail)
+ = @$params{qw(parent_bug template_vars template_suffix bug_data dep_comment dep_errors send_mail)};
+
+ my $old_error_mode = Bugzilla->error_mode;
+ Bugzilla->error_mode(ERROR_MODE_DIE);
+
+ my $new_bug;
+ eval {
+ my $comment;
+ my $full_template = "bug/create/comment-moz-project-review-$template_suffix.txt.tmpl";
+ Bugzilla->template->process($full_template, $template_vars, \$comment)
+ || ThrowTemplateError(Bugzilla->template->error());
+ $bug_data->{'comment'} = $comment;
+ if (exists $auto_cc{$template_suffix}) {
+ $bug_data->{'cc'} = $auto_cc{$template_suffix};
+ }
+ if ($new_bug = Bugzilla::Bug->create($bug_data)) {
+ my $set_all = {
+ dependson => { add => [ $new_bug->bug_id ] }
+ };
+ if (exists $auto_cc{$template_suffix}) {
+ $set_all->{'cc'} = { add => $auto_cc{$template_suffix} };
+ }
+ $parent_bug->set_all($set_all);
+ $parent_bug->update($parent_bug->creation_ts);
+ }
+ };
+
+ if ($@ || !($new_bug && $new_bug->{'bug_id'})) {
+ push(@$dep_comment, "Error creating $template_suffix review bug");
+ push(@$dep_errors, "$template_suffix : $@") if $@;
+ # Since we performed Bugzilla::Bug::create in an eval block, we
+ # need to manually rollback the commit as this is not done
+ # in Bugzilla::Error automatically for eval'ed code.
+ Bugzilla->dbh->bz_rollback_transaction();
+ }
+ else {
+ push(@$send_mail, $new_bug->id);
+ push(@$dep_comment, "Bug " . $new_bug->id . " - " . $new_bug->short_desc);
+ }
+
+ undef $@;
+ Bugzilla->error_mode($old_error_mode);
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-finance.txt.tmpl b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-finance.txt.tmpl
new file mode 100644
index 000000000..eaa626a7e
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-finance.txt.tmpl
@@ -0,0 +1,30 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% PROCESS "bug/create/comment-moz-project-review.txt.tmpl" %]
+
+Finance Questions:
+
+Vendor: [% cgi.param('finance_purchase_vendor') %]
+Is this line item in budget?:
+[%+ cgi.param('finance_purchase_inbudget') %]
+What is the purchase for?:
+[%+ cgi.param('finance_purchase_what') %]
+Why is the purchase needed?:
+[%+ cgi.param('finance_purchase_why') %]
+What is the risk if not purchased?:
+[%+ cgi.param('finance_purchase_risk') %]
+What is the alternative?:
+[%+ cgi.param('finance_purchase_alternative') %]
+What is the urgency?: [% cgi.param('finance_purchase_urgency') %]
+What is the shipping address?:
+[%+ cgi.param('finance_shipment_address') %]
+Total Cost: [% cgi.param('finance_purchase_cost') %]
diff --git a/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-legal.txt.tmpl b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-legal.txt.tmpl
new file mode 100644
index 000000000..9856736d1
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-legal.txt.tmpl
@@ -0,0 +1,51 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% PROCESS "bug/create/comment-moz-project-review.txt.tmpl" %]
+
+Legal Questions:
+
+Priority: [% cgi.param('legal_priority') %]
+Time Frame For Completion of Legal Portion?: [% cgi.param('legal_timeframe') %]
+Other Party: [% cgi.param('legal_other_party') %]
+What help do you need from Legal?:
+[%+ cgi.param('legal_help_from_legal') %]
+[% IF cgi.param('legal_vendor_services_where') %]
+Vendor Services from Where:
+[% IF cgi.param('legal_vendor_services_where') == 'A single country' %]
+[%- cgi.param('legal_vendor_single_country') %]
+[% ELSE %]
+[%- cgi.param('legal_vendor_services_where') %]
+[% END %]
+[% END %]
+[% IF cgi.param('separate_party') == 'Yes' && cgi.param('relationship_type') == 'Vendor/Services' %]
+SOW Details:
+Legal Vendor Name: [% cgi.param('legal_sow_vendor_name') %]
+Vendor Address:
+[%+ cgi.param('legal_sow_vendor_address') %]
+Vendor Email for Notices: [% cgi.param('legal_sow_vendor_email') %]
+Mozilla Contact: [% cgi.param('legal_sow_vendor_mozcontact') %]
+Vendor Contact and Email Address: [% cgi.param('legal_sow_vendor_contact') %]
+Description of Services:
+[%+ cgi.param('legal_sow_vendor_services') %]
+Description of Deliverables:
+[%+ cgi.param('legal_sow_vendor_deliverables') %]
+Start Date: [% cgi.param('legal_sow_start_date') %]
+End Date: [% cgi.param('legal_sow_end_date') %]
+Rate of Pay: [% cgi.param('legal_sow_vendor_payment') %]
+Basis for Payment: [% cgi.param('legal_sow_vendor_payment_basis') %]
+Average/Maximum Hours: [% cgi.param('legal_sow_vendor_hours') %]
+Payment Schedule: [% cgi.param('legal_sow_vendor_payment_schedule') %]
+Total Not to Exceed Amount: [% cgi.param('legal_sow_vendor_total_max') %]
+Special Terms:
+[%+ cgi.param('legal_sow_vendor_special_terms') %]
+Product Line: [% cgi.param('legal_sow_vendor_product_line') %]
+[% END %]
diff --git a/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-policy.txt.tmpl b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-policy.txt.tmpl
new file mode 100644
index 000000000..ff3f5adb6
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-policy.txt.tmpl
@@ -0,0 +1,17 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% PROCESS "bug/create/comment-moz-project-review.txt.tmpl" %]
+
+Is there a privacy policy for this new feature/product?:
+[%+ cgi.param('privacy_policy_project_link') %]
+What assistance do you need from the privacy team (if any)?:
+[%+ cgi.param('privacy_policy_project_assistance') %]
diff --git a/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-tech.txt.tmpl b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-tech.txt.tmpl
new file mode 100644
index 000000000..7b72cf1bc
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-tech.txt.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% PROCESS "bug/create/comment-moz-project-review.txt.tmpl" %]
diff --git a/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-vendor.txt.tmpl b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-vendor.txt.tmpl
new file mode 100644
index 000000000..eaf9f12e3
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-vendor.txt.tmpl
@@ -0,0 +1,16 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% PROCESS "bug/create/comment-moz-project-review.txt.tmpl" %]
+
+Privacy Policy: [% cgi.param('privacy_policy_vendor_user_data') %]
+Vendor's Privacy Policy: [% cgi.param('privacy_policy_vendor_link') %]
+Privacy Questionnaire: [% cgi.param('privacy_policy_vendor_questionnaire') %]
diff --git a/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-sec-review.txt.tmpl b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-sec-review.txt.tmpl
new file mode 100644
index 000000000..029f6df48
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-sec-review.txt.tmpl
@@ -0,0 +1,20 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% PROCESS "bug/create/comment-moz-project-review.txt.tmpl" %]
+
+Security Review Questions:
+
+Affects Products: [% cgi.param('sec_affects_products') %]
+Review Due Date: [% cgi.param('sec_review_date') %]
+Review Invitees: [% cgi.param('sec_review_invitees') %]
+Extra Information:
+[%+ cgi.param('sec_review_extra') %]
diff --git a/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review.txt.tmpl b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review.txt.tmpl
new file mode 100644
index 000000000..07d5fa5ad
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review.txt.tmpl
@@ -0,0 +1,33 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Initial Questions:
+
+Project/Feature Name: [% cgi.param('short_desc') %]
+Tracking [% terms.Bug %] ID:[% cgi.param('tracking_id') %]
+Description:
+[%+ cgi.param('description') %]
+Additional Information:
+[%+ cgi.param('additional') %]
+Key Initiative: [% cgi.param('key_initiative') == 'Other'
+ ? cgi.param('key_initiative_other')
+ : cgi.param('key_initiative') %]
+Release Date: [% cgi.param('release_date') %]
+Project Status: [% cgi.param('project_status') %]
+Mozilla Data: [% cgi.param('mozilla_data') %]
+Mozilla Related: [% cgi.param('mozilla_related') %]
+Separate Party: [% cgi.param('separate_party') %]
+[% IF cgi.param('separate_party') == 'Yes' %]
+Type of Relationship: [% cgi.param('relationship_type') %]
+Data Access: [% cgi.param('data_access') %]
+Privacy Policy: [% cgi.param('privacy_policy') %]
+Vendor Cost: [% cgi.param('vendor_cost') %]
+[% END %]
diff --git a/extensions/MozProjectReview/template/en/default/bug/create/create-moz-project-review.html.tmpl b/extensions/MozProjectReview/template/en/default/bug/create/create-moz-project-review.html.tmpl
new file mode 100644
index 000000000..0dd3da5a8
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/bug/create/create-moz-project-review.html.tmpl
@@ -0,0 +1,711 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Project Review"
+ style_urls = [ 'extensions/MozProjectReview/web/style/moz_project_review.css' ]
+ javascript_urls = [ 'js/field.js', 'js/util.js',
+ 'extensions/MozProjectReview/web/js/moz_project_review.js' ]
+ yui = [ 'autocomplete', 'calendar' ]
+%]
+
+<p>
+ <strong>Please use this form for submitting a Mozilla Project Review</strong>
+ If you have a [% terms.bug %] to file, go <a href="enter_bug.cgi">here</a>.
+</p>
+
+<p>
+ (<span class="required_star">*</span> =
+ <span class="required_explanation">Required Field</span>)
+</p>
+
+<form method="post" action="post_bug.cgi" id="mozProjectForm" enctype="multipart/form-data"
+ onSubmit="return MPR.validateAndSubmit();">
+ <input type="hidden" id="product" name="product" value="mozilla.org">
+ <input type="hidden" id="component" name="component" value="Project Review">
+ <input type="hidden" id="rep_platform" name="rep_platform" value="All">
+ <input type="hidden" id="op_sys" name="op_sys" value="All">
+ <input type="hidden" id="priority" name="priority" value="--">
+ <input type="hidden" id="version" name="version" value="other">
+ <input type="hidden" id="format" name="format" value="moz-project-review">
+ <input type="hidden" id="bug_severity" name="bug_severity" value="normal">
+ <input type="hidden" id="token" name="token" value="[% token FILTER html %]">
+
+ <div id="initial_questions">
+ <div class="header">Initial Questions</div>
+
+ <div id="project_feature_summary_row" class="field_row">
+ <span class="field_label required">Project/Feature Name:</span>
+ <span class="field_data">
+ <div class="field_description">Be brief yet descriptive as possible. Include name of product,
+ feature, or name of vendor involved as well if appropriate.</div>
+ <input type="text" name="short_desc" id="short_desc" size="60" maxsize="255">
+ </span>
+ </div>
+
+ <div id="visibility_row" class="field_row">
+ <span class="field_label required">Project Visibility:</span>
+ <span class="field_data">
+ <div class="field_description">
+ Whether project itself is a secret or not (dependent [% terms.bugs %],
+ e.g. Finance and Sec Review, will be made secure whatever you choose).
+ </div>
+ <select name="visibility" id="visibility">
+ <option value="">Select One</option>
+ <option value="public">Public</option>
+ <option value="private">MoCo Confidentional</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="tracking_id_row" class="field_row">
+ <span class="field_label">Tracking [% terms.Bug %] ID:</span>
+ <span class="field_data">
+ <div class="field_description">Master tracking [% terms.bug %] number (if it exists)?</div>
+ <input type="text" name="tracking_id" id="tracking_id" size="60">
+ </span>
+ </div>
+
+ <div id="contacts_row" class="field_row">
+ <span class="field_label required">Points of Contact:</span>
+ <span class="field_data">
+ <div class="field_description">Who are the points of contact for this review?</div>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => ""
+ size => 60
+ classes => ["bz_userfield"]
+ multiple => 5
+ %]
+ </span>
+ </div>
+
+ <div id="description_row" class="field_row">
+ <span class="field_label required">Description:</span>
+ <span class="field_data">
+ <div class="field_description">Please provide a short description of the feature / application / project /
+ business relationship (e.g. problem solved, use cases, etc.)</div>
+ <textarea name="description" id="description" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="additional_row" class="field_row">
+ <span class="field_label">Additional Information:</span>
+ <span class="field_data">
+ <div class="field_description">Please provide links to additional information (e.g. feature page, wiki)
+ if available and not yet included in feature description.)</div>
+ <textarea name="additional" id="additional" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="key_initiative_row" class="field_row">
+ <span class="field_label required">Key Initiative:</span>
+ <span class="field_data">
+ <div class="field_description">Which key initiative does this support?</div>
+ <select name="key_initiative" id="key_initiative">
+ <option value="">Select One</option>
+ <option value="Firefox Desktop">Firefox Desktop</option>
+ <option value="Firefox Mobile">Firefox Mobile</option>
+ <option value="Firefox OS">Firefox OS</option>
+ <option value="Firefox Platform">Firefox Platform</option>
+ <option value="Marketplace / Apps">Marketplace / Apps</option>
+ <option value="Services: Persona">Services: Persona</option>
+ <option value="Services: WebRTC">Services: WebRTC</option>
+ <option value="Services: UP">Services: UP</option>
+ <option value="Services: Social API">Services: Social API</option>
+ <option value="Labs / Research / H3">Labs / Research / H3</option>
+ <option value="Business Support">Business Support</option>
+ <option value="Other">Other</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="key_initiative_other_row" class="field_row bz_default_hidden">
+ <span class="field_label">&nbsp;</span>
+ <span class="field_data">
+ <input type="text" name="key_initiative_other" id="key_initiative_other" size="60">
+ </span>
+ </div>
+
+ <div id="release_date_row" class="field_row">
+ <span class="field_label required">Release Date:</span>
+ <span class="field_data">
+ <div class="field_description">What is your overall key release / launch date / go live date?</div>
+ <input name="release_date" size="20" id="release_date" value=""
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_release_date"
+ onclick="showCalendar('release_date')">
+ <span>Calendar</span>
+ </button>
+ <div id="con_calendar_release_date"></div>
+ <script type="text/javascript">
+ createCalendar('release_date')
+ </script>
+ </span>
+ </div>
+
+ <div id="project_status_row" class="field_row">
+ <span class="field_label required">Project Status:</span>
+ <span class="field_data">
+ <div class="field_description">What is the current state of your project?</div>
+ <select name="project_status" id="project_status">
+ <option value="">Select One</option>
+ <option value="future">Future project under discussion</option>
+ <option value="active">Active planning</option>
+ <option value="development">Development</option>
+ <option value="ready">Ready to launch/commit</option>
+ <option value="launched">Already launched/committed</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="mozilla_data_row" class="field_row">
+ <span class="field_label required">Mozilla Data:</span>
+ <span class="field_data">
+ <div class="field_description">Does this product/service/project access, interact with, or store Mozilla
+ (customer, contributor, user, employee) data? Example of such data includes
+ email addresses, first and last name, addresses, phone numbers, credit card data.)</div>
+ <select name="mozilla_data" id="mozilla_data">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="mozilla_related_row" class="field_row">
+ <span class="field_label">Mozilla Related:</span>
+ <span class="field_data">
+ <div class="field_description">What Mozilla products/services/projects does this product/service/project
+ integrate with or relate to?</div>
+ <input type="text" name="mozilla_related" id="mozilla_related" size="60">
+ </span>
+ </div>
+
+ <div id="separate_party_row" class="field_row">
+ <span class="field_label required">Separate Party:</span>
+ <span class="field_data">
+ <div class="field_description">Hardware Purchases,
+ Vendor agreements, NDAs, Contracts etc</div>
+ <select name="separate_party" id="separate_party">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="initial_separate_party_questions" class="bz_default_hidden">
+ <div id="relation_type_row" class="field_row">
+ <span class="field_label required">Type of Relationship:</span>
+ <span class="field_data">
+ <div class="field_description">What type of relationship?</div>
+ <select name="relationship_type" id="relationship_type">
+ <option value="">Select One</option>
+ <option value="Hardware Purchase">Hardware Purchase</option>
+ <option value="Vendor/Services">Vendor/Services</option>
+ <option value="Distribution/Bundling">Distribution/Bundling</option>
+ <option value="Search">Search</option>
+ <option value="NDA">NDA</option>
+ <option value="Other">Other</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="data_access_row" class="field_row">
+ <span class="field_label required">Data Access:</span>
+ <span class="field_data">
+ <div class="field_description">Will the other party have access to Mozilla (customer, contributor, user,
+ employee) data? (If this is for an NDA, choose no)</div>
+ <select name="data_access" id="data_access">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="privacy_policy_row" class="field_row">
+ <span class="field_label">Privacy Policy:</span>
+ <span class="field_data">
+ <div class="field_description">What is the url for their privacy policy?</div>
+ <input type="text" name="privacy_policy" id="privacy_policy" size="60">
+ </span>
+ </div>
+
+ <div id="vendor_cost_row" class="field_row">
+ <span class="field_label required">Vendor Cost:</span>
+ <span class="field_data">
+ <div class="field_description">What is the anticipated cost of the vendor relationship?
+ (Entire Contract Cost, not monthly cost)</div>
+ <select name="vendor_cost" id="vendor_cost">
+ <option value="">Select One</option>
+ <option value="N/A">N/A</option>
+ <option value="&lt;= $25,000">&lt;= $25,000</option>
+ <option value="&gt; $25,000">&gt; $25,000</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="po_needed_row" class="field_row bz_default_hidden">
+ <span class="field_label required">PO Needed?:</span>
+ <span class="field_data">
+ <select name="po_needed" id="po_needed">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <div id="sec_review_questions" class="bz_default_hidden">
+ <div class="header">Security Review</div>
+
+ <div id="sec_review_affects_products_row">
+ <span class="field_label">Affects Products:</span>
+ <span class="field_data">
+ <div class="field_description">Does this feature or code change affect Firefox, Thunderbird or any
+ product or service the Mozilla ships to end users?</div>
+ <select name="sec_affects_products" id="sec_affects_products">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="sec_review_date_row" class="field_row">
+ <span class="field_label">Review Due Date:</span>
+ <span class="field_data">
+ <div class="field_description">When would you like the review to be completed?
+ (<a href="https://mail.mozilla.com/home/ckoenig@mozilla.com/Security%20Review.html"
+ target="_blank">more info</a>)</div>
+ <input name="sec_review_date" size="20" id="sec_review_date" value=""
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_sec_review_date"
+ onclick="showCalendar('sec_review_date')">
+ <span>Calendar</span>
+ </button>
+ <div id="con_calendar_sec_review_date"></div>
+ <script type="text/javascript">
+ createCalendar('sec_review_date')
+ </script>
+ </span>
+ </div>
+
+ <div id="sec_review_invitees_row" class="field_row">
+ <span class="field_label">Review Invitees:</span>
+ <span class="field_data">
+ <div class="field_description">Whom should be invited to the review?</div>
+ <input type="text" name="sec_review_invitees" id="sec_review_invitees" size="60">
+ </span>
+ </div>
+
+ <div id="sec_review_extra_row" class="field_row">
+ <span class="field_label">Extra Information:</span>
+ <span class="field_data">
+ <div class="field_description">If you feel something is missing here or you would like to provide other
+ kind of feedback, feel free to do so here?</div>
+ <textarea name="sec_review_extra" id="sec_review_extra" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+ </div>
+
+ <div id="privacy_policy_project_questions" class="bz_default_hidden">
+ <div class="header">Privacy (Policy/Project)</div>
+
+ <div id="privacy_policy_project_link_row" class="field_row">
+ <span class="field_label">Is there a privacy policy for<br>this new feature/product?:</span>
+ <span class="field_data">
+ <div class="field_description">If yes, please enter a url to the policy.</div>
+ <input type="text" name="privacy_policy_project_link" id="privacy_policy_project_link" size="60">
+ </span>
+ </div>
+
+ <div id="privacy_policy_project_assistance_row" class="field_row">
+ <span class="field_label required">What assistance do you need from the privacy team (if any)?:</span>
+ <span class="field_data">
+ <textarea name="privacy_policy_project_assistance" id="privacy_policy_project_assistance" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+ </div>
+
+ <div id="privacy_policy_vendor_questions" class="bz_default_hidden">
+ <div class="header">Privacy (Policy/Vendor)</div>
+
+ <div id="privacy_policy_vendor_user_data_row" class="field_row">
+ <span class="field_label">Privacy Policy:</span>
+ <span class="field_data">
+ <div class="field_description">Will the vendor have access to Mozilla (customer, contributor, user, employee) data?</div>
+ <select name="privacy_policy_vendor_user_data" id="privacy_policy_vendor_user_data">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="privacy_policy_vendor_extra" class="bz_default_hidden">
+ <div id="privacy_policy_vendor_link_row" class="field_row">
+ <span class="field_label">Vendor's Privacy Policy:</span>
+ <span class="field_data">
+ <div class="field_description">Please provide link to vendor's privacy policy</div>
+ <input type="text" name="privacy_policy_vendor_link" id="privacy_policy_vendor_link" size="60">
+ </span>
+ </div>
+
+ <div id="privacy_policy_vendor_questionnaire_row" class="field_row">
+ <span class="field_label">Privacy Questionnaire:</span>
+ <span class="field_data">
+ <div class="field_description">Has vendor completed Mozilla Vendor Privacy Questionnaire?</div>
+ <select name="privacy_policy_vendor_questionnaire" id="privacy_policy_vendor_questionnaire">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <div id="legal_questions" class="bz_default_hidden">
+ <div class="header">Legal</div>
+
+ <div id="legal_priority_row" class="field_row">
+ <span class="field_label required">Priority:</span>
+ <span class="field_data">
+ <div class="field_description">Priority to your team</div>
+ <select name="legal_priority" id="legal_priority">
+ <option value="">Select One</option>
+ <option value="high">High</option>
+ <option value="medium">Medium</option>
+ <option value="low">Low</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="legal_timeframe_row" class="field_row">
+ <span class="field_label required">Time Frame For Completion<br>of Legal Portion?:</span>
+ <span class="field_data">
+ <div class="field_description">What is the desired time frame to have the legal component/involvement completed.</div>
+ <select name="legal_timeframe" id="legal_timeframe">
+ <option value="2 days">2 days</option>
+ <option value="a week">a week</option>
+ <option value="2-4 weeks">2-4 weeks</option>
+ <option value="will take a while but please start soon">
+ will take a while but please start soon</option>
+ <option value="no rush" selected>no rush</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="legal_cc_row" class="field_row">
+ <span class="field_label">Cc:</span>
+ <span class="field_data">
+ [% INCLUDE global/userselect.html.tmpl
+ id => "legal_cc"
+ name => "legal_cc"
+ value => ""
+ size => 60
+ classes => ["bz_userfield"]
+ multiple => 5
+ %]
+ </span>
+ </div>
+
+ <div id="legal_other_party_row" class="field_row">
+ <span class="field_label">Other Party:</span>
+ <span class="field_data">
+ <div class="field_description">Name of other party involved</div>
+ <input type="text" name="legal_other_party" id="legal_other_party" size="60">
+ </span>
+ </div>
+
+ <div id="legal_help_from_legal_row" class="field_row">
+ <span class="field_label required">What help do you<br>need from Legal?</span>
+ <span class="field_data">
+ <div class="field_description">
+ Please explain specifically what help you need from Legal. If none, put "No Legal help needed."</div>
+ <textarea name="legal_help_from_legal" id="legal_help_from_legal" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="legal_sow_questions" class="bz_default_hidden">
+ <div class=field_row">
+ <span class="field_label">SOW Details:</span>
+ <span class="field_data">
+ Please provide the following information for the SOW
+ </span>
+ </div>
+
+ <div id="legal_sow_vendor_name_row" class="field_row">
+ <span class="field_label required">Legal Vendor Name:</span>
+ <span class="field_data">
+ <input type="text" name="legal_sow_vendor_name" id="legal_sow_vendor_name" size="60">
+ </span>
+ </div>
+
+ <div id="legal_sow_vendor_address_row" class="field_row">
+ <span class="field_label required">Vendor Address:</span>
+ <span class="field_data">
+ <textarea name="legal_sow_vendor_address" id="legal_sow_vendor_address" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="legal_sow_vendor_email_row" class="field_row">
+ <span class="field_label required">Vendor Email for Notices:</span>
+ <span class="field_data">
+ <input type="text" name="legal_sow_vendor_email" id="legal_sow_vendor_email" size="60">
+ </span>
+ </div>
+
+ <div id="legal_sow_vendor_mozcontact_row" class="field_row">
+ <span class="field_label required">Main Mozilla Contact:</span>
+ <span class="field_data">
+ [% INCLUDE global/userselect.html.tmpl
+ id => "legal_sow_vendor_mozcontact"
+ name => "legal_sow_vendor_mozcontact"
+ value => ""
+ size => 60
+ classes => ["bz_userfield"]
+ multiple => 5
+ %]
+ </span>
+ </div>
+
+ <div id="legal_sow_vendor_contact_row" class="field_row">
+ <span class="field_label required">Main Vendor Contact and Email:</span>
+ <span class="field_data">
+ <input type="text" name="legal_sow_vendor_contact" id="legal_sow_vendor_contact" size="60">
+ </span>
+ </div>
+
+ <div id="legal_sow_vendor_services_row" class="field_row">
+ <span class="field_label required">Vendor Services to be Provided:</span>
+ <span class="field_data">
+ <textarea name="legal_sow_vendor_services" id="legal_sow_vendor_services" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="legal_sow_vendor_deliverables_row" class="field_row">
+ <span class="field_label required">Description of Deliverables:</span>
+ <span class="field_data">
+ <textarea name="legal_sow_vendor_deliverables" id="legal_sow_vendor_deliverables" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="legal_sow_start_date_row" class="field_row">
+ <span class="field_label required">Start Date:</span>
+ <span class="field_data">
+ <input name="legal_sow_start_date" size="20" id="legal_sow_start_date" value=""
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_legal_sow_start_date"
+ onclick="showCalendar('legal_sow_start_date')">
+ <span>Calendar</span>
+ </button>
+ <div id="con_calendar_legal_sow_start_date"></div>
+ <script type="text/javascript">
+ createCalendar('legal_sow_start_date')
+ </script>
+ </span>
+ </div>
+
+ <div id="legal_sow_end_date_row" class="field_row">
+ <span class="field_label required">End Date:</span>
+ <span class="field_data">
+ <input name="legal_sow_end_date" size="20" id="legal_sow_end_date" value=""
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_legal_sow_end_date"
+ onclick="showCalendar('legal_sow_end_date')">
+ <span>Calendar</span>
+ </button>
+ <div id="con_calendar_legal_sow_end_date"></div>
+ <script type="text/javascript">
+ createCalendar('legal_sow_end_date')
+ </script>
+ </span>
+ </div>
+
+ <div id="legal_sow_vendor_payment_row" class="field_row">
+ <span class="field_label required">Rate of Pay:</span>
+ <span class="field_data">
+ <div class="field_description">Include currency</div>
+ <input type="text" name="legal_sow_vendor_payment" id="legal_sow_vendor_payment" size="60">
+ </span>
+ </div>
+
+ <div id="legal_sow_vendor_payment_basis_row" class="field_row">
+ <span class="field_label required">Basis for Payment:</span>
+ <span class="field_data">
+ <div class="field_description">hourly, flat fee, per deliverable, etc.</div>
+ <input type="text" name="legal_sow_vendor_payment_basis" id="legal_sow_vendor_payment_basis" size="60">
+ </span>
+ </div>
+
+ <div id="legal_sow_vendor_hours_row" class="field_row">
+ <span class="field_label">Average/Max Hours:</span>
+ <span class="field_data">
+ <div class="field_description">If hourly, either average or maximum hours per week/month</div>
+ <input type="text" name="legal_sow_vendor_hours" id="legal_sow_vendor_hours" size="60">
+ </span>
+ </div>
+
+ <div id="legal_sow_vendor_payment_schedule_row" class="field_row">
+ <span class="field_label required">Payment Schedule:</span>
+ <span class="field_data">
+ <div class="field_description">"When will we make payments? E.g. every 30 days; half due up front,
+ half on completion; following acceptance of each deliverable, etc.</div>
+ <input type="text" name="legal_sow_vendor_payment_schedule" id="legal_sow_vendor_payment_schedule" size="60">
+ </span>
+ </div>
+
+ <div id="legal_sow_vendor_total_max_row" class="field_row">
+ <span class="field_label required">Total Not to Exceed Amount:</span>
+ <span class="field_data">
+ <input type="text" name="legal_sow_vendor_total_max" id="legal_sow_vendor_total_max" size="60">
+ </span>
+ </div>
+
+ <div id="legal_sow_vendor_special_terms_row" class="field_row">
+ <span class="field_label">Any Special Terms:</span>
+ <span class="field_data">
+ <textarea name="legal_sow_vendor_special_terms" id="legal_sow_vendor_special_terms" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="legal_sow_vendor_product_line_row" class="field_row">
+ <span class="field_label required">Product Line:</span>
+ <span class="field_data">
+ <select id="legal_sow_vendor_product_line" name="legal_sow_vendor_product_line">
+ <option value="">Select One</option>
+ <option value="Firefox OS">Firefox OS</option>
+ <option value="Firefox Desktop">Firefox Desktop</option>
+ <option value="Firefox Mobile">Firefox Mobile</option>
+ <option value="Firefox Platform">Firefox Platform</option>
+ <option value="Marketplace/Apps">Marketplace/Apps</option>
+ <option value="Lab/Research">Lab/Research</option>
+ <option value="Services">Services</option>
+ <option value="Product Support">Product Support</option>
+ <option value="Corp Support">Corp Support</option>
+ </select>
+ </span>
+ </div>
+ </div>
+
+ <div id="legal_vendor_services_where_row" class="field_row bz_default_hidden">
+ <span class="field_label required">Vendor Services Location:</span>
+ <span class="field_data">
+ <div class="field_description">Where will the services primarily be provided?</div>
+ <select name="legal_vendor_services_where" id="legal_vendor_services_where">
+ <option value="">Select One</option>
+ <option value="U.S.">U.S.</option>
+ <option value="Europe">Europe</option>
+ <option value="Canada">Canada</option>
+ <option value="Global">Global</option>
+ <option value="Another region of the world">Another region of the world</option>
+ <option value="A single country">A single country</option>
+ </select>
+ <br>
+ <input class="bz_default_hidden" type="text"
+ name="legal_vendor_single_country" id="legal_vendor_single_country" size="60">
+ </span>
+ </div>
+ </div>
+
+ <div id="finance_questions" class="bz_default_hidden">
+ <div class="header">Finance</div>
+
+ <div id="finance_purchase_vendor_row" class="field_row">
+ <span class="field_label required">Vendor:</span>
+ <span class="field_data">
+ <input type="text" name="finance_purchase_vendor" maxsize="255" size="60" id="finance_purchase_vendor">
+ </span>
+ </div>
+
+ <div id="finance_purchase_inbudget_row" class="field_row">
+ <span class="field_label required">Is this line item in budget?:</span>
+ <span class="field_data">
+ <div class="field_description">If not, please explain purchase need and why not in budget.</div>
+ <textarea name="finance_purchase_inbudget" id="finance_purchase_inbudget" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="finance_purchase_what_row" class="field_row">
+ <span class="field_label required">What is the purchase for?:</span>
+ <span class="field_data">
+ <textarea name="finance_purchase_what" id="finance_purchase_what" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="finance_purchase_why_row" class="field_row">
+ <span class="field_label required">Why is the purchase needed?:</span>
+ <span class="field_data">
+ <textarea name="finance_purchase_why" id="finance_purchase_why" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="finance_purchase_risk_row" class="field_row">
+ <span class="field_label required">What is the risk<br>if not purchased?:</span>
+ <span class="field_data">
+ <textarea name="finance_purchase_risk" id="finance_purchase_risk" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="finance_purchase_alternative_row" class="field_row">
+ <span class="field_label required">What is the alternative?:</span>
+ <span class="field_data">
+ <textarea name="finance_purchase_alternative" id="finance_purchase_alternative" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="finance_purchase_urgency_row" class="field_row">
+ <span class="field_label required">When do the items need<br>to be purchased by?:</span>
+ <span class="field_data">
+ <select name="finance_purchase_urgency" id="finance_purchase_urgency">
+ <option value="3 to 5 days">3 to 5 days</option>
+ <option value="7 to 10 days">7 to 10 days</option>
+ <option value="two weeks">two weeks</option>
+ <option value="no rush" selected>no rush</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="finance_shipment_address_row" class="field_row">
+ <span class="field_label">What is the shipment address<br>(if applicable)?:</span>
+ <span class="field_data">
+ <div class="field_description">Please enter the full address.</div>
+ <textarea name="finance_shipment_address" id="finance_shipment_address" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="finance_purchase_cost_row" class="field_row">
+ <span class="field_label required">Total Cost:</span>
+ <span class="field_data">
+ <div class="field_description">Please include currency type (e.g. USD, EUR)</div>
+ <input type="text" name="finance_purchase_cost" id="finance_purchase_cost" size="60">
+ </span>
+ </div>
+ </div>
+
+ <input type="submit" id="commit" value="Submit Review">
+</form>
+
+<p>
+ Thanks for contacting us. You will be notified by email of any progress made in resolving your request.
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/MozProjectReview/template/en/default/hook/global/messages-messages.html.tmpl b/extensions/MozProjectReview/template/en/default/hook/global/messages-messages.html.tmpl
new file mode 100644
index 000000000..ac7c1f6c7
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/hook/global/messages-messages.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF message_tag == "moz_project_review_creation_failed" %]
+ The parent [% terms.bug %] was created successfully, but creation of
+ the dependent [% terms.bugs %] failed. The error has been logged
+ and no further action is required at this time.
+[% END %]
diff --git a/extensions/MozProjectReview/web/js/moz_project_review.js b/extensions/MozProjectReview/web/js/moz_project_review.js
new file mode 100644
index 000000000..22e79f707
--- /dev/null
+++ b/extensions/MozProjectReview/web/js/moz_project_review.js
@@ -0,0 +1,267 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+var Dom = YAHOO.util.Dom;
+var Event = YAHOO.util.Event;
+
+var MPR = {
+ required_fields: {
+ "initial_questions": {
+ "short_desc": "Please enter a value for project or feature name in the initial questions section",
+ "visibility": "Please select a value for project visibility in the initial questions section",
+ "cc": "Please enter a value for points of contact in the initial questions section",
+ "key_initiative": "Please select a value for key initiative in the initial questions section",
+ "release_date": "Please enter a value for release date in the initial questions section",
+ "project_status": "Please select a value for project status in the initial questions section",
+ "mozilla_data": "Please select a value for mozilla data in the initial questions section",
+ "separate_party": "Please select a value for separate party in the initial questions section"
+ },
+ "finance_questions": {
+ "finance_purchase_vendor": "Please enter a value for vendor in the finance questions section",
+ "finance_purchase_what": "Please enter a value for what in the finance questions section",
+ "finance_purchase_why": "Please enter a value for why in the finance questions section",
+ "finance_purchase_risk": "Please enter a value for risk in the finance questions section",
+ "finance_purchase_alternative": "Please enter a value for alternative in the finance questions section",
+ "finance_purchase_inbudget": "Please enter a value for in budget in the finance questions section",
+ "finance_purchase_urgency": "Please select a value for urgency in the finance questions section",
+ "finance_purchase_cost": "Please enter a value for total cost in the finance questions section"
+ },
+ "legal_questions": {
+ "legal_priority": "Please select a value for priority in the legal questions section",
+ "legal_timeframe": "Please select a value for timeframe in the legal questions section",
+ "legal_help_from_legal": "Please describe the help needed from the Legal department"
+ },
+ "legal_sow_questions": {
+ "legal_sow_vendor_name": "Please enter a value for SOW legal vendor name",
+ "legal_sow_vendor_address": "Please enter a value for SOW vendor address",
+ "legal_sow_vendor_email": "Please enter a value for SOW vendor email for notices",
+ "legal_sow_vendor_mozcontact": "Please enter a value for SOW Mozilla contact",
+ "legal_sow_vendor_contact": "Please enter a value for SOW vendor contact and email address",
+ "legal_sow_vendor_services": "Please enter a value for SOW vendor services description",
+ "legal_sow_vendor_deliverables": "Please enter a value for SOW vendor deliverables description",
+ "legal_sow_start_date": "Please enter a value for SOW vendor start date",
+ "legal_sow_end_date": "Please enter a value for SOW vendor end date",
+ "legal_sow_vendor_payment": "Please enter a value for SOW vendor payment amount",
+ "legal_sow_vendor_payment_basis": "Please enter a value for SOW vendor payment basis",
+ "legal_sow_vendor_payment_schedule": "Please enter a value for SOW vendor payment schedule",
+ "legal_sow_vendor_total_max": "Please enter a value for SOW vendor maximum total to be paid",
+ "legal_sow_vendor_product_line": "Please enter a value for SOW vendor product line"
+ },
+ "privacy_policy_project_questions": {
+ "privacy_policy_project_assistance": "Please enter a value for any assistance needed in the privacy policy project questions section",
+ "privacy_policy_project_link": "Please enter a value for project link in the privacy policy project questions section"
+ }
+ },
+
+ select_inputs: [
+ 'key_initiative',
+ 'project_status',
+ 'mozilla_data',
+ 'separate_party',
+ 'relationship_type',
+ 'data_access',
+ 'vendor_cost',
+ 'po_needed',
+ 'sec_affects_products',
+ 'privacy_policy_vendor_user_data',
+ 'privacy_policy_vendor_questionnaire',
+ 'legal_priority',
+ 'legal_sow_vendor_product_line',
+ 'legal_vendor_services_where',
+ 'finance_purchase_urgency'
+ ],
+
+ init: function () {
+ // Bind the updateSections function to each of the inputs desired
+ for (var i = 0, l = this.select_inputs.length; i < l; i++) {
+ Event.on(this.select_inputs[i], 'change', MPR.updateSections);
+ }
+ MPR.updateSections();
+ },
+
+ fieldValue: function (id) {
+ var field = Dom.get(id);
+ if (!field) return '';
+ if (field.type == 'text'
+ || field.type == 'textarea')
+ {
+ return field.value;
+ }
+ return field.options[field.selectedIndex].value;
+ },
+
+ updateSections: function () {
+ // Sections that will be hidden/shown based on the input values
+ // Start out as all false except for initial questions which is always visible
+ var page_sections = {
+ initial_questions: true,
+ key_initiative_other_row: false,
+ initial_separate_party_questions: false,
+ finance_questions: false,
+ po_needed_row: false,
+ legal_questions: false,
+ legal_sow_questions: false,
+ legal_vendor_single_country: false,
+ legal_vendor_services_where_row: false,
+ sec_review_questions: false,
+ privacy_policy_project_questions: false,
+ privacy_policy_vendor_questions: false,
+ privacy_policy_vendor_extra: false
+ };
+
+ if (MPR.fieldValue('key_initiative') == 'Other') {
+ page_sections.key_initiative_other_row = true;
+ }
+
+ if (MPR.fieldValue('mozilla_data') == 'Yes') {
+ page_sections.legal_questions = true;
+ page_sections.privacy_policy_project_questions = true;
+ page_sections.sec_review_questions = true;
+ }
+
+ if (MPR.fieldValue('separate_party') == 'Yes') {
+ page_sections.initial_separate_party_questions = true;
+
+ if (MPR.fieldValue('relationship_type')
+ && MPR.fieldValue('relationship_type') != 'Hardware Purchase')
+ {
+ page_sections.legal_questions = true;
+ }
+
+ if (MPR.fieldValue('relationship_type') == 'Vendor/Services'
+ || MPR.fieldValue('relationship_type') == 'Distribution/Bundling')
+ {
+ page_sections.legal_sow_questions = true;
+ page_sections.legal_vendor_services_where_row = true;
+ }
+
+ if (MPR.fieldValue('relationship_type') == 'Hardware Purchase') {
+ page_sections.finance_questions = true;
+ }
+
+ if (MPR.fieldValue('data_access') == 'Yes') {
+ page_sections.legal_questions = true;
+ page_sections.sec_review_questions = true;
+ page_sections.privacy_policy_vendor_questions = true;
+ }
+
+ if (MPR.fieldValue('vendor_cost') == '<= $25,000') {
+ page_sections.po_needed_row = true;
+ }
+
+ if (MPR.fieldValue('po_needed') == 'Yes') {
+ page_sections.finance_questions = true;
+ }
+
+ if (MPR.fieldValue('vendor_cost') == '> $25,000') {
+ page_sections.finance_questions = true;
+ }
+ }
+
+ if (MPR.fieldValue('legal_vendor_services_where') == 'A single country') {
+ page_sections.legal_vendor_single_country = true;
+ }
+
+ if (MPR.fieldValue('privacy_policy_vendor_user_data') == 'Yes') {
+ page_sections.privacy_policy_vendor_extra = true;
+ }
+
+ // Toggle the individual page_sections
+ for (section in page_sections) {
+ MPR.toggleShowSection(section, page_sections[section]);
+ }
+ },
+
+ toggleShowSection: function (section, show) {
+ if (show) {
+ Dom.removeClass(section, 'bz_default_hidden');
+ }
+ else {
+ Dom.addClass(section ,'bz_default_hidden');
+ }
+ },
+
+ validateAndSubmit: function () {
+ var alert_text = '';
+ var section = '';
+ for (section in this.required_fields) {
+ if (!Dom.hasClass(section, 'bz_default_hidden')) {
+ var field = '';
+ for (field in MPR.required_fields[section]) {
+ if (!MPR.isFilledOut(field)) {
+ alert_text += this.required_fields[section][field] + "\n";
+ }
+ }
+ }
+ }
+
+ // Special case checks
+ if (MPR.fieldValue('relationship_type') == 'Vendor/Services'
+ && MPR.fieldValue('legal_vendor_services_where') == '')
+ {
+ alert_text += "Please select a value for vendor services where\n";
+ }
+
+ if (MPR.fieldValue('relationship_type') == 'Vendor/Services'
+ && MPR.fieldValue('legal_vendor_services_where') == 'A single country'
+ && MPR.fieldValue('legal_vendor_single_country') == '')
+ {
+ alert_text += "Please select a value for vendor services where single country\n";
+ }
+
+ if (MPR.fieldValue('key_initiative') == 'Other') {
+ if (!MPR.isFilledOut('key_initiative_other')) {
+ alert_text += "Please enter a value for key initiative in the initial questions section\n";
+ }
+ }
+
+ if (MPR.fieldValue('separate_party') == 'Yes') {
+ if (!MPR.isFilledOut('relationship_type')) {
+ alert_text += "Please select a value for type of relationship\n";
+ }
+ if (!MPR.isFilledOut('data_access')) {
+ alert_text += "Please select a value for data access\n";
+ }
+ if (!MPR.isFilledOut('vendor_cost')) {
+ alert_text += "Please select a value for vendor cost\n";
+ }
+ }
+
+ if (MPR.fieldValue('vendor_cost') == '<= $25,000'
+ && MPR.fieldValue('po_needed') == '')
+ {
+ alert_text += "Please select whether a PO is needed or not\n";
+ }
+
+ if (alert_text) {
+ alert(alert_text);
+ return false;
+ }
+
+ var visibility = MPR.fieldValue('visibility');
+ if (visibility == 'private') {
+ var groups = document.createElement('input');
+ groups.type = 'hidden';
+ groups.name = 'groups';
+ groups.value = 'mozilla-employee-confidential';
+ Dom.get('mozProjectForm').appendChild(groups);
+ }
+
+ return true;
+ },
+
+ //Takes a DOM element id and makes sure that it is filled out
+ isFilledOut: function (elem_id) {
+ var str = MPR.fieldValue(elem_id);
+ return str.length > 0 ? true : false;
+ }
+};
+
+Event.onDOMReady(function () {
+ MPR.init();
+});
diff --git a/extensions/MozProjectReview/web/style/moz_project_review.css b/extensions/MozProjectReview/web/style/moz_project_review.css
new file mode 100644
index 000000000..cf1c3a8b8
--- /dev/null
+++ b/extensions/MozProjectReview/web/style/moz_project_review.css
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+.header {
+ width: 95%;
+ border-bottom: 1px solid rgb(116,126,147);
+ font-size: 1.5em;
+ color: rgb(102, 100, 88);
+ padding-bottom: 5px;
+ margin-bottom: 5px;
+ margin-top: 12px;
+}
+
+.field_row {
+ width: 100%;
+ min-width: 700px;
+ clear: both;
+}
+
+.field_label {
+ float: left;
+ width: 20%;
+}
+
+.field_data {
+ float: left;
+ width: 75%;
+ margin-left: 5px;
+ margin-bottom: 5px;
+}
+
+.field_description {
+ font-style: italic;
+ font-size: 90%;
+ color: rgb(102, 100, 88);
+}
+
+span.required:before {
+ content: "* ";
+}
+
+span.required:before, span.required_star {
+ color: red;
+}
diff --git a/extensions/MyDashboard/Config.pm b/extensions/MyDashboard/Config.pm
new file mode 100644
index 000000000..7c14936ff
--- /dev/null
+++ b/extensions/MyDashboard/Config.pm
@@ -0,0 +1,14 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::MyDashboard;
+
+use strict;
+
+use constant NAME => 'MyDashboard';
+
+__PACKAGE__->NAME;
diff --git a/extensions/MyDashboard/Extension.pm b/extensions/MyDashboard/Extension.pm
new file mode 100644
index 000000000..082f1c562
--- /dev/null
+++ b/extensions/MyDashboard/Extension.pm
@@ -0,0 +1,126 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::MyDashboard;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Search::Saved;
+
+use Bugzilla::Extension::MyDashboard::Queries qw(QUERY_DEFS);
+
+our $VERSION = BUGZILLA_VERSION;
+
+################
+# Installation #
+################
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+
+ my $schema = $args->{schema};
+
+ $schema->{'mydashboard'} = {
+ FIELDS => [
+ namedquery_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'namedqueries',
+ COLUMN => 'id',
+ DELETE => 'CASCADE'}},
+ user_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
+ ],
+ INDEXES => [
+ mydashboard_namedquery_id_idx => {FIELDS => [qw(namedquery_id user_id)],
+ TYPE => 'UNIQUE'},
+ mydashboard_user_id_idx => ['user_id'],
+ ],
+ };
+}
+
+###########
+# Objects #
+###########
+
+BEGIN {
+ *Bugzilla::Search::Saved::in_mydashboard = \&_in_mydashboard;
+}
+
+sub _in_mydashboard {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ return $self->{'in_mydashboard'} if exists $self->{'in_mydashboard'};
+ $self->{'in_mydashboard'} = $dbh->selectrow_array("
+ SELECT 1 FROM mydashboard WHERE namedquery_id = ? AND user_id = ?",
+ undef, $self->id, Bugzilla->user->id);
+ return $self->{'in_mydashboard'};
+}
+
+#############
+# Templates #
+#############
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my $page = $args->{'page_id'};
+ my $vars = $args->{'vars'};
+
+ return if $page ne 'mydashboard.html';
+
+ # require user to be logged in for this page
+ Bugzilla->login(LOGIN_REQUIRED);
+
+ $vars->{queries} = [ QUERY_DEFS ];
+}
+
+#########
+# Hooks #
+#########
+
+sub user_preferences {
+ my ($self, $args) = @_;
+ my $tab = $args->{'current_tab'};
+ return unless $tab eq 'saved-searches';
+
+ my $save = $args->{'save_changes'};
+ my $handled = $args->{'handled'};
+ my $vars = $args->{'vars'};
+
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+ my $params = Bugzilla->input_params;
+
+ if ($save) {
+ my $sth_insert_fp = $dbh->prepare('INSERT INTO mydashboard
+ (namedquery_id, user_id)
+ VALUES (?, ?)');
+ my $sth_delete_fp = $dbh->prepare('DELETE FROM mydashboard
+ WHERE namedquery_id = ?
+ AND user_id = ?');
+ foreach my $q (@{$user->queries}) {
+ if (defined $params->{'in_mydashboard_' . $q->id}) {
+ $sth_insert_fp->execute($q->id, $user->id) if !$q->in_mydashboard;
+ }
+ else {
+ $sth_delete_fp->execute($q->id, $user->id) if $q->in_mydashboard;
+ }
+ }
+ }
+}
+
+sub webservice {
+ my ($self, $args) = @_;
+ my $dispatch = $args->{dispatch};
+ $dispatch->{MyDashboard} = "Bugzilla::Extension::MyDashboard::WebService";
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/MyDashboard/lib/Queries.pm b/extensions/MyDashboard/lib/Queries.pm
new file mode 100644
index 000000000..9dff5abe4
--- /dev/null
+++ b/extensions/MyDashboard/lib/Queries.pm
@@ -0,0 +1,313 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::MyDashboard::Queries;
+
+use strict;
+
+use Bugzilla;
+use Bugzilla::Bug;
+use Bugzilla::CGI;
+use Bugzilla::Search;
+use Bugzilla::Flag;
+use Bugzilla::Status qw(is_open_state);
+use Bugzilla::Util qw(format_time datetime_from);
+
+use Bugzilla::Extension::MyDashboard::Util qw(open_states quoted_open_states);
+use Bugzilla::Extension::MyDashboard::TimeAgo qw(time_ago);
+
+use DateTime;
+
+use base qw(Exporter);
+our @EXPORT = qw(
+ QUERY_ORDER
+ SELECT_COLUMNS
+ QUERY_DEFS
+ query_bugs
+ query_flags
+);
+
+# Default sort order
+use constant QUERY_ORDER => ("changeddate desc", "bug_id");
+
+# List of columns that we will be selecting. In the future this should be configurable
+# Share with buglist.cgi?
+use constant SELECT_COLUMNS => qw(
+ bug_id
+ bug_status
+ short_desc
+ changeddate
+);
+
+sub QUERY_DEFS {
+ my $user = Bugzilla->user;
+
+ my @query_defs = (
+ {
+ name => 'assignedbugs',
+ heading => 'Assigned to You',
+ description => 'The bug has been assigned to you, and it is not resolved or closed.',
+ params => {
+ 'bug_status' => ['__open__'],
+ 'emailassigned_to1' => 1,
+ 'emailtype1' => 'exact',
+ 'email1' => $user->login
+ }
+ },
+ {
+ name => 'newbugs',
+ heading => 'New Reported by You',
+ description => 'You reported the bug; it\'s unconfirmed or new. No one has assigned themselves to fix it yet.',
+ params => {
+ 'bug_status' => ['UNCONFIRMED', 'NEW'],
+ 'emailreporter1' => 1,
+ 'emailtype1' => 'exact',
+ 'email1' => $user->login
+ }
+ },
+ {
+ name => 'inprogressbugs',
+ heading => "In Progress Reported by You",
+ description => 'A developer accepted your bug and is working on it. (It has someone in the "Assigned to" field.)',
+ params => {
+ 'bug_status' => [ map { $_->name } grep($_->name ne 'UNCONFIRMED' && $_->name ne 'NEW', open_states()) ],
+ 'emailreporter1' => 1,
+ 'emailtype1' => 'exact',
+ 'email1' => $user->login
+ }
+ },
+ {
+ name => 'openccbugs',
+ heading => "You Are CC'd On",
+ description => 'You are in the CC list of the bug, so you are watching it.',
+ params => {
+ 'bug_status' => ['__open__'],
+ 'emailcc1' => 1,
+ 'emailtype1' => 'exact',
+ 'email1' => $user->login
+ }
+ },
+ {
+ name => 'mentorbugs',
+ heading => "You Are a Mentor",
+ description => 'You are one of the mentors for the bug.',
+ params => {
+ 'bug_status' => ['__open__'],
+ 'emailbug_mentor1' => 1,
+ 'emailtype1' => 'exact',
+ 'email1' => $user->login
+ }
+ },
+ {
+ name => 'lastvisitedbugs',
+ heading => 'Updated Since Last Visit',
+ description => 'Bugs updated since last visited',
+ params => {
+ o1 => 'lessthan',
+ v1 => '%last_changed%',
+ f1 => 'last_visit_ts',
+ },
+ },
+ {
+ name => 'nevervisitbugs',
+ heading => 'Involved with and Never Visited',
+ description => "Bugs you've never visited, but are involved with",
+ params => {
+ query_format => "advanced",
+ bug_status => ['__open__'],,
+ o1 => "isempty",
+ f1 => "last_visit_ts",
+ j2 => "OR",
+ f2 => "OP",
+ f3 => "assigned_to",
+ o3 => "equals",
+ v3 => $user->login,
+ o4 => "equals",
+ f4 => "reporter",
+ v4 => $user->login,
+ v5 => $user->login,
+ f5 => "qa_contact",
+ o5 => "equals",
+ o6 => "equals",
+ f6 => "cc",
+ v6 => $user->login,
+ f7 => "bug_mentor",
+ o7 => "equals",
+ v7 => $user->login,
+ f9 => "CP",
+ },
+ },
+ );
+
+ if (Bugzilla->params->{'useqacontact'}) {
+ push(@query_defs, {
+ name => 'qacontactbugs',
+ heading => 'You Are QA Contact',
+ description => 'You are the qa contact on this bug, and it is not resolved or closed.',
+ params => {
+ 'bug_status' => ['__open__'],
+ 'emailqa_contact1' => 1,
+ 'emailtype1' => 'exact',
+ 'email1' => $user->login
+ }
+ });
+ }
+
+ if ($user->showmybugslink) {
+ my $query = Bugzilla->params->{mybugstemplate};
+ my $login = $user->login;
+ $query =~ s/%userid%/$login/;
+ $query =~ s/^buglist.cgi\?//;
+ push(@query_defs, {
+ name => 'mybugs',
+ heading => "My Bugs",
+ saved => 1,
+ params => $query,
+ });
+ }
+
+ foreach my $q (@{$user->queries}) {
+ next if !$q->in_mydashboard;
+ push(@query_defs, { name => $q->name,
+ saved => 1,
+ params => $q->url });
+ }
+
+ return @query_defs;
+}
+
+sub query_bugs {
+ my $qdef = shift;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+ my $datetime_now = DateTime->now(time_zone => $user->timezone);
+
+ ## HACK to remove POST
+ delete $ENV{REQUEST_METHOD};
+
+ my $params = new Bugzilla::CGI($qdef->{params});
+
+ my $search = new Bugzilla::Search( fields => [ SELECT_COLUMNS ],
+ params => scalar $params->Vars,
+ order => [ QUERY_ORDER ]);
+ my $data = $search->data;
+
+ my @bugs;
+ foreach my $row (@$data) {
+ my $bug = {};
+ foreach my $column (SELECT_COLUMNS) {
+ $bug->{$column} = shift @$row;
+ if ($column eq 'changeddate') {
+ my $datetime = datetime_from($bug->{$column});
+ $datetime->set_time_zone($user->timezone);
+ $bug->{$column} = $datetime->strftime('%Y-%m-%d %T %Z');
+ $bug->{'changeddate_fancy'} = time_ago($datetime, $datetime_now);
+
+ # Provide a version for use by Bug.history and also for looking up last comment.
+ # We have to set to server's timezone and also subtract one second.
+ $datetime->set_time_zone(Bugzilla->local_timezone);
+ $datetime->subtract(seconds => 1);
+ $bug->{changeddate_api} = $datetime->strftime('%Y-%m-%d %T');
+ }
+ }
+ push(@bugs, $bug);
+ }
+
+ return (\@bugs, $params->canonicalise_query());
+}
+
+sub query_flags {
+ my ($type) = @_;
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+ my $datetime_now = DateTime->now(time_zone => $user->timezone);
+
+ ($type ne 'requestee' || $type ne 'requester')
+ || ThrowCodeError('param_required', { param => 'type' });
+
+ my $match_params = { status => '?' };
+
+ if ($type eq 'requestee') {
+ $match_params->{'requestee_id'} = $user->id;
+ }
+ else {
+ $match_params->{'setter_id'} = $user->id;
+ }
+
+ my $matched = Bugzilla::Flag->match($match_params);
+
+ return [] if !@$matched;
+
+ my @unfiltered_flags;
+ my %all_bugs; # Use hash to filter out duplicates
+ foreach my $flag (@$matched) {
+ next if ($flag->attach_id && $flag->attachment->isprivate && !$user->is_insider);
+
+ my $data = {
+ id => $flag->id,
+ type => $flag->type->name,
+ status => $flag->status,
+ attach_id => $flag->attach_id,
+ is_patch => $flag->attach_id ? $flag->attachment->ispatch : 0,
+ bug_id => $flag->bug_id,
+ requester => $flag->setter->login,
+ requestee => $flag->requestee ? $flag->requestee->login : '',
+ updated => $flag->modification_date,
+ };
+ push(@unfiltered_flags, $data);
+
+ # Record bug id for later retrieval of status/summary
+ $all_bugs{$flag->{'bug_id'}}++;
+ }
+
+ # Filter the bug list based on permission to see the bug
+ my %visible_bugs = map { $_ => 1 } @{ $user->visible_bugs([ keys %all_bugs ]) };
+
+ return [] if !scalar keys %visible_bugs;
+
+ # Get all bug statuses and summaries in one query instead of loading
+ # many separate bug objects
+ my $bug_rows = $dbh->selectall_arrayref("SELECT bug_id, bug_status, short_desc
+ FROM bugs
+ WHERE " . $dbh->sql_in('bug_id', [ keys %visible_bugs ]),
+ { Slice => {} });
+ foreach my $row (@$bug_rows) {
+ $visible_bugs{$row->{'bug_id'}} = {
+ bug_status => $row->{'bug_status'},
+ short_desc => $row->{'short_desc'}
+ };
+ }
+
+ # Now drop out any flags for bugs the user cannot see
+ # or if the user did not want to see closed bugs
+ my @filtered_flags;
+ foreach my $flag (@unfiltered_flags) {
+ # Skip this flag if the bug is not visible to the user
+ next if !$visible_bugs{$flag->{'bug_id'}};
+
+ # Include bug status and summary with each flag
+ $flag->{'bug_status'} = $visible_bugs{$flag->{'bug_id'}}->{'bug_status'};
+ $flag->{'bug_summary'} = $visible_bugs{$flag->{'bug_id'}}->{'short_desc'};
+
+ # Format the updated date specific to the user's timezone
+ # and add the fancy human readable version
+ my $datetime = datetime_from($flag->{'updated'});
+ $datetime->set_time_zone($user->timezone);
+ $flag->{'updated'} = $datetime->strftime('%Y-%m-%d %T %Z');
+ $flag->{'updated_epoch'} = $datetime->epoch;
+ $flag->{'updated_fancy'} = time_ago($datetime, $datetime_now);
+
+ push(@filtered_flags, $flag);
+ }
+
+ return [] if !@filtered_flags;
+
+ # Sort by most recently updated
+ return [ sort { $b->{'updated_epoch'} <=> $a->{'updated_epoch'} } @filtered_flags ];
+}
+
+1;
diff --git a/extensions/MyDashboard/lib/TimeAgo.pm b/extensions/MyDashboard/lib/TimeAgo.pm
new file mode 100644
index 000000000..0206bfebd
--- /dev/null
+++ b/extensions/MyDashboard/lib/TimeAgo.pm
@@ -0,0 +1,179 @@
+package Bugzilla::Extension::MyDashboard::TimeAgo;
+
+use strict;
+use utf8;
+use DateTime;
+use Carp;
+use Exporter qw(import);
+
+use if $ENV{ARCH_64BIT}, 'integer';
+
+our @EXPORT_OK = qw(time_ago);
+
+our $VERSION = '0.06';
+
+my @ranges = (
+ [ -1, 'in the future' ],
+ [ 60, 'just now' ],
+ [ 900, 'a few minutes ago'], # 15*60
+ [ 3000, 'less than an hour ago'], # 50*60
+ [ 4500, 'about an hour ago'], # 75*60
+ [ 7200, 'more than an hour ago'], # 2*60*60
+ [ 21600, 'several hours ago'], # 6*60*60
+ [ 86400, 'today', sub { # 24*60*60
+ my $time = shift;
+ my $now = shift;
+ if ( $time->day < $now->day
+ or $time->month < $now->month
+ or $time->year < $now->year
+ ) {
+ return 'yesterday'
+ }
+ if ($time->hour < 5) {
+ return 'tonight'
+ }
+ if ($time->hour < 10) {
+ return 'this morning'
+ }
+ if ($time->hour < 15) {
+ return 'today'
+ }
+ if ($time->hour < 19) {
+ return 'this afternoon'
+ }
+ return 'this evening'
+ }],
+ [ 172800, 'yesterday'], # 2*24*60*60
+ [ 604800, 'this week'], # 7*24*60*60
+ [ 1209600, 'last week'], # 2*7*24*60*60
+ [ 2678400, 'this month', sub { # 31*24*60*60
+ my $time = shift;
+ my $now = shift;
+ if ($time->year == $now->year and $time->month == $now->month) {
+ return 'this month'
+ }
+ return 'last month'
+ }],
+ [ 5356800, 'last month'], # 2*31*24*60*60
+ [ 24105600, 'several months ago'], # 9*31*24*60*60
+ [ 31536000, 'about a year ago'], # 365*24*60*60
+ [ 34214400, 'last year'], # (365+31)*24*60*60
+ [ 63072000, 'more than a year ago'], # 2*365*24*60*60
+ [ 283824000, 'several years ago'], # 9*365*24*60*60
+ [ 315360000, 'about a decade ago'], # 10*365*24*60*60
+ [ 630720000, 'last decade'], # 20*365*24*60*60
+ [ 2838240000, 'several decades ago'], # 90*365*24*60*60
+ [ 3153600000, 'about a century ago'], # 100*365*24*60*60
+ [ 6307200000, 'last century'], # 200*365*24*60*60
+ [ 6622560000, 'more than a century ago'], # 210*365*24*60*60
+ [ 28382400000, 'several centuries ago'], # 900*365*24*60*60
+ [ 31536000000, 'about a millenium ago'], # 1000*365*24*60*60
+ [ 63072000000, 'more than a millenium ago'], # 2000*365*24*60*60
+);
+
+sub time_ago {
+ my ($time, $now) = @_;
+
+ if (not defined $time or not $time->isa('DateTime')) {
+ croak('DateTime::Duration::Fuzzy::time_ago needs a DateTime object as first parameter')
+ }
+ if (not defined $now) {
+ $now = DateTime->now();
+ }
+ if (not $now->isa('DateTime')) {
+ croak('Invalid second parameter provided to DateTime::Duration::Fuzzy::time_ago; it must be a DateTime object if provided')
+ }
+
+ my $dur = $now->subtract_datetime_absolute($time)->in_units('seconds');
+
+ foreach my $range ( @ranges ) {
+ if ( $dur <= $range->[0] ) {
+ if ( $range->[2] ) {
+ return $range->[2]->($time, $now)
+ }
+ return $range->[1]
+ }
+ }
+
+ return 'millenia ago'
+}
+
+1
+
+__END__
+
+=head1 NAME
+
+DateTime::Duration::Fuzzy -- express dates as fuzzy human-friendly strings
+
+=head1 SYNOPSIS
+
+ use DateTime::Duration::Fuzzy qw(time_ago);
+ use DateTime;
+
+ my $now = DateTime->new(
+ year => 2010, month => 12, day => 12,
+ hour => 19, minute => 59,
+ );
+ my $then = DateTime->new(
+ year => 2010, month => 12, day => 12,
+ hour => 15,
+ );
+ print time_ago($then, $now);
+ # outputs 'several hours ago'
+
+ print time_ago($then);
+ # $now taken from C<time> function
+
+=head1 DESCRIPTION
+
+DateTime::Duration::Fuzzy is inspired from the timeAgo jQuery module
+L<http://timeago.yarp.com/>.
+
+It takes two DateTime objects -- first one representing a moment in the past
+and second optional one representine the present, and returns a human-friendly
+fuzzy expression of the time gone.
+
+=head2 functions
+
+=over 4
+
+=item time_ago($then, $now)
+
+The only exportable function.
+
+First obligatory parameter is a DateTime object.
+
+Second optional parameter is also a DateTime object.
+If it's not provided, then I<now> as the C<time> function returns is
+substituted.
+
+Returns a string expression of the interval between the two DateTime
+objects, like C<several hours ago>, C<yesterday> or <last century>.
+
+=back
+
+=head2 performance
+
+On 64bit machines, it is asvisable to 'use integer', which makes
+the calculations faster. You can turn this on by setting the
+C<ARCH_64BIT> environmental variable to a true value.
+
+If you do this on a 32bit machine, you will get wrong results for
+intervals starting with "several decades ago".
+
+=head1 AUTHOR
+
+Jan Oldrich Kruza, C<< <sixtease at cpan.org> >>
+
+=head1 LICENSE AND COPYRIGHT
+
+Copyright 2010 Jan Oldrich Kruza.
+
+This program is free software; you can redistribute it and/or modify it
+under the terms of either: the GNU General Public License as published
+by the Free Software Foundation; or the Artistic License.
+
+See http://dev.perl.org/licenses/ for more information.
+
+=cut
diff --git a/extensions/MyDashboard/lib/Util.pm b/extensions/MyDashboard/lib/Util.pm
new file mode 100644
index 000000000..fa7cf83b0
--- /dev/null
+++ b/extensions/MyDashboard/lib/Util.pm
@@ -0,0 +1,50 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::MyDashboard::Util;
+
+use strict;
+
+use Bugzilla::CGI;
+use Bugzilla::Search;
+use Bugzilla::Status;
+
+use base qw(Exporter);
+@Bugzilla::Extension::MyDashboard::Util::EXPORT = qw(
+ open_states
+ closed_states
+ quoted_open_states
+ quoted_closed_states
+);
+
+our $_open_states;
+sub open_states {
+ $_open_states ||= Bugzilla::Status->match({ is_open => 1, isactive => 1 });
+ return wantarray ? @$_open_states : $_open_states;
+}
+
+our $_quoted_open_states;
+sub quoted_open_states {
+ my $dbh = Bugzilla->dbh;
+ $_quoted_open_states ||= [ map { $dbh->quote($_->name) } open_states() ];
+ return wantarray ? @$_quoted_open_states : $_quoted_open_states;
+}
+
+our $_closed_states;
+sub closed_states {
+ $_closed_states ||= Bugzilla::Status->match({ is_open => 0, isactive => 1 });
+ return wantarray ? @$_closed_states : $_closed_states;
+}
+
+our $_quoted_closed_states;
+sub quoted_closed_states {
+ my $dbh = Bugzilla->dbh;
+ $_quoted_closed_states ||= [ map { $dbh->quote($_->name) } closed_states() ];
+ return wantarray ? @$_quoted_closed_states : $_quoted_closed_states;
+}
+
+1;
diff --git a/extensions/MyDashboard/lib/WebService.pm b/extensions/MyDashboard/lib/WebService.pm
new file mode 100644
index 000000000..87061eabe
--- /dev/null
+++ b/extensions/MyDashboard/lib/WebService.pm
@@ -0,0 +1,145 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Extension::MyDashboard::WebService;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::WebService Bugzilla::WebService::Bug);
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Util qw(detaint_natural trick_taint template_var datetime_from);
+use Bugzilla::WebService::Util qw(validate);
+
+use Bugzilla::Extension::MyDashboard::Queries qw(QUERY_DEFS query_bugs query_flags);
+
+use constant READ_ONLY => qw(
+ run_bug_query
+ run_flag_query
+);
+
+sub run_last_changes {
+ my ($self, $params) = @_;
+
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+ trick_taint($params->{changeddate_api});
+ trick_taint($params->{bug_id});
+
+ my $last_comment_sql = "
+ SELECT comment_id
+ FROM longdescs
+ WHERE bug_id = ? AND bug_when > ?";
+ if (!$user->is_insider) {
+ $last_comment_sql .= " AND isprivate = 0";
+ }
+ $last_comment_sql .= " LIMIT 1";
+ my $last_comment_sth = $dbh->prepare($last_comment_sql);
+
+ my $last_changes = {};
+ my $activity = $self->history({ ids => [ $params->{bug_id} ],
+ new_since => $params->{changeddate_api} });
+ if (@{$activity->{bugs}[0]{history}}) {
+ my $change_set = $activity->{bugs}[0]{history}[0];
+ $last_changes->{activity} = $change_set->{changes};
+ foreach my $change (@{ $last_changes->{activity} }) {
+ $change->{field_desc}
+ = template_var('field_descs')->{$change->{field_name}} || $change->{field_name};
+ }
+ $last_changes->{email} = $change_set->{who};
+ my $datetime = datetime_from($change_set->{when});
+ $datetime->set_time_zone($user->timezone);
+ $last_changes->{when} = $datetime->strftime('%Y-%m-%d %T %Z');
+ }
+ my $last_comment_id = $dbh->selectrow_array(
+ $last_comment_sth, undef, $params->{bug_id}, $params->{changeddate_api});
+ if ($last_comment_id) {
+ my $comments = $self->comments({ comment_ids => [ $last_comment_id ] });
+ my $comment = $comments->{comments}{$last_comment_id};
+ $last_changes->{comment} = $comment->{text};
+ $last_changes->{email} = $comment->{creator} if !$last_changes->{email};
+ my $datetime = datetime_from($comment->{creation_time});
+ $datetime->set_time_zone($user->timezone);
+ $last_changes->{when} = $datetime->strftime('%Y-%m-%d %T %Z');
+ }
+
+ return { results => [ {last_changes => $last_changes } ] };
+}
+
+sub run_bug_query {
+ my($self, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+ defined $params->{query}
+ || ThrowCodeError('param_required',
+ { function => 'MyDashboard.run_bug_query',
+ param => 'query' });
+
+ my $result;
+ foreach my $qdef (QUERY_DEFS) {
+ next if $qdef->{name} ne $params->{query};
+ my ($bugs, $query_string) = query_bugs($qdef);
+
+ # Add last changes to each bug
+ foreach my $b (@$bugs) {
+ # Set the data type properly for webservice clients
+ # for non-string values.
+ $b->{bug_id} = $self->type('int', $b->{bug_id});
+ }
+
+ $query_string =~ s/^POSTDATA=&//;
+ $qdef->{bugs} = $bugs;
+ $qdef->{buffer} = $query_string;
+ $result = $qdef;
+ last;
+ }
+
+ return { result => $result };
+}
+
+sub run_flag_query {
+ my ($self, $params) =@_;
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+ my $type = $params->{type};
+ $type || ThrowCodeError('param_required',
+ { function => 'MyDashboard.run_flag_query',
+ param => 'type' });
+
+ my $results = query_flags($type);
+
+ # Set the data type properly for webservice clients
+ # for non-string values.
+ foreach my $flag (@$results) {
+ $flag->{id} = $self->type('int', $flag->{id});
+ $flag->{attach_id} = $self->type('int', $flag->{attach_id});
+ $flag->{bug_id} = $self->type('int', $flag->{bug_id});
+ $flag->{is_patch} = $self->type('boolean', $flag->{is_patch});
+ }
+
+ return { result => { $type => $results }};
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Extension::MyDashboard::Webservice - The MyDashboard WebServices API
+
+=head1 DESCRIPTION
+
+This module contains API methods that are useful to user's of bugzilla.mozilla.org.
+
+=head1 METHODS
+
+See L<Bugzilla::WebService> for a description of how parameters are passed,
+and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
diff --git a/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-header.html.tmpl b/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-header.html.tmpl
new file mode 100644
index 000000000..c822ab040
--- /dev/null
+++ b/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-header.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<th>
+ My Dashboard
+</th>
diff --git a/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-row.html.tmpl b/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-row.html.tmpl
new file mode 100644
index 000000000..cd6a36705
--- /dev/null
+++ b/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-row.html.tmpl
@@ -0,0 +1,15 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<td align="center">
+ <input type="checkbox"
+ name="in_mydashboard_[% q.id FILTER html %]"
+ value="1"
+ alt="[% q.name FILTER html %]"
+ [% " checked" IF q.in_mydashboard %]>
+</td>
diff --git a/extensions/MyDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl b/extensions/MyDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl
new file mode 100644
index 000000000..518743ccf
--- /dev/null
+++ b/extensions/MyDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF user.login %]
+ <li><span class="separator"> | </span>
+ <a href="[% urlbase FILTER none %]page.cgi?id=mydashboard.html">My Dashboard</a></li>
+[% END %]
diff --git a/extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl b/extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl
new file mode 100644
index 000000000..16f363f49
--- /dev/null
+++ b/extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl
@@ -0,0 +1,156 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "My Dashboard"
+ style_urls = [ "extensions/MyDashboard/web/styles/mydashboard.css",
+ "extensions/ProdCompSearch/web/styles/prod_comp_search.css" ]
+ yui = ["json", "connection"],
+ javascript_urls = [ "js/yui3/yui/yui-min.js",
+ "extensions/MyDashboard/web/js/query.js",
+ "extensions/MyDashboard/web/js/flags.js",
+ "extensions/ProdCompSearch/web/js/prod_comp_search.js",
+ "js/bug.js" ]
+%]
+
+[% standard_queries = [] %]
+[% saved_queries = [] %]
+[% FOREACH q = queries %]
+ [% standard_queries.push(q) IF !q.saved %]
+ [% saved_queries.push(q) IF q.saved %]
+[% END %]
+
+<script id="last-changes-stub" type="text/x-handlebars-template">
+ <div id="last_changes_stub_{{bug_id}}">Loading...</div>
+</script>
+<script id="last-changes-template" type="text/x-handlebars-template">
+ <div id="last_changes_{{bug_id}}">
+ {{#if email}}
+ <div id="last_changes_header">
+ Last Changes :: {{email}} :: {{when}}
+ </div>
+ {{#if activity}}
+ <table id="activity">
+ {{#each activity}}
+ <tr>
+ <td class="field_label">{{field_desc}}:</td>
+ <td class="field_data">
+ {{#if removed}}
+ {{#unless added}}
+ Removed:
+ {{/unless}}
+ {{removed}}
+ {{/if}}
+ {{#if added}}
+ {{#if removed}}
+ &rarr;
+ {{/if}}
+ {{/if}}
+ {{#if added}}
+ {{#unless removed}}
+ Added:
+ {{/unless}}
+ {{added}}
+ {{/if}}
+ </td>
+ </tr>
+ {{/each}}
+ </table>
+ {{/if}}
+ {{#if comment}}
+ <pre class='bz_comment_text'>{{comment}}</pre>
+ {{/if}}
+ {{else}}
+ This is a new [% terms.bug %] and no changes have been made yet.
+ {{/if}}
+ </div>
+</script>
+
+<script type="text/javascript">
+ [% IF Param('splinter_base') %]
+ MyDashboard.splinter_base = '[% Bugzilla.splinter_review_base FILTER js %]';
+ [% END %]
+</script>
+
+<div id="mydashboard">
+ <div class="yui3-skin-sam">
+ <div id="left">
+ <div id="query_list_container">
+ Choose query:
+ <select id="query" name="query">
+ <optgroup id="standard_queries" label="Standard">
+ [% FOREACH r = standard_queries %]
+ <option value="[% r.name FILTER html %]">[% r.heading || r.name FILTER html %]</option>
+ [% END%]
+ </optgroup>
+ <optgroup id="saved_queries" label="Saved">
+ [% FOREACH r = saved_queries %]
+ <option value="[% r.name FILTER html %]">[% r.heading || r.name FILTER html %]</option>
+ [% END %]
+ </optgroup>
+ </select>
+ <small>
+ (<a href="userprefs.cgi?tab=saved-searches">add or remove saved searches</a>)
+ </small>
+ </div>
+
+ <div id="query_container">
+ <div class="query_heading"></div>
+ <div class="query_description"></div>
+ <span id="query_count_refresh" class="bz_default_hidden">
+ <span class="items_found" id="query_bugs_found">0 [% terms.bugs %] found</span>
+ | <a class="refresh" href="javascript:void(0);" id="query_refresh">Refresh</a>
+ | <a class="markvisited" href="javascript:void(0);" id="query_markvisited">Mark Visited</a>
+ <span class="markvisited bz_default_hidden" id="query_markvisited_text">Mark Visited</span>
+ | <a class="buglist" href="javascript:void(0);" id="query_buglist">Buglist</a>
+ </span>
+ <div id="query_pagination_top"></div>
+ <div id="query_table"></div>
+ </div>
+ </div>
+
+ <div id="right">
+ <div id="prod_comp_search_main">
+ [% PROCESS prodcompsearch/form.html.tmpl
+ input_label = "File a $terms.Bug:"
+ script_name = "enter_bug.cgi"
+ new_tab = 1
+ %]
+ </div>
+
+ <div id="requestee_container">
+ <div class="query_heading">
+ Flags Requested of You
+ </div>
+ <span id="requestee_count_refresh" class="bz_default_hidden">
+ <span class="items_found" id="requestee_flags_found">0 flags found</span>
+ | <a class="refresh" href="javascript:void(0);" id="requestee_refresh">Refresh</a>
+ | <a class="buglist" href="javascript:void(0);" id="requestee_buglist">Buglist</a>
+ </span>
+ <div id="requestee_table"></div>
+ </div>
+
+ <div id="requester_container">
+ <div class="query_heading">
+ Flags You Have Requested
+ </div>
+ <span id="requester_count_refresh" class="bz_default_hidden">
+ <span class="items_found" id="requester_flags_found">0 flags found</span>
+ | <a class="refresh" href="javascript:void(0);" id="requester_refresh">Refresh</a>
+ | <a class="buglist" href="javascript:void(0);" id="requester_buglist">Buglist</a>
+ </span>
+ <div id="requester_table"></div>
+ </div>
+ </div>
+ <div style="clear:both;"></div>
+ </div>
+</div>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/MyDashboard/web/js/flags.js b/extensions/MyDashboard/web/js/flags.js
new file mode 100644
index 000000000..0fcf75618
--- /dev/null
+++ b/extensions/MyDashboard/web/js/flags.js
@@ -0,0 +1,228 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+// Flag tables
+YUI({
+ base: 'js/yui3/',
+ combine: false
+}).use("node", "datatable", "datatable-sort", "json-stringify", "escape",
+ "datatable-datasource", "datasource-io", "datasource-jsonschema", function(Y) {
+ // Common
+ var counter = 0;
+ var dataSource = {
+ requestee: null,
+ requester: null
+ };
+ var dataTable = {
+ requestee: null,
+ requester: null
+ };
+
+ var updateFlagTable = function(type) {
+ if (!type) return;
+
+ counter = counter + 1;
+
+ var callback = {
+ success: function(e) {
+ if (e.response) {
+ Y.one('#' + type + '_count_refresh').removeClass('bz_default_hidden');
+ Y.one("#" + type + "_flags_found").setHTML(
+ e.response.results.length + ' flags found');
+ dataTable[type].set('data', e.response.results);
+ }
+ },
+ failure: function(o) {
+ if (o.error) {
+ alert("Failed to load flag list from Bugzilla:\n\n" + o.error.message);
+ } else {
+ alert("Failed to load flag list from Bugzilla.");
+ }
+ }
+ };
+
+ var json_object = {
+ version: "1.1",
+ method: "MyDashboard.run_flag_query",
+ id: counter,
+ params: { type : type }
+ };
+
+ var stringified = Y.JSON.stringify(json_object);
+
+ Y.one('#' + type + '_count_refresh').addClass('bz_default_hidden');
+
+ dataTable[type].set('data', []);
+ dataTable[type].render("#" + type + "_table");
+ dataTable[type].showMessage('loadingMessage');
+
+ dataSource[type].sendRequest({
+ request: stringified,
+ cfg: {
+ method: "POST",
+ headers: { 'Content-Type': 'application/json' }
+ },
+ callback: callback
+ });
+ };
+
+ var loadBugList = function(type) {
+ if (!type) return;
+ var data = dataTable[type].data;
+ var ids = [];
+ for (var i = 0, l = data.size(); i < l; i++) {
+ ids.push(data.item(i).get('bug_id'));
+ }
+ var url = 'buglist.cgi?bug_id=' + ids.join('%2C');
+ window.open(url, '_blank');
+ };
+
+ var bugLinkFormatter = function(o) {
+ var bug_closed = "";
+ if (o.data.bug_status == 'RESOLVED' || o.data.bug_status == 'VERIFIED') {
+ bug_closed = "bz_closed";
+ }
+ return '<a href="show_bug.cgi?id=' + encodeURIComponent(o.value) +
+ '" target="_blank" ' + 'title="' + Y.Escape.html(o.data.bug_status) + ' - ' +
+ Y.Escape.html(o.data.bug_summary) + '" class="' + Y.Escape.html(bug_closed) +
+ '">' + o.value + '</a>';
+ };
+
+ var updatedFormatter = function(o) {
+ return '<span title="' + Y.Escape.html(o.value) + '">' +
+ Y.Escape.html(o.data.updated_fancy) + '</span>';
+ };
+
+ var requesteeFormatter = function(o) {
+ return o.value
+ ? Y.Escape.html(o.value)
+ : '<i>anyone</i>';
+ };
+
+ var flagNameFormatter = function(o) {
+ if (parseInt(o.data.attach_id)
+ && parseInt(o.data.is_patch)
+ && MyDashboard.splinter_base)
+ {
+ return '<a href="' + MyDashboard.splinter_base +
+ (MyDashboard.splinter_base.indexOf('?') == -1 ? '?' : '&') +
+ 'bug=' + encodeURIComponent(o.data.bug_id) +
+ '&attachment=' + encodeURIComponent(o.data.attach_id) +
+ '" target="_blank" title="Review this patch">' +
+ Y.Escape.html(o.value) + '</a>';
+ }
+ else {
+ return Y.Escape.html(o.value);
+ }
+ };
+
+ // Requestee
+ dataSource.requestee = new Y.DataSource.IO({ source: 'jsonrpc.cgi' });
+ dataSource.requestee.on('error', function(e) {
+ try {
+ var response = Y.JSON.parse(e.data.responseText);
+ if (response.error)
+ e.error.message = response.error.message;
+ } catch(ex) {
+ // ignore
+ }
+ });
+ dataTable.requestee = new Y.DataTable({
+ columns: [
+ { key: "requester", label: "Requester", sortable: true },
+ { key: "type", label: "Flag", sortable: true,
+ formatter: flagNameFormatter, allowHTML: true },
+ { key: "bug_id", label: "Bug", sortable: true,
+ formatter: bugLinkFormatter, allowHTML: true },
+ { key: "updated", label: "Updated", sortable: true,
+ formatter: updatedFormatter, allowHTML: true }
+ ],
+ strings: {
+ emptyMessage: 'No flag data found.',
+ }
+ });
+
+ dataTable.requestee.plug(Y.Plugin.DataTableSort);
+
+ dataTable.requestee.plug(Y.Plugin.DataTableDataSource, {
+ datasource: dataSource.requestee
+ });
+
+ dataSource.requestee.plug(Y.Plugin.DataSourceJSONSchema, {
+ schema: {
+ resultListLocator: "result.result.requestee",
+ resultFields: ["requester", "type", "attach_id", "is_patch", "bug_id",
+ "bug_status", "bug_summary", "updated", "updated_fancy"]
+ }
+ });
+
+ dataTable.requestee.render("#requestee_table");
+
+ Y.one('#requestee_refresh').on('click', function(e) {
+ updateFlagTable('requestee');
+ });
+ Y.one('#requestee_buglist').on('click', function(e) {
+ loadBugList('requestee');
+ });
+
+ // Requester
+ dataSource.requester = new Y.DataSource.IO({ source: 'jsonrpc.cgi' });
+ dataSource.requester.on('error', function(e) {
+ try {
+ var response = Y.JSON.parse(e.data.responseText);
+ if (response.error)
+ e.error.message = response.error.message;
+ } catch(ex) {
+ // ignore
+ }
+ });
+ dataTable.requester = new Y.DataTable({
+ columns: [
+ { key:"requestee", label:"Requestee", sortable:true,
+ formatter: requesteeFormatter, allowHTML: true },
+ { key:"type", label:"Flag", sortable:true,
+ formatter: flagNameFormatter, allowHTML: true },
+ { key:"bug_id", label:"Bug", sortable:true,
+ formatter: bugLinkFormatter, allowHTML: true },
+ { key: "updated", label: "Updated", sortable: true,
+ formatter: updatedFormatter, allowHTML: true }
+ ],
+ strings: {
+ emptyMessage: 'No flag data found.',
+ }
+ });
+
+ dataTable.requester.plug(Y.Plugin.DataTableSort);
+
+ dataTable.requester.plug(Y.Plugin.DataTableDataSource, {
+ datasource: dataSource.requester
+ });
+
+ dataSource.requester.plug(Y.Plugin.DataSourceJSONSchema, {
+ schema: {
+ resultListLocator: "result.result.requester",
+ resultFields: ["requestee", "type", "attach_id", "is_patch", "bug_id",
+ "bug_status", "bug_summary", "updated", "updated_fancy"]
+ }
+ });
+
+ // Initial load
+ Y.on("contentready", function (e) {
+ updateFlagTable("requestee");
+ }, "#requestee_table");
+ Y.on("contentready", function (e) {
+ updateFlagTable("requester");
+ }, "#requester_table");
+
+ Y.one('#requester_refresh').on('click', function(e) {
+ updateFlagTable('requester');
+ });
+ Y.one('#requester_buglist').on('click', function(e) {
+ loadBugList('requester');
+ });
+});
diff --git a/extensions/MyDashboard/web/js/query.js b/extensions/MyDashboard/web/js/query.js
new file mode 100644
index 000000000..4a6b64157
--- /dev/null
+++ b/extensions/MyDashboard/web/js/query.js
@@ -0,0 +1,265 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+if (typeof(MyDashboard) == 'undefined') {
+ var MyDashboard = {};
+}
+
+// Main query code
+YUI({
+ base: 'js/yui3/',
+ combine: false,
+ groups: {
+ gallery: {
+ combine: false,
+ base: 'js/yui3/',
+ patterns: { 'gallery-': {} }
+ }
+ }
+}).use("node", "datatable", "datatable-sort", "datatable-message", "json-stringify",
+ "datatable-datasource", "datasource-io", "datasource-jsonschema", "cookie",
+ "gallery-datatable-row-expansion-bmo", "handlebars", "escape", function(Y) {
+ var counter = 0,
+ bugQueryTable = null,
+ bugQuery = null,
+ lastChangesQuery = null,
+ lastChangesCache = {},
+ default_query = "assignedbugs";
+
+ // Grab last used query name from cookie or use default
+ var query_cookie = Y.Cookie.get("my_dashboard_query");
+ if (query_cookie) {
+ var cookie_value_found = 0;
+ Y.one("#query").get("options").each( function() {
+ if (this.get("value") == query_cookie) {
+ this.set('selected', true);
+ default_query = query_cookie;
+ cookie_value_found = 1;
+ }
+ });
+ if (!cookie_value_found) {
+ Y.Cookie.set("my_dashboard_query", "");
+ }
+ }
+
+ var bugQuery = new Y.DataSource.IO({ source: 'jsonrpc.cgi' });
+
+ bugQuery.plug(Y.Plugin.DataSourceJSONSchema, {
+ schema: {
+ resultListLocator: "result.result.bugs",
+ resultFields: ["bug_id", "changeddate", "changeddate_fancy",
+ "bug_status", "short_desc", "changeddate_api" ],
+ metaFields: {
+ description: "result.result.description",
+ heading: "result.result.heading",
+ buffer: "result.result.buffer"
+ }
+ }
+ });
+
+ bugQuery.on('error', function(e) {
+ try {
+ var response = Y.JSON.parse(e.data.responseText);
+ if (response.error)
+ e.error.message = response.error.message;
+ } catch(ex) {
+ // ignore
+ }
+ });
+
+ var bugQueryCallback = {
+ success: function(e) {
+ if (e.response) {
+ Y.one('#query_count_refresh').removeClass('bz_default_hidden');
+ Y.one("#query_container .query_description").setHTML(e.response.meta.description);
+ Y.one("#query_container .query_heading").setHTML(e.response.meta.heading);
+ Y.one("#query_bugs_found").setHTML(
+ '<a href="buglist.cgi?' + e.response.meta.buffer +
+ '" target="_blank">' + e.response.results.length + ' bugs found</a>');
+ bugQueryTable.set('data', e.response.results);
+ }
+ },
+ failure: function(o) {
+ if (o.error) {
+ alert("Failed to load bug list from Bugzilla:\n\n" + o.error.message);
+ } else {
+ alert("Failed to load bug list from Bugzilla.");
+ }
+ }
+ };
+
+ var updateQueryTable = function(query_name) {
+ if (!query_name) return;
+
+ counter = counter + 1;
+ lastChangesCache = {};
+
+ Y.one('#query_markvisited').removeClass('bz_default_hidden');
+ Y.one('#query_markvisited_text').addClass('bz_default_hidden');
+ Y.one('#query_count_refresh').addClass('bz_default_hidden');
+ bugQueryTable.set('data', []);
+ bugQueryTable.render("#query_table");
+ bugQueryTable.showMessage('loadingMessage');
+
+ var bugQueryParams = {
+ version: "1.1",
+ method: "MyDashboard.run_bug_query",
+ id: counter,
+ params: { query : query_name }
+ };
+
+ bugQuery.sendRequest({
+ request: Y.JSON.stringify(bugQueryParams),
+ cfg: {
+ method: "POST",
+ headers: { 'Content-Type': 'application/json' }
+ },
+ callback: bugQueryCallback
+ });
+ };
+
+ var updatedFormatter = function(o) {
+ return '<span title="' + Y.Escape.html(o.value) + '">' +
+ Y.Escape.html(o.data.changeddate_fancy) + '</span>';
+ };
+
+
+ lastChangesQuery = new Y.DataSource.IO({ source: 'jsonrpc.cgi' });
+
+ lastChangesQuery.plug(Y.Plugin.DataSourceJSONSchema, {
+ schema: {
+ resultListLocator: "result.results",
+ resultFields: ["last_changes"],
+ }
+ });
+
+ lastChangesQuery.on('error', function(e) {
+ try {
+ var response = Y.JSON.parse(e.data.responseText);
+ if (response.error)
+ e.error.message = response.error.message;
+ } catch(ex) {
+ // ignore
+ }
+ });
+
+ bugQueryTable = new Y.DataTable({
+ columns: [
+ { key: Y.Plugin.DataTableRowExpansion.column_key, label: ' ', sortable: false },
+ { key: "bug_id", label: "Bug", allowHTML: true, sortable: true,
+ formatter: '<a href="show_bug.cgi?id={value}" target="_blank">{value}</a>' },
+ { key: "changeddate", label: "Updated", formatter: updatedFormatter,
+ allowHTML: true, sortable: true },
+ { key: "bug_status", label: "Status", sortable: true },
+ { key: "short_desc", label: "Summary", sortable: true },
+ ],
+ });
+
+ var last_changes_source = Y.one('#last-changes-template').getHTML(),
+ last_changes_template = Y.Handlebars.compile(last_changes_source);
+
+ var stub_source = Y.one('#last-changes-stub').getHTML(),
+ stub_template = Y.Handlebars.compile(stub_source);
+
+
+ bugQueryTable.plug(Y.Plugin.DataTableRowExpansion, {
+ uniqueIdKey: 'bug_id',
+ template: function(data) {
+ var bug_id = data.bug_id;
+
+ var lastChangesCallback = {
+ success: function(e) {
+ if (e.response) {
+ var last_changes = e.response.results[0].last_changes;
+ last_changes['bug_id'] = bug_id;
+ lastChangesCache[bug_id] = last_changes;
+ Y.one('#last_changes_stub_' + bug_id).setHTML(last_changes_template(last_changes));
+ }
+ },
+ failure: function(o) {
+ if (o.error) {
+ alert("Failed to load last changes from Bugzilla:\n\n" + o.error.message);
+ } else {
+ alert("Failed to load last changes from Bugzilla.");
+ }
+ }
+ };
+
+ if (!lastChangesCache[bug_id]) {
+ var lastChangesParams = {
+ version: "1.1",
+ method: "MyDashboard.run_last_changes",
+ params: { bug_id: data.bug_id, changeddate_api: data.changeddate_api }
+ };
+
+ lastChangesQuery.sendRequest({
+ request: Y.JSON.stringify(lastChangesParams),
+ cfg: {
+ method: "POST",
+ headers: { 'Content-Type': 'application/json' }
+ },
+ callback: lastChangesCallback
+ });
+
+ return stub_template({bug_id: bug_id});
+ }
+ else {
+ return last_changes_template(lastChangesCache[bug_id]);
+ }
+
+ }
+ });
+
+ bugQueryTable.plug(Y.Plugin.DataTableSort);
+
+ bugQueryTable.plug(Y.Plugin.DataTableDataSource, {
+ datasource: bugQuery
+ });
+
+ // Initial load
+ Y.on("contentready", function (e) {
+ updateQueryTable(default_query);
+ }, "#query_table");
+
+ Y.one('#query').on('change', function(e) {
+ var index = e.target.get('selectedIndex');
+ var selected_value = e.target.get("options").item(index).getAttribute('value');
+ updateQueryTable(selected_value);
+ Y.Cookie.set("my_dashboard_query", selected_value, { expires: new Date("January 12, 2025") });
+ });
+
+ Y.one('#query_refresh').on('click', function(e) {
+ var query_select = Y.one('#query');
+ var index = query_select.get('selectedIndex');
+ var selected_value = query_select.get("options").item(index).getAttribute('value');
+ updateQueryTable(selected_value);
+ });
+
+ Y.one('#query_markvisited').on('click', function(e) {
+ var data = bugQueryTable.data;
+ var bug_ids = [];
+
+ Y.one('#query_markvisited').addClass('bz_default_hidden');
+ Y.one('#query_markvisited_text').removeClass('bz_default_hidden');
+
+ for (var i = 0, l = data.size(); i < l; i++) {
+ bug_ids.push(data.item(i).get('bug_id'));
+ }
+ YAHOO.bugzilla.bugUserLastVisit.update(bug_ids);
+ });
+
+ Y.one('#query_buglist').on('click', function(e) {
+ var data = bugQueryTable.data;
+ var ids = [];
+ for (var i = 0, l = data.size(); i < l; i++) {
+ ids.push(data.item(i).get('bug_id'));
+ }
+ var url = 'buglist.cgi?bug_id=' + ids.join('%2C');
+ window.open(url, '_blank');
+ });
+});
diff --git a/extensions/MyDashboard/web/styles/mydashboard.css b/extensions/MyDashboard/web/styles/mydashboard.css
new file mode 100644
index 000000000..2ce19d96b
--- /dev/null
+++ b/extensions/MyDashboard/web/styles/mydashboard.css
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#mydashboard {
+ min-width: 900px;
+}
+
+#mydashboard .yui3-skin-sam .yui3-datatable-table {
+ width: 100%;
+}
+
+.yui3-datatable-col-changeddate,
+.yui3-datatable-col-created {
+ white-space: nowrap;
+}
+
+.query_heading {
+ font-size: 18px;
+ font-weight: strong;
+ padding-bottom: 5px;
+ padding-top: 5px;
+ color: rgb(72, 72, 72);
+}
+
+.query_description {
+ font-size: 90%;
+ font-style: italic;
+ padding-bottom: 5px;
+ color: rgb(109, 117, 129);
+}
+
+#mydashboard_container {
+ margin: 0 auto;
+}
+
+#left {
+ float: left;
+ width: 58%;
+}
+
+#right {
+ float: right;
+ width: 40%;
+}
+
+.items_found, .refresh, .buglist, .markvisited {
+
+ font-size: 80%;
+}
+
+#query_list_container {
+ text-align:center;
+}
+
+#query_list_container,
+#prod_comp_search_main {
+ padding: 20px !important;
+ height: 40px;
+}
+
+#last_changes_header {
+ font-size: 12px;
+ font-weight: bold;
+ padding-bottom: 5px;
+ border-bottom: 1px solid rgb(200, 200, 186);
+}
+
+#last_changes .field_label {
+ text-align: left;
+}
diff --git a/extensions/Needinfo/Config.pm b/extensions/Needinfo/Config.pm
new file mode 100644
index 000000000..86c99ec59
--- /dev/null
+++ b/extensions/Needinfo/Config.pm
@@ -0,0 +1,18 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Extension::Needinfo;
+use strict;
+
+use constant NAME => 'Needinfo';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/Needinfo/Extension.pm b/extensions/Needinfo/Extension.pm
new file mode 100644
index 000000000..2a4bfa3b3
--- /dev/null
+++ b/extensions/Needinfo/Extension.pm
@@ -0,0 +1,180 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Extension::Needinfo;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Error;
+use Bugzilla::Flag;
+use Bugzilla::FlagType;
+use Bugzilla::User;
+
+our $VERSION = '0.01';
+
+sub install_update_db {
+ my ($self, $args) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ if (@{ Bugzilla::FlagType::match({ name => 'needinfo' }) }) {
+ return;
+ }
+
+ print "Creating needinfo flag ... " .
+ "enable the Needinfo feature by editing the flag's properties.\n";
+
+ # Initially populate the list of exclusions as __Any__:__Any__ to
+ # allow admin to decide which products to enable the flag for.
+ my $flagtype = Bugzilla::FlagType->create({
+ name => 'needinfo',
+ description => "Set this flag when the bug is in need of additional information",
+ target_type => 'bug',
+ cc_list => '',
+ sortkey => 1,
+ is_active => 1,
+ is_requestable => 1,
+ is_requesteeble => 1,
+ is_multiplicable => 0,
+ request_group => '',
+ grant_group => '',
+ inclusions => [],
+ exclusions => ['0:0'],
+ });
+}
+
+# Clear the needinfo? flag if comment is being given by
+# requestee or someone used the override flag.
+sub bug_start_of_update {
+ my ($self, $args) = @_;
+ my $bug = $args->{bug};
+ my $old_bug = $args->{old_bug};
+
+ my $user = Bugzilla->user;
+ my $cgi = Bugzilla->cgi;
+ my $params = Bugzilla->input_params;
+
+ if ($params->{needinfo}) {
+ # do a match if applicable
+ Bugzilla::User::match_field({
+ 'needinfo_from' => { 'type' => 'multi' }
+ });
+ }
+
+ # Set needinfo_done param to true so as to not loop back here
+ return if $params->{needinfo_done};
+ $params->{needinfo_done} = 1;
+ Bugzilla->input_params($params);
+
+ my $add_needinfo = delete $params->{needinfo};
+ my $needinfo_from = delete $params->{needinfo_from};
+ my $needinfo_role = delete $params->{needinfo_role};
+ my $is_private = $params->{'comment_is_private'};
+
+ my @needinfo_overrides;
+ foreach my $key (grep(/^needinfo_override_/, keys %$params)) {
+ my ($id) = $key =~ /(\d+)$/;
+ # Should always be true if key exists (checkbox) but better to be sure
+ push(@needinfo_overrides, $id) if $id && $params->{$key};
+ }
+
+ # Set the needinfo flag if user is requesting more information
+ my @new_flags;
+ my $needinfo_requestee;
+
+ if ($add_needinfo) {
+ foreach my $type (@{ $bug->flag_types }) {
+ next if $type->name ne 'needinfo';
+ my %requestees;
+
+ # Allow anyone to be the requestee
+ if (!$needinfo_role) {
+ $requestees{'anyone'} = 1;
+ }
+ # Use assigned_to as requestee
+ elsif ($needinfo_role eq 'assigned_to') {
+ $requestees{$bug->assigned_to->login} = 1;
+ }
+ # Use reporter as requestee
+ elsif ($needinfo_role eq 'reporter') {
+ $requestees{$bug->reporter->login} = 1;
+ }
+ # Use qa_contact as requestee
+ elsif ($needinfo_role eq 'qa_contact') {
+ $requestees{$bug->qa_contact->login} = 1;
+ }
+ # Use current user as requestee
+ elsif ($needinfo_role eq 'user') {
+ $requestees{$user->login} = 1;
+ }
+ # Use user specified requestee
+ elsif ($needinfo_role eq 'other' && $needinfo_from) {
+ my @needinfo_from_list = ref $needinfo_from
+ ? @$needinfo_from :
+ ($needinfo_from);
+ foreach my $requestee (@needinfo_from_list) {
+ my $requestee_obj = Bugzilla::User->check($requestee);
+ $requestees{$requestee_obj->login} = 1;
+ }
+ }
+
+ # Find out if the requestee has already been used and skip if so
+ my $requestee_found;
+ foreach my $flag (@{ $type->{flags} }) {
+ if (!$flag->requestee && $requestees{'anyone'}) {
+ delete $requestees{'anyone'};
+ }
+ if ($flag->requestee && $requestees{$flag->requestee->login}) {
+ delete $requestees{$flag->requestee->login};
+ }
+ }
+
+ foreach my $requestee (keys %requestees) {
+ my $needinfo_flag = { type_id => $type->id, status => '?' };
+ if ($requestee ne 'anyone') {
+ $needinfo_flag->{requestee} = $requestee;
+ }
+ push(@new_flags, $needinfo_flag);
+ }
+ }
+ }
+
+ my @flags;
+ foreach my $flag (@{ $bug->flags }) {
+ next if $flag->type->name ne 'needinfo';
+ # Clear if somehow the flag has been set to +/-
+ # or if the "clear needinfo" override checkbox is selected
+ if ($flag->status ne '?'
+ or grep { $_ == $flag->id } @needinfo_overrides)
+ {
+ push(@flags, { id => $flag->id, status => 'X' });
+ }
+ }
+
+ if (@flags || @new_flags) {
+ $bug->set_flags(\@flags, \@new_flags);
+ }
+}
+
+sub object_before_delete {
+ my ($self, $args) = @_;
+ my $object = $args->{object};
+ return unless $object->isa('Bugzilla::Flag')
+ && $object->type->name eq 'needinfo';
+ my $user = Bugzilla->user;
+
+ # Require canconfirm to clear requests targetted at someone else
+ if ($object->setter_id != $user->id
+ && $object->requestee
+ && $object->requestee->id != $user->id
+ && !$user->in_group('canconfirm'))
+ {
+ ThrowUserError('needinfo_illegal_change');
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/Needinfo/template/en/default/bug/needinfo.html.tmpl b/extensions/Needinfo/template/en/default/bug/needinfo.html.tmpl
new file mode 100644
index 000000000..5edd70f72
--- /dev/null
+++ b/extensions/Needinfo/template/en/default/bug/needinfo.html.tmpl
@@ -0,0 +1,199 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% needinfo_flagtype = "" %]
+[% needinfo_flags = [] %]
+
+[% FOREACH type = bug.flag_types %]
+ [% IF type.name == 'needinfo' %]
+ [% needinfo_flagtype = type %]
+ [% FOREACH flag = type.flags %]
+ [% IF flag.status == '?' %]
+ [% needinfo_flags.push(flag) %]
+ [% END %]
+ [% END %]
+ [% LAST IF needinfo_flagtype %]
+ [% END %]
+[% END %]
+
+[%
+ BLOCK needinfo_comment_div;
+ match = needinfo_flags.last.creation_date.match('^(\d{4})\.(\d{2})\.(\d{2})(.+)$');
+ date = "$match.0-$match.1-$match.2$match.3";
+ FOREACH comment IN bug.comments;
+ NEXT IF comment.is_private AND NOT (user.is_insider || user.id == comment.author.id);
+ IF comment.creation_ts == date;
+ GET "c$comment.count";
+ LAST;
+ END;
+ END;
+ END;
+%]
+
+[% IF needinfo_flagtype %]
+ <div id="needinfo_container">
+ [% IF needinfo_flags.size > 0 %]
+ [%# Displays NEEDINFO tag in bug header %]
+ <script>
+ var summary_container = document.getElementById('static_bug_status');
+ if (summary_container) {
+ var needinfo_comment_div = '[% INCLUDE needinfo_comment_div FILTER js %]';
+ if (document.getElementById('inline-history-ext')) {
+ needinfo_comment_div = inline_history.getNeedinfoDiv();
+ }
+
+ if (needinfo_comment_div) {
+ var a = document.createElement('a');
+ a.id = 'needinfo-lnk';
+ a.href = "#" + needinfo_comment_div;
+ a.appendChild(document.createTextNode('NEEDINFO'));
+ summary_container.appendChild(document.createTextNode('['));
+ summary_container.appendChild(a);
+ summary_container.appendChild(document.createTextNode(']'));
+ }
+ else {
+ summary_container.appendChild(document.createTextNode('[NEEDINFO]'));
+ }
+ }
+ </script>
+ [% END %]
+ <table>
+ [% FOREACH flag = needinfo_flags %]
+ <tr>
+ [% IF !flag.requestee || flag.requestee.id == user.id %]
+ [%# needinfo targetted at the current user, or anyone %]
+ <td align="center">
+ <input type="checkbox" id="needinfo_override_[% flag.id FILTER html %]"
+ name="needinfo_override_[% flag.id FILTER html %]" value="1"
+ [% " checked" IF flag.requestee || user.in_group("canconfirm") %]>
+ </td>
+ <td>
+ <label for="needinfo_override_[% flag.id FILTER html %]">
+ Clear the needinfo request for
+ <em>[% IF !flag.requestee %]anyone[% ELSE %][% flag.requestee.login FILTER html %][% END %]</em>.
+ </label>
+ </td>
+ [% ELSIF user.in_group("canconfirm") || flag.setter_id == user.id %]
+ [%# needinfo targetted at someone else, but the user can clear %]
+ <td align="center">
+ <input type="checkbox" id="needinfo_override_[% flag.id FILTER html %]"
+ name="needinfo_override_[% flag.id FILTER html %]" value="1">
+ </td>
+ <td>
+ <label for="needinfo_override_[% flag.id FILTER html %]">
+ I am providing the requested information for <em>[% flag.requestee.login FILTER html %]</em>
+ (clears the needinfo request).
+ </label>
+ </td>
+ [% ELSE %]
+ [%# current user does not have permissions to clear needinfo %]
+ <td>&nbsp;</td>
+ <td>
+ Needinfo requested from <em>[% flag.requestee.login FILTER html %]</em>.
+ </td>
+ [% END %]
+ </tr>
+ [% END %]
+ [% IF needinfo_flags.size == 0 || needinfo_flagtype.is_multiplicable %]
+ <tr>
+ <td align="center">
+ <script>
+ function needinfo_init() {
+ needinfo_visibility();
+ [% FOREACH flag = needinfo_flags %]
+ YAHOO.util.Event.on('requestee-[% flag.id FILTER none %]', 'blur', function(e) {
+ YAHOO.util.Dom.get('needinfo_override_[% flag.id FILTER none %]').checked =
+ e.target.value == '[% flag.requestee.login FILTER js %]';
+ });
+ [% END %]
+ }
+
+ function needinfo_visibility() {
+ var role = YAHOO.util.Dom.get('needinfo_role').value;
+ if (role == 'other') {
+ YAHOO.util.Dom.removeClass('needinfo_from_container', 'bz_default_hidden');
+ YAHOO.util.Dom.get('needinfo_from').disabled = false;
+ YAHOO.util.Dom.get('needinfo_role_identity').innerHTML = '';
+ } else {
+ YAHOO.util.Dom.addClass('needinfo_from_container', 'bz_default_hidden');
+ YAHOO.util.Dom.get('needinfo_from').disabled = true;
+ var identity = '';
+ if (role == 'reporter') {
+ identity = '[% bug.reporter.realname || bug.reporter.login FILTER html FILTER js %]';
+ } else if (role == 'assigned_to') {
+ identity = '[% bug.assigned_to.realname || bug.assigned_to.login FILTER html FILTER js %]';
+ } else if (role == 'qa_contact') {
+ identity = '[% bug.qa_contact.realname || bug.qa_contact.login FILTER html FILTER js %]';
+ } else if (role == 'user') {
+ identity = '[% user.realname || user.login FILTER html FILTER js %]';
+ [% FOREACH mentor = bug.mentors %]
+ } else if (role == '[% mentor.login FILTER js %]') {
+ identity = '[% mentor.realname || mentor.login FILTER html FILTER js +%] [%+ IF bug.mentors.size > 1 %](mentor)[% END %]';
+ [% END %]
+ }
+ YAHOO.util.Dom.get('needinfo_role_identity').innerHTML = identity;
+ }
+ }
+
+ function needinfo_focus() {
+ if (YAHOO.util.Dom.get('needinfo').checked
+ && YAHOO.util.Dom.get('needinfo_role').value == 'other')
+ {
+ YAHOO.util.Dom.get('needinfo_from').focus();
+ YAHOO.util.Dom.get('needinfo_from').select();
+ }
+ }
+
+ function needinfo_role_changed() {
+ YAHOO.util.Dom.get('needinfo').checked = true;
+ needinfo_visibility();
+ needinfo_focus();
+ }
+
+ function needinfo_other_changed() {
+ YAHOO.util.Dom.get('needinfo').checked = YAHOO.util.Dom.get('needinfo_from').value != '';
+ }
+
+ YAHOO.util.Event.onDOMReady(needinfo_init);
+ </script>
+ <input type="checkbox" name="needinfo" value="1" id="needinfo" onchange="needinfo_focus()">
+ </td>
+ <td>
+ <label for="needinfo">Need more information from</label>
+ <select name="needinfo_role" id="needinfo_role" onchange="needinfo_role_changed()">
+ <option value="other">other</option>
+ <option value="reporter">reporter</option>
+ <option value="assigned_to">assignee</option>
+ [% IF Param('useqacontact') && bug.qa_contact.login != "" %]
+ <option value="qa_contact">qa contact</option>
+ [% END %]
+ <option value="user">myself</option>
+ [% FOREACH mentor = bug.mentors %]
+ <option [% IF bug.mentors.size > 1 %]title="mentor"[% END %] value="[% mentor.login FILTER html %]">
+ [% bug.mentors.size == 1 ? "mentor" : mentor.login FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ <span id="needinfo_from_container">
+ [% INCLUDE global/userselect.html.tmpl
+ id => "needinfo_from"
+ name => "needinfo_from"
+ value => ""
+ size => 30
+ multiple => 5
+ onchange => "needinfo_other_changed()"
+ field_title => "Enter one or more comma separated users to request more information from"
+ %]
+ </span>
+ <span id="needinfo_role_identity"></span>
+ </td>
+ </tr>
+ [% END %]
+ </table>
+ </div>
+[% END %]
diff --git a/extensions/Needinfo/template/en/default/hook/attachment/create-form_before_submit.html.tmpl b/extensions/Needinfo/template/en/default/hook/attachment/create-form_before_submit.html.tmpl
new file mode 100644
index 000000000..81b6d57ea
--- /dev/null
+++ b/extensions/Needinfo/template/en/default/hook/attachment/create-form_before_submit.html.tmpl
@@ -0,0 +1,16 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<tr>
+ <td>&nbsp;</td>
+ <td>
+ [% PROCESS bug/needinfo.html.tmpl
+ bug => bug
+ %]
+ </td>
+</tr>
diff --git a/extensions/Needinfo/template/en/default/hook/attachment/edit-after_comment_textarea.html.tmpl b/extensions/Needinfo/template/en/default/hook/attachment/edit-after_comment_textarea.html.tmpl
new file mode 100644
index 000000000..b14de653a
--- /dev/null
+++ b/extensions/Needinfo/template/en/default/hook/attachment/edit-after_comment_textarea.html.tmpl
@@ -0,0 +1,25 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS bug/needinfo.html.tmpl
+ bug => attachment.bug
+%]
+<script type="text/javascript">
+ document.getElementById('editButton').addEventListener('click', function() {
+ document.getElementById('attachment_view_window')
+ .appendChild(document.getElementById('needinfo_container'));
+ });
+ document.getElementById('redoEditButton').addEventListener('click', function() {
+ document.getElementById('attachment_view_window')
+ .appendChild(document.getElementById('needinfo_container'));
+ });
+ document.getElementById('undoEditButton').addEventListener('click', function() {
+ document.getElementById('smallCommentFrame')
+ .appendChild(document.getElementById('needinfo_container'));
+ });
+</script>
diff --git a/extensions/Needinfo/template/en/default/hook/bug/edit-after_comment_commit_button.html.tmpl b/extensions/Needinfo/template/en/default/hook/bug/edit-after_comment_commit_button.html.tmpl
new file mode 100644
index 000000000..90f0cc584
--- /dev/null
+++ b/extensions/Needinfo/template/en/default/hook/bug/edit-after_comment_commit_button.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS bug/needinfo.html.tmpl
+ bug = bug
+%]
diff --git a/extensions/Needinfo/template/en/default/hook/global/header-start.html.tmpl b/extensions/Needinfo/template/en/default/hook/global/header-start.html.tmpl
new file mode 100644
index 000000000..7f2095e3d
--- /dev/null
+++ b/extensions/Needinfo/template/en/default/hook/global/header-start.html.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF template.name == 'attachment/create.html.tmpl'
+ || template.name == 'attachment/edit.html.tmpl' %]
+ [% style_urls.push('extensions/Needinfo/web/styles/needinfo.css') %]
+[% END %]
diff --git a/extensions/Needinfo/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/Needinfo/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..f1241bc61
--- /dev/null
+++ b/extensions/Needinfo/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "needinfo_illegal_change" %]
+ [% title = 'Needinfo Illegal Change' %]
+ Only the requestee or a user with the required permissions can clear a
+ needinfo flag.
+[% END %]
diff --git a/extensions/Needinfo/template/en/default/hook/request/email-after_summary.txt.tmpl b/extensions/Needinfo/template/en/default/hook/request/email-after_summary.txt.tmpl
new file mode 100644
index 000000000..6a302b76a
--- /dev/null
+++ b/extensions/Needinfo/template/en/default/hook/request/email-after_summary.txt.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS flag && flag.type.name == 'needinfo' && flag.status == '?' %]
+---
+--- This request has set a needinfo flag on the [% terms.bug %].
+--- You can clear it by logging in and replying in a comment.
+---
diff --git a/extensions/Needinfo/web/styles/needinfo.css b/extensions/Needinfo/web/styles/needinfo.css
new file mode 100644
index 000000000..e375ba610
--- /dev/null
+++ b/extensions/Needinfo/web/styles/needinfo.css
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#needinfo_container label {
+ font-weight: normal !important;
+}
diff --git a/extensions/OpenGraph/Config.pm b/extensions/OpenGraph/Config.pm
new file mode 100644
index 000000000..9204db234
--- /dev/null
+++ b/extensions/OpenGraph/Config.pm
@@ -0,0 +1,16 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::OpenGraph;
+
+use strict;
+
+use constant NAME => 'OpenGraph';
+use constant REQUIRED_MODULES => [ ];
+use constant OPTIONAL_MODULES => [ ];
+
+__PACKAGE__->NAME;
diff --git a/extensions/OpenGraph/Extension.pm b/extensions/OpenGraph/Extension.pm
new file mode 100644
index 000000000..f278a8958
--- /dev/null
+++ b/extensions/OpenGraph/Extension.pm
@@ -0,0 +1,16 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::OpenGraph;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+our $VERSION = '1';
+
+__PACKAGE__->NAME;
diff --git a/extensions/OpenGraph/template/en/default/hook/global/header-start.html.tmpl b/extensions/OpenGraph/template/en/default/hook/global/header-start.html.tmpl
new file mode 100644
index 000000000..2a6ab37df
--- /dev/null
+++ b/extensions/OpenGraph/template/en/default/hook/global/header-start.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+<meta property="og:type" content="website">
+<meta property="og:image" content="[% urlbase FILTER none %]extensions/OpenGraph/web/bugzilla.png">
+<meta property="og:title" content="[% title FILTER none %]">
+<meta property="og:url" content="[% Bugzilla.cgi.self_url FILTER none %]">
diff --git a/extensions/OpenGraph/web/bugzilla.png b/extensions/OpenGraph/web/bugzilla.png
new file mode 100644
index 000000000..55ee01210
--- /dev/null
+++ b/extensions/OpenGraph/web/bugzilla.png
Binary files differ
diff --git a/extensions/OrangeFactor/Config.pm b/extensions/OrangeFactor/Config.pm
new file mode 100644
index 000000000..9fb0d74ef
--- /dev/null
+++ b/extensions/OrangeFactor/Config.pm
@@ -0,0 +1,13 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::OrangeFactor;
+use strict;
+
+use constant NAME => 'OrangeFactor';
+
+__PACKAGE__->NAME;
diff --git a/extensions/OrangeFactor/Extension.pm b/extensions/OrangeFactor/Extension.pm
new file mode 100644
index 000000000..af629e323
--- /dev/null
+++ b/extensions/OrangeFactor/Extension.pm
@@ -0,0 +1,43 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::OrangeFactor;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::User::Setting;
+use Bugzilla::Constants;
+use Bugzilla::Attachment;
+
+our $VERSION = '1.0';
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my $file = $args->{'file'};
+ my $vars = $args->{'vars'};
+
+ my $user = Bugzilla->user;
+
+ return unless ($file eq 'bug/show-header.html.tmpl'
+ || $file eq 'bug/edit.html.tmpl');
+ return unless ($user->id
+ && $user->settings->{'orange_factor'}->{'value'} eq 'on');
+
+ # in the header we just need to set the var,
+ # to ensure the css and javascript get included
+ my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'};
+ if ($bug && grep($_->name eq 'intermittent-failure', @{ $bug->keyword_objects })) {
+ $vars->{'orange_factor'} = 1;
+ }
+}
+
+sub install_before_final_checks {
+ my ($self, $args) = @_;
+ add_setting('orange_factor', ['on', 'off'], 'off');
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/OrangeFactor/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl b/extensions/OrangeFactor/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
new file mode 100644
index 000000000..a41188a63
--- /dev/null
+++ b/extensions/OrangeFactor/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
@@ -0,0 +1,26 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% IF orange_factor %]
+ <tr>
+ <th class="field_label" valign="top">
+ Orange Factor:
+ </th>
+ <td>
+ [% IF cgi.user_agent.match('(?i)gecko') %]
+ <canvas id="orange-graph" class="bz_default_hidden"></canvas>
+ <span id="orange-count"></span>
+ [% END %]
+ (<a href="https://brasstacks.mozilla.com/orangefactor/?display=Bug&bugid=[% bug.bug_id FILTER uri %]"
+ title="Click to load Orange Factor page for this [% terms.bug %]">link</a>)
+ </td>
+ </tr>
+[% END %]
diff --git a/extensions/OrangeFactor/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/OrangeFactor/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..b41431dcf
--- /dev/null
+++ b/extensions/OrangeFactor/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,17 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% IF orange_factor && cgi.user_agent.match('(?i)gecko') %]
+ [% style_urls.push('extensions/OrangeFactor/web/style/orangefactor.css') %]
+ [% javascript_urls.push('extensions/OrangeFactor/web/js/sparklines.min.js') %]
+ [% javascript_urls.push('extensions/OrangeFactor/web/js/orange_factor.js') %]
+[% END %]
+
diff --git a/extensions/OrangeFactor/template/en/default/hook/global/setting-descs-settings.none.tmpl b/extensions/OrangeFactor/template/en/default/hook/global/setting-descs-settings.none.tmpl
new file mode 100644
index 000000000..21a525deb
--- /dev/null
+++ b/extensions/OrangeFactor/template/en/default/hook/global/setting-descs-settings.none.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%
+ setting_descs.orange_factor = "When viewing a $terms.bug, show its corresponding Orange Factor page"
+%]
diff --git a/extensions/OrangeFactor/web/js/AUTHORS.processing.js b/extensions/OrangeFactor/web/js/AUTHORS.processing.js
new file mode 100644
index 000000000..e1244b717
--- /dev/null
+++ b/extensions/OrangeFactor/web/js/AUTHORS.processing.js
@@ -0,0 +1,35 @@
+John Resig
+Alistair MacDonald
+David Humphrey
+Corban Brook
+Anna Sobiepanek
+Andor Salga
+Daniel Hodgin
+Scott Downe
+Yuri Delendik
+Mike Kamermans
+Chris Lonnen
+Mickael Medel
+Matthew Lam
+Jon Buckley
+Dominic Baranski
+Elijah Grey
+Thomas Saunders
+Abel Allison
+Andrew Grimo
+Donghui Liu
+Edward Sin
+Alex Londono
+Robert O'Rourke
+Thanh Dao
+Zhibin Huang
+John Turner
+Tom Brown
+Minoo Ziaei
+Ricard Marxer
+Matt Postill
+Tiago Moreira
+Jonathan Brodsky
+Roger Sodre
+James Boelen
+Michal Ejdys`
diff --git a/extensions/OrangeFactor/web/js/LICENSE.processing.js b/extensions/OrangeFactor/web/js/LICENSE.processing.js
new file mode 100644
index 000000000..404e5d5eb
--- /dev/null
+++ b/extensions/OrangeFactor/web/js/LICENSE.processing.js
@@ -0,0 +1,22 @@
+Copyright (C) 2008 John Resig
+Copyright (C) 2009-2011; see the AUTHORS file for authors and
+copyright holders.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/extensions/OrangeFactor/web/js/LICENSE.sparklines.js b/extensions/OrangeFactor/web/js/LICENSE.sparklines.js
new file mode 100644
index 000000000..73aaca832
--- /dev/null
+++ b/extensions/OrangeFactor/web/js/LICENSE.sparklines.js
@@ -0,0 +1,20 @@
+Copyright (C) 2008 Will Larson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/extensions/OrangeFactor/web/js/orange_factor.js b/extensions/OrangeFactor/web/js/orange_factor.js
new file mode 100644
index 000000000..da993580d
--- /dev/null
+++ b/extensions/OrangeFactor/web/js/orange_factor.js
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YAHOO.namespace('OrangeFactor');
+
+var OrangeFactor = YAHOO.OrangeFactor;
+
+OrangeFactor.dayMs = 24 * 60 * 60 * 1000,
+OrangeFactor.limit = 7;
+
+OrangeFactor.getOrangeCount = function (data) {
+ data = data.oranges;
+ var total = 0,
+ days = [],
+ date = OrangeFactor.getCurrentDateMs() - OrangeFactor.limit * OrangeFactor.dayMs;
+ for(var i = 0; i < OrangeFactor.limit; i++) {
+ var iso = OrangeFactor.dateString(new Date(date));
+ var count = data[iso] ? data[iso].orangecount : 0;
+ days.push(count);
+ total += count;
+ date += OrangeFactor.dayMs;
+ }
+ OrangeFactor.displayGraph(days);
+ OrangeFactor.displayCount(total);
+}
+
+OrangeFactor.displayGraph = function (dayCounts) {
+ var max = dayCounts.reduce(function(max, count) {
+ return count > max ? count : max;
+ });
+ var graphContainer = YAHOO.util.Dom.get('orange-graph');
+ Dom.removeClass(graphContainer, 'bz_default_hidden');
+ YAHOO.util.Dom.setAttribute(graphContainer, 'title',
+ 'failures over the past week, max in a day: ' + max);
+ var opts = {
+ "percentage_lines":[0.25, 0.5, 0.75],
+ "fill_between_percentage_lines": false,
+ "left_padding": 0,
+ "right_padding": 0,
+ "top_padding": 0,
+ "bottom_padding": 0,
+ "background": "#D0D0D0",
+ "stroke": "#000000",
+ "percentage_fill_color": "#CCCCFF",
+ "scale_from_zero": true,
+ };
+ new Sparkline('orange-graph', dayCounts, opts).draw();
+}
+
+OrangeFactor.displayCount = function (count) {
+ var countContainer = YAHOO.util.Dom.get('orange-count');
+ countContainer.innerHTML = encodeURIComponent(count) +
+ ' failures on trunk in the past week';
+}
+
+OrangeFactor.dateString = function (date) {
+ function norm(part) {
+ return JSON.stringify(part).length == 2 ? part : '0' + part;
+ }
+ return date.getFullYear()
+ + "-" + norm(date.getMonth() + 1)
+ + "-" + norm(date.getDate());
+}
+
+OrangeFactor.getCurrentDateMs = function () {
+ var d = new Date;
+ return d.getTime();
+}
+
+OrangeFactor.orangify = function () {
+ var bugId = document.forms['changeform'].id.value;
+ var url = "https://brasstacks.mozilla.com/orangefactor/api/count?" +
+ "bugid=" + encodeURIComponent(bugId) +
+ "&tree=trunk" +
+ "&callback=OrangeFactor.getOrangeCount";
+ var script = document.createElement('script');
+ Dom.setAttribute(script, 'src', url);
+ Dom.setAttribute(script, 'type', 'text/javascript');
+ var head = document.getElementsByTagName('head')[0];
+ head.appendChild(script);
+ var countContainer = YAHOO.util.Dom.get('orange-count');
+ Dom.removeClass(countContainer, 'bz_default_hidden');
+ countContainer.innerHTML = 'Loading...';a
+}
+
+YAHOO.util.Event.onDOMReady(OrangeFactor.orangify);
diff --git a/extensions/OrangeFactor/web/js/sparklines.min.js b/extensions/OrangeFactor/web/js/sparklines.min.js
new file mode 100644
index 000000000..f1043c55e
--- /dev/null
+++ b/extensions/OrangeFactor/web/js/sparklines.min.js
@@ -0,0 +1,133 @@
+/* Sparklines.js - Will Larson (http://lethain.com)
+ * This code is distributed under the MIT license.
+ * See LICENSE.sparklines.js
+ * More information: https://github.com/lethain/sparklines.js
+ *
+ * Processing.js - John Resig (http://ejohn.org/)
+ * See LICENSE.processing.js and AUTHORS.processing.js
+ * More information: http://processingjs.org/
+ */
+(function(){this.Processing=function Processing(aElement,aCode){if(typeof aElement=="string")
+aElement=document.getElementById(aElement);var p=buildProcessing(aElement);if(aCode)
+p.init(aCode);return p;};function log(){try{console.log.apply(console,arguments);}catch(e){try{opera.postError.apply(opera,arguments);}catch(e){}}}
+var parse=Processing.parse=function parse(aCode,p){aCode=aCode.replace(/\/\/ .*\n/g,"\n");aCode=aCode.replace(/([^\s])%([^\s])/g,"$1 % $2");aCode=aCode.replace(/(?:static )?(\w+ )(\w+)\s*(\([^\)]*\)\s*{)/g,function(all,type,name,args){if(name=="if"||name=="for"||name=="while"){return all;}else{return"Processing."+name+" = function "+name+args;}});aCode=aCode.replace(/\.length\(\)/g,".length");aCode=aCode.replace(/([\(,]\s*)(\w+)((?:\[\])+| )\s*(\w+\s*[\),])/g,"$1$4");aCode=aCode.replace(/([\(,]\s*)(\w+)((?:\[\])+| )\s*(\w+\s*[\),])/g,"$1$4");aCode=aCode.replace(/new (\w+)((?:\[([^\]]*)\])+)/g,function(all,name,args){return"new ArrayList("+args.slice(1,-1).split("][").join(", ")+")";});aCode=aCode.replace(/(?:static )?\w+\[\]\s*(\w+)\[?\]?\s*=\s*{.*?};/g,function(all){return all.replace(/{/g,"[").replace(/}/g,"]");});var intFloat=/(\n\s*(?:int|float)(?:\[\])?(?:\s*|[^\(]*?,\s*))([a-z]\w*)(;|,)/i;while(intFloat.test(aCode)){aCode=aCode.replace(new RegExp(intFloat),function(all,type,name,sep){return type+" "+name+" = 0"+sep;});}
+aCode=aCode.replace(/(?:static )?(\w+)((?:\[\])+| ) *(\w+)\[?\]?(\s*[=,;])/g,function(all,type,arr,name,sep){if(type=="return")
+return all;else
+return"var "+name+sep;});aCode=aCode.replace(/=\s*{((.|\s)*?)};/g,function(all,data){return"= ["+data.replace(/{/g,"[").replace(/}/g,"]")+"]";});aCode=aCode.replace(/static\s*{((.|\n)*?)}/g,function(all,init){return init;});aCode=aCode.replace(/super\(/g,"superMethod(");var classes=["int","float","boolean","string"];function ClassReplace(all,name,extend,vars,last){classes.push(name);var static="";vars=vars.replace(/final\s+var\s+(\w+\s*=\s*.*?;)/g,function(all,set){static+=" "+name+"."+set;return"";});return"function "+name+"() {with(this){\n "+
+(extend?"var __self=this;function superMethod(){extendClass(__self,arguments,"+extend+");}\n":"")+
+vars.replace(/,\s?/g,";\n this.").replace(/\b(var |final |public )+\s*/g,"this.").replace(/this.(\w+);/g,"this.$1 = null;")+
+(extend?"extendClass(this, "+extend+");\n":"")+"<CLASS "+name+" "+static+">"+(typeof last=="string"?last:name+"(");}
+var matchClasses=/(?:public |abstract |static )*class (\w+)\s*(?:extends\s*(\w+)\s*)?{\s*((?:.|\n)*?)\b\1\s*\(/g;var matchNoCon=/(?:public |abstract |static )*class (\w+)\s*(?:extends\s*(\w+)\s*)?{\s*((?:.|\n)*?)(Processing)/g;aCode=aCode.replace(matchClasses,ClassReplace);aCode=aCode.replace(matchNoCon,ClassReplace);var matchClass=/<CLASS (\w+) (.*?)>/,m;while((m=aCode.match(matchClass))){var left=RegExp.leftContext,allRest=RegExp.rightContext,rest=nextBrace(allRest),className=m[1],staticVars=m[2]||"";allRest=allRest.slice(rest.length+1);rest=rest.replace(new RegExp("\\b"+className+"\\(([^\\)]*?)\\)\\s*{","g"),function(all,args){args=args.split(/,\s*?/);if(args[0].match(/^\s*$/))
+args.shift();var fn="if ( arguments.length == "+args.length+" ) {\n";for(var i=0;i<args.length;i++){fn+=" var "+args[i]+" = arguments["+i+"];\n";}
+return fn;});rest=rest.replace(/(?:public )?Processing.\w+ = function (\w+)\((.*?)\)/g,function(all,name,args){return"ADDMETHOD(this, '"+name+"', function("+args+")";});var matchMethod=/ADDMETHOD([\s\S]*?{)/,mc;var methods="";while((mc=rest.match(matchMethod))){var prev=RegExp.leftContext,allNext=RegExp.rightContext,next=nextBrace(allNext);methods+="addMethod"+mc[1]+next+"});"
+rest=prev+allNext.slice(next.length+1);}
+rest=methods+rest;aCode=left+rest+"\n}}"+staticVars+allRest;}
+aCode=aCode.replace(/Processing.\w+ = function addMethod/g,"addMethod");function nextBrace(right){var rest=right;var position=0;var leftCount=1,rightCount=0;while(leftCount!=rightCount){var nextLeft=rest.indexOf("{");var nextRight=rest.indexOf("}");if(nextLeft<nextRight&&nextLeft!=-1){leftCount++;rest=rest.slice(nextLeft+1);position+=nextLeft+1;}else{rightCount++;rest=rest.slice(nextRight+1);position+=nextRight+1;}}
+return right.slice(0,position-1);}
+aCode=aCode.replace(/\(int\)/g,"0|");aCode=aCode.replace(new RegExp("\\(("+classes.join("|")+")(\\[\\])?\\)","g"),"");aCode=aCode.replace(/(\d+)f/g,"$1");aCode=aCode.replace(/('[a-zA-Z0-9]')/g,"$1.charCodeAt(0)");aCode=aCode.replace(/#([a-f0-9]{6})/ig,function(m,hex){var num=toNumbers(hex);return"color("+num[0]+","+num[1]+","+num[2]+")";});function toNumbers(str){var ret=[];str.replace(/(..)/g,function(str){ret.push(parseInt(str,16));});return ret;}
+return aCode;};function buildProcessing(curElement){var p={};p.PI=Math.PI;p.TWO_PI=2*p.PI;p.HALF_PI=p.PI/2;p.P3D=3;p.CORNER=0;p.RADIUS=1;p.CENTER_RADIUS=1;p.CENTER=2;p.POLYGON=2;p.QUADS=5;p.TRIANGLES=6;p.POINTS=7;p.LINES=8;p.TRIANGLE_STRIP=9;p.TRIANGLE_FAN=4;p.QUAD_STRIP=3;p.CORNERS=10;p.CLOSE=true;p.RGB=1;p.HSB=2;p.LEFT=1;p.CENTER=2;p.RIGHT=3;var curContext=curElement.getContext("2d");var doFill=true;var doStroke=true;var loopStarted=false;var hasBackground=false;var doLoop=true;var looping=0;var curRectMode=p.CORNER;var curEllipseMode=p.CENTER;var inSetup=false;var inDraw=false;var curBackground="rgba(204,204,204,1)";var curFrameRate=1000;var curShape=p.POLYGON;var curShapeCount=0;var curvePoints=[];var curTightness=0;var opacityRange=255;var redRange=255;var greenRange=255;var blueRange=255;var pathOpen=false;var mousePressed=false;var keyPressed=false;var firstX,firstY,secondX,secondY,prevX,prevY;var curColorMode=p.RGB;var curTint=-1;var curTextSize=12;var curTextFont="Arial";var getLoaded=false;var start=(new Date).getTime();p.pmouseX=0;p.pmouseY=0;p.mouseX=0;p.mouseY=0;p.mouseButton=0;p.mouseDragged=undefined;p.mouseMoved=undefined;p.mousePressed=undefined;p.mouseReleased=undefined;p.keyPressed=undefined;p.keyReleased=undefined;p.draw=undefined;p.setup=undefined;p.width=curElement.width-0;p.height=curElement.height-0;p.frameCount=0;p.color=function color(aValue1,aValue2,aValue3,aValue4){var aColor="";if(arguments.length==3){aColor=p.color(aValue1,aValue2,aValue3,opacityRange);}else if(arguments.length==4){var a=aValue4/opacityRange;a=isNaN(a)?1:a;if(curColorMode==p.HSB){var rgb=HSBtoRGB(aValue1,aValue2,aValue3);var r=rgb[0],g=rgb[1],b=rgb[2];}else{var r=getColor(aValue1,redRange);var g=getColor(aValue2,greenRange);var b=getColor(aValue3,blueRange);}
+aColor="rgba("+r+","+g+","+b+","+a+")";}else if(typeof aValue1=="string"){aColor=aValue1;if(arguments.length==2){var c=aColor.split(",");c[3]=(aValue2/opacityRange)+")";aColor=c.join(",");}}else if(arguments.length==2){aColor=p.color(aValue1,aValue1,aValue1,aValue2);}else if(typeof aValue1=="number"){aColor=p.color(aValue1,aValue1,aValue1,opacityRange);}else{aColor=p.color(redRange,greenRange,blueRange,opacityRange);}
+function HSBtoRGB(h,s,b){h=(h/redRange)*100;s=(s/greenRange)*100;b=(b/blueRange)*100;if(s==0){return[b,b,b];}else{var hue=h%360;var f=hue%60;var br=Math.round(b/100*255);var p=Math.round((b*(100-s))/10000*255);var q=Math.round((b*(6000-s*f))/600000*255);var t=Math.round((b*(6000-s*(60-f)))/600000*255);switch(Math.floor(hue/60)){case 0:return[br,t,p];case 1:return[q,br,p];case 2:return[p,br,t];case 3:return[p,q,br];case 4:return[t,p,br];case 5:return[br,p,q];}}}
+function getColor(aValue,range){return Math.round(255*(aValue/range));}
+return aColor;}
+p.nf=function(num,pad){var str=""+num;while(pad-str.length)
+str="0"+str;return str;};p.AniSprite=function(prefix,frames){this.images=[];this.pos=0;for(var i=0;i<frames;i++){this.images.push(prefix+p.nf(i,(""+frames).length)+".gif");}
+this.display=function(x,y){p.image(this.images[this.pos],x,y);if(++this.pos>=frames)
+this.pos=0;};this.getWidth=function(){return getImage(this.images[0]).width;};this.getHeight=function(){return getImage(this.images[0]).height;};};function buildImageObject(obj){var pixels=obj.data;var data=p.createImage(obj.width,obj.height);if(data.__defineGetter__&&data.__lookupGetter__&&!data.__lookupGetter__("pixels")){var pixelsDone;data.__defineGetter__("pixels",function(){if(pixelsDone)
+return pixelsDone;pixelsDone=[];for(var i=0;i<pixels.length;i+=4){pixelsDone.push(p.color(pixels[i],pixels[i+1],pixels[i+2],pixels[i+3]));}
+return pixelsDone;});}else{data.pixels=[];for(var i=0;i<pixels.length;i+=4){data.pixels.push(p.color(pixels[i],pixels[i+1],pixels[i+2],pixels[i+3]));}}
+return data;}
+p.createImage=function createImage(w,h,mode){var data={};data.width=w;data.height=h;data.data=[];if(curContext.createImageData){data=curContext.createImageData(w,h);}
+data.pixels=new Array(w*h);data.get=function(x,y){return this.pixels[w*y+x];};data._mask=null;data.mask=function(img){this._mask=img;};data.loadPixels=function(){};data.updatePixels=function(){};return data;};p.createGraphics=function createGraphics(w,h){var canvas=document.createElement("canvas");var ret=buildProcessing(canvas);ret.size(w,h);ret.canvas=canvas;return ret;};p.beginDraw=function beginDraw(){};p.endDraw=function endDraw(){};p.tint=function tint(rgb,a){curTint=a;};function getImage(img){if(typeof img=="string"){return document.getElementById(img);}
+if(img.img||img.canvas){return img.img||img.canvas;}
+for(var i=0,l=img.pixels.length;i<l;i++){var pos=i*4;var c=(img.pixels[i]||"rgba(0,0,0,1)").slice(5,-1).split(",");img.data[pos]=parseInt(c[0]);img.data[pos+1]=parseInt(c[1]);img.data[pos+2]=parseInt(c[2]);img.data[pos+3]=parseFloat(c[3])*100;}
+var canvas=document.createElement("canvas")
+canvas.width=img.width;canvas.height=img.height;var context=canvas.getContext("2d");context.putImageData(img,0,0);img.canvas=canvas;return canvas;}
+p.image=function image(img,x,y,w,h){x=x||0;y=y||0;var obj=getImage(img);if(curTint>=0){var oldAlpha=curContext.globalAlpha;curContext.globalAlpha=curTint/opacityRange;}
+if(arguments.length==3){curContext.drawImage(obj,x,y);}else{curContext.drawImage(obj,x,y,w,h);}
+if(curTint>=0){curContext.globalAlpha=oldAlpha;}
+if(img._mask){var oldComposite=curContext.globalCompositeOperation;curContext.globalCompositeOperation="darker";p.image(img._mask,x,y);curContext.globalCompositeOperation=oldComposite;}};p.exit=function exit(){clearInterval(looping);};p.save=function save(file){};p.loadImage=function loadImage(file){var img=document.getElementById(file);if(!img)
+return;var h=img.height,w=img.width;var canvas=document.createElement("canvas");canvas.width=w;canvas.height=h;var context=canvas.getContext("2d");context.drawImage(img,0,0);var data=buildImageObject(context.getImageData(0,0,w,h));data.img=img;return data;};p.loadFont=function loadFont(name){return{name:name,width:function(str){if(curContext.mozMeasureText)
+return curContext.mozMeasureText(typeof str=="number"?String.fromCharCode(str):str)/curTextSize;else
+return 0;}};};p.textFont=function textFont(name,size){curTextFont=name;p.textSize(size);};p.textSize=function textSize(size){if(size){curTextSize=size;}};p.textAlign=function textAlign(){};p.text=function text(str,x,y){if(str&&curContext.mozDrawText){curContext.save();curContext.mozTextStyle=curTextSize+"px "+curTextFont.name;curContext.translate(x,y);curContext.mozDrawText(typeof str=="number"?String.fromCharCode(str):str);curContext.restore();}};p.char=function char(key){return key;};p.println=function println(){};p.map=function map(value,istart,istop,ostart,ostop){return ostart+(ostop-ostart)*((value-istart)/(istop-istart));};String.prototype.replaceAll=function(re,replace){return this.replace(new RegExp(re,"g"),replace);};p.Point=function Point(x,y){this.x=x;this.y=y;this.copy=function(){return new Point(x,y);}};p.Random=function(){var haveNextNextGaussian=false;var nextNextGaussian;this.nextGaussian=function(){if(haveNextNextGaussian){haveNextNextGaussian=false;return nextNextGaussian;}else{var v1,v2,s;do{v1=2*p.random(1)-1;v2=2*p.random(1)-1;s=v1*v1+v2*v2;}while(s>=1||s==0);var multiplier=Math.sqrt(-2*Math.log(s)/s);nextNextGaussian=v2*multiplier;haveNextNextGaussian=true;return v1*multiplier;}};};p.ArrayList=function ArrayList(size,size2,size3){var array=new Array(0|size);if(size2){for(var i=0;i<size;i++){array[i]=[];for(var j=0;j<size2;j++){var a=array[i][j]=size3?new Array(size3):0;for(var k=0;k<size3;k++){a[k]=0;}}}}else{for(var i=0;i<size;i++){array[i]=0;}}
+array.size=function(){return this.length;};array.get=function(i){return this[i];};array.remove=function(i){return this.splice(i,1);};array.add=function(item){return this.push(item);};array.clone=function(){var a=new ArrayList(size);for(var i=0;i<size;i++){a[i]=this[i];}
+return a;};array.isEmpty=function(){return!this.length;};array.clear=function(){this.length=0;};return array;};p.colorMode=function colorMode(mode,range1,range2,range3,range4){curColorMode=mode;if(arguments.length>=4){redRange=range1;greenRange=range2;blueRange=range3;}
+if(arguments.length==5){opacityRange=range4;}
+if(arguments.length==2){p.colorMode(mode,range1,range1,range1,range1);}};p.beginShape=function beginShape(type){curShape=type;curShapeCount=0;curvePoints=[];};p.endShape=function endShape(close){if(curShapeCount!=0){if(close||doFill)
+curContext.lineTo(firstX,firstY);if(doFill)
+curContext.fill();if(doStroke)
+curContext.stroke();curContext.closePath();curShapeCount=0;pathOpen=false;}
+if(pathOpen){if(doFill)
+curContext.fill();if(doStroke)
+curContext.stroke();curContext.closePath();curShapeCount=0;pathOpen=false;}};p.vertex=function vertex(x,y,x2,y2,x3,y3){if(curShapeCount==0&&curShape!=p.POINTS){pathOpen=true;curContext.beginPath();curContext.moveTo(x,y);firstX=x;firstY=y;}else{if(curShape==p.POINTS){p.point(x,y);}else if(arguments.length==2){if(curShape!=p.QUAD_STRIP||curShapeCount!=2)
+curContext.lineTo(x,y);if(curShape==p.TRIANGLE_STRIP){if(curShapeCount==2){p.endShape(p.CLOSE);pathOpen=true;curContext.beginPath();curContext.moveTo(prevX,prevY);curContext.lineTo(x,y);curShapeCount=1;}
+firstX=prevX;firstY=prevY;}
+if(curShape==p.TRIANGLE_FAN&&curShapeCount==2){p.endShape(p.CLOSE);pathOpen=true;curContext.beginPath();curContext.moveTo(firstX,firstY);curContext.lineTo(x,y);curShapeCount=1;}
+if(curShape==p.QUAD_STRIP&&curShapeCount==3){curContext.lineTo(prevX,prevY);p.endShape(p.CLOSE);pathOpen=true;curContext.beginPath();curContext.moveTo(prevX,prevY);curContext.lineTo(x,y);curShapeCount=1;}
+if(curShape==p.QUAD_STRIP){firstX=secondX;firstY=secondY;secondX=prevX;secondY=prevY;}}else if(arguments.length==4){if(curShapeCount>1){curContext.moveTo(prevX,prevY);curContext.quadraticCurveTo(firstX,firstY,x,y);curShapeCount=1;}}else if(arguments.length==6){curContext.bezierCurveTo(x,y,x2,y2,x3,y3);curShapeCount=-1;}}
+prevX=x;prevY=y;curShapeCount++;if(curShape==p.LINES&&curShapeCount==2||(curShape==p.TRIANGLES)&&curShapeCount==3||(curShape==p.QUADS)&&curShapeCount==4){p.endShape(p.CLOSE);}};p.curveVertex=function(x,y,x2,y2){if(curvePoints.length<3){curvePoints.push([x,y]);}else{var b=[],s=1-curTightness;curvePoints.push([x,y]);b[0]=[curvePoints[1][0],curvePoints[1][1]];b[1]=[curvePoints[1][0]+(s*curvePoints[2][0]-s*curvePoints[0][0])/6,curvePoints[1][1]+(s*curvePoints[2][1]-s*curvePoints[0][1])/6];b[2]=[curvePoints[2][0]+(s*curvePoints[1][0]-s*curvePoints[3][0])/6,curvePoints[2][1]+(s*curvePoints[1][1]-s*curvePoints[3][1])/6];b[3]=[curvePoints[2][0],curvePoints[2][1]];if(!pathOpen){p.vertex(b[0][0],b[0][1]);}else{curShapeCount=1;}
+p.vertex(b[1][0],b[1][1],b[2][0],b[2][1],b[3][0],b[3][1]);curvePoints.shift();}};p.curveTightness=function(tightness){curTightness=tightness;};p.bezierVertex=p.vertex;p.rectMode=function rectMode(aRectMode){curRectMode=aRectMode;};p.imageMode=function(){};p.ellipseMode=function ellipseMode(aEllipseMode){curEllipseMode=aEllipseMode;};p.dist=function dist(x1,y1,x2,y2){return Math.sqrt(Math.pow(x2-x1,2)+Math.pow(y2-y1,2));};p.year=function year(){return(new Date).getYear()+1900;};p.month=function month(){return(new Date).getMonth();};p.day=function day(){return(new Date).getDay();};p.hour=function hour(){return(new Date).getHours();};p.minute=function minute(){return(new Date).getMinutes();};p.second=function second(){return(new Date).getSeconds();};p.millis=function millis(){return(new Date).getTime()-start;};p.ortho=function ortho(){};p.translate=function translate(x,y){curContext.translate(x,y);};p.scale=function scale(x,y){curContext.scale(x,y||x);};p.rotate=function rotate(aAngle){curContext.rotate(aAngle);};p.pushMatrix=function pushMatrix(){curContext.save();};p.popMatrix=function popMatrix(){curContext.restore();};p.redraw=function redraw(){if(hasBackground){p.background();}
+p.frameCount++;inDraw=true;p.pushMatrix();p.draw();p.popMatrix();inDraw=false;};p.loop=function loop(){if(loopStarted)
+return;looping=setInterval(function(){try{p.redraw();}
+catch(e){clearInterval(looping);throw e;}},1000/curFrameRate);loopStarted=true;};p.frameRate=function frameRate(aRate){curFrameRate=aRate;};p.background=function background(img){if(arguments.length){if(img&&img.img){curBackground=img;}else{curBackground=p.color.apply(this,arguments);}}
+if(curBackground.img){p.image(curBackground,0,0);}else{var oldFill=curContext.fillStyle;curContext.fillStyle=curBackground+"";curContext.fillRect(0,0,p.width,p.height);curContext.fillStyle=oldFill;}};p.sq=function sq(aNumber){return aNumber*aNumber;};p.sqrt=function sqrt(aNumber){return Math.sqrt(aNumber);};p.int=function int(aNumber){return Math.floor(aNumber);};p.min=function min(aNumber,aNumber2){return Math.min(aNumber,aNumber2);};p.max=function max(aNumber,aNumber2){return Math.max(aNumber,aNumber2);};p.ceil=function ceil(aNumber){return Math.ceil(aNumber);};p.floor=function floor(aNumber){return Math.floor(aNumber);};p.float=function float(aNumber){return typeof aNumber=="string"?p.float(aNumber.charCodeAt(0)):parseFloat(aNumber);};p.byte=function byte(aNumber){return aNumber||0;};p.random=function random(aMin,aMax){return arguments.length==2?aMin+(Math.random()*(aMax-aMin)):Math.random()*aMin;};p.noise=function(x,y,z){return arguments.length>=2?PerlinNoise_2D(x,y):PerlinNoise_2D(x,x);};function Noise(x,y){var n=x+y*57;n=(n<<13)^n;return Math.abs(1.0-(((n*((n*n*15731)+789221)+1376312589)&0x7fffffff)/1073741824.0));};function SmoothedNoise(x,y){var corners=(Noise(x-1,y-1)+Noise(x+1,y-1)+Noise(x-1,y+1)+Noise(x+1,y+1))/16;var sides=(Noise(x-1,y)+Noise(x+1,y)+Noise(x,y-1)+Noise(x,y+1))/8;var center=Noise(x,y)/4;return corners+sides+center;};function InterpolatedNoise(x,y){var integer_X=Math.floor(x);var fractional_X=x-integer_X;var integer_Y=Math.floor(y);var fractional_Y=y-integer_Y;var v1=SmoothedNoise(integer_X,integer_Y);var v2=SmoothedNoise(integer_X+1,integer_Y);var v3=SmoothedNoise(integer_X,integer_Y+1);var v4=SmoothedNoise(integer_X+1,integer_Y+1);var i1=Interpolate(v1,v2,fractional_X);var i2=Interpolate(v3,v4,fractional_X);return Interpolate(i1,i2,fractional_Y);}
+function PerlinNoise_2D(x,y){var total=0;var p=0.25;var n=3;for(var i=0;i<=n;i++){var frequency=Math.pow(2,i);var amplitude=Math.pow(p,i);total=total+InterpolatedNoise(x*frequency,y*frequency)*amplitude;}
+return total;}
+function Interpolate(a,b,x){var ft=x*p.PI;var f=(1-p.cos(ft))*.5;return a*(1-f)+b*f;}
+p.red=function(aColor){return parseInt(aColor.slice(5));};p.green=function(aColor){return parseInt(aColor.split(",")[1]);};p.blue=function(aColor){return parseInt(aColor.split(",")[2]);};p.alpha=function(aColor){return parseInt(aColor.split(",")[3]);};p.abs=function abs(aNumber){return Math.abs(aNumber);};p.cos=function cos(aNumber){return Math.cos(aNumber);};p.sin=function sin(aNumber){return Math.sin(aNumber);};p.pow=function pow(aNumber,aExponent){return Math.pow(aNumber,aExponent);};p.constrain=function constrain(aNumber,aMin,aMax){return Math.min(Math.max(aNumber,aMin),aMax);};p.sqrt=function sqrt(aNumber){return Math.sqrt(aNumber);};p.atan2=function atan2(aNumber,aNumber2){return Math.atan2(aNumber,aNumber2);};p.radians=function radians(aAngle){return(aAngle/180)*p.PI;};p.size=function size(aWidth,aHeight){var fillStyle=curContext.fillStyle;var strokeStyle=curContext.strokeStyle;curElement.width=p.width=aWidth;curElement.height=p.height=aHeight;curContext.fillStyle=fillStyle;curContext.strokeStyle=strokeStyle;};p.noStroke=function noStroke(){doStroke=false;};p.noFill=function noFill(){doFill=false;};p.smooth=function smooth(){};p.noLoop=function noLoop(){doLoop=false;};p.fill=function fill(){doFill=true;curContext.fillStyle=p.color.apply(this,arguments);};p.stroke=function stroke(){doStroke=true;curContext.strokeStyle=p.color.apply(this,arguments);};p.strokeWeight=function strokeWeight(w){curContext.lineWidth=w;};p.point=function point(x,y){var oldFill=curContext.fillStyle;curContext.fillStyle=curContext.strokeStyle;curContext.fillRect(Math.round(x),Math.round(y),1,1);curContext.fillStyle=oldFill;};p.get=function get(x,y){if(arguments.length==0){var c=p.createGraphics(p.width,p.height);c.image(curContext,0,0);return c;}
+if(!getLoaded){getLoaded=buildImageObject(curContext.getImageData(0,0,p.width,p.height));}
+return getLoaded.get(x,y);};p.set=function set(x,y,obj){if(obj&&obj.img){p.image(obj,x,y);}else{var oldFill=curContext.fillStyle;var color=obj;curContext.fillStyle=color;curContext.fillRect(Math.round(x),Math.round(y),1,1);curContext.fillStyle=oldFill;}};p.arc=function arc(x,y,width,height,start,stop){if(width<=0)
+return;if(curEllipseMode==p.CORNER){x+=width/2;y+=height/2;}
+curContext.beginPath();curContext.moveTo(x,y);curContext.arc(x,y,curEllipseMode==p.CENTER_RADIUS?width:width/2,start,stop,false);if(doFill)
+curContext.fill();if(doStroke)
+curContext.stroke();curContext.closePath();};p.line=function line(x1,y1,x2,y2){curContext.lineCap="round";curContext.beginPath();curContext.moveTo(x1||0,y1||0);curContext.lineTo(x2||0,y2||0);curContext.stroke();curContext.closePath();};p.bezier=function bezier(x1,y1,x2,y2,x3,y3,x4,y4){curContext.lineCap="butt";curContext.beginPath();curContext.moveTo(x1,y1);curContext.bezierCurveTo(x2,y2,x3,y3,x4,y4);curContext.stroke();curContext.closePath();};p.triangle=function triangle(x1,y1,x2,y2,x3,y3){p.beginShape();p.vertex(x1,y1);p.vertex(x2,y2);p.vertex(x3,y3);p.endShape();};p.quad=function quad(x1,y1,x2,y2,x3,y3,x4,y4){p.beginShape();p.vertex(x1,y1);p.vertex(x2,y2);p.vertex(x3,y3);p.vertex(x4,y4);p.endShape();};p.rect=function rect(x,y,width,height){if(width==0&&height==0)
+return;curContext.beginPath();var offsetStart=0;var offsetEnd=0;if(curRectMode==p.CORNERS){width-=x;height-=y;}
+if(curRectMode==p.RADIUS){width*=2;height*=2;}
+if(curRectMode==p.CENTER||curRectMode==p.RADIUS){x-=width/2;y-=height/2;}
+curContext.rect(Math.round(x)-offsetStart,Math.round(y)-offsetStart,Math.round(width)+offsetEnd,Math.round(height)+offsetEnd);if(doFill)
+curContext.fill();if(doStroke)
+curContext.stroke();curContext.closePath();};p.ellipse=function ellipse(x,y,width,height){x=x||0;y=y||0;if(width<=0&&height<=0)
+return;curContext.beginPath();if(curEllipseMode==p.RADIUS){width*=2;height*=2;}
+var offsetStart=0;if(width==height)
+curContext.arc(x-offsetStart,y-offsetStart,width/2,0,Math.PI*2,false);if(doFill)
+curContext.fill();if(doStroke)
+curContext.stroke();curContext.closePath();};p.link=function(href,target){window.location=href;};p.loadPixels=function(){p.pixels=buildImageObject(curContext.getImageData(0,0,p.width,p.height)).pixels;};p.updatePixels=function(){var colors=/(\d+),(\d+),(\d+),(\d+)/;var pixels={};pixels.width=p.width;pixels.height=p.height;pixels.data=[];if(curContext.createImageData){pixels=curContext.createImageData(p.width,p.height);}
+var data=pixels.data;var pos=0;for(var i=0,l=p.pixels.length;i<l;i++){var c=(p.pixels[i]||"rgba(0,0,0,1)").match(colors);data[pos]=parseInt(c[1]);data[pos+1]=parseInt(c[2]);data[pos+2]=parseInt(c[3]);data[pos+3]=parseFloat(c[4])*100;pos+=4;}
+curContext.putImageData(pixels,0,0);};p.extendClass=function extendClass(obj,args,fn){if(arguments.length==3){fn.apply(obj,args);}else{args.call(obj);}};p.addMethod=function addMethod(object,name,fn){if(object[name]){var args=fn.length;var oldfn=object[name];object[name]=function(){if(arguments.length==args)
+return fn.apply(this,arguments);else
+return oldfn.apply(this,arguments);};}else{object[name]=fn;}};p.init=function init(code){p.stroke(0);p.fill(255);curContext.translate(0.5,0.5);if(code){(function(Processing){with(p){eval(parse(code,p));}})(p);}
+if(p.setup){inSetup=true;p.setup();}
+inSetup=false;if(p.draw){if(!doLoop){p.redraw();}else{p.loop();}}
+attach(curElement,"mousemove",function(e){var scrollX=window.scrollX!=null?window.scrollX:window.pageXOffset;var scrollY=window.scrollY!=null?window.scrollY:window.pageYOffset;p.pmouseX=p.mouseX;p.pmouseY=p.mouseY;p.mouseX=e.clientX-curElement.offsetLeft+scrollX;p.mouseY=e.clientY-curElement.offsetTop+scrollY;if(p.mouseMoved){p.mouseMoved();}
+if(mousePressed&&p.mouseDragged){p.mouseDragged();}});attach(curElement,"mousedown",function(e){mousePressed=true;p.mouseButton=e.which;if(typeof p.mousePressed=="function"){p.mousePressed();}else{p.mousePressed=true;}});attach(curElement,"contextmenu",function(e){e.preventDefault();e.stopPropagation();});attach(curElement,"mouseup",function(e){mousePressed=false;if(typeof p.mousePressed!="function"){p.mousePressed=false;}
+if(p.mouseReleased){p.mouseReleased();}});attach(document,"keydown",function(e){keyPressed=true;p.key=e.keyCode+32;if(e.shiftKey){p.key=String.fromCharCode(p.key).toUpperCase().charCodeAt(0);}
+if(typeof p.keyPressed=="function"){p.keyPressed();}else{p.keyPressed=true;}});attach(document,"keyup",function(e){keyPressed=false;if(typeof p.keyPressed!="function"){p.keyPressed=false;}
+if(p.keyReleased){p.keyReleased();}});function attach(elem,type,fn){if(elem.addEventListener)
+elem.addEventListener(type,fn,false);else
+elem.attachEvent("on"+type,fn);}};return p;}})();if(!Array.prototype.map)
+{Array.prototype.map=function(fun)
+{var len=this.length;if(typeof fun!="function")
+throw new TypeError();var res=new Array(len);var thisp=arguments[1];for(var i=0;i<len;i++)
+{if(i in this)
+res[i]=fun.call(thisp,this[i],i,this);}
+return res;};}
+var BaseSparkline=function(){this.init=function(id,data,mixins){this.background=50;this.stroke="rgba(230,230,230,0.70);";this.percentage_color="#5555FF";this.percentage_fill_color=75;this.value_line_color="#7777FF";this.value_line_fill_color=85;this.canvas=document.getElementById(id);this.data=data;this.scale_from=undefined;this.scale_to=undefined;this.top_padding=10;this.bottom_padding=10;this.left_padding=10;this.right_padding=10;this.percentage_lines=[];this.fill_between_percentage_lines=false;this.value_lines=[];this.fill_between_value_lines=false;for(var property in mixins)this[property]=mixins[property];};this.parse_height=function(x){return x;};this.heights=function(){return this.data.map(this.parse_height);};this.max=function(){var vals=this.heights();var max=vals[0];var l=vals.length;for(var i=1;i<l;i++)max=Math.max(max,vals[i]);return max;};this.min=function(){var vals=this.heights();var min=vals[0];var l=vals.length;for(var i=1;i<l;i++)min=Math.min(min,vals[i]);return min;};this.height=function(){return this.canvas.height-this.top_padding-this.bottom_padding;};this.width=function(){return this.canvas.width-this.left_padding-this.right_padding;};this.scale_values=function(values,max){if(!max)max=this.max();var p=this.top_padding;var h=this.height();var top=(this.scale_to!=undefined)?this.scale_to:max;var bottom=(this.scale_from!=undefined)?this.scale_from:this.min();var range=Math.abs(top-bottom);var scale=function(x){var percentage=((x-bottom)*1.0)/range;return h-(h*percentage)+p;};return values.map(scale,this);};this.calc_value_lines=function(){var scaled=this.scale_values(this.value_lines);scaled.sort(function(a,b){return a-b;});return scaled;};this.calc_percentages=function(){var sorted=this.heights();sorted.sort(function(a,b){return a-b;});var points=[];var n=sorted.length;var l=this.percentage_lines.length;for(var i=0;i<l;i++){var percentage=this.percentage_lines[i];var position=Math.round(percentage*(n+1));points.push(sorted[position]);}
+var max=sorted[n-1];var raws=this.scale_values(points,max);raws.sort(function(a,b){return a-b;});return raws;};this.scale_height=function(){return this.scale_values(this.heights());};this.segment_width=function(){var w=this.width();var l=this.data.length;return(w*1.0)/(l-1);};this.scale_width=function(){var widths=[];var l=this.data.length;var segment_width=this.segment_width();for(var i=0;i<l;i++){widths.push((i*segment_width)+this.left_padding);}
+return widths;};this.scale_data=function(){var heights=this.scale_height();var widths=this.scale_width();var l=heights.length;var data=[];for(var i=0;i<l;i++)
+data.push({'y':heights[i],'x':widths[i]});return data;};this.draw=function(){var sl=this;with(Processing(sl.canvas)){setup=function(){};draw=function(){background(sl.background);scaled=sl.scale_data();var l=scaled.length;var percentages=sl.calc_percentages();if(sl.fill_between_percentage_lines&&percentages.length>1){noStroke();fill(sl.percentage_fill_color);var height=percentages[percentages.length-1]-percentages[0];var width=scaled[l-1].x-scaled[0].x;rect(scaled[0].x,percentages[0],width,height);}
+var value_lines=sl.calc_value_lines();if(sl.fill_between_value_lines&&value_lines.length>1){noStroke();fill(sl.value_line_fill_color);var height=value_lines[value_lines.length-1]-value_lines[0];var width=scaled[l-1].x-scaled[0].x;rect(scaled[0].x,value_lines[0],width,height);}
+stroke(sl.value_line_color);for(var h=0;h<value_lines.length;h++){var y=value_lines[h];line(scaled[0].x,y,scaled[l-1].x,y);}
+stroke(sl.percentage_color);for(var j=0;j<percentages.length;j++){var y=percentages[j];line(scaled[0].x,y,scaled[l-1].x,y);}
+stroke(sl.stroke);for(var i=1;i<l;i++){var curr=scaled[i];var previous=scaled[i-1];line(previous.x,previous.y,curr.x,curr.y);}
+this.exit();};init();};};};var Sparkline=function(id,data,mixins){this.init(id,data,mixins);}
+Sparkline.prototype=new BaseSparkline();var BarSparkline=function(id,data,mixins){if(!mixins)mixins={};this.marking_padding=5;this.padding_between_bars=5;this.extend_markings=true;if(!mixins.hasOwnProperty('scale_from'))mixins.scale_from=0;this.init(id,data,mixins);this.segment_width=function(){var l=this.data.length;var w=this.width();return((w*1.0)-((l-1)*this.padding_between_bars))/l;};this.scale_width=function(){var widths=[];var l=this.data.length;var segment_width=this.segment_width();for(var i=0;i<l;i++){widths.push((i*segment_width)+(this.padding_between_bars*i)+this.left_padding);}
+return widths;};this.draw=function(){var sl=this;with(Processing(sl.canvas)){draw=function(){background(sl.background);var scaled=sl.scale_data();var l=scaled.length;var sw=sl.segment_width();var gap=sl.padding_between_bars;var mp=sl.marking_padding;var value_lines=sl.calc_value_lines();if(sl.fill_between_value_lines&&value_lines.length>1){noStroke();fill(sl.percentage_fill_color);var height=value_lines[value_lines.length-1]-value_lines[0];var width=scaled[l-1].x-scaled[0].x+sw;if(sl.extend_markings){width+=2*mp;rect(scaled[0].x-mp,value_lines[0],width,height);}
+else rect(scaled[0].x,value_lines[0],width,height);}
+stroke(sl.value_line_color);for(var h=0;h<value_lines.length;h++){var y=value_lines[h];if(sl.extend_markings){line(scaled[0].x-mp,y,scaled[l-1].x+mp+sw,y);}
+else line(scaled[0].x,y,scaled[l-1].x+sw,y);}
+var percentages=sl.calc_percentages();if(sl.fill_between_percentage_lines&&percentages.length>1){noStroke();fill(sl.percentage_fill_color);var height=percentages[percentages.length-1]-percentages[0];var width=scaled[l-1].x-scaled[0].x+sw;if(sl.extend_markings){width+=2*mp;rect(scaled[0].x-mp,percentages[0],width,height);}
+else rect(scaled[0].x,percentages[0],width,height);}
+stroke(sl.percentage_color);for(var j=0;j<percentages.length;j++){var y=percentages[j];if(sl.extend_markings){line(scaled[0].x-mp,y,scaled[l-1].x+mp+sw,y);}
+else line(scaled[0].x,y,scaled[l-1].x+sw,y);}
+stroke(sl.stroke);fill(sl.stroke);var width=sl.segment_width();var height=sl.height();for(var i=0;i<l;i++){var d=scaled[i];rect(d.x,d.y,width,height-d.y);};this.exit();};init();};};}
+BarSparkline.prototype=new BaseSparkline();
diff --git a/extensions/OrangeFactor/web/style/orangefactor.css b/extensions/OrangeFactor/web/style/orangefactor.css
new file mode 100644
index 000000000..211ad575e
--- /dev/null
+++ b/extensions/OrangeFactor/web/style/orangefactor.css
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#orange-graph {
+ display: block;
+ width: 180px;
+ height: 38px;
+ margin: 0 .5em .5em 0;
+}
diff --git a/extensions/Persona/Config.pm b/extensions/Persona/Config.pm
new file mode 100644
index 000000000..8709655d1
--- /dev/null
+++ b/extensions/Persona/Config.pm
@@ -0,0 +1,29 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Persona;
+use strict;
+
+use constant NAME => 'Persona';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'JSON',
+ module => 'JSON',
+ version => 0,
+ },
+ {
+ package => 'libwww-perl',
+ module => 'LWP::UserAgent',
+ version => 0,
+ },
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/Persona/Extension.pm b/extensions/Persona/Extension.pm
new file mode 100644
index 000000000..f288702e8
--- /dev/null
+++ b/extensions/Persona/Extension.pm
@@ -0,0 +1,73 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Persona;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Config qw(SetParam write_params);
+
+our $VERSION = '0.01';
+
+sub install_update_db {
+ # The extension changed from BrowserID to Persona
+ # so we need to update user_info_class if this system
+ # was using BrowserID for verification.
+ my $params = Bugzilla->params || Bugzilla::Config::read_param_file();
+ my $user_info_class = $params->{'user_info_class'};
+ if ($user_info_class =~ /BrowserID/) {
+ $user_info_class =~ s/BrowserID/Persona/;
+ SetParam('user_info_class', $user_info_class);
+ write_params();
+ }
+}
+
+sub auth_login_methods {
+ my ($self, $args) = @_;
+ my $modules = $args->{'modules'};
+ if (exists($modules->{'Persona'})) {
+ $modules->{'Persona'} = 'Bugzilla/Extension/Persona/Login.pm';
+ }
+}
+
+sub config_modify_panels {
+ my ($self, $args) = @_;
+ my $panels = $args->{'panels'};
+ my $auth_panel_params = $panels->{'auth'}->{'params'};
+
+ my ($user_info_class) =
+ grep { $_->{'name'} eq 'user_info_class' } @$auth_panel_params;
+
+ if ($user_info_class) {
+ push(@{ $user_info_class->{'choices'} }, "Persona,CGI");
+ }
+
+ # The extension changed from BrowserID to Persona
+ # so we need to retain the current values for the new
+ # params that will be created.
+ my $params = Bugzilla->params || Bugzilla::Config::read_param_file();
+ my $verify_url = $params->{'browserid_verify_url'};
+ my $includejs_url = $params->{'browserid_includejs_url'};
+ if ($verify_url && $includejs_url) {
+ foreach my $param (@{ $panels->{'persona'}->{'params'} }) {
+ if ($param->{'name'} eq 'persona_verify_url') {
+ $param->{'default'} = $verify_url;
+ }
+ if ($param->{'name'} eq 'persona_includejs_url') {
+ $param->{'default'} = $includejs_url;
+ }
+ }
+ }
+}
+
+sub config_add_panels {
+ my ($self, $args) = @_;
+ my $modules = $args->{panel_modules};
+ $modules->{Persona} = "Bugzilla::Extension::Persona::Config";
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/Persona/TODO b/extensions/Persona/TODO
new file mode 100644
index 000000000..ac94a3c42
--- /dev/null
+++ b/extensions/Persona/TODO
@@ -0,0 +1,19 @@
+ToDo:
+
+* Cache the LWP::UserAgent in Login.pm?
+
+* Fix Bugzilla::Auth::Login::Stack to allow failure part way down the chain
+ (currently, it seems that both CGI and BrowserID have to be last in order
+ to report login failures correctly.)
+
+* JS inclusions noticeably slow page load. Do we want a local copy of
+ browserid.js? Do the browserid folks object to that? How can we get good
+ performance? How can we avoid including it in every logged-in page? Can we
+ do demand loading onclick, and/or load-on-reveal?
+
+* Fix -8px margin-bottom hack in login-small-additional_methods.html.tmpl
+
+
+
+
+
diff --git a/extensions/Persona/lib/Config.pm b/extensions/Persona/lib/Config.pm
new file mode 100644
index 000000000..9c483cb51
--- /dev/null
+++ b/extensions/Persona/lib/Config.pm
@@ -0,0 +1,41 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Persona::Config;
+
+use strict;
+use warnings;
+
+use Bugzilla::Config::Common;
+
+our $sortkey = 1350;
+
+sub get_param_list {
+ my ($class) = @_;
+
+ my @param_list = (
+ {
+ name => 'persona_verify_url',
+ type => 't',
+ default => 'https://verifier.login.persona.org/verify',
+ },
+ {
+ name => 'persona_includejs_url',
+ type => 't',
+ default => 'https://login.persona.org/include.js',
+ },
+ {
+ name => 'persona_proxy_url',
+ type => 't',
+ default => '',
+ },
+ );
+
+ return @param_list;
+}
+
+1;
diff --git a/extensions/Persona/lib/Login.pm b/extensions/Persona/lib/Login.pm
new file mode 100644
index 000000000..ece92a3c0
--- /dev/null
+++ b/extensions/Persona/lib/Login.pm
@@ -0,0 +1,127 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Persona::Login;
+use strict;
+use base qw(Bugzilla::Auth::Login);
+
+use Bugzilla::Constants;
+use Bugzilla::Util;
+use Bugzilla::Error;
+use Bugzilla::Token;
+
+use JSON;
+use LWP::UserAgent;
+
+use constant requires_verification => 0;
+use constant is_automatic => 1;
+use constant user_can_create_account => 1;
+
+sub get_login_info {
+ my ($self) = @_;
+
+ my $cgi = Bugzilla->cgi;
+
+ my $assertion = $cgi->param("persona_assertion");
+ # Avoid the assertion being copied into any 'echoes' of the current URL
+ # in the page.
+ $cgi->delete('persona_assertion');
+
+ if (!$assertion || !Bugzilla->params->{persona_verify_url}) {
+ return { failure => AUTH_NODATA };
+ }
+
+ my $token = $cgi->param("token");
+ $cgi->delete('token');
+ check_hash_token($token, ['login']);
+
+ my $urlbase = new URI(correct_urlbase());
+ my $audience = $urlbase->scheme . "://" . $urlbase->host_port;
+
+ my $ua = new LWP::UserAgent( timeout => 10 );
+ if (Bugzilla->params->{persona_proxy_url}) {
+ $ua->proxy('https', Bugzilla->params->{persona_proxy_url});
+ }
+
+ my $response = $ua->post(Bugzilla->params->{persona_verify_url},
+ [ assertion => $assertion,
+ audience => $audience ]);
+ if ($response->is_error) {
+ return { failure => AUTH_ERROR,
+ user_error => 'persona_server_fail',
+ details => { reason => $response->message }};
+ }
+
+ my $info;
+ eval {
+ $info = decode_json($response->decoded_content());
+ };
+ if ($@) {
+ return { failure => AUTH_ERROR,
+ user_error => 'persona_server_fail',
+ details => { reason => 'Received a malformed response.' }};
+ }
+ if ($info->{'status'} eq 'failure') {
+ return { failure => AUTH_ERROR,
+ user_error => 'persona_server_fail',
+ details => { reason => $info->{reason} }};
+ }
+
+ if ($info->{'status'} eq "okay" &&
+ $info->{'audience'} eq $audience &&
+ ($info->{'expires'} / 1000) > time())
+ {
+ my $login_data = {
+ 'username' => $info->{'email'}
+ };
+
+ my $result = Bugzilla::Auth::Verify->create_or_update_user($login_data);
+ return $result if $result->{'failure'};
+
+ my $user = $result->{'user'};
+
+ # You can restrict people in a particular group from logging in using
+ # Persona by making that group a member of a group called
+ # "no-browser-id".
+ #
+ # If you have your "createemailregexp" set up in such a way that a
+ # newly-created account is a member of "no-browser-id", this code will
+ # create an account for them and then fail their login. Which isn't
+ # great, but they can still use normal-Bugzilla-login password
+ # recovery.
+ if ($user->in_group('no-browser-id')) {
+ return { failure => AUTH_ERROR,
+ user_error => 'persona_account_too_powerful' };
+ }
+
+ $login_data->{'user'} = $user;
+ $login_data->{'user_id'} = $user->id;
+
+ return $login_data;
+ }
+ else {
+ return { failure => AUTH_LOGINFAILED };
+ }
+}
+
+# Pinched from Bugzilla::Auth::Login::CGI
+sub fail_nodata {
+ my ($self) = @_;
+ my $cgi = Bugzilla->cgi;
+ my $template = Bugzilla->template;
+
+ if (Bugzilla->usage_mode != USAGE_MODE_BROWSER) {
+ ThrowUserError('login_required');
+ }
+
+ print $cgi->header();
+ $template->process("account/auth/login.html.tmpl", { 'target' => $cgi->url(-relative=>1) })
+ || ThrowTemplateError($template->error());
+ exit;
+}
+
+1;
diff --git a/extensions/Persona/template/en/default/admin/params/browserid.html.tmpl b/extensions/Persona/template/en/default/admin/params/browserid.html.tmpl
new file mode 100644
index 000000000..379d12058
--- /dev/null
+++ b/extensions/Persona/template/en/default/admin/params/browserid.html.tmpl
@@ -0,0 +1,22 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%
+ title = "Persona"
+ desc = "Configure Persona Authentication"
+%]
+
+[% param_descs = {
+ persona_verify_url => "This is the URL for the Persona authority that the " _
+ "user will be verified against. " _
+ "Example: <kbd>https://verifier.login.persona.org/verify</kbd>.",
+ persona_includejs_url => "This is the URL needed by Persona to load the necessary " _
+ "javascript library for authentication. " _
+ "Example: <kbd>https://persona.org/include.js</kbd>."
+ }
+%]
diff --git a/extensions/Persona/template/en/default/admin/params/persona.html.tmpl b/extensions/Persona/template/en/default/admin/params/persona.html.tmpl
new file mode 100644
index 000000000..ef3cf32d2
--- /dev/null
+++ b/extensions/Persona/template/en/default/admin/params/persona.html.tmpl
@@ -0,0 +1,24 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%
+ title = "Persona"
+ desc = "Configure Persona Authentication"
+%]
+
+[% param_descs = {
+ persona_verify_url => "This is the URL for the Persona authority that the " _
+ "user will be verified against. " _
+ "Example: <kbd>https://verifier.login.persona.org/verify</kbd>.",
+ persona_includejs_url => "This is the URL needed by Persona to load the necessary " _
+ "javascript library for authentication. " _
+ "Example: <kbd>https://login.persona.org/include.js</kbd>."
+ persona_proxy_url => "The URL of a HTTPS proxy server (optional). " _
+ "Example: <kbd>http://proxy.example.com:3128</kbd>."
+ }
+%]
diff --git a/extensions/Persona/template/en/default/hook/account/auth/login-additional_methods.html.tmpl b/extensions/Persona/template/en/default/hook/account/auth/login-additional_methods.html.tmpl
new file mode 100644
index 000000000..5be7910ad
--- /dev/null
+++ b/extensions/Persona/template/en/default/hook/account/auth/login-additional_methods.html.tmpl
@@ -0,0 +1,6 @@
+[% IF Param('user_info_class').split(',').contains('Persona')
+ && Param('persona_includejs_url') %]
+<p>
+ <img src="extensions/Persona/web/images/persona_sign_in.png" width="185" height="25" onclick="persona_sign_in()">
+</p>
+[% END %]
diff --git a/extensions/Persona/template/en/default/hook/account/auth/login-small-additional_methods.html.tmpl b/extensions/Persona/template/en/default/hook/account/auth/login-small-additional_methods.html.tmpl
new file mode 100644
index 000000000..5d8503d73
--- /dev/null
+++ b/extensions/Persona/template/en/default/hook/account/auth/login-small-additional_methods.html.tmpl
@@ -0,0 +1,17 @@
+[% IF Param('user_info_class').split(',').contains('Persona')
+ && Param('persona_includejs_url') %]
+<script type="text/javascript">
+ YAHOO.util.Event.addListener('login_link[% qs_suffix FILTER js %]','click', function () {
+ var login_link = YAHOO.util.Dom.get('persona_mini_login[% qs_suffix FILTER js %]');
+ YAHOO.util.Dom.removeClass(login_link, 'bz_default_hidden');
+ });
+ YAHOO.util.Event.addListener('hide_mini_login[% qs_suffix FILTER js %]','click', function () {
+ var login_link = YAHOO.util.Dom.get('persona_mini_login[% qs_suffix FILTER js %]');
+ YAHOO.util.Dom.addClass(login_link, 'bz_default_hidden');
+ });
+</script>
+<span id="persona_mini_login[% qs_suffix FILTER html %]" class="bz_default_hidden">
+ <img src="extensions/Persona/web/images/sign_in.png" height="22" width="75" align="absmiddle"
+ title="Sign in with Persona" onclick="persona_sign_in()"> or
+</span>
+[% END %]
diff --git a/extensions/Persona/template/en/default/hook/account/create-additional_methods.html.tmpl b/extensions/Persona/template/en/default/hook/account/create-additional_methods.html.tmpl
new file mode 100644
index 000000000..355ce3629
--- /dev/null
+++ b/extensions/Persona/template/en/default/hook/account/create-additional_methods.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS Param('user_info_class').split(',').contains('Persona') %]
+
+Or, use your Persona account:
+<img src="extensions/Persona/web/images/sign_in.png" onclick="persona_sign_in()"
+ width="95" height="25" align="absmiddle">
diff --git a/extensions/Persona/template/en/default/hook/global/header-additional_header.html.tmpl b/extensions/Persona/template/en/default/hook/global/header-additional_header.html.tmpl
new file mode 100644
index 000000000..786010a34
--- /dev/null
+++ b/extensions/Persona/template/en/default/hook/global/header-additional_header.html.tmpl
@@ -0,0 +1,87 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS Param('persona_includejs_url')
+ && Param('user_info_class').split(',').contains('Persona') %]
+
+[%# for now don't inject persona javascript on authenticated users.
+ # we've seen sessions being logged out unexpectedly
+ # we should only inject this code for users who used persona to authenicate %]
+[% RETURN IF user.id %]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+<script defer src="[% Param('persona_includejs_url') %]" type="text/javascript"></script>
+<script type="text/javascript">
+
+function createHidden(name, value, form) {
+ var field = document.createElement('input');
+ field.type = 'hidden';
+ field.name = name;
+ field.value = value;;
+ form.appendChild(field);
+}
+
+[% login_target = cgi.url("-relative" => 1, "-query" => 1) %]
+[% IF !login_target
+ OR login_target.match("^token\.cgi")
+ OR login_target.match("^createaccount\.cgi") %]
+ [% login_target = "index.cgi" %]
+[% END %]
+[% login_target = urlbase _ login_target %]
+
+[%# we only want to honour explicit login requests %]
+var persona_ignore_login = true;
+
+function persona_onlogin(assertion) {
+ if (persona_ignore_login)
+ return;
+ [% IF !user.id %]
+ var form = document.createElement('form');
+ form.action = '[% login_target FILTER js %]';
+ form.method = 'POST';
+ form.style.display = 'none';
+
+ createHidden('token', '[% issue_hash_token(['login']) FILTER js %]', form);
+ createHidden('Bugzilla_remember', 'on', form);
+ createHidden('persona_assertion', assertion, form);
+
+ [% FOREACH field = cgi.param() %]
+ [% NEXT IF field.search("^(Bugzilla_(login|password|restrictlogin)|token|persona_assertion)$") %]
+ [% NEXT UNLESS cgi.param(field).can('slice') %]
+ [% FOREACH mvalue = cgi.param(field).slice(0) %]
+ createHidden('[% field FILTER js %]', '[% mvalue FILTER html_linebreak FILTER js %]', form);
+ [% END %]
+ [% END %]
+
+ document.body.appendChild(form);
+ form.submit();
+ [% END %]
+}
+
+YAHOO.util.Event.on(window, 'load', persona_init);
+function persona_init() {
+ navigator.id.watch({
+ [%# we can't set loggedInUser to user.login as this causes cgi authenticated
+ sessions to be logged out by persona %]
+ loggedInUser: null,
+ onlogin: persona_onlogin,
+ onlogout: function () {
+ [%# this should be redirecting to index.cgi?logout=1 however there's a
+ persona bug which causes this to break chrome and safari logins.
+ https://github.com/mozilla/browserid/issues/2423 %]
+ }
+ });
+}
+
+function persona_sign_in() {
+ persona_ignore_login = false;
+ navigator.id.request({ siteName: '[% terms.BugzillaTitle FILTER js %]' });
+}
+</script>
diff --git a/extensions/Persona/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/Persona/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..f2e5bda24
--- /dev/null
+++ b/extensions/Persona/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,12 @@
+[% IF error == "persona_account_too_powerful" %]
+ [% title = "Account Too Powerful" %]
+ Your account is a member of a group which is not permitted to use Persona to
+ log in. Please log in with your [% terms.Bugzilla %] username and password.
+ <br><br>
+ (Persona logins are disabled for accounts which are members of certain
+ particularly sensitive groups, while we gain experience with the technology.)
+[% ELSIF error == "persona_server_fail" %]
+ An error occurred during communication with the Persona servers:
+ <br>
+ [% reason FILTER html %]
+[% END %]
diff --git a/extensions/Persona/web/images/persona_sign_in.png b/extensions/Persona/web/images/persona_sign_in.png
new file mode 100644
index 000000000..ab88a7154
--- /dev/null
+++ b/extensions/Persona/web/images/persona_sign_in.png
Binary files differ
diff --git a/extensions/Persona/web/images/sign_in.png b/extensions/Persona/web/images/sign_in.png
new file mode 100644
index 000000000..82594ba82
--- /dev/null
+++ b/extensions/Persona/web/images/sign_in.png
Binary files differ
diff --git a/extensions/ProdCompSearch/Config.pm b/extensions/ProdCompSearch/Config.pm
new file mode 100644
index 000000000..c28b6d8f6
--- /dev/null
+++ b/extensions/ProdCompSearch/Config.pm
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ProdCompSearch;
+use strict;
+
+use constant NAME => 'ProdCompSearch';
+use constant REQUIRED_MODULES => [];
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/ProdCompSearch/Extension.pm b/extensions/ProdCompSearch/Extension.pm
new file mode 100644
index 000000000..a5955fd8b
--- /dev/null
+++ b/extensions/ProdCompSearch/Extension.pm
@@ -0,0 +1,21 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ProdCompSearch;
+use strict;
+use base qw(Bugzilla::Extension);
+
+our $VERSION = '1';
+
+sub webservice {
+ my ($self, $args) = @_;
+ my $dispatch = $args->{dispatch};
+ $dispatch->{PCS} = "Bugzilla::Extension::ProdCompSearch::WebService";
+}
+
+
+__PACKAGE__->NAME;
diff --git a/extensions/ProdCompSearch/lib/WebService.pm b/extensions/ProdCompSearch/lib/WebService.pm
new file mode 100644
index 000000000..d668809f6
--- /dev/null
+++ b/extensions/ProdCompSearch/lib/WebService.pm
@@ -0,0 +1,121 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ProdCompSearch::WebService;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::WebService);
+
+use Bugzilla::Error;
+use Bugzilla::Util qw(detaint_natural trick_taint trim);
+
+sub prod_comp_search {
+ my ($self, $params) = @_;
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->switch_to_shadow_db();
+
+ my $search = trim($params->{'search'} || '');
+ $search || ThrowCodeError('param_required',
+ { function => 'PCS.prod_comp_search', param => 'search' });
+
+ my $limit = detaint_natural($params->{'limit'})
+ ? $dbh->sql_limit($params->{'limit'})
+ : '';
+
+ # We do this in the DB directly as we want it to be fast and
+ # not have the overhead of loading full product objects
+
+ # All products which the user has "Entry" access to.
+ my $enterable_ids = $dbh->selectcol_arrayref(
+ 'SELECT products.id FROM products
+ LEFT JOIN group_control_map
+ ON group_control_map.product_id = products.id
+ AND group_control_map.entry != 0
+ AND group_id NOT IN (' . $user->groups_as_string . ')
+ WHERE group_id IS NULL
+ AND products.isactive = 1');
+
+ if (scalar @$enterable_ids) {
+ # And all of these products must have at least one component
+ # and one version.
+ $enterable_ids = $dbh->selectcol_arrayref(
+ 'SELECT DISTINCT products.id FROM products
+ WHERE ' . $dbh->sql_in('products.id', $enterable_ids) .
+ ' AND products.id IN (SELECT DISTINCT components.product_id
+ FROM components
+ WHERE components.isactive = 1)
+ AND products.id IN (SELECT DISTINCT versions.product_id
+ FROM versions
+ WHERE versions.isactive = 1)');
+ }
+
+ return { products => [] } if !scalar @$enterable_ids;
+
+ trick_taint($search);
+ my @terms;
+ my @order;
+
+ if ($search =~ /^(.*?)::(.*)$/) {
+ my ($product, $component) = (trim($1), trim($2));
+ push @terms, _build_terms($product, 1, 0);
+ push @terms, _build_terms($component, 0, 1);
+ push @order, "products.name != " . $dbh->quote($product) if $product ne '';
+ push @order, "components.name != " . $dbh->quote($component) if $component ne '';
+ push @order, "products.name";
+ push @order, "components.name";
+ } else {
+ push @terms, _build_terms($search, 1, 1);
+ push @order, "products.name != " . $dbh->quote($search);
+ push @order, "components.name != " . $dbh->quote($search);
+ push @order, "products.name";
+ push @order, "components.name";
+ }
+ return { products => [] } if !scalar @terms;
+
+ # To help mozilla staff file bmo administration bugs into the right
+ # component, sort bmo first when searching for 'bugzilla'
+ if ($search =~ /bugzilla/i && $search !~ /^bugzilla\s*::/i
+ && ($user->in_group('mozilla-corporation') || $user->in_group('mozilla-foundation')))
+ {
+ unshift @order, "products.name != 'bugzilla.mozilla.org'";
+ }
+
+ my $products = $dbh->selectall_arrayref("
+ SELECT products.name AS product,
+ components.name AS component
+ FROM products
+ INNER JOIN components ON products.id = components.product_id
+ WHERE (" . join(" AND ", @terms) . ")
+ AND products.id IN (" . join(",", @$enterable_ids) . ")
+ AND components.isactive = 1
+ ORDER BY " . join(", ", @order) . " $limit",
+ { Slice => {} });
+
+ return { products => $products };
+}
+
+sub _build_terms {
+ my ($query, $product, $component) = @_;
+ my $dbh = Bugzilla->dbh();
+
+ my @fields;
+ push @fields, 'products.name', 'products.description' if $product;
+ push @fields, 'components.name', 'components.description' if $component;
+ # note: CONCAT_WS is MySQL specific
+ my $field = "CONCAT_WS(' ', ". join(',', @fields) . ")";
+
+ my @terms;
+ foreach my $word (split(/[\s,]+/, $query)) {
+ push(@terms, $dbh->sql_iposition($dbh->quote($word), $field) . " > 0")
+ if $word ne '';
+ }
+ return @terms;
+}
+
+1;
diff --git a/extensions/ProdCompSearch/template/en/default/pages/prodcompsearch.html.tmpl b/extensions/ProdCompSearch/template/en/default/pages/prodcompsearch.html.tmpl
new file mode 100644
index 000000000..5b39315b5
--- /dev/null
+++ b/extensions/ProdCompSearch/template/en/default/pages/prodcompsearch.html.tmpl
@@ -0,0 +1,25 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "File a $terms.Bug"
+ javascript_urls = [ "js/yui3/yui/yui-min.js",
+ "extensions/ProdCompSearch/web/js/prod_comp_search.js" ]
+ style_urls = [ "extensions/ProdCompSearch/web/styles/prod_comp_search.css" ]
+%]
+
+<div id="prod_comp_search_main">
+ [% PROCESS prodcompsearch/form.html.tmpl
+ query_header = "File a $terms.Bug:"
+ script_name = "enter_bug.cgi"
+ %]
+</div>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl b/extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl
new file mode 100644
index 000000000..38f87dc1a
--- /dev/null
+++ b/extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl
@@ -0,0 +1,40 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% DEFAULT max_results = 100 %]
+<script type="text/javascript">
+ [% IF script_name %]
+ ProdCompSearch.script_name = '[% script_name FILTER js %]';
+ [% END %]
+ [% IF format %]
+ ProdCompSearch.format = '[% format FILTER js %]';
+ [% END %]
+ [% IF cloned_bug_id %]
+ ProdCompSearch.cloned_bug_id = '[% cloned_bug_id FILTER js %]';
+ [% END %]
+ [% IF new_tab %]
+ ProdCompSearch.new_tab = true;
+ [% END %]
+ ProdCompSearch.max_results = [% max_results FILTER js %];
+</script>
+
+<div id="prod_comp_search_form" class="yui3-skin-sam">
+ <div id="prod_comp_search_header">
+ [% input_label FILTER none %]&nbsp;
+ <img id="prod_comp_throbber" src="extensions/ProdCompSearch/web/images/throbber.gif"
+ class="bz_default_hidden" width="16" height="11">
+ <span id="prod_comp_no_components" class="bz_default_hidden">
+ No components found</span>
+ <span id="prod_comp_too_many_components" class="bz_default_hidden">
+ Result limited to [% max_results FILTER html %] components
+ <span id="prod_comp_error" class="bz_default_hidden">
+ An error occured</span>
+ </div>
+ <input id="prod_comp_search" type="text" size="50"
+ placeholder="Search by product and component keywords">
+</div>
diff --git a/extensions/ProdCompSearch/web/images/throbber.gif b/extensions/ProdCompSearch/web/images/throbber.gif
new file mode 100644
index 000000000..bc4fa6561
--- /dev/null
+++ b/extensions/ProdCompSearch/web/images/throbber.gif
Binary files differ
diff --git a/extensions/ProdCompSearch/web/js/prod_comp_search.js b/extensions/ProdCompSearch/web/js/prod_comp_search.js
new file mode 100644
index 000000000..f294994e3
--- /dev/null
+++ b/extensions/ProdCompSearch/web/js/prod_comp_search.js
@@ -0,0 +1,144 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+// Product and component search to file a new bug
+
+var ProdCompSearch = {
+ script_name: 'enter_bug.cgi',
+ script_choices: ['enter_bug.cgi', 'describecomponents.cgi'],
+ format: null,
+ cloned_bug_id: null,
+ new_tab: null,
+ max_results: 100
+};
+
+YUI({
+ base: 'js/yui3/',
+ combine: false
+}).use("node", "json-stringify", "autocomplete", "escape",
+ "datasource-io", "datasource-jsonschema", function(Y) {
+ Y.on("domready", function() {
+ var counter = 0,
+ dataSource = null,
+ autoComplete = null;
+
+ var resultListFormat = function(query, results) {
+ return Y.Array.map(results, function(result) {
+ var data = result.raw;
+ result.text = data.product + ' :: ' + data.component;
+ return Y.Escape.html(result.text);
+ });
+ };
+
+ var requestTemplate = function(query) {
+ counter = counter + 1;
+ var json_object = {
+ version: "1.1",
+ method : "PCS.prod_comp_search",
+ id : counter,
+ params : { search: query, limit: ProdCompSearch.max_results }
+ };
+ return Y.JSON.stringify(json_object);
+ };
+
+ var dataSource = new Y.DataSource.IO({
+ source: 'jsonrpc.cgi',
+ ioConfig: {
+ method: "POST",
+ headers: { 'Content-Type': 'application/json' }
+ },
+ on: {
+ error: function(e) {
+ if (console.error && e.response.meta.error) {
+ console.error(e.response.meta.error.message);
+ }
+ Y.one("#prod_comp_throbber").addClass('bz_default_hidden');
+ Y.one("#prod_comp_error").removeClass('bz_default_hidden');
+ }
+ }
+ });
+
+ dataSource.plug(Y.Plugin.DataSourceJSONSchema, {
+ schema: {
+ resultListLocator : "result.products",
+ resultFields : [ "product", "component" ],
+ metaFields : { error : 'error' }
+ }
+ });
+
+ var input = Y.one('#prod_comp_search');
+
+ input.plug(Y.Plugin.AutoComplete, {
+ activateFirstItem: false,
+ enableCache: true,
+ source: dataSource,
+ minQueryLength: 3,
+ queryDelay: 0.05,
+ resultFormatter: resultListFormat,
+ suppressInputUpdate: true,
+ maxResults: ProdCompSearch.max_results,
+ scrollIntoView: true,
+ requestTemplate: requestTemplate,
+ on: {
+ query: function(e) {
+ Y.one("#prod_comp_throbber").removeClass('bz_default_hidden');
+ Y.one("#prod_comp_no_components").addClass('bz_default_hidden');
+ Y.one("#prod_comp_too_many_components").addClass('bz_default_hidden');
+ Y.one("#prod_comp_error").addClass('bz_default_hidden');
+ },
+ results: function(e) {
+ Y.one("#prod_comp_throbber").addClass('bz_default_hidden');
+ input.ac.set('activateFirstItem', e.results.length == 1);
+ if (e.results.length == 0) {
+ Y.one("#prod_comp_no_components").removeClass('bz_default_hidden');
+ }
+ else if (e.results.length + 1 > ProdCompSearch.max_results) {
+ Y.one("#prod_comp_too_many_components").removeClass('bz_default_hidden');
+ }
+ },
+ select: function(e) {
+ // Only redirect if the script_name is a valid choice
+ if (Y.Array.indexOf(ProdCompSearch.script_choices, ProdCompSearch.script_name) == -1)
+ return;
+
+ var data = e.result.raw;
+ var url = ProdCompSearch.script_name +
+ "?product=" + encodeURIComponent(data.product) +
+ "&component=" + encodeURIComponent(data.component);
+ if (ProdCompSearch.script_name == 'enter_bug.cgi') {
+ if (ProdCompSearch.format)
+ url += "&format=" + encodeURIComponent(ProdCompSearch.format);
+ if (ProdCompSearch.cloned_bug_id)
+ url += "&cloned_bug_id=" + encodeURIComponent(ProdCompSearch.cloned_bug_id);
+ }
+ if (ProdCompSearch.script_name == 'describecomponents.cgi') {
+ url += "#" + encodeURIComponent(data.component);
+ }
+ if (ProdCompSearch.new_tab) {
+ window.open(url, '_blank');
+ }
+ else {
+ window.location.href = url;
+ }
+ }
+ },
+ after: {
+ select: function(e) {
+ if (ProdCompSearch.new_tab) {
+ input.set('value','');
+ }
+ }
+ }
+ });
+
+ input.on('focus', function (e) {
+ if (e.target.value && e.target.value.length > 3) {
+ dataSource.load(e.target.value);
+ }
+ });
+ });
+});
diff --git a/extensions/ProdCompSearch/web/styles/prod_comp_search.css b/extensions/ProdCompSearch/web/styles/prod_comp_search.css
new file mode 100644
index 000000000..ccb5887c4
--- /dev/null
+++ b/extensions/ProdCompSearch/web/styles/prod_comp_search.css
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#prod_comp_search_main {
+ width: 400px;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+#prod_comp_search_form .yui3-aclist-input {
+ width: 360px;
+}
+
+#prod_comp_search_form .yui3-aclist-content {
+ max-height: 500px;
+ overflow: auto;
+}
+
+#prod_comp_no_components,
+#prod_comp_error,
+#prod_comp_too_many_components {
+ color: red;
+}
diff --git a/extensions/ProductDashboard/Config.pm b/extensions/ProductDashboard/Config.pm
new file mode 100644
index 000000000..3a4654974
--- /dev/null
+++ b/extensions/ProductDashboard/Config.pm
@@ -0,0 +1,14 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ProductDashboard;
+
+use strict;
+
+use constant NAME => 'ProductDashboard';
+
+__PACKAGE__->NAME;
diff --git a/extensions/ProductDashboard/Extension.pm b/extensions/ProductDashboard/Extension.pm
new file mode 100644
index 000000000..1e6ddffe9
--- /dev/null
+++ b/extensions/ProductDashboard/Extension.pm
@@ -0,0 +1,200 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ProductDashboard;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Util;
+use Bugzilla::Error;
+use Bugzilla::Product;
+use Bugzilla::Field;
+
+use Bugzilla::Extension::ProductDashboard::Queries;
+use Bugzilla::Extension::ProductDashboard::Util;
+
+our $VERSION = BUGZILLA_VERSION;
+
+sub page_before_template {
+ my ($self, $args) = @_;
+
+ my $page = $args->{page_id};
+ my $vars = $args->{vars};
+
+ if ($page =~ m{^productdashboard\.}) {
+ _page_dashboard($vars);
+ }
+}
+
+sub _page_dashboard {
+ my $vars = shift;
+
+ my $cgi = Bugzilla->cgi;
+ my $input = Bugzilla->input_params;
+ my $user = Bugzilla->user;
+
+ # Switch to shadow db since we are just reading information
+ Bugzilla->switch_to_shadow_db();
+
+ # All pages point to the same part of the documentation.
+ $vars->{'doc_section'} = 'bugreports.html';
+
+ # Forget any previously selected product
+ $cgi->send_cookie(-name => 'PRODUCT_DASHBOARD',
+ -value => 'X',
+ -expires => "Fri, 01-Jan-1970 00:00:00 GMT");
+
+ # If the user cannot enter bugs in any product, stop here.
+ scalar @{$user->get_selectable_products}
+ || ThrowUserError('no_products');
+
+ # Create data structures representing each classification
+ my @classifications = ();
+ foreach my $c (@{$user->get_selectable_classifications}) {
+ # Create hash to hold attributes for each classification.
+ my %classification = (
+ 'name' => $c->name,
+ 'products' => [ @{$user->get_selectable_products($c->id)} ]
+ );
+ # Assign hash back to classification array.
+ push @classifications, \%classification;
+ }
+ $vars->{'classifications'} = \@classifications;
+
+ my $product_name = trim($input->{'product'} || '');
+
+ if (!$product_name && $cgi->cookie('PRODUCT_DASHBOARD')) {
+ $product_name = $cgi->cookie('PRODUCT_DASHBOARD');
+ }
+
+ return if !$product_name;
+
+ # Do not use Bugzilla::Product::check_product() here, else the user
+ # could know whether the product doesn't exist or is not accessible.
+ my $product = new Bugzilla::Product({'name' => $product_name});
+
+ # We need to check and make sure that the user has permission
+ # to enter a bug against this product.
+ if (!$product || !$user->can_enter_product($product->name)) {
+ return;
+ }
+
+ # Remember selected product
+ $cgi->send_cookie(-name => 'PRODUCT_DASHBOARD',
+ -value => $product->name,
+ -expires => "Fri, 01-Jan-2038 00:00:00 GMT");
+
+ my $current_tab_name = $input->{'tab'} || "summary";
+ trick_taint($current_tab_name);
+ $vars->{'current_tab_name'} = $current_tab_name;
+
+ my $bug_status = trim($input->{'bug_status'} || 'open');
+
+ $vars->{'bug_status'} = $bug_status;
+ $vars->{'product'} = $product;
+ $vars->{'bug_link_all'} = bug_link_all($product);
+ $vars->{'bug_link_open'} = bug_link_open($product);
+ $vars->{'bug_link_closed'} = bug_link_closed($product);
+ $vars->{'total_bugs'} = total_bugs($product);
+ $vars->{'total_open_bugs'} = total_open_bugs($product);
+ $vars->{'total_closed_bugs'} = total_closed_bugs($product);
+ $vars->{'severities'} = get_legal_field_values('bug_severity');
+
+ if ($vars->{'total_bugs'}) {
+ $vars->{'open_bugs_percentage'}
+ = int($vars->{'total_open_bugs'} / $vars->{'total_bugs'} * 100);
+ $vars->{'closed_bugs_percentage'}
+ = int($vars->{'total_closed_bugs'} / $vars->{'total_bugs'} * 100);
+ }
+ else {
+ $vars->{'open_bugs_percentage'} = 0;
+ $vars->{'closed_bugs_percentage'} = 0;
+ }
+
+ if ($current_tab_name eq 'summary') {
+ $vars->{'by_priority'} = by_priority($product, $bug_status);
+ $vars->{'by_severity'} = by_severity($product, $bug_status);
+ $vars->{'by_assignee'} = by_assignee($product, $bug_status, 50);
+ $vars->{'by_status'} = by_status($product, $bug_status);
+ }
+
+ if ($current_tab_name eq 'recents') {
+ my $recent_days = $input->{'recent_days'} || 7;
+ (detaint_natural($recent_days) && $recent_days > 0 && $recent_days < 101)
+ || ThrowUserError('product_dashboard_invalid_recent_days');
+
+ my $params = {
+ product => $product,
+ days => $recent_days,
+ date_from => $input->{'date_from'} || '',
+ date_to => $input->{'date_to'} || '',
+ };
+
+ $vars->{'recently_opened'} = recently_opened($params);
+ $vars->{'recently_closed'} = recently_closed($params);
+ $vars->{'recent_days'} = $recent_days;
+ $vars->{'date_from'} = $input->{'date_from'};
+ $vars->{'date_to'} = $input->{'date_to'};
+ }
+
+ if ($current_tab_name eq 'components') {
+ if ($input->{'component'}) {
+ $vars->{'summary'} = by_value_summary($product, 'component', $input->{'component'}, $bug_status);
+ $vars->{'summary'}{'type'} = 'component';
+ $vars->{'summary'}{'value'} = $input->{'component'};
+ }
+ elsif ($input->{'version'}) {
+ $vars->{'summary'} = by_value_summary($product, 'version', $input->{'version'}, $bug_status);
+ $vars->{'summary'}{'type'} = 'version';
+ $vars->{'summary'}{'value'} = $input->{'version'};
+ }
+ elsif ($input->{'target_milestone'} && Bugzilla->params->{'usetargetmilestone'}) {
+ $vars->{'summary'} = by_value_summary($product, 'target_milestone', $input->{'target_milestone'}, $bug_status);
+ $vars->{'summary'}{'type'} = 'target_milestone';
+ $vars->{'summary'}{'value'} = $input->{'target_milestone'};
+ }
+ else {
+ $vars->{'by_component'} = by_component($product, $bug_status);
+ $vars->{'by_version'} = by_version($product, $bug_status);
+ if (Bugzilla->params->{'usetargetmilestone'}) {
+ $vars->{'by_milestone'} = by_milestone($product, $bug_status);
+ }
+ }
+ }
+
+ if ($current_tab_name eq 'duplicates') {
+ $vars->{'by_duplicate'} = by_duplicate($product, $bug_status);
+ }
+
+ if ($current_tab_name eq 'popularity') {
+ $vars->{'by_popularity'} = by_popularity($product, $bug_status);
+ }
+
+ if ($current_tab_name eq 'roadmap') {
+ foreach my $milestone (@{$product->milestones}){
+ my %milestone_stats;
+ $milestone_stats{'name'} = $milestone->name;
+ $milestone_stats{'total_bugs'} = total_bug_milestone($product, $milestone);
+ $milestone_stats{'open_bugs'} = bug_milestone_by_status($product, $milestone, 'open');
+ $milestone_stats{'closed_bugs'} = bug_milestone_by_status($product, $milestone, 'closed');
+ $milestone_stats{'link_total'} = bug_milestone_link_total($product, $milestone);
+ $milestone_stats{'link_open'} = bug_milestone_link_open($product, $milestone);
+ $milestone_stats{'link_closed'} = bug_milestone_link_closed($product, $milestone);
+ $milestone_stats{'percentage'} = $milestone_stats{'total_bugs'}
+ ? int(($milestone_stats{'closed_bugs'} / $milestone_stats{'total_bugs'}) * 100)
+ : 0;
+ push (@{$vars->{'by_roadmap'}}, \%milestone_stats);
+ }
+ }
+}
+
+__PACKAGE__->NAME;
+
diff --git a/extensions/ProductDashboard/lib/Queries.pm b/extensions/ProductDashboard/lib/Queries.pm
new file mode 100644
index 000000000..ec27d3c6c
--- /dev/null
+++ b/extensions/ProductDashboard/lib/Queries.pm
@@ -0,0 +1,476 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Extension::ProductDashboard::Queries;
+
+use strict;
+
+use base qw(Exporter);
+@Bugzilla::Extension::ProductDashboard::Queries::EXPORT = qw(
+ total_bugs
+ total_open_bugs
+ total_closed_bugs
+ by_version
+ by_value_summary
+ by_milestone
+ by_priority
+ by_severity
+ by_component
+ by_assignee
+ by_status
+ by_duplicate
+ by_popularity
+ recently_opened
+ recently_closed
+ total_bug_milestone
+ bug_milestone_by_status
+);
+
+use Bugzilla::CGI;
+use Bugzilla::Error;
+use Bugzilla::User;
+use Bugzilla::Util;
+use Bugzilla::Component;
+use Bugzilla::Version;
+use Bugzilla::Milestone;
+
+use Bugzilla::Extension::ProductDashboard::Util qw(open_states closed_states
+ quoted_open_states quoted_closed_states);
+
+sub total_bugs {
+ my $product = shift;
+ my $dbh = Bugzilla->dbh;
+
+ return $dbh->selectrow_array("SELECT COUNT(bug_id)
+ FROM bugs
+ WHERE product_id = ?", undef, $product->id);
+}
+
+sub total_open_bugs {
+ my $product = shift;
+ my $bug_status = shift;
+ my $dbh = Bugzilla->dbh;
+
+ return $dbh->selectrow_array("SELECT COUNT(bug_id)
+ FROM bugs
+ WHERE bug_status IN (" . join(',', quoted_open_states()) . ")
+ AND product_id = ?", undef, $product->id);
+}
+
+sub total_closed_bugs {
+ my $product = shift;
+ my $dbh = Bugzilla->dbh;
+
+ return $dbh->selectrow_array("SELECT COUNT(bug_id)
+ FROM bugs
+ WHERE bug_status IN (" . join(',', quoted_closed_states()) . ")
+ AND product_id = ?", undef, $product->id);
+}
+
+sub bug_link_all {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name);
+}
+
+sub bug_link_open {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) . "&bug_status=__open__";
+}
+
+sub bug_link_closed {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) . "&bug_status=__closed__";
+}
+
+sub by_version {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra = '';
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT version, COUNT(bug_id),
+ ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100))
+ FROM bugs
+ WHERE product_id = ?
+ $extra
+ GROUP BY version
+ ORDER BY COUNT(bug_id) DESC",
+ undef, $product->id, $product->id);
+}
+
+sub by_milestone {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra = '';
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT target_milestone, COUNT(bug_id),
+ ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100))
+ FROM bugs
+ WHERE product_id = ?
+ $extra
+ GROUP BY target_milestone
+ ORDER BY COUNT(bug_id) DESC",
+ undef, $product->id, $product->id);
+}
+
+sub by_priority {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra = '';
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT priority, COUNT(bug_id),
+ ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100))
+ FROM bugs
+ WHERE product_id = ?
+ $extra
+ GROUP BY priority
+ ORDER BY COUNT(bug_id) DESC",
+ undef, $product->id, $product->id);
+}
+
+sub by_severity {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra = '';
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT bug_severity, COUNT(bug_id),
+ ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100))
+ FROM bugs
+ WHERE product_id = ?
+ $extra
+ GROUP BY bug_severity
+ ORDER BY COUNT(bug_id) DESC",
+ undef, $product->id, $product->id);
+}
+
+sub by_component {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra = '';
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT components.name, COUNT(bugs.bug_id),
+ ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100))
+ FROM bugs INNER JOIN components ON bugs.component_id = components.id
+ WHERE bugs.product_id = ?
+ $extra
+ GROUP BY components.name
+ ORDER BY COUNT(bugs.bug_id) DESC",
+ undef, $product->id, $product->id);
+}
+
+sub by_value_summary {
+ my ($product, $type, $value, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $query = "SELECT bugs.bug_id AS id,
+ bugs.bug_status AS status,
+ bugs.version AS version,
+ components.name AS component,
+ bugs.bug_severity AS severity,
+ bugs.short_desc AS summary
+ FROM bugs, components
+ WHERE bugs.product_id = ?
+ AND bugs.component_id = components.id ";
+
+ if ($type eq 'component') {
+ Bugzilla::Component->check({ product => $product, name => $value });
+ $query .= "AND components.name = ? " if $type eq 'component';
+ }
+ elsif ($type eq 'version') {
+ Bugzilla::Version->check({ product => $product, name => $value });
+ $query .= "AND bugs.version = ? " if $type eq 'version';
+ }
+ elsif ($type eq 'target_milestone') {
+ Bugzilla::Milestone->check({ product => $product, name => $value });
+ $query .= "AND bugs.target_milestone = ? " if $type eq 'target_milestone';
+ }
+
+ $query .= "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ") " if $bug_status eq 'open';
+ $query .= "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ") " if $bug_status eq 'closed';
+
+ trick_taint($value);
+
+ my $past_due_bugs = $dbh->selectall_arrayref($query .
+ "AND (bugs.deadline IS NOT NULL AND bugs.deadline != '')
+ AND bugs.deadline < now() ORDER BY bugs.deadline LIMIT 10",
+ {'Slice' => {}}, $product->id, $value);
+
+ my $updated_recently_bugs = $dbh->selectall_arrayref($query .
+ "AND bugs.delta_ts != bugs.creation_ts " .
+ "ORDER BY bugs.delta_ts DESC LIMIT 10",
+ {'Slice' => {}}, $product->id, $value);
+
+ my $timestamp = $dbh->selectrow_array("SELECT " . $dbh->sql_date_format("LOCALTIMESTAMP(0)", "%Y-%m-%d"));
+
+ return {
+ timestamp => $timestamp,
+ past_due => _filter_bugs($past_due_bugs),
+ updated_recently => _filter_bugs($updated_recently_bugs),
+ };
+}
+
+sub by_assignee {
+ my ($product, $bug_status, $limit) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra = '';
+
+ $limit = ($limit && detaint_natural($limit)) ? $dbh->sql_limit($limit) : "";
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ my @result = map { [ Bugzilla::User->new($_->[0]), $_->[1], $_->[2] ] }
+ @{$dbh->selectall_arrayref("SELECT bugs.assigned_to AS userid, COUNT(bugs.bug_id),
+ ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100))
+ FROM bugs, profiles
+ WHERE bugs.product_id = ?
+ AND bugs.assigned_to = profiles.userid
+ $extra
+ GROUP BY profiles.login_name
+ ORDER BY COUNT(bugs.bug_id) DESC $limit",
+ undef, $product->id, $product->id)};
+
+ return \@result;
+}
+
+sub by_status {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra = '';
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT bugs.bug_status, COUNT(bugs.bug_id),
+ ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100))
+ FROM bugs
+ WHERE bugs.product_id = ?
+ $extra
+ GROUP BY bugs.bug_status
+ ORDER BY COUNT(bugs.bug_id) DESC",
+ undef, $product->id, $product->id);
+}
+
+sub total_bug_milestone {
+ my ($product, $milestone) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ return $dbh->selectrow_array("SELECT COUNT(bug_id)
+ FROM bugs
+ WHERE target_milestone = ?
+ AND product_id = ?",
+ undef, $milestone->name, $product->id);
+}
+
+sub bug_milestone_by_status {
+ my ($product, $milestone, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra = '';
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectrow_array("SELECT COUNT(bug_id)
+ FROM bugs
+ WHERE target_milestone = ?
+ AND product_id = ? $extra",
+ undef,
+ $milestone->name,
+ $product->id);
+
+}
+
+sub by_duplicate {
+ my ($product, $bug_status, $limit) = @_;
+ my $dbh = Bugzilla->dbh;
+ $limit = ($limit && detaint_natural($limit)) ? $dbh->sql_limit($limit) : "";
+
+ my $extra = '';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ my $unfiltered_bugs = $dbh->selectall_arrayref("SELECT bugs.bug_id AS id,
+ bugs.bug_status AS status,
+ bugs.version AS version,
+ components.name AS component,
+ bugs.bug_severity AS severity,
+ bugs.short_desc AS summary,
+ COUNT(duplicates.dupe) AS dupe_count
+ FROM bugs, duplicates, components
+ WHERE bugs.product_id = ?
+ AND bugs.component_id = components.id
+ AND bugs.bug_id = duplicates.dupe_of
+ $extra
+ GROUP BY bugs.bug_id, bugs.bug_status, components.name,
+ bugs.bug_severity, bugs.short_desc
+ HAVING COUNT(duplicates.dupe) > 1
+ ORDER BY COUNT(duplicates.dupe) DESC $limit",
+ {'Slice' => {}}, $product->id);
+
+ return _filter_bugs($unfiltered_bugs);
+}
+
+sub by_popularity {
+ my ($product, $bug_status, $limit) = @_;
+ my $dbh = Bugzilla->dbh;
+ $limit = ($limit && detaint_natural($limit)) ? $dbh->sql_limit($limit) : "";
+
+ my $extra = '';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ my $unfiltered_bugs = $dbh->selectall_arrayref("SELECT bugs.bug_id AS id,
+ bugs.bug_status AS status,
+ bugs.version AS version,
+ components.name AS component,
+ bugs.bug_severity AS severity,
+ bugs.short_desc AS summary,
+ bugs.votes AS votes
+ FROM bugs, components
+ WHERE bugs.product_id = ?
+ AND bugs.component_id = components.id
+ AND bugs.votes > 1
+ $extra
+ ORDER BY bugs.votes DESC $limit",
+ {'Slice' => {}}, $product->id);
+
+ return _filter_bugs($unfiltered_bugs);
+}
+
+sub recently_opened {
+ my ($params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $product = $params->{'product'};
+ my $days = $params->{'days'};
+ my $limit = $params->{'limit'};
+ my $date_from = $params->{'date_from'};
+ my $date_to = $params->{'date_to'};
+
+ $days ||= 7;
+ $limit = ($limit && detaint_natural($limit)) ? $dbh->sql_limit($limit) : "";
+
+ my @values = ($product->id);
+
+ my $date_part;
+ if ($date_from && $date_to) {
+ validate_date($date_from)
+ || ThrowUserError('illegal_date', { date => $date_from,
+ format => 'YYYY-MM-DD' });
+ validate_date($date_to)
+ || ThrowUserError('illegal_date', { date => $date_to,
+ format => 'YYYY-MM-DD' });
+ $date_part = "AND bugs.creation_ts >= ? AND bugs.creation_ts <= ?";
+ push(@values, trick_taint($date_from), trick_taint($date_to));
+ }
+ else {
+ $date_part = "AND bugs.creation_ts >= CURRENT_DATE() - INTERVAL ? DAY";
+ push(@values, $days);
+ }
+
+ my $unfiltered_bugs = $dbh->selectall_arrayref("SELECT bugs.bug_id AS id,
+ bugs.bug_status AS status,
+ bugs.version AS version,
+ components.name AS component,
+ bugs.bug_severity AS severity,
+ bugs.short_desc AS summary
+ FROM bugs, components
+ WHERE bugs.product_id = ?
+ AND bugs.component_id = components.id
+ AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")
+ $date_part
+ ORDER BY bugs.bug_id DESC $limit",
+ {'Slice' => {}}, @values);
+
+ return _filter_bugs($unfiltered_bugs);
+}
+
+sub recently_closed {
+ my ($params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $product = $params->{'product'};
+ my $days = $params->{'days'};
+ my $limit = $params->{'limit'};
+ my $date_from = $params->{'date_from'};
+ my $date_to = $params->{'date_to'};
+
+ $days ||= 7;
+ $limit = ($limit && detaint_natural($limit)) ? $dbh->sql_limit($limit) : "";
+
+ my @values = ($product->id);
+
+ my $date_part;
+ if ($date_from && $date_to) {
+ validate_date($date_from)
+ || ThrowUserError('illegal_date', { date => $date_from,
+ format => 'YYYY-MM-DD' });
+ validate_date($date_to)
+ || ThrowUserError('illegal_date', { date => $date_to,
+ format => 'YYYY-MM-DD' });
+ $date_part = "AND bugs_activity.bug_when >= ? AND bugs_activity.bug_when <= ?";
+ push(@values, trick_taint($date_from), trick_taint($date_to));
+ }
+ else {
+ $date_part = "AND bugs_activity.bug_when >= CURRENT_DATE() - INTERVAL ? DAY";
+ push(@values, $days);
+ }
+
+ my $unfiltered_bugs = $dbh->selectall_arrayref("SELECT DISTINCT bugs.bug_id AS id,
+ bugs.bug_status AS status,
+ bugs.version AS version,
+ components.name AS component,
+ bugs.bug_severity AS severity,
+ bugs.short_desc AS summary
+ FROM bugs, components, bugs_activity
+ WHERE bugs.product_id = ?
+ AND bugs.component_id = components.id
+ AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")
+ AND bugs.bug_id = bugs_activity.bug_id
+ AND bugs_activity.added IN (" . join(',', quoted_closed_states()) . ")
+ $date_part
+ ORDER BY bugs.bug_id DESC $limit",
+ {'Slice' => {}}, @values);
+
+ return _filter_bugs($unfiltered_bugs);
+}
+
+sub _filter_bugs {
+ my ($unfiltered_bugs) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ return [] if !$unfiltered_bugs;
+
+ my @unfiltered_bug_ids = map { $_->{'id'} } @$unfiltered_bugs;
+ my %filtered_bug_ids = map { $_ => 1 } @{ Bugzilla->user->visible_bugs(\@unfiltered_bug_ids) };
+
+ my @filtered_bugs;
+ foreach my $bug (@$unfiltered_bugs) {
+ next if !$filtered_bug_ids{$bug->{'id'}};
+ push(@filtered_bugs, $bug);
+ }
+
+ return \@filtered_bugs;
+}
+
+1;
diff --git a/extensions/ProductDashboard/lib/Util.pm b/extensions/ProductDashboard/lib/Util.pm
new file mode 100644
index 000000000..5d9c161ef
--- /dev/null
+++ b/extensions/ProductDashboard/lib/Util.pm
@@ -0,0 +1,95 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Extension::ProductDashboard::Util;
+
+use strict;
+
+use base qw(Exporter);
+@Bugzilla::Extension::ProductDashboard::Util::EXPORT = qw(
+ bug_link_all
+ bug_link_open
+ bug_link_closed
+ open_states
+ closed_states
+ quoted_open_states
+ quoted_closed_states
+ bug_milestone_link_total
+ bug_milestone_link_open
+ bug_milestone_link_closed
+);
+
+use Bugzilla::Status;
+use Bugzilla::Util;
+
+our $_open_states;
+sub open_states {
+ $_open_states ||= Bugzilla::Status->match({ is_open => 1, isactive => 1 });
+ return wantarray ? @$_open_states : $_open_states;
+}
+
+our $_quoted_open_states;
+sub quoted_open_states {
+ my $dbh = Bugzilla->dbh;
+ $_quoted_open_states ||= [ map { $dbh->quote($_->name) } open_states() ];
+ return wantarray ? @$_quoted_open_states : $_quoted_open_states;
+}
+
+our $_closed_states;
+sub closed_states {
+ $_closed_states ||= Bugzilla::Status->match({ is_open => 0, isactive => 1 });
+ return wantarray ? @$_closed_states : $_closed_states;
+}
+
+our $_quoted_closed_states;
+sub quoted_closed_states {
+ my $dbh = Bugzilla->dbh;
+ $_quoted_closed_states ||= [ map { $dbh->quote($_->name) } closed_states() ];
+ return wantarray ? @$_quoted_closed_states : $_quoted_closed_states;
+}
+
+sub bug_link_all {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name);
+}
+
+sub bug_link_open {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) .
+ "&bug_status=__open__";
+}
+
+sub bug_link_closed {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) .
+ "&bug_status=__closed__";
+}
+
+sub bug_milestone_link_total {
+ my ($product, $milestone) = @_;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) .
+ "&target_milestone=" . url_quote($milestone->name);
+}
+
+sub bug_milestone_link_open {
+ my ($product, $milestone) = @_;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) .
+ "&target_milestone=" . url_quote($milestone->name) . "&bug_status=__open__";
+}
+
+sub bug_milestone_link_closed {
+ my ($product, $milestone) = @_;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) .
+ "&target_milestone=" . url_quote($milestone->name) . "&bug_status=__closed__";
+}
+
+1;
diff --git a/extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl b/extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl
new file mode 100644
index 000000000..e9be8a13d
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+ <li><span class="separator"> | </span><a href="page.cgi?id=productdashboard.html">Product Dashboard</a></li>
diff --git a/extensions/ProductDashboard/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/ProductDashboard/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..d8af64d31
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "product_dashboard_invalid_recent_days" %]
+ [% title = "Invalid Recent Days" %]
+ Invalid value for recent days.
+[% END %]
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl
new file mode 100644
index 000000000..ac588ac26
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl
@@ -0,0 +1,237 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% javascript_urls = [ "js/yui3/yui/yui-min.js",
+ "js/util.js",
+ "js/field.js" ]
+%]
+
+[% IF current_tab_name == 'summary' %]
+ [% javascript_urls.push("extensions/ProductDashboard/web/js/summary.js") %]
+ [% ELSIF current_tab_name == 'recents' %]
+ [% yui = [ "calendar" ] %]
+ [% javascript_urls.push("js/field.js") %]
+ [% javascript_urls.push("js/util.js") %]
+ [% javascript_urls.push("extensions/ProductDashboard/web/js/recents.js") %]
+[% ELSIF current_tab_name == 'components' %]
+ [% javascript_urls.push("extensions/ProductDashboard/web/js/components.js") %]
+[% ELSIF current_tab_name == 'duplicates' %]
+ [% javascript_urls.push("extensions/ProductDashboard/web/js/duplicates.js") %]
+[% ELSIF current_tab_name == 'popularity' %]
+ [% javascript_urls.push("extensions/ProductDashboard/web/js/popularity.js") %]
+[% ELSIF current_tab_name == 'roadmap' && Param('usetargetmilestone') %]
+ [% javascript_urls.push("extensions/ProductDashboard/web/js/roadmap.js") %]
+[% END %]
+
+[% filtered_product = product.name FILTER html %]
+[% PROCESS global/header.html.tmpl
+ title = "Product Dashboard: $filtered_product"
+ style_urls = [ "skins/standard/buglist.css",
+ "js/yui/assets/skins/sam/paginator.css",
+ "extensions/ProductDashboard/web/styles/productdashboard.css" ]
+%]
+
+<script type="text/javascript">
+<!--
+ PD = {};
+ [%# Set up severities list for proper sorting %]
+ PD.severities = new Array();
+ [% sort_count = 0 %]
+ [% FOREACH s = severities %]
+ PD.severities['[% s FILTER js %]'] = [% sort_count FILTER js %];
+ [% sort_count = sort_count + 1 %]
+ [% END %]
+-->
+</script>
+
+[% url_filtered_product = product.name FILTER uri %]
+[% url_filtered_status = bug_status FILTER uri %]
+
+[% tabs = [
+ {
+ name => "summary",
+ label => "Summary",
+ link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=summary"
+ },
+ {
+ name => "recents",
+ label => "Recents",
+ link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=recents"
+ },
+ {
+ name => "components",
+ label => "Components/Versions",
+ link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=components"
+ },
+ {
+ name => "duplicates",
+ label => "Duplicates",
+ link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=duplicates"
+ },
+ {
+ name => "roadmap",
+ label => "Road Map",
+ link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=roadmap"
+ },
+ ]
+%]
+
+[% IF product.votesperuser %]
+ [%
+ tabs.push({
+ name => "popularity",
+ label => "Popularity",
+ link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=popularity"
+ })
+ %]
+[% END %]
+
+[% FOREACH tab IN tabs %]
+ [% IF tab.name == current_tab_name %]
+ [% current_tab = tab %]
+ [% LAST %]
+ [% END %]
+[% END %]
+
+[% full_bug_count = 0 %]
+[% IF bug_status == 'open' %]
+ [% full_bug_count = total_open_bugs %]
+[% ELSIF bug_status == 'closed' %]
+ [% full_bug_count = total_closed_bugs %]
+[% ELSE %]
+ [% full_bug_count = total_bugs %]
+[% END %]
+
+[% bug_link = bug_link_all %]
+[% IF bug_status == 'open' %]
+ [% bug_link = bug_link_open %]
+[% ELSIF bug_status == 'closed' %]
+ [% bug_link = bug_link_closed %]
+[% END %]
+
+<div class="yui3-skin-sam">
+ <a name="top"></a>
+
+ <form action="page.cgi" method="get">
+ <input type="hidden" name="id" value="productdashboard.html">
+ <input type="hidden" name="tab" value="[% current_tab.name FILTER html %]">
+
+ [% IF summary.keys %]
+ <input type="hidden" name="[% summary.type FILTER html %]" value="[% summary.value FILTER html %]">
+ [% END %]
+
+ [% IF product %]
+ <span id="product_dashboard_links">
+ <ul>
+ <li><a href="[% urlbase FILTER none %]enter_bug.cgi?product=[% product.name FILTER uri %]">
+ Create a new [% terms.bug %] in this product</a></li>
+ <li><a href="[% urlbase FILTER none %]describecomponents.cgi?product=[% product.name FILTER uri %]">
+ Show full component descriptions for this product</a></li>
+ </ul>
+ </span>
+ [% END %]
+
+ <strong>Choose product:</strong>
+ <select name="product">
+ [% FOREACH c = classifications %]
+ <optgroup label="[% c.name FILTER html %]">
+ [% FOREACH p = c.products %]
+ <option value="[% p.name FILTER html %]"
+ [% IF p.name == product.name %]selected="selected"[% END %]>
+ [% p.name FILTER html %]</option>
+ [% END %]</optgroup>
+ [% END %]
+ </select>
+ <select name="bug_status" id="bug_status">
+ [% statuses = [ { name = 'open', label = "Open $terms.Bugs" },
+ { name = 'closed', label = "Closed $terms.Bugs" },
+ { name = 'all', label = "All $terms.Bugs" } ] %]
+ [% FOREACH status = statuses %]
+ <option value="[% status.name FILTER html %]"
+ [% " selected" IF bug_status == "${status.name}" %]>
+ [% status.label FILTER html %]
+ </option>
+ [% END %]
+ </select>
+
+ <input type="submit" value="[% IF product %]Change[% ELSE %]Submit[% END %]">
+
+ [% IF product %]
+ <div class="product_name">
+ [% product.name FILTER html %]
+ </div>
+
+ <div class="product_description">
+ [% product.description FILTER none %]
+ </div>
+
+ [% WRAPPER global/tabs.html.tmpl
+ tabs = tabs
+ current_tab = current_tab
+ %]
+
+ [% IF current_tab.name == 'summary' %]
+ [% PROCESS pages/productdashboard/summary.html.tmpl %]
+ [% END %]
+
+ [% IF current_tab.name == 'recents' %]
+ [% PROCESS pages/productdashboard/recents.html.tmpl %]
+ [% END %]
+
+ [% IF current_tab.name == 'components' %]
+ [% PROCESS pages/productdashboard/components.html.tmpl %]
+ [% END %]
+
+ [% IF current_tab.name == 'duplicates' %]
+ [% PROCESS pages/productdashboard/duplicates.html.tmpl %]
+ [% END %]
+
+ [% IF current_tab.name == 'popularity' %]
+ [% PROCESS pages/productdashboard/popularity.html.tmpl %]
+ [% END %]
+
+ [% IF current_tab.name == 'roadmap' && Param('usetargetmilestone') %]
+ [% PROCESS pages/productdashboard/roadmap.html.tmpl %]
+ [% END %]
+
+ [% END %][%# END WRAPPER %]
+ [% END %]
+
+ </form>
+</div>
+
+[% PROCESS global/footer.html.tmpl %]
+
+[% BLOCK bar_graph %]
+ [% IF full_bug_count > 0 %][%# No divide by zero %]
+ [% percentage_bugs = (count / full_bug_count) * 100 FILTER format('%02.2f') %]
+ [% ELSE %]
+ [% percentage_bugs = 0 %]
+ [% END %]
+ <div class="bar_graph">
+ <table cellpadding="0" cellspacing="0" width="300px">
+ <tr>
+ <td width="[% percentage_bugs FILTER html %]%">
+ <table cellpadding="0" cellspacing="0" width="100%">
+ <tr>
+ <td bgcolor="#3c78b5">
+ <a title="[% percentage_bugs FILTER html %]%">
+ <img src="extensions/ProductDashboard/web/images/spacer.gif" height=10 width="100%" title="[% percentage_bugs FILTER html %]%">
+ </a>
+ </td>
+ </tr>
+ </table>
+ </td>
+ <td width="[% 100 - percentage_bugs FILTER html %]%">&nbsp;&nbsp;&nbsp;[% percentage_bugs FILTER html %]%</td>
+ </tr>
+ </table>
+ </div>
+[% END %]
+
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl
new file mode 100644
index 000000000..6b0e7240a
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl
@@ -0,0 +1,146 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF summary.keys %]
+
+<h3>Summary for [% summary.type FILTER html %]: [% summary.value FILTER html %]</h3>
+
+<script>
+<!--
+ // Past due
+ [% IF user.is_timetracker %]
+ PD.past_due = [
+ [% FOREACH bug = summary.past_due %]
+ {
+ id: '[% bug.id FILTER js %]',
+ bug_status: '[% bug.status FILTER js %]',
+ version: '[% bug.version FILTER js %]',
+ component: '[% bug.component FILTER js %]',
+ severity: '[% bug.severity FILTER js %]',
+ summary: '[% bug.summary FILTER js %]'
+ },
+ [% END %]
+ ];
+ [% END %]
+
+ // Updated recently
+ PD.updated_recently = [
+ [% FOREACH bug = summary.updated_recently %]
+ {
+ id: '[% bug.id FILTER js %]',
+ bug_status: '[% bug.status FILTER js %]',
+ version: '[% bug.version FILTER js %]',
+ component: '[% bug.component FILTER js %]',
+ severity: '[% bug.severity FILTER js %]',
+ summary: '[% bug.summary FILTER js %]'
+ },
+ [% END %]
+ ];
+-->
+</script>
+
+[% IF user.is_timetracker %]
+ <p>
+ <a href="#past_due">Past Due</a> |
+ <a href="#updated_recently">Updated Recently</a>
+ </p>
+[% END %]
+
+<div class="yui3-skin-sam">
+
+ [% IF user.is_timetracker %]
+ <a name="past_due"></a>
+ <b>[% summary.past_due.size FILTER html %] Past Due [% terms.Bugs %]</b> (deadline is before today's date)
+ (<a href="[% bug_link FILTER html %]&amp;[% summary.type FILTER uri %]=[% summary.value FILTER uri %]&field0-0-0=deadline&type0-0-0=lessthan&value0-0-0=[% summary.timestamp FILTER uri %]&order=deadline">full list</a>)
+ <div id="past_due"></div>
+ <br>
+ [% END %]
+
+ <a name="updated_recently"></a>
+ <b>[% summary.updated_recently.size FILTER html %] Most Recently Updated [% terms.Bugs %]</b>
+ [% IF user.is_timetracker %](<a href="#top">back to top</a>)[% END %]
+ (<a href="[% bug_link FILTER html %]&amp;[% summary.type FILTER uri %]=[% summary.value FILTER uri %]&order=changeddate DESC">full list</a>)
+ <div id="updated_recently"></div>
+</div>
+
+[% ELSE %]
+
+<script type="text/javascript">
+<!--
+ PD.product_name = '[% product.name FILTER js %]';
+ PD.bug_status = '[% bug_status FILTER js %]';
+
+ // Component counts
+ PD.component_counts = [
+ [% FOREACH col = by_component %]
+ {
+ name: "[% col.0 FILTER js %]",
+ count: [% col.1 || 0 FILTER js %],
+ percentage: [% col.2 || 0 FILTER js %],
+ link: '<a href="[% bug_link FILTER html %]&amp;component=[% col.0 FILTER uri %]">Link</a>'
+ },
+ [% END %]
+ ];
+
+ // Version counts
+ PD.version_counts = [
+ [% FOREACH col = by_version %]
+ {
+ name: "[% col.0 FILTER js %]",
+ count: [% col.1 || 0 FILTER js %],
+ percentage: [% col.2 || 0 FILTER js %],
+ link: '<a href="[% bug_link FILTER html %]&amp;version=[% col.0 FILTER uri %]">Link</a>'
+ },
+ [% END %]
+ ];
+
+ [% IF Param('usetargetmilestone') %]
+ // Milestone counts
+ PD.milestone_counts = [
+ [% FOREACH col = by_milestone %]
+ {
+ name: "[% col.0 FILTER js %]",
+ count: [% col.1 || 0 FILTER js %],
+ percentage: [% col.2 || 0 FILTER js %],
+ link: '<a href="[% bug_link FILTER html %]&amp;target_milestone=[% col.0 FILTER uri %]">Link</a>'
+ },
+ [% END %]
+ ];
+ [% END %]
+-->
+</script>
+
+<h3>[% terms.Bug %] counts per component, version and milestone.</h3>
+
+<p>
+ <a href="#component">Component</a> |
+ <a href="#version">Version</a> |
+ <a href="#milestone">Milestone</a>
+</p>
+
+<p>Click on a value to show a list of most recently updated [% terms.bugs %].</p>
+
+<div class="yui3-skin-sam">
+ <a name="component"></a>
+ <b>Component</b>
+ <div id="component_counts"></div>
+ <br>
+ <a name="version"></a>
+ <b>Version</b>
+ (<a href="#top">back to top</a>)
+ <div id="version_counts"></div>
+ [% IF Param('usetargetmilestone') %]
+ <br>
+ <a name="milestone"></a>
+ <b>Milestone</b>
+ (<a href="#top">back to top</a>)
+ <div id="milestone_counts"></div>
+ [% END %]
+</div>
+
+[% END %]
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl
new file mode 100644
index 000000000..585cdc829
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl
@@ -0,0 +1,34 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<script type="text/javascript">
+ PD.duplicates = [
+ [% FOREACH bug = by_duplicate %]
+ {
+ id: '[% bug.id FILTER js %]',
+ count: '[% bug.dupe_count FILTER js %]',
+ status: '[% bug.status FILTER js %]',
+ version: '[% bug.version FILTER js %]',
+ component: '[% bug.component FILTER js %]',
+ severity: '[% bug.severity FILTER js %]',
+ summary: '[% bug.summary FILTER js %]'
+ },
+ [% END %]
+ ];
+</script>
+
+<h3>Most duplicated [% terms.bugs %]</h3>
+
+[% IF by_duplicate.size %]
+ <b>[% by_duplicate.size FILTER html %]&nbsp;[% terms.Bugs %] Found</b>
+ <div class="yui3-skin-sam">
+ <div id="duplicates"></div>
+ </div>
+[% ELSE %]
+ <b>No duplicate [% terms.bugs %] found.</b>
+[% END %]
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl
new file mode 100644
index 000000000..933f26c81
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl
@@ -0,0 +1,38 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<style>
+ .yui-skin-sam .yui-dt table {width:100%;}
+</style>
+
+<script type="text/javascript">
+ PD.popularity = [
+ [% FOREACH bug = by_popularity %]
+ {
+ id: '[% bug.id FILTER js %]',
+ count: '[% bug.votes FILTER js %]',
+ status: '[% bug.status FILTER js %]',
+ version: '[% bug.version FILTER js %]',
+ component: '[% bug.component FILTER js %]',
+ severity: '[% bug.severity FILTER js %]',
+ summary: '[% bug.summary FILTER js %]'
+ },
+ [% END %]
+ ];
+</script>
+
+<h3>Most voted on [% terms.bugs %]</h3>
+
+[% IF by_popularity.size %]
+ <b>[% by_popularity.size FILTER html %]&nbsp;[% terms.Bugs %] Found</b>
+ <div class="yui3-skin-sam">
+ <div id="popularity"></div>
+ </div>
+[% ELSE %]
+ <b>No [% terms.bugs %] found.</b>
+[% END %]
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl
new file mode 100644
index 000000000..66320e174
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl
@@ -0,0 +1,87 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<script type="text/javascript">
+ PD.recents = {};
+
+ // Recently opened
+ PD.recents.opened = [
+ [% FOREACH bug = recently_opened %]
+ {
+ id: '[% bug.id FILTER js %]',
+ status: '[% bug.status FILTER js %]',
+ version: '[% bug.version FILTER js %]',
+ component: '[% bug.component FILTER js %]',
+ severity: '[% bug.severity FILTER js %]',
+ summary: '[% bug.summary FILTER js %]'
+ },
+ [% END %]
+ ];
+
+ // Recently closed
+ PD.recents.closed = [
+ [% FOREACH bug = recently_closed %]
+ {
+ id: '[% bug.id FILTER js %]',
+ status: '[% bug.status FILTER js %]',
+ version: '[% bug.version FILTER js %]',
+ component: '[% bug.component FILTER js %]',
+ severity: '[% bug.severity FILTER js %]',
+ summary: '[% bug.summary FILTER js %]'
+ },
+ [% END %]
+ ];
+</script>
+
+<h3>Most recently opened and closed [% terms.bugs %]</h3>
+
+<p>
+ Activity within the last <input type="text" size="4" name="recent_days"
+ value="[% recent_days FILTER html %]">
+ days (between 1 and 100) or from
+ <input name="date_from" size="10" id="date_from"
+ value="[% date_from FILTER html %]"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_date_from"
+ onclick="showCalendar('date_from')">
+ <span>Calendar</span>
+ </button>
+ <span id="con_calendar_date_from"></span>
+ to
+ <input name="date_to" size="10" id="date_to"
+ value="[% date_to FILTER html %]"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_date_to"
+ onclick="showCalendar('date_to')">
+ <span>Calendar</span>
+ </button>
+ <span id="con_calendar_date_to"></span>
+ <script type="text/javascript">
+ createCalendar('date_from')
+ createCalendar('date_to')
+ </script>
+ <input type="submit" name="change" value="Change">
+</p>
+<p>
+ <a href="#recently_opened">Recently Opened</a>
+ <span class="separator"> | </span>
+ <a href="#recently_closed">Recently Closed</a>
+</p>
+
+<div class="yui-skin-sam">
+ <a name="recently_opened"></a>
+ <b>[% recently_opened.size FILTER html %] Recently Opened [% terms.Bugs %]</b>
+ <div id="recently_opened"></div>
+ <br>
+ <a name="recently_closed"></a>
+ <b>[% recently_closed.size FILTER html %] Recently Closed [% terms.Bugs %]</b>
+ (<a href="#top">back to top</a>)
+ <div id="recently_closed"></div>
+</div>
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl
new file mode 100644
index 000000000..b31827fbd
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl
@@ -0,0 +1,27 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<script type="text/javascript">
+<!--
+ PD.roadmap = [
+ [% FOREACH milestone = by_roadmap %]
+ {
+ name: '[% milestone.name FILTER js %]',
+ percentage: '[% milestone.percentage FILTER js %]',
+ link: '<a href="[% milestone.link_closed FILTER html %]">[% milestone.closed_bugs FILTER html %]</a> of <a href="[% milestone.link_total FILTER html %]"> [% milestone.total_bugs FILTER html %]</a> [% terms.bugs %] have been closed',
+ },
+ [% END %]
+ ];
+-->
+</script>
+
+<h3>Percentage of [% terms.bug %] closure per milestone</h3>
+
+<div class="yui3-skin-sam">
+ <div id="bug_milestones"></div>
+</div>
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl
new file mode 100644
index 000000000..30b6f3dca
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl
@@ -0,0 +1,122 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<script>
+ PD.summary = {};
+
+ // global counts
+ PD.summary.bug_counts = [
+ {
+ name: "Total [% terms.Bugs %]",
+ count: [% total_bugs || 0 FILTER js %],
+ percentage: [% total_bugs ? "100" : "0" %],
+ link: '<a href="[% bug_link_all FILTER js %]">Link</a>',
+ },
+ {
+ name: "Open [% terms.Bugs %]",
+ count: [% total_open_bugs || 0 FILTER js %],
+ percentage: [% open_bugs_percentage FILTER js %],
+ link: '<a href="[% bug_link_open FILTER js %]">Link</a>',
+ },
+ {
+ name: "Closed [% terms.Bugs %]",
+ count: [% total_closed_bugs || 0 FILTER js %],
+ percentage: [% closed_bugs_percentage FILTER js %],
+ link: '<a href="[% bug_link_closed FILTER js %]">Link</a>',
+ }
+ ];
+
+ // Status counts
+ PD.summary.status_counts = [
+ [% FOREACH col = by_status %]
+ [% NEXT IF col.0 == 'CLOSED' %]
+ {
+ name: "[% col.0 FILTER js %]",
+ count: [% col.1 || 0 FILTER js %],
+ percentage: [% col.2 || 0 FILTER js %],
+ link: '<a href="[% bug_link_all FILTER js %]&amp;bug_status=[% col.0 FILTER uri FILTER js %]">Link</a>'
+ },
+ [% END %]
+ ];
+
+ // Priority counts
+ PD.summary.priority_counts = [
+ [% FOREACH col = by_priority %]
+ {
+ name: "[% col.0 FILTER js %]",
+ count: [% col.1 || 0 FILTER js %],
+ percentage: [% col.2 || 0 FILTER js %],
+ link: '<a href="[% bug_link FILTER js %]&amp;priority=[% col.0 FILTER uri FILTER js %]">Link</a>'
+ },
+ [% END %]
+ ];
+
+ // Severity counts
+ PD.summary.severity_counts = [
+ [% FOREACH col = by_severity %]
+ {
+ name: "[% col.0 FILTER js %]",
+ count: [% col.1 || 0 FILTER js %],
+ percentage: [% col.2 || 0 FILTER js %],
+ link: '<a href="[% bug_link FILTER js %]&amp;bug_severity=[% col.0 FILTER uri FILTER js %]">Link</a>'
+ },
+ [% END %]
+ ];
+
+ // Assignee counts
+ PD.summary.assignee_counts = [
+ [% FOREACH col = by_assignee %]
+ {
+ name: "[% IF user.id %][% col.0.email FILTER js %][% ELSE %][% col.0.realname || 'No Name' FILTER js %][% END %]",
+ count: [% col.1 || 0 FILTER js %],
+ percentage: [% col.2 || 0 FILTER js %],
+ link: '[% IF user.id %]<a href="[% bug_link FILTER js %]&amp;emailassigned_to1=1&amp;emailtype1=exact&amp;email1=[% col.0.email FILTER uri FILTER js %]">Link</a>[% END %]'
+ },
+ [% END %]
+ ];
+</script>
+
+<h3>Summary of [% terms.bug %] counts</h3>
+
+<p>
+ <a href="#counts">Counts</a>
+ <span class="separator"> | </span>
+ <a href="#status">Status</a>
+ <span class="separator"> | </span>
+ <a href="#priority">Priority</a>
+ <span class="separator"> | </span>
+ <a href="#severity">Severity</a>
+ <span class="separator"> | </span>
+ <a href="#assignee">Assignee</a>
+</p>
+
+<div class="yui3-skin-sam">
+ <a name="counts"></a>
+ <b>[% terms.Bug %] Counts</b>
+ <div id="bug_counts"></div>
+ <br>
+ <a name="status"></a>
+ <b>Status</b>
+ (<a href="#top">back to top</a>)
+ <div id="status_counts"></div>
+ <br>
+ <a name="priority"></a>
+ <b>Priority</b>
+ (<a href="#top">back to top</a>)
+ <div id="priority_counts"></div>
+ <br>
+ <a name="severity"></a>
+ <b>Severity</b>
+ (<a href="#top">back to top</a>)
+ <div id="severity_counts"></div>
+ <br>
+ <a name="assignee"></a>
+ <b>Assignee</b>
+ (<a href="#top">back to top</a>)
+ <div id="assignee_counts"></div>
+</div>
diff --git a/extensions/ProductDashboard/web/images/spacer.gif b/extensions/ProductDashboard/web/images/spacer.gif
new file mode 100644
index 000000000..fc2560981
--- /dev/null
+++ b/extensions/ProductDashboard/web/images/spacer.gif
Binary files differ
diff --git a/extensions/ProductDashboard/web/js/components.js b/extensions/ProductDashboard/web/js/components.js
new file mode 100644
index 000000000..8b0d28587
--- /dev/null
+++ b/extensions/ProductDashboard/web/js/components.js
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YUI({
+ base: 'js/yui3/',
+ combine: false
+}).use("datatable", "datatable-sort", "escape", function(Y) {
+ if (typeof PD.updated_recently != 'undefined') {
+ var columns = [
+ { key:"id", label:"ID", sortable:true, allowHTML: true,
+ formatter: '<a href="show_bug.cgi?id={value}" target="_blank">{value}</a>' },
+ { key:"bug_status", label:"Status", sortable:true },
+ { key:"version", label:"Version", sortable:true },
+ { key:"component", label:"Component", sortable:true },
+ { key:"severity", label:"Severity", sortable:true },
+ { key:"summary", label:"Summary", sortable:false },
+ ];
+
+ var updatedRecentlyDataTable = new Y.DataTable({
+ columns: columns,
+ data: PD.updated_recently
+ });
+ updatedRecentlyDataTable.render("#updated_recently");
+
+ if (typeof PD.past_due != 'undefined') {
+ var pastDueDataTable = new Y.DataTable({
+ columns: columns,
+ data: PD.past_due
+ });
+ pastDueDataTable.render('#past_due');
+ }
+ }
+
+ if (typeof PD.component_counts != 'undefined') {
+ var summary_url = '<a href="page.cgi?id=productdashboard.html&amp;product=' +
+ encodeURIComponent(PD.product_name) + '&bug_status=' +
+ encodeURIComponent(PD.bug_status) + '&tab=components';
+
+ var columns = [
+ { key:"name", label:"Name", sortable:true, allowHTML: true,
+ formatter: function (o) {
+ return summary_url + '&component=' +
+ encodeURIComponent(o.value) + '">' +
+ Y.Escape.html(o.value) + '</a>'
+ }
+ },
+ { key:"count", label:"Count", sortable:true },
+ { key:"percentage", label:"Percentage", sortable:false, allowHTML: true,
+ formatter: '<div class="percentage"><div class="bar" style="width:{value}%"></div><div class="percent">{value}%</div></div>' },
+ { key:"link", label:"Link", sortable:false, allowHTML: true }
+ ];
+
+ var componentsDataTable = new Y.DataTable({
+ columns: columns,
+ data: PD.component_counts
+ });
+ componentsDataTable.render("#component_counts");
+
+ columns[0].formatter = function (o) {
+ return summary_url + '&version=' +
+ encodeURIComponent(o.value) + '">' +
+ Y.Escape.html(o.value) + '</a>';
+ };
+
+ var versionsDataTable = new Y.DataTable({
+ columns: columns,
+ data: PD.version_counts
+ });
+ versionsDataTable.render('#version_counts');
+
+ if (typeof PD.milestone_counts != 'undefined') {
+ columns[0].formatter = function (o) {
+ return summary_url + '&target_milestone=' +
+ encodeURIComponent(o.value) + '">' +
+ Y.Escape.html(o.value) + '</a>';
+ };
+
+ var milestonesDataTable = new Y.DataTable({
+ columns: columns,
+ data: PD.milestone_counts
+ });
+ milestonesDataTable.render('#milestone_counts');
+ }
+ }
+});
diff --git a/extensions/ProductDashboard/web/js/duplicates.js b/extensions/ProductDashboard/web/js/duplicates.js
new file mode 100644
index 000000000..5e3193a65
--- /dev/null
+++ b/extensions/ProductDashboard/web/js/duplicates.js
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YUI({
+ base: 'js/yui3/',
+ combine: false
+}).use("datatable", "datatable-sort", function (Y) {
+ var column_defs = [
+ { key:"id", label:"ID", sortable:true, allowHTML: true,
+ formatter: '<a href="show_bug.cgi?id={value}" target="_blank">{value}</a>' },
+ { key:"count", label:"Count", sortable:true },
+ { key:"status", label:"Status", sortable:true },
+ { key:"version", label:"Version", sortable:true },
+ { key:"component", label:"Component", sortable:true },
+ { key:"severity", label:"Severity", sortable:true },
+ { key:"summary", label:"Summary", sortable:false },
+ ];
+
+ var duplicatesDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.duplicates
+ }).render('#duplicates');
+});
diff --git a/extensions/ProductDashboard/web/js/popularity.js b/extensions/ProductDashboard/web/js/popularity.js
new file mode 100644
index 000000000..b78b67867
--- /dev/null
+++ b/extensions/ProductDashboard/web/js/popularity.js
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YUI({
+ base: 'js/yui3/',
+ combine: false
+}).use("datatable", "datatable-sort", function (Y) {
+ var column_defs = [
+ { key:"id", label:"ID", sortable:true, allowHTML: true,
+ formatter: '<a href="show_bug.cgi?id={value}" target="_blank">{value}</a>' },
+ { key:"count", label:"Count", sortable:true },
+ { key:"status", label:"Status", sortable:true },
+ { key:"version", label:"Version", sortable:true },
+ { key:"component", label:"Component", sortable:true },
+ { key:"severity", label:"Severity", sortable:true },
+ { key:"summary", label:"Summary", sortable:false },
+ ];
+
+ var popularityDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.popularity
+ }).render('#popularity');
+});
diff --git a/extensions/ProductDashboard/web/js/recents.js b/extensions/ProductDashboard/web/js/recents.js
new file mode 100644
index 000000000..84e1758b6
--- /dev/null
+++ b/extensions/ProductDashboard/web/js/recents.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YUI({
+ base: 'js/yui3/',
+ combine: false
+}).use("datatable", "datatable-sort", function (Y) {
+ var column_defs = [
+ { key:"id", label:"ID", sortable:true, allowHTML: true,
+ formatter: '<a href="show_bug.cgi?id={value}" target="_blank">{value}</a>' },
+ { key:"status", label:"Status", sortable:true },
+ { key:"version", label:"Version", sortable:true },
+ { key:"component", label:"Component", sortable:true },
+ { key:"severity", label:"Severity", sortable:true },
+ { key:"summary", label:"Summary", sortable:false },
+ ];
+
+ var recentlyOpenedDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.recents.opened
+ }).render('#recently_opened');
+
+ var recentlyClosedDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.recents.closed
+ }).render('#recently_closed');
+});
diff --git a/extensions/ProductDashboard/web/js/roadmap.js b/extensions/ProductDashboard/web/js/roadmap.js
new file mode 100644
index 000000000..1bef5b091
--- /dev/null
+++ b/extensions/ProductDashboard/web/js/roadmap.js
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YUI({
+ base: 'js/yui3/',
+ combine: false
+}).use("datatable", "datatable-sort", function (Y) {
+ var column_defs = [
+ { key: 'name', label: 'Name', sortable: true },
+ { key: 'percentage', label: 'Percentage', sortable: false, allowHTML: true,
+ formatter: '<div class="percentage"><div class="bar" style="width:{value}%"></div><div class="percent">{value}%</div></div>' },
+ { key: 'link', label: 'Links', allowHTML: true, sortable: false }
+ ];
+
+ var roadmapDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.roadmap,
+ }).render('#bug_milestones');
+});
diff --git a/extensions/ProductDashboard/web/js/summary.js b/extensions/ProductDashboard/web/js/summary.js
new file mode 100644
index 000000000..59d000d7b
--- /dev/null
+++ b/extensions/ProductDashboard/web/js/summary.js
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YUI({
+ base: 'js/yui3/',
+ combine: false
+}).use("datatable", "datatable-sort", function (Y) {
+ var column_defs = [
+ { key: 'name', label: 'Name', sortable: true },
+ { key: 'count', label: 'Count', sortable: true },
+ { key: 'percentage', label: 'Percentage', sortable: true, allowHTML: true,
+ formatter: '<div class="percentage"><div class="bar" style="width:{value}%"></div><div class="percent">{value}%</div></div>' },
+ { key: 'link', label: 'Link', allowHTML: true }
+ ];
+
+ var bugsCountDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.summary.bug_counts
+ }).render('#bug_counts');
+
+ var statusCountsDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.summary.status_counts
+ }).render('#status_counts');
+
+ var priorityCountsDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.summary.priority_counts
+ }).render('#priority_counts');
+
+ var severityCountsDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.summary.severity_counts
+ }).render('#severity_counts');
+
+ var assigneeCountsDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.summary.assignee_counts
+ }).render('#assignee_counts');
+});
diff --git a/extensions/ProductDashboard/web/styles/productdashboard.css b/extensions/ProductDashboard/web/styles/productdashboard.css
new file mode 100644
index 000000000..c0c45cf38
--- /dev/null
+++ b/extensions/ProductDashboard/web/styles/productdashboard.css
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#product_dashboard_links {
+ float: right;
+ padding-right: 25px;
+ border: 1px solid rgb(116, 126, 147);
+}
+
+.product_name {
+ font-size: 2em;
+ margin: 10px 0 10px 0;
+ color: rgb(109, 117, 129);
+}
+
+.product_description {
+ font-size: 90%;
+ font-style: italic;
+ padding-bottom: 5px;
+ margin-bottom: 10px;
+}
+
+.percentage {
+ position:relative;
+ width: 200px;
+ border: 1px solid rgb(203, 203, 203);
+ position: relative;
+ padding: 3px;
+}
+
+.bar{
+ background-color: #00ff00;
+ height: 20px;
+}
+
+.percent{
+ position: absolute;
+ display: inline-block;
+ top: 3px;
+ left: 48%;
+}
diff --git a/extensions/Profanivore/Config.pm b/extensions/Profanivore/Config.pm
new file mode 100644
index 000000000..354325c58
--- /dev/null
+++ b/extensions/Profanivore/Config.pm
@@ -0,0 +1,40 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Profanivore Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Gervase Markham <gerv@gerv.net>
+
+package Bugzilla::Extension::Profanivore;
+use strict;
+
+use constant NAME => 'Profanivore';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'Regexp-Common',
+ module => 'Regexp::Common',
+ version => 0
+ },
+ {
+ package => 'HTML-Tree',
+ module => 'HTML::Tree',
+ version => 0,
+ }
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/Profanivore/Extension.pm b/extensions/Profanivore/Extension.pm
new file mode 100644
index 000000000..cdec6e1c6
--- /dev/null
+++ b/extensions/Profanivore/Extension.pm
@@ -0,0 +1,169 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Profanivore Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Gervase Markham <gerv@gerv.net>
+
+package Bugzilla::Extension::Profanivore;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Regexp::Common 'RE_ALL';
+
+use Bugzilla::Util qw(is_7bit_clean);
+
+our $VERSION = '0.01';
+
+sub bug_format_comment {
+ my ($self, $args) = @_;
+ my $regexes = $args->{'regexes'};
+ my $comment = $args->{'comment'};
+
+ # Censor profanities if the comment author is not reasonably trusted.
+ # However, allow people to see their own profanities, which might stop
+ # them immediately noticing and trying to go around the filter. (I.e.
+ # it tries to stop an arms race starting.)
+ if ($comment &&
+ !$comment->author->in_group('editbugs') &&
+ $comment->author->id != Bugzilla->user->id)
+ {
+ push (@$regexes, {
+ match => RE_profanity('-i'),
+ replace => \&_replace_profanity
+ });
+ }
+}
+
+sub _replace_profanity {
+ # We don't have access to the actual profanity.
+ return "****";
+}
+
+sub mailer_before_send {
+ my ($self, $args) = @_;
+ my $email = $args->{'email'};
+
+ my $author = $email->header("X-Bugzilla-Who");
+ my $recipient = $email->header("To");
+
+ if ($author && $recipient && lc($author) ne lc($recipient)) {
+ my $email_suffix = Bugzilla->params->{'emailsuffix'};
+ if ($email_suffix ne '') {
+ $recipient =~ s/\Q$email_suffix\E$//;
+ $author =~ s/\Q$email_suffix\E$//;
+ }
+
+ $author = new Bugzilla::User({ name => $author });
+
+ if ($author &&
+ $author->id &&
+ !$author->in_group('editbugs'))
+ {
+ # Multipart emails
+ if (scalar $email->parts > 1) {
+ $email->walk_parts(sub {
+ my ($part) = @_;
+ return if $part->parts > 1; # Top-level
+ # do not filter attachments such as patches, etc.
+ if ($part->header('Content-Disposition')
+ && $part->header('Content-Disposition') =~ /attachment/)
+ {
+ return;
+ }
+ _fix_encoding($part);
+ my $body = $part->body_str;
+ my $new_body;
+ if ($part->content_type =~ /^text\/html/) {
+ $new_body = _filter_html($body);
+ if ($new_body ne $body) {
+ # HTML::Tree removes unnecessary whitespace,
+ # resulting in very long lines. We need to use
+ # quoted-printable encoding to avoid exceeding
+ # email's maximum line length.
+ $part->encoding_set('quoted-printable');
+ }
+ }
+ elsif ($part->content_type =~ /^text\/plain/) {
+ $new_body = _filter_text($body);
+ }
+ if ($new_body && $new_body ne $body) {
+ $part->body_str_set($new_body);
+ }
+ });
+ }
+ # Single part email
+ else {
+ _fix_encoding($email);
+ $email->body_str_set(_filter_text($email->body_str));
+ }
+ }
+ }
+}
+
+sub _fix_encoding {
+ my $part = shift;
+ my $body = $part->body;
+ if (Bugzilla->params->{'utf8'}) {
+ $part->charset_set('UTF-8');
+ # encoding_set works only with bytes, not with utf8 strings.
+ my $raw = $part->body_raw;
+ if (utf8::is_utf8($raw)) {
+ utf8::encode($raw);
+ $part->body_set($raw);
+ }
+ }
+ $part->encoding_set('quoted-printable') if !is_7bit_clean($body);
+}
+
+sub _filter_text {
+ my $text = shift;
+ my $offensive = RE_profanity('-i');
+ $text =~ s/$offensive/****/g;
+ return $text;
+}
+
+sub _filter_html {
+ my $html = shift;
+ my $tree = HTML::Tree->new->parse_content($html);
+ my $comments_div = $tree->look_down( _tag => 'div', id => 'comments' );
+ return $html if !$comments_div;
+ my @comments = $comments_div->look_down( _tag => 'pre' );
+ my $dirty = 0;
+ foreach my $comment (@comments) {
+ _filter_html_node($comment, \$dirty);
+ }
+ return $dirty ? $tree->as_HTML : $html;
+}
+
+sub _filter_html_node {
+ my ($node, $dirty) = @_;
+ my $content = [ $node->content_list ];
+ foreach my $item_r ($node->content_refs_list) {
+ if (ref $$item_r) {
+ _filter_html_node($$item_r);
+ } else {
+ my $new_text = _filter_text($$item_r);
+ if ($new_text ne $$item_r) {
+ $$item_r = $new_text;
+ $$dirty = 1;
+ }
+ }
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/Profanivore/README b/extensions/Profanivore/README
new file mode 100644
index 000000000..5ccab103f
--- /dev/null
+++ b/extensions/Profanivore/README
@@ -0,0 +1,14 @@
+Profanivore 'eats' English profanities in comments, leaving behind instead a
+trail of droppings ('****'). It finds its food using a standard library Perl
+regexp. The profanity is only eaten where the comment was written by a user
+who does not have the global 'editbugs' privilege. The digestion happens at
+display time, so the comment in the database is unaltered.
+
+However, it does not eat profanities when showing people their own comments;
+the aim here is to prevent people immediately noticing they are being
+censored, and getting 'creative'.
+
+The purpose of Profanivore is to make it a little harder for trolls to
+vandalise public Bugzilla installations.
+
+It does not currently affect fields other than comments.
diff --git a/extensions/Push/Config.pm b/extensions/Push/Config.pm
new file mode 100644
index 000000000..11e3502fd
--- /dev/null
+++ b/extensions/Push/Config.pm
@@ -0,0 +1,56 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push;
+
+use strict;
+
+use constant NAME => 'Push';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'Daemon-Generic',
+ module => 'Daemon::Generic',
+ version => '0'
+ },
+ {
+ package => 'JSON-XS',
+ module => 'JSON::XS',
+ version => '2.0'
+ },
+ {
+ package => 'Crypt-CBC',
+ module => 'Crypt::CBC',
+ version => '0'
+ },
+ {
+ package => 'Crypt-DES',
+ module => 'Crypt::DES',
+ version => '0'
+ },
+ {
+ package => 'Crypt-DES_EDE3',
+ module => 'Crypt::DES_EDE3',
+ version => '0'
+ },
+];
+
+use constant OPTIONAL_MODULES => [
+ # connectors need the ability to extend this
+ {
+ package => 'Net-SFTP',
+ module => 'Net::SFTP',
+ version => '0'
+ },
+ {
+ package => 'XML-Simple',
+ module => 'XML::Simple',
+ version => '0'
+ },
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/Push/Extension.pm b/extensions/Push/Extension.pm
new file mode 100644
index 000000000..1d6ec5b62
--- /dev/null
+++ b/extensions/Push/Extension.pm
@@ -0,0 +1,658 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Constants;
+use Bugzilla::Comment;
+use Bugzilla::Error;
+use Bugzilla::Extension::Push::Admin;
+use Bugzilla::Extension::Push::Connectors;
+use Bugzilla::Extension::Push::Logger;
+use Bugzilla::Extension::Push::Message;
+use Bugzilla::Extension::Push::Push;
+use Bugzilla::Extension::Push::Serialise;
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::Install::Filesystem;
+
+use Encode;
+use Scalar::Util 'blessed';
+use Storable 'dclone';
+
+our $VERSION = '1';
+
+$Carp::CarpInternal{'CGI::Carp'} = 1;
+
+#
+# monkey patch for convience
+#
+
+BEGIN {
+ *Bugzilla::push_ext = \&_get_instance;
+}
+
+sub _get_instance {
+ my $cache = Bugzilla->request_cache;
+ if (!$cache->{'push.instance'}) {
+ my $instance = Bugzilla::Extension::Push::Push->new();
+ $cache->{'push.instance'} = $instance;
+ $instance->logger(Bugzilla::Extension::Push::Logger->new());
+ $instance->connectors(Bugzilla::Extension::Push::Connectors->new());
+ }
+ return $cache->{'push.instance'};
+}
+
+#
+# enabled
+#
+
+sub _enabled {
+ my ($self) = @_;
+ if (!exists $self->{'enabled'}) {
+ my $push = Bugzilla->push_ext;
+ $self->{'enabled'} = $push->config->{enabled} eq 'Enabled';
+ if ($self->{'enabled'}) {
+ # if no connectors are enabled, no need to push anything
+ $self->{'enabled'} = 0;
+ foreach my $connector (Bugzilla->push_ext->connectors->list) {
+ if ($connector->enabled) {
+ $self->{'enabled'} = 1;
+ last;
+ }
+ }
+ }
+ }
+ return $self->{'enabled'};
+}
+
+#
+# deal with creation and updated events
+#
+
+sub _object_created {
+ my ($self, $args) = @_;
+
+ my $object = _get_object_from_args($args);
+ return unless $object;
+ return unless _should_push($object);
+
+ $self->_push_object('create', $object, change_set_id(), { timestamp => $args->{'timestamp'} });
+}
+
+sub _object_modified {
+ my ($self, $args) = @_;
+
+ my $object = _get_object_from_args($args);
+ return unless $object;
+ return unless _should_push($object);
+
+ my $changes = $args->{'changes'} || {};
+ return unless scalar keys %$changes;
+
+ my $change_set = change_set_id();
+
+ # detect when a bug changes from public to private (or back), so connectors
+ # can remove now-private bugs if required.
+ if ($object->isa('Bugzilla::Bug')) {
+ # we can't use user->can_see_bug(old_bug) as that works on IDs, and the
+ # bug has already been updated, so for now assume that a bug without
+ # groups is public.
+ my $old_bug = $args->{'old_bug'};
+ my $is_public = is_public($object);
+ my $was_public = $old_bug ? !@{$old_bug->groups_in} : $is_public;
+
+ if (!$is_public && $was_public) {
+ # bug is changing from public to private
+ # push a fake update with the just is_private change
+ my $private_changes = {
+ timestamp => $args->{'timestamp'},
+ changes => [
+ {
+ field => 'is_private',
+ removed => '0',
+ added => '1',
+ },
+ ],
+ };
+ # note we're sending the old bug object so we don't leak any
+ # security sensitive information.
+ $self->_push_object('modify', $old_bug, $change_set, $private_changes);
+ } elsif ($is_public && !$was_public) {
+ # bug is changing from private to public
+ # push a fake update with the just is_private change
+ my $private_changes = {
+ timestamp => $args->{'timestamp'},
+ changes => [
+ {
+ field => 'is_private',
+ removed => '1',
+ added => '0',
+ },
+ ],
+ };
+ # it's ok to send the new bug state here
+ $self->_push_object('modify', $object, $change_set, $private_changes);
+ }
+ }
+
+ # make flagtypes changes easier to process
+ if (exists $changes->{'flagtypes.name'}) {
+ _split_flagtypes($changes);
+ }
+
+ # TODO split group changes?
+
+ # restructure the changes hash
+ my $changes_data = {
+ timestamp => $args->{'timestamp'},
+ changes => [],
+ };
+ foreach my $field_name (sort keys %$changes) {
+ my $new_field_name = $field_name;
+ $new_field_name =~ s/isprivate/is_private/;
+
+ push @{$changes_data->{'changes'}}, {
+ field => $new_field_name,
+ removed => $changes->{$field_name}[0],
+ added => $changes->{$field_name}[1],
+ };
+ }
+
+ $self->_push_object('modify', $object, $change_set, $changes_data);
+}
+
+sub _get_object_from_args {
+ my ($args) = @_;
+ return get_first_value($args, qw(object bug flag group));
+}
+
+sub _should_push {
+ my ($object_or_class) = @_;
+ my $class = blessed($object_or_class) || $object_or_class;
+ return grep { $_ eq $class } qw(Bugzilla::Bug Bugzilla::Attachment Bugzilla::Comment);
+}
+
+# changes to bug flags are presented in a single field 'flagtypes.name' split
+# into individual fields
+sub _split_flagtypes {
+ my ($changes) = @_;
+
+ my @removed = _split_flagtype($changes->{'flagtypes.name'}->[0]);
+ my @added = _split_flagtype($changes->{'flagtypes.name'}->[1]);
+ delete $changes->{'flagtypes.name'};
+
+ foreach my $ra (@removed, @added) {
+ $changes->{$ra->[0]} = ['', ''];
+ }
+ foreach my $ra (@removed) {
+ my ($name, $value) = @$ra;
+ $changes->{$name}->[0] = $value;
+ }
+ foreach my $ra (@added) {
+ my ($name, $value) = @$ra;
+ $changes->{$name}->[1] = $value;
+ }
+}
+
+sub _split_flagtype {
+ my ($value) = @_;
+ my @result;
+ foreach my $change (split(/, /, $value)) {
+ my $requestee = '';
+ if ($change =~ s/\(([^\)]+)\)$//) {
+ $requestee = $1;
+ }
+ my ($name, $value) = $change =~ /^(.+)(.)$/;
+ $value .= " ($requestee)" if $requestee;
+ push @result, [ "flag.$name", $value ];
+ }
+ return @result;
+}
+
+# changes to attachment flags come in via flag_end_of_update which has a
+# completely different structure for reporting changes than
+# object_end_of_update. this morphs flag to object updates.
+sub _morph_flag_updates {
+ my ($args) = @_;
+
+ my @removed = _morph_flag_update($args->{'old_flags'});
+ my @added = _morph_flag_update($args->{'new_flags'});
+
+ my $changes = {};
+ foreach my $ra (@removed, @added) {
+ $changes->{$ra->[0]} = ['', ''];
+ }
+ foreach my $ra (@removed) {
+ my ($name, $value) = @$ra;
+ $changes->{$name}->[0] = $value;
+ }
+ foreach my $ra (@added) {
+ my ($name, $value) = @$ra;
+ $changes->{$name}->[1] = $value;
+ }
+
+ foreach my $flag (keys %$changes) {
+ if ($changes->{$flag}->[0] eq $changes->{$flag}->[1]) {
+ delete $changes->{$flag};
+ }
+ }
+
+ $args->{'changes'} = $changes;
+}
+
+sub _morph_flag_update {
+ my ($values) = @_;
+ my @result;
+ foreach my $orig_change (@$values) {
+ my $change = $orig_change; # work on a copy
+ $change =~ s/^[^:]+://;
+ my $requestee = '';
+ if ($change =~ s/\(([^\)]+)\)$//) {
+ $requestee = $1;
+ }
+ my ($name, $value) = $change =~ /^(.+)(.)$/;
+ $value .= " ($requestee)" if $requestee;
+ push @result, [ "flag.$name", $value ];
+ }
+ return @result;
+}
+
+#
+# serialise and insert into the table
+#
+
+sub _push_object {
+ my ($self, $message_type, $object, $change_set, $changes) = @_;
+ my $rh;
+
+ # serialise the object
+ my ($rh_object, $name) = Bugzilla::Extension::Push::Serialise->instance->object_to_hash($object);
+
+ if (!$rh_object) {
+ warn "empty hash from serialiser ($message_type $object)\n";
+ return;
+ }
+ $rh->{$name} = $rh_object;
+
+ # add in the events hash
+ my $rh_event = Bugzilla::Extension::Push::Serialise->instance->changes_to_event($changes);
+ return unless $rh_event;
+ $rh_event->{'action'} = $message_type;
+ $rh_event->{'target'} = $name;
+ $rh_event->{'change_set'} = $change_set;
+ $rh_event->{'routing_key'} = "$name.$message_type";
+ if (exists $rh_event->{'changes'}) {
+ $rh_event->{'routing_key'} .= ':' . join(',', map { $_->{'field'} } @{$rh_event->{'changes'}});
+ }
+ $rh->{'event'} = $rh_event;
+
+ # create message object
+ my $message = Bugzilla::Extension::Push::Message->new_transient({
+ payload => to_json($rh),
+ change_set => $change_set,
+ routing_key => $rh_event->{'routing_key'},
+ });
+
+ # don't hit the database unless there are interested connectors
+ my $should_push = 0;
+ foreach my $connector (Bugzilla->push_ext->connectors->list) {
+ next unless $connector->enabled;
+ next unless $connector->should_send($message);
+ $should_push = 1;
+ last;
+ }
+ return unless $should_push;
+
+ # insert into push table
+ $message->create_from_transient();
+}
+
+#
+# update/create hooks
+#
+
+sub object_end_of_create {
+ my ($self, $args) = @_;
+ return unless $self->_enabled;
+
+ # it's better to process objects from a non-generic end_of_create where
+ # possible; don't process them here to avoid duplicate messages
+ my $object = _get_object_from_args($args);
+ return if !$object ||
+ $object->isa('Bugzilla::Bug') ||
+ blessed($object) =~ /^Bugzilla::Extension/;
+
+ $self->_object_created($args);
+}
+
+sub object_end_of_update {
+ my ($self, $args) = @_;
+
+ # User objects are updated with every page load (to touch the session
+ # token). Because we ignore user objects, there's no need to create an
+ # instance of Push to check if we're enabled.
+ my $object = _get_object_from_args($args);
+ return if !$object || $object->isa('Bugzilla::User');
+
+ return unless $self->_enabled;
+
+ # it's better to process objects from a non-generic end_of_update where
+ # possible; don't process them here to avoid duplicate messages
+ return if $object->isa('Bugzilla::Bug') ||
+ $object->isa('Bugzilla::Flag') ||
+ blessed($object) =~ /^Bugzilla::Extension/;
+
+ $self->_object_modified($args);
+}
+
+# process bugs once they are fully formed
+# object_end_of_update is triggered while a bug is being created
+sub bug_end_of_create {
+ my ($self, $args) = @_;
+ return unless $self->_enabled;
+ $self->_object_created($args);
+}
+
+sub bug_end_of_update {
+ my ($self, $args) = @_;
+ return unless $self->_enabled;
+ $self->_object_modified($args);
+}
+
+sub flag_end_of_update {
+ my ($self, $args) = @_;
+ return unless $self->_enabled;
+ _morph_flag_updates($args);
+ $self->_object_modified($args);
+ delete $args->{changes};
+}
+
+# comments in bugzilla 4.0 doesn't aren't included in the bug_end_of_* hooks,
+# this code uses custom hooks to trigger
+sub bug_comment_create {
+ my ($self, $args) = @_;
+ return unless $self->_enabled;
+
+ return unless _should_push('Bugzilla::Comment');
+ my $bug = $args->{'bug'} or return;
+ my $timestamp = $args->{'timestamp'} or return;
+
+ my $comments = Bugzilla::Comment->match({ bug_id => $bug->id, bug_when => $timestamp });
+
+ foreach my $comment (@$comments) {
+ if ($comment->body ne '') {
+ $self->_push_object('create', $comment, change_set_id(), { timestamp => $timestamp });
+ }
+ }
+}
+
+sub bug_comment_update {
+ my ($self, $args) = @_;
+ return unless $self->_enabled;
+
+ return unless _should_push('Bugzilla::Comment');
+ my $bug = $args->{'bug'} or return;
+ my $timestamp = $args->{'timestamp'} or return;
+
+ my $comment_id = $args->{'comment_id'};
+ if ($comment_id) {
+ # XXX this should set changes. only is_private changes will trigger this event
+ my $comment = Bugzilla::Comment->new($comment_id);
+ $self->_push_object('update', $comment, change_set_id(), { timestamp => $timestamp });
+
+ } else {
+ # when a bug is created, an update is also triggered; we don't want to sent
+ # update messages for the initial comment, or for empty comments
+ my $comments = Bugzilla::Comment->match({ bug_id => $bug->id, bug_when => $timestamp });
+ foreach my $comment (@$comments) {
+ if ($comment->body ne '' && $comment->count) {
+ $self->_push_object('create', $comment, change_set_id(), { timestamp => $timestamp });
+ }
+ }
+ }
+}
+
+#
+# admin hooks
+#
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my $page = $args->{'page_id'};
+ my $vars = $args->{'vars'};
+
+ if ($page eq 'push_config.html') {
+ Bugzilla->user->in_group('admin')
+ || ThrowUserError('auth_failure',
+ { group => 'admin',
+ action => 'access',
+ object => 'administrative_pages' });
+ admin_config($vars);
+
+ } elsif ($page eq 'push_queues.html'
+ || $page eq 'push_queues_view.html'
+ ) {
+ Bugzilla->user->in_group('admin')
+ || ThrowUserError('auth_failure',
+ { group => 'admin',
+ action => 'access',
+ object => 'administrative_pages' });
+ admin_queues($vars, $page);
+
+ } elsif ($page eq 'push_log.html') {
+ Bugzilla->user->in_group('admin')
+ || ThrowUserError('auth_failure',
+ { group => 'admin',
+ action => 'access',
+ object => 'administrative_pages' });
+ admin_log($vars);
+ }
+}
+
+#
+# installation/config hooks
+#
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'push'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ push_ts => {
+ TYPE => 'DATETIME',
+ NOTNULL => 1,
+ },
+ payload => {
+ TYPE => 'LONGTEXT',
+ NOTNULL => 1,
+ },
+ change_set => {
+ TYPE => 'VARCHAR(32)',
+ NOTNULL => 1,
+ },
+ routing_key => {
+ TYPE => 'VARCHAR(64)',
+ NOTNULL => 1,
+ },
+ ],
+ };
+ $args->{'schema'}->{'push_backlog'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ message_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ },
+ push_ts => {
+ TYPE => 'DATETIME',
+ NOTNULL => 1,
+ },
+ payload => {
+ TYPE => 'LONGTEXT',
+ NOTNULL => 1,
+ },
+ change_set => {
+ TYPE => 'VARCHAR(32)',
+ NOTNULL => 1,
+ },
+ routing_key => {
+ TYPE => 'VARCHAR(64)',
+ NOTNULL => 1,
+ },
+ connector => {
+ TYPE => 'VARCHAR(32)',
+ NOTNULL => 1,
+ },
+ attempt_ts => {
+ TYPE => 'DATETIME',
+ },
+ attempts => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ },
+ last_error => {
+ TYPE => 'MEDIUMTEXT',
+ },
+ ],
+ INDEXES => [
+ push_backlog_idx => {
+ FIELDS => ['message_id', 'connector'],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'push_backoff'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ connector => {
+ TYPE => 'VARCHAR(32)',
+ NOTNULL => 1,
+ },
+ next_attempt_ts => {
+ TYPE => 'DATETIME',
+ },
+ attempts => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ },
+ ],
+ INDEXES => [
+ push_backoff_idx => {
+ FIELDS => ['connector'],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'push_options'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ connector => {
+ TYPE => 'VARCHAR(32)',
+ NOTNULL => 1,
+ },
+ option_name => {
+ TYPE => 'VARCHAR(32)',
+ NOTNULL => 1,
+ },
+ option_value => {
+ TYPE => 'VARCHAR(255)',
+ NOTNULL => 1,
+ },
+ ],
+ INDEXES => [
+ push_options_idx => {
+ FIELDS => ['connector', 'option_name'],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'push_log'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ message_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ },
+ change_set => {
+ TYPE => 'VARCHAR(32)',
+ NOTNULL => 1,
+ },
+ routing_key => {
+ TYPE => 'VARCHAR(64)',
+ NOTNULL => 1,
+ },
+ connector => {
+ TYPE => 'VARCHAR(32)',
+ NOTNULL => 1,
+ },
+ push_ts => {
+ TYPE => 'DATETIME',
+ NOTNULL => 1,
+ },
+ processed_ts => {
+ TYPE => 'DATETIME',
+ NOTNULL => 1,
+ },
+ result => {
+ TYPE => 'INT1',
+ NOTNULL => 1,
+ },
+ data => {
+ TYPE => 'MEDIUMTEXT',
+ },
+ ],
+ };
+}
+
+sub install_filesystem {
+ my ($self, $args) = @_;
+ my $files = $args->{'files'};
+
+ my $extensionsdir = bz_locations()->{'extensionsdir'};
+ my $scriptname = $extensionsdir . "/Push/bin/bugzilla-pushd.pl";
+
+ $files->{$scriptname} = {
+ perms => Bugzilla::Install::Filesystem::WS_EXECUTE
+ };
+}
+
+sub db_sanitize {
+ my $dbh = Bugzilla->dbh;
+ print "Deleting push extension logs and messages...\n";
+ $dbh->do("DELETE FROM push");
+ $dbh->do("DELETE FROM push_backlog");
+ $dbh->do("DELETE FROM push_backoff");
+ $dbh->do("DELETE FROM push_log");
+ $dbh->do("DELETE FROM push_options");
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/Push/bin/bugzilla-pushd.pl b/extensions/Push/bin/bugzilla-pushd.pl
new file mode 100755
index 000000000..f048df157
--- /dev/null
+++ b/extensions/Push/bin/bugzilla-pushd.pl
@@ -0,0 +1,54 @@
+#!/usr/bin/perl
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+
+use FindBin '$RealBin';
+use lib "$RealBin/../../..";
+use lib "$RealBin/../../../lib";
+use lib "$RealBin/../lib";
+
+BEGIN {
+ use Bugzilla;
+ Bugzilla->extensions;
+}
+
+use Bugzilla::Extension::Push::Daemon;
+Bugzilla::Extension::Push::Daemon->start();
+
+=head1 NAME
+
+bugzilla-push.pl - Pushes changes queued by the Push extension to connectors.
+
+=head1 SYNOPSIS
+
+ bugzilla-push.pl [OPTIONS] COMMAND
+
+ OPTIONS:
+ -f Run in the foreground (don't detach)
+ -d Output a lot of debugging information
+ -p file Specify the file where bugzilla-push.pl should store its current
+ process id. Defaults to F<data/bugzilla-push.pl.pid>.
+ -n name What should this process call itself in the system log?
+ Defaults to the full path you used to invoke the script.
+
+ COMMANDS:
+ start Starts a new bugzilla-push daemon if there isn't one running already
+ stop Stops a running bugzilla-push daemon
+ restart Stops a running bugzilla-push if one is running, and then
+ starts a new one.
+ check Report the current status of the daemon.
+ install On some *nix systems, this automatically installs and
+ configures bugzilla-push.pl as a system service so that it will
+ start every time the machine boots.
+ uninstall Removes the system service for bugzilla-push.pl.
+ help Display this usage info
+
+
diff --git a/extensions/Push/bin/nagios_push_checker.pl b/extensions/Push/bin/nagios_push_checker.pl
new file mode 100755
index 000000000..f022f584d
--- /dev/null
+++ b/extensions/Push/bin/nagios_push_checker.pl
@@ -0,0 +1,54 @@
+#!/usr/bin/perl
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+
+use FindBin '$RealBin';
+use lib "$RealBin/../../..";
+use lib "$RealBin/../../../lib";
+use lib "$RealBin/../lib";
+
+use Bugzilla;
+use Bugzilla::Constants;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+# Number of jobs required in the queue before we alert
+
+use constant WARN_COUNT => 500;
+use constant ALARM_COUNT => 750;
+
+use constant NAGIOS_OK => 0;
+use constant NAGIOS_WARNING => 1;
+use constant NAGIOS_CRITICAL => 2;
+
+my $connector = shift
+ || die "Syntax: $0 connector\neg. $0 TCL\n";
+$connector = uc($connector);
+
+my $sql = <<EOF;
+ SELECT COUNT(*)
+ FROM push_backlog
+ WHERE connector = ?
+EOF
+
+my $dbh = Bugzilla->switch_to_shadow_db;
+my ($count) = @{ $dbh->selectcol_arrayref($sql, undef, $connector) };
+
+if ($count < WARN_COUNT) {
+ print "push $connector OK: $count messages found.\n";
+ exit NAGIOS_OK;
+} elsif ($count < ALARM_COUNT) {
+ print "push $connector WARNING: $count messages found.\n";
+ exit NAGIOS_WARNING;
+} else {
+ print "push $connector CRITICAL: $count messages found.\n";
+ exit NAGIOS_CRITICAL;
+}
diff --git a/extensions/Push/lib/Admin.pm b/extensions/Push/lib/Admin.pm
new file mode 100644
index 000000000..f579409bd
--- /dev/null
+++ b/extensions/Push/lib/Admin.pm
@@ -0,0 +1,122 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Admin;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Error;
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::Util qw(trim detaint_natural trick_taint);
+
+use base qw(Exporter);
+our @EXPORT = qw(
+ admin_config
+ admin_queues
+ admin_log
+);
+
+sub admin_config {
+ my ($vars) = @_;
+ my $push = Bugzilla->push_ext;
+ my $input = Bugzilla->input_params;
+
+ if ($input->{save}) {
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
+ _update_config_from_form('global', $push->config);
+ foreach my $connector ($push->connectors->list) {
+ _update_config_from_form($connector->name, $connector->config);
+ }
+ $push->set_config_last_modified();
+ $dbh->bz_commit_transaction();
+ $vars->{message} = 'push_config_updated';
+ }
+
+ $vars->{push} = $push;
+ $vars->{connectors} = $push->connectors;
+}
+
+sub _update_config_from_form {
+ my ($name, $config) = @_;
+ my $input = Bugzilla->input_params;
+
+ # read values from form
+ my $values = {};
+ foreach my $option ($config->options) {
+ my $option_name = $option->{name};
+ $values->{$option_name} = trim($input->{$name . ".$option_name"});
+ }
+
+ # validate
+ if ($values->{enabled} eq 'Enabled') {
+ eval {
+ $config->validate($values);
+ };
+ if ($@) {
+ ThrowUserError('push_error', { error_message => clean_error($@) });
+ }
+ }
+
+ # update
+ foreach my $option ($config->options) {
+ my $option_name = $option->{name};
+ trick_taint($values->{$option_name});
+ $config->{$option_name} = $values->{$option_name};
+ }
+ $config->update();
+}
+
+sub admin_queues {
+ my ($vars, $page) = @_;
+ my $push = Bugzilla->push_ext;
+ my $input = Bugzilla->input_params;
+
+ if ($page eq 'push_queues.html') {
+ $vars->{push} = $push;
+
+ } elsif ($page eq 'push_queues_view.html') {
+ my $queue;
+ if ($input->{connector}) {
+ my $connector = $push->connectors->by_name($input->{connector})
+ || ThrowUserError('push_error', { error_message => 'Invalid connector' });
+ $queue = $connector->backlog;
+ } else {
+ $queue = $push->queue;
+ }
+ $vars->{queue} = $queue;
+
+ my $id = $input->{message} || 0;
+ detaint_natural($id)
+ || ThrowUserError('push_error', { error_message => 'Invalid message ID' });
+ my $message = $queue->by_id($id)
+ || ThrowUserError('push_error', { error_message => 'Invalid message ID' });
+
+ if ($input->{delete}) {
+ $message->remove_from_db();
+ $vars->{message} = 'push_message_deleted';
+
+ } else {
+ $vars->{message_obj} = $message;
+ eval {
+ $vars->{json} = to_json($message->payload_decoded, 1);
+ };
+ }
+ }
+}
+
+sub admin_log {
+ my ($vars) = @_;
+ my $push = Bugzilla->push_ext;
+ my $input = Bugzilla->input_params;
+
+ $vars->{push} = $push;
+}
+
+1;
diff --git a/extensions/Push/lib/BacklogMessage.pm b/extensions/Push/lib/BacklogMessage.pm
new file mode 100644
index 000000000..cd40ebefb
--- /dev/null
+++ b/extensions/Push/lib/BacklogMessage.pm
@@ -0,0 +1,150 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::BacklogMessage;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Object';
+
+use constant AUDIT_CREATES => 0;
+use constant AUDIT_UPDATES => 0;
+use constant AUDIT_REMOVES => 0;
+use constant USE_MEMCACHED => 0;
+
+use Bugzilla;
+use Bugzilla::Error;
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::Util;
+use Encode;
+
+#
+# initialisation
+#
+
+use constant DB_TABLE => 'push_backlog';
+use constant DB_COLUMNS => qw(
+ id
+ message_id
+ push_ts
+ payload
+ change_set
+ routing_key
+ connector
+ attempt_ts
+ attempts
+ last_error
+);
+use constant UPDATE_COLUMNS => qw(
+ attempt_ts
+ attempts
+ last_error
+);
+use constant LIST_ORDER => 'push_ts';
+use constant VALIDATORS => {
+ payload => \&_check_payload,
+ change_set => \&_check_change_set,
+ routing_key => \&_check_routing_key,
+ connector => \&_check_connector,
+ attempts => \&_check_attempts,
+};
+
+#
+# constructors
+#
+
+sub create_from_message {
+ my ($class, $message, $connector) = @_;
+ my $self = $class->create({
+ message_id => $message->id,
+ push_ts => $message->push_ts,
+ payload => $message->payload,
+ change_set => $message->change_set,
+ routing_key => $message->routing_key,
+ connector => $connector->name,
+ attempt_ts => undef,
+ attempts => 0,
+ last_error => undef,
+ });
+ return $self;
+}
+
+#
+# accessors
+#
+
+sub message_id { return $_[0]->{'message_id'} }
+sub push_ts { return $_[0]->{'push_ts'}; }
+sub payload { return $_[0]->{'payload'}; }
+sub change_set { return $_[0]->{'change_set'}; }
+sub routing_key { return $_[0]->{'routing_key'}; }
+sub connector { return $_[0]->{'connector'}; }
+sub attempt_ts { return $_[0]->{'attempt_ts'}; }
+sub attempts { return $_[0]->{'attempts'}; }
+sub last_error { return $_[0]->{'last_error'}; }
+
+sub payload_decoded {
+ my ($self) = @_;
+ return from_json($self->{'payload'});
+}
+
+sub attempt_time {
+ my ($self) = @_;
+ if (!exists $self->{'attempt_time'}) {
+ $self->{'attempt_time'} = datetime_from($self->attempt_ts)->epoch;
+ }
+ return $self->{'attempt_time'};
+}
+
+#
+# mutators
+#
+
+sub inc_attempts {
+ my ($self, $error) = @_;
+ $self->{attempt_ts} = Bugzilla->dbh->selectrow_array('SELECT NOW()');
+ $self->{attempts} = $self->{attempts} + 1;
+ $self->{last_error} = $error;
+ $self->update;
+}
+
+#
+# validators
+#
+
+sub _check_payload {
+ my ($invocant, $value) = @_;
+ length($value) || ThrowCodeError('push_invalid_payload');
+ return $value;
+}
+
+sub _check_change_set {
+ my ($invocant, $value) = @_;
+ (defined($value) && length($value)) || ThrowCodeError('push_invalid_change_set');
+ return $value;
+}
+
+sub _check_routing_key {
+ my ($invocant, $value) = @_;
+ (defined($value) && length($value)) || ThrowCodeError('push_invalid_routing_key');
+ return $value;
+}
+
+sub _check_connector {
+ my ($invocant, $value) = @_;
+ Bugzilla->push_ext->connectors->exists($value) || ThrowCodeError('push_invalid_connector');
+ return $value;
+}
+
+sub _check_attempts {
+ my ($invocant, $value) = @_;
+ return $value || 0;
+}
+
+1;
+
diff --git a/extensions/Push/lib/BacklogQueue.pm b/extensions/Push/lib/BacklogQueue.pm
new file mode 100644
index 000000000..79b9b72ee
--- /dev/null
+++ b/extensions/Push/lib/BacklogQueue.pm
@@ -0,0 +1,127 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::BacklogQueue;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Extension::Push::BacklogMessage;
+
+sub new {
+ my ($class, $connector) = @_;
+ my $self = {};
+ bless($self, $class);
+ $self->{connector} = $connector;
+ return $self;
+}
+
+sub count {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ return $dbh->selectrow_array("
+ SELECT COUNT(*)
+ FROM push_backlog
+ WHERE connector = ?",
+ undef,
+ $self->{connector});
+}
+
+sub oldest {
+ my ($self) = @_;
+ my @messages = $self->list(
+ limit => 1,
+ filter => 'AND ((next_attempt_ts IS NULL) OR (next_attempt_ts <= NOW()))',
+ );
+ return scalar(@messages) ? $messages[0] : undef;
+}
+
+sub by_id {
+ my ($self, $id) = @_;
+ my @messages = $self->list(
+ limit => 1,
+ filter => "AND (log.id = $id)",
+ );
+ return scalar(@messages) ? $messages[0] : undef;
+}
+
+sub list {
+ my ($self, %args) = @_;
+ $args{limit} ||= 10;
+ $args{filter} ||= '';
+ my @result;
+ my $dbh = Bugzilla->dbh;
+
+ my $filter_sql = $args{filter} || '';
+ my $sth = $dbh->prepare("
+ SELECT log.id, message_id, push_ts, payload, change_set, routing_key, attempt_ts, log.attempts
+ FROM push_backlog log
+ LEFT JOIN push_backoff off ON off.connector = log.connector
+ WHERE log.connector = ? ".
+ $args{filter} . "
+ ORDER BY push_ts " .
+ $dbh->sql_limit($args{limit})
+ );
+ $sth->execute($self->{connector});
+ while (my $row = $sth->fetchrow_hashref()) {
+ push @result, Bugzilla::Extension::Push::BacklogMessage->new({
+ id => $row->{id},
+ message_id => $row->{message_id},
+ push_ts => $row->{push_ts},
+ payload => $row->{payload},
+ change_set => $row->{change_set},
+ routing_key => $row->{routing_key},
+ connector => $self->{connector},
+ attempt_ts => $row->{attempt_ts},
+ attempts => $row->{attempts},
+ });
+ }
+ return @result;
+}
+
+#
+# backoff
+#
+
+sub backoff {
+ my ($self) = @_;
+ if (!$self->{backoff}) {
+ my $ra = Bugzilla::Extension::Push::Backoff->match({
+ connector => $self->{connector}
+ });
+ if (@$ra) {
+ $self->{backoff} = $ra->[0];
+ } else {
+ $self->{backoff} = Bugzilla::Extension::Push::Backoff->create({
+ connector => $self->{connector}
+ });
+ }
+ }
+ return $self->{backoff};
+}
+
+sub reset_backoff {
+ my ($self) = @_;
+ my $backoff = $self->backoff;
+ $backoff->reset();
+ $backoff->update();
+}
+
+sub inc_backoff {
+ my ($self) = @_;
+ my $backoff = $self->backoff;
+ $backoff->inc();
+ $backoff->update();
+}
+
+sub connector {
+ my ($self) = @_;
+ return $self->{connector};
+}
+
+1;
diff --git a/extensions/Push/lib/Backoff.pm b/extensions/Push/lib/Backoff.pm
new file mode 100644
index 000000000..55552e5e1
--- /dev/null
+++ b/extensions/Push/lib/Backoff.pm
@@ -0,0 +1,110 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Backoff;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Object';
+
+use constant AUDIT_CREATES => 0;
+use constant AUDIT_UPDATES => 0;
+use constant AUDIT_REMOVES => 0;
+use constant USE_MEMCACHED => 0;
+
+use Bugzilla;
+use Bugzilla::Util;
+
+#
+# initialisation
+#
+
+use constant DB_TABLE => 'push_backoff';
+use constant DB_COLUMNS => qw(
+ id
+ connector
+ next_attempt_ts
+ attempts
+);
+use constant UPDATE_COLUMNS => qw(
+ next_attempt_ts
+ attempts
+);
+use constant VALIDATORS => {
+ connector => \&_check_connector,
+ next_attempt_ts => \&_check_next_attempt_ts,
+ attempts => \&_check_attempts,
+};
+use constant LIST_ORDER => 'next_attempt_ts';
+
+#
+# accessors
+#
+
+sub connector { return $_[0]->{'connector'}; }
+sub next_attempt_ts { return $_[0]->{'next_attempt_ts'}; }
+sub attempts { return $_[0]->{'attempts'}; }
+
+sub next_attempt_time {
+ my ($self) = @_;
+ if (!exists $self->{'next_attempt_time'}) {
+ $self->{'next_attempt_time'} = datetime_from($self->next_attempt_ts)->epoch;
+ }
+ return $self->{'next_attempt_time'};
+}
+
+#
+# mutators
+#
+
+sub reset {
+ my ($self) = @_;
+ $self->{next_attempt_ts} = Bugzilla->dbh->selectrow_array('SELECT NOW()');
+ $self->{attempts} = 0;
+ Bugzilla->push_ext->logger->debug(
+ sprintf("resetting backoff for %s", $self->connector)
+ );
+}
+
+sub inc {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $attempts = $self->attempts + 1;
+ my $seconds = $attempts <= 4 ? 5 ** $attempts : 15 * 60;
+ my ($date) = $dbh->selectrow_array("SELECT " . $dbh->sql_date_math('NOW()', '+', $seconds, 'SECOND'));
+
+ $self->{next_attempt_ts} = $date;
+ $self->{attempts} = $attempts;
+ Bugzilla->push_ext->logger->debug(
+ sprintf("setting next attempt for %s to %s (attempt %s)", $self->connector, $date, $attempts)
+ );
+}
+
+#
+# validators
+#
+
+sub _check_connector {
+ my ($invocant, $value) = @_;
+ Bugzilla->push_ext->connectors->exists($value) || ThrowCodeError('push_invalid_connector');
+ return $value;
+}
+
+sub _check_next_attempt_ts {
+ my ($invocant, $value) = @_;
+ return $value || Bugzilla->dbh->selectrow_array('SELECT NOW()');
+}
+
+sub _check_attempts {
+ my ($invocant, $value) = @_;
+ return $value || 0;
+}
+
+1;
+
diff --git a/extensions/Push/lib/Config.pm b/extensions/Push/lib/Config.pm
new file mode 100644
index 000000000..7033b4195
--- /dev/null
+++ b/extensions/Push/lib/Config.pm
@@ -0,0 +1,215 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Config;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Extension::Push::Option;
+use Crypt::CBC;
+
+sub new {
+ my ($class, $name, @options) = @_;
+ my $self = {
+ _name => $name
+ };
+ bless($self, $class);
+
+ $self->{_options} = [@options];
+ unshift @{$self->{_options}}, {
+ name => 'enabled',
+ label => 'Status',
+ help => '',
+ type => 'select',
+ values => [ 'Enabled', 'Disabled' ],
+ default => 'Disabled',
+ };
+
+ return $self;
+}
+
+sub options {
+ my ($self) = @_;
+ return @{$self->{_options}};
+}
+
+sub option {
+ my ($self, $name) = @_;
+ foreach my $option ($self->options) {
+ return $option if $option->{name} eq $name;
+ }
+ return undef;
+}
+
+sub load {
+ my ($self) = @_;
+ my $config = {};
+ my $logger = Bugzilla->push_ext->logger;
+
+ # prime $config with defaults
+ foreach my $rh ($self->options) {
+ $config->{$rh->{name}} = $rh->{default};
+ }
+
+ # override defaults with values from database
+ my $options = Bugzilla::Extension::Push::Option->match({
+ connector => $self->{_name},
+ });
+ foreach my $option (@$options) {
+ my $option_config = $self->option($option->name)
+ || next;
+ if ($option_config->{type} eq 'password') {
+ $config->{$option->name} = $self->_decrypt($option->value);
+ } else {
+ $config->{$option->name} = $option->value;
+ }
+ }
+
+ # validate when running from the daemon
+ if (Bugzilla->push_ext->is_daemon) {
+ $self->_validate_config($config);
+ }
+
+ # done, update self
+ foreach my $name (keys %$config) {
+ my $value = $self->option($name)->{type} eq 'password' ? '********' : $config->{$name};
+ $logger->debug(sprintf("%s: set %s=%s\n", $self->{_name}, $name, $value || ''));
+ $self->{$name} = $config->{$name};
+ }
+}
+
+sub validate {
+ my ($self, $config) = @_;
+ $self->_validate_mandatory($config);
+ $self->_validate_config($config);
+}
+
+sub update {
+ my ($self) = @_;
+
+ my @valid_options = map { $_->{name} } $self->options;
+
+ my %options;
+ my $options_list = Bugzilla::Extension::Push::Option->match({
+ connector => $self->{_name},
+ });
+ foreach my $option (@$options_list) {
+ $options{$option->name} = $option;
+ }
+
+ # delete options which are no longer valid
+ foreach my $name (keys %options) {
+ if (!grep { $_ eq $name } @valid_options) {
+ $options{$name}->remove_from_db();
+ delete $options{$name};
+ }
+ }
+
+ # update options
+ foreach my $name (keys %options) {
+ my $option = $options{$name};
+ if ($self->option($name)->{type} eq 'password') {
+ $option->set_value($self->_encrypt($self->{$name}));
+ } else {
+ $option->set_value($self->{$name});
+ }
+ $option->update();
+ }
+
+ # add missing options
+ foreach my $name (@valid_options) {
+ next if exists $options{$name};
+ Bugzilla::Extension::Push::Option->create({
+ connector => $self->{_name},
+ option_name => $name,
+ option_value => $self->{$name},
+ });
+ }
+}
+
+sub _remove_invalid_options {
+ my ($self, $config) = @_;
+ my @names;
+ foreach my $rh ($self->options) {
+ push @names, $rh->{name};
+ }
+ foreach my $name (keys %$config) {
+ if ($name =~ /^_/ || !grep { $_ eq $name } @names) {
+ delete $config->{$name};
+ }
+ }
+}
+
+sub _validate_mandatory {
+ my ($self, $config) = @_;
+ $self->_remove_invalid_options($config);
+
+ my @missing;
+ foreach my $option ($self->options) {
+ next unless $option->{required};
+ my $name = $option->{name};
+ if (!exists $config->{$name} || !defined($config->{$name}) || $config->{$name} eq '') {
+ push @missing, $option;
+ }
+ }
+ if (@missing) {
+ my $connector = $self->{_name};
+ @missing = map { $_->{label} } @missing;
+ if (scalar @missing == 1) {
+ die "The option '$missing[0]' for the connector '$connector' is mandatory\n";
+ } else {
+ die "The following options for the connector '$connector' are mandatory:\n "
+ . join("\n ", @missing) . "\n";
+ }
+ }
+}
+
+sub _validate_config {
+ my ($self, $config) = @_;
+ $self->_remove_invalid_options($config);
+
+ my @errors;
+ foreach my $option ($self->options) {
+ my $name = $option->{name};
+ next unless exists $config->{$name} && exists $option->{validate};
+ eval {
+ $option->{validate}->($config->{$name}, $config);
+ };
+ push @errors, $@ if $@;
+ }
+ die join("\n", @errors) if @errors;
+
+ if ($self->{_name} ne 'global') {
+ my $class = 'Bugzilla::Extension::Push::Connector::' . $self->{_name};
+ $class->options_validate($config);
+ }
+}
+
+sub _cipher {
+ my ($self) = @_;
+ $self->{_cipher} ||= Crypt::CBC->new(
+ -key => Bugzilla->localconfig->{'site_wide_secret'},
+ -cipher => 'DES_EDE3');
+ return $self->{_cipher};
+}
+
+sub _decrypt {
+ my ($self, $value) = @_;
+ my $result;
+ eval { $result = $self->_cipher->decrypt_hex($value) };
+ return $@ ? '' : $result;
+}
+
+sub _encrypt {
+ my ($self, $value) = @_;
+ return $self->_cipher->encrypt_hex($value);
+}
+
+1;
diff --git a/extensions/Push/lib/Connector.disabled/AMQP.pm b/extensions/Push/lib/Connector.disabled/AMQP.pm
new file mode 100644
index 000000000..7b7d4aa72
--- /dev/null
+++ b/extensions/Push/lib/Connector.disabled/AMQP.pm
@@ -0,0 +1,230 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Connector::AMQP;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Extension::Push::Connector::Base';
+
+use Bugzilla::Constants;
+use Bugzilla::Extension::Push::Constants;
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::Util qw(generate_random_password);
+use DateTime;
+
+sub init {
+ my ($self) = @_;
+ $self->{mq} = 0;
+ $self->{channel} = 1;
+
+ if ($self->config->{queue}) {
+ $self->{queue_name} = $self->config->{queue};
+ } else {
+ my $queue_name = Bugzilla->params->{'urlbase'};
+ $queue_name =~ s#^https?://##;
+ $queue_name =~ s#/$#|#;
+ $queue_name .= generate_random_password(16);
+ $self->{queue_name} = $queue_name;
+ }
+}
+
+sub options {
+ return (
+ {
+ name => 'host',
+ label => 'AMQP Hostname',
+ type => 'string',
+ default => 'localhost',
+ required => 1,
+ },
+ {
+ name => 'port',
+ label => 'AMQP Port',
+ type => 'string',
+ default => '5672',
+ required => 1,
+ validate => sub {
+ $_[0] =~ /\D/ && die "Invalid port (must be numeric)\n";
+ },
+ },
+ {
+ name => 'username',
+ label => 'Username',
+ type => 'string',
+ default => 'guest',
+ required => 1,
+ },
+ {
+ name => 'password',
+ label => 'Password',
+ type => 'password',
+ default => 'guest',
+ required => 1,
+ },
+ {
+ name => 'vhost',
+ label => 'Virtual Host',
+ type => 'string',
+ default => '/',
+ required => 1,
+ },
+ {
+ name => 'exchange',
+ label => 'Exchange',
+ type => 'string',
+ default => '',
+ required => 1,
+ },
+ {
+ name => 'queue',
+ label => 'Queue',
+ type => 'string',
+ },
+ );
+}
+
+sub stop {
+ my ($self) = @_;
+ if ($self->{mq}) {
+ Bugzilla->push_ext->logger->debug('AMQP: disconnecting');
+ $self->{mq}->disconnect();
+ $self->{mq} = 0;
+ }
+}
+
+sub _connect {
+ my ($self) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+ my $config = $self->config;
+
+ $self->stop();
+
+ $logger->debug('AMQP: Connecting to RabbitMQ ' . $config->{host} . ':' . $config->{port});
+ require Net::RabbitMQ;
+ my $mq = Net::RabbitMQ->new();
+ $mq->connect(
+ $config->{host},
+ {
+ port => $config->{port},
+ user => $config->{username},
+ password => $config->{password},
+ }
+ );
+ $self->{mq} = $mq;
+
+ $logger->debug('AMQP: Opening channel ' . $self->{channel});
+ $self->{mq}->channel_open($self->{channel});
+
+ $logger->debug('AMQP: Declaring queue ' . $self->{queue_name});
+ $self->{mq}->queue_declare(
+ $self->{channel},
+ $self->{queue_name},
+ {
+ passive => 0,
+ durable => 1,
+ exclusive => 0,
+ auto_delete => 0,
+ },
+ );
+}
+
+sub _bind {
+ my ($self, $message) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+ my $config = $self->config;
+
+ # bind to queue (also acts to verify the connection is still valid)
+ if ($self->{mq}) {
+ eval {
+ $logger->debug('AMQP: binding queue(' . $self->{queue_name} . ') with exchange(' . $config->{exchange} . ')');
+ $self->{mq}->queue_bind(
+ $self->{channel},
+ $self->{queue_name},
+ $config->{exchange},
+ $message->routing_key,
+ );
+ };
+ if ($@) {
+ $logger->debug('AMQP: ' . clean_error($@));
+ $self->{mq} = 0;
+ }
+ }
+
+}
+
+sub should_send {
+ my ($self, $message) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+
+ my $payload = $message->payload_decoded();
+ my $target = $payload->{event}->{target};
+ my $is_private = $payload->{$target}->{is_private} ? 1 : 0;
+ if (!$is_private && exists $payload->{$target}->{bug}) {
+ $is_private = $payload->{$target}->{bug}->{is_private} ? 1 : 0;
+ }
+
+ if ($is_private) {
+ # we only want to push the is_private message from the change_set, as
+ # this is guaranteed to contain public information only
+ if ($message->routing_key !~ /\.modify:is_private$/) {
+ $logger->debug('AMQP: Ignoring private message');
+ return 0;
+ }
+ $logger->debug('AMQP: Sending change of message to is_private');
+ }
+ return 1;
+}
+
+sub send {
+ my ($self, $message) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+ my $config = $self->config;
+
+ # don't push comments to pulse
+ if ($message->routing_key =~ /^comment\./) {
+ $logger->debug('AMQP: Ignoring comment');
+ return PUSH_RESULT_IGNORED;
+ }
+
+ # don't push private data
+ $self->should_push($message)
+ || return PUSH_RESULT_IGNORED;
+
+ $self->_bind($message);
+
+ eval {
+ # reconnect if required
+ if (!$self->{mq}) {
+ $self->_connect();
+ }
+
+ # send message
+ $logger->debug('AMQP: Publishing message');
+ $self->{mq}->publish(
+ $self->{channel},
+ $message->routing_key,
+ $message->payload,
+ {
+ exchange => $config->{exchange},
+ },
+ {
+ content_type => 'text/plain',
+ content_encoding => '8bit',
+ },
+ );
+ };
+ if ($@) {
+ return (PUSH_RESULT_TRANSIENT, clean_error($@));
+ }
+
+ return PUSH_RESULT_OK;
+}
+
+1;
+
diff --git a/extensions/Push/lib/Connector.disabled/ServiceNow.pm b/extensions/Push/lib/Connector.disabled/ServiceNow.pm
new file mode 100644
index 000000000..832cc9262
--- /dev/null
+++ b/extensions/Push/lib/Connector.disabled/ServiceNow.pm
@@ -0,0 +1,434 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Connector::ServiceNow;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Extension::Push::Connector::Base';
+
+use Bugzilla::Attachment;
+use Bugzilla::Bug;
+use Bugzilla::Component;
+use Bugzilla::Constants;
+use Bugzilla::Extension::Push::Constants;
+use Bugzilla::Extension::Push::Serialise;
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::Field;
+use Bugzilla::Mailer;
+use Bugzilla::Product;
+use Bugzilla::User;
+use Bugzilla::Util qw(trim trick_taint);
+use Email::MIME;
+use FileHandle;
+use LWP;
+use MIME::Base64;
+use Net::LDAP;
+
+use constant SEND_COMPONENTS => (
+ {
+ product => 'mozilla.org',
+ component => 'Server Operations: Desktop Issues',
+ },
+);
+
+sub options {
+ return (
+ {
+ name => 'bugzilla_user',
+ label => 'Bugzilla Service-Now User',
+ type => 'string',
+ default => 'service.now@bugzilla.tld',
+ required => 1,
+ validate => sub {
+ Bugzilla::User->new({ name => $_[0] })
+ || die "Invalid Bugzilla user ($_[0])\n";
+ },
+ },
+ {
+ name => 'ldap_scheme',
+ label => 'Mozilla LDAP Scheme',
+ type => 'select',
+ values => [ 'LDAP', 'LDAPS' ],
+ default => 'LDAPS',
+ required => 1,
+ },
+ {
+ name => 'ldap_host',
+ label => 'Mozilla LDAP Host',
+ type => 'string',
+ default => '',
+ required => 1,
+ },
+ {
+ name => 'ldap_user',
+ label => 'Mozilla LDAP Bind Username',
+ type => 'string',
+ default => '',
+ required => 1,
+ },
+ {
+ name => 'ldap_pass',
+ label => 'Mozilla LDAP Password',
+ type => 'password',
+ default => '',
+ required => 1,
+ },
+ {
+ name => 'ldap_poll',
+ label => 'Mozilla LDAP Poll Frequency',
+ type => 'string',
+ default => '3',
+ required => 1,
+ help => 'minutes',
+ validate => sub {
+ $_[0] =~ /\D/
+ && die "LDAP Poll Frequency must be an integer\n";
+ $_[0] == 0
+ && die "LDAP Poll Frequency cannot be less than one minute\n";
+ },
+ },
+ {
+ name => 'service_now_url',
+ label => 'Service Now JSON URL',
+ type => 'string',
+ default => 'https://mozilladev.service-now.com',
+ required => 1,
+ help => "Must start with https:// and end with ?JSON",
+ validate => sub {
+ $_[0] =~ m#^https://[^\.\/]+\.service-now\.com\/#
+ || die "Invalid Service Now JSON URL\n";
+ $_[0] =~ m#\?JSON$#
+ || die "Invalid Service Now JSON URL (must end with ?JSON)\n";
+ },
+ },
+ {
+ name => 'service_now_user',
+ label => 'Service Now JSON Username',
+ type => 'string',
+ default => '',
+ required => 1,
+ },
+ {
+ name => 'service_now_pass',
+ label => 'Service Now JSON Password',
+ type => 'password',
+ default => '',
+ required => 1,
+ },
+ );
+}
+
+sub options_validate {
+ my ($self, $config) = @_;
+ my $host = $config->{ldap_host};
+ trick_taint($host);
+ my $scheme = lc($config->{ldap_scheme});
+ eval {
+ my $ldap = Net::LDAP->new($host, scheme => $scheme, onerror => 'die', timeout => 5)
+ or die $!;
+ $ldap->bind($config->{ldap_user}, password => $config->{ldap_pass});
+ };
+ if ($@) {
+ die sprintf("Failed to connect to %s://%s/: %s\n", $scheme, $host, $@);
+ }
+}
+
+my $_instance;
+
+sub init {
+ my ($self) = @_;
+ $_instance = $self;
+}
+
+sub load_config {
+ my ($self) = @_;
+ $self->SUPER::load_config(@_);
+ $self->{bugzilla_user} ||= Bugzilla::User->new({ name => $self->config->{bugzilla_user} });
+}
+
+sub should_send {
+ my ($self, $message) = @_;
+
+ my $data = $message->payload_decoded;
+ my $bug_data = $self->_get_bug_data($data)
+ || return 0;
+
+ # we don't want to send the initial comment in a separate message
+ # because we inject it into the inital message
+ if (exists $data->{comment} && $data->{comment}->{number} == 0) {
+ return 0;
+ }
+
+ my $target = $data->{event}->{target};
+ unless ($target eq 'bug' || $target eq 'comment' || $target eq 'attachment') {
+ return 0;
+ }
+
+ # ensure the service-now user can see the bug
+ if (!$self->{bugzilla_user} || !$self->{bugzilla_user}->is_enabled) {
+ return 0;
+ }
+ $self->{bugzilla_user}->can_see_bug($bug_data->{id})
+ || return 0;
+
+ # don't push changes made by the service-now account
+ $data->{event}->{user}->{id} == $self->{bugzilla_user}->id
+ && return 0;
+
+ # filter based on the component
+ my $bug = Bugzilla::Bug->new($bug_data->{id});
+ my $send = 0;
+ foreach my $rh (SEND_COMPONENTS) {
+ if ($bug->product eq $rh->{product} && $bug->component eq $rh->{component}) {
+ $send = 1;
+ last;
+ }
+ }
+ return $send;
+}
+
+sub send {
+ my ($self, $message) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+ my $config = $self->config;
+
+ # should_send intiailises bugzilla_user; make sure we return a useful error message
+ if (!$self->{bugzilla_user}) {
+ return (PUSH_RESULT_TRANSIENT, "Invalid bugzilla-user (" . $self->config->{bugzilla_user} . ")");
+ }
+
+ # load the bug
+ my $data = $message->payload_decoded;
+ my $bug_data = $self->_get_bug_data($data);
+ my $bug = Bugzilla::Bug->new($bug_data->{id});
+
+ if ($message->routing_key eq 'bug.create') {
+ # inject the comment into the data for new bugs
+ my $comment = shift @{ $bug->comments };
+ if ($comment->body ne '') {
+ $bug_data->{comment} = Bugzilla::Extension::Push::Serialise->instance->object_to_hash($comment, 1);
+ }
+
+ } elsif ($message->routing_key eq 'attachment.create') {
+ # inject the attachment payload
+ my $attachment = Bugzilla::Attachment->new($data->{attachment}->{id});
+ $data->{attachment}->{data} = encode_base64($attachment->data);
+ }
+
+ # map bmo login to ldap login and insert into json payload
+ $self->_add_ldap_logins($data, {});
+
+ # flatten json data
+ $self->_flatten($data);
+
+ # add sysparm_action
+ $data->{sysparm_action} = 'insert';
+
+ if ($logger->debugging) {
+ $logger->debug(to_json(ref($data) ? $data : from_json($data), 1));
+ }
+
+ # send to service-now
+ my $request = HTTP::Request->new(POST => $self->config->{service_now_url});
+ $request->content_type('application/json');
+ $request->content(to_json($data));
+ $request->authorization_basic($self->config->{service_now_user}, $self->config->{service_now_pass});
+
+ $self->{lwp} ||= LWP::UserAgent->new(agent => Bugzilla->params->{urlbase});
+ my $result = $self->{lwp}->request($request);
+
+ # http level errors
+ if (!$result->is_success) {
+ # treat these as transient
+ return (PUSH_RESULT_TRANSIENT, $result->status_line);
+ }
+
+ # empty response
+ if (length($result->content) == 0) {
+ # malformed request, treat as transient to allow code to fix
+ # may also be misconfiguration on servicenow, also transient
+ return (PUSH_RESULT_TRANSIENT, "Empty response");
+ }
+
+ # json errors
+ my $result_data;
+ eval {
+ $result_data = from_json($result->content);
+ };
+ if ($@) {
+ return (PUSH_RESULT_TRANSIENT, clean_error($@));
+ }
+ if ($logger->debugging) {
+ $logger->debug(to_json($result_data, 1));
+ }
+ if (exists $result_data->{error}) {
+ return (PUSH_RESULT_ERROR, $result_data->{error});
+ };
+
+ # malformed/unexpected json response
+ if (!exists $result_data->{records}
+ || ref($result_data->{records}) ne 'ARRAY'
+ || scalar(@{$result_data->{records}}) == 0
+ ) {
+ return (PUSH_RESULT_ERROR, "Malformed JSON response from ServiceNow: missing or empty 'records' array");
+ }
+
+ my $record = $result_data->{records}->[0];
+ if (ref($record) ne 'HASH') {
+ return (PUSH_RESULT_ERROR, "Malformed JSON response from ServiceNow: 'records' array does not contain an object");
+ }
+
+ # sys_id is the unique identifier for this action
+ if (!exists $record->{sys_id} || $record->{sys_id} eq '') {
+ return (PUSH_RESULT_ERROR, "Malformed JSON response from ServiceNow: 'records object' does not contain a valid sys_id");
+ }
+
+ # success
+ return (PUSH_RESULT_OK, "sys_id: " . $record->{sys_id});
+}
+
+sub _get_bug_data {
+ my ($self, $data) = @_;
+ my $target = $data->{event}->{target};
+ if ($target eq 'bug') {
+ return $data->{bug};
+ } elsif (exists $data->{$target}->{bug}) {
+ return $data->{$target}->{bug};
+ } else {
+ return;
+ }
+}
+
+sub _flatten {
+ # service-now expects a flat json object
+ my ($self, $data) = @_;
+
+ my $target = $data->{event}->{target};
+
+ # delete unnecessary deep objects
+ if ($target eq 'comment' || $target eq 'attachment') {
+ $data->{$target}->{bug_id} = $data->{$target}->{bug}->{id};
+ delete $data->{$target}->{bug};
+ }
+ delete $data->{event}->{changes};
+
+ $self->_flatten_hash($data, $data, 'u');
+}
+
+sub _flatten_hash {
+ my ($self, $base_hash, $hash, $prefix) = @_;
+ foreach my $key (keys %$hash) {
+ if (ref($hash->{$key}) eq 'HASH') {
+ $self->_flatten_hash($base_hash, $hash->{$key}, $prefix . "_$key");
+ } elsif (ref($hash->{$key}) ne 'ARRAY') {
+ $base_hash->{$prefix . "_$key"} = $hash->{$key};
+ }
+ delete $hash->{$key};
+ }
+}
+
+sub _add_ldap_logins {
+ my ($self, $rh, $cache) = @_;
+ if (exists $rh->{login}) {
+ my $login = $rh->{login};
+ $cache->{$login} ||= $self->_bmo_to_ldap($login);
+ Bugzilla->push_ext->logger->debug("BMO($login) --> LDAP(" . $cache->{$login} . ")");
+ $rh->{ldap} = $cache->{$login};
+ }
+ foreach my $key (keys %$rh) {
+ next unless ref($rh->{$key}) eq 'HASH';
+ $self->_add_ldap_logins($rh->{$key}, $cache);
+ }
+}
+
+sub _bmo_to_ldap {
+ my ($self, $login) = @_;
+ my $ldap = $self->_ldap_cache();
+
+ return '' unless $login =~ /\@mozilla\.(?:com|org)$/;
+
+ foreach my $check ($login, canon_email($login)) {
+ # check for matching bugmail entry
+ foreach my $mail (keys %$ldap) {
+ next unless $ldap->{$mail}{bugmail_canon} eq $check;
+ return $mail;
+ }
+
+ # check for matching mail
+ if (exists $ldap->{$check}) {
+ return $check;
+ }
+
+ # check for matching email alias
+ foreach my $mail (sort keys %$ldap) {
+ next unless grep { $check eq $_ } @{$ldap->{$mail}{aliases}};
+ return $mail;
+ }
+ }
+
+ return '';
+}
+
+sub _ldap_cache {
+ my ($self) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+ my $config = $self->config;
+
+ # cache of all ldap entries; updated infrequently
+ if (!$self->{ldap_cache_time} || (time) - $self->{ldap_cache_time} > $config->{ldap_poll} * 60) {
+ $logger->debug('refreshing LDAP cache');
+
+ my $cache = {};
+
+ my $host = $config->{ldap_host};
+ trick_taint($host);
+ my $scheme = lc($config->{ldap_scheme});
+ my $ldap = Net::LDAP->new($host, scheme => $scheme, onerror => 'die')
+ or die $!;
+ $ldap->bind($config->{ldap_user}, password => $config->{ldap_pass});
+ foreach my $ldap_base ('o=com,dc=mozilla', 'o=org,dc=mozilla') {
+ my $result = $ldap->search(
+ base => $ldap_base,
+ scope => 'sub',
+ filter => '(mail=*)',
+ attrs => ['mail', 'bugzillaEmail', 'emailAlias', 'cn', 'employeeType'],
+ );
+ foreach my $entry ($result->entries) {
+ my ($name, $bugMail, $mail, $type) =
+ map { $entry->get_value($_) || '' }
+ qw(cn bugzillaEmail mail employeeType);
+ next if $type eq 'DISABLED';
+ $mail = lc $mail;
+ $bugMail = '' if $bugMail !~ /\@/;
+ $bugMail = trim($bugMail);
+ if ($bugMail =~ / /) {
+ $bugMail = (grep { /\@/ } split / /, $bugMail)[0];
+ }
+ $name =~ s/\s+/ /g;
+ $cache->{$mail}{name} = trim($name);
+ $cache->{$mail}{bugmail} = $bugMail;
+ $cache->{$mail}{bugmail_canon} = canon_email($bugMail);
+ $cache->{$mail}{aliases} = [];
+ foreach my $alias (
+ @{$entry->get_value('emailAlias', asref => 1) || []}
+ ) {
+ push @{$cache->{$mail}{aliases}}, canon_email($alias);
+ }
+ }
+ }
+
+ $self->{ldap_cache} = $cache;
+ $self->{ldap_cache_time} = (time);
+ }
+
+ return $self->{ldap_cache};
+}
+
+1;
+
diff --git a/extensions/Push/lib/Connector/Base.pm b/extensions/Push/lib/Connector/Base.pm
new file mode 100644
index 000000000..290ea9740
--- /dev/null
+++ b/extensions/Push/lib/Connector/Base.pm
@@ -0,0 +1,106 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Connector::Base;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Extension::Push::Config;
+use Bugzilla::Extension::Push::BacklogMessage;
+use Bugzilla::Extension::Push::BacklogQueue;
+use Bugzilla::Extension::Push::Backoff;
+
+sub new {
+ my ($class) = @_;
+ my $self = {};
+ bless($self, $class);
+ ($self->{name}) = $class =~ /^.+:(.+)$/;
+ $self->init();
+ return $self;
+}
+
+sub name {
+ my $self = shift;
+ return $self->{name};
+}
+
+sub init {
+ my ($self) = @_;
+ # abstract
+ # perform any initialisation here
+ # will be run when created by the web pages or by the daemon
+ # and also when the configuration needs to be reloaded
+}
+
+sub stop {
+ my ($self) = @_;
+ # abstract
+ # run from the daemon only; disconnect from remote hosts, etc
+}
+
+sub should_send {
+ my ($self, $message) = @_;
+ # abstract
+ # return boolean indicating if the connector will be sending the message.
+ # this will be called each message, and should be a very quick simple test.
+ # the connector can perform a more exhaustive test in the send() method.
+ return 0;
+}
+
+sub send {
+ my ($self, $message) = @_;
+ # abstract
+ # deliver the message, daemon only
+}
+
+sub options {
+ my ($self) = @_;
+ # abstract
+ # return an array of configuration variables
+ return ();
+}
+
+sub options_validate {
+ my ($class, $config) = @_;
+ # abstract, static
+ # die if a combination of options in $config is invalid
+}
+
+#
+#
+#
+
+sub config {
+ my ($self) = @_;
+ if (!$self->{config}) {
+ $self->load_config();
+ }
+ return $self->{config};
+}
+
+sub load_config {
+ my ($self) = @_;
+ my $config = Bugzilla::Extension::Push::Config->new($self->name, $self->options);
+ $config->load();
+ $self->{config} = $config;
+}
+
+sub enabled {
+ my ($self) = @_;
+ return $self->config->{enabled} eq 'Enabled';
+}
+
+sub backlog {
+ my ($self) = @_;
+ $self->{backlog} ||= Bugzilla::Extension::Push::BacklogQueue->new($self->name);
+ return $self->{backlog};
+}
+
+1;
+
diff --git a/extensions/Push/lib/Connector/File.pm b/extensions/Push/lib/Connector/File.pm
new file mode 100644
index 000000000..2a8f4193d
--- /dev/null
+++ b/extensions/Push/lib/Connector/File.pm
@@ -0,0 +1,68 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Connector::File;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Extension::Push::Connector::Base';
+
+use Bugzilla::Constants;
+use Bugzilla::Extension::Push::Constants;
+use Bugzilla::Extension::Push::Util;
+use Encode;
+use FileHandle;
+
+sub init {
+ my ($self) = @_;
+}
+
+sub options {
+ return (
+ {
+ name => 'filename',
+ label => 'Filename',
+ type => 'string',
+ default => 'push.log',
+ required => 1,
+ validate => sub {
+ my $filename = shift;
+ $filename =~ m#^/#
+ && die "Absolute paths are not permitted\n";
+ },
+ },
+ );
+}
+
+sub should_send {
+ my ($self, $message) = @_;
+ return 1;
+}
+
+sub send {
+ my ($self, $message) = @_;
+
+ # pretty-format json payload
+ my $payload = $message->payload_decoded;
+ $payload = to_json($payload, 1);
+
+ my $filename = bz_locations()->{'datadir'} . '/' . $self->config->{filename};
+ Bugzilla->push_ext->logger->debug("File: Appending to $filename");
+ my $fh = FileHandle->new(">>$filename");
+ $fh->binmode(':utf8');
+ $fh->print(
+ "[" . scalar(localtime) . "]\n" .
+ $payload . "\n\n"
+ );
+ $fh->close;
+
+ return PUSH_RESULT_OK;
+}
+
+1;
+
diff --git a/extensions/Push/lib/Connector/ReviewBoard.pm b/extensions/Push/lib/Connector/ReviewBoard.pm
new file mode 100644
index 000000000..b5d1a9214
--- /dev/null
+++ b/extensions/Push/lib/Connector/ReviewBoard.pm
@@ -0,0 +1,187 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Connector::ReviewBoard;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Extension::Push::Connector::Base';
+
+use Bugzilla::Constants;
+use Bugzilla::Extension::Push::Constants;
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::Bug;
+use Bugzilla::Attachment;
+use Bugzilla::Extension::Push::Connector::ReviewBoard::Client;
+
+use JSON 'decode_json';
+use DateTime;
+use Scalar::Util 'blessed';
+
+use constant RB_CONTENT_TYPE => 'text/x-review-board-request';
+
+sub client {
+ my $self = shift;
+
+ $self->{client} //= Bugzilla::Extension::Push::Connector::ReviewBoard::Client->new(
+ base_uri => $self->config->{base_uri},
+ username => $self->config->{username},
+ password => $self->config->{password},
+ $self->config->{proxy} ? (proxy => $self->config->{proxy}) : (),
+ );
+
+ return $self->{client};
+}
+
+sub options {
+ return (
+ {
+ name => 'base_uri',
+ label => 'Base URI for ReviewBoard',
+ type => 'string',
+ default => 'https://reviewboard.allizom.org',
+ required => 1,
+ },
+ {
+ name => 'username',
+ label => 'Username',
+ type => 'string',
+ default => 'guest',
+ required => 1,
+ },
+ {
+ name => 'password',
+ label => 'Password',
+ type => 'password',
+ default => 'guest',
+ required => 1,
+ },
+ {
+ name => 'proxy',
+ label => 'Proxy',
+ type => 'string',
+ },
+ );
+}
+
+sub stop {
+ my ($self) = @_;
+}
+
+sub should_send {
+ my ($self, $message) = @_;
+
+ if ($message->routing_key =~ /^(?:attachment|bug)\.modify:.*\bis_private\b/) {
+ my $payload = $message->payload_decoded();
+ my $target = $payload->{event}->{target};
+
+ if ($target ne 'bug' && exists $payload->{$target}->{bug}) {
+ return 0 if $payload->{$target}->{bug}->{is_private};
+ return 0 if $payload->{$target}->{content_type} ne RB_CONTENT_TYPE;
+ }
+
+ return $payload->{$target}->{is_private} ? 1 : 0;
+ }
+ else {
+ # We're not interested in the message.
+ return 0;
+ }
+}
+
+sub send {
+ my ($self, $message) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+ my $config = $self->config;
+
+ eval {
+ my $payload = $message->payload_decoded();
+ my $target = $payload->{event}->{target};
+
+ if (my $method = $self->can("_process_$target")) {
+ $self->$method($payload->{$target});
+ }
+ };
+ if ($@) {
+ return (PUSH_RESULT_TRANSIENT, clean_error($@));
+ }
+
+ return PUSH_RESULT_OK;
+}
+
+sub _process_attachment {
+ my ($self, $payload_target) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+ my $attachment = blessed($payload_target)
+ ? $payload_target
+ : Bugzilla::Attachment->new({ id => $payload_target->{id}, cache => 1 });
+
+ if ($attachment) {
+ my $content = $attachment->data;
+ my $base_uri = quotemeta($self->config->{base_uri});
+ if (my ($id) = $content =~ m|$base_uri/r/([0-9]+)|) {
+ my $resp = $self->client->review_request->delete($id);
+ my $content = $resp->decoded_content;
+ my $status = $resp->code;
+ my $result = $content && decode_json($content) ;
+
+ if ($status == 204) {
+ # Success, review request deleted!
+ $logger->debug("Deleted review request $id");
+ }
+ elsif ($status == 404) {
+ # API error 100 - Does Not Exist
+ $logger->debug("Does Not Exist: Review Request $id does not exist");
+ }
+ elsif ($status == 403) {
+ # API error 101 - Permission Denied
+ $logger->error("Permission Denied: ReviewBoard Push Connector may be misconfigured");
+ die $result->{err}{msg};
+ }
+ elsif ($status == 401) {
+ # API error 103 - Not logged in
+ $logger->error("Not logged in: ReviewBoard Push Connector may be misconfigured");
+ die $result->{err}{msg};
+ }
+ else {
+ if ($result) {
+ my $code = $result->{err}{code};
+ my $msg = $result->{err}{msg};
+ $logger->error("Unexpected API Error: ($code) $msg");
+ die $msg;
+ }
+ else {
+ $logger->error("Unexpected HTTP Response $status");
+ die "HTTP Status: $status";
+ }
+ }
+ }
+ else {
+ $logger->error("Cannot find link: ReviewBoard Push Connector may be misconfigured");
+ die "Unable to find link in $content";
+ }
+ }
+ else {
+ $logger->error("Cannot find attachment with id = $payload_target->{id}");
+ }
+}
+
+sub _process_bug {
+ my ($self, $payload_target) = @_;
+
+ Bugzilla->set_user(Bugzilla::User->super_user);
+ my $bug = Bugzilla::Bug->new({ id => $payload_target->{id}, cache => 1 });
+ my @attachments = @{ $bug->attachments };
+ Bugzilla->logout;
+
+ foreach my $attachment (@attachments) {
+ next if $attachment->contenttype ne RB_CONTENT_TYPE;
+ $self->_process_attachment($attachment);
+ }
+}
+
+1;
diff --git a/extensions/Push/lib/Connector/ReviewBoard/Client.pm b/extensions/Push/lib/Connector/ReviewBoard/Client.pm
new file mode 100644
index 000000000..7ec4938d2
--- /dev/null
+++ b/extensions/Push/lib/Connector/ReviewBoard/Client.pm
@@ -0,0 +1,67 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Connector::ReviewBoard::Client;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Carp qw(croak);
+use LWP::UserAgent;
+use Scalar::Util qw(blessed);
+use URI;
+
+use Bugzilla::Extension::Push::Connector::ReviewBoard::ReviewRequest;
+
+sub new {
+ my ($class, %params) = @_;
+
+ croak "->new() is a class method" if blessed($class);
+ return bless(\%params, $class);
+}
+
+sub username { $_[0]->{username} }
+sub password { $_[0]->{password} }
+sub base_uri { $_[0]->{base_uri} }
+sub realm { $_[0]->{realm} // 'Web API' }
+sub proxy { $_[0]->{proxy} }
+
+sub _netloc {
+ my $self = shift;
+
+ my $uri = URI->new($self->base_uri);
+ return $uri->host . ':' . $uri->port;
+}
+
+sub useragent {
+ my $self = shift;
+
+ unless ($self->{useragent}) {
+ my $ua = LWP::UserAgent->new(agent => Bugzilla->params->{urlbase});
+ $ua->credentials(
+ $self->_netloc,
+ $self->realm,
+ $self->username,
+ $self->password,
+ );
+ $ua->proxy('https', $self->proxy) if $self->proxy;
+ $ua->timeout(10);
+
+ $self->{useragent} = $ua;
+ }
+
+ return $self->{useragent};
+}
+
+sub review_request {
+ my $self = shift;
+
+ return Bugzilla::Extension::Push::Connector::ReviewBoard::ReviewRequest->new(client => $self, @_);
+}
+
+1;
diff --git a/extensions/Push/lib/Connector/ReviewBoard/Resource.pm b/extensions/Push/lib/Connector/ReviewBoard/Resource.pm
new file mode 100644
index 000000000..3f8d434ce
--- /dev/null
+++ b/extensions/Push/lib/Connector/ReviewBoard/Resource.pm
@@ -0,0 +1,38 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Connector::ReviewBoard::Resource;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use URI;
+use Carp qw(croak confess);
+use Scalar::Util qw(blessed);
+
+sub new {
+ my ($class, %params) = @_;
+
+ croak "->new() is a class method" if blessed($class);
+ return bless(\%params, $class);
+}
+
+sub client { $_[0]->{client} }
+
+sub path { confess 'Unimplemented'; }
+
+sub uri {
+ my ($self, @path) = @_;
+
+ my $uri = URI->new($self->client->base_uri);
+ $uri->path(join('/', $self->path, @path) . '/');
+
+ return $uri;
+}
+
+1;
diff --git a/extensions/Push/lib/Connector/ReviewBoard/ReviewRequest.pm b/extensions/Push/lib/Connector/ReviewBoard/ReviewRequest.pm
new file mode 100644
index 000000000..32bebfbe8
--- /dev/null
+++ b/extensions/Push/lib/Connector/ReviewBoard/ReviewRequest.pm
@@ -0,0 +1,28 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Connector::ReviewBoard::ReviewRequest;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use base 'Bugzilla::Extension::Push::Connector::ReviewBoard::Resource';
+
+# Reference: http://www.reviewboard.org/docs/manual/dev/webapi/2.0/resources/review-request/
+
+sub path {
+ return '/api/review-requests';
+}
+
+sub delete {
+ my ($self, $id) = @_;
+
+ return $self->client->useragent->delete($self->uri($id));
+}
+
+1;
diff --git a/extensions/Push/lib/Connector/TCL.pm b/extensions/Push/lib/Connector/TCL.pm
new file mode 100644
index 000000000..16ebb0319
--- /dev/null
+++ b/extensions/Push/lib/Connector/TCL.pm
@@ -0,0 +1,352 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Connector::TCL;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Extension::Push::Connector::Base';
+
+use Bugzilla::Constants;
+use Bugzilla::Extension::Push::Constants;
+use Bugzilla::Extension::Push::Serialise;
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::User;
+use Bugzilla::Attachment;
+
+use Digest::MD5 qw(md5_hex);
+use Encode qw(encode_utf8);
+
+sub options {
+ return (
+ {
+ name => 'tcl_user',
+ label => 'Bugzilla TCL User',
+ type => 'string',
+ default => 'tcl@bugzilla.tld',
+ required => 1,
+ validate => sub {
+ Bugzilla::User->new({ name => $_[0] })
+ || die "Invalid Bugzilla user ($_[0])\n";
+ },
+ },
+ {
+ name => 'sftp_host',
+ label => 'SFTP Host',
+ type => 'string',
+ default => '',
+ required => 1,
+ },
+ {
+ name => 'sftp_port',
+ label => 'SFTP Port',
+ type => 'string',
+ default => '22',
+ required => 1,
+ validate => sub {
+ $_[0] =~ /\D/ && die "SFTP Port must be an integer\n";
+ },
+ },
+ {
+ name => 'sftp_user',
+ label => 'SFTP Username',
+ type => 'string',
+ default => '',
+ required => 1,
+ },
+ {
+ name => 'sftp_pass',
+ label => 'SFTP Password',
+ type => 'password',
+ default => '',
+ required => 1,
+ },
+ {
+ name => 'sftp_remote_path',
+ label => 'SFTP Remote Path',
+ type => 'string',
+ default => '',
+ required => 0,
+ },
+ );
+}
+
+my $_instance;
+
+sub init {
+ my ($self) = @_;
+ $_instance = $self;
+}
+
+sub load_config {
+ my ($self) = @_;
+ $self->SUPER::load_config(@_);
+}
+
+sub should_send {
+ my ($self, $message) = @_;
+
+ my $data = $message->payload_decoded;
+ my $bug_data = $self->_get_bug_data($data)
+ || return 0;
+
+ # sanity check user
+ $self->{tcl_user} ||= Bugzilla::User->new({ name => $self->config->{tcl_user} });
+ if (!$self->{tcl_user} || !$self->{tcl_user}->is_enabled) {
+ return 0;
+ }
+
+ # only send bugs created by the tcl user
+ unless ($bug_data->{reporter}->{id} == $self->{tcl_user}->id) {
+ return 0;
+ }
+
+ # don't push changes made by the tcl user
+ if ($data->{event}->{user}->{id} == $self->{tcl_user}->id) {
+ return 0;
+ }
+
+ # send comments
+ if ($data->{event}->{routing_key} eq 'comment.create') {
+ return 0 if $data->{comment}->{is_private};
+ return 1;
+ }
+
+ # send status and resolution updates
+ foreach my $change (@{ $data->{event}->{changes} }) {
+ return 1 if $change->{field} eq 'bug_status'
+ || $change->{field} eq 'resolution'
+ || $change->{field} eq 'cf_blocking_b2g';
+ }
+
+ # send attachments
+ if ($data->{event}->{routing_key} =~ /^attachment\./) {
+ return 0 if $data->{attachment}->{is_private};
+ return 1;
+ }
+
+ # and nothing else
+ return 0;
+}
+
+sub send {
+ my ($self, $message) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+ my $config = $self->config;
+
+ require XML::Simple;
+ require Net::SFTP;
+
+ $self->{tcl_user} ||= Bugzilla::User->new({ name => $self->config->{tcl_user} });
+ if (!$self->{tcl_user}) {
+ return (PUSH_RESULT_TRANSIENT, "Invalid bugzilla-user (" . $self->config->{tcl_user} . ")");
+ }
+
+ # load the bug
+ my $data = $message->payload_decoded;
+ my $bug_data = $self->_get_bug_data($data);
+
+ # build payload
+ my $attachment;
+ my %xml = (
+ Mozilla_ID => $bug_data->{id},
+ When => $data->{event}->{time},
+ Who => $data->{event}->{user}->{login},
+ Status => $bug_data->{status}->{name},
+ Resolution => $bug_data->{resolution},
+ Blocking_B2G => $bug_data->{cf_blocking_b2g},
+ );
+ if ($data->{event}->{routing_key} eq 'comment.create') {
+ $xml{Comment} = $data->{comment}->{body};
+ } elsif ($data->{event}->{routing_key} =~ /^attachment\.(\w+)/) {
+ my $is_update = $1 eq 'modify';
+ if (!$is_update) {
+ $attachment = Bugzilla::Attachment->new($data->{attachment}->{id});
+ }
+ $xml{Attach} = {
+ Attach_ID => $data->{attachment}->{id},
+ Filename => $data->{attachment}->{file_name},
+ Description => $data->{attachment}->{description},
+ ContentType => $data->{attachment}->{content_type},
+ IsPatch => $data->{attachment}->{is_patch} ? 'true' : 'false',
+ IsObsolete => $data->{attachment}->{is_obsolete} ? 'true' : 'false',
+ IsUpdate => $is_update ? 'true' : 'false',
+ };
+ }
+
+ # convert to xml
+ my $xml = XML::Simple::XMLout(
+ \%xml,
+ NoAttr => 1,
+ RootName => 'sync',
+ XMLDecl => 1,
+ );
+ $xml = encode_utf8($xml);
+
+ # generate md5
+ my $md5 = md5_hex($xml);
+
+ # build filename
+ my ($sec, $min, $hour, $day, $mon, $year) = localtime(time);
+ my $change_set = $data->{event}->{change_set};
+ $change_set =~ s/\.//g;
+ my $filename = sprintf(
+ '%04s%02d%02d%02d%02d%02d%s',
+ $year + 1900,
+ $mon + 1,
+ $day,
+ $hour,
+ $min,
+ $sec,
+ $change_set,
+ );
+
+ # create temp files;
+ my $temp_dir = File::Temp::Directory->new();
+ my $local_dir = $temp_dir->dirname;
+ _write_file("$local_dir/$filename.sync", $xml);
+ _write_file("$local_dir/$filename.sync.check", $md5);
+ _write_file("$local_dir/$filename.done", '');
+ if ($attachment) {
+ _write_file("$local_dir/$filename.sync.attach", $attachment->data);
+ }
+
+ my $remote_dir = $self->config->{sftp_remote_path} eq ''
+ ? ''
+ : $self->config->{sftp_remote_path} . '/';
+
+ # send files via sftp
+ $logger->debug("Connecting to " . $self->config->{sftp_host} . ":" . $self->config->{sftp_port});
+ my $sftp = Net::SFTP->new(
+ $self->config->{sftp_host},
+ ssh_args => {
+ port => $self->config->{sftp_port},
+ },
+ user => $self->config->{sftp_user},
+ password => $self->config->{sftp_pass},
+ );
+
+ $logger->debug("Uploading $local_dir/$filename.sync");
+ $sftp->put("$local_dir/$filename.sync", "$remote_dir$filename.sync")
+ or return (PUSH_RESULT_ERROR, "Failed to upload $local_dir/$filename.sync");
+
+ $logger->debug("Uploading $local_dir/$filename.sync.check");
+ $sftp->put("$local_dir/$filename.sync.check", "$remote_dir$filename.sync.check")
+ or return (PUSH_RESULT_ERROR, "Failed to upload $local_dir/$filename.sync.check");
+
+ if ($attachment) {
+ $logger->debug("Uploading $local_dir/$filename.sync.attach");
+ $sftp->put("$local_dir/$filename.sync.attach", "$remote_dir$filename.sync.attach")
+ or return (PUSH_RESULT_ERROR, "Failed to upload $local_dir/$filename.sync.attach");
+ }
+
+ $logger->debug("Uploading $local_dir/$filename.done");
+ $sftp->put("$local_dir/$filename.done", "$remote_dir$filename.done")
+ or return (PUSH_RESULT_ERROR, "Failed to upload $local_dir/$filename.done");
+
+ # success
+ return (PUSH_RESULT_OK, "uploaded $filename.sync");
+}
+
+sub _get_bug_data {
+ my ($self, $data) = @_;
+ my $target = $data->{event}->{target};
+ if ($target eq 'bug') {
+ return $data->{bug};
+ } elsif (exists $data->{$target}->{bug}) {
+ return $data->{$target}->{bug};
+ } else {
+ return;
+ }
+}
+
+sub _write_file {
+ my ($filename, $content) = @_;
+ open(my $fh, ">$filename") or die "Failed to write to $filename: $!\n";
+ binmode($fh);
+ print $fh $content;
+ close($fh) or die "Failed to write to $filename: $!\n";
+}
+
+1;
+
+# File::Temp->newdir() requires a newer version of File::Temp than we have on
+# production, so here's a small inline package which performs the same task.
+
+package File::Temp::Directory;
+
+use strict;
+use warnings;
+
+use File::Temp;
+use File::Path qw(rmtree);
+use File::Spec;
+
+my @chars;
+
+sub new {
+ my ($class) = @_;
+ my $self = {};
+ bless($self, $class);
+
+ @chars = qw/ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
+ a b c d e f g h i j k l m n o p q r s t u v w x y z
+ 0 1 2 3 4 5 6 7 8 9 _
+ /;
+
+ $self->{TEMPLATE} = File::Spec->catdir(File::Spec->tmpdir, 'X' x 10);
+ $self->{DIRNAME} = $self->_mktemp();
+ return $self;
+}
+
+sub _mktemp {
+ my ($self) = @_;
+ my $path = $self->_random_name();
+ while(1) {
+ if (mkdir($path, 0700)) {
+ # in case of odd umask
+ chmod(0700, $path);
+ return $path;
+ } else {
+ # abort with error if the reason for failure was anything except eexist
+ die "Could not create directory $path: $!\n" unless ($!{EEXIST});
+ # loop round for another try
+ }
+ $path = $self->_random_name();
+ }
+
+ return $path;
+}
+
+sub _random_name {
+ my ($self) = @_;
+ my $path = $self->{TEMPLATE};
+ $path =~ s/X/$chars[int(rand(@chars))]/ge;
+ return $path;
+}
+
+sub dirname {
+ my ($self) = @_;
+ return $self->{DIRNAME};
+}
+
+sub DESTROY {
+ my ($self) = @_;
+ local($., $@, $!, $^E, $?);
+ if (-d $self->{DIRNAME}) {
+ # Some versions of rmtree will abort if you attempt to remove the
+ # directory you are sitting in. We protect that and turn it into a
+ # warning. We do this because this occurs during object destruction and
+ # so can not be caught by the user.
+ eval { rmtree($self->{DIRNAME}, 0, 0); };
+ warn $@ if ($@ && $^W);
+ }
+}
+
+1;
+
diff --git a/extensions/Push/lib/Connectors.pm b/extensions/Push/lib/Connectors.pm
new file mode 100644
index 000000000..026d3f7f1
--- /dev/null
+++ b/extensions/Push/lib/Connectors.pm
@@ -0,0 +1,116 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Connectors;
+
+use strict;
+use warnings;
+
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::Constants;
+use Bugzilla::Util qw(trick_taint);
+use File::Basename;
+
+sub new {
+ my ($class) = @_;
+ my $self = {};
+ bless($self, $class);
+
+ $self->{names} = [];
+ $self->{objects} = {};
+ $self->{path} = bz_locations->{'extensionsdir'} . '/Push/lib/Connector';
+
+ my $logger = Bugzilla->push_ext->logger;
+ foreach my $file (glob($self->{path} . '/*.pm')) {
+ my $name = basename($file);
+ $name =~ s/\.pm$//;
+ next if $name eq 'Base';
+ if (length($name) > 32) {
+ $logger->info("Ignoring connector '$name': Name longer than 32 characters");
+ }
+ push @{$self->{names}}, $name;
+ $logger->debug("Found connector '$name'");
+ }
+
+ return $self;
+}
+
+sub _load {
+ my ($self) = @_;
+ return if scalar keys %{$self->{objects}};
+
+ my $logger = Bugzilla->push_ext->logger;
+ foreach my $name (@{$self->{names}}) {
+ next if exists $self->{objects}->{$name};
+ my $file = $self->{path} . "/$name.pm";
+ trick_taint($file);
+ require $file;
+ my $package = "Bugzilla::Extension::Push::Connector::$name";
+
+ $logger->debug("Loading connector '$name'");
+ my $old_error_mode = Bugzilla->error_mode;
+ Bugzilla->error_mode(ERROR_MODE_DIE);
+ eval {
+ my $connector = $package->new();
+ $connector->load_config();
+ $self->{objects}->{$name} = $connector;
+ };
+ if ($@) {
+ $logger->error("Connector '$name' failed to load: " . clean_error($@));
+ }
+ Bugzilla->error_mode($old_error_mode);
+ }
+}
+
+sub stop {
+ my ($self) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+ foreach my $connector ($self->list) {
+ next unless $connector->enabled;
+ $logger->debug("Stopping '" . $connector->name . "'");
+ eval {
+ $connector->stop();
+ };
+ if ($@) {
+ $logger->error("Connector '" . $connector->name . "' failed to stop: " . clean_error($@));
+ $logger->debug("Connector '" . $connector->name . "' failed to stop: $@");
+ }
+ }
+}
+
+sub reload {
+ my ($self) = @_;
+ $self->stop();
+ $self->{objects} = {};
+ $self->_load();
+}
+
+sub names {
+ my ($self) = @_;
+ return @{$self->{names}};
+}
+
+sub list {
+ my ($self) = @_;
+ $self->_load();
+ return sort { $a->name cmp $b->name } values %{$self->{objects}};
+}
+
+sub exists {
+ my ($self, $name) = @_;
+ $self->by_name($name) ? 1 : 0;
+}
+
+sub by_name {
+ my ($self, $name) = @_;
+ $self->_load();
+ return unless exists $self->{objects}->{$name};
+ return $self->{objects}->{$name};
+}
+
+1;
+
diff --git a/extensions/Push/lib/Constants.pm b/extensions/Push/lib/Constants.pm
new file mode 100644
index 000000000..18b12d511
--- /dev/null
+++ b/extensions/Push/lib/Constants.pm
@@ -0,0 +1,41 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Constants;
+
+use strict;
+use base 'Exporter';
+
+our @EXPORT = qw(
+ PUSH_RESULT_OK
+ PUSH_RESULT_IGNORED
+ PUSH_RESULT_TRANSIENT
+ PUSH_RESULT_ERROR
+ PUSH_RESULT_UNKNOWN
+ push_result_to_string
+
+ POLL_INTERVAL_SECONDS
+);
+
+use constant PUSH_RESULT_OK => 1;
+use constant PUSH_RESULT_IGNORED => 2;
+use constant PUSH_RESULT_TRANSIENT => 3;
+use constant PUSH_RESULT_ERROR => 4;
+use constant PUSH_RESULT_UNKNOWN => 5;
+
+sub push_result_to_string {
+ my ($result) = @_;
+ return 'OK' if $result == PUSH_RESULT_OK;
+ return 'OK-IGNORED' if $result == PUSH_RESULT_IGNORED;
+ return 'TRANSIENT-ERROR' if $result == PUSH_RESULT_TRANSIENT;
+ return 'FATAL-ERROR' if $result == PUSH_RESULT_ERROR;
+ return 'UNKNOWN' if $result == PUSH_RESULT_UNKNOWN;
+}
+
+use constant POLL_INTERVAL_SECONDS => 30;
+
+1;
diff --git a/extensions/Push/lib/Daemon.pm b/extensions/Push/lib/Daemon.pm
new file mode 100644
index 000000000..66e15783e
--- /dev/null
+++ b/extensions/Push/lib/Daemon.pm
@@ -0,0 +1,96 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Daemon;
+
+use strict;
+use warnings;
+
+use Bugzilla::Constants;
+use Bugzilla::Extension::Push::Push;
+use Bugzilla::Extension::Push::Logger;
+use Carp qw(confess);
+use Daemon::Generic;
+use File::Basename;
+use Pod::Usage;
+
+sub start {
+ newdaemon();
+}
+
+#
+# daemon::generic config
+#
+
+sub gd_preconfig {
+ my $self = shift;
+ my $pidfile = $self->{gd_args}{pidfile};
+ if (!$pidfile) {
+ $pidfile = bz_locations()->{datadir} . '/' . $self->{gd_progname} . ".pid";
+ }
+ return (pidfile => $pidfile);
+}
+
+sub gd_getopt {
+ my $self = shift;
+ $self->SUPER::gd_getopt();
+ if ($self->{gd_args}{progname}) {
+ $self->{gd_progname} = $self->{gd_args}{progname};
+ } else {
+ $self->{gd_progname} = basename($0);
+ }
+ $self->{_original_zero} = $0;
+ $0 = $self->{gd_progname};
+}
+
+sub gd_postconfig {
+ my $self = shift;
+ $0 = delete $self->{_original_zero};
+}
+
+sub gd_more_opt {
+ my $self = shift;
+ return (
+ 'pidfile=s' => \$self->{gd_args}{pidfile},
+ 'n=s' => \$self->{gd_args}{progname},
+ );
+}
+
+sub gd_usage {
+ pod2usage({ -verbose => 0, -exitval => 'NOEXIT' });
+ return 0;
+};
+
+sub gd_redirect_output {
+ my $self = shift;
+
+ my $filename = bz_locations()->{datadir} . '/' . $self->{gd_progname} . ".log";
+ open(STDERR, ">>$filename") or (print "could not open stderr: $!" && exit(1));
+ close(STDOUT);
+ open(STDOUT, ">&STDERR") or die "redirect STDOUT -> STDERR: $!";
+ $SIG{HUP} = sub {
+ close(STDERR);
+ open(STDERR, ">>$filename") or (print "could not open stderr: $!" && exit(1));
+ };
+}
+
+sub gd_setup_signals {
+ my $self = shift;
+ $self->SUPER::gd_setup_signals();
+ $SIG{TERM} = sub { $self->gd_quit_event(); }
+}
+
+sub gd_run {
+ my $self = shift;
+ $::SIG{__DIE__} = \&Carp::confess if $self->{debug};
+ my $push = Bugzilla->push_ext;
+ $push->logger->{debug} = $self->{debug};
+ $push->is_daemon(1);
+ $push->start();
+}
+
+1;
diff --git a/extensions/Push/lib/Log.pm b/extensions/Push/lib/Log.pm
new file mode 100644
index 000000000..6faabea97
--- /dev/null
+++ b/extensions/Push/lib/Log.pm
@@ -0,0 +1,45 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Log;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Extension::Push::Message;
+
+sub new {
+ my ($class) = @_;
+ my $self = {};
+ bless($self, $class);
+ return $self;
+}
+
+sub count {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ return $dbh->selectrow_array("SELECT COUNT(*) FROM push_log");
+}
+
+sub list {
+ my ($self, %args) = @_;
+ $args{limit} ||= 10;
+ $args{filter} ||= '';
+ my @result;
+ my $dbh = Bugzilla->dbh;
+
+ my $ids = $dbh->selectcol_arrayref("
+ SELECT id
+ FROM push_log
+ ORDER BY processed_ts DESC " .
+ $dbh->sql_limit(100)
+ );
+ return Bugzilla::Extension::Push::LogEntry->new_from_list($ids);
+}
+
+1;
diff --git a/extensions/Push/lib/LogEntry.pm b/extensions/Push/lib/LogEntry.pm
new file mode 100644
index 000000000..848df0480
--- /dev/null
+++ b/extensions/Push/lib/LogEntry.pm
@@ -0,0 +1,71 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::LogEntry;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Object';
+
+use constant AUDIT_CREATES => 0;
+use constant AUDIT_UPDATES => 0;
+use constant AUDIT_REMOVES => 0;
+use constant USE_MEMCACHED => 0;
+
+use Bugzilla;
+use Bugzilla::Error;
+use Bugzilla::Extension::Push::Constants;
+
+#
+# initialisation
+#
+
+use constant DB_TABLE => 'push_log';
+use constant DB_COLUMNS => qw(
+ id
+ message_id
+ change_set
+ routing_key
+ connector
+ push_ts
+ processed_ts
+ result
+ data
+);
+use constant VALIDATORS => {
+ data => \&_check_data,
+};
+use constant NAME_FIELD => '';
+use constant LIST_ORDER => 'processed_ts DESC';
+
+#
+# accessors
+#
+
+sub message_id { return $_[0]->{'message_id'}; }
+sub change_set { return $_[0]->{'change_set'}; }
+sub routing_key { return $_[0]->{'routing_key'}; }
+sub connector { return $_[0]->{'connector'}; }
+sub push_ts { return $_[0]->{'push_ts'}; }
+sub processed_ts { return $_[0]->{'processed_ts'}; }
+sub result { return $_[0]->{'result'}; }
+sub data { return $_[0]->{'data'}; }
+
+sub result_string { return push_result_to_string($_[0]->result) }
+
+#
+# validators
+#
+
+sub _check_data {
+ my ($invocant, $value) = @_;
+ return $value eq '' ? undef : $value;
+}
+
+1;
+
diff --git a/extensions/Push/lib/Logger.pm b/extensions/Push/lib/Logger.pm
new file mode 100644
index 000000000..68cec1e69
--- /dev/null
+++ b/extensions/Push/lib/Logger.pm
@@ -0,0 +1,70 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Logger;
+
+use strict;
+use warnings;
+
+use Apache2::Log;
+use Bugzilla::Extension::Push::Constants;
+use Bugzilla::Extension::Push::LogEntry;
+
+sub new {
+ my ($class) = @_;
+ my $self = {};
+ bless($self, $class);
+ return $self;
+}
+
+sub info { shift->_log_it('INFO', @_) }
+sub error { shift->_log_it('ERROR', @_) }
+sub debug { shift->_log_it('DEBUG', @_) }
+
+sub debugging {
+ my ($self) = @_;
+ return $self->{debug};
+}
+
+sub _log_it {
+ my ($self, $method, $message) = @_;
+ return if $method eq 'DEBUG' && !$self->debugging;
+ chomp $message;
+ if ($ENV{MOD_PERL}) {
+ Apache2::ServerRec::warn("Push $method: $message");
+ } elsif ($ENV{SCRIPT_FILENAME}) {
+ print STDERR "Push $method: $message\n";
+ } else {
+ print STDERR '[' . localtime(time) ."] $method: $message\n";
+ }
+}
+
+sub result {
+ my ($self, $connector, $message, $result, $data) = @_;
+ $data ||= '';
+
+ $self->info(sprintf(
+ "%s: Message #%s: %s %s",
+ $connector->name,
+ $message->message_id,
+ push_result_to_string($result),
+ $data
+ ));
+
+ Bugzilla::Extension::Push::LogEntry->create({
+ message_id => $message->message_id,
+ change_set => $message->change_set,
+ routing_key => $message->routing_key,
+ connector => $connector->name,
+ push_ts => $message->push_ts,
+ processed_ts => Bugzilla->dbh->selectrow_array('SELECT NOW()'),
+ result => $result,
+ data => $data,
+ });
+}
+
+1;
diff --git a/extensions/Push/lib/Message.pm b/extensions/Push/lib/Message.pm
new file mode 100644
index 000000000..6d2ed2531
--- /dev/null
+++ b/extensions/Push/lib/Message.pm
@@ -0,0 +1,104 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Message;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Object';
+
+use constant AUDIT_CREATES => 0;
+use constant AUDIT_UPDATES => 0;
+use constant AUDIT_REMOVES => 0;
+use constant USE_MEMCACHED => 0;
+
+use Bugzilla;
+use Bugzilla::Error;
+use Bugzilla::Extension::Push::Util;
+use Encode;
+
+#
+# initialisation
+#
+
+use constant DB_TABLE => 'push';
+use constant DB_COLUMNS => qw(
+ id
+ push_ts
+ payload
+ change_set
+ routing_key
+);
+use constant LIST_ORDER => 'push_ts';
+use constant VALIDATORS => {
+ push_ts => \&_check_push_ts,
+ payload => \&_check_payload,
+ change_set => \&_check_change_set,
+ routing_key => \&_check_routing_key,
+};
+
+# this creates an object which doesn't exist on the database
+sub new_transient {
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+ my $object = shift;
+ bless($object, $class) if $object;
+ return $object;
+}
+
+# take a transient object and commit
+sub create_from_transient {
+ my ($self) = @_;
+ return $self->create($self);
+}
+
+#
+# accessors
+#
+
+sub push_ts { return $_[0]->{'push_ts'}; }
+sub payload { return $_[0]->{'payload'}; }
+sub change_set { return $_[0]->{'change_set'}; }
+sub routing_key { return $_[0]->{'routing_key'}; }
+sub message_id { return $_[0]->id; }
+
+sub payload_decoded {
+ my ($self) = @_;
+ return from_json($self->{'payload'});
+}
+
+#
+# validators
+#
+
+sub _check_push_ts {
+ my ($invocant, $value) = @_;
+ $value ||= Bugzilla->dbh->selectrow_array('SELECT NOW()');
+ return $value;
+}
+
+sub _check_payload {
+ my ($invocant, $value) = @_;
+ length($value) || ThrowCodeError('push_invalid_payload');
+ return $value;
+}
+
+sub _check_change_set {
+ my ($invocant, $value) = @_;
+ (defined($value) && length($value)) || ThrowCodeError('push_invalid_change_set');
+ return $value;
+}
+
+sub _check_routing_key {
+ my ($invocant, $value) = @_;
+ (defined($value) && length($value)) || ThrowCodeError('push_invalid_routing_key');
+ return $value;
+}
+
+1;
+
diff --git a/extensions/Push/lib/Option.pm b/extensions/Push/lib/Option.pm
new file mode 100644
index 000000000..25d529f98
--- /dev/null
+++ b/extensions/Push/lib/Option.pm
@@ -0,0 +1,66 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Option;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Object';
+
+use Bugzilla;
+use Bugzilla::Error;
+use Bugzilla::Util;
+
+#
+# initialisation
+#
+
+use constant DB_TABLE => 'push_options';
+use constant DB_COLUMNS => qw(
+ id
+ connector
+ option_name
+ option_value
+);
+use constant UPDATE_COLUMNS => qw(
+ option_value
+);
+use constant VALIDATORS => {
+ connector => \&_check_connector,
+};
+use constant LIST_ORDER => 'connector';
+
+#
+# accessors
+#
+
+sub connector { return $_[0]->{'connector'}; }
+sub name { return $_[0]->{'option_name'}; }
+sub value { return $_[0]->{'option_value'}; }
+
+#
+# mutators
+#
+
+sub set_value { $_[0]->{'option_value'} = $_[1]; }
+
+#
+# validators
+#
+
+sub _check_connector {
+ my ($invocant, $value) = @_;
+ $value eq '*'
+ || $value eq 'global'
+ || Bugzilla->push_ext->connectors->exists($value)
+ || ThrowCodeError('push_invalid_connector');
+ return $value;
+}
+
+1;
+
diff --git a/extensions/Push/lib/Push.pm b/extensions/Push/lib/Push.pm
new file mode 100644
index 000000000..aaac0bbd6
--- /dev/null
+++ b/extensions/Push/lib/Push.pm
@@ -0,0 +1,264 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Push;
+
+use strict;
+use warnings;
+
+use Bugzilla::Extension::Push::BacklogMessage;
+use Bugzilla::Extension::Push::Config;
+use Bugzilla::Extension::Push::Connectors;
+use Bugzilla::Extension::Push::Constants;
+use Bugzilla::Extension::Push::Log;
+use Bugzilla::Extension::Push::Logger;
+use Bugzilla::Extension::Push::Message;
+use Bugzilla::Extension::Push::Option;
+use Bugzilla::Extension::Push::Queue;
+use Bugzilla::Extension::Push::Util;
+use DateTime;
+
+sub new {
+ my ($class) = @_;
+ my $self = {};
+ bless($self, $class);
+ $self->{is_daemon} = 0;
+ return $self;
+}
+
+sub is_daemon {
+ my ($self, $value) = @_;
+ if (defined $value) {
+ $self->{is_daemon} = $value ? 1 : 0;
+ }
+ return $self->{is_daemon};
+}
+
+sub start {
+ my ($self) = @_;
+ my $connectors = $self->connectors;
+ $self->{config_last_modified} = $self->get_config_last_modified();
+ $self->{config_last_checked} = (time);
+
+ foreach my $connector ($connectors->list) {
+ $connector->backlog->reset_backoff();
+ }
+
+ while(1) {
+ if ($self->_dbh_check()) {
+ $self->_reload();
+ $self->push();
+ }
+ sleep(POLL_INTERVAL_SECONDS);
+ }
+}
+
+sub push {
+ my ($self) = @_;
+ my $logger = $self->logger;
+ my $connectors = $self->connectors;
+
+ my $enabled = 0;
+ foreach my $connector ($connectors->list) {
+ if ($connector->enabled) {
+ $enabled = 1;
+ last;
+ }
+ }
+ return unless $enabled;
+
+ $logger->debug("polling");
+
+ # process each message
+ while(my $message = $self->queue->oldest) {
+ foreach my $connector ($connectors->list) {
+ next unless $connector->enabled;
+ next unless $connector->should_send($message);
+ $logger->debug("pushing to " . $connector->name);
+
+ my $is_backlogged = $connector->backlog->count;
+
+ if (!$is_backlogged) {
+ # connector isn't backlogged, immediate send
+ $logger->debug("immediate send");
+ my ($result, $data);
+ eval {
+ ($result, $data) = $connector->send($message);
+ };
+ if ($@) {
+ $result = PUSH_RESULT_TRANSIENT;
+ $data = clean_error($@);
+ }
+ if (!$result) {
+ $logger->error($connector->name . " failed to return a result code");
+ $result = PUSH_RESULT_UNKNOWN;
+ }
+ $logger->result($connector, $message, $result, $data);
+
+ if ($result == PUSH_RESULT_TRANSIENT) {
+ $is_backlogged = 1;
+ }
+ }
+
+ # if the connector is backlogged, push to the backlog queue
+ if ($is_backlogged) {
+ $logger->debug("backlogged");
+ my $backlog = Bugzilla::Extension::Push::BacklogMessage->create_from_message($message, $connector);
+ }
+ }
+
+ # message processed
+ $message->remove_from_db();
+ }
+
+ # process backlog
+ foreach my $connector ($connectors->list) {
+ next unless $connector->enabled;
+ my $message = $connector->backlog->oldest();
+ next unless $message;
+
+ $logger->debug("processing backlog for " . $connector->name);
+ while ($message) {
+ my ($result, $data);
+ eval {
+ ($result, $data) = $connector->send($message);
+ };
+ if ($@) {
+ $result = PUSH_RESULT_TRANSIENT;
+ $data = $@;
+ }
+ $message->inc_attempts($result == PUSH_RESULT_OK ? '' : $data);
+ if (!$result) {
+ $logger->error($connector->name . " failed to return a result code");
+ $result = PUSH_RESULT_UNKNOWN;
+ }
+ $logger->result($connector, $message, $result, $data);
+
+ if ($result == PUSH_RESULT_TRANSIENT) {
+ # connector is still down, stop trying
+ $connector->backlog->inc_backoff();
+ last;
+ }
+
+ # message was processed
+ $message->remove_from_db();
+
+ $message = $connector->backlog->oldest();
+ }
+ }
+}
+
+sub _reload {
+ my ($self) = @_;
+
+ # check for updated config every 60 seconds
+ my $now = (time);
+ if ($now - $self->{config_last_checked} < 60) {
+ return;
+ }
+ $self->{config_last_checked} = $now;
+
+ $self->logger->debug('Checking for updated configuration');
+ if ($self->get_config_last_modified eq $self->{config_last_modified}) {
+ return;
+ }
+ $self->{config_last_modified} = $self->get_config_last_modified();
+
+ $self->logger->debug('Configuration has been updated');
+ $self->connectors->reload();
+}
+
+sub get_config_last_modified {
+ my ($self) = @_;
+ my $options_list = Bugzilla::Extension::Push::Option->match({
+ connector => '*',
+ option_name => 'last-modified',
+ });
+ if (@$options_list) {
+ return $options_list->[0]->value;
+ } else {
+ return $self->set_config_last_modified();
+ }
+}
+
+sub set_config_last_modified {
+ my ($self) = @_;
+ my $options_list = Bugzilla::Extension::Push::Option->match({
+ connector => '*',
+ option_name => 'last-modified',
+ });
+ my $now = DateTime->now->datetime();
+ if (@$options_list) {
+ $options_list->[0]->set_value($now);
+ $options_list->[0]->update();
+ } else {
+ Bugzilla::Extension::Push::Option->create({
+ connector => '*',
+ option_name => 'last-modified',
+ option_value => $now,
+ });
+ }
+ return $now;
+}
+
+sub config {
+ my ($self) = @_;
+ if (!$self->{config}) {
+ $self->{config} = Bugzilla::Extension::Push::Config->new(
+ 'global',
+ {
+ name => 'log_purge',
+ label => 'Purge logs older than (days)',
+ type => 'string',
+ default => '7',
+ required => '1',
+ validate => sub { $_[0] =~ /\D/ && die "Invalid purge duration (must be numeric)\n"; },
+ },
+ );
+ $self->{config}->load();
+ }
+ return $self->{config};
+}
+
+sub logger {
+ my ($self, $value) = @_;
+ $self->{logger} = $value if $value;
+ return $self->{logger};
+}
+
+sub connectors {
+ my ($self, $value) = @_;
+ $self->{connectors} = $value if $value;
+ return $self->{connectors};
+}
+
+sub queue {
+ my ($self) = @_;
+ $self->{queue} ||= Bugzilla::Extension::Push::Queue->new();
+ return $self->{queue};
+}
+
+sub log {
+ my ($self) = @_;
+ $self->{log} ||= Bugzilla::Extension::Push::Log->new();
+ return $self->{log};
+}
+
+sub _dbh_check {
+ my ($self) = @_;
+ eval {
+ Bugzilla->dbh->selectrow_array("SELECT 1 FROM push");
+ };
+ if ($@) {
+ $self->logger->error(clean_error($@));
+ return 0;
+ } else {
+ return 1;
+ }
+}
+
+1;
diff --git a/extensions/Push/lib/Queue.pm b/extensions/Push/lib/Queue.pm
new file mode 100644
index 000000000..d89cb23c3
--- /dev/null
+++ b/extensions/Push/lib/Queue.pm
@@ -0,0 +1,72 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Queue;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Extension::Push::Message;
+
+sub new {
+ my ($class) = @_;
+ my $self = {};
+ bless($self, $class);
+ return $self;
+}
+
+sub count {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ return $dbh->selectrow_array("SELECT COUNT(*) FROM push");
+}
+
+sub oldest {
+ my ($self) = @_;
+ my @messages = $self->list(limit => 1);
+ return scalar(@messages) ? $messages[0] : undef;
+}
+
+sub by_id {
+ my ($self, $id) = @_;
+ my @messages = $self->list(
+ limit => 1,
+ filter => "AND (push.id = $id)",
+ );
+ return scalar(@messages) ? $messages[0] : undef;
+}
+
+sub list {
+ my ($self, %args) = @_;
+ $args{limit} ||= 10;
+ $args{filter} ||= '';
+ my @result;
+ my $dbh = Bugzilla->dbh;
+
+ my $sth = $dbh->prepare("
+ SELECT id, push_ts, payload, change_set, routing_key
+ FROM push
+ WHERE (1 = 1) " .
+ $args{filter} . "
+ ORDER BY push_ts " .
+ $dbh->sql_limit($args{limit})
+ );
+ $sth->execute();
+ while (my $row = $sth->fetchrow_hashref()) {
+ push @result, Bugzilla::Extension::Push::Message->new({
+ id => $row->{id},
+ push_ts => $row->{push_ts},
+ payload => $row->{payload},
+ change_set => $row->{change_set},
+ routing_key => $row->{routing_key},
+ });
+ }
+ return @result;
+}
+
+1;
diff --git a/extensions/Push/lib/Serialise.pm b/extensions/Push/lib/Serialise.pm
new file mode 100644
index 000000000..94f33c754
--- /dev/null
+++ b/extensions/Push/lib/Serialise.pm
@@ -0,0 +1,318 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Serialise;
+
+use strict;
+use warnings;
+
+use Bugzilla::Constants;
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::Version;
+
+use Scalar::Util 'blessed';
+use JSON ();
+
+my $_instance;
+sub instance {
+ $_instance ||= Bugzilla::Extension::Push::Serialise->_new();
+ return $_instance;
+}
+
+sub _new {
+ my ($class) = @_;
+ my $self = {};
+ bless($self, $class);
+ return $self;
+}
+
+# given an object, serliase to a hash
+sub object_to_hash {
+ my ($self, $object, $is_shallow) = @_;
+
+ my $method = lc(blessed($object));
+ $method =~ s/::/_/g;
+ $method =~ s/^bugzilla//;
+ return unless $self->can($method);
+ (my $name = $method) =~ s/^_//;
+
+ # check for a cached hash
+ my $cache = Bugzilla->request_cache;
+ my $cache_id = "push." . ($is_shallow ? 'shallow.' : 'deep.') . $object;
+ if (exists($cache->{$cache_id})) {
+ return wantarray ? ($cache->{$cache_id}, $name) : $cache->{$cache_id};
+ }
+
+ # call the right method to serialise to a hash
+ my $rh = $self->$method($object, $is_shallow);
+
+ # store in cache
+ if ($cache_id) {
+ $cache->{$cache_id} = $rh;
+ }
+
+ return wantarray ? ($rh, $name) : $rh;
+}
+
+# given a changes hash, return an event hash
+sub changes_to_event {
+ my ($self, $changes) = @_;
+
+ my $event = {};
+
+ # create common (created and modified) fields
+ $event->{'user'} = $self->object_to_hash(Bugzilla->user);
+ my $timestamp =
+ $changes->{'timestamp'}
+ || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ $event->{'time'} = datetime_to_timestamp($timestamp);
+
+ foreach my $change (@{$changes->{'changes'}}) {
+ if (exists $change->{'field'}) {
+ # map undef to emtpy
+ hash_undef_to_empty($change);
+
+ # custom_fields change from undef to empty, ignore these changes
+ return if ($change->{'added'} || "") eq "" &&
+ ($change->{'removed'} || "") eq "";
+
+ # use saner field serialisation
+ my $field = $change->{'field'};
+ $change->{'field'} = $field;
+
+ if ($field eq 'priority' || $field eq 'target_milestone') {
+ $change->{'added'} = _select($change->{'added'});
+ $change->{'removed'} = _select($change->{'removed'});
+
+ } elsif ($field =~ /^cf_/) {
+ $change->{'added'} = _custom_field($field, $change->{'added'});
+ $change->{'removed'} = _custom_field($field, $change->{'removed'});
+ }
+
+ $event->{'changes'} = [] unless exists $event->{'changes'};
+ push @{$event->{'changes'}}, $change;
+ }
+ }
+
+ return $event;
+}
+
+# bugzilla returns '---' or '--' for single-select fields that have no value
+# selected. it makes more sense to return an empty string.
+sub _select {
+ my ($value) = @_;
+ return '' if $value eq '---' or $value eq '--';
+ return $value;
+}
+
+# return an object which serialises to a json boolean, but still acts as a perl
+# boolean
+sub _boolean {
+ my ($value) = @_;
+ return $value ? JSON::true : JSON::false;
+}
+
+sub _string {
+ my ($value) = @_;
+ return defined($value) ? $value : '';
+}
+
+sub _time {
+ my ($value) = @_;
+ return defined($value) ? datetime_to_timestamp($value) : undef;
+}
+
+sub _integer {
+ my ($value) = @_;
+ return defined($value) ? $value + 0 : undef;
+}
+
+sub _array {
+ my ($value) = @_;
+ return defined($value) ? $value : [];
+}
+
+sub _custom_field {
+ my ($field, $value) = @_;
+ $field = Bugzilla::Field->new({ name => $field }) unless blessed $field;
+
+ if ($field->type == FIELD_TYPE_DATETIME) {
+ return _time($value);
+
+ } elsif ($field->type == FIELD_TYPE_SINGLE_SELECT) {
+ return _select($value);
+
+ } elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
+ return _array($value);
+
+ } else {
+ return _string($value);
+ }
+}
+
+#
+# class mappings
+# automatically derrived from the class name
+# Bugzilla::Bug --> _bug, Bugzilla::User --> _user, etc
+#
+
+sub _bug {
+ my ($self, $bug) = @_;
+
+ my $version = $bug->can('version_obj')
+ ? $bug->version_obj
+ : Bugzilla::Version->new({ name => $bug->version, product => $bug->product_obj });
+
+ my $milestone;
+ if (_select($bug->target_milestone) ne '') {
+ $milestone = $bug->can('target_milestone_obj')
+ ? $bug->target_milestone_obj
+ : Bugzilla::Milestone->new({ name => $bug->target_milestone, product => $bug->product_obj });
+ }
+
+ my $status = $bug->can('status_obj')
+ ? $bug->status_obj
+ : Bugzilla::Status->new({ name => $bug->bug_status });
+
+ my $rh = {
+ id => _integer($bug->bug_id),
+ alias => _string($bug->alias),
+ assigned_to => $self->_user($bug->assigned_to),
+ classification => _string($bug->classification),
+ component => $self->_component($bug->component_obj),
+ creation_time => _time($bug->creation_ts || $bug->delta_ts),
+ flags => (mapr { $self->_flag($_) } $bug->flags),
+ is_private => _boolean(!is_public($bug)),
+ keywords => (mapr { _string($_->name) } $bug->keyword_objects),
+ last_change_time => _time($bug->delta_ts),
+ operating_system => _string($bug->op_sys),
+ platform => _string($bug->rep_platform),
+ priority => _select($bug->priority),
+ product => $self->_product($bug->product_obj),
+ qa_contact => $self->_user($bug->qa_contact),
+ reporter => $self->_user($bug->reporter),
+ resolution => _string($bug->resolution),
+ severity => _string($bug->bug_severity),
+ status => $self->_status($status),
+ summary => _string($bug->short_desc),
+ target_milestone => $self->_milestone($milestone),
+ url => _string($bug->bug_file_loc),
+ version => $self->_version($version),
+ whiteboard => _string($bug->status_whiteboard),
+ };
+
+ # add custom fields
+ my @custom_fields = Bugzilla->active_custom_fields(
+ { product => $bug->product_obj, component => $bug->component_obj });
+ foreach my $field (@custom_fields) {
+ my $name = $field->name;
+ $rh->{$name} = _custom_field($field, $bug->$name);
+ }
+
+ return $rh;
+}
+
+sub _user {
+ my ($self, $user) = @_;
+ return undef unless $user;
+ return {
+ id => _integer($user->id),
+ login => _string($user->login),
+ real_name => _string($user->name),
+ };
+}
+
+sub _component {
+ my ($self, $component) = @_;
+ return {
+ id => _integer($component->id),
+ name => _string($component->name),
+ };
+}
+
+sub _attachment {
+ my ($self, $attachment, $is_shallow) = @_;
+ my $rh = {
+ id => _integer($attachment->id),
+ content_type => _string($attachment->contenttype),
+ creation_time => _time($attachment->attached),
+ description => _string($attachment->description),
+ file_name => _string($attachment->filename),
+ flags => (mapr { $self->_flag($_) } $attachment->flags),
+ is_obsolete => _boolean($attachment->isobsolete),
+ is_patch => _boolean($attachment->ispatch),
+ is_private => _boolean(!is_public($attachment)),
+ last_change_time => _time($attachment->modification_time),
+ };
+ if (!$is_shallow) {
+ $rh->{bug} = $self->_bug($attachment->bug);
+ }
+ return $rh;
+}
+
+sub _comment {
+ my ($self, $comment, $is_shallow) = @_;
+ my $rh = {
+ id => _integer($comment->bug_id),
+ body => _string($comment->body),
+ creation_time => _time($comment->creation_ts),
+ is_private => _boolean($comment->is_private),
+ number => _integer($comment->count),
+ };
+ if (!$is_shallow) {
+ $rh->{bug} = $self->_bug($comment->bug);
+ }
+ return $rh;
+}
+
+sub _product {
+ my ($self, $product) = @_;
+ return {
+ id => _integer($product->id),
+ name => _string($product->name),
+ };
+}
+
+sub _flag {
+ my ($self, $flag) = @_;
+ my $rh = {
+ id => _integer($flag->id),
+ name => _string($flag->type->name),
+ value => _string($flag->status),
+ };
+ if ($flag->requestee) {
+ $rh->{'requestee'} = $self->_user($flag->requestee);
+ }
+ return $rh;
+}
+
+sub _version {
+ my ($self, $version) = @_;
+ return {
+ id => _integer($version->id),
+ name => _string($version->name),
+ };
+}
+
+sub _milestone {
+ my ($self, $milestone) = @_;
+ return undef unless $milestone;
+ return {
+ id => _integer($milestone->id),
+ name => _string($milestone->name),
+ };
+}
+
+sub _status {
+ my ($self, $status) = @_;
+ return {
+ id => _integer($status->id),
+ name => _string($status->name),
+ };
+}
+
+1;
diff --git a/extensions/Push/lib/Util.pm b/extensions/Push/lib/Util.pm
new file mode 100644
index 000000000..f52db6936
--- /dev/null
+++ b/extensions/Push/lib/Util.pm
@@ -0,0 +1,162 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Util;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Util qw(datetime_from trim);
+use Data::Dumper;
+use Encode;
+use JSON ();
+use Scalar::Util qw(blessed);
+use Time::HiRes;
+
+use base qw(Exporter);
+our @EXPORT = qw(
+ datetime_to_timestamp
+ debug_dump
+ get_first_value
+ hash_undef_to_empty
+ is_public
+ mapr
+ clean_error
+ change_set_id
+ canon_email
+ to_json from_json
+);
+
+# returns true if the specified object is public
+sub is_public {
+ my ($object) = @_;
+
+ my $default_user = Bugzilla::User->new();
+
+ if ($object->isa('Bugzilla::Bug')) {
+ return unless $default_user->can_see_bug($object->bug_id);
+ return 1;
+
+ } elsif ($object->isa('Bugzilla::Comment')) {
+ return if $object->is_private;
+ return unless $default_user->can_see_bug($object->bug_id);
+ return 1;
+
+ } elsif ($object->isa('Bugzilla::Attachment')) {
+ return if $object->isprivate;
+ return unless $default_user->can_see_bug($object->bug_id);
+ return 1;
+
+ } else {
+ warn "Unsupported class " . blessed($object) . " passed to is_public()\n";
+ }
+
+ return 1;
+}
+
+# return the first existing value from the hashref for the given list of keys
+sub get_first_value {
+ my ($rh, @keys) = @_;
+ foreach my $field (@keys) {
+ return $rh->{$field} if exists $rh->{$field};
+ }
+ return;
+}
+
+# wrapper for map that works on array references
+sub mapr(&$) {
+ my ($filter, $ra) = @_;
+ my @result = map(&$filter, @$ra);
+ return \@result;
+}
+
+
+# convert datetime string (from db) to a UTC json friendly datetime
+sub datetime_to_timestamp {
+ my ($datetime_string) = @_;
+ return '' unless $datetime_string;
+ return datetime_from($datetime_string, 'UTC')->datetime();
+}
+
+# replaces all undef values in a hashref with an empty string (deep)
+sub hash_undef_to_empty {
+ my ($rh) = @_;
+ foreach my $key (keys %$rh) {
+ my $value = $rh->{$key};
+ if (!defined($value)) {
+ $rh->{$key} = '';
+ } elsif (ref($value) eq 'HASH') {
+ hash_undef_to_empty($value);
+ }
+ }
+}
+
+# debugging methods
+sub debug_dump {
+ my ($object) = @_;
+ local $Data::Dumper::Sortkeys = 1;
+ my $output = Dumper($object);
+ $output =~ s/</&lt;/g;
+ print "<pre>$output</pre>";
+}
+
+# removes stacktrace and "at /some/path ..." from errors
+sub clean_error {
+ my ($error) = @_;
+ my $path = bz_locations->{'extensionsdir'};
+ $error = $1 if $error =~ /^(.+?) at \Q$path/s;
+ $path = '/loader/0x';
+ $error = $1 if $error =~ /^(.+?) at \Q$path/s;
+ $error =~ s/(^\s+|\s+$)//g;
+ return $error;
+}
+
+# generate a new change_set id
+sub change_set_id {
+ return "$$." . Time::HiRes::time();
+}
+
+# remove guff from email addresses
+sub clean_email {
+ my $email = shift;
+ $email = trim($email);
+ $email = $1 if $email =~ /^(\S+)/;
+ $email =~ s/&#64;/@/;
+ $email = lc $email;
+ return $email;
+}
+
+# resolve to canonised email form
+# eg. glob+bmo@mozilla.com --> glob@mozilla.com
+sub canon_email {
+ my $email = shift;
+ $email = clean_email($email);
+ $email =~ s/^([^\+]+)\+[^\@]+(\@.+)$/$1$2/;
+ return $email;
+}
+
+# json helpers
+sub to_json {
+ my ($object, $pretty) = @_;
+ if ($pretty) {
+ return decode('utf8', JSON->new->utf8(1)->pretty(1)->encode($object));
+ } else {
+ return JSON->new->ascii(1)->shrink(1)->encode($object);
+ }
+}
+
+sub from_json {
+ my ($json) = @_;
+ if (utf8::is_utf8($json)) {
+ $json = encode('utf8', $json);
+ }
+ return JSON->new->utf8(1)->decode($json);
+}
+
+1;
diff --git a/extensions/Push/t/ReviewBoard.t b/extensions/Push/t/ReviewBoard.t
new file mode 100644
index 000000000..f2a508f59
--- /dev/null
+++ b/extensions/Push/t/ReviewBoard.t
@@ -0,0 +1,224 @@
+#!/usr/bin/perl -T
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+use strict;
+use warnings;
+use lib qw( . lib );
+
+use Test::More;
+use Bugzilla;
+use Bugzilla::Extension;
+use Bugzilla::Attachment;
+use Scalar::Util 'blessed';
+use YAML;
+
+BEGIN {
+ eval {
+ require Test::LWP::UserAgent;
+ require Test::MockObject;
+ };
+ if ($@) {
+ plan skip_all =>
+ 'Tests require Test::LWP::UserAgent and Test::MockObject';
+ exit;
+ }
+}
+
+BEGIN {
+ Bugzilla->extensions; # load all of them
+ use_ok 'Bugzilla::Extension::Push::Connector::ReviewBoard::Client';
+ use_ok 'Bugzilla::Extension::Push::Constants';
+}
+
+my ($push) = grep { blessed($_) eq 'Bugzilla::Extension::Push' } @{Bugzilla->extensions };
+my $connectors = $push->_get_instance->connectors;
+my $con = $connectors->by_name('ReviewBoard');
+
+my $ua_204 = Test::LWP::UserAgent->new;
+$ua_204->map_response(
+ qr{https://reviewboard-dev\.allizom\.org/api/review-requests/\d+},
+ HTTP::Response->new('204'));
+
+my $ua_404 = Test::LWP::UserAgent->new;
+$ua_404->map_response(
+ qr{https://reviewboard-dev\.allizom\.org/api/review-requests/\d+},
+ HTTP::Response->new('404', undef, undef, q[{ "err": { "code": 100, "msg": "Object does not exist" }, "stat": "fail" }]));
+
+# forbidden
+my $ua_403 = Test::LWP::UserAgent->new;
+$ua_403->map_response(
+ qr{https://reviewboard-dev\.allizom\.org/api/review-requests/\d+},
+ HTTP::Response->new('403', undef, undef, q[ {"err":{"code":101,"msg":"You don't have permission for this"},"stat":"fail"}]));
+
+# not logged in
+my $ua_401 = Test::LWP::UserAgent->new;
+$ua_401->map_response(
+ qr{https://reviewboard-dev\.allizom\.org/api/review-requests/\d+},
+ HTTP::Response->new('401', undef, undef, q[ { "err": { "code": 103, "msg": "You are not logged in" }, "stat": "fail" } ]));
+
+# not logged in
+my $ua_500 = Test::LWP::UserAgent->new;
+$ua_500->map_response(
+ qr{https://reviewboard-dev\.allizom\.org/api/review-requests/\d+},
+ HTTP::Response->new('500'));
+
+$con->client->{useragent} = $ua_204;
+$con->config->{base_uri} = 'https://reviewboard-dev.allizom.org';
+$con->client->{base_uri} = 'https://reviewboard-dev.allizom.org';
+
+{
+ my $msg = message(
+ event => {
+ routing_key => 'attachment.modify:is_private',
+ target => 'attachment',
+ },
+ attachment => {
+ is_private => 1,
+ content_type => 'text/plain',
+ bug => { id => 1, is_private => 0 },
+ },
+ );
+
+ ok(not($con->should_send($msg)), "text/plain message should not be sent");
+}
+
+my $data = slurp("extensions/Push/t/rblink.txt");
+Bugzilla::User::DEFAULT_USER->{userid} = 42;
+Bugzilla->set_user(Bugzilla::User->super_user);
+diag " " . Bugzilla::User->super_user->id;
+
+my $dbh = Bugzilla->dbh;
+$dbh->bz_start_transaction;
+my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+my $bug = Bugzilla::Bug->new({id => 9000});
+my $attachment = Bugzilla::Attachment->create(
+ { bug => $bug,
+ creation_ts => $timestamp,
+ data => $data,
+ filesize => length $data,
+ description => "rblink.txt",
+ filename => "rblink.txt",
+ isprivate => 1, ispatch => 0,
+ mimetype => 'text/x-review-board-request'});
+diag "".$attachment->id;
+$dbh->bz_commit_transaction;
+
+{
+ my $msg = message(
+ event => {
+ routing_key => 'attachment.modify:cc,is_private',
+ target => 'attachment',
+ },
+ attachment => {
+ id => $attachment->id,
+ is_private => 1,
+ content_type => 'text/x-review-board-request',
+ bug => { id => $bug->id, is_private => 0 },
+ },
+ );
+ ok($con->should_send($msg), "rb attachment should be sent");
+
+ {
+ my ($rv, $err) = $con->send($msg);
+ is($rv, PUSH_RESULT_OK, "good push result");
+ diag $err if $err;
+ }
+
+ {
+ local $con->client->{useragent} = $ua_404;
+ my ($rv, $err) = $con->send($msg);
+ is($rv, PUSH_RESULT_OK, "good push result for 404");
+ diag $err if $err;
+ }
+
+
+ {
+ local $con->client->{useragent} = $ua_403;
+ my ($rv, $err) = $con->send($msg);
+ is($rv, PUSH_RESULT_TRANSIENT, "transient error on 403");
+ diag $err if $err;
+ }
+
+
+ {
+ local $con->client->{useragent} = $ua_401;
+ my ($rv, $err) = $con->send($msg);
+ is($rv, PUSH_RESULT_TRANSIENT, "transient error on 401");
+ diag $err if $err;
+ }
+
+ {
+ local $con->client->{useragent} = $ua_500;
+ my ($rv, $err) = $con->send($msg);
+ is($rv, PUSH_RESULT_TRANSIENT, "transient error on 500");
+ diag $err if $err;
+ }
+}
+
+{
+ my $msg = message(
+ event => {
+ routing_key => 'bug.modify:is_private',
+ target => 'bug',
+ },
+ bug => {
+ is_private => 1,
+ id => $bug->id,
+ },
+ );
+
+ ok($con->should_send($msg), "rb attachment should be sent");
+ my ($rv, $err) = $con->send($msg);
+ is($rv, PUSH_RESULT_OK, "good push result");
+
+ {
+ local $con->client->{useragent} = $ua_404;
+ my ($rv, $err) = $con->send($msg);
+ is($rv, PUSH_RESULT_OK, "good push result for 404");
+ }
+
+ {
+ local $con->client->{useragent} = $ua_403;
+ my ($rv, $err) = $con->send($msg);
+ is($rv, PUSH_RESULT_TRANSIENT, "transient error on 404");
+ diag $err if $err;
+ }
+
+
+ {
+ local $con->client->{useragent} = $ua_401;
+ my ($rv, $err) = $con->send($msg);
+ is($rv, PUSH_RESULT_TRANSIENT, "transient error on 401");
+ diag $err if $err;
+ }
+
+ {
+ local $con->client->{useragent} = $ua_401;
+ my ($rv, $err) = $con->send($msg);
+ is($rv, PUSH_RESULT_TRANSIENT, "transient error on 401");
+ diag $err if $err;
+ }
+}
+
+sub message {
+ my $msg_data = { @_ };
+
+ return Test::MockObject->new
+ ->set_always( routing_key => $msg_data->{event}{routing_key} )
+ ->set_always( payload_decoded => $msg_data );
+}
+
+sub slurp {
+ my $file = shift;
+ local $/ = undef;
+ open my $fh, '<', $file or die "unable to open $file";
+ my $s = readline $fh;
+ close $fh;
+ return $s;
+}
+
+done_testing;
diff --git a/extensions/Push/template/en/default/hook/admin/admin-end_links_right.html.tmpl b/extensions/Push/template/en/default/hook/admin/admin-end_links_right.html.tmpl
new file mode 100644
index 000000000..78e314ab2
--- /dev/null
+++ b/extensions/Push/template/en/default/hook/admin/admin-end_links_right.html.tmpl
@@ -0,0 +1,18 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF user.in_group('admin') %]
+ <dt id="push">
+ Push
+ </dt>
+ <dd>
+ <a href="page.cgi?id=push_config.html">Configuration</a><br>
+ <a href="page.cgi?id=push_queues.html">Queues</a><br>
+ <a href="page.cgi?id=push_log.html">Log</a><br>
+ </dd>
+[% END %]
diff --git a/extensions/Push/template/en/default/hook/global/code-error-errors.html.tmpl b/extensions/Push/template/en/default/hook/global/code-error-errors.html.tmpl
new file mode 100644
index 000000000..515f00fa8
--- /dev/null
+++ b/extensions/Push/template/en/default/hook/global/code-error-errors.html.tmpl
@@ -0,0 +1,25 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "push_invalid_payload" %]
+ [% title = "Invalid payload" %]
+ An invalid or empty payload was passed to Push.
+
+[% ELSIF error == "push_invalid_change_set" %]
+ [% title = "Invalid change_set" %]
+ An invalid or empty change_set was passed to Push.
+
+[% ELSIF error == "push_invalid_routing_key" %]
+ [% title = "Invalid routing_key" %]
+ An invalid or empty routing_key was passed to Push.
+
+[% ELSIF error == "push_invalid_connector" %]
+ [% title = "Invalid connector" %]
+ An invalid connector was passed to Push.
+
+[% END %]
diff --git a/extensions/Push/template/en/default/hook/global/messages-messages.html.tmpl b/extensions/Push/template/en/default/hook/global/messages-messages.html.tmpl
new file mode 100644
index 000000000..e4a016aee
--- /dev/null
+++ b/extensions/Push/template/en/default/hook/global/messages-messages.html.tmpl
@@ -0,0 +1,16 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF message_tag == "push_config_updated" %]
+ Changes to the configuration have been saved.
+ Please allow up to 60 seconds for the change to be active.
+
+[% ELSIF message_tag == "push_message_deleted" %]
+ The message has been deleted.
+
+[% END %]
diff --git a/extensions/Push/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/Push/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..2b8a1c4e0
--- /dev/null
+++ b/extensions/Push/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "push_error" %]
+ [% error_message FILTER html %]
+[% END %]
diff --git a/extensions/Push/template/en/default/pages/push_config.html.tmpl b/extensions/Push/template/en/default/pages/push_config.html.tmpl
new file mode 100644
index 000000000..6e6507a39
--- /dev/null
+++ b/extensions/Push/template/en/default/pages/push_config.html.tmpl
@@ -0,0 +1,134 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Push Administration: Configuration"
+ javascript_urls = [ 'extensions/Push/web/admin.js' ]
+ style_urls = [ 'extensions/Push/web/admin.css' ]
+%]
+
+<script>
+var push_defaults = new Array();
+[% FOREACH option = push.config.options %]
+ [% IF option.name != 'enabled' && option.default != '' %]
+ push_defaults['global_[% option.name FILTER js %]'] = '[% option.default FILTER js %]';
+ [% END %]
+[% END %]
+[% FOREACH connector = connectors.list %]
+ [% FOREACH option = connector.config.options %]
+ [% IF option.name != 'enabled' && option.default != '' %]
+ push_defaults['[% connector.name FILTER js %]_[% option.name FILTER js %]'] = '[% option.default FILTER js %]';
+ [% END %]
+ [% END %]
+[% END %]
+</script>
+
+<form method="POST" action="page.cgi">
+<input type="hidden" name="id" value="push_config.html">
+<input type="hidden" name="save" value="1">
+
+<table border="0" cellspacing="0" cellpadding="5" width="100%">
+
+[% PROCESS options
+ name = 'global',
+ config = push.config
+%]
+
+[% FOREACH connector = connectors.list %]
+ [% PROCESS options
+ name = connector.name
+ config = connector.config
+ %]
+[% END %]
+
+<tr>
+ <td>&nbsp;</td>
+ <td colspan="2"><hr></td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td colspan="2">
+ <input type="submit" value="Submit Changes">
+ <input type="submit" value="Reset to Defaults" onclick="reset_to_defaults(); return false">
+ </td>
+</tr>
+
+
+<tr>
+ <td style="min-width: 10em">&nbsp;</td>
+ <td>&nbsp;</td>
+ <td width="100%">&nbsp;</td>
+</tr>
+
+</table>
+
+</form>
+
+[% INCLUDE global/footer.html.tmpl %]
+
+[% BLOCK options %]
+ <tr class="connector">
+ <th>[% name FILTER ucfirst FILTER html %]</th>
+ <td colspan="2"><hr></td>
+ </tr>
+ [% FOREACH option = config.options %]
+ [% class = name _ '_tr' IF option.name != 'enabled' %]
+ <tr class="[% class FILTER html %] option">
+ <th>
+ [% IF option.required %]
+ <span class="required_option" title="Mandatory option">*</span>&nbsp;
+ [% END %]
+ [% option.label FILTER html %]
+ </th>
+ <td>
+ [% IF option.type == 'string' %]
+ <input type="text" name="[% name FILTER html %].[% option.name FILTER html %]"
+ value="[% config.${option.name} FILTER html %]" size="60"
+ id="[% name FILTER html %]_[% option.name FILTER html %]">
+
+ [% ELSIF option.type == 'password' %]
+ <input type="password" name="[% name FILTER html %].[% option.name FILTER html %]"
+ value="[% config.${option.name} FILTER html %]" size="60"
+ id="[% name FILTER html %]_[% option.name FILTER html %]">
+
+ [% ELSIF option.type == 'select' %]
+ <select name="[% name FILTER html %].[% option.name FILTER html %]"
+ id="[% name FILTER html %]_[% option.name FILTER html %]"
+ [% IF option.name == 'enabled' && name != 'global' %]
+ onchange="toggle_options(this.value == 'Enabled', '[% name FILTER js %]')"
+ [% END %]
+ >
+ [% IF option.name != 'enabled' && !option.required %]
+ <option value="""
+ [% ' selected' IF config.${option.name} == "" %]></option>
+ [% END %]
+ [% FOREACH value = option.values %]
+ <option value="[% value FILTER html %]"
+ [% ' selected' IF config.${option.name} == value %]>[% value FILTER html %]</option>
+ [% END %]
+ </select>
+
+ [% ELSE %]
+ unsupported option type '[% option.type FILTER html %]'
+ [% END %]
+ </td>
+ [% IF option.help %]
+ <td class="help">[% option.help FILTER html %]</td>
+ [% ELSE %]
+ <td>&nbsp;</td>
+ [% END %]
+ </tr>
+ [% END %]
+ [% IF name != 'global' %]
+ <script>
+ var is_enabled = document.getElementById('[% name FILTER js %]_enabled').value == 'Enabled';
+ toggle_options(is_enabled, '[% name FILTER js %]');
+ </script>
+ [% END %]
+[% END %]
diff --git a/extensions/Push/template/en/default/pages/push_log.html.tmpl b/extensions/Push/template/en/default/pages/push_log.html.tmpl
new file mode 100644
index 000000000..a51cb22cf
--- /dev/null
+++ b/extensions/Push/template/en/default/pages/push_log.html.tmpl
@@ -0,0 +1,45 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Push Administration: Logs"
+ javascript_urls = [ 'extensions/Push/web/admin.js' ]
+ style_urls = [ 'extensions/Push/web/admin.css' ]
+%]
+[% logs = push.log %]
+
+<table id="report" cellspacing="0">
+
+[% IF logs.count %]
+ <tr class="report-subheader">
+ <th nowrap>Connector</th>
+ <th nowrap>Event Timestamp</th>
+ <th nowrap>Processed Timestamp</th>
+ <th nowrap>Status</th>
+ <th nowrap>Message</th>
+ </tr>
+[% END %]
+
+[% FOREACH log = logs.list %]
+ <tr class="row [% loop.count % 2 == 1 ? "report_row_odd" : "report_row_even" %]">
+ <td nowrap>[% log.connector FILTER html %]</td>
+ <td nowrap>[% log.push_ts FILTER time FILTER html %]</td>
+ <td nowrap>[% log.processed_ts FILTER time FILTER html %]</td>
+ <td nowrap>[% log.result_string FILTER html %]</td>
+ <td>[% log.data FILTER html %]</td>
+ </tr>
+[% END %]
+
+<tr>
+ <td colspan="5">&nbsp;</td>
+</tr>
+
+</table>
+
+[% INCLUDE global/footer.html.tmpl %]
+
diff --git a/extensions/Push/template/en/default/pages/push_queues.html.tmpl b/extensions/Push/template/en/default/pages/push_queues.html.tmpl
new file mode 100644
index 000000000..d1985c89a
--- /dev/null
+++ b/extensions/Push/template/en/default/pages/push_queues.html.tmpl
@@ -0,0 +1,102 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Push Administration: Queues"
+ javascript_urls = [ 'extensions/Push/web/admin.js' ]
+ style_urls = [ 'extensions/Push/web/admin.css' ]
+%]
+
+<table id="report" cellspacing="0">
+
+[% PROCESS show_queue
+ queue = push.queue
+ title = 'Pending'
+ pending = 1
+%]
+
+[% FOREACH connector = push.connectors.list %]
+ [% NEXT UNLESS connector.enabled %]
+ [% PROCESS show_queue
+ queue = connector.backlog
+ title = connector.name _ ' Backlog'
+ pending = 0
+ %]
+[% END %]
+
+</table>
+
+[% INCLUDE global/footer.html.tmpl %]
+
+[% BLOCK show_queue %]
+ [% count = queue.count %]
+ <tr class="report-header">
+ <th colspan="2">
+ [% title FILTER html %] Queue ([% count FILTER html %])
+ </th>
+ [% IF queue.backoff && count %]
+ <th class="rhs" colspan="5">
+ Next Attempt: [% queue.backoff.next_attempt_ts FILTER time %]
+ </th>
+ [% ELSE %]
+ <th colspan="5">&nbsp;</td>
+ [% END %]
+ </tr>
+
+ [% IF count %]
+ <tr class="report-subheader">
+ <th nowrap>Timestamp</th>
+ <th nowrap>Change Set</th>
+ [% IF pending %]
+ <th nowrap colspan="4">Routing Key</th>
+ [% ELSE %]
+ <th nowrap>Routing Key</th>
+ <th nowrap>Last Attempt</th>
+ <th nowrap>Attempts</th>
+ <th nowrap>Last Error</th>
+ [% END %]
+ <th>&nbsp;</th>
+ </tr>
+ [% END %]
+
+ [% FOREACH message = queue.list('limit', 10) %]
+ <tr class="row [% loop.count % 2 == 1 ? "report_row_odd" : "report_row_even" %]">
+ <td nowrap>[% message.push_ts FILTER html %]</td>
+ <td nowrap>[% message.change_set FILTER html %]</td>
+ [% IF pending %]
+ <td nowrap colspan="4">[% message.routing_key FILTER html %]</td>
+ [% ELSE %]
+ <td nowrap>[% message.routing_key FILTER html %]</td>
+ [% IF message.attempt_ts %]
+ <td nowrap>[% message.attempt_ts FILTER time %]</td>
+ <td nowrap>[% message.attempts FILTER html %]</td>
+ <td width="100%">
+ [% IF message.last_error.length > 40 %]
+ [% last_error = message.last_error.substr(0, 40) _ '...' %]
+ [% ELSE %]
+ [% last_error = message.last_error %]
+ [% END %]
+ [% last_error FILTER html %]</td>
+ [% ELSE %]
+ <td>-</td>
+ <td>-</td>
+ <td width="100%">-</td>
+ [% END %]
+ [% END %]
+ <td class="rhs">
+ <a href="?id=push_queues_view.html&amp;[% ~%]
+ message=[% message.id FILTER uri %]&amp;[% ~%]
+ connector=[% queue.connector FILTER uri %]">View</a>
+ </td>
+ </tr>
+ [% END %]
+
+ <tr>
+ <td colspan="7">&nbsp;</td>
+ </tr>
+[% END %]
diff --git a/extensions/Push/template/en/default/pages/push_queues_view.html.tmpl b/extensions/Push/template/en/default/pages/push_queues_view.html.tmpl
new file mode 100644
index 000000000..6330d8ae4
--- /dev/null
+++ b/extensions/Push/template/en/default/pages/push_queues_view.html.tmpl
@@ -0,0 +1,80 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Push Administration: Queues: Payload"
+ javascript_urls = [ 'extensions/Push/web/admin.js' ]
+ style_urls = [ 'extensions/Push/web/admin.css' ]
+%]
+
+[% IF !message_obj %]
+ <a href="?id=push_queues.html">Return</a>
+ [% RETURN %]
+[% END %]
+
+<table id="report" cellspacing="0">
+
+<tr>
+ <th class="report-header" nowrap>Connector</th>
+ <td width="100%">[% message_obj.connector || '-' FILTER html %]</td>
+</tr>
+<tr>
+ <th class="report-header" nowrap>Message ID</th>
+ <td width="100%">[% message_obj.message_id FILTER html %]</td>
+</tr>
+<tr>
+ <th class="report-header" nowrap>Push Time</th>
+ <td width="100%">[% message_obj.push_ts FILTER time FILTER html %]</td>
+</tr>
+<tr>
+ <th class="report-header" nowrap>Change Set</th>
+ <td width="100%">[% message_obj.change_set FILTER html %]</td>
+</tr>
+<tr>
+ <th class="report-header" nowrap>Routing Key</th>
+ <td width="100%">[% message_obj.routing_key FILTER html %]</td>
+</tr>
+
+[% IF message_obj.attempts %]
+ <tr>
+ <th class="report-header" nowrap>Attempts</th>
+ <td width="100%">[% message_obj.attempts FILTER html %]</td>
+ </tr>
+ <tr>
+ <th class="report-header" nowrap>Last Attempt Time</th>
+ <td width="100%">[% message_obj.attempt_ts FILTER time FILTER html %]</td>
+ </tr>
+ <tr>
+ <th class="report-header" nowrap>Last Error</th>
+ <td width="100%"><b>[% message_obj.last_error FILTER html %]</b></td>
+ </tr>
+[% END %]
+
+<tr>
+ <td colspan="2">
+ [% IF json %]
+ <pre>[% json FILTER html %]</pre>
+ [% ELSE %]
+ <pre>[% message_obj.payload FILTER html %]</pre>
+ [% END %]
+ </td>
+</tr>
+
+<tr class="report-header">
+ <th colspan="2">
+ <a href="?id=push_queues.html">Return</a> |
+ <a onclick="return confirm('Are you sure you want to delete this message forever (a long time)?')"
+ href="?id=push_queues_view.html&amp;delete=1
+ [%- %]&amp;message=[% message_obj.id FILTER uri %]
+ [%- %]&amp;connector=[% message_obj.connector FILTER uri %]">Delete</a>
+ </th>
+</tr>
+
+</table>
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/Push/template/en/default/setup/strings.txt.pl b/extensions/Push/template/en/default/setup/strings.txt.pl
new file mode 100644
index 000000000..bb135f5bb
--- /dev/null
+++ b/extensions/Push/template/en/default/setup/strings.txt.pl
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+%strings = (
+ feature_push_amqp => 'Push: AMQP Support',
+ feature_push_stomp => 'Push: STOMP Support',
+);
diff --git a/extensions/Push/web/admin.css b/extensions/Push/web/admin.css
new file mode 100644
index 000000000..c204fa62a
--- /dev/null
+++ b/extensions/Push/web/admin.css
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+.connector th {
+ text-align: left;
+ vertical-align: middle !important;
+}
+
+.option th {
+ text-align: right;
+ font-weight: normal !important;
+ vertical-align: middle !important;
+}
+
+.option .help {
+ font-style: italic;
+}
+
+.hidden {
+ display: none;
+}
+
+.required_option {
+ color: red;
+ cursor: help;
+}
+
+#report {
+ border: 1px solid #888888;
+ width: 100%;
+}
+
+#report td, #report th {
+ padding: 3px 10px 3px 3px;
+ border: 0px;
+}
+
+#report th {
+ text-align: left;
+}
+
+.report-header {
+ background: #cccccc;
+}
+
+.report-subheader {
+ background: #ffffff;
+}
+
+.report_row_odd {
+ background-color: #eeeeee;
+ color: #000000;
+}
+
+.report_row_even {
+ background-color: #ffffff;
+ color: #000000;
+}
+
+#report tr.row:hover {
+ background-color: #ccccff;
+}
+
+.rhs {
+ text-align: right !important;
+}
+
diff --git a/extensions/Push/web/admin.js b/extensions/Push/web/admin.js
new file mode 100644
index 000000000..599bfd742
--- /dev/null
+++ b/extensions/Push/web/admin.js
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+var Dom = YAHOO.util.Dom;
+
+function toggle_options(visible, name) {
+ var rows = Dom.getElementsByClassName(name + '_tr');
+ for (var i = 0, l = rows.length; i < l; i++) {
+ if (visible) {
+ Dom.removeClass(rows[i], 'hidden');
+ } else {
+ Dom.addClass(rows[i], 'hidden');
+ }
+ }
+}
+
+function reset_to_defaults() {
+ if (!push_defaults) return;
+ for (var id in push_defaults) {
+ var el = Dom.get(id);
+ if (!el) continue;
+ if (el.nodeName == 'INPUT') {
+ el.value = push_defaults[id];
+ } else if (el.nodeName == 'SELECT') {
+ for (var i = 0, l = el.options.length; i < l; i++) {
+ if (el.options[i].value == push_defaults[id]) {
+ el.options[i].selected = true;
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/extensions/REMO/Config.pm b/extensions/REMO/Config.pm
new file mode 100644
index 000000000..625e2afd9
--- /dev/null
+++ b/extensions/REMO/Config.pm
@@ -0,0 +1,34 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the REMO Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Mozilla Foundation
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Byron Jones <glob@mozilla.com>
+# David Lawrence <dkl@mozilla.com>
+
+package Bugzilla::Extension::REMO;
+use strict;
+
+use constant NAME => 'REMO';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/REMO/Extension.pm b/extensions/REMO/Extension.pm
new file mode 100644
index 000000000..b436d09d3
--- /dev/null
+++ b/extensions/REMO/Extension.pm
@@ -0,0 +1,309 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the REMO Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Mozilla Foundation
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Byron Jones <glob@mozilla.com>
+# David Lawrence <dkl@mozilla.com>
+
+package Bugzilla::Extension::REMO;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Constants;
+use Bugzilla::Util qw(trick_taint trim detaint_natural);
+use Bugzilla::Token;
+use Bugzilla::Error;
+
+our $VERSION = '0.01';
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my $page = $args->{'page_id'};
+ my $vars = $args->{'vars'};
+
+ if ($page eq 'remo-form-payment.html') {
+ _remo_form_payment($vars);
+ }
+}
+
+sub _remo_form_payment {
+ my ($vars) = @_;
+ my $input = Bugzilla->input_params;
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+ if ($input->{'action'} eq 'commit') {
+ my $template = Bugzilla->template;
+ my $cgi = Bugzilla->cgi;
+ my $dbh = Bugzilla->dbh;
+
+ my $bug_id = $input->{'bug_id'};
+ detaint_natural($bug_id);
+ my $bug = Bugzilla::Bug->check($bug_id);
+
+ # Detect if the user already used the same form to submit again
+ my $token = trim($input->{'token'});
+ if ($token) {
+ my ($creator_id, $date, $old_attach_id) = Bugzilla::Token::GetTokenData($token);
+ if (!$creator_id
+ || $creator_id != $user->id
+ || $old_attach_id !~ "^remo_form_payment:")
+ {
+ # The token is invalid.
+ ThrowUserError('token_does_not_exist');
+ }
+
+ $old_attach_id =~ s/^remo_form_payment://;
+ if ($old_attach_id) {
+ ThrowUserError('remo_payment_cancel_dupe',
+ { bugid => $bug_id, attachid => $old_attach_id });
+ }
+ }
+
+ # Make sure the user can attach to this bug
+ if (!$bug->user->{'canedit'}) {
+ ThrowUserError("remo_payment_bug_edit_denied",
+ { bug_id => $bug->id });
+ }
+
+ # Make sure the bug is under the correct product/component
+ if ($bug->product ne 'Mozilla Reps'
+ || $bug->component ne 'Budget Requests')
+ {
+ ThrowUserError('remo_payment_invalid_product');
+ }
+
+ my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
+
+ $dbh->bz_start_transaction;
+
+ # Create the comment to be added based on the form fields from rep-payment-form
+ my $comment;
+ $template->process("pages/comment-remo-form-payment.txt.tmpl", $vars, \$comment)
+ || ThrowTemplateError($template->error());
+ $bug->add_comment($comment, { isprivate => 0 });
+
+ # Attach expense report
+ # FIXME: Would be nice to be able to have the above prefilled comment and
+ # the following attachments all show up under a single comment. But the longdescs
+ # table can only handle one attach_id per comment currently. At least only one
+ # email is sent the way it is done below.
+ my $attachment;
+ if (defined $cgi->upload('expenseform')) {
+ # Determine content-type
+ my $content_type = $cgi->uploadInfo($cgi->param('expenseform'))->{'Content-Type'};
+
+ $attachment = Bugzilla::Attachment->create(
+ { bug => $bug,
+ creation_ts => $timestamp,
+ data => $cgi->upload('expenseform'),
+ description => 'Expense Form',
+ filename => scalar $cgi->upload('expenseform'),
+ ispatch => 0,
+ isprivate => 0,
+ mimetype => $content_type,
+ });
+
+ # Insert comment for attachment
+ $bug->add_comment('', { isprivate => 0,
+ type => CMT_ATTACHMENT_CREATED,
+ extra_data => $attachment->id });
+ }
+
+ # Attach receipts file
+ if (defined $cgi->upload("receipts")) {
+ # Determine content-type
+ my $content_type = $cgi->uploadInfo($cgi->param("receipts"))->{'Content-Type'};
+
+ $attachment = Bugzilla::Attachment->create(
+ { bug => $bug,
+ creation_ts => $timestamp,
+ data => $cgi->upload('receipts'),
+ description => "Receipts",
+ filename => scalar $cgi->upload("receipts"),
+ ispatch => 0,
+ isprivate => 0,
+ mimetype => $content_type,
+ });
+
+ # Insert comment for attachment
+ $bug->add_comment('', { isprivate => 0,
+ type => CMT_ATTACHMENT_CREATED,
+ extra_data => $attachment->id });
+ }
+
+ $bug->update($timestamp);
+
+ if ($token) {
+ trick_taint($token);
+ $dbh->do('UPDATE tokens SET eventdata = ? WHERE token = ?', undef,
+ ("remo_form_payment:" . $attachment->id, $token));
+ }
+
+ $dbh->bz_commit_transaction;
+
+ # Define the variables and functions that will be passed to the UI template.
+ $vars->{'attachment'} = $attachment;
+ $vars->{'bugs'} = [ new Bugzilla::Bug($bug_id) ];
+ $vars->{'header_done'} = 1;
+ $vars->{'contenttypemethod'} = 'autodetect';
+
+ my $recipients = { 'changer' => $user };
+ $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bug_id, $recipients);
+
+ print $cgi->header();
+ # Generate and return the UI (HTML page) from the appropriate template.
+ $template->process("attachment/created.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+ exit;
+ }
+ else {
+ $vars->{'token'} = issue_session_token('remo_form_payment:');
+ }
+}
+
+my %CSV_COLUMNS = (
+ "Date Required" => { pos => 1, value => '%cf_due_date' },
+ "Requester" => { pos => 2, value => 'Konstantina Papadea' },
+ "Email 1" => { pos => 3, value => 'kpapadea@mozilla.com' },
+ "Mozilla Space" => { pos => 4, value => 'Remote' },
+ "Team" => { pos => 5, value => 'Community Engagement' },
+ "Department Code" => { pos => 6, value => '2300' },
+ "Purpose" => { pos => 7, value => 'Rep event: %eventpage' },
+ "Item 1" => { pos => 8 },
+ "Item 2" => { pos => 9 },
+ "Item 3" => { pos => 10 },
+ "Item 4" => { pos => 11 },
+ "Item 5" => { pos => 12 },
+ "Item 6" => { pos => 13 },
+ "Item 7" => { pos => 14 },
+ "Item 8" => { pos => 15 },
+ "Item 9" => { pos => 16 },
+ "Item 10" => { pos => 17 },
+ "Item 11" => { pos => 18 },
+ "Item 12" => { pos => 19 },
+ "Item 13" => { pos => 20 },
+ "Item 14" => { pos => 21 },
+ "Recipient Name" => { pos => 22, value => '%shiptofirstname %shiptolastname' },
+ "Email 2" => { pos => 23, value => sub { Bugzilla->user->email } },
+ "Address 1" => { pos => 24, value => '%shiptoaddress1' },
+ "Address 2" => { pos => 25, value => '%shiptoaddress2' },
+ "City" => { pos => 26, value => '%shiptocity' },
+ "State" => { pos => 27, value => '%shiptostate' },
+ "Zip" => { pos => 28, value => '%shiptopcode' },
+ "Country" => { pos => 29, value => '%shiptocountry' },
+ "Phone number" => { pos => 30, value => '%shiptophone' },
+ "Notes" => { pos => 31, value => '%shipadditional' },
+);
+
+sub _expand_value {
+ my $value = shift;
+ if (ref $value && ref $value eq 'CODE') {
+ return $value->();
+ }
+ else {
+ my $cgi = Bugzilla->cgi;
+ $value =~ s/%(\w+)/$cgi->param($1)/ge;
+ return $value;
+ }
+}
+
+sub _csv_quote {
+ my $s = shift;
+ $s =~ s/"/""/g;
+ return qq{"$s"};
+}
+
+sub _csv_line {
+ return join(",", map { _csv_quote($_) } @_);
+}
+
+sub _csv_encode {
+ return join("\r\n", map { _csv_line(@$_) } @_) . "\r\n";
+}
+
+sub post_bug_after_creation {
+ my ($self, $args) = @_;
+ my $vars = $args->{vars};
+ my $bug = $vars->{bug};
+ my $template = Bugzilla->template;
+
+ if (Bugzilla->input_params->{format}
+ && Bugzilla->input_params->{format} eq 'remo-swag')
+ {
+ # If the attachment cannot be successfully added to the bug,
+ # we notify the user, but we don't interrupt the bug creation process.
+ my $error_mode_cache = Bugzilla->error_mode;
+ Bugzilla->error_mode(ERROR_MODE_DIE);
+
+ my @attachments;
+ eval {
+ my $xml;
+ $template->process("bug/create/create-remo-swag.xml.tmpl", {}, \$xml)
+ || ThrowTemplateError($template->error());
+
+ push @attachments, Bugzilla::Attachment->create(
+ { bug => $bug,
+ creation_ts => $bug->creation_ts,
+ data => $xml,
+ description => 'Remo Swag Request (XML)',
+ filename => 'remo-swag.xml',
+ ispatch => 0,
+ isprivate => 0,
+ mimetype => 'text/xml',
+ });
+
+ my @columns_raw = sort { $CSV_COLUMNS{$a}{pos} <=> $CSV_COLUMNS{$b}{pos} } keys %CSV_COLUMNS;
+ my @data = map { _expand_value( $CSV_COLUMNS{$_}{value} ) } @columns_raw;
+ my @columns = map { s/^(Item|Email) \d+$/$1/g; $_ } @columns_raw;
+ my $csv = _csv_encode(\@columns, \@data);
+
+ push @attachments, Bugzilla::Attachment->create({
+ bug => $bug,
+ creation_ts => $bug->creation_ts,
+ data => $csv,
+ description => 'Remo Swag Request (CSV)',
+ filename => 'remo-swag.csv',
+ ispatch => 0,
+ isprivate => 0,
+ mimetype => 'text/csv',
+ });
+ };
+ if ($@) {
+ warn "$@";
+ }
+
+ if (@attachments) {
+ # Insert comment for attachment
+ foreach my $attachment (@attachments) {
+ $bug->add_comment('', { isprivate => 0,
+ type => CMT_ATTACHMENT_CREATED,
+ extra_data => $attachment->id });
+ }
+ $bug->update($bug->creation_ts);
+ delete $bug->{attachments};
+ }
+ else {
+ $vars->{'message'} = 'attachment_creation_failed';
+ }
+
+ Bugzilla->error_mode($error_mode_cache);
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/REMO/template/en/default/bug/create/comment-mozreps.txt.tmpl b/extensions/REMO/template/en/default/bug/create/comment-mozreps.txt.tmpl
new file mode 100644
index 000000000..95ab1c3e4
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/comment-mozreps.txt.tmpl
@@ -0,0 +1,95 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the REMO Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s): Byron Jones <glob@mozilla.com>
+ #%]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+First Name:
+[%+ cgi.param('first_name') %]
+
+Last Name:
+[%+ cgi.param('last_name') %]
+
+Under 18 years old:
+[%+ IF cgi.param('underage') %]Yes[% ELSE %]No[% END %]
+
+Sex:
+[%+ cgi.param('sex') %]
+
+City:
+[%+ cgi.param('city') %]
+
+Country:
+[%+ cgi.param('country') %]
+
+Local Community:
+[% IF cgi.param('community') %]
+[%+ cgi.param('community') %]
+[% ELSE %]
+-
+[% END %]
+
+IM:
+[% IF cgi.param('im') %]
+[%+ cgi.param('im') %]
+[% ELSE %]
+-
+[% END %]
+
+Mozillians.org Account:
+[% IF cgi.param('mozillian') %]
+[%+ cgi.param('mozillian') %]
+[% ELSE %]
+-
+[% END %]
+
+References:
+[% IF cgi.param('references') %]
+[%+ cgi.param('references') %]
+[% ELSE %]
+-
+[% END %]
+
+Currently Involved with Mozilla:
+[% IF cgi.param('involved') %]
+[%+ cgi.param('involved') %]
+[% ELSE %]
+-
+[% END %]
+
+When First Contributed:
+[% IF cgi.param('firstcontribute') %]
+[%+ cgi.param('firstcontribute') %]
+[% ELSE %]
+-
+[% END %]
+
+Languages Spoken:
+[%+ cgi.param('languages') %]
+
+How did you learn about Mozilla Reps:
+[%+ cgi.param('learn') %]
+
+What motivates you most about joining Mozilla Reps:
+[%+ cgi.param('motivation') %]
+
+Comments:
+[% IF cgi.param('comments') %]
+[%+ cgi.param('comments') %]
+[% ELSE %]
+-
+[% END %]
diff --git a/extensions/REMO/template/en/default/bug/create/comment-remo-budget.txt.tmpl b/extensions/REMO/template/en/default/bug/create/comment-remo-budget.txt.tmpl
new file mode 100644
index 000000000..40b08331b
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/comment-remo-budget.txt.tmpl
@@ -0,0 +1,57 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham <gerv@gerv.net>
+ #%]
+[%# INTERFACE:
+ # This template has no interface.
+ #
+ # Form variables from a bug submission (i.e. the fields on a template from
+ # enter_bug.cgi) can be access via Bugzilla.cgi.param. It can be used to
+ # pull out various custom fields and format an initial Description entry
+ # from them.
+ #%]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Requester info:
+
+Requester: [% cgi.param('firstname') %] [%+ cgi.param('lastname') %]
+Profile page: [% cgi.param('profilepage') %]
+Event page: [% cgi.param('eventpage') %]
+Event hosted by a Firefox Student Ambassador(s)?: [% cgi.param('ambassador_hosted') %]
+Main audience of the event are Firefox Student Ambassadors: [% cgi.param('ambassador_audience') %]
+Mentor Email: [% cgi.param('mentoremail') %]
+Paypal Account: [% cgi.param('paypal') %]
+Country You Reside: [% cgi.param('country') %]
+Advance payment needed: [% IF cgi.param('advancepayment') %]Yes[% ELSE %]No[% END %]
+
+Budget breakdown:
+
+Total amount requested in $USD: [% cgi.param('budgettotal') %]
+Costs per service:
+Service 1: [% cgi.param('service1') %] Cost: [% cgi.param('cost1') %]
+Service 2: [% cgi.param('service2') %] Cost: [% cgi.param('cost2') %]
+Service 3: [% cgi.param('service3') %] Cost: [% cgi.param('cost3') %]
+Service 4: [% cgi.param('service4') %] Cost: [% cgi.param('cost4') %]
+Service 5: [% cgi.param('service5') %] Cost: [% cgi.param('cost5') %]
+
+Additional costs: (add comment box)
+[% cgi.param('costadditional') %]
+
+[%+ cgi.param("comment") IF cgi.param("comment") %]
+
diff --git a/extensions/REMO/template/en/default/bug/create/comment-remo-it.txt.tmpl b/extensions/REMO/template/en/default/bug/create/comment-remo-it.txt.tmpl
new file mode 100644
index 000000000..7e95dd017
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/comment-remo-it.txt.tmpl
@@ -0,0 +1,79 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Name:
+[%+ cgi.param('name') %]
+
+Mozillians.org Profile:
+[%+ cgi.param('mozillian') %]
+
+Reps Profile:
+[%+ cgi.param('reps') || "-" %]
+
+Community Name:
+[%+ cgi.param('community') %]
+
+[% FOREACH item = cgi.param('items') %]
+[% IF item == "apps_email" || item == "domain" || item == "ssl" %]
+[% IF item == "apps_email" %]
+[% domain_title = domain_title _ ":: Google Apps Emails\n" %]
+[% END %]
+[% IF item == "domain" %]
+[% domain_title = domain_title _ ":: Domain Name\n" %]
+[% END %]
+[% IF item == "ssl" %]
+[% domain_title = domain_title _ ":: SSL\n" %]
+[% END %]
+[% END %]
+[% END %]
+
+[% FOREACH item = cgi.param('items') %]
+[% IF item == "hosting" %]
+:: Hosting
+
+Expected visits per month:
+[%+ cgi.param('hosting_visits') %]
+
+What will run on the hosting:
+[%+ cgi.param('hosting_running') %]
+
+Hosting data:
+[%+ cgi.param('hosting_data') || "-" %]
+
+[% ELSIF (item == "apps_email" || item == "domain" || item == "ssl")
+ && domain_title %]
+[% domain_title FILTER html %]
+[% domain_title = "" %]
+Domain Name:
+[%+ cgi.param('domain_name') %]
+
+[% ELSIF item == "discourse" %]
+:: Discourse Category
+
+Category Names:
+[%+ cgi.param('discourse_names') %]
+
+Moderators:
+[%+ cgi.param('discourse_mods') %]
+
+Background Hex Code:
+[%+ cgi.param('discourse_bg') || "-" %]
+
+[% ELSIF item == "other" %]
+:: Other
+
+[%+ cgi.param('other_value') %]
+
+[% END %]
+[% END %]
+
+Comments:
+[%+ cgi.param('comments') || "-" %]
diff --git a/extensions/REMO/template/en/default/bug/create/comment-remo-swag.txt.tmpl b/extensions/REMO/template/en/default/bug/create/comment-remo-swag.txt.tmpl
new file mode 100644
index 000000000..ef7419dc9
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/comment-remo-swag.txt.tmpl
@@ -0,0 +1,73 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham <gerv@gerv.net>
+ #%]
+[%# INTERFACE:
+ # This template has no interface.
+ #
+ # Form variables from a bug submission (i.e. the fields on a template from
+ # enter_bug.cgi) can be access via Bugzilla.cgi.param. It can be used to
+ # pull out various custom fields and format an initial Description entry
+ # from them.
+ #%]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Requester info:
+
+First name: [% cgi.param('firstname') %]
+Last name: [% cgi.param('lastname') %]
+Profile page: [% cgi.param('profilepage') %]
+Event name: [% cgi.param('eventname') %]
+Event page: [% cgi.param('eventpage') %]
+Estimated attendance: [% cgi.param('attendance') %]
+Event hosted by a Firefox Student Ambassador(s)?: [% cgi.param('ambassador_hosted') %]
+Main audience of the event are Firefox Student Ambassadors: [% cgi.param('ambassador_audience') %]
+
+Shipping details:
+
+Ship swag before: [% cgi.param('cf_due_date') %]
+
+First name: [% cgi.param("shiptofirstname") %]
+Last name: [% cgi.param("shiptolastname") %]
+Address line 1: [% cgi.param("shiptoaddress1") %]
+Address line 2: [% cgi.param("shiptoaddress2") %]
+City: [% cgi.param("shiptocity") %]
+State/Region: [% cgi.param("shiptostate") %]
+Postal code: [% cgi.param("shiptopcode") %]
+Country: [% cgi.param("shiptocountry") %]
+Phone: [% cgi.param("shiptophone") %]
+[%+ IF cgi.param("shiptoidrut") %]Custom reference: [% cgi.param("shiptoidrut") %][% END %]
+
+Addition information for delivery person:
+[%+ cgi.param('shipadditional') %]
+
+Swag requested:
+
+Stickers: [% IF cgi.param('stickers') %]Yes[% ELSE %]No[% END %]
+Buttons: [% IF cgi.param('buttons') %]Yes[% ELSE %]No[% END %]
+Lanyards: [% IF cgi.param('lanyards') %]Yes[% ELSE %]No[% END %]
+T-shirts: [% IF cgi.param('tshirts') %]Yes[% ELSE %]No[% END %]
+Roll-up banners: [% IF cgi.param('rollupbanners') %]Yes[% ELSE %]No[% END %]
+Horizontal banner: [% IF cgi.param('horizontalbanner') %]Yes[% ELSE %]No[% END %]
+Booth cloth: [% IF cgi.param('boothcloth') %]Yes[% ELSE %]No[% END %]
+Pens: [% IF cgi.param('pens') %]Yes[% ELSE %]No[% END %]
+Other: [% IF cgi.param('otherswag') %][% cgi.param('otherswag') %][% ELSE %]No[% END %]
+
+[%+ cgi.param("comment") IF cgi.param("comment") %]
+
diff --git a/extensions/REMO/template/en/default/bug/create/create-mozreps.html.tmpl b/extensions/REMO/template/en/default/bug/create/create-mozreps.html.tmpl
new file mode 100644
index 000000000..be461c795
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/create-mozreps.html.tmpl
@@ -0,0 +1,247 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the REMO Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s): Byron Jones <glob@mozilla.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Reps - Application Form"
+ style_urls = [ "extensions/REMO/web/styles/moz_reps.css" ]
+%]
+
+[% USE Bugzilla %]
+[% mandatory = '<span class="mandatory" title="Required">*</span>' %]
+
+<script type="text/javascript">
+var Dom = YAHOO.util.Dom;
+
+function mandatory(ids) {
+ result = true;
+ for (i in ids) {
+ id = ids[i];
+ el = Dom.get(id);
+
+ if (el.type.toString() == "checkbox") {
+ value = el.checked;
+ } else {
+ value = el.value.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
+ el.value = value;
+ }
+
+ if (value == '') {
+ Dom.addClass(id, 'missing');
+ result = false;
+ } else {
+ Dom.removeClass(id, 'missing');
+ }
+ }
+ return result;
+}
+
+function underageWarning (el) {
+ if (el.checked) {
+ Dom.removeClass('underage_warning', 'bz_default_hidden');
+ Dom.get('submit').disabled = true;
+ }
+ else {
+ Dom.addClass('underage_warning', 'bz_default_hidden');
+ Dom.get('submit').disabled = false;
+ }
+}
+
+function submitForm() {
+ if (!mandatory([ 'first_name', 'last_name', 'sex', 'city', 'country',
+ 'mozillian', 'languages', 'learn', 'motivation', 'privacy' ])
+ ) {
+ alert('Please enter all the required fields.');
+ return false;
+ }
+
+ Dom.get('short_desc').value =
+ "Application Form: " + Dom.get('first_name').value + ' ' + Dom.get('last_name').value;
+
+ return true;
+}
+
+</script>
+
+<noscript>
+<h1>Javascript is required to use this form.</h1>
+</noscript>
+
+<h1>Mozilla Reps - Application Form</h1>
+
+<p>
+ If you have questions while completing this form, please contact the
+ <a href="mailto:reps-council@lists.mozilla.org">Reps Council</a> for
+ assistance.
+</p>
+
+<form method="post" action="post_bug.cgi" id="tmRequestForm">
+<input type="hidden" name="product" value="Mozilla Reps">
+<input type="hidden" name="component" value="Mentorship">
+<input type="hidden" name="bug_severity" value="normal">
+<input type="hidden" name="rep_platform" value="All">
+<input type="hidden" name="priority" value="--">
+<input type="hidden" name="op_sys" value="Other">
+<input type="hidden" name="version" value="unspecified">
+<input type="hidden" name="groups" value="mozilla-reps">
+<input type="hidden" name="format" value="[% format FILTER html %]">
+<input type="hidden" name="created-format" value="[% format FILTER html %]">
+<input type="hidden" name="comment" id="comment" value="">
+<input type="hidden" name="short_desc" id="short_desc" value="">
+<input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table id="reps-form">
+
+<tr class="odd">
+ <th>First Name:[% mandatory FILTER none %]</th>
+ <td><input id="first_name" name="first_name" size="40" placeholder="John"></td>
+</tr>
+
+<tr class="even">
+ <th>Last Name:[% mandatory FILTER none %]</th>
+ <td><input id="last_name" name="last_name" size="40" placeholder="Doe"></td>
+</tr>
+
+<tr class="odd">
+ <th>Are you under 18 years old?:</th>
+ <td>
+ <input type="checkbox" id="underage" name="underage"
+ value="1" onclick="underageWarning(this);"><br>
+ </td>
+</tr>
+
+<tr id="underage_warning" class="odd bz_default_hidden">
+ <td colspan="2">
+ Mozilla Reps program is not currently accepting people under 18 years old.
+ Sorry for the inconvenience. In the meantime please check with your local Mozilla
+ group for other contribution opportunities
+ </td>
+</tr>
+
+<tr class="even">
+ <th>Sex:[% mandatory FILTER none %]</th>
+ <td>
+ <select id="sex" name="sex">
+ <option value="Male">Male</option>
+ <option value="Female">Female</option>
+ <option value="Other">Other</option>
+ </select>
+ </td>
+</tr>
+
+<tr class="odd">
+ <th>City:[% mandatory FILTER none %]</th>
+ <td><input id="city" name="city" size="40" placeholder="Your city"></td>
+</tr>
+
+<tr class="even">
+ <th>Country:[% mandatory FILTER none %]</th>
+ <td><input id="country" name="country" size="40" placeholder="Your country"></td>
+</tr>
+
+<tr class="odd">
+ <th>Local Community you participate in:</th>
+ <td><input id="community" name="community" size="40" placeholder="Name of your community"></td>
+</tr>
+
+<tr class="even">
+ <th>IM (specify service):</th>
+ <td><input id="im" name="im" size="40"></td>
+</tr>
+
+<tr class="odd">
+ <th>Mozillians.org Account:[% mandatory FILTER none %]</th>
+ <td><input id="mozillian" name="mozillian" size="40"></td>
+</tr>
+
+<tr class="even">
+ <th colspan="2">
+ References:
+ </th>
+</tr>
+<tr class="even">
+ <td colspan="2">
+ <textarea id="references" name="references" rows="4"
+ placeholder="Add contact info of people referencing you."></textarea>
+ </td>
+</tr>
+
+<tr class="odd">
+ <th colspan="2">
+ How are you involved with Mozilla?
+ </th>
+</tr>
+<tr class="odd">
+ <td colspan="2">
+ <textarea id="involved" name="involved" rows="4" placeholder="Add-ons, l10n, SUMO, QA, ..."></textarea>
+ </td>
+</tr>
+
+<tr class="even">
+ <th>
+ When did you first start contributing to Mozilla?
+ </th>
+ <td><input id="firstcontribute" name="firstcontribute" size="40"></td>
+</tr>
+
+<tr class="odd">
+ <th>Languages Spoken:[% mandatory FILTER none %]</th>
+ <td><input id="languages" name="languages" size="40"></td>
+</tr>
+
+<tr class="even">
+ <th>How did you learn about Mozilla Reps?[% mandatory FILTER none %]</th>
+ <td><input id="learn" name="learn" size="40"></td>
+</tr>
+
+<tr class="odd">
+ <th colspan="2">What motivates you most about joining Mozilla Reps?[% mandatory FILTER none %]</th>
+</tr>
+<tr class="odd">
+ <td colspan="2"><textarea id="motivation" name="motivation" rows="4"></textarea></td>
+</tr>
+
+<tr class="even">
+ <th colspan="2">Comments:</th>
+</tr>
+<tr class="even">
+ <td colspan="2"><textarea id="comments" name="comments" rows="4"></textarea></td>
+</tr>
+
+<tr class="odd">
+ <th>
+ I have read the
+ <a href="http://www.mozilla.com/en-US/privacy-policy" target="_blank">Mozilla Privacy Policy</a>:[% mandatory FILTER none %]
+ </th>
+ <td><input id="privacy" type="checkbox"></td>
+</tr>
+
+<tr class="even">
+ <td>&nbsp;</td>
+ <td align="right">
+ <input id="submit" type="submit" value="Submit" onclick="return submitForm()">
+ </td>
+</tr>
+
+</table>
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/REMO/template/en/default/bug/create/create-remo-budget.html.tmpl b/extensions/REMO/template/en/default/bug/create/create-remo-budget.html.tmpl
new file mode 100644
index 000000000..6e393612c
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/create-remo-budget.html.tmpl
@@ -0,0 +1,295 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Reps Budget Request Form"
+ style_urls = [ 'extensions/REMO/web/styles/moz_reps.css' ]
+ javascript_urls = [ 'extensions/REMO/web/js/form_validate.js',
+ 'js/util.js',
+ 'js/field.js' ]
+ yui = [ 'autocomplete', 'calendar' ]
+%]
+
+[% IF user.in_group("mozilla-reps") %]
+
+<p>These requests will only be visible to the person who submitted the request,
+any persons designated in the CC line, and authorized members of the Mozilla
+Rep team.</p>
+
+<script language="javascript" type="text/javascript">
+function trySubmit() {
+ var firstname = document.getElementById('firstname').value;
+ var lastname = document.getElementById('lastname').value;
+ var eventpage = document.getElementById('eventpage').value;
+ var shortdesc = 'Budget Request - ' + firstname + ' ' + lastname + ' - ' + eventpage;
+ document.getElementById('short_desc').value = shortdesc;
+ document.getElementById('cc').value = document.getElementById('mentoremail').value;
+ return true;
+}
+
+function validateAndSubmit() {
+ var alert_text = '';
+ if(!isFilledOut('firstname')) alert_text += "Please enter your first name\n";
+ if(!isFilledOut('lastname')) alert_text += "Please enter your last name\n";
+ if(!isFilledOut('profilepage')) alert_text += "Please enter a Mozilla Reps profile page.\n";
+ if(!isFilledOut('eventpage')) alert_text += "Please enter an event page address.\n";
+ if(!isFilledOut('cf_due_date')) alert_text += "Please enter an event date.\n";
+ if(!isFilledOut('ambassador_hosted')) alert_text += "Please select whether this event is hosted by ambassadors.\n";
+ if(!isFilledOut('ambassador_audience')) alert_text += "Please select whether this event's main audience is ambassadors.\n";
+ if(!isFilledOut('mentoremail')) alert_text += "Please enter a valid [% terms.Bugzilla %] email for mentor.\n";
+ if(!isFilledOut('country')) alert_text += "Please enter a valid value for country.\n";
+ if(!isFilledOut('budgettotal')) alert_text += "Please enter the total budget for the event.\n";
+ if(!isFilledOut('service1') || !isFilledOut('cost1')) alert_text += "Please enter at least one service and cost value.\n";
+
+ //Everything required is filled out..try to submit the form!
+ if(alert_text == '') {
+ return trySubmit();
+ }
+
+ //alert text, stay here on the pagee
+ alert(alert_text);
+ return false;
+}
+</script>
+
+<h1>Mozilla Reps - Budget Request Form</h1>
+
+<p>
+ If your request is Community IT related please file it
+ <a href="https://bugzilla.mozilla.org/enter_bug.cgi?product=Mozilla%20Reps;component=Community%20IT%20Requests">here</a>.
+</p>
+
+<p>
+ <span class="required_star">*</span> - <span class="required_explanation">Required Fields</span>
+</p>
+
+<form method="post" action="post_bug.cgi" id="swagRequestForm" enctype="multipart/form-data"
+ onSubmit="return validateAndSubmit();">
+
+ <input type="hidden" name="format" value="remo-budget">
+ <input type="hidden" name="created-format" value="remo-budget">
+ <input type="hidden" name="product" value="Mozilla Reps">
+ <input type="hidden" name="component" value="Budget Requests">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="priority" value="--">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="short_desc" id="short_desc" value="">
+ <input type="hidden" name="cc" id="cc" value="">
+ <input type="hidden" name="groups" value="mozilla-reps">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table id="reps-form">
+
+<tr class="odd">
+ <th class="field_label required">First Name:</th>
+ <td>
+ <input type="text" name="firstname" id="firstname" value="" size="40" placeholder="John">
+ </td>
+</tr>
+
+<tr class="even">
+ <th class="field_label required">Last Name:</th>
+ <td>
+ <input type="text" name="lastname" id="lastname" value="" size="40" placeholder="Doe">
+ </td>
+</tr>
+
+<tr class="odd">
+ <th class="field_label required">Mozilla Reps Profile Page:</th>
+ <td>
+ <input type="text" name="profilepage" id="profilepage"
+ value="" size="40" placeholder="https://reps.mozilla.org/u/JohnDoe">
+ </td>
+</tr>
+
+<tr class="even">
+ <th class="field_label required">Event Page:</th>
+ <td>
+ <input type="text" name="eventpage" id="eventpage"
+ value="" size="40" placeholder="https://reps.mozilla.org/e/TestEvent">
+ </td>
+</tr>
+
+<tr class="odd">
+ <th class="field_label required">Event Date:</th>
+ <td>
+ <input name="cf_due_date" size="20" id="cf_due_date" value=""
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_cf_due_date"
+ onclick="showCalendar('cf_due_date')">
+ <span>Calendar</span>
+ </button>
+ <div id="con_calendar_cf_due_date"></div>
+ <script type="text/javascript">
+ createCalendar('cf_due_date')
+ </script>
+ </td>
+</tr>
+
+<tr class="even">
+ <th class="field_label required">
+ Is this event being hosted by a<br>Firefox Student Ambassador(s)?:
+ </th>
+ <td>
+ <select id="ambassador_hosted" name="ambassador_hosted">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </td>
+</tr>
+
+<tr class="odd">
+ <th class="field_label required">
+ Is the main audience of this event<br>Firefox Student Ambassadors?:
+ </th>
+ <td>
+ <select id="ambassador_audience" name="ambassador_audience">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </td>
+</tr>
+
+<tr class="even">
+ <th class="field_label required">[% terms.Bugzilla %] Email of Your Mentor:</th>
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "mentoremail"
+ name => "mentoremail"
+ value => ""
+ size => 40
+ %]
+ </td>
+</tr>
+
+<tr class="odd">
+ <th class="field_label">Paypal Account Email:</th>
+ <td>
+ <input type="text" name="paypal" id="paypal"
+ value="" size="40" placeholder=""><br>
+ <span style="font-size: smaller;">
+ * Currently, you CANNOT make payments using other online payment services.</span>
+ </td>
+</tr>
+
+<tr class="even">
+ <th class="field_label required">Country You Reside:</th>
+ <td>
+ <input type="text" name="country" id="country"
+ value="" size="40" placeholder="USA">
+ </td>
+</tr>
+
+<tr class="odd">
+ <th class="field_label">Is advance payment needed?</th>
+ <td>
+ <input type="checkbox" name="advancepayment" id="advancepayment" value="1">
+ </td>
+</tr>
+
+<tr class="even">
+ <td><!--spacer-->&nbsp;</td>
+ <td><!--spacer-->&nbsp;</td>
+</tr>
+
+<tr class="odd">
+ <th colspan="2" class="field_label">Budget Request:</th>
+</tr>
+
+<tr class="odd">
+ <th class="field_label required">Total amount requested in $USD:</th>
+ <td>
+ <input type="text" name="budgettotal" id="budgettotal" value="" size="40">
+ </td>
+ </tr>
+
+<tr class="odd">
+ <th colspan="2" class="field_label">Costs per service:</th>
+</tr>
+
+<tr class="odd">
+ <td colspan="2">
+ <table>
+ <tr>
+ <th class="field_label required">Service 1:</th>
+ <td><input type="text" id="service1" name="service1" size="30"></td>
+ <th class="field_label required">Cost 1:</th>
+ <td><input type="text" id="cost1" name="cost1" size="30"></td>
+ </tr>
+ <tr>
+ <th class="field_lable">Service 2:</th>
+ <td><input type="text" id="service2" name="service2" size="30"></td>
+ <th class="field_lable">Cost 2:</th>
+ <td><input type="text" id="cost2" name="cost2" size="30"></td>
+ </tr>
+ <tr>
+ <th class="field_lable">Service 3:</th>
+ <td><input type="text" id="service3" name="service3" size="30"></td>
+ <th class="field_lable">Cost 3:</th>
+ <td><input type="text" id="cost3" name="cost3" size="30"></td>
+ </tr>
+ <tr>
+ <th class="field_lable">Service 4:</th>
+ <td><input type="text" id="service4" name="service4" size="30"></td>
+ <th class="field_lable">Cost 4:</th>
+ <td><input type="text" id="cost4" name="cost4" size="30"></td>
+ </tr>
+ <tr>
+ <th class="field_lable">Service 5:</th>
+ <td><input type="text" id="service5" name="service5" size="30"></td>
+ <th class="field_lable">Cost 5:</th>
+ <td><input type="text" id="cost5" name="cost5" size="30"></td>
+ </tr>
+ </table>
+ </td>
+</tr>
+
+<tr class="odd">
+ <th colspan="2" class="field_label">Additional costs:</th>
+</tr>
+
+<tr class="odd">
+ <td colspan="2">
+ <textarea id="costadditional" name="costadditional" rows="5" cols="50"></textarea>
+ </td>
+</tr>
+
+<tr class="even">
+ <td>&nbsp;</td>
+ <td align="right">
+ <input type="submit" id="commit" value="Submit Request">
+ </td>
+</tr>
+
+</table>
+
+</form>
+
+<p style="font-weight:bold;">
+ Budget requests received less than 3 weeks before the targeted launch date of the
+ event/activity in question will automatically be rejected (exceptions can be made
+ but only with council approval). This 3-week “buffer†guarantees that each budget
+ request undergoes the same thorough selection process.
+</p>
+
+<p>
+ Thanks for contacting us.
+</p>
+
+[% ELSE %]
+ <p>Sorry, you do not have access to this page.</p>
+[% END %]
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/REMO/template/en/default/bug/create/create-remo-it.html.tmpl b/extensions/REMO/template/en/default/bug/create/create-remo-it.html.tmpl
new file mode 100644
index 000000000..a1085ae97
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/create-remo-it.html.tmpl
@@ -0,0 +1,294 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ onload = "init()"
+ title = "Community IT Request"
+ style_urls = [ "extensions/REMO/web/styles/moz_reps.css" ]
+%]
+
+[% USE Bugzilla %]
+[% mandatory = '<span class="mandatory" title="Required">*</span>' %]
+
+<script type="text/javascript">
+var Dom = YAHOO.util.Dom;
+
+function mandatory(elements) {
+ result = true;
+ for (i in elements) {
+ element = elements[i];
+
+ if (typeof(element) == "object") {
+ missing = true;
+ for (j = 0; j < element.length; j++) {
+ if (element[j].checked) {
+ missing = false;
+ break;
+ }
+ }
+
+ if (missing) {
+ Dom.addClass(element[0].name, 'missing');
+ } else {
+ Dom.removeClass(element[0].name, 'missing');
+ }
+ } else {
+ el = Dom.get(element);
+ value = el.value.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
+ el.value = value;
+
+ if (value == '') {
+ Dom.addClass(element, 'missing');
+ result = false;
+ } else {
+ Dom.removeClass(element, 'missing');
+ }
+ }
+ }
+ return result;
+}
+
+function submitForm() {
+ fields = [ 'name', 'mozillian', 'community', document.forms.f.items ];
+ if (Dom.get('item_hosting').checked) {
+ fields.push('hosting_visits');
+ fields.push('hosting_running');
+ }
+ if (Dom.get('item_domain').checked
+ || Dom.get('item_apps_email').checked
+ || Dom.get('item_ssl').checked
+ ) {
+ fields.push('domain_name');
+ }
+ if (Dom.get('item_discourse').checked) {
+ fields.push('discourse_names');
+ fields.push('discourse_mods');
+ }
+ if (Dom.get('item_other').checked) {
+ fields.push('other_value');
+ }
+
+ if (!mandatory(fields)) {
+ alert('Please enter all the required fields.');
+ return false;
+ }
+
+ Dom.get('short_desc').value =
+ "IT Request: " + Dom.get('community').value + ' (' + Dom.get('name').value + ')';
+ return true;
+}
+
+function setItemVisibility() {
+ if (Dom.get('item_hosting').checked) {
+ Dom.removeClass('hosting', 'bz_default_hidden');
+ } else {
+ Dom.addClass('hosting', 'bz_default_hidden');
+ }
+ if (Dom.get('item_domain').checked
+ || Dom.get('item_apps_email').checked
+ || Dom.get('item_ssl').checked
+ ) {
+ var title = [];
+ if (Dom.get('item_apps_email').checked)
+ title.push('Google Apps Email');
+ if (Dom.get('item_domain').checked)
+ title.push('Domain');
+ if (Dom.get('item_ssl').checked)
+ title.push('SSL');
+ Dom.get('domain_title').innerHTML = title.join(', ');
+ Dom.removeClass('domain', 'bz_default_hidden');
+ } else {
+ Dom.addClass('domain', 'bz_default_hidden');
+ }
+ if (Dom.get('item_discourse').checked) {
+ Dom.removeClass('discourse', 'bz_default_hidden');
+ } else {
+ Dom.addClass('discourse', 'bz_default_hidden');
+ }
+ if (Dom.get('item_other').checked) {
+ Dom.removeClass('other', 'bz_default_hidden');
+ } else {
+ Dom.addClass('other', 'bz_default_hidden');
+ }
+}
+
+function init() {
+ items = document.forms.f.items;
+ for (i = 0; i < items.length; i++) {
+ YAHOO.util.Event.on(items[i], 'click', setItemVisibility);
+ }
+ setItemVisibility();
+}
+
+</script>
+
+<noscript>
+<h1>Javascript is required to use this form.</h1>
+</noscript>
+
+<h1>Community IT Request</h1>
+
+<form method="post" action="post_bug.cgi" id="tmRequestForm" name="f">
+<input type="hidden" name="product" value="Mozilla Reps">
+<input type="hidden" name="component" value="Community IT Requests">
+<input type="hidden" name="bug_severity" value="normal">
+<input type="hidden" name="rep_platform" value="All">
+<input type="hidden" name="priority" value="--">
+<input type="hidden" name="op_sys" value="Other">
+<input type="hidden" name="version" value="unspecified">
+<input type="hidden" name="groups" value="mozilla-reps">
+<input type="hidden" name="format" value="[% format FILTER html %]">
+<input type="hidden" name="comment" id="comment" value="">
+<input type="hidden" name="short_desc" id="short_desc" value="">
+<input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table id="reps-form">
+
+<tr class="odd">
+ <th>Your Name:[% mandatory FILTER none %]</th>
+ <td><input id="name" name="name" size="40" value="[% user.name FILTER html %]"></td>
+</tr>
+
+<tr class="even">
+ <th>Mozillians.org Profile:[% mandatory FILTER none %]</th>
+ <td><input id="mozillian" name="mozillian" size="40"></td>
+</tr>
+
+<tr class="odd">
+ <th>Reps Profile (if applicable):</th>
+ <td><input id="reps" name="reps" size="40"></td>
+</tr>
+
+<tr class="even">
+ <th>Your Community's Name:[% mandatory FILTER none %]</th>
+ <td><input id="community" name="community" size="40"></td>
+</tr>
+
+<tr class="odd">
+ <th>
+ Items Requesting:[% mandatory FILTER none %]
+ </th>
+ <td>
+ <div id="items">
+ <div>
+ <input type="checkbox" name="items" value="hosting" id="item_hosting">
+ <label for="item_hosting">Hosting</label>
+ </div>
+ <div>
+ <input type="checkbox" name="items" value="apps_email" id="item_apps_email">
+ <label for="item_apps_email">Google Apps Emails</label>
+ </div>
+ <div>
+ <input type="checkbox" name="items" value="domain" id="item_domain">
+ <label for="item_domain">Domain</label>
+ </div>
+ <div>
+ <input type="checkbox" name="items" value="discourse" id="item_discourse">
+ <label for="item_discourse">Discourse Category</label>
+ </div>
+ <div>
+ <input type="checkbox" name="items" value="ssl" id="item_ssl">
+ <label for="item_ssl">SSL</label>
+ </div>
+ <div>
+ <input type="checkbox" name="items" value="other" id="item_other">
+ <label for="item_other">Other</label>
+ </div>
+ </div>
+ </td>
+</tr>
+
+<tbody id="hosting">
+<tr class="even">
+ <th colspan="2">Hosting</th>
+</tr>
+<tr class="odd">
+ <th>Expected visits per month:[% mandatory FILTER none %]</th>
+ <td><input id="hosting_visits" name="hosting_visits" size="40"></td>
+</tr>
+<tr class="odd">
+ <th>What will run on the hosting?:[% mandatory FILTER none %]</th>
+ <td><textarea id="hosting_running" name="hosting_running" class="small"></textarea></td>
+</tr>
+<tr class="odd">
+ <th>Data:</td>
+ <td>
+ Any data we can use to help choose the best solution (traffic graphs etc).<br>
+ <textarea id="hosting_data" name="hosting_data" class="small"></textarea>
+ </td>
+</tr>
+</tbody>
+
+<tbody id="domain">
+<tr class="even">
+ <th colspan="2" id="domain_title">Domain</th>
+</tr>
+<tr class="odd">
+ <th>Domain Name:[% mandatory FILTER none %]</th>
+ <td><input id="domain_name" name="domain_name" size="40"></td>
+</tr>
+</tbody>
+
+<tbody id="discourse">
+<tr class="even">
+ <th colspan="2">Discourse Category</th>
+</tr>
+<tr class="odd">
+ <th>Discourse Category Names:[% mandatory FILTER none %]</th>
+ <td><input id="discourse_names" name="discourse_names" size="40"></td>
+</tr>
+<tr class="odd">
+ <th>Moderators:[% mandatory FILTER none %]</th>
+ <td><input id="discourse_mods" name="discourse_mods" size="40"></td>
+</tr>
+<tr class="odd">
+ <th>Hex code of background of category tag:</th>
+ <td><input id="discourse_bg" name="discourse_bg" size="40"></td>
+</tr>
+</tbody>
+
+<tbody id="other">
+<tr class="even">
+ <th colspan="2">Other Item</th>
+</tr>
+<tr class="odd">
+ <th>Other:[% mandatory FILTER none %]</th>
+ <td><input id="other_value" name="other_value" size="40"></td>
+</tr>
+</tbody>
+
+<tr class="even">
+ <th colspan="2">
+ Other Comments
+ </th>
+</tr>
+<tr class="even">
+ <td colspan="2">
+ Please explain why you'd like the hosting, and anything else this form does not include.<br>
+ <textarea id="comments" name="comments" rows="4"></textarea>
+ </td>
+</tr>
+
+<tr class="even">
+ <td colspan="2">
+ <input id="submit" type="submit" value="Submit" onclick="return submitForm()">
+ </td>
+</tr>
+
+<tr class="even">
+ <td width="35%">&nbsp;</td>
+ <td width="65%">&nbsp;</td>
+</tr>
+
+</table>
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/REMO/template/en/default/bug/create/create-remo-swag.html.tmpl b/extensions/REMO/template/en/default/bug/create/create-remo-swag.html.tmpl
new file mode 100644
index 000000000..70fba6cb8
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/create-remo-swag.html.tmpl
@@ -0,0 +1,326 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Reps Swag Request Form"
+ javascript_urls = [ 'extensions/REMO/web/js/swag.js',
+ 'extensions/REMO/web/js/form_validate.js',
+ 'js/field.js',
+ 'js/util.js' ]
+ style_urls = [ "extensions/REMO/web/styles/moz_reps.css" ]
+ yui = [ 'calendar' ]
+%]
+
+[% IF !user.in_group("mozilla-reps") %]
+ <p>Sorry, you do not have access to this page.</p>
+ [% RETURN %]
+[% END %]
+
+
+<p>These requests will only be visible to the person who submitted the request,
+any persons designated in the CC line, and authorized members of the Mozilla Rep team.</p>
+
+<script language="javascript" type="text/javascript">
+function trySubmit() {
+ var eventname = document.getElementById('eventname').value;
+ var shortdesc = 'Swag Request - ' + eventname;
+ document.getElementById('short_desc').value = shortdesc;
+ return true;
+}
+
+function validateAndSubmit() {
+ var alert_text = '';
+ if(!isFilledOut('firstname')) alert_text += "Please enter your first name\n";
+ if(!isFilledOut('lastname')) alert_text += "Please enter your last name\n";
+ if(!isFilledOut('profilepage')) alert_text += "Please enter your Mozilla Reps profile page\n";
+ if(!isFilledOut('eventname')) alert_text += "Please enter your event name\n";
+ if(!isFilledOut('eventpage')) alert_text += "Please enter the event page.\n";
+ if(!isFilledOut('attendance')) alert_text += "Please enter the estimated attendance.\n";
+ if(!isFilledOut('ambassador_hosted')) alert_text += "Please select whether this event is hosted by ambassadors.\n";
+ if(!isFilledOut('ambassador_audience')) alert_text += "Please select whether this event's main audience is ambassadors.\n";
+ if(!isFilledOut('shiptofirstname')) alert_text += "Please enter the shipping first name\n";
+ if(!isFilledOut('shiptolastname')) alert_text += "Please enter the shipping last name\n";
+ if(!isFilledOut('shiptoaddress1')) alert_text += "Please enter the ship to address\n";
+ if(!isFilledOut('shiptocity')) alert_text += "Please enter the ship to city\n";
+ if(!isFilledOut('shiptocountry')) alert_text += "Please enter the ship to country\n";
+ if(!isFilledOut('shiptopcode')) alert_text += "Please enter the ship to postal code\n";
+ if(!isFilledOut('shiptophone')) alert_text += "Please enter the ship to contact number\n";
+
+ //Everything required is filled out..try to submit the form!
+ if(alert_text == '') {
+ return trySubmit();
+ }
+
+ //alert text, stay here on the pagee
+ alert(alert_text);
+ return false;
+}
+
+</script>
+
+<h1>Mozilla Reps - Swag Request Form</h1>
+
+<p>Review the <a href="https://wiki.mozilla.org/ReMo/SOPs/Swag_Requests" target="_blank">
+ Swag Requests SOP</a> before you complete this form.</p>
+
+<form method="post" action="post_bug.cgi" id="swagRequestForm" enctype="multipart/form-data"
+ onSubmit="return validateAndSubmit();">
+
+ <input type="hidden" name="format" value="remo-swag">
+ <input type="hidden" name="product" value="Mozilla Reps">
+ <input type="hidden" name="component" value="Swag Requests">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="priority" value="--">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="short_desc" id="short_desc" value="">
+ <input type="hidden" name="groups" value="mozilla-reps">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table id="reps-form">
+
+<tr class="odd">
+ <td><strong>First Name: <span style="color: red;" title="Required">*</span></strong></td>
+ <td>
+ <input type="text" name="firstname" id="firstname" placeholder="John" size="40">
+ </td>
+</tr>
+
+<tr class="even">
+ <td><strong>Last Name: <span style="color: red;" title="Required">*</span></strong></td>
+ <td>
+ <input type="text" name="lastname" id="lastname" placeholder="Doe" size="40">
+ </td>
+</tr>
+
+<tr class="odd">
+ <td>
+ <strong>Mozilla Reps Profile Page:
+ <span style="color: red;" title="Required">*</span></strong>
+ </td>
+ <td>
+ <input type="text" name="profilepage" id="profilepage" size="40">
+ </td>
+</tr>
+
+<tr class="even">
+ <td><strong>Event Name: <span style="color: red;" title="Required">*</span></strong></td>
+ <td>
+ <input type="text" name="eventname" id="eventname" size="40">
+ </td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Event Page: <span style="color: red;" title="Required">*</span></strong></td>
+ <td>
+ <input type="text" name="eventpage" id="eventpage" size="40">
+ </td>
+</tr>
+
+<tr class="even">
+ <td><strong>Estimated Attendance: <span style="color: red;" title="Required">*</span></strong></td>
+ <td>
+ <select id="attendance" name="attendance">
+ <option value="1-50">1-50</option>
+ <option value="51-200">51-200</option>
+ <option value="201-500">201-500</option>
+ <option value="501-1000+">501-1000+</option>
+ </select>
+ </td>
+</tr>
+
+<tr class="odd">
+ <td>
+ <strong>Is this event being hosted by a<br>Firefox Student Ambassador(s)?:
+ <span style="color: red;" title="Required">*</span></strong>
+ </td>
+ <td>
+ <select id="ambassador_hosted" name="ambassador_hosted">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </td>
+</tr>
+
+<tr class="even">
+ <td>
+ <strong>Is the main audience of this event<br>Firefox Student Ambassadors?:
+ <span style="color: red;" title="Required">*</span></strong>
+ </td>
+ <td>
+ <select id="ambassador_audience" name="ambassador_audience">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </td>
+</tr>
+
+<tr class="odd">
+ <td><!--spacer-->&nbsp;</td>
+ <td><!--spacer-->&nbsp;</td>
+</tr>
+
+<tr class="even">
+ <td colspan="2"><strong>Shipping Details:</strong></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Ship Before:</strong>
+ <td>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default,
+ field = bug_fields.cf_due_date
+ value = default.cf_due_date,
+ editable = 1,
+ no_tds = 1
+ %]
+ </td>
+</tr>
+
+<tr class="even">
+ <td><strong>First Name: <span style="color: red;" title="Required">*</span></strong></td>
+ <td><input name="shiptofirstname" id="shiptofirstname" placeholder="John" size="40"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Last Name: <span style="color: red;" title="Required">*</span></strong></td>
+ <td><input name="shiptolastname" id="shiptolastname" placeholder="Doe" size="40"></td>
+</tr>
+
+<tr class="even">
+ <td><strong>Address Line 1: <span style="color: red;" title="Required">*</span></strong></td>
+ <td><input name="shiptoaddress1" id="shiptoaddress1" placeholder="123 Main St." size="40"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Address Line 2:</strong></td>
+ <td><input name="shiptoaddress2" id="shiptoaddress2" size="40"></td>
+</tr>
+
+<tr class="even">
+ <td><strong>City: <span style="color: red;" title="Required">*</span></strong></td>
+ <td><input name="shiptocity" id="shiptocity" size="40" placeholder="Anytown"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>State/Region (if applicable):</strong></td>
+ <td><input name="shiptostate" id="shiptostate" placeholder="CA" size="40"></td>
+</tr>
+
+<tr class="even">
+ <td><strong>Country: <span style="color: red;" title="Required">*</span></strong></td>
+ <td><input name="shiptocountry" id="shiptocountry" placeholder="USA" size="40"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Postal Code: <span style="color: red;" title="Required">*</span></strong></td>
+ <td><input name="shiptopcode" id="shiptopcode" placeholder="90210" size="40"></td>
+</tr>
+
+<tr class="even">
+ <td><strong>Phone (including country code): <span style="color: red;" title="Required">*</span></strong></td>
+ <td><input name="shiptophone" id="shiptophone" placeholder="919-555-1212" size="40"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Custom Reference<br>
+ (Fiscal or VAT-number, if known):</strong><br><small>(if your country requires this)</small>
+ </td>
+ <td><input name="shiptoidrut" id="shiptoidrut" size="40"></td>
+</tr>
+
+<tr class="even">
+ <td colspan="2">
+ <strong>Addition information for delivery person:</strong><br>
+ <textarea id="shipadditional" name="shipadditional" rows="4"></textarea>
+ </td>
+</tr>
+
+<tr class="odd">
+ <td><!--spacer-->&nbsp;</td>
+ <td><!--spacer-->&nbsp;</td>
+</tr>
+
+<tr class="even">
+ <td colspan="2"><strong>Swag Requested:</strong></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Stickers:</strong></td>
+ <td><input type="checkbox" id="stickers" name="stickers" value="1"></td>
+</tr>
+
+<tr class="even">
+ <td><strong>Buttons:</strong></td>
+ <td><input type="checkbox" id="buttons" name="buttons" value="1"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Lanyards:</strong></td>
+ <td><input type="checkbox" id="lanyards" name="lanyards" value="1"></td>
+</tr>
+
+<tr class="even">
+ <td><strong>T-Shirts:</strong></td>
+ <td><input type="checkbox" id="tshirts" name="tshirts" value="1"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Roll-Up Banners:</strong></td>
+ <td><input type="checkbox" id="rollupbanners" name="rollupbanners" value="1"></td>
+</tr>
+
+<tr class="even">
+ <td><strong>Horizontal Banner:</strong></td>
+ <td><input type="checkbox" id="horizontalbanner" name="horizontalbanner" value="1"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Booth Cloth:</strong></td>
+ <td><input type="checkbox" id="boothcloth" name="boothcloth" value="1"></td>
+</tr>
+
+<tr class="even">
+ <td><strong>Pens:</strong></td>
+ <td><input type="checkbox" id="pens" name="pens" value="1"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Other:</strong> (please specify)</td>
+ <td><input type="text" id="otherswag" name="otherswag" size="40"></td>
+</tr>
+
+<tr class="even">
+ <td>&nbsp;</td>
+ <td align="right">
+ <input type="submit" id="commit" value="Submit Request">
+ </td>
+</tr>
+
+</table>
+
+<p>
+ Quantities of different swag items requested that will actually be shipped
+ depend on stock availability and number of attendees. Mozilla cannot guarantee
+ that all items requested will be in stock at the time of shipment and you will
+ be notified in case an item cannot be shipped. Please request swag at least 1
+ month before desired delivery date.
+</p>
+
+<p>
+ <strong><span style="color: red;">*</span></strong> - Required field<br />
+ Thanks for contacting us.
+ You will be notified by email of any progress made in resolving your request.
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/REMO/template/en/default/bug/create/create-remo-swag.xml.tmpl b/extensions/REMO/template/en/default/bug/create/create-remo-swag.xml.tmpl
new file mode 100644
index 000000000..4308bc5ac
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/create-remo-swag.xml.tmpl
@@ -0,0 +1,104 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+<?xml version="1.0" [% IF Param('utf8') %]encoding="UTF-8" [% END %]standalone="yes" ?>
+<!DOCTYPE remoswag [
+<!ELEMENT remoswag (firstname,
+ lastname,
+ wikiprofile,
+ eventname,
+ wikipage,
+ attendance,
+ shipping,
+ swagrequested)>
+<!ELEMENT firstname (#PCDATA)>
+<!ELEMENT lastname (#PCDATA)>
+<!ELEMENT wikiprofile (#PCDATA)>
+<!ELEMENT eventname (#PCDATA)>
+<!ELEMENT wikipage (#PCDATA)>
+<!ELEMENT attendance (#PCDATA)>
+<!ELEMENT shipping (shipbeforedate,
+ shiptofirstname,
+ shiptolastname,
+ shiptoaddress1,
+ shiptoaddress2,
+ shiptocity,
+ shiptostate,
+ shiptopcode,
+ shiptocountry,
+ shiptophone,
+ shiptoidrut,
+ shipadditional)>
+<!ELEMENT shipbeforedate (#PCDATA)>
+<!ELEMENT shiptofirstname (#PCDATA)>
+<!ELEMENT shiptolastname (#PCDATA)>
+<!ELEMENT shiptoaddress1 (#PCDATA)>
+<!ELEMENT shiptoaddress2 (#PCDATA)>
+<!ELEMENT shiptocity (#PCDATA)>
+<!ELEMENT shiptostate (#PCDATA)>
+<!ELEMENT shiptopcode (#PCDATA)>
+<!ELEMENT shiptocountry (#PCDATA)>
+<!ELEMENT shiptophone (#PCDATA)>
+<!ELEMENT shiptoidrut (#PCDATA)>
+<!ELEMENT shipadditional (#PCDATA)>
+<!ELEMENT swagrequested (stickers,
+ buttons,
+ posters,
+ lanyards,
+ tshirts,
+ rollupbanners,
+ horizontalbanner,
+ boothcloth,
+ pens,
+ otherswag)>
+<!ELEMENT stickers (#PCDATA)>
+<!ELEMENT buttons (#PCDATA)>
+<!ELEMENT posters (#PCDATA)>
+<!ELEMENT lanyards (#PCDATA)>
+<!ELEMENT tshirts (#PCDATA)>
+<!ELEMENT rollupbanners (#PCDATA)>
+<!ELEMENT horizontalbanners (#PCDATA)>
+<!ELEMENT boothcloth (#PCDATA)>
+<!ELEMENT pens (#PCDATA)>
+<!ELEMENT otherswag (#PCDATA)>]>
+<remoswag>
+ <firstname>[% cgi.param('firstname') FILTER xml %]</firstname>
+ <lastname>[% cgi.param('lastname') FILTER xml %]</lastname>
+ <wikiprofile>[% cgi.param('wikiprofile') FILTER xml %]</wikiprofile>
+ <eventname>[% cgi.param('eventname') FILTER xml %]</eventname>
+ <wikipage>[% cgi.param('wikipage') FILTER xml %]</wikipage>
+ <attendance> [% cgi.param('attendance') FILTER xml %]</attendance>
+ <shipping>
+ <shipbeforedate>[% cgi.param('cf_due_date') FILTER xml %]</shipbeforedate>
+ <shiptofirstname>[% cgi.param("shiptofirstname") FILTER xml %]</shiptofirstname>
+ <shiptolastname>[% cgi.param("shiptolastname") FILTER xml %]</shiptolastname>
+ <shiptoaddress1>[% cgi.param("shiptoaddress1") FILTER xml %]</shiptoaddress1>
+ <shiptoaddress2>[% cgi.param("shiptoaddress2") FILTER xml %]</shiptoaddress2>
+ <shiptocity>[% cgi.param("shiptocity") FILTER xml %]</shiptocity>
+ <shiptostate>[% cgi.param("shiptostate") FILTER xml %]</shiptostate>
+ <shiptopcode>[% cgi.param("shiptopcode") FILTER xml %]</shiptopcode>
+ <shiptocountry>[% cgi.param("shiptocountry") FILTER xml %]</shiptocountry>
+ <shiptophone>[% cgi.param("shiptophone") FILTER xml %]</shiptophone>
+ <shiptoidrut>[% cgi.param("shiptoidrut") FILTER xml %]</shiptoidrut>
+ <shipadditional>[% cgi.param('shipadditional') || '' FILTER xml %]</shipadditional>
+ </shipping>
+ <swagrequested>
+ <stickers>[% (cgi.param('stickers') ? 1 : 0) FILTER xml %]</stickers>
+ <buttons>[% (cgi.param('buttons') ? 1 : 0) FILTER xml %]</buttons>
+ <posters>[% (cgi.param('posters') ? 1 : 0) FILTER xml %]</posters>
+ <lanyards>[% (cgi.param('lanyards') ? 1 : 0) FILTER xml %]</lanyards>
+ <tshirts>[% (cgi.param('tshirts') ? 1 : 0) FILTER xml %]</tshirts>
+ <rollupbanners>[% (cgi.param('rollupbanners') ? 1 : 0) FILTER xml %]</rollupbanners>
+ <horizontalbanner>[% (cgi.param('horizontalbanner') ? 1 : 0) FILTER xml %]</horizontalbanner>
+ <boothcloth>[% (cgi.param('boothcloth') ? 1 : 0) FILTER xml %]</boothcloth>
+ <pens>[% (cgi.param('pens') ? 1 : 0) FILTER xml %]</pens>
+ <otherswag>[% cgi.param('otherswag') || '' FILTER xml %]</otherswag>
+ </swagrequested>
+</remoswag>
diff --git a/extensions/REMO/template/en/default/bug/create/created-mozreps.html.tmpl b/extensions/REMO/template/en/default/bug/create/created-mozreps.html.tmpl
new file mode 100644
index 000000000..a8a3ca112
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/created-mozreps.html.tmpl
@@ -0,0 +1,38 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the REMO Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s): Byron Jones <glob@mozilla.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Reps - Application Form"
+
+%]
+
+<h1>Thank you!</h1>
+
+<p>
+Thank you for submitting your Mozilla Reps Application Form. A Mozilla Rep
+mentor will contact you shortly at your bugzilla email address.
+</p>
+
+<p style="font-size: x-small">
+Reference: <a href="show_bug.cgi?id=[% id FILTER uri %]">#[% id FILTER html %]</a>
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/REMO/template/en/default/bug/create/created-remo-budget.html.tmpl b/extensions/REMO/template/en/default/bug/create/created-remo-budget.html.tmpl
new file mode 100644
index 000000000..62430bf9c
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/created-remo-budget.html.tmpl
@@ -0,0 +1,27 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Reps Budget Request Form"
+%]
+
+<h1>Thank you!</h1>
+
+<p>
+ Your budget request has been successfully submitted. Please make sure to
+ follow-up with your mentor so (s)he can verify your request. CC him/her
+ on the [% terms.bug %] if needed.
+</p>
+
+<p style="font-size: x-small">
+ Reference: <a href="show_bug.cgi?id=[% id FILTER uri %]">#[% id FILTER html %]</a>
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/REMO/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/REMO/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..200e678be
--- /dev/null
+++ b/extensions/REMO/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,40 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the REMO Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Byron Jones <bjones@mozilla.com>
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+
+[% IF error == "remo_payment_invalid_product" %]
+ [% title = "Mozilla Reps Payment Invalid Bug" %]
+ You can only attach budget payment information to [% terms.bugs %] under
+ the product 'Mozilla Reps' and component 'Budget Requests'.
+
+[% ELSIF error == "remo_payment_bug_edit_denied" %]
+ [% title = "Mozilla Reps Payment Bug Edit Denied" %]
+ You do not have permission to edit [% terms.bug %] '[% bug_id FILTER html %]'.
+
+[% ELSIF error == "remo_payment_cancel_dupe" %]
+ [% title = "Already filed payment request" %]
+ You already used the form to file
+ <a href="[% urlbase FILTER html %]attachment.cgi?id=[% attachid FILTER uri %]&action=edit">
+ attachment [% attachid FILTER uri %]</a>.<br>
+ <br>
+ You can either <a href="[% urlbase FILTER html %]page.cgi?id=remo-form-payment.html">
+ create a new payment request</a> or [% "go back to $terms.bug $bugid" FILTER bug_link(bugid) FILTER none %].
+
+[% END %]
diff --git a/extensions/REMO/template/en/default/pages/comment-remo-form-payment.txt.tmpl b/extensions/REMO/template/en/default/pages/comment-remo-form-payment.txt.tmpl
new file mode 100644
index 000000000..95c0af6e8
--- /dev/null
+++ b/extensions/REMO/template/en/default/pages/comment-remo-form-payment.txt.tmpl
@@ -0,0 +1,37 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the REMO Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Dave Lawrence <dkl@mozilla.com>
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Mozilla Reps Payment Request
+----------------------------
+
+Requester info:
+
+First name: [% cgi.param('firstname') %]
+Last name: [% cgi.param('lastname') %]
+Wiki user profile: [% cgi.param('wikiprofile') %]
+Event wiki page: [% cgi.param('wikipage') %]
+Budget request [% terms.bug %]: [% cgi.param('bug_id') %]
+Have you already received payment for this event? [% IF cgi.param('receivedpayment') %]Yes[% ELSE %]No[% END %]
+
+[%+ cgi.param("comment") IF cgi.param("comment") %]
+
diff --git a/extensions/REMO/template/en/default/pages/remo-form-payment.html.tmpl b/extensions/REMO/template/en/default/pages/remo-form-payment.html.tmpl
new file mode 100644
index 000000000..0f5f206d3
--- /dev/null
+++ b/extensions/REMO/template/en/default/pages/remo-form-payment.html.tmpl
@@ -0,0 +1,243 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the REMO Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Dave Lawrence <dkl@mozilla.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Reps Payment Form"
+ style_urls = [ 'extensions/REMO/web/styles/moz_reps.css' ]
+ javascript_urls = [ 'extensions/REMO/web/js/form_validate.js',
+ 'js/util.js',
+ 'js/field.js' ]
+ yui = ['connection', 'json']
+%]
+
+<script language="javascript" type="text/javascript">
+
+var bug_cache = {};
+
+function validateAndSubmit() {
+ var alert_text = '';
+ if(!isFilledOut('firstname')) alert_text += "Please enter your first name\n";
+ if(!isFilledOut('lastname')) alert_text += "Please enter your last name\n";
+ if(!isFilledOut('wikiprofile')) alert_text += "Please enter a wiki user profile.\n";
+ if(!isFilledOut('wikipage')) alert_text += "Please enter a wiki page address.\n";
+ if(!isFilledOut('bug_id')) alert_text += "Please enter a valid [% terms.bug %] id to attach this additional information to.\n";
+ if(!isFilledOut('expenseform')) alert_text += "Please enter an expense form to upload.\n";
+ if(!isFilledOut('receipts')) alert_text += "Please enter a receipts file to upload.\n";
+
+ if (alert_text) {
+ alert(alert_text);
+ return false;
+ }
+
+ return true;
+}
+
+function togglePaymentInfo (e) {
+ var div = document.getElementById('paymentinfo');
+ if (e.checked == false) {
+ div.style.display = 'block';
+ }
+ else {
+ div.style.display = 'none';
+ }
+}
+
+function getBugInfo (e, div) {
+ var bug_id = e.value;
+ div = document.getElementById(div);
+
+ if (!bug_id) {
+ div.innerHTML = "";
+ return true;
+ }
+
+ div.style.display = 'block';
+
+ if (bug_cache[bug_id]) {
+ div.innerHTML = bug_cache[bug_id];
+ e.disabled = false;
+ return true;
+ }
+
+ e.disabled = true;
+ div.innerHTML = 'Getting [% terms.bug %] info...';
+
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ YAHOO.util.Connect.asyncRequest(
+ 'POST',
+ 'jsonrpc.cgi',
+ {
+ success: function(res) {
+ var bug_message = "";
+ data = YAHOO.lang.JSON.parse(res.responseText);
+ if (data.error) {
+ bug_message = "Get [% terms.bug %] failed: " + data.error.message;
+ }
+ else if (data.result) {
+ if (data.result.bugs[0].product !== 'Mozilla Reps'
+ || data.result.bugs[0].component !== 'Budget Requests')
+ {
+ bug_message = "You can only attach budget payment " +
+ "information to [% terms.bugs %] under the product " +
+ "'Mozilla Reps' and component 'Budget Requests'.";
+ }
+ else {
+ bug_message = "[% terms.Bug %] " + bug_id + " - " + data.result.bugs[0].status +
+ " - " + data.result.bugs[0].summary;
+ }
+ }
+ else {
+ bug_message = "Get [% terms.bug %] failed: " + res.responseText;
+ }
+ div.innerHTML = bug_message;
+ bug_cache[bug_id] = bug_message;
+ e.disabled = false;
+ },
+ failure: function(res) {
+ if (res.responseText) {
+ div.innerHTML = "Get [% terms.bug %] failed: " + res.responseText;
+ }
+ }
+ },
+ YAHOO.lang.JSON.stringify({
+ version: "1.1",
+ method: "Bug.get",
+ id: bug_id,
+ params: {
+ ids: [ bug_id ],
+ include_fields: [ 'product', 'component', 'status', 'summary' ]
+ }
+ })
+ );
+}
+
+</script>
+
+<h1>Mozilla Reps - Payment Form</h1>
+
+<form method="post" action="page.cgi" id="paymentForm" enctype="multipart/form-data"
+ onSubmit="return validateAndSubmit();">
+<input type="hidden" id="id" name="id" value="remo-form-payment.html">
+<input type="hidden" id="token" name="token" value="[% token FILTER html %]">
+<input type="hidden" id="action" name="action" value="commit">
+
+<table id="reps-form">
+
+<tr class="odd">
+ <td width="25%"><strong>First Name: <span style="color: red;">*</span></strong></td>
+ <td>
+ <input type="text" name="firstname" id="firstname" value="" size="40" placeholder="John">
+ </td>
+</tr>
+
+<tr class="even">
+ <td><strong>Last Name: <span style="color: red;">*</span></strong></td>
+ <td>
+ <input type="text" name="lastname" id="lastname" value="" size="40" placeholder="Doe">
+ </td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Wiki user profile:<span style="color: red;">*</span></strong></td>
+ <td>
+ <input type="text" name="wikiprofile" id="wikiprofile" value="" size="40" placeholder="JohnDoe">
+ </td>
+</tr>
+
+<tr class="even">
+ <td><strong>Event wiki page: <span style="color: red;">*</span></strong></td>
+ <td>
+ <input type="text" name="wikipage" id="wikipage" value="" size="40">
+ </td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Budget request [% terms.bug %]: <span style="color: red;">*</span></strong></td>
+ <td>
+ <input type="text" name="bug_id" id="bug_id" value="" size="40"
+ onblur="getBugInfo(this,'bug_info');")>
+ </td>
+</tr>
+
+<tr class="odd">
+ <td colspan="2">
+ <div id="bug_info" style="display:none;"></div>
+ </td>
+</tr>
+
+<tr class="even">
+ <td colspan="2">
+ <strong>Have you already received payment for this event?</strong>
+ <input type="checkbox" name="receivedpayment" id="receivedpayment" value="1"
+ onchange="togglePaymentInfo(this);" checked="true">
+ <div id="paymentinfo" style="display:none;">
+ Please send an email to William at mozilla.com with all the information below:<br>
+ <br>
+ Payment information:<br>
+ Bank name:<br>
+ Bank address: <br>
+ IBAN:<br>
+ Swift code/BIC:<br>
+ Additional bank details (if necessary):
+ </div>
+ </td>
+</tr>
+
+<tr class="odd">
+ <td colspan="2">
+ <strong>Expense form and scanned receipts/invoices:</strong>
+ </td>
+</tr>
+
+<tr class="odd">
+ <td>Expense Form: <span style="color: red;">*</span></td>
+ <td><input type="file" id="expenseform" name="expenseform" size="40"></td>
+</tr>
+
+<tr class="odd">
+ <td valign="top">Receipts File: <span style="color: red;">*</span></td>
+ <td>
+ <input type="file" id="receipts" name="receipts" size="40"><br>
+ <font style="color:red;">
+ Please black out any bank account information included<br>
+ on receipts before attaching them.
+ </font>
+ </td>
+</tr>
+
+<tr class="even">
+ <td>&nbsp;</td>
+ <td align="right">
+ <input type="submit" id="commit" value="Submit Request">
+ </td>
+</tr>
+
+</table>
+
+</form>
+
+<p>
+ <strong><span style="color: red;">*</span></strong> - Required field<br>
+ Thanks for contacting us.
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/REMO/web/js/form_validate.js b/extensions/REMO/web/js/form_validate.js
new file mode 100644
index 000000000..6c8fa6f07
--- /dev/null
+++ b/extensions/REMO/web/js/form_validate.js
@@ -0,0 +1,21 @@
+/**
+ * Some Form Validation and Interaction
+ **/
+//Makes sure that there is an '@' in the address with a '.'
+//somewhere after it (and at least one character in between them
+
+function isValidEmail(email) {
+ var at_index = email.indexOf("@");
+ var last_dot = email.lastIndexOf(".");
+ return at_index > 0 && last_dot > (at_index + 1);
+}
+
+//Takes a DOM element id and makes sure that it is filled out
+function isFilledOut(elem_id) {
+ var str = document.getElementById(elem_id).value;
+ return str.length>0 && str!="noneselected";
+}
+
+function isChecked(elem_id) {
+ return document.getElementById(elem_id).checked;
+}
diff --git a/extensions/REMO/web/js/swag.js b/extensions/REMO/web/js/swag.js
new file mode 100644
index 000000000..3b69bbab8
--- /dev/null
+++ b/extensions/REMO/web/js/swag.js
@@ -0,0 +1,60 @@
+/**
+ * Swag Request Form Functions
+ * Form Interal Swag Request Form
+ * dtran
+ * 7/6/09
+ **/
+
+
+function evalToNumber(numberString) {
+ if(numberString=='') return 0;
+ return parseInt(numberString);
+}
+
+function evalToNumberString(numberString) {
+ if(numberString=='') return '0';
+ return numberString;
+}
+//item_array should be an array of DOM element ids
+function getTotal(item_array) {
+ var total = 0;
+ for(var i in item_array) {
+ total += evalToNumber(document.getElementById(item_array[i]).value);
+ }
+ return total;
+}
+
+function calculateTotalSwag() {
+ document.getElementById('Totalswag').value =
+ getTotal( new Array('Lanyards',
+ 'Stickers',
+ 'Bracelets',
+ 'Tattoos',
+ 'Buttons',
+ 'Posters'));
+
+}
+
+
+function calculateTotalMensShirts() {
+ document.getElementById('mens_total').value =
+ getTotal( new Array('mens_s',
+ 'mens_m',
+ 'mens_l',
+ 'mens_xl',
+ 'mens_xxl',
+ 'mens_xxxl'));
+
+}
+
+
+function calculateTotalWomensShirts() {
+ document.getElementById('womens_total').value =
+ getTotal( new Array('womens_s',
+ 'womens_m',
+ 'womens_l',
+ 'womens_xl',
+ 'womens_xxl',
+ 'womens_xxxl'));
+
+}
diff --git a/extensions/REMO/web/styles/moz_reps.css b/extensions/REMO/web/styles/moz_reps.css
new file mode 100644
index 000000000..216bdd234
--- /dev/null
+++ b/extensions/REMO/web/styles/moz_reps.css
@@ -0,0 +1,53 @@
+#reps-form {
+ width: 700px;
+ border-spacing: 0px;
+ border: 4px solid #e0e0e0;
+}
+
+#reps-form th, #reps-form td {
+ padding: 5px;
+ vertical-align: top;
+}
+
+#reps-form .even th, #reps-form .even td {
+ background: #e0e0e0;
+}
+
+#reps-form th {
+ text-align: left;
+}
+
+#reps-form textarea {
+ font-family: Verdana, sans-serif;
+ font-size: small;
+ width: 590px;
+}
+
+#reps-form textarea.small {
+ width: 295px;
+}
+
+#reps-form .mandatory {
+ color: red;
+ font-size: 80%;
+}
+
+#reps-form .missing {
+ box-shadow: #FF0000 0 0 1.5px 1px;
+}
+
+#reps-form .hidden {
+ display: none;
+}
+
+#reps-form .subTH {
+ padding-left: 2em;
+}
+
+#reps-form .missing {
+ background: #FFC1C1;
+}
+
+.yui-calcontainer {
+ z-index: 2;
+}
diff --git a/extensions/RequestNagger/Config.pm b/extensions/RequestNagger/Config.pm
new file mode 100644
index 000000000..6b9488c80
--- /dev/null
+++ b/extensions/RequestNagger/Config.pm
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::RequestNagger;
+use strict;
+
+use constant NAME => 'RequestNagger';
+use constant REQUIRED_MODULES => [ ];
+use constant OPTIONAL_MODULES => [ ];
+
+__PACKAGE__->NAME;
diff --git a/extensions/RequestNagger/Extension.pm b/extensions/RequestNagger/Extension.pm
new file mode 100644
index 000000000..2be828fd1
--- /dev/null
+++ b/extensions/RequestNagger/Extension.pm
@@ -0,0 +1,349 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::RequestNagger;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Extension::RequestNagger::TimeAgo qw(time_ago);
+use Bugzilla::Flag;
+use Bugzilla::Install::Filesystem;
+use Bugzilla::User::Setting;
+use Bugzilla::Util qw(datetime_from detaint_natural);
+use DateTime;
+
+our $VERSION = '1';
+
+BEGIN {
+ *Bugzilla::Flag::age = \&_flag_age;
+ *Bugzilla::Flag::deferred = \&_flag_deferred;
+ *Bugzilla::Product::nag_interval = \&_product_nag_interval;
+}
+
+sub _flag_age {
+ return time_ago(datetime_from($_[0]->modification_date));
+}
+
+sub _flag_deferred {
+ my ($self) = @_;
+ if (!exists $self->{deferred}) {
+ my $dbh = Bugzilla->dbh;
+ my ($defer_until) = $dbh->selectrow_array(
+ "SELECT defer_until FROM nag_defer WHERE flag_id=?",
+ undef,
+ $self->id
+ );
+ $self->{deferred} = $defer_until ? datetime_from($defer_until) : undef;
+ }
+ return $self->{deferred};
+}
+
+sub _product_nag_interval { $_[0]->{nag_interval} }
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my ($class, $columns) = @$args{qw(class columns)};
+ if ($class->isa('Bugzilla::Product')) {
+ push @$columns, 'nag_interval';
+ }
+}
+
+sub object_update_columns {
+ my ($self, $args) = @_;
+ my ($object, $columns) = @$args{qw(object columns)};
+ if ($object->isa('Bugzilla::Product')) {
+ push @$columns, 'nag_interval';
+ }
+}
+
+sub object_before_create {
+ my ($self, $args) = @_;
+ my ($class, $params) = @$args{qw(class params)};
+ return unless $class->isa('Bugzilla::Product');
+ my $input = Bugzilla->input_params;
+ if (exists $input->{nag_interval}) {
+ my $interval = _check_nag_interval($input->{nag_interval});
+ $params->{nag_interval} = $interval;
+ }
+}
+
+sub object_end_of_set_all {
+ my ($self, $args) = @_;
+ my ($object, $params) = @$args{qw(object params)};
+ return unless $object->isa('Bugzilla::Product');
+ my $input = Bugzilla->input_params;
+ if (exists $input->{nag_interval}) {
+ my $interval = _check_nag_interval($input->{nag_interval});
+ $object->set('nag_interval', $interval);
+ }
+}
+
+sub _check_nag_interval {
+ my ($value) = @_;
+ detaint_natural($value)
+ || ThrowUserError('invalid_parameter', { name => 'request reminding interval', err => 'must be numeric' });
+ return $value < 0 ? 0 : $value * 24;
+}
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my ($vars, $page) = @$args{qw(vars page_id)};
+ return unless $page eq 'request_defer.html';
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ my $input = Bugzilla->input_params;
+
+ # load flag
+ my $flag_id = scalar($input->{flag})
+ || ThrowUserError('request_nagging_flag_invalid');
+ detaint_natural($flag_id)
+ || ThrowUserError('request_nagging_flag_invalid');
+ my $flag = Bugzilla::Flag->new({ id => $flag_id, cache => 1 })
+ || ThrowUserError('request_nagging_flag_invalid');
+
+ # you can only defer flags directed at you
+ $user->can_see_bug($flag->bug->id)
+ || ThrowUserError("bug_access_denied", { bug_id => $flag->bug->id });
+ $flag->status eq '?'
+ || ThrowUserError('request_nagging_flag_set');
+ $flag->requestee
+ || ThrowUserError('request_nagging_flag_wind');
+ $flag->requestee->id == $user->id
+ || ThrowUserError('request_nagging_flag_not_owned');
+
+ my $date = DateTime->now()->truncate(to => 'day');
+ my $defer_until;
+ if ($input->{'defer-until'}
+ && $input->{'defer-until'} =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/)
+ {
+ $defer_until = DateTime->new(year => $1, month => $2, day => $3);
+ if ($defer_until > $date->clone->add(days => 7)) {
+ $defer_until = undef;
+ }
+ }
+
+ if ($input->{save} && $defer_until) {
+ $self->_defer_until($flag_id, $defer_until);
+ $vars->{saved} = "1";
+ $vars->{defer_until} = $defer_until;
+ }
+ else {
+ my @dates;
+ foreach my $i (1..7) {
+ $date->add(days => 1);
+ unshift @dates, { days => $i, date => $date->clone };
+ }
+ $vars->{defer_until} = \@dates;
+ }
+
+ $vars->{flag} = $flag;
+}
+
+sub _defer_until {
+ my ($self, $flag_id, $defer_until) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->bz_start_transaction();
+
+ my ($defer_id) = $dbh->selectrow_array("SELECT id FROM nag_defer WHERE flag_id=?", undef, $flag_id);
+ if ($defer_id) {
+ $dbh->do("UPDATE nag_defer SET defer_until=? WHERE id=?", undef, $defer_until->ymd, $flag_id);
+ } else {
+ $dbh->do("INSERT INTO nag_defer(flag_id, defer_until) VALUES (?, ?)", undef, $flag_id, $defer_until->ymd);
+ }
+
+ $dbh->bz_commit_transaction();
+}
+
+#
+# hooks
+#
+
+sub object_end_of_update {
+ my ($self, $args) = @_;
+ if ($args->{object}->isa("Bugzilla::Flag") && exists $args->{changes}) {
+ # any change to the flag (setting, clearing, or retargetting) will clear the deferals
+ my $flag = $args->{object};
+ Bugzilla->dbh->do("DELETE FROM nag_defer WHERE flag_id=?", undef, $flag->id);
+ }
+}
+
+sub user_preferences {
+ my ($self, $args) = @_;
+ my $tab = $args->{'current_tab'};
+ return unless $tab eq 'request_nagging';
+
+ my $save = $args->{'save_changes'};
+ my $vars = $args->{'vars'};
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+
+ my %watching =
+ map { $_ => 1 }
+ @{ $dbh->selectcol_arrayref(
+ "SELECT profiles.login_name
+ FROM nag_watch
+ INNER JOIN profiles ON nag_watch.nagged_id = profiles.userid
+ WHERE nag_watch.watcher_id = ?
+ ORDER BY profiles.login_name",
+ undef,
+ $user->id
+ ) };
+
+ if ($save) {
+ my $input = Bugzilla->input_params;
+ Bugzilla::User::match_field({ 'add_watching' => {'type' => 'multi'} });
+
+ $dbh->bz_start_transaction();
+
+ # user preference
+ if (my $value = $input->{request_nagging}) {
+ my $settings = $user->settings;
+ my $setting = new Bugzilla::User::Setting('request_nagging');
+ if ($value eq 'default') {
+ $settings->{request_nagging}->reset_to_default;
+ }
+ else {
+ $setting->validate_value($value);
+ $settings->{request_nagging}->set($value);
+ }
+ }
+
+ # watching
+ if ($input->{remove_watched_users}) {
+ my $del_watching = ref($input->{del_watching}) ? $input->{del_watching} : [ $input->{del_watching} ];
+ foreach my $login (@$del_watching) {
+ my $u = Bugzilla::User->new({ name => $login, cache => 1 })
+ || next;
+ next unless exists $watching{$u->login};
+ $dbh->do(
+ "DELETE FROM nag_watch WHERE watcher_id=? AND nagged_id=?",
+ undef,
+ $user->id, $u->id
+ );
+ delete $watching{$u->login};
+ }
+ }
+ if ($input->{add_watching}) {
+ my $add_watching = ref($input->{add_watching}) ? $input->{add_watching} : [ $input->{add_watching} ];
+ foreach my $login (@$add_watching) {
+ my $u = Bugzilla::User->new({ name => $login, cache => 1 })
+ || next;
+ next if exists $watching{$u->login};
+ $dbh->do(
+ "INSERT INTO nag_watch(watcher_id, nagged_id) VALUES(?, ?)",
+ undef,
+ $user->id, $u->id
+ );
+ $watching{$u->login} = 1;
+ }
+ }
+
+ $dbh->bz_commit_transaction();
+ }
+
+ $vars->{watching} = [ sort keys %watching ];
+
+ my $handled = $args->{'handled'};
+ $$handled = 1;
+}
+
+#
+# installation
+#
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'nag_watch'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ nagged_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE',
+ }
+ },
+ watcher_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE',
+ }
+ },
+ ],
+ INDEXES => [
+ nag_watch_idx => {
+ FIELDS => [ 'nagged_id', 'watcher_id' ],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'nag_defer'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ flag_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'flags',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ }
+ },
+ defer_until => {
+ TYPE => 'DATETIME',
+ NOTNULL => 1,
+ },
+ ],
+ INDEXES => [
+ nag_defer_idx => {
+ FIELDS => [ 'flag_id' ],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+}
+
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_add_column('products', 'nag_interval', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 7 * 24 });
+}
+
+sub install_filesystem {
+ my ($self, $args) = @_;
+ my $files = $args->{'files'};
+ my $extensions_dir = bz_locations()->{'extensionsdir'};
+ my $script_name = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/send-request-nags.pl";
+ $files->{$script_name} = {
+ perms => Bugzilla::Install::Filesystem::WS_EXECUTE
+ };
+}
+
+sub install_before_final_checks {
+ my ($self, $args) = @_;
+ add_setting('request_nagging', ['on', 'off'], 'on');
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/RequestNagger/bin/send-request-nags.pl b/extensions/RequestNagger/bin/send-request-nags.pl
new file mode 100755
index 000000000..93265b9ee
--- /dev/null
+++ b/extensions/RequestNagger/bin/send-request-nags.pl
@@ -0,0 +1,209 @@
+#!/usr/bin/perl
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../..";
+
+use Bugzilla;
+BEGIN { Bugzilla->extensions() }
+
+use Bugzilla::Attachment;
+use Bugzilla::Bug;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Extension::RequestNagger::Constants;
+use Bugzilla::Extension::RequestNagger::Bug;
+use Bugzilla::Mailer;
+use Bugzilla::User;
+use Bugzilla::Util qw(format_time);
+use Email::MIME;
+use Sys::Hostname;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my $DO_NOT_NAG = grep { $_ eq '-d' } @ARGV;
+
+my $dbh = Bugzilla->dbh;
+my $date = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+$date = format_time($date, '%a, %d %b %Y %T %z', 'UTC');
+
+# delete expired defers
+$dbh->do("DELETE FROM nag_defer WHERE defer_until <= CURRENT_DATE()");
+Bugzilla->switch_to_shadow_db();
+
+# send nags to requestees
+send_nags(
+ sql => REQUESTEE_NAG_SQL,
+ template => 'requestee',
+ recipient_field => 'requestee_id',
+ date => $date,
+);
+
+# send nags to watchers
+send_nags(
+ sql => WATCHING_NAG_SQL,
+ template => 'watching',
+ recipient_field => 'watcher_id',
+ date => $date,
+);
+
+sub send_nags {
+ my (%args) = @_;
+ my $rows = $dbh->selectall_arrayref($args{sql}, { Slice => {} });
+
+ # iterate over rows, sending email when the current recipient changes
+ my $requests = [];
+ my $current_recipient;
+ foreach my $request (@$rows) {
+ # send previous user's requests
+ if (!$current_recipient || $request->{$args{recipient_field}} != $current_recipient->id) {
+ send_email(%args, recipient => $current_recipient, requests => $requests);
+ $current_recipient = Bugzilla::User->new({ id => $request->{$args{recipient_field}}, cache => 1 });
+ $requests = [];
+ }
+
+ # check group membership
+ $request->{requestee} = Bugzilla::User->new({ id => $request->{requestee_id}, cache => 1 });
+ my $group;
+ foreach my $type (FLAG_TYPES) {
+ next unless $type->{type} eq $request->{flag_type};
+ $group = $type->{group};
+ last;
+ }
+ next unless $request->{requestee}->in_group($group);
+
+ # check bug visibility
+ next unless $current_recipient->can_see_bug($request->{bug_id});
+
+ # create objects
+ $request->{bug} = Bugzilla::Bug->new({ id => $request->{bug_id}, cache => 1 });
+ $request->{requester} = Bugzilla::User->new({ id => $request->{requester_id}, cache => 1 });
+ $request->{flag} = Bugzilla::Flag->new({ id => $request->{flag_id}, cache => 1 });
+ if ($request->{attach_id}) {
+ $request->{attachment} = Bugzilla::Attachment->new({ id => $request->{attach_id}, cache => 1 });
+ # check attachment visibility
+ next if $request->{attachment}->isprivate && !$current_recipient->is_insider;
+ }
+ if (exists $request->{watcher_id}) {
+ $request->{watcher} = Bugzilla::User->new({ id => $request->{watcher_id}, cache => 1 });
+ }
+
+ # add this request to the current user's list
+ push(@$requests, $request);
+ }
+ send_email(%args, recipient => $current_recipient, requests => $requests);
+}
+
+sub send_email {
+ my (%vars) = @_;
+ my $vars = \%vars;
+ return unless $vars->{recipient} && @{ $vars->{requests} };
+
+ my $request_list = delete $vars->{requests};
+
+ # if securemail is installed, we need to encrypt or censor emails which
+ # contain non-public bugs
+ my $default_user = Bugzilla::User->new();
+ my $securemail = $vars->{recipient}->can('public_key');
+ my $has_key = $securemail && $vars->{recipient}->public_key;
+ # have to do this each time as objects are shared between requests
+ my $has_private_bug = 0;
+ foreach my $request (@{ $request_list }) {
+ # rebless bug objects into our subclass
+ bless($request->{bug}, 'Bugzilla::Extension::RequestNagger::Bug');
+ # and tell that object to hide the summary if required
+ if ($securemail && !$default_user->can_see_bug($request->{bug})) {
+ $has_private_bug = 1;
+ $request->{bug}->{secure_bug} = !$has_key;
+ }
+ else {
+ $request->{bug}->{secure_bug} = 0;
+ }
+ }
+ my $encrypt = $securemail && $has_private_bug && $has_key;
+
+ # restructure the list to group by requestee then flag type
+ my $requests = {};
+ my %seen_types;
+ foreach my $request (@{ $request_list }) {
+ # by requestee
+ my $requestee_login = $request->{requestee}->login;
+ $requests->{$requestee_login} ||= {
+ requestee => $request->{requestee},
+ types => {},
+ typelist => [],
+ };
+
+ # by flag type
+ my $types = $requests->{$requestee_login}->{types};
+ my $flag_type = $request->{flag_type};
+ $types->{$flag_type} ||= [];
+
+ push @{ $types->{$flag_type} }, $request;
+ $seen_types{$requestee_login}{$flag_type} = 1;
+ }
+ foreach my $requestee_login (keys %seen_types) {
+ my @flag_types;
+ foreach my $flag_type (map { $_->{type} } FLAG_TYPES) {
+ push @flag_types, $flag_type if $seen_types{$requestee_login}{$flag_type};
+ }
+ $requests->{$requestee_login}->{typelist} = \@flag_types;
+ }
+ $vars->{requests} = $requests;
+
+ # generate email
+ my $template = Bugzilla->template_inner($vars->{recipient}->setting('lang'));
+ my $template_file = $vars->{template};
+
+ my ($header, $text);
+ $template->process("email/request_nagging-$template_file-header.txt.tmpl", $vars, \$header)
+ || ThrowTemplateError($template->error());
+ $header .= "\n";
+ $template->process("email/request_nagging-$template_file.txt.tmpl", $vars, \$text)
+ || ThrowTemplateError($template->error());
+
+ my @parts = (
+ Email::MIME->create(
+ attributes => { content_type => "text/plain" },
+ body => $text,
+ )
+ );
+ if ($vars->{recipient}->setting('email_format') eq 'html') {
+ my $html;
+ $template->process("email/request_nagging-$template_file.html.tmpl", $vars, \$html)
+ || ThrowTemplateError($template->error());
+ push @parts, Email::MIME->create(
+ attributes => { content_type => "text/html" },
+ body => $html,
+ );
+ }
+
+ my $email = Email::MIME->new($header);
+ $email->header_set('X-Generated-By' => hostname());
+ if (scalar(@parts) == 1) {
+ $email->content_type_set($parts[0]->content_type);
+ } else {
+ $email->content_type_set('multipart/alternative');
+ }
+ $email->parts_set(\@parts);
+ if ($encrypt) {
+ $email->header_set('X-Bugzilla-Encrypt' => '1');
+ }
+
+ # send
+ if ($DO_NOT_NAG) {
+ print $email->as_string, "\n";
+ } else {
+ MessageToMTA($email);
+ }
+}
+
diff --git a/extensions/RequestNagger/lib/Bug.pm b/extensions/RequestNagger/lib/Bug.pm
new file mode 100644
index 000000000..de6d5eae5
--- /dev/null
+++ b/extensions/RequestNagger/lib/Bug.pm
@@ -0,0 +1,30 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::RequestNagger::Bug;
+
+use strict;
+use parent qw(Bugzilla::Bug);
+
+sub short_desc {
+ my ($self) = @_;
+ return $self->{secure_bug} ? '(Secure bug)' : $self->SUPER::short_desc;
+}
+
+sub tooltip {
+ my ($self) = @_;
+ my $tooltip = $self->bug_status;
+ if ($self->bug_status eq 'RESOLVED') {
+ $tooltip .= '/' . $self->resolution;
+ }
+ if (!$self->{secure_bug}) {
+ $tooltip .= ' ' . $self->product . ' :: ' . $self->component;
+ }
+ return $tooltip;
+}
+
+1;
diff --git a/extensions/RequestNagger/lib/Constants.pm b/extensions/RequestNagger/lib/Constants.pm
new file mode 100644
index 000000000..f61e616a7
--- /dev/null
+++ b/extensions/RequestNagger/lib/Constants.pm
@@ -0,0 +1,116 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::RequestNagger::Constants;
+
+use strict;
+use base qw(Exporter);
+
+our @EXPORT = qw(
+ FLAG_TYPES
+ REQUESTEE_NAG_SQL
+ WATCHING_NAG_SQL
+);
+
+# the order of this array determines the order used in email
+use constant FLAG_TYPES => (
+ {
+ type => 'review', # flag_type.name
+ group => 'everyone', # the user must be a member of this group to receive reminders
+ },
+ {
+ type => 'superview',
+ group => 'everyone',
+ },
+ {
+ type => 'feedback',
+ group => 'everyone',
+ },
+ {
+ type => 'needinfo',
+ group => 'editbugs',
+ },
+);
+
+sub REQUESTEE_NAG_SQL {
+ my $dbh = Bugzilla->dbh;
+ my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES;
+
+ return "
+ SELECT
+ flagtypes.name AS flag_type,
+ flags.id AS flag_id,
+ flags.bug_id,
+ flags.attach_id,
+ flags.modification_date,
+ requester.userid AS requester_id,
+ requestee.userid AS requestee_id
+ FROM
+ flags
+ INNER JOIN flagtypes ON flagtypes.id = flags.type_id
+ INNER JOIN profiles AS requester ON requester.userid = flags.setter_id
+ INNER JOIN profiles AS requestee ON requestee.userid = flags.requestee_id
+ INNER JOIN bugs ON bugs.bug_id = flags.bug_id
+ INNER JOIN products ON products.id = bugs.product_id
+ LEFT JOIN attachments ON attachments.attach_id = flags.attach_id
+ LEFT JOIN profile_setting ON profile_setting.setting_name = 'request_nagging'
+ AND profile_setting.user_id = flags.requestee_id
+ LEFT JOIN nag_defer ON nag_defer.flag_id = flags.id
+ WHERE
+ " . $dbh->sql_in('flagtypes.name', \@flag_types_sql) . "
+ AND flags.status = '?'
+ AND products.nag_interval != 0
+ AND TIMESTAMPDIFF(HOUR, flags.modification_date, CURRENT_DATE()) >= products.nag_interval
+ AND (profile_setting.setting_value IS NULL OR profile_setting.setting_value = 'on')
+ AND requestee.disable_mail = 0
+ AND nag_defer.id IS NULL
+ ORDER BY
+ flags.requestee_id,
+ flagtypes.name,
+ flags.modification_date
+ ";
+}
+
+sub WATCHING_NAG_SQL {
+ my $dbh = Bugzilla->dbh;
+ my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES;
+
+ return "
+ SELECT
+ nag_watch.watcher_id,
+ flagtypes.name AS flag_type,
+ flags.id AS flag_id,
+ flags.bug_id,
+ flags.attach_id,
+ flags.modification_date,
+ requester.userid AS requester_id,
+ requestee.userid AS requestee_id
+ FROM
+ flags
+ INNER JOIN flagtypes ON flagtypes.id = flags.type_id
+ INNER JOIN profiles AS requester ON requester.userid = flags.setter_id
+ INNER JOIN profiles AS requestee ON requestee.userid = flags.requestee_id
+ INNER JOIN bugs ON bugs.bug_id = flags.bug_id
+ INNER JOIN products ON products.id = bugs.product_id
+ LEFT JOIN attachments ON attachments.attach_id = flags.attach_id
+ LEFT JOIN nag_defer ON nag_defer.flag_id = flags.id
+ INNER JOIN nag_watch ON nag_watch.nagged_id = flags.requestee_id
+ INNER JOIN profiles AS watcher ON watcher.userid = nag_watch.watcher_id
+ WHERE
+ " . $dbh->sql_in('flagtypes.name', \@flag_types_sql) . "
+ AND flags.status = '?'
+ AND products.nag_interval != 0
+ AND TIMESTAMPDIFF(HOUR, flags.modification_date, CURRENT_DATE()) >= products.nag_interval
+ AND watcher.disable_mail = 0
+ ORDER BY
+ nag_watch.watcher_id,
+ flags.requestee_id,
+ flags.modification_date
+ ";
+}
+
+1;
diff --git a/extensions/RequestNagger/lib/TimeAgo.pm b/extensions/RequestNagger/lib/TimeAgo.pm
new file mode 100644
index 000000000..3dfbbeaac
--- /dev/null
+++ b/extensions/RequestNagger/lib/TimeAgo.pm
@@ -0,0 +1,186 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::RequestNagger::TimeAgo;
+
+use strict;
+use utf8;
+use DateTime;
+use Carp;
+use Exporter qw(import);
+
+use if $ENV{ARCH_64BIT}, 'integer';
+
+our @EXPORT_OK = qw(time_ago);
+
+our $VERSION = '0.06';
+
+my @ranges = (
+ [ -1, 'in the future' ],
+ [ 60, 'just now' ],
+ [ 900, 'a few minutes ago'], # 15*60
+ [ 3000, 'less than an hour ago'], # 50*60
+ [ 4500, 'about an hour ago'], # 75*60
+ [ 7200, 'more than an hour ago'], # 2*60*60
+ [ 21600, 'several hours ago'], # 6*60*60
+ [ 86400, 'today', sub { # 24*60*60
+ my $time = shift;
+ my $now = shift;
+ if ( $time->day < $now->day
+ or $time->month < $now->month
+ or $time->year < $now->year
+ ) {
+ return 'yesterday'
+ }
+ if ($time->hour < 5) {
+ return 'tonight'
+ }
+ if ($time->hour < 10) {
+ return 'this morning'
+ }
+ if ($time->hour < 15) {
+ return 'today'
+ }
+ if ($time->hour < 19) {
+ return 'this afternoon'
+ }
+ return 'this evening'
+ }],
+ [ 172800, 'yesterday'], # 2*24*60*60
+ [ 604800, 'this week'], # 7*24*60*60
+ [ 1209600, 'last week'], # 2*7*24*60*60
+ [ 2678400, 'this month', sub { # 31*24*60*60
+ my $time = shift;
+ my $now = shift;
+ if ($time->year == $now->year and $time->month == $now->month) {
+ return 'this month'
+ }
+ return 'last month'
+ }],
+ [ 5356800, 'last month'], # 2*31*24*60*60
+ [ 24105600, 'several months ago'], # 9*31*24*60*60
+ [ 31536000, 'about a year ago'], # 365*24*60*60
+ [ 34214400, 'last year'], # (365+31)*24*60*60
+ [ 63072000, 'more than a year ago'], # 2*365*24*60*60
+ [ 283824000, 'several years ago'], # 9*365*24*60*60
+ [ 315360000, 'about a decade ago'], # 10*365*24*60*60
+ [ 630720000, 'last decade'], # 20*365*24*60*60
+ [ 2838240000, 'several decades ago'], # 90*365*24*60*60
+ [ 3153600000, 'about a century ago'], # 100*365*24*60*60
+ [ 6307200000, 'last century'], # 200*365*24*60*60
+ [ 6622560000, 'more than a century ago'], # 210*365*24*60*60
+ [ 28382400000, 'several centuries ago'], # 900*365*24*60*60
+ [ 31536000000, 'about a millenium ago'], # 1000*365*24*60*60
+ [ 63072000000, 'more than a millenium ago'], # 2000*365*24*60*60
+);
+
+sub time_ago {
+ my ($time, $now) = @_;
+
+ if (not defined $time or not $time->isa('DateTime')) {
+ croak('DateTime::Duration::Fuzzy::time_ago needs a DateTime object as first parameter')
+ }
+ if (not defined $now) {
+ $now = DateTime->now();
+ }
+ if (not $now->isa('DateTime')) {
+ croak('Invalid second parameter provided to DateTime::Duration::Fuzzy::time_ago; it must be a DateTime object if provided')
+ }
+
+ my $dur = $now->subtract_datetime_absolute($time)->in_units('seconds');
+
+ foreach my $range ( @ranges ) {
+ if ( $dur <= $range->[0] ) {
+ if ( $range->[2] ) {
+ return $range->[2]->($time, $now)
+ }
+ return $range->[1]
+ }
+ }
+
+ return 'millenia ago'
+}
+
+1
+
+__END__
+
+=head1 NAME
+
+DateTime::Duration::Fuzzy -- express dates as fuzzy human-friendly strings
+
+=head1 SYNOPSIS
+
+ use DateTime::Duration::Fuzzy qw(time_ago);
+ use DateTime;
+
+ my $now = DateTime->new(
+ year => 2010, month => 12, day => 12,
+ hour => 19, minute => 59,
+ );
+ my $then = DateTime->new(
+ year => 2010, month => 12, day => 12,
+ hour => 15,
+ );
+ print time_ago($then, $now);
+ # outputs 'several hours ago'
+
+ print time_ago($then);
+ # $now taken from C<time> function
+
+=head1 DESCRIPTION
+
+DateTime::Duration::Fuzzy is inspired from the timeAgo jQuery module
+L<http://timeago.yarp.com/>.
+
+It takes two DateTime objects -- first one representing a moment in the past
+and second optional one representine the present, and returns a human-friendly
+fuzzy expression of the time gone.
+
+=head2 functions
+
+=over 4
+
+=item time_ago($then, $now)
+
+The only exportable function.
+
+First obligatory parameter is a DateTime object.
+
+Second optional parameter is also a DateTime object.
+If it's not provided, then I<now> as the C<time> function returns is
+substituted.
+
+Returns a string expression of the interval between the two DateTime
+objects, like C<several hours ago>, C<yesterday> or <last century>.
+
+=back
+
+=head2 performance
+
+On 64bit machines, it is asvisable to 'use integer', which makes
+the calculations faster. You can turn this on by setting the
+C<ARCH_64BIT> environmental variable to a true value.
+
+If you do this on a 32bit machine, you will get wrong results for
+intervals starting with "several decades ago".
+
+=head1 AUTHOR
+
+Jan Oldrich Kruza, C<< <sixtease at cpan.org> >>
+
+=head1 LICENSE AND COPYRIGHT
+
+Copyright 2010 Jan Oldrich Kruza.
+
+This program is free software; you can redistribute it and/or modify it
+under the terms of either: the GNU General Public License as published
+by the Free Software Foundation; or the Artistic License.
+
+See http://dev.perl.org/licenses/ for more information.
+
+=cut
diff --git a/extensions/RequestNagger/template/en/default/account/prefs/request_nagging.html.tmpl b/extensions/RequestNagger/template/en/default/account/prefs/request_nagging.html.tmpl
new file mode 100644
index 000000000..34bba0064
--- /dev/null
+++ b/extensions/RequestNagger/template/en/default/account/prefs/request_nagging.html.tmpl
@@ -0,0 +1,56 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<label for="request_nagging">
+ Send me reminders for overdue requests:
+</label>
+<select name="request_nagging" id="request_nagging">
+ <option value="default" [% "selected" IF user.settings.request_nagging.is_default %]>
+ Site Default (On)
+ </option>
+ <option value="on" [% "selected" IF !user.settings.request_nagging.is_default
+ && user.settings.request_nagging.value == "on" %]>
+ On
+ </option>
+ <option value="off" [% "selected" IF !user.settings.request_nagging.is_default
+ && user.settings.request_nagging.value == "off" %]>
+ Off
+ </option>
+</select>
+
+<h4>User Request Reminder Watching</h4>
+
+<p>
+ If you watch a user, you will receive a report of their overdue
+ requests.
+</p>
+
+<p>
+ [% IF watching.size %]
+ You are watching everyone in the following list:<br>
+ <select id="del_watching" name="del_watching" multiple="multiple" size="5">
+ [% FOREACH u = watching %]
+ <option value="[% u FILTER html %]">[% u FILTER html %]</option>
+ [% END %]
+ </select><br>
+ <input type="checkbox" id="remove_watched_users" name="remove_watched_users">
+ <label for="remove_watched_users">Remove selected users from my watch list</label>
+ [% ELSE %]
+ <i>You are currently not watching any users.</i>
+ [% END %]
+</p>
+
+<p>Add users to my watch list (comma separated list):
+ [% INCLUDE global/userselect.html.tmpl
+ id => "add_watching"
+ name => "add_watching"
+ value => ""
+ size => 60
+ multiple => 5
+ %]
+</p>
diff --git a/extensions/RequestNagger/template/en/default/email/request_nagging-requestee-header.txt.tmpl b/extensions/RequestNagger/template/en/default/email/request_nagging-requestee-header.txt.tmpl
new file mode 100644
index 000000000..8ad9d6cb1
--- /dev/null
+++ b/extensions/RequestNagger/template/en/default/email/request_nagging-requestee-header.txt.tmpl
@@ -0,0 +1,19 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+[% PROCESS "global/reason-descs.none.tmpl" %]
+From: [% Param('mailfrom') %]
+To: [% recipient.email %]
+Subject: [[% terms.Bugzilla %]] Your Overdue Requests
+ ([% FOREACH type = requests.item(recipient.email).typelist %]
+ [%- requests.item(recipient.email).types.item(type).size %] [%+ type %]
+ [% ", " UNLESS loop.last %]
+ [% END %])
+Date: [% date %]
+X-Bugzilla-Type: nag
diff --git a/extensions/RequestNagger/template/en/default/email/request_nagging-requestee.html.tmpl b/extensions/RequestNagger/template/en/default/email/request_nagging-requestee.html.tmpl
new file mode 100644
index 000000000..cc570e4c4
--- /dev/null
+++ b/extensions/RequestNagger/template/en/default/email/request_nagging-requestee.html.tmpl
@@ -0,0 +1,90 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+
+<!doctype html>
+<html>
+
+<head>
+ <title>[[% terms.Bugzilla %]] Your Overdue Requests</title>
+</head>
+
+<body bgcolor="#ffffff">
+
+<p>
+ The following is a list of requests people have made of you, which are
+ currently overdue. To avoid disappointing others, please deal with them as
+ quickly as possible.
+</p>
+
+[% requests = requests.item(recipient.login) %]
+[% FOREACH type = requests.typelist %]
+
+ <h3>
+ [% type FILTER upper FILTER html %] requests
+ <span style="font-size: x-small; font-weight: normal">
+ (<a href="[% urlbase FILTER none %]buglist.cgi?bug_id=
+ [% FOREACH request = requests.types.$type %]
+ [% request.bug.id FILTER none %]
+ [% "%2C" UNLESS loop.last %]
+ [% END %]">buglist</a>)
+ </span>
+ </h3>
+
+ <ul>
+ [% FOREACH request = requests.types.$type %]
+ <li>
+ <a href="[% urlbase FILTER none %]show_bug.cgi?id=[% request.bug.id FILTER none %]"
+ title="[% request.bug.tooltip FILTER html %]">
+ [% request.bug.id FILTER none %] - [% request.bug.short_desc FILTER html %]
+ </a><br>
+ <b>[%+ request.flag.age FILTER html %]</b> from [% request.requester.identity FILTER html %]<br>
+ <div style="font-size: x-small">
+ [% IF request.attachment %]
+ <a href="[% urlbase FILTER none %]attachment.cgi?id=[% request.attachment.id FILTER none %]&amp;action=edit">Details</a>
+ [% IF request.attachment.ispatch %]
+ | <a href="[% urlbase FILTER none %]attachment.cgi?id=[% request.attachment.id FILTER none %]&amp;action=diff">Diff</a>
+ | <a href="[% urlbase FILTER none %]review?bug=[% request.bug.id FILTER none %]&amp;attachment=[% request.attachment.id FILTER none %]">Review</a>
+ [% END %]
+ |
+ [% END %]
+ <a href="[% urlbase FILTER none %]request_defer?flag=[% request.flag.id FILTER none %]">Defer</a>
+ </div>
+ <br>
+ </li>
+ [% END %]
+ </ul>
+
+[% END %]
+
+<div>
+ <hr style="border: 1px dashed #969696">
+ [% IF requests.types.item('review').size || requests.types.item('feedback').size %]
+ <a href="https://wiki.mozilla.org/BMO/Handling_Requests">
+ Guidance on handling requests
+ </a><br>
+ [% END %]
+ <a href="[% urlbase FILTER none %]request.cgi?action=queue&amp;requestee=[% recipient.login FILTER uri %]&amp;group=type">
+ See all your overdue requests
+ </a><br>
+ <a href="[% urlbase FILTER none %]userprefs.cgi#request_nagging">
+ Opt out of these emails
+ </a><br>
+</div>
+
+<div style="font-size: 90%; color: #666666">
+ <hr style="border: 1px dashed #969696">
+ <b>You are receiving this mail because:</b>
+ <ul>
+ <li>You have overdue requests.</li>
+ </ul>
+</div>
+
+</body>
+</html>
diff --git a/extensions/RequestNagger/template/en/default/email/request_nagging-requestee.txt.tmpl b/extensions/RequestNagger/template/en/default/email/request_nagging-requestee.txt.tmpl
new file mode 100644
index 000000000..2dae504e5
--- /dev/null
+++ b/extensions/RequestNagger/template/en/default/email/request_nagging-requestee.txt.tmpl
@@ -0,0 +1,45 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+
+The following is a list of requests people have made of you, which are
+currently overdue. To avoid disappointing others, please deal with them as
+quickly as possible.
+
+[% requests = requests.item(recipient.login) %]
+[% FOREACH type = requests.typelist %]
+:: [% type FILTER upper FILTER html %] requests
+
+[% FOREACH request = requests.types.$type %]
+[[% terms.Bug %] [%+ request.bug.id %]] [% request.bug.short_desc %]
+ [%+ request.flag.age %] from [% request.requester.identity %]
+ [%+ urlbase %]show_bug.cgi?id=[% request.bug.id +%]
+ [% IF request.attachment && request.attachment.ispatch %]
+ Review: [% urlbase %]review?bug=[% request.bug.id %]&attachment=[% request.attachment.id %]
+ [% END %]
+ Defer: [% urlbase %]request_defer?flag=[% request.flag.id %]
+
+[% END %]
+[% END %]
+
+::
+
+[% IF requests.types.item('review').size || requests.types.item('feedback').size %]
+Guidance on handling requests:
+ https://wiki.mozilla.org/BMO/Handling_Requests
+[% END %]
+
+See all your overdue requests:
+ [%+ urlbase %]request.cgi?action=queue&requestee=[% recipient.login FILTER uri %]&group=type
+
+Opt out of these emails:
+ [%+ urlbase %]userprefs.cgi#request_nagging
+
+--
+You are receiving this mail because: you have overdue requests.
diff --git a/extensions/RequestNagger/template/en/default/email/request_nagging-watching-header.txt.tmpl b/extensions/RequestNagger/template/en/default/email/request_nagging-watching-header.txt.tmpl
new file mode 100644
index 000000000..261e92f13
--- /dev/null
+++ b/extensions/RequestNagger/template/en/default/email/request_nagging-watching-header.txt.tmpl
@@ -0,0 +1,15 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+[% PROCESS "global/reason-descs.none.tmpl" %]
+From: [% Param('mailfrom') %]
+To: [% recipient.email %]
+Subject: [[% terms.Bugzilla %]] Overdue Requests Report
+Date: [% date %]
+X-Bugzilla-Type: nag-watch
diff --git a/extensions/RequestNagger/template/en/default/email/request_nagging-watching.html.tmpl b/extensions/RequestNagger/template/en/default/email/request_nagging-watching.html.tmpl
new file mode 100644
index 000000000..a3010f8a5
--- /dev/null
+++ b/extensions/RequestNagger/template/en/default/email/request_nagging-watching.html.tmpl
@@ -0,0 +1,104 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+
+<!doctype html>
+<html>
+
+<head>
+ <title>[[% terms.Bugzilla %]] Overdue Requests Report</title>
+</head>
+
+<body bgcolor="#ffffff">
+
+<p>
+ The following is a list of people who you are watching that have overdue
+ requests.
+</p>
+
+<hr>
+
+[% FOREACH login = requests.keys.sort %]
+ [% requestee = requests.$login.requestee %]
+ [% requestee.identity FILTER html %]
+ <ul>
+ <li>
+ [%+ FOREACH type = requests.$login.typelist %]
+ [% requests.$login.types.item(type).size %] [%+ type FILTER html %]
+ [% ", " UNLESS loop.last %]
+ [% END %]
+ </li>
+ </ul>
+[% END %]
+
+[% FOREACH login = requests.keys.sort %]
+ [% requestee = requests.$login.requestee %]
+
+ [% bug_ids = [] %]
+ [% FOREACH type = requests.$login.typelist %]
+ [% FOREACH request = requests.$login.types.$type %]
+ [% bug_ids.push(request.bug.id) %]
+ [% END %]
+ [% END %]
+
+ <hr>
+ <h3>
+ [% requestee.identity FILTER html %]
+ <span style="font-size: x-small; font-weight: normal">
+ (<a href="[% urlbase FILTER none %]buglist.cgi?bug_id=[% bug_ids.join(",") FILTER uri %]">buglist</a>)
+ </span><br>
+ <span style="font-size: x-small; font-weight: normal">
+ [% FOREACH type = requests.$login.typelist %]
+ [% requests.$login.types.item(type).size %] [%+ type FILTER html %]
+ [% ", " UNLESS loop.last %]
+ [% END %]
+ </span>
+ </h3>
+
+ [% FOREACH type = requests.$login.typelist %]
+
+ <h3>[% type FILTER upper FILTER html %] requests</h3>
+
+ <ul>
+ [% FOREACH request = requests.$login.types.$type %]
+ <li>
+ <a href="[% urlbase FILTER none %]show_bug.cgi?id=[% request.bug.id FILTER none %]"
+ title="[% request.bug.tooltip FILTER html %]">
+ [% request.bug.id FILTER none %] - [% request.bug.short_desc FILTER html %]
+ </a><br>
+ <b>[%+ request.flag.age FILTER html %]</b> from [% request.requester.identity FILTER html %]<br>
+ [% IF request.flag.deferred %]
+ Deferred until [%+ request.flag.deferred.ymd FILTER html %]<br>
+ [% END %]
+ <br>
+ </li>
+ [% END %]
+ </ul>
+
+ [% END %]
+
+[% END %]
+
+<div>
+ <hr style="border: 1px dashed #969696">
+ <a href="[% urlbase FILTER none %]userprefs.cgi?tab=request_nagging">
+ Change who you are watching
+ </a>
+</div>
+
+<div style="font-size: 90%; color: #666666">
+ <hr style="border: 1px dashed #969696">
+ <b>You are receiving this mail because:</b>
+ <ul>
+ <li>you are watching someone with overdue requests.</li>
+ </ul>
+</div>
+
+</body>
+</html>
diff --git a/extensions/RequestNagger/template/en/default/email/request_nagging-watching.txt.tmpl b/extensions/RequestNagger/template/en/default/email/request_nagging-watching.txt.tmpl
new file mode 100644
index 000000000..e36224109
--- /dev/null
+++ b/extensions/RequestNagger/template/en/default/email/request_nagging-watching.txt.tmpl
@@ -0,0 +1,47 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+
+The following is a list of people who you are watching that have overdue
+requests.
+
+[% FOREACH login = requests.keys.sort %]
+[% requestee = requests.$login.requestee %]
+::
+:: [% requestee.identity %]
+:: [% FOREACH type = requests.$login.typelist %]
+ [%- requests.$login.types.item(type).size %] [%+ type %]
+ [% ", " UNLESS loop.last %]
+ [% END %]
+::
+
+[% FOREACH type = requests.$login.typelist %]
+:: [% type FILTER upper FILTER html %] requests
+
+[% FOREACH request = requests.$login.types.$type %]
+[[% terms.Bug %] [%+ request.bug.id %]] [% request.bug.short_desc %]
+ [%+ request.flag.age %] from [% request.requester.identity %]
+ [%+ urlbase %]show_bug.cgi?id=[% request.bug.id +%]
+ [% IF request.flag.deferred %]
+ Deferred until [%+ request.flag.deferred.ymd %]
+ [% END %]
+
+[% END %]
+[% END %]
+
+[% END %]
+
+::
+
+Change who you are watching
+ [%+ urlbase %]userprefs.cgi?tab=request_nagging
+
+--
+You are receiving this mail because: you are watching someone with overdue
+requests.
diff --git a/extensions/RequestNagger/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl b/extensions/RequestNagger/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
new file mode 100644
index 000000000..ed3e29c64
--- /dev/null
+++ b/extensions/RequestNagger/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
@@ -0,0 +1,14 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% tabs = tabs.import([{
+ name => "request_nagging",
+ label => "Request Reminders",
+ link => "userprefs.cgi?tab=request_nagging",
+ saveable => 1
+ }]) %]
diff --git a/extensions/RequestNagger/template/en/default/hook/admin/products/edit-common-rows.html.tmpl b/extensions/RequestNagger/template/en/default/hook/admin/products/edit-common-rows.html.tmpl
new file mode 100644
index 000000000..6dcd58f67
--- /dev/null
+++ b/extensions/RequestNagger/template/en/default/hook/admin/products/edit-common-rows.html.tmpl
@@ -0,0 +1,16 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<tr>
+ <th align="right">Remind for overdue requests after:</th>
+ <td>
+ <input name="nag_interval" size="5"
+ value="[% product.id ? product.nag_interval / 24 : 7 %]">
+ days (Setting this to 0 disables request reminding).
+ </td>
+</tr>
diff --git a/extensions/RequestNagger/template/en/default/hook/admin/products/updated-changes.html.tmpl b/extensions/RequestNagger/template/en/default/hook/admin/products/updated-changes.html.tmpl
new file mode 100644
index 000000000..9baccce86
--- /dev/null
+++ b/extensions/RequestNagger/template/en/default/hook/admin/products/updated-changes.html.tmpl
@@ -0,0 +1,14 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF changes.nag_interval.defined %]
+ <p>
+ Changed request reminder interval from '[% changes.nag_interval.0 / 24 FILTER html %]' to
+ '[% product.nag_interval / 24 FILTER html %]'.
+ </p>
+[% END %]
diff --git a/extensions/RequestNagger/template/en/default/hook/global/setting-descs-settings.none.tmpl b/extensions/RequestNagger/template/en/default/hook/global/setting-descs-settings.none.tmpl
new file mode 100644
index 000000000..c421a47de
--- /dev/null
+++ b/extensions/RequestNagger/template/en/default/hook/global/setting-descs-settings.none.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%
+ setting_descs.request_nagging = "Send me reminders for overdue requests"
+%]
diff --git a/extensions/RequestNagger/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/RequestNagger/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..12ef38370
--- /dev/null
+++ b/extensions/RequestNagger/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,25 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "request_nagging_flag_invalid" %]
+ [% title = "Invalid Flag" %]
+ Invalid or missing Flag ID
+
+[% ELSIF error == "request_nagging_flag_set" %]
+ [% title = "Flag Already Set" %]
+ The requested Flag has been set, and is no longer pending.
+
+[% ELSIF error == "request_nagging_flag_wind" %]
+ [% title = "No Requestee" %]
+ The requested Flag does not have a requestee, and cannot be deferred.
+
+[% ELSIF error == "request_nagging_flag_not_owned" %]
+ [% title = "Not The Requestee" %]
+ You cannot defer Flags unless you are the requestee.
+
+[% END %]
diff --git a/extensions/RequestNagger/template/en/default/pages/request_defer.html.tmpl b/extensions/RequestNagger/template/en/default/pages/request_defer.html.tmpl
new file mode 100644
index 000000000..e89409ce1
--- /dev/null
+++ b/extensions/RequestNagger/template/en/default/pages/request_defer.html.tmpl
@@ -0,0 +1,101 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Defer Request Reminder"
+ style_urls = [ "extensions/RequestNagger/web/style/requestnagger.css" ]
+ javascript_urls = [ "js/util.js" , "extensions/RequestNagger/web/js/requestnagger.js" ]
+%]
+
+<h2>Defer Request Reminder</h2>
+
+[% IF saved %]
+ <div id="message">
+ Request reminder deferral has been saved.
+ </div>
+[% END %]
+
+<form method="post" action="page.cgi">
+<input type="hidden" name="id" value="request_defer.html">
+<input type="hidden" name="flag" value="[% flag.id FILTER none %]">
+<input type="hidden" name="save" value="1">
+
+<table class="edit_form">
+<tr><td>
+
+ <div class="flag-bug">
+ <a href="show_bug.cgi?id=[% flag.bug.id FILTER none %]">
+ [% terms.Bug %] [%+ flag.bug.id FILTER none %]
+ </a>
+ -
+ <a href="show_bug.cgi?id=[% flag.bug.id FILTER none %]">
+ [% flag.bug.short_desc FILTER html %]
+ </a>
+ </div>
+
+ [% IF flag.attachment %]
+ <div class="flag-attach">
+ <div class="flag-attach-desc">
+ <a href="attachment.cgi?id=[% flag.attachment.id FILTER none %]&amp;action=edit">
+ [% flag.attachment.description FILTER html %]
+ </a>
+ </div>
+ <div class="flag-attach-details">
+ [% flag.attachment.filename FILTER html %] ([% flag.attachment.contenttype FILTER html %]),
+ [% IF flag.attachment.datasize %]
+ [%+ flag.attachment.datasize FILTER unitconvert %]
+ [% ELSE %]
+ <em>deleted</em>
+ [% END %],
+ created by [%+ INCLUDE global/user.html.tmpl who = flag.attachment.attacher %]
+ </div>
+ [% IF flag.attachment.ispatch %]
+ <div class="flag-attach-actions">
+ <a href="attachment.cgi?id=[% flag.attachment.id FILTER none ~%]
+ &amp;action=diff">Diff</a> |
+ <a href="review?bug=[% flag.bug.id FILTER none ~%]
+ &amp;attachment=[% flag.attachment.id FILTER none %]">Review</a>
+ </div>
+ [% END %]
+ </div>
+ [% END %]
+
+ <div class="flag-details">
+ <span class="flag-type">
+ [% flag.type.name FILTER html %]
+ </span>
+ requested by [%+ INCLUDE global/user.html.tmpl who = flag.setter %]
+ [% flag.age FILTER html %]
+ </div>
+
+ [% IF saved %]
+ <div class="deferred">
+ Deferred until [% defer_until.ymd FILTER html %].
+ </div>
+ [% ELSE %]
+ <div class="defer">
+ Defer[% "ed" IF flag.deferred %] for
+ <select name="defer-until" id="defer-until">
+ [% FOREACH defer = defer_until %]
+ <option value="[% defer.date.ymd FILTER html %]"
+ [%+ "selected" IF flag.deferred.ymd == defer.date.ymd %]
+ >
+ [% defer.days FILTER html %] Day[% "s" UNLESS defer.days == 1 %]
+ </option>
+ [% END %]
+ </select>
+ <span id="defer-date"></span>
+ </div>
+ <input type="submit" value="Submit">
+ [% END %]
+</td></tr>
+</table>
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/RequestNagger/web/js/requestnagger.js b/extensions/RequestNagger/web/js/requestnagger.js
new file mode 100644
index 000000000..e5cc43deb
--- /dev/null
+++ b/extensions/RequestNagger/web/js/requestnagger.js
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+YAHOO.util.Event.onDOMReady(function() {
+ YAHOO.util.Event.addListener('defer-until', 'change', function() {
+ YAHOO.util.Dom.get('defer-date').innerHTML = 'until ' + this.value;
+ });
+ bz_fireEvent(YAHOO.util.Dom.get('defer-until'), 'change');
+});
diff --git a/extensions/RequestNagger/web/style/requestnagger.css b/extensions/RequestNagger/web/style/requestnagger.css
new file mode 100644
index 000000000..c4870a08e
--- /dev/null
+++ b/extensions/RequestNagger/web/style/requestnagger.css
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+.edit_form {
+ width: 100%;
+}
+
+.flag-bug {
+ font-size: large;
+}
+
+.flag-bug, .flag-attach, .flag-details {
+ margin-bottom: 1em;
+}
+
+.flag-attach-details {
+ font-size: small;
+}
+
+.flag-attach-actions {
+ font-size: small;
+}
+
+.flag-attach-desc {
+ font-weight: bold;
+}
+
+.flag-type {
+ font-weight: bold;
+}
+
+.defer {
+ margin-bottom: 2em;
+}
+
+.deferred {
+ font-weight: bold;
+}
diff --git a/extensions/RestrictComments/Config.pm b/extensions/RestrictComments/Config.pm
new file mode 100644
index 000000000..bef472cc1
--- /dev/null
+++ b/extensions/RestrictComments/Config.pm
@@ -0,0 +1,16 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::RestrictComments;
+
+use strict;
+
+use constant NAME => 'RestrictComments';
+use constant REQUIRED_MODULES => [];
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/RestrictComments/Extension.pm b/extensions/RestrictComments/Extension.pm
new file mode 100644
index 000000000..001332a8e
--- /dev/null
+++ b/extensions/RestrictComments/Extension.pm
@@ -0,0 +1,95 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::RestrictComments;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Constants;
+
+BEGIN {
+ *Bugzilla::Bug::restrict_comments = \&_bug_restrict_comments;
+}
+
+sub _bug_restrict_comments {
+ my ($self) = @_;
+ return $self->{restrict_comments};
+}
+
+sub bug_check_can_change_field {
+ my ($self, $args) = @_;
+ my ($bug, $priv_results) = @$args{qw(bug priv_results)};
+ my $user = Bugzilla->user;
+
+ if ($user->id
+ && $bug->restrict_comments
+ && !$user->in_group(Bugzilla->params->{'restrict_comments_group'}))
+ {
+ push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ return;
+ }
+}
+
+sub _can_restrict_comments {
+ my ($self, $object) = @_;
+ return unless $object->isa('Bugzilla::Bug');
+ $self->{setter_group} ||= Bugzilla->params->{'restrict_comments_enable_group'};
+ return Bugzilla->user->in_group($self->{setter_group});
+}
+
+sub object_end_of_set_all {
+ my ($self, $args) = @_;
+ my $object = $args->{object};
+ if ($self->_can_restrict_comments($object)) {
+ my $input = Bugzilla->input_params;
+ $object->set('restrict_comments', $input->{restrict_comments} ? 1 : undef);
+ }
+}
+
+sub object_update_columns {
+ my ($self, $args) = @_;
+ my ($object, $columns) = @$args{qw(object columns)};
+ if ($self->_can_restrict_comments($object)) {
+ push(@$columns, 'restrict_comments');
+ }
+}
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my ($class, $columns) = @$args{qw(class columns)};
+ if ($class->isa('Bugzilla::Bug')) {
+ push(@$columns, 'restrict_comments');
+ }
+}
+
+sub bug_fields {
+ my ($self, $args) = @_;
+ my $fields = $args->{'fields'};
+ push (@$fields, 'restrict_comments')
+}
+
+sub config_add_panels {
+ my ($self, $args) = @_;
+ my $modules = $args->{panel_modules};
+ $modules->{RestrictComments} = "Bugzilla::Extension::RestrictComments::Config";
+}
+
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+
+ my $field = new Bugzilla::Field({ name => 'restrict_comments' });
+ if (!$field) {
+ Bugzilla::Field->create({ name => 'restrict_comments', description => 'Restrict Comments' });
+ }
+
+ $dbh->bz_add_column('bugs', 'restrict_comments', { TYPE => 'BOOLEAN' });
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/RestrictComments/lib/Config.pm b/extensions/RestrictComments/lib/Config.pm
new file mode 100644
index 000000000..33607e680
--- /dev/null
+++ b/extensions/RestrictComments/lib/Config.pm
@@ -0,0 +1,47 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::RestrictComments::Config;
+
+use strict;
+use warnings;
+
+use Bugzilla::Config::Common;
+use Bugzilla::Group;
+
+our $sortkey = 510;
+
+sub get_param_list {
+ my ($class) = @_;
+
+ my @param_list = (
+ {
+ name => 'restrict_comments_group',
+ type => 's',
+ choices => \&_get_all_group_names,
+ default => '',
+ checker => \&check_group
+ },
+ {
+ name => 'restrict_comments_enable_group',
+ type => 's',
+ choices => \&_get_all_group_names,
+ default => '',
+ checker => \&check_group
+ },
+ );
+
+ return @param_list;
+}
+
+sub _get_all_group_names {
+ my @group_names = map {$_->name} Bugzilla::Group->get_all;
+ unshift(@group_names, '');
+ return \@group_names;
+}
+
+1;
diff --git a/extensions/RestrictComments/template/en/default/admin/params/restrictcomments.html.tmpl b/extensions/RestrictComments/template/en/default/admin/params/restrictcomments.html.tmpl
new file mode 100644
index 000000000..d2a050563
--- /dev/null
+++ b/extensions/RestrictComments/template/en/default/admin/params/restrictcomments.html.tmpl
@@ -0,0 +1,23 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%
+ title = "Restrict Comments"
+ desc = "Edit Restrict Comments Configuration"
+%]
+
+[% param_descs =
+{
+ restrict_comments_group => "Users must be a member of this group to " _
+ "comment on bug with restricted commenting " _
+ "enabled."
+
+ restrict_comments_enable_group => "Members of this group can toggle " _
+ "'restrict comments' on bugs."
+}
+%]
diff --git a/extensions/RestrictComments/template/en/default/hook/bug/edit-after_comment_commit_button.html.tmpl b/extensions/RestrictComments/template/en/default/hook/bug/edit-after_comment_commit_button.html.tmpl
new file mode 100644
index 000000000..c5250c8c2
--- /dev/null
+++ b/extensions/RestrictComments/template/en/default/hook/bug/edit-after_comment_commit_button.html.tmpl
@@ -0,0 +1,26 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+
+[% RETURN UNLESS user.in_group(Param('restrict_comments_enable_group')) %]
+
+[%# using a table to match alignment of the needinfo checkboxes %]
+<table>
+<tr>
+ <td>
+ <input type="checkbox" name="restrict_comments" id="restrict_comments"
+ [% " checked" IF bug.restrict_comments %]>
+ <label for="restrict_comments">
+ Restrict commenting on this [% terms.bug %] to users in the
+ <b>[% Param('restrict_comments_group') FILTER html %]</b> group.
+ </label>
+ (<a href="page.cgi?id=restrict_comments_guidelines.html"
+ target="_blank">guidelines</a>)
+ </td>
+</tr>
+</table>
diff --git a/extensions/RestrictComments/template/en/default/pages/restrict_comments_guidelines.html.tmpl b/extensions/RestrictComments/template/en/default/pages/restrict_comments_guidelines.html.tmpl
new file mode 100644
index 000000000..694681ad7
--- /dev/null
+++ b/extensions/RestrictComments/template/en/default/pages/restrict_comments_guidelines.html.tmpl
@@ -0,0 +1,62 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Restrict Comments - Guidelines"
+%]
+
+<h3>Restricting Comments</h3>
+
+<p>
+ Some [% terms.bug %] reports are inundated with comments that make it
+ difficult for developers to conduct technical discussions. Restricting
+ comments provides the ability for users in the
+ [%+ Param('restrict_comments_enable_group') FILTER html %] group to prevent
+ users who are not in the [% Param('restrict_comments_group') FILTER html %]
+ from making additional comments.
+</p>
+
+<h3>Guidelines</h3>
+
+<ul>
+ <li>
+ Restrictions may be applied to [% terms.bugs %] which are subject to high
+ volumes of off topic comments, or [% terms.bugs %] which contain high volumes
+ of violations of [% terms.Bugzilla %]
+ <a href="page.cgi?id=etiquette.html">etiquette guidelines</a>.
+ </li>
+ <li>
+ Restrictions should not be used as a preemptive measure against comments
+ which have not yet occurred.
+ </li>
+ <li>
+ Restrictions should not be used to privilege
+ [%+ Param('restrict_comments_group') FILTER html %] users over other users
+ in valid disputes/discussions.
+ </li>
+</ul>
+
+<h3>Impact</h3>
+
+<ul>
+ <li>
+ Users who are not in the [% Param('restrict_comments_group') FILTER html %]
+ group will not be able to comment on the [% terms.bug %], nor will they be
+ able to change the value of any field.
+ </li>
+ <li>
+ All users will still be able to CC themselves to the [% terms.bug %].
+ </li>
+ <li>
+ All users will still be able to vote for the [% terms.bug %].
+ </li>
+</ul>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/Review/Config.pm b/extensions/Review/Config.pm
new file mode 100644
index 000000000..f7da458af
--- /dev/null
+++ b/extensions/Review/Config.pm
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Review;
+use strict;
+
+use constant NAME => 'Review';
+use constant REQUIRED_MODULES => [];
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/Review/Extension.pm b/extensions/Review/Extension.pm
new file mode 100644
index 000000000..f6a3bf743
--- /dev/null
+++ b/extensions/Review/Extension.pm
@@ -0,0 +1,939 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Review;
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+our $VERSION = '1';
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Extension::Review::FlagStateActivity;
+use Bugzilla::Extension::Review::Util;
+use Bugzilla::Install::Filesystem;
+use Bugzilla::Search;
+use Bugzilla::User;
+use Bugzilla::Util qw(clean_text diff_arrays);
+
+use constant UNAVAILABLE_RE => qr/\b(?:unavailable|pto|away)\b/i;
+
+#
+# monkey-patched methods
+#
+
+BEGIN {
+ *Bugzilla::Product::reviewers = \&_product_reviewers;
+ *Bugzilla::Product::reviewers_objs = \&_product_reviewers_objs;
+ *Bugzilla::Product::reviewer_required = \&_product_reviewer_required;
+ *Bugzilla::Component::reviewers = \&_component_reviewers;
+ *Bugzilla::Component::reviewers_objs = \&_component_reviewers_objs;
+ *Bugzilla::Bug::mentors = \&_bug_mentors;
+ *Bugzilla::Bug::bug_mentors = \&_bug_mentors;
+ *Bugzilla::Bug::is_mentor = \&_bug_is_mentor;
+ *Bugzilla::Bug::set_bug_mentors = \&_bug_set_bug_mentors;
+ *Bugzilla::User::review_count = \&_user_review_count;
+}
+
+#
+# monkey-patched methods
+#
+
+sub _product_reviewers { _reviewers($_[0], 'product', $_[1]) }
+sub _product_reviewers_objs { _reviewers_objs($_[0], 'product', $_[1]) }
+sub _component_reviewers { _reviewers($_[0], 'component', $_[1]) }
+sub _component_reviewers_objs { _reviewers_objs($_[0], 'component', $_[1]) }
+
+sub _reviewers {
+ my ($object, $type, $include_disabled) = @_;
+ return join(', ', map { $_->login } @{ _reviewers_objs($object, $type, $include_disabled) });
+}
+
+sub _reviewers_objs {
+ my ($object, $type, $include_disabled) = @_;
+ if (!$object->{reviewers}) {
+ my $dbh = Bugzilla->dbh;
+ my $user_ids = $dbh->selectcol_arrayref(
+ "SELECT user_id FROM ${type}_reviewers WHERE ${type}_id = ? ORDER BY sortkey",
+ undef,
+ $object->id,
+ );
+ # new_from_list always sorts according to the object's definition,
+ # so we have to reorder the list
+ my $users = Bugzilla::User->new_from_list($user_ids);
+ my %user_map = map { $_->id => $_ } @$users;
+ my @reviewers = map { $user_map{$_} } @$user_ids;
+ if (!$include_disabled) {
+ @reviewers = grep { $_->is_enabled
+ && $_->name !~ UNAVAILABLE_RE } @reviewers;
+ }
+ $object->{reviewers} = \@reviewers;
+ }
+ return $object->{reviewers};
+}
+
+sub _user_review_count {
+ my ($self) = @_;
+ if (!exists $self->{review_count}) {
+ my $dbh = Bugzilla->dbh;
+ ($self->{review_count}) = $dbh->selectrow_array(
+ "SELECT COUNT(*)
+ FROM flags
+ INNER JOIN flagtypes ON flagtypes.id = flags.type_id
+ WHERE flags.requestee_id = ?
+ AND " . $dbh->sql_in('flagtypes.name', [ "'review'", "'feedback'" ]),
+ undef,
+ $self->id,
+ );
+ }
+ return $self->{review_count};
+}
+
+#
+# mentor
+#
+
+sub _bug_mentors {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ if (!$self->{bug_mentors}) {
+ my $mentor_ids = $dbh->selectcol_arrayref("
+ SELECT user_id FROM bug_mentors WHERE bug_id = ?",
+ undef,
+ $self->id);
+ $self->{bug_mentors} = [];
+ foreach my $mentor_id (@$mentor_ids) {
+ push(@{ $self->{bug_mentors} },
+ Bugzilla::User->new({ id => $mentor_id, cache => 1 }));
+ }
+ $self->{bug_mentors} = [
+ sort { $a->login cmp $b->login } @{ $self->{bug_mentors} }
+ ];
+ }
+ return $self->{bug_mentors};
+}
+
+sub _bug_is_mentor {
+ my ($self, $user) = @_;
+ my $user_id = ($user || Bugzilla->user)->id;
+ return (grep { $_->id == $user_id} @{ $self->mentors }) ? 1 : 0;
+}
+
+sub _bug_set_bug_mentors {
+ my ($self, $value) = @_;
+ $self->set('bug_mentors', $value);
+}
+
+sub object_validators {
+ my ($self, $args) = @_;
+ return unless $args->{class} eq 'Bugzilla::Bug';
+ $args->{validators}->{bug_mentors} = \&_bug_check_bug_mentors;
+}
+
+sub _bug_check_bug_mentors {
+ my ($self, $value) = @_;
+ return [
+ map { Bugzilla::User->check({ name => $_, cache => 1 }) }
+ ref($value) ? @$value : ($value)
+ ];
+}
+
+sub bug_user_match_fields {
+ my ($self, $args) = @_;
+ $args->{fields}->{bug_mentors} = { type => 'multi' };
+}
+
+sub bug_before_create {
+ my ($self, $args) = @_;
+ my $params = $args->{params};
+ my $stash = $args->{stash};
+ $stash->{bug_mentors} = delete $params->{bug_mentors};
+}
+
+sub bug_end_of_create {
+ my ($self, $args) = @_;
+ my $bug = $args->{bug};
+ my $stash = $args->{stash};
+ if (my $mentors = $stash->{bug_mentors}) {
+ $self->_update_user_table({
+ object => $bug,
+ old_users => [],
+ new_users => $self->_bug_check_bug_mentors($mentors),
+ table => 'bug_mentors',
+ id_field => 'bug_id',
+ });
+ }
+}
+
+sub _update_user_table {
+ my ($self, $args) = @_;
+ my ($object, $old_users, $new_users, $table, $id_field, $has_sortkey, $return) =
+ @$args{qw(object old_users new_users table id_field has_sortkey return)};
+ my $dbh = Bugzilla->dbh;
+ my (@removed, @added);
+
+ # remove deleted users
+ foreach my $old_user (@$old_users) {
+ if (!grep { $_->id == $old_user->id } @$new_users) {
+ $dbh->do(
+ "DELETE FROM $table WHERE $id_field = ? AND user_id = ?",
+ undef,
+ $object->id, $old_user->id,
+ );
+ push @removed, $old_user;
+ }
+ }
+ # add new users
+ foreach my $new_user (@$new_users) {
+ if (!grep { $_->id == $new_user->id } @$old_users) {
+ $dbh->do(
+ "INSERT INTO $table ($id_field, user_id) VALUES (?, ?)",
+ undef,
+ $object->id, $new_user->id,
+ );
+ push @added, $new_user;
+ }
+ }
+
+ return unless @removed || @added;
+
+ if ($has_sortkey) {
+ # update the sortkey for all users
+ for (my $i = 0; $i < scalar(@$new_users); $i++) {
+ $dbh->do(
+ "UPDATE $table SET sortkey=? WHERE $id_field = ? AND user_id = ?",
+ undef,
+ ($i + 1) * 10, $object->id, $new_users->[$i]->id,
+ );
+ }
+ }
+
+ if (!$return) {
+ return undef;
+ }
+ elsif ($return eq 'diff') {
+ return [
+ @removed ? join(', ', map { $_->login } @removed) : undef,
+ @added ? join(', ', map { $_->login } @added) : undef,
+ ];
+ }
+ elsif ($return eq 'old-new') {
+ return [
+ @$old_users ? join(', ', map { $_->login } @$old_users) : '',
+ @$new_users ? join(', ', map { $_->login } @$new_users) : '',
+ ];
+ }
+}
+
+#
+# reviewer-required, review counters, etc
+#
+
+sub _product_reviewer_required { $_[0]->{reviewer_required} }
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my ($class, $columns) = @$args{qw(class columns)};
+ if ($class->isa('Bugzilla::Product')) {
+ push @$columns, 'reviewer_required';
+ }
+ elsif ($class->isa('Bugzilla::User')) {
+ push @$columns, qw(review_request_count feedback_request_count needinfo_request_count);
+ }
+}
+
+sub object_update_columns {
+ my ($self, $args) = @_;
+ my ($object, $columns) = @$args{qw(object columns)};
+ if ($object->isa('Bugzilla::Product')) {
+ push @$columns, 'reviewer_required';
+ }
+ elsif ($object->isa('Bugzilla::User')) {
+ push @$columns, qw(review_request_count feedback_request_count needinfo_request_count);
+ }
+}
+
+sub _new_users_from_input {
+ my ($field) = @_;
+ my $input_params = Bugzilla->input_params;
+ return undef unless exists $input_params->{$field};
+ return [] unless $input_params->{$field};
+ Bugzilla::User::match_field({ $field => {'type' => 'multi'} });;
+ my $value = $input_params->{$field};
+ return [
+ map { Bugzilla::User->check({ name => $_, cache => 1 }) }
+ ref($value) ? @$value : ($value)
+ ];
+}
+
+#
+# create/update
+#
+
+sub object_before_create {
+ my ($self, $args) = @_;
+ my ($class, $params) = @$args{qw(class params)};
+ return unless $class->isa('Bugzilla::Product');
+
+ $params->{reviewer_required} = Bugzilla->cgi->param('reviewer_required') ? 1 : 0;
+}
+
+sub object_end_of_set_all {
+ my ($self, $args) = @_;
+ my ($object, $params) = @$args{qw(object params)};
+ return unless $object->isa('Bugzilla::Product');
+
+ $object->set('reviewer_required', Bugzilla->cgi->param('reviewer_required') ? 1 : 0);
+}
+
+sub object_end_of_create {
+ my ($self, $args) = @_;
+ my ($object, $params) = @$args{qw(object params)};
+
+ if ($object->isa('Bugzilla::Product')) {
+ $self->_update_user_table({
+ object => $object,
+ old_users => [],
+ new_users => _new_users_from_input('reviewers'),
+ table => 'product_reviewers',
+ id_field => 'product_id',
+ has_sortkey => 1,
+ });
+ }
+ elsif ($object->isa('Bugzilla::Component')) {
+ $self->_update_user_table({
+ object => $object,
+ old_users => [],
+ new_users => _new_users_from_input('reviewers'),
+ table => 'component_reviewers',
+ id_field => 'component_id',
+ has_sortkey => 1,
+ });
+ }
+ elsif (_is_countable_flag($object) && $object->requestee_id && $object->status eq '?') {
+ _adjust_request_count($object, +1);
+ }
+ if (_is_countable_flag($object)) {
+ $self->_log_flag_state_activity($object, $object->status, $object->modification_date);
+ }
+}
+
+sub object_end_of_update {
+ my ($self, $args) = @_;
+ my ($object, $old_object, $changes) = @$args{qw(object old_object changes)};
+
+ if ($object->isa('Bugzilla::Product') && exists Bugzilla->input_params->{reviewers}) {
+ my $diff = $self->_update_user_table({
+ object => $object,
+ old_users => $old_object->reviewers_objs(1),
+ new_users => _new_users_from_input('reviewers'),
+ table => 'product_reviewers',
+ id_field => 'product_id',
+ has_sortkey => 1,
+ return => 'old-new',
+ });
+ $changes->{reviewers} = $diff if $diff;
+ }
+ elsif ($object->isa('Bugzilla::Component')) {
+ my $diff = $self->_update_user_table({
+ object => $object,
+ old_users => $old_object->reviewers_objs(1),
+ new_users => _new_users_from_input('reviewers'),
+ table => 'component_reviewers',
+ id_field => 'component_id',
+ has_sortkey => 1,
+ return => 'old-new',
+ });
+ $changes->{reviewers} = $diff if $diff;
+ }
+ elsif ($object->isa('Bugzilla::Bug')) {
+ my $diff = $self->_update_user_table({
+ object => $object,
+ old_users => $old_object->mentors,
+ new_users => $object->mentors,
+ table => 'bug_mentors',
+ id_field => 'bug_id',
+ return => 'diff',
+ });
+ $changes->{bug_mentor} = $diff if $diff;
+ }
+ elsif (_is_countable_flag($object)) {
+ my ($old_status, $new_status) = ($old_object->status, $object->status);
+ if ($old_status ne '?' && $new_status eq '?') {
+ # setting flag to ?
+ _adjust_request_count($object, +1);
+ }
+ elsif ($old_status eq '?' && $new_status ne '?') {
+ # setting flag from ?
+ _adjust_request_count($old_object, -1);
+ }
+ elsif ($old_object->requestee_id && !$object->requestee_id) {
+ # removing requestee
+ _adjust_request_count($old_object, -1);
+ }
+ elsif (!$old_object->requestee_id && $object->requestee_id) {
+ # setting requestee
+ _adjust_request_count($object, +1);
+ }
+ elsif ($old_object->requestee_id && $object->requestee_id
+ && $old_object->requestee_id != $object->requestee_id)
+ {
+ # changing requestee
+ _adjust_request_count($old_object, -1);
+ _adjust_request_count($object, +1);
+ }
+ }
+}
+
+sub flag_updated {
+ my ($self, $args) = @_;
+ my $flag = $args->{flag};
+ my $timestamp = $args->{timestamp};
+ my $changes = $args->{changes};
+
+ return unless scalar(keys %$changes);
+ if (_is_countable_flag($flag)) {
+ $self->_log_flag_state_activity($flag, $flag->status, $timestamp);
+ }
+}
+
+sub flag_deleted {
+ my ($self, $args) = @_;
+ my $flag = $args->{flag};
+ my $timestamp = $args->{timestamp};
+
+ if (_is_countable_flag($flag) && $flag->requestee_id && $flag->status eq '?') {
+ _adjust_request_count($flag, -1);
+ }
+
+ if (_is_countable_flag($flag)) {
+ $self->_log_flag_state_activity($flag, 'X', $timestamp, Bugzilla->user->id);
+ }
+}
+
+sub _is_countable_flag {
+ my ($object) = @_;
+ return unless $object->isa('Bugzilla::Flag');
+ my $type_name = $object->type->name;
+ return $type_name eq 'review' || $type_name eq 'feedback' || $type_name eq 'needinfo';
+}
+
+sub _log_flag_state_activity {
+ my ($self, $flag, $status, $timestamp, $setter_id) = @_;
+
+ $setter_id //= $flag->setter_id;
+
+ Bugzilla::Extension::Review::FlagStateActivity->create({
+ flag_when => $timestamp,
+ setter_id => $setter_id,
+ status => $status,
+ type_id => $flag->type_id,
+ flag_id => $flag->id,
+ requestee_id => $flag->requestee_id,
+ bug_id => $flag->bug_id,
+ attachment_id => $flag->attach_id,
+ });
+}
+
+sub _adjust_request_count {
+ my ($flag, $add) = @_;
+ return unless my $requestee_id = $flag->requestee_id;
+ my $field = $flag->type->name . '_request_count';
+
+ # update the current user's object so things are display correctly on the
+ # post-processing page
+ my $user = Bugzilla->user;
+ if ($requestee_id == $user->id) {
+ $user->{$field} += $add;
+ }
+
+ # update database directly to avoid creating audit_log entries
+ $add = $add == -1 ? ' - 1' : ' + 1';
+ Bugzilla->dbh->do(
+ "UPDATE profiles SET $field = $field $add WHERE userid = ?",
+ undef,
+ $requestee_id
+ );
+ Bugzilla->memcached->clear({ table => 'profiles', id => $requestee_id });
+}
+
+# bugzilla's handling of requestee matching when creating bugs is "if it's
+# wrong, or matches too many, default to empty", which breaks mandatory
+# reviewer requirements. instead we just throw an error.
+sub post_bug_attachment_flags {
+ my ($self, $args) = @_;
+ $self->_check_review_flag($args);
+}
+
+sub create_attachment_flags {
+ my ($self, $args) = @_;
+ $self->_check_review_flag($args);
+}
+
+sub _check_review_flag {
+ my ($self, $args) = @_;
+ my $bug = $args->{bug};
+ my $cgi = Bugzilla->cgi;
+
+ # extract the set flag-types
+ my @flagtype_ids = map { /^flag_type-(\d+)$/ ? $1 : () } $cgi->param();
+ @flagtype_ids = grep { $cgi->param("flag_type-$_") eq '?' } @flagtype_ids;
+ return unless scalar(@flagtype_ids);
+
+ # find valid review flagtypes
+ my $flag_types = Bugzilla::FlagType::match({
+ product_id => $bug->product_id,
+ component_id => $bug->component_id,
+ is_active => 1
+ });
+ foreach my $flag_type (@$flag_types) {
+ next unless $flag_type->name eq 'review'
+ && $flag_type->target_type eq 'attachment';
+ my $type_id = $flag_type->id;
+ next unless scalar(grep { $_ == $type_id } @flagtype_ids);
+
+ my $reviewers = clean_text($cgi->param("requestee_type-$type_id") || '');
+ if ($reviewers eq '' && $bug->product_obj->reviewer_required) {
+ ThrowUserError('reviewer_required');
+ }
+
+ foreach my $reviewer (split(/[,;]+/, $reviewers)) {
+ # search on the reviewer
+ my $users = Bugzilla::User::match($reviewer, 2, 1);
+
+ # no matches
+ if (scalar(@$users) == 0) {
+ ThrowUserError('user_match_failed', { name => $reviewer });
+ }
+
+ # more than one match, throw error
+ if (scalar(@$users) > 1) {
+ ThrowUserError('user_match_too_many', { fields => [ 'review' ] });
+ }
+ }
+ }
+}
+
+sub flag_end_of_update {
+ my ($self, $args) = @_;
+ my ($object, $new_flags) = @$args{qw(object new_flags)};
+ my $bug = $object->isa('Bugzilla::Attachment') ? $object->bug : $object;
+ return unless $bug->product_obj->reviewer_required;
+
+ foreach my $orig_change (@$new_flags) {
+ my $change = $orig_change; # work on a copy
+ $change =~ s/^[^:]+://;
+ my $reviewer = '';
+ if ($change =~ s/\(([^\)]+)\)$//) {
+ $reviewer = $1;
+ }
+ my ($name, $value) = $change =~ /^(.+)(.)$/;
+
+ if ($name eq 'review' && $value eq '?' && $reviewer eq '') {
+ ThrowUserError('reviewer_required');
+ }
+ }
+}
+
+#
+# search
+#
+
+sub buglist_columns {
+ my ($self, $args) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $columns = $args->{columns};
+ $columns->{bug_mentor} = { title => 'Mentor' };
+ if (Bugzilla->user->id) {
+ $columns->{bug_mentor}->{name}
+ = $dbh->sql_group_concat('map_mentors_names.login_name');
+ }
+ else {
+ $columns->{bug_mentor}->{name}
+ = $dbh->sql_group_concat('map_mentors_names.realname');
+
+ }
+}
+
+sub buglist_column_joins {
+ my ($self, $args) = @_;
+ my $column_joins = $args->{column_joins};
+ $column_joins->{bug_mentor} = {
+ as => 'map_mentors',
+ table => 'bug_mentors',
+ then_to => {
+ as => 'map_mentors_names',
+ table => 'profiles',
+ from => 'map_mentors.user_id',
+ to => 'userid',
+ },
+ },
+}
+
+sub search_operator_field_override {
+ my ($self, $args) = @_;
+ my $operators = $args->{operators};
+ $operators->{bug_mentor} = {
+ _non_changed => sub {
+ Bugzilla::Search::_user_nonchanged(@_)
+ }
+ };
+}
+
+#
+# web service / pages
+#
+
+sub webservice {
+ my ($self, $args) = @_;
+ my $dispatch = $args->{dispatch};
+ $dispatch->{Review} = "Bugzilla::Extension::Review::WebService";
+}
+
+sub page_before_template {
+ my ($self, $args) = @_;
+
+ if ($args->{page_id} eq 'review_suggestions.html') {
+ $self->review_suggestions_report($args);
+ }
+ elsif ($args->{page_id} eq 'review_requests_rebuild.html') {
+ $self->review_requests_rebuild($args);
+ }
+ elsif ($args->{page_id} eq 'review_history.html') {
+ $self->review_history($args);
+ }
+}
+
+sub review_suggestions_report {
+ my ($self, $args) = @_;
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ my $products = [];
+ my @products = sort { lc($a->name) cmp lc($b->name) }
+ @{ Bugzilla->user->get_accessible_products };
+ foreach my $product_obj (@products) {
+ my $has_reviewers = 0;
+ my $product = {
+ name => $product_obj->name,
+ components => [],
+ reviewers => $product_obj->reviewers_objs(1),
+ };
+ $has_reviewers = scalar @{ $product->{reviewers} };
+
+ foreach my $component_obj (@{ $product_obj->components }) {
+ my $component = {
+ name => $component_obj->name,
+ reviewers => $component_obj->reviewers_objs(1),
+ };
+ if (@{ $component->{reviewers} }) {
+ push @{ $product->{components} }, $component;
+ $has_reviewers = 1;
+ }
+ }
+
+ if ($has_reviewers) {
+ push @$products, $product;
+ }
+ }
+ $args->{vars}->{products} = $products;
+}
+
+sub review_requests_rebuild {
+ my ($self, $args) = @_;
+
+ Bugzilla->user->in_group('admin')
+ || ThrowUserError('auth_failure', { group => 'admin',
+ action => 'run',
+ object => 'review_requests_rebuild' });
+ if (Bugzilla->cgi->param('rebuild')) {
+ my $processed_users = 0;
+ rebuild_review_counters(sub {
+ my ($count, $total) = @_;
+ $processed_users = $total;
+ });
+ $args->{vars}->{rebuild} = 1;
+ $args->{vars}->{total} = $processed_users;
+ }
+}
+
+sub review_history {
+ my ($self, $args) = @_;
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+ Bugzilla::User::match_field({ 'requestee' => { 'type' => 'single' } });
+ my $requestee = Bugzilla->input_params->{requestee};
+ if ($requestee) {
+ $args->{vars}{requestee} = Bugzilla::User->check({ name => $requestee, cache => 1 });
+ }
+ else {
+ $args->{vars}{requestee} = $user;
+ }
+}
+
+#
+# installation
+#
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'product_reviewers'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE',
+ }
+ },
+ display_name => {
+ TYPE => 'VARCHAR(64)',
+ },
+ product_id => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'products',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ }
+ },
+ sortkey => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ DEFAULT => 0,
+ },
+ ],
+ INDEXES => [
+ product_reviewers_idx => {
+ FIELDS => [ 'user_id', 'product_id' ],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'component_reviewers'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE',
+ }
+ },
+ display_name => {
+ TYPE => 'VARCHAR(64)',
+ },
+ component_id => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'components',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ }
+ },
+ sortkey => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ DEFAULT => 0,
+ },
+ ],
+ INDEXES => [
+ component_reviewers_idx => {
+ FIELDS => [ 'user_id', 'component_id' ],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+
+ $args->{'schema'}->{'flag_state_activity'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+
+ flag_when => {
+ TYPE => 'DATETIME',
+ NOTNULL => 1,
+ },
+
+ type_id => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'flagtypes',
+ COLUMN => 'id',
+ DELETE => 'CASCADE'
+ }
+ },
+
+ flag_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ },
+
+ setter_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ },
+ },
+
+ requestee_id => {
+ TYPE => 'INT3',
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ },
+ },
+
+ bug_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE'
+ }
+ },
+
+ attachment_id => {
+ TYPE => 'INT3',
+ REFERENCES => {
+ TABLE => 'attachments',
+ COLUMN => 'attach_id',
+ DELETE => 'CASCADE'
+ }
+ },
+
+ status => {
+ TYPE => 'CHAR(1)',
+ NOTNULL => 1,
+ },
+ ],
+ };
+
+ $args->{'schema'}->{'bug_mentors'} = {
+ FIELDS => [
+ bug_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE',
+ },
+ },
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE',
+ }
+ },
+ ],
+ INDEXES => [
+ bug_mentors_idx => {
+ FIELDS => [ 'bug_id', 'user_id' ],
+ TYPE => 'UNIQUE',
+ },
+ bug_mentors_bug_id_idx => [ 'bug_id' ],
+ ],
+ };
+
+ $args->{'schema'}->{'bug_mentors'} = {
+ FIELDS => [
+ bug_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE',
+ },
+ },
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE',
+ }
+ },
+ ],
+ INDEXES => [
+ bug_mentors_idx => {
+ FIELDS => [ 'bug_id', 'user_id' ],
+ TYPE => 'UNIQUE',
+ },
+ bug_mentors_bug_id_idx => [ 'bug_id' ],
+ ],
+ };
+}
+
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_add_column(
+ 'products',
+ 'reviewer_required', { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' }
+ );
+ $dbh->bz_add_column(
+ 'profiles',
+ 'review_request_count', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0 }
+ );
+ $dbh->bz_add_column(
+ 'profiles',
+ 'feedback_request_count', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0 }
+ );
+ $dbh->bz_add_column(
+ 'profiles',
+ 'needinfo_request_count', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0 }
+ );
+
+ my $field = Bugzilla::Field->new({ name => 'bug_mentor' });
+ if (!$field) {
+ Bugzilla::Field->create({
+ name => 'bug_mentor',
+ description => 'Mentor'
+ });
+ }
+}
+
+sub install_filesystem {
+ my ($self, $args) = @_;
+ my $files = $args->{files};
+ my $extensions_dir = bz_locations()->{extensionsdir};
+ $files->{"$extensions_dir/Review/bin/review_requests_rebuild.pl"} = {
+ perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE
+ };
+}
+
+
+__PACKAGE__->NAME;
diff --git a/extensions/Review/bin/migrate_mentor_from_whiteboard.pl b/extensions/Review/bin/migrate_mentor_from_whiteboard.pl
new file mode 100755
index 000000000..8d34963ec
--- /dev/null
+++ b/extensions/Review/bin/migrate_mentor_from_whiteboard.pl
@@ -0,0 +1,229 @@
+#!/usr/bin/perl
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+$| = 1;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../..";
+
+use Bugzilla;
+BEGIN { Bugzilla->extensions() }
+
+use Bugzilla::Bug;
+use Bugzilla::Constants;
+use Bugzilla::Group;
+use Bugzilla::User;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+print <<EOF;
+This script migrates mentors from the whiteboard to BMO's bug_mentor field.
+The mentor needs to be in the form of [mentor=UUU].
+
+It's safe to run this script multiple times, or to cancel this script while
+running.
+
+Press <Return> to start, or Ctrl+C to cancel..
+EOF
+<>;
+
+# we need to be logged in to do user searching and update bugs
+my $nobody = Bugzilla::User->check({ name => 'nobody@mozilla.org' });
+$nobody->{groups} = [ Bugzilla::Group->get_all ];
+Bugzilla->set_user($nobody);
+
+my $mentor_field = Bugzilla::Field->check({ name => 'bug_mentor' });
+my $dbh = Bugzilla->dbh;
+
+# fix broken migration
+
+my $sth = $dbh->prepare("
+ SELECT id, bug_id, bug_when, removed, added
+ FROM bugs_activity
+ WHERE fieldid = ?
+ ORDER BY bug_id,bug_when,removed
+");
+$sth->execute($mentor_field->id);
+my %pair;
+while (my $row = $sth->fetchrow_hashref) {
+ if ($row->{added} && $row->{removed}) {
+ %pair = ();
+ next;
+ }
+ if ($row->{added}) {
+ $pair{bug_id} = $row->{bug_id};
+ $pair{bug_when} = $row->{bug_when};
+ $pair{who} = $row->{added};
+ next;
+ }
+ if (!$pair{bug_id}) {
+ next;
+ }
+ if ($row->{removed}) {
+ if ($row->{bug_id} == $pair{bug_id}
+ && $row->{bug_when} eq $pair{bug_when}
+ && $row->{removed} eq $pair{who})
+ {
+ print "Fixing mentor on bug $row->{bug_id}\n";
+ my $user = Bugzilla::User->check({ name => $row->{removed} });
+ $dbh->bz_start_transaction;
+ $dbh->do(
+ "DELETE FROM bugs_activity WHERE id = ?",
+ undef,
+ $row->{id}
+ );
+ my ($exists) = $dbh->selectrow_array(
+ "SELECT 1 FROM bug_mentors WHERE bug_id = ? AND user_id = ?",
+ undef,
+ $row->{bug_id}, $user->id
+ );
+ if (!$exists) {
+ $dbh->do(
+ "INSERT INTO bug_mentors (bug_id, user_id) VALUES (?, ?)",
+ undef,
+ $row->{bug_id}, $user->id,
+ );
+ }
+ $dbh->bz_commit_transaction;
+ %pair = ();
+ }
+ }
+}
+
+# migrate remaining bugs
+
+my $bug_ids = $dbh->selectcol_arrayref("
+ SELECT bug_id
+ FROM bugs
+ WHERE status_whiteboard LIKE '%[mentor=%'
+ AND resolution=''
+ ORDER BY bug_id
+");
+print "Bugs found: " . scalar(@$bug_ids) . "\n";
+my $bugs = Bugzilla::Bug->new_from_list($bug_ids);
+foreach my $bug (@$bugs) {
+ my $whiteboard = $bug->status_whiteboard;
+ my $orig_whiteboard = $whiteboard;
+ my ($mentors, $errors) = extract_mentors($whiteboard);
+
+ printf "%7s %s\n", $bug->id, $whiteboard;
+ foreach my $error (@$errors) {
+ print " $error\n";
+ }
+ foreach my $user (@$mentors) {
+ print " Mentor: " . $user->identity . "\n";
+ }
+ next if @$errors;
+ $whiteboard =~ s/\[mentor=[^\]]+\]//g;
+
+ my $migrated = $dbh->selectcol_arrayref(
+ "SELECT user_id FROM bug_mentors WHERE bug_id = ?",
+ undef,
+ $bug->id
+ );
+ if (@$migrated) {
+ foreach my $migrated_id (@$migrated) {
+ $mentors = [
+ grep { $_->id != $migrated_id }
+ @$mentors
+ ];
+ }
+ if (!@$mentors) {
+ print " mentor(s) already migrated\n";
+ next;
+ }
+ }
+
+ my $delta_ts = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ $dbh->bz_start_transaction;
+ $dbh->do(
+ "UPDATE bugs SET status_whiteboard=? WHERE bug_id=?",
+ undef,
+ $whiteboard, $bug->id
+ );
+ Bugzilla::Bug::LogActivityEntry(
+ $bug->id,
+ 'status_whiteboard',
+ $orig_whiteboard,
+ $whiteboard,
+ $nobody->id,
+ $delta_ts,
+ );
+ foreach my $mentor (@$mentors) {
+ $dbh->do(
+ "INSERT INTO bug_mentors (bug_id, user_id) VALUES (?, ?)",
+ undef,
+ $bug->id, $mentor->id,
+ );
+ Bugzilla::Bug::LogActivityEntry(
+ $bug->id,
+ 'bug_mentor',
+ '',
+ $mentor->login,
+ $nobody->id,
+ $delta_ts,
+ );
+ }
+ $dbh->do(
+ "UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?",
+ undef,
+ $bug->id,
+ );
+ $dbh->bz_commit_transaction;
+}
+
+sub extract_mentors {
+ my ($whiteboard) = @_;
+
+ my (@mentors, @errors);
+ my $logout = 0;
+ while ($whiteboard =~ /\[mentor=([^\]]+)\]/g) {
+ my $mentor_string = $1;
+ $mentor_string =~ s/(^\s+|\s+$)//g;
+ if ($mentor_string =~ /\@/) {
+ # assume it's a full username if it contains an @
+ my $user = Bugzilla::User->new({ name => $mentor_string });
+ if (!$user) {
+ push @errors, "'$mentor_string' failed to match any users";
+ } else {
+ push @mentors, $user;
+ }
+ } else {
+ # otherwise assume it's a : prefixed nick
+
+ $mentor_string =~ s/^://;
+ my $matches = find_users(":$mentor_string");
+ if (!@$matches) {
+ $matches = find_users($mentor_string);
+ }
+
+ if (!$matches || !@$matches) {
+ push @errors, "'$mentor_string' failed to match any users";
+ } elsif (scalar(@$matches) > 1) {
+ push @errors, "'$mentor_string' matches more than one user: " .
+ join(', ', map { $_->identity } @$matches);
+ } else {
+ push @mentors, $matches->[0];
+ }
+ }
+ }
+ return (\@mentors, \@errors);
+}
+
+sub find_users {
+ my ($query) = @_;
+ my $matches = Bugzilla::User::match("*$query*", 2);
+ return [
+ grep { $_->name =~ /:?\Q$query\E\b/i }
+ @$matches
+ ];
+}
diff --git a/extensions/Review/bin/review_requests_rebuild.pl b/extensions/Review/bin/review_requests_rebuild.pl
new file mode 100755
index 000000000..04f8b1042
--- /dev/null
+++ b/extensions/Review/bin/review_requests_rebuild.pl
@@ -0,0 +1,29 @@
+#!/usr/bin/perl
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+$| = 1;
+
+use FindBin qw($Bin);
+use lib "$Bin/../../..";
+
+use Bugzilla;
+BEGIN { Bugzilla->extensions() }
+
+use Bugzilla::Constants;
+use Bugzilla::Install::Util qw(indicate_progress);
+use Bugzilla::Extension::Review::Util;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+rebuild_review_counters(sub{
+ my ($count, $total) = @_;
+ indicate_progress({ current => $count, total => $total, every => 5 });
+});
diff --git a/extensions/Review/lib/FlagStateActivity.pm b/extensions/Review/lib/FlagStateActivity.pm
new file mode 100644
index 000000000..46e9300a5
--- /dev/null
+++ b/extensions/Review/lib/FlagStateActivity.pm
@@ -0,0 +1,122 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Review::FlagStateActivity;
+use strict;
+use warnings;
+
+use Bugzilla::Error qw(ThrowUserError);
+use Bugzilla::Util qw(trim datetime_from);
+use List::MoreUtils qw(none);
+
+use base qw( Bugzilla::Object );
+
+use constant DB_TABLE => 'flag_state_activity';
+use constant LIST_ORDER => 'id';
+use constant AUDIT_CREATES => 0;
+use constant AUDIT_UPDATES => 0;
+use constant AUDIT_REMOVES => 0;
+
+use constant DB_COLUMNS => qw(
+ id
+ flag_when
+ type_id
+ flag_id
+ setter_id
+ requestee_id
+ bug_id
+ attachment_id
+ status
+);
+
+
+sub _check_param_required {
+ my ($param) = @_;
+
+ return sub {
+ my ($invocant, $value) = @_;
+ $value = trim($value)
+ or ThrowCodeError('param_required', {param => $param});
+ return $value;
+ },
+}
+
+sub _check_date {
+ my ($invocant, $date) = @_;
+
+ $date = trim($date);
+ datetime_from($date)
+ or ThrowUserError('illegal_date', { date => $date,
+ format => 'YYYY-MM-DD HH24:MI:SS' });
+ return $date;
+}
+
+sub _check_status {
+ my ($self, $status) = @_;
+
+ # - Make sure the status is valid.
+ # - Make sure the user didn't request the flag unless it's requestable.
+ # If the flag existed and was requested before it became unrequestable,
+ # leave it as is.
+ if (none { $status eq $_ } qw( X + - ? )) {
+ ThrowUserError(
+ 'flag_status_invalid',
+ {
+ id => $self->id,
+ status => $status
+ }
+ );
+ }
+ return $status;
+}
+
+use constant VALIDATORS => {
+ flag_when => \&_check_date,
+ type_id => _check_param_required('type_id'),
+ flag_id => _check_param_required('flag_id'),
+ setter_id => _check_param_required('setter_id'),
+ bug_id => _check_param_required('bug_id'),
+ status => \&_check_status,
+};
+
+sub flag_when { return $_[0]->{flag_when} }
+sub type_id { return $_[0]->{type_id} }
+sub flag_id { return $_[0]->{flag_id} }
+sub setter_id { return $_[0]->{setter_id} }
+sub bug_id { return $_[0]->{bug_id} }
+sub requestee_id { return $_[0]->{requestee_id} }
+sub attachment_id { return $_[0]->{attachment_id} }
+sub status { return $_[0]->{status} }
+
+sub type {
+ my ($self) = @_;
+ return $self->{type} //= Bugzilla::FlagType->new({ id => $self->type_id, cache => 1 });
+}
+
+sub setter {
+ my ($self) = @_;
+ return $self->{setter} //= Bugzilla::User->new({ id => $self->setter_id, cache => 1 });
+}
+
+sub requestee {
+ my ($self) = @_;
+ return undef unless defined $self->requestee_id;
+ return $self->{requestee} //= Bugzilla::User->new({ id => $self->requestee_id, cache => 1 });
+}
+
+sub bug {
+ my ($self) = @_;
+ return $self->{bug} //= Bugzilla::Bug->new({ id => $self->bug_id, cache => 1 });
+}
+
+sub attachment {
+ my ($self) = @_;
+ return $self->{attachment} //=
+ Bugzilla::Attachment->new({ id => $self->attachment_id, cache => 1 });
+}
+
+1;
diff --git a/extensions/Review/lib/Util.pm b/extensions/Review/lib/Util.pm
new file mode 100644
index 000000000..c00e31b6b
--- /dev/null
+++ b/extensions/Review/lib/Util.pm
@@ -0,0 +1,84 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Review::Util;
+use strict;
+use warnings;
+
+use base qw(Exporter);
+use Bugzilla;
+
+our @EXPORT = qw( rebuild_review_counters );
+
+sub rebuild_review_counters {
+ my ($callback) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->bz_start_transaction;
+
+ my $rows = $dbh->selectall_arrayref("
+ SELECT flags.requestee_id AS user_id,
+ flagtypes.name AS flagtype,
+ COUNT(*) as count
+ FROM flags
+ INNER JOIN profiles ON profiles.userid = flags.requestee_id
+ INNER JOIN flagtypes ON flagtypes.id = flags.type_id
+ WHERE flags.status = '?'
+ AND flagtypes.name IN ('review', 'feedback', 'needinfo')
+ GROUP BY flags.requestee_id, flagtypes.name
+ ", { Slice => {} });
+
+ my ($count, $total, $current) = (1, scalar(@$rows), { id => 0 });
+ foreach my $row (@$rows) {
+ $callback->($count++, $total) if $callback;
+ if ($row->{user_id} != $current->{id}) {
+ _update_profile($dbh, $current) if $current->{id};
+ $current = { id => $row->{user_id} };
+ }
+ $current->{$row->{flagtype}} = $row->{count};
+ }
+ _update_profile($dbh, $current) if $current->{id};
+
+ foreach my $field (qw( review feedback needinfo )) {
+ _fix_negatives($dbh, $field);
+ }
+
+ $dbh->bz_commit_transaction;
+}
+
+sub _fix_negatives {
+ my ($dbh, $field) = @_;
+ my $user_ids = $dbh->selectcol_arrayref(
+ "SELECT userid FROM profiles WHERE ${field}_request_count < 0"
+ );
+ return unless @$user_ids;
+ $dbh->do(
+ "UPDATE profiles SET ${field}_request_count = 0 WHERE " . $dbh->sql_in('userid', $user_ids)
+ );
+ foreach my $user_id (@$user_ids) {
+ Bugzilla->memcached->clear({ table => 'profiles', id => $user_id });
+ }
+}
+
+sub _update_profile {
+ my ($dbh, $data) = @_;
+ $dbh->do("
+ UPDATE profiles
+ SET review_request_count = ?,
+ feedback_request_count = ?,
+ needinfo_request_count = ?
+ WHERE userid = ?",
+ undef,
+ $data->{review} || 0,
+ $data->{feedback} || 0,
+ $data->{needinfo} || 0,
+ $data->{id}
+ );
+ Bugzilla->memcached->clear({ table => 'profiles', id => $data->{id} });
+}
+
+1;
diff --git a/extensions/Review/lib/WebService.pm b/extensions/Review/lib/WebService.pm
new file mode 100644
index 000000000..d16ab3dd8
--- /dev/null
+++ b/extensions/Review/lib/WebService.pm
@@ -0,0 +1,488 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Review::WebService;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::WebService);
+
+use Bugzilla::Bug;
+use Bugzilla::Component;
+use Bugzilla::Error;
+use Bugzilla::Util qw(detaint_natural trick_taint);
+use Bugzilla::WebService::Util 'filter';
+
+sub suggestions {
+ my ($self, $params) = @_;
+ my $dbh = Bugzilla->switch_to_shadow_db();
+
+ my ($bug, $product, $component);
+ if (exists $params->{bug_id}) {
+ $bug = Bugzilla::Bug->check($params->{bug_id});
+ $product = $bug->product_obj;
+ $component = $bug->component_obj;
+ }
+ elsif (exists $params->{product}) {
+ $product = Bugzilla::Product->check($params->{product});
+ if (exists $params->{component}) {
+ $component = Bugzilla::Component->check({
+ product => $product, name => $params->{component}
+ });
+ }
+ }
+ else {
+ ThrowUserError("reviewer_suggestions_param_required");
+ }
+
+ my @reviewers;
+ if ($bug) {
+ # we always need to be authentiated to perform user matching
+ my $user = Bugzilla->user;
+ if (!$user->id) {
+ Bugzilla->set_user(Bugzilla::User->check({ name => 'nobody@mozilla.org' }));
+ push @reviewers, @{ $bug->mentors };
+ Bugzilla->set_user($user);
+ } else {
+ push @reviewers, @{ $bug->mentors };
+ }
+ }
+ if ($component) {
+ push @reviewers, @{ $component->reviewers_objs };
+ }
+ if (!@{ $component->reviewers_objs }) {
+ push @reviewers, @{ $product->reviewers_objs };
+ }
+
+ my @result;
+ foreach my $reviewer (@reviewers) {
+ push @result, {
+ id => $self->type('int', $reviewer->id),
+ email => $self->type('email', $reviewer->login),
+ name => $self->type('string', $reviewer->name),
+ review_count => $self->type('int', $reviewer->review_count),
+ };
+ }
+ return \@result;
+}
+
+sub flag_activity {
+ my ($self, $params) = @_;
+ my $dbh = Bugzilla->switch_to_shadow_db();
+ my %match_criteria;
+
+ if (my $flag_id = $params->{flag_id}) {
+ detaint_natural($flag_id)
+ or ThrowUserError('invalid_flag_id', { flag_id => $flag_id });
+
+ $match_criteria{flag_id} = $flag_id;
+ }
+
+ if (my $flag_ids = $params->{flag_ids}) {
+ foreach my $flag_id (@$flag_ids) {
+ detaint_natural($flag_id)
+ or ThrowUserError('invalid_flag_id', { flag_id => $flag_id });
+ }
+
+ $match_criteria{flag_id} = $flag_ids;
+ }
+
+ if (my $type_id = $params->{type_id}) {
+ detaint_natural($type_id)
+ or ThrowUserError('invalid_flag_type_id', { type_id => $type_id });
+
+ $match_criteria{type_id} = $type_id;
+ }
+
+ if (my $type_name = $params->{type_name}) {
+ trick_taint($type_name);
+ my $flag_types = Bugzilla::FlagType::match({ name => $type_name });
+ $match_criteria{type_id} = [map { $_->id } @$flag_types];
+ }
+
+ for my $user_field (qw( requestee setter )) {
+ if (my $user_name = $params->{$user_field}) {
+ my $user = Bugzilla::User->check({ name => $user_name, cache => 1, _error => 'invalid_username' });
+
+ $match_criteria{ $user_field . "_id" } = $user->id;
+ }
+ }
+
+ ThrowCodeError('param_required', { param => 'limit', function => 'Review.flag_activity()' })
+ if defined $params->{offset} && !defined $params->{limit};
+
+ my $limit = delete $params->{limit};
+ my $offset = delete $params->{offset};
+ my $max_results = Bugzilla->params->{max_search_results};
+
+ if (!$limit || $limit > $max_results) {
+ $limit = $max_results;
+ }
+
+ $match_criteria{LIMIT} = $limit;
+ $match_criteria{OFFSET} = $offset if defined $offset;
+ # Hide data until Bug 1073364 is resolved.
+ $match_criteria{WHERE} = { 'flag_when > ?' => '2014-09-23 21:17:16' };
+
+ # Throw error if no other parameters have been passed other than limit and offset
+ if (!grep(!/^(LIMIT|OFFSET)$/, keys %match_criteria)) {
+ ThrowUserError('flag_activity_parameters_required');
+ }
+
+ my $matches = Bugzilla::Extension::Review::FlagStateActivity->match(\%match_criteria);
+ my $user = Bugzilla->user;
+ $user->visible_bugs([ map { $_->bug_id } @$matches ]);
+ my @results = map { $self->_flag_state_activity_to_hash($_, $params) }
+ grep { $user->can_see_bug($_->bug_id) && _can_see_attachment($user, $_) }
+ @$matches;
+ return \@results;
+}
+
+sub _can_see_attachment {
+ my ($user, $flag_state_activity) = @_;
+
+ return 1 if !$flag_state_activity->attachment_id;
+ return 0 if $flag_state_activity->attachment->isprivate && !$user->is_insider;
+ return 1;
+}
+
+sub rest_resources {
+ return [
+ # bug-id
+ qr{^/review/suggestions/(\d+)$}, {
+ GET => {
+ method => 'suggestions',
+ params => sub {
+ return { bug_id => $_[0] };
+ },
+ },
+ },
+ # product/component
+ qr{^/review/suggestions/([^/]+)/(.+)$}, {
+ GET => {
+ method => 'suggestions',
+ params => sub {
+ return { product => $_[0], component => $_[1] };
+ },
+ },
+ },
+ # just product
+ qr{^/review/suggestions/([^/]+)$}, {
+ GET => {
+ method => 'suggestions',
+ params => sub {
+ return { product => $_[0] };
+ },
+ },
+ },
+ # named parameters
+ qr{^/review/suggestions$}, {
+ GET => {
+ method => 'suggestions',
+ },
+ },
+ # flag activity by flag id
+ qr{^/review/flag_activity/(\d+)$}, {
+ GET => {
+ method => 'flag_activity',
+ params => sub {
+ return { flag_id => $_[0] }
+ },
+ },
+ },
+ qr{^/review/flag_activity/type_name/(\w+)$}, {
+ GET => {
+ method => 'flag_activity',
+ params => sub {
+ return { type_name => $_[0] }
+ },
+ },
+ },
+ # flag activity by user
+ qr{^/review/flag_activity/(requestee|setter|type_id)/(.*)$}, {
+ GET => {
+ method => 'flag_activity',
+ params => sub {
+ return { $_[0] => $_[1] };
+ },
+ },
+ },
+ # flag activity with only query strings
+ qr{^/review/flag_activity$}, {
+ GET => { method => 'flag_activity' },
+ },
+ ];
+}
+
+sub _flag_state_activity_to_hash {
+ my ($self, $fsa, $params) = @_;
+
+ my %flag = (
+ id => $self->type('int', $fsa->id),
+ creation_time => $self->type('string', $fsa->flag_when),
+ type => $self->_flagtype_to_hash($fsa->type),
+ setter => $self->_user_to_hash($fsa->setter),
+ bug_id => $self->type('int', $fsa->bug_id),
+ attachment_id => $self->type('int', $fsa->attachment_id),
+ status => $self->type('string', $fsa->status),
+ );
+
+ $flag{requestee} = $self->_user_to_hash($fsa->requestee) if $fsa->requestee;
+ $flag{flag_id} = $self->type('int', $fsa->flag_id) unless $params->{flag_id};
+
+ return filter($params, \%flag);
+}
+
+sub _flagtype_to_hash {
+ my ($self, $flagtype) = @_;
+ my $user = Bugzilla->user;
+
+ return {
+ id => $self->type('int', $flagtype->id),
+ name => $self->type('string', $flagtype->name),
+ description => $self->type('string', $flagtype->description),
+ type => $self->type('string', $flagtype->target_type),
+ is_active => $self->type('boolean', $flagtype->is_active),
+ is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble),
+ is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable),
+ };
+}
+
+sub _user_to_hash {
+ my ($self, $user) = @_;
+
+ return {
+ id => $self->type('int', $user->id),
+ real_name => $self->type('string', $user->name),
+ name => $self->type('email', $user->login),
+ };
+}
+
+1;
+__END__
+=head1 NAME
+
+Bugzilla::Extension::Review::WebService - Functions for the Mozilla specific
+'review' flag optimisations.
+
+=head1 METHODS
+
+See L<Bugzilla::WebService> for a description of how parameters are passed,
+and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
+
+Although the data input and output is the same for JSONRPC, XMLRPC and REST,
+the directions for how to access the data via REST is noted in each method
+where applicable.
+
+=head2 suggestions
+
+B<EXPERIMENTAL>
+
+=over
+
+=item B<Description>
+
+Returns the list of suggestions for reviewers.
+
+=item B<REST>
+
+GET /rest/review/suggestions/C<bug-id>
+
+GET /rest/review/suggestions/C<product-name>
+
+GET /rest/review/suggestions/C<product-name>/C<component-name>
+
+GET /rest/review/suggestions?product=C<product-name>
+
+GET /rest/review/suggestions?product=C<product-name>&component=C<component-name>
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+Query by Bug:
+
+=over
+
+=over
+
+=item C<bug_id> (integer) - The bug ID.
+
+=back
+
+=back
+
+Query by Product or Component:
+
+=over
+
+=over
+
+=item C<product> (string) - The product name.
+
+=item C<component> (string) - The component name (optional). If providing a C<component>, a C<product> must also be provided.
+
+=back
+
+=back
+
+=item B<Returns>
+
+An array of hashes with the following keys/values:
+
+=over
+
+=item C<id> (integer) - The user's ID.
+
+=item C<email> (string) - The user's email address (aka login).
+
+=item C<name> (string) - The user's display name (may not match the Bugzilla "real name").
+
+=item C<review_count> (string) - The number of "review" and "feedback" requests in the user's queue.
+
+=back
+
+=back
+
+=head2 flag_activity
+
+B<EXPERIMENTAL>
+
+=over
+
+=item B<Description>
+
+Returns the history of flag status changes based on requestee, setter, flag_id, type_id, or all.
+
+=item B<REST>
+
+GET /rest/review/flag_activity/C<flag_id>
+
+GET /rest/review/flag_activity/requestee/C<requestee>
+
+GET /rest/review/flag_activity/setter/C<setter>
+
+GET /rest/review/flag_activity/type_id/C<type_id>
+
+GET /rest/review/flag_activity/type_name/C<type_name>
+
+GET /rest/review/flag_activity
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+Use one or more of the following parameters to find specific flag status changes.
+
+=over
+
+=item C<flag_id> (integer) - The flag ID.
+
+Note that searching by C<flag_id> is not reliable because when flags are removed, flag_ids cease to exist.
+
+=item C<requestee> (string) - The bugzilla login of the flag's requestee
+
+=item C<setter> (string) - The bugzilla login of the flag's setter
+
+=item C<type_id> (int) - The flag type id of a change
+
+=item C<type_name> (string) - the flag type name of a change
+
+=back
+
+=item B<Returns>
+
+An array of hashes with the following keys/values:
+
+=over
+
+=item C<flag_id> (integer)
+
+The id of the flag that changed. This field may be absent after a flag is deleted.
+
+=item C<creation_time> (dateTime)
+
+Timestamp of when the flag status changed.
+
+=item C<type> (object)
+
+An object with the following fields:
+
+=over
+
+=item C<id> (integer)
+
+The flag type id of the flag that changed
+
+=item C<name> (string)
+
+The name of the flag type (review, feedback, etc)
+
+=item C<description> (string)
+
+A plain english description of the flag type.
+
+=item C<type> (string)
+
+The content of the target_type field of the flagtypes table.
+
+=item C<is_active> (boolean)
+
+Boolean flag indicating if the flag type is available for use.
+
+=item C<is_requesteeble> (boolean)
+
+Boolean flag indicating if the flag type is requesteeable.
+
+=item C<is_multiplicable> (boolean)
+
+Boolean flag indicating if the flag type is multiplicable.
+
+=back
+
+=item C<setter> (object)
+
+The setter is the bugzilla user that set the flag. It is represented by an object with the following fields.
+
+=over
+
+=item C<id> (integer)
+
+The id of the bugzilla user. A unique integer value.
+
+=item C<real_name> (string)
+
+The real name of the bugzilla user.
+
+=item C<name> (string)
+
+The bugzilla login of the bugzilla user (typically an email address).
+
+=back
+
+=item C<requestee> (object)
+
+The requestee is the bugzilla user that is specified by the flag. Optional - absent if there is no requestee.
+
+Requestee has the same keys/values as the setter object.
+
+=item C<bug_id> (integer)
+
+The id of the bugzilla bug that the changed flag belongs to.
+
+=item C<attachment_id> (integer)
+
+The id of the bugzilla attachment that the changed flag belongs to.
+
+=item C<status> (string)
+
+The status of the bugzilla flag that changed. One of C<+ - ? X>.
+
+=back
+
+=back
diff --git a/extensions/Review/template/en/default/hook/admin/components/edit-common-rows.html.tmpl b/extensions/Review/template/en/default/hook/admin/components/edit-common-rows.html.tmpl
new file mode 100644
index 000000000..42aa91ada
--- /dev/null
+++ b/extensions/Review/template/en/default/hook/admin/components/edit-common-rows.html.tmpl
@@ -0,0 +1,23 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<tr>
+ <th align="right">Suggested Reviewers:</th>
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "reviewers"
+ name => "reviewers"
+ value => comp.reviewers(1)
+ size => 64
+ emptyok => 1
+ title => "One or more email address (comma delimited)"
+ placeholder => product.reviewers(1)
+ multiple => 5
+ %]
+ </td>
+</tr>
diff --git a/extensions/Review/template/en/default/hook/admin/products/edit-common-rows.html.tmpl b/extensions/Review/template/en/default/hook/admin/products/edit-common-rows.html.tmpl
new file mode 100644
index 000000000..61a275e72
--- /dev/null
+++ b/extensions/Review/template/en/default/hook/admin/products/edit-common-rows.html.tmpl
@@ -0,0 +1,28 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<tr>
+ <th align="right">Reviewer required:</th>
+ <td>
+ <input type="checkbox" name="reviewer_required" value="1"
+ [% "checked" IF product.reviewer_required %]>
+ </td>
+</tr>
+<tr>
+ <th align="right">Suggested Reviewers:</th>
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "reviewers"
+ name => "reviewers"
+ value => product.reviewers(1)
+ size => 64
+ emptyok => 1
+ title => "One or more email address (comma delimited)"
+ %]
+ </td>
+</tr>
diff --git a/extensions/Review/template/en/default/hook/admin/products/updated-changes.html.tmpl b/extensions/Review/template/en/default/hook/admin/products/updated-changes.html.tmpl
new file mode 100644
index 000000000..667848281
--- /dev/null
+++ b/extensions/Review/template/en/default/hook/admin/products/updated-changes.html.tmpl
@@ -0,0 +1,19 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF changes.reviewers.defined %]
+ <p>
+ Updated suggested reviewers from '[% changes.reviewers.0 FILTER html %]' to
+ '[% product.reviewers FILTER html %]'.
+ </p>
+[% END %]
+[% IF changes.reviewer_required.defined %]
+ <p>
+ [% changes.reviewer_required.1 ? "Enabled" : "Disabled" %] 'review required'.
+ </p>
+[% END %]
diff --git a/extensions/Review/template/en/default/hook/attachment/create-end.html.tmpl b/extensions/Review/template/en/default/hook/attachment/create-end.html.tmpl
new file mode 100644
index 000000000..55226545d
--- /dev/null
+++ b/extensions/Review/template/en/default/hook/attachment/create-end.html.tmpl
@@ -0,0 +1,20 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% UNLESS bug %]
+ [% bug = attachment.bug %]
+[% END %]
+
+<script>
+ YAHOO.util.Event.onDOMReady(function() {
+ [% IF bug.product_obj.reviewer_required %]
+ REVIEW.init_mandatory();
+ [% END %]
+ REVIEW.init_create_attachment();
+ });
+</script>
diff --git a/extensions/Review/template/en/default/hook/attachment/edit-end.html.tmpl b/extensions/Review/template/en/default/hook/attachment/edit-end.html.tmpl
new file mode 100644
index 000000000..bc6230d1c
--- /dev/null
+++ b/extensions/Review/template/en/default/hook/attachment/edit-end.html.tmpl
@@ -0,0 +1,15 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF attachment.bug.product_obj.reviewer_required %]
+<script>
+YAHOO.util.Event.onDOMReady(function() {
+ REVIEW.init_mandatory();
+});
+</script>
+[% END %]
diff --git a/extensions/Review/template/en/default/hook/bug/create/create-after_custom_fields.html.tmpl b/extensions/Review/template/en/default/hook/bug/create/create-after_custom_fields.html.tmpl
new file mode 100644
index 000000000..4a8f05755
--- /dev/null
+++ b/extensions/Review/template/en/default/hook/bug/create/create-after_custom_fields.html.tmpl
@@ -0,0 +1,20 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<tr>
+ <th class="field_label">Mentors:</th>
+ <td colspan="3" class="field_value">
+ [% INCLUDE global/userselect.html.tmpl
+ id = "bug_mentors"
+ name = "bug_mentors"
+ size = 30
+ multiple = 5
+ value = bug_mentors
+ %]
+ </td>
+</tr>
diff --git a/extensions/Review/template/en/default/hook/bug/create/create-end.html.tmpl b/extensions/Review/template/en/default/hook/bug/create/create-end.html.tmpl
new file mode 100644
index 000000000..a59cef950
--- /dev/null
+++ b/extensions/Review/template/en/default/hook/bug/create/create-end.html.tmpl
@@ -0,0 +1,16 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<script>
+ YAHOO.util.Event.onDOMReady(function() {
+ [% IF product.reviewer_required %]
+ REVIEW.init_mandatory();
+ [% END %]
+ REVIEW.init_enter_bug();
+ });
+</script>
diff --git a/extensions/Review/template/en/default/hook/bug/edit-after_people.html.tmpl b/extensions/Review/template/en/default/hook/bug/edit-after_people.html.tmpl
new file mode 100644
index 000000000..5f8ea8fa9
--- /dev/null
+++ b/extensions/Review/template/en/default/hook/bug/edit-after_people.html.tmpl
@@ -0,0 +1,53 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% mentor_logins = [] %]
+[% FOREACH mentor = bug.mentors %]
+ [% mentor_logins.push(mentor.login) %]
+[% END %]
+<tr>
+ <th class="field_label">Mentors:</th>
+ <td>
+ [% IF bug.check_can_change_field("bug_mentors", 0, 1) %]
+ <div id="bz_bug_mentors_edit_container" class="bz_default_hidden">
+ <span>
+ [% FOREACH mentor = bug.mentors %]
+ [% INCLUDE global/user.html.tmpl who = mentor %]
+ [% "<br>" UNLESS loop.last %]
+ [% END %]
+ (<a href="#" id="bz_bug_mentors_edit_action">edit</a>)
+ </span>
+ </div>
+ <div id="bz_bug_mentors_input">
+ <input type="hidden" name="defined_bug_mentors"
+ value="[% mentor_logins.join(", ") FILTER html %]">
+ [% INCLUDE global/userselect.html.tmpl
+ id = "bug_mentors"
+ name = "bug_mentors"
+ value = mentor_logins.join(", ")
+ classes = ["bz_userfield"]
+ size = 30
+ multiple = 5
+ %]
+ <br>
+ </div>
+ <script type="text/javascript">
+ hideEditableField('bz_bug_mentors_edit_container',
+ 'bz_bug_mentors_input',
+ 'bz_bug_mentors_edit_action',
+ 'bug_mentors',
+ '[% mentor_logins.join(", ") FILTER js %]' );
+ </script>
+ [% ELSE %]
+ [% FOREACH mentor = bug.mentors %]
+ [% INCLUDE global/user.html.tmpl who = mentor %]<br>
+ [% END %]
+ [% END %]
+ </td>
+</tr>
+
diff --git a/extensions/Review/template/en/default/hook/bug/show-bug_end.xml.tmpl b/extensions/Review/template/en/default/hook/bug/show-bug_end.xml.tmpl
new file mode 100644
index 000000000..9ad650b2f
--- /dev/null
+++ b/extensions/Review/template/en/default/hook/bug/show-bug_end.xml.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% FOREACH mentor = bug.mentors %]
+ <mentor name="[% mentor.name FILTER xml %]">
+ [% mentor.login FILTER email FILTER xml %]</mentor>
+[% END %]
diff --git a/extensions/Review/template/en/default/hook/flag/list-requestee.html.tmpl b/extensions/Review/template/en/default/hook/flag/list-requestee.html.tmpl
new file mode 100644
index 000000000..a3f0e8a44
--- /dev/null
+++ b/extensions/Review/template/en/default/hook/flag/list-requestee.html.tmpl
@@ -0,0 +1,17 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS type.name == 'review' %]
+
+<span id="[% fid FILTER none %]_suggestions" class="bz_default_hidden">
+ &nbsp;&nbsp;(<a href="#" id="[% fid FILTER none %]_suggestions_link">suggested reviewers</a>)
+</span>
+
+<script>
+ REVIEW.init_review_flag('[% fid FILTER none %]', '[% flag_name FILTER none %]');
+</script>
diff --git a/extensions/Review/template/en/default/hook/global/header-message.html.tmpl b/extensions/Review/template/en/default/hook/global/header-message.html.tmpl
new file mode 100644
index 000000000..e4bb1c687
--- /dev/null
+++ b/extensions/Review/template/en/default/hook/global/header-message.html.tmpl
@@ -0,0 +1,23 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS
+ user.review_request_count
+ || user.feedback_request_count
+ || user.needinfo_request_count
+%]
+
+<a id="badge"
+ href="request.cgi?action=queue&amp;requestee=[% user.login FILTER uri %]&amp;group=type"
+ title="Flags requested of you:
+ [%- " review (" _ user.review_request_count _ ")" IF user.review_request_count -%]
+ [%- " feedback (" _ user.feedback_request_count _ ")" IF user.feedback_request_count -%]
+ [%- " needinfo (" _ user.needinfo_request_count _ ")" IF user.needinfo_request_count -%]
+">
+ [%- user.review_request_count + user.feedback_request_count + user.needinfo_request_count ~%]
+</a>
diff --git a/extensions/Review/template/en/default/hook/global/header-start.html.tmpl b/extensions/Review/template/en/default/hook/global/header-start.html.tmpl
new file mode 100644
index 000000000..ff166ac4c
--- /dev/null
+++ b/extensions/Review/template/en/default/hook/global/header-start.html.tmpl
@@ -0,0 +1,91 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF user.review_request_count
+ || user.feedback_request_count
+ || user.needinfo_request_count
+%]
+ [% style_urls.push('extensions/Review/web/styles/badge.css') %]
+[% END %]
+
+[% RETURN UNLESS template.name == 'attachment/edit.html.tmpl'
+ || template.name == 'attachment/create.html.tmpl'
+ || template.name == 'attachment/diff-header.html.tmpl'
+ || template.name == 'bug/create/create.html.tmpl'
+ || template.name == 'pages/splinter.html.tmpl' %]
+
+[% style_urls.push('extensions/Review/web/styles/review.css') %]
+[% javascript_urls.push('extensions/Review/web/js/review.js') %]
+
+[% IF bug %]
+ [%# create attachment %]
+ [% mentors = bug.mentors %]
+ [% product_obj = bug.product_obj %]
+ [% component_obj = bug.component_obj %]
+[% ELSIF attachment.bug %]
+ [%# edit attachment %]
+ [% mentors = attachment.bug.mentors %]
+ [% product_obj = attachment.bug.product_obj %]
+ [% component_obj = attachment.bug.component_obj %]
+[% ELSE %]
+ [%# create bug %]
+ [% mentors = [] %]
+ [% product_obj = product %]
+ [% component_obj = 0 %]
+[% END %]
+
+[% review_js = BLOCK %]
+ review_suggestions = {
+ _mentors: [
+ [% FOREACH u = mentors %]
+ [% PROCESS reviewer %][% "," UNLESS loop.last %]
+ [% END %]
+ ],
+
+ [% IF product_obj.reviewers %]
+ _product: [
+ [% FOREACH u = product_obj.reviewers_objs %]
+ [% PROCESS reviewer %][% "," UNLESS loop.last %]
+ [% END %]
+ ],
+ [% END %]
+
+ [% IF component_obj %]
+ [%# single component (create/edit attachment) %]
+ '[% component_obj.name FILTER js %]': [
+ [% FOREACH u = component_obj.reviewers_objs %]
+ [% PROCESS reviewer %][% "," UNLESS loop.last %]
+ [% END %]
+ ],
+ [% ELSE %]
+ [%# all components (create bug) %]
+ [% FOREACH c = product_obj.components %]
+ [% NEXT UNLESS c.reviewers %]
+ '[% c.name FILTER js %]': [
+ [% FOREACH u = c.reviewers_objs %]
+ [% PROCESS reviewer %][% "," UNLESS loop.last %]
+ [% END %]
+ ],
+ [% END %]
+ [% END %]
+
+ [%# to keep IE happy, no trailing commas %]
+ _end: 1
+ };
+
+ [% IF component_obj %]
+ static_component = '[% component_obj.name FILTER js %]';
+ [% ELSE %]
+ static_component = false;
+ [% END %]
+[% END %]
+[% javascript = javascript _ review_js %]
+
+[% BLOCK reviewer %]
+ { login: '[% u.login FILTER js%]', identity: '[% u.identity FILTER js %]', review_count: [% u.review_count FILTER js %] }
+[% END %]
diff --git a/extensions/Review/template/en/default/hook/global/messages-component_updated_fields.html.tmpl b/extensions/Review/template/en/default/hook/global/messages-component_updated_fields.html.tmpl
new file mode 100644
index 000000000..05b7bde82
--- /dev/null
+++ b/extensions/Review/template/en/default/hook/global/messages-component_updated_fields.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF changes.reviewers.defined %]
+ <li>Suggested Reviewers changed to '[% comp.reviewers.join(", ") FILTER html %]'</li>
+[% END %]
diff --git a/extensions/Review/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl b/extensions/Review/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
new file mode 100644
index 000000000..156f0aa93
--- /dev/null
+++ b/extensions/Review/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF object == 'review_requests_rebuild' %]
+ rebuild review request counters
+[% END %]
diff --git a/extensions/Review/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/Review/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..ca143cca3
--- /dev/null
+++ b/extensions/Review/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,26 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "reviewer_required" %]
+ [% title = "Reviewer Required" %]
+ You must provide a reviewer for review requests.
+
+[% ELSIF error == "reviewer_suggestions_param_required" %]
+ [% title = "Parameter Required" %]
+ You must provide either a bug_id, or a product (and optionally a
+ component).
+
+[% ELSIF error == "invalid_flag_type_id" %]
+ [% title = "Invalid Flag Type ID" %]
+ The flag type id [% type_id FILTER html %] is invalid.
+
+[% ELSIF error == "flag_activity_parameters_required" %]
+ [% title = "Parameters Required" %]
+ You may not search flag state activity without any search terms.
+
+[% END %]
diff --git a/extensions/Review/template/en/default/hook/reports/menu-end.html.tmpl b/extensions/Review/template/en/default/hook/reports/menu-end.html.tmpl
new file mode 100644
index 000000000..d25ba20ee
--- /dev/null
+++ b/extensions/Review/template/en/default/hook/reports/menu-end.html.tmpl
@@ -0,0 +1,16 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<ul>
+ <li>
+ <strong>
+ <a href="[% urlbase FILTER none %]page.cgi?id=review_suggestions.html">Suggested Reviewers</a>
+ </strong> - All suggestions for the "review" flag.
+ </li>
+</ul>
+
diff --git a/extensions/Review/template/en/default/pages/review_history.html.tmpl b/extensions/Review/template/en/default/pages/review_history.html.tmpl
new file mode 100644
index 000000000..32ac83ceb
--- /dev/null
+++ b/extensions/Review/template/en/default/pages/review_history.html.tmpl
@@ -0,0 +1,62 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Review History"
+ style_urls = [ "extensions/Review/web/styles/review_history.css" ]
+ javascript_urls = [ 'js/yui3/yui/yui-min.js',
+ 'extensions/Review/web/js/review_history.js',
+ 'extensions/Review/web/js/moment.min.js',
+ 'js/util.js',
+ 'js/field.js' ]
+ yui = [ "autocomplete" ]
+%]
+
+<script type="text/javascript">
+ YUI({
+ base: 'js/yui3/',
+ combine: false,
+ groups: {
+ gallery: {
+ combine: false,
+ base: 'js/yui3/',
+ patterns: { 'gallery-': {} }
+ }
+ }
+ }).use('bz-review-history', function(Y) {
+ Y.ReviewHistory.render('#history', '#history-loading');
+ var requestee = Y.one('#requestee');
+
+ Y.ReviewHistory.refresh('[% requestee.login FILTER js %]', '[% requestee.name FILTER html FILTER js %]');
+ });
+</script>
+
+<div>
+ <form method="get">
+ <label class="field_label" for="user">Requestee </label>
+
+ [% INCLUDE global/userselect.html.tmpl
+ id => "requestee"
+ name => "requestee"
+ value => requestee.login
+ classes => ["bz_userfield"]
+ %]
+
+ <input type="submit" value="Generate Report">
+ <input type="hidden" name="id" value="review_history.html">
+ </form>
+</div>
+
+<div class="yui3-skin-sam">
+ <div id="history-loading">Loading...</div>
+ <div id="history"></div>
+</div>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/Review/template/en/default/pages/review_requests_rebuild.html.tmpl b/extensions/Review/template/en/default/pages/review_requests_rebuild.html.tmpl
new file mode 100644
index 000000000..5ec811126
--- /dev/null
+++ b/extensions/Review/template/en/default/pages/review_requests_rebuild.html.tmpl
@@ -0,0 +1,23 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE global/header.html.tmpl
+ title = "Review Requests Rebuild"
+%]
+
+[% IF rebuild %]
+ Counters rebuilt for [% total FILTER html %] users.
+[% ELSE %]
+ <form method="post">
+ <input type="hidden" name="id" value="review_requests_rebuild.html">
+ <input type="hidden" name="rebuild" value="1">
+ <input type="submit" value="Rebuild Review Request Counters">
+ </form>
+[% END %]
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/Review/template/en/default/pages/review_suggestions.html.tmpl b/extensions/Review/template/en/default/pages/review_suggestions.html.tmpl
new file mode 100644
index 000000000..5d9132e40
--- /dev/null
+++ b/extensions/Review/template/en/default/pages/review_suggestions.html.tmpl
@@ -0,0 +1,76 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE global/header.html.tmpl
+ title = "Suggested Reviewers Report"
+ style_urls = [ "extensions/BMO/web/styles/reports.css",
+ "extensions/Review/web/styles/reports.css" ]
+%]
+
+Products:
+<ul>
+ [% FOREACH product = products %]
+ <li>
+ <a href="#[% product.name FILTER uri %]">
+ [% product.name FILTER html %]
+ </a>
+ </li>
+ [% END %]
+</ul>
+
+<a href="enter_bug.cgi?product=bugzilla.mozilla.org&amp;component=Administration&amp;format=__default__">Request a change</a>
+
+<table id="report" class="hover" cellspacing="0">
+
+<tr id="report-header">
+ <th>Product/Component</th>
+ <th>Suggested Reviewers</th>
+</tr>
+
+[% FOREACH product = products %]
+ <tr class="report_subheader">
+ <td class="product_name">
+ <a name="[% product.name FILTER html %]">
+ [% product.name FILTER html %]
+ </a>
+ </td>
+ <td>
+ </td>
+ </tr>
+ [% row_class = "report_row_even" %]
+ [% FOREACH component = product.components %]
+ <tr class="[% row_class FILTER none %]">
+ <td class="component_name">[% component.name FILTER html %]</td>
+ <td class="reviewers">
+ [% FOREACH reviewer = component.reviewers %]
+ <span title="[% reviewer.name FILTER html %]">
+ [% reviewer.email FILTER html %]</span>
+ [% ", " UNLESS loop.last %]
+ [% END %]
+ </td>
+ </tr>
+ [% row_class = row_class == "report_row_even" ? "report_row_odd" : "report_row_even" %]
+ [% END %]
+ [% IF product.reviewers.size %]
+ <tr class="[% row_class FILTER none %]">
+ <td class="other_components">All [% product.components.size ? "other" : "" %] components</td>
+ <td class="reviewers">
+ [% FOREACH reviewer = product.reviewers %]
+ <span title="[% reviewer.name FILTER html %]">
+ [% reviewer.email FILTER html %]</span>
+ [% ", " UNLESS loop.last %]
+ [% END %]
+ </td>
+ </tr>
+ [% row_class = row_class == "report_row_even" ? "report_row_odd" : "report_row_even" %]
+ [% END %]
+[% END %]
+
+</table>
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/Review/web/js/moment.min.js b/extensions/Review/web/js/moment.min.js
new file mode 100644
index 000000000..26ac5cc9d
--- /dev/null
+++ b/extensions/Review/web/js/moment.min.js
@@ -0,0 +1,6 @@
+//! moment.js
+//! version : 2.8.2
+//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
+//! license : MIT
+//! momentjs.com
+(function(a){function b(a,b,c){switch(arguments.length){case 2:return null!=a?a:b;case 3:return null!=a?a:null!=b?b:c;default:throw new Error("Implement me")}}function c(a,b){return yb.call(a,b)}function d(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function e(a){sb.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+a)}function f(a,b){var c=!0;return m(function(){return c&&(e(a),c=!1),b.apply(this,arguments)},b)}function g(a,b){pc[a]||(e(b),pc[a]=!0)}function h(a,b){return function(c){return p(a.call(this,c),b)}}function i(a,b){return function(c){return this.localeData().ordinal(a.call(this,c),b)}}function j(){}function k(a,b){b!==!1&&F(a),n(this,a),this._d=new Date(+a._d)}function l(a){var b=y(a),c=b.year||0,d=b.quarter||0,e=b.month||0,f=b.week||0,g=b.day||0,h=b.hour||0,i=b.minute||0,j=b.second||0,k=b.millisecond||0;this._milliseconds=+k+1e3*j+6e4*i+36e5*h,this._days=+g+7*f,this._months=+e+3*d+12*c,this._data={},this._locale=sb.localeData(),this._bubble()}function m(a,b){for(var d in b)c(b,d)&&(a[d]=b[d]);return c(b,"toString")&&(a.toString=b.toString),c(b,"valueOf")&&(a.valueOf=b.valueOf),a}function n(a,b){var c,d,e;if("undefined"!=typeof b._isAMomentObject&&(a._isAMomentObject=b._isAMomentObject),"undefined"!=typeof b._i&&(a._i=b._i),"undefined"!=typeof b._f&&(a._f=b._f),"undefined"!=typeof b._l&&(a._l=b._l),"undefined"!=typeof b._strict&&(a._strict=b._strict),"undefined"!=typeof b._tzm&&(a._tzm=b._tzm),"undefined"!=typeof b._isUTC&&(a._isUTC=b._isUTC),"undefined"!=typeof b._offset&&(a._offset=b._offset),"undefined"!=typeof b._pf&&(a._pf=b._pf),"undefined"!=typeof b._locale&&(a._locale=b._locale),Hb.length>0)for(c in Hb)d=Hb[c],e=b[d],"undefined"!=typeof e&&(a[d]=e);return a}function o(a){return 0>a?Math.ceil(a):Math.floor(a)}function p(a,b,c){for(var d=""+Math.abs(a),e=a>=0;d.length<b;)d="0"+d;return(e?c?"+":"":"-")+d}function q(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function r(a,b){var c;return b=K(b,a),a.isBefore(b)?c=q(a,b):(c=q(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c}function s(a,b){return function(c,d){var e,f;return null===d||isNaN(+d)||(g(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period)."),f=c,c=d,d=f),c="string"==typeof c?+c:c,e=sb.duration(c,d),t(this,e,a),this}}function t(a,b,c,d){var e=b._milliseconds,f=b._days,g=b._months;d=null==d?!0:d,e&&a._d.setTime(+a._d+e*c),f&&mb(a,"Date",lb(a,"Date")+f*c),g&&kb(a,lb(a,"Month")+g*c),d&&sb.updateOffset(a,f||g)}function u(a){return"[object Array]"===Object.prototype.toString.call(a)}function v(a){return"[object Date]"===Object.prototype.toString.call(a)||a instanceof Date}function w(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;e>d;d++)(c&&a[d]!==b[d]||!c&&A(a[d])!==A(b[d]))&&g++;return g+f}function x(a){if(a){var b=a.toLowerCase().replace(/(.)s$/,"$1");a=ic[a]||jc[b]||b}return a}function y(a){var b,d,e={};for(d in a)c(a,d)&&(b=x(d),b&&(e[b]=a[d]));return e}function z(b){var c,d;if(0===b.indexOf("week"))c=7,d="day";else{if(0!==b.indexOf("month"))return;c=12,d="month"}sb[b]=function(e,f){var g,h,i=sb._locale[b],j=[];if("number"==typeof e&&(f=e,e=a),h=function(a){var b=sb().utc().set(d,a);return i.call(sb._locale,b,e||"")},null!=f)return h(f);for(g=0;c>g;g++)j.push(h(g));return j}}function A(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=b>=0?Math.floor(b):Math.ceil(b)),c}function B(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function C(a,b,c){return gb(sb([a,11,31+b-c]),b,c).week}function D(a){return E(a)?366:365}function E(a){return a%4===0&&a%100!==0||a%400===0}function F(a){var b;a._a&&-2===a._pf.overflow&&(b=a._a[Ab]<0||a._a[Ab]>11?Ab:a._a[Bb]<1||a._a[Bb]>B(a._a[zb],a._a[Ab])?Bb:a._a[Cb]<0||a._a[Cb]>23?Cb:a._a[Db]<0||a._a[Db]>59?Db:a._a[Eb]<0||a._a[Eb]>59?Eb:a._a[Fb]<0||a._a[Fb]>999?Fb:-1,a._pf._overflowDayOfYear&&(zb>b||b>Bb)&&(b=Bb),a._pf.overflow=b)}function G(a){return null==a._isValid&&(a._isValid=!isNaN(a._d.getTime())&&a._pf.overflow<0&&!a._pf.empty&&!a._pf.invalidMonth&&!a._pf.nullInput&&!a._pf.invalidFormat&&!a._pf.userInvalidated,a._strict&&(a._isValid=a._isValid&&0===a._pf.charsLeftOver&&0===a._pf.unusedTokens.length)),a._isValid}function H(a){return a?a.toLowerCase().replace("_","-"):a}function I(a){for(var b,c,d,e,f=0;f<a.length;){for(e=H(a[f]).split("-"),b=e.length,c=H(a[f+1]),c=c?c.split("-"):null;b>0;){if(d=J(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&w(e,c,!0)>=b-1)break;b--}f++}return null}function J(a){var b=null;if(!Gb[a]&&Ib)try{b=sb.locale(),require("./locale/"+a),sb.locale(b)}catch(c){}return Gb[a]}function K(a,b){return b._isUTC?sb(a).zone(b._offset||0):sb(a).local()}function L(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function M(a){var b,c,d=a.match(Mb);for(b=0,c=d.length;c>b;b++)d[b]=oc[d[b]]?oc[d[b]]:L(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function N(a,b){return a.isValid()?(b=O(b,a.localeData()),kc[b]||(kc[b]=M(b)),kc[b](a)):a.localeData().invalidDate()}function O(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(Nb.lastIndex=0;d>=0&&Nb.test(a);)a=a.replace(Nb,c),Nb.lastIndex=0,d-=1;return a}function P(a,b){var c,d=b._strict;switch(a){case"Q":return Yb;case"DDDD":return $b;case"YYYY":case"GGGG":case"gggg":return d?_b:Qb;case"Y":case"G":case"g":return bc;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return d?ac:Rb;case"S":if(d)return Yb;case"SS":if(d)return Zb;case"SSS":if(d)return $b;case"DDD":return Pb;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Tb;case"a":case"A":return b._locale._meridiemParse;case"X":return Wb;case"Z":case"ZZ":return Ub;case"T":return Vb;case"SSSS":return Sb;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return d?Zb:Ob;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return Ob;case"Do":return Xb;default:return c=new RegExp(Y(X(a.replace("\\","")),"i"))}}function Q(a){a=a||"";var b=a.match(Ub)||[],c=b[b.length-1]||[],d=(c+"").match(gc)||["-",0,0],e=+(60*d[1])+A(d[2]);return"+"===d[0]?-e:e}function R(a,b,c){var d,e=c._a;switch(a){case"Q":null!=b&&(e[Ab]=3*(A(b)-1));break;case"M":case"MM":null!=b&&(e[Ab]=A(b)-1);break;case"MMM":case"MMMM":d=c._locale.monthsParse(b),null!=d?e[Ab]=d:c._pf.invalidMonth=b;break;case"D":case"DD":null!=b&&(e[Bb]=A(b));break;case"Do":null!=b&&(e[Bb]=A(parseInt(b,10)));break;case"DDD":case"DDDD":null!=b&&(c._dayOfYear=A(b));break;case"YY":e[zb]=sb.parseTwoDigitYear(b);break;case"YYYY":case"YYYYY":case"YYYYYY":e[zb]=A(b);break;case"a":case"A":c._isPm=c._locale.isPM(b);break;case"H":case"HH":case"h":case"hh":e[Cb]=A(b);break;case"m":case"mm":e[Db]=A(b);break;case"s":case"ss":e[Eb]=A(b);break;case"S":case"SS":case"SSS":case"SSSS":e[Fb]=A(1e3*("0."+b));break;case"X":c._d=new Date(1e3*parseFloat(b));break;case"Z":case"ZZ":c._useUTC=!0,c._tzm=Q(b);break;case"dd":case"ddd":case"dddd":d=c._locale.weekdaysParse(b),null!=d?(c._w=c._w||{},c._w.d=d):c._pf.invalidWeekday=b;break;case"w":case"ww":case"W":case"WW":case"d":case"e":case"E":a=a.substr(0,1);case"gggg":case"GGGG":case"GGGGG":a=a.substr(0,2),b&&(c._w=c._w||{},c._w[a]=A(b));break;case"gg":case"GG":c._w=c._w||{},c._w[a]=sb.parseTwoDigitYear(b)}}function S(a){var c,d,e,f,g,h,i;c=a._w,null!=c.GG||null!=c.W||null!=c.E?(g=1,h=4,d=b(c.GG,a._a[zb],gb(sb(),1,4).year),e=b(c.W,1),f=b(c.E,1)):(g=a._locale._week.dow,h=a._locale._week.doy,d=b(c.gg,a._a[zb],gb(sb(),g,h).year),e=b(c.w,1),null!=c.d?(f=c.d,g>f&&++e):f=null!=c.e?c.e+g:g),i=hb(d,e,f,h,g),a._a[zb]=i.year,a._dayOfYear=i.dayOfYear}function T(a){var c,d,e,f,g=[];if(!a._d){for(e=V(a),a._w&&null==a._a[Bb]&&null==a._a[Ab]&&S(a),a._dayOfYear&&(f=b(a._a[zb],e[zb]),a._dayOfYear>D(f)&&(a._pf._overflowDayOfYear=!0),d=cb(f,0,a._dayOfYear),a._a[Ab]=d.getUTCMonth(),a._a[Bb]=d.getUTCDate()),c=0;3>c&&null==a._a[c];++c)a._a[c]=g[c]=e[c];for(;7>c;c++)a._a[c]=g[c]=null==a._a[c]?2===c?1:0:a._a[c];a._d=(a._useUTC?cb:bb).apply(null,g),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()+a._tzm)}}function U(a){var b;a._d||(b=y(a._i),a._a=[b.year,b.month,b.day,b.hour,b.minute,b.second,b.millisecond],T(a))}function V(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function W(a){if(a._f===sb.ISO_8601)return void $(a);a._a=[],a._pf.empty=!0;var b,c,d,e,f,g=""+a._i,h=g.length,i=0;for(d=O(a._f,a._locale).match(Mb)||[],b=0;b<d.length;b++)e=d[b],c=(g.match(P(e,a))||[])[0],c&&(f=g.substr(0,g.indexOf(c)),f.length>0&&a._pf.unusedInput.push(f),g=g.slice(g.indexOf(c)+c.length),i+=c.length),oc[e]?(c?a._pf.empty=!1:a._pf.unusedTokens.push(e),R(e,c,a)):a._strict&&!c&&a._pf.unusedTokens.push(e);a._pf.charsLeftOver=h-i,g.length>0&&a._pf.unusedInput.push(g),a._isPm&&a._a[Cb]<12&&(a._a[Cb]+=12),a._isPm===!1&&12===a._a[Cb]&&(a._a[Cb]=0),T(a),F(a)}function X(a){return a.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e})}function Y(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function Z(a){var b,c,e,f,g;if(0===a._f.length)return a._pf.invalidFormat=!0,void(a._d=new Date(0/0));for(f=0;f<a._f.length;f++)g=0,b=n({},a),b._pf=d(),b._f=a._f[f],W(b),G(b)&&(g+=b._pf.charsLeftOver,g+=10*b._pf.unusedTokens.length,b._pf.score=g,(null==e||e>g)&&(e=g,c=b));m(a,c||b)}function $(a){var b,c,d=a._i,e=cc.exec(d);if(e){for(a._pf.iso=!0,b=0,c=ec.length;c>b;b++)if(ec[b][1].exec(d)){a._f=ec[b][0]+(e[6]||" ");break}for(b=0,c=fc.length;c>b;b++)if(fc[b][1].exec(d)){a._f+=fc[b][0];break}d.match(Ub)&&(a._f+="Z"),W(a)}else a._isValid=!1}function _(a){$(a),a._isValid===!1&&(delete a._isValid,sb.createFromInputFallback(a))}function ab(b){var c,d=b._i;d===a?b._d=new Date:v(d)?b._d=new Date(+d):null!==(c=Jb.exec(d))?b._d=new Date(+c[1]):"string"==typeof d?_(b):u(d)?(b._a=d.slice(0),T(b)):"object"==typeof d?U(b):"number"==typeof d?b._d=new Date(d):sb.createFromInputFallback(b)}function bb(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 1970>a&&h.setFullYear(a),h}function cb(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function db(a,b){if("string"==typeof a)if(isNaN(a)){if(a=b.weekdaysParse(a),"number"!=typeof a)return null}else a=parseInt(a,10);return a}function eb(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function fb(a,b,c){var d=sb.duration(a).abs(),e=xb(d.as("s")),f=xb(d.as("m")),g=xb(d.as("h")),h=xb(d.as("d")),i=xb(d.as("M")),j=xb(d.as("y")),k=e<lc.s&&["s",e]||1===f&&["m"]||f<lc.m&&["mm",f]||1===g&&["h"]||g<lc.h&&["hh",g]||1===h&&["d"]||h<lc.d&&["dd",h]||1===i&&["M"]||i<lc.M&&["MM",i]||1===j&&["y"]||["yy",j];return k[2]=b,k[3]=+a>0,k[4]=c,eb.apply({},k)}function gb(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=sb(a).add(f,"d"),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function hb(a,b,c,d,e){var f,g,h=cb(a,0,1).getUTCDay();return h=0===h?7:h,c=null!=c?c:e,f=e-h+(h>d?7:0)-(e>h?7:0),g=7*(b-1)+(c-e)+f+1,{year:g>0?a:a-1,dayOfYear:g>0?g:D(a-1)+g}}function ib(b){var c=b._i,d=b._f;return b._locale=b._locale||sb.localeData(b._l),null===c||d===a&&""===c?sb.invalid({nullInput:!0}):("string"==typeof c&&(b._i=c=b._locale.preparse(c)),sb.isMoment(c)?new k(c,!0):(d?u(d)?Z(b):W(b):ab(b),new k(b)))}function jb(a,b){var c,d;if(1===b.length&&u(b[0])&&(b=b[0]),!b.length)return sb();for(c=b[0],d=1;d<b.length;++d)b[d][a](c)&&(c=b[d]);return c}function kb(a,b){var c;return"string"==typeof b&&(b=a.localeData().monthsParse(b),"number"!=typeof b)?a:(c=Math.min(a.date(),B(a.year(),b)),a._d["set"+(a._isUTC?"UTC":"")+"Month"](b,c),a)}function lb(a,b){return a._d["get"+(a._isUTC?"UTC":"")+b]()}function mb(a,b,c){return"Month"===b?kb(a,c):a._d["set"+(a._isUTC?"UTC":"")+b](c)}function nb(a,b){return function(c){return null!=c?(mb(this,a,c),sb.updateOffset(this,b),this):lb(this,a)}}function ob(a){return 400*a/146097}function pb(a){return 146097*a/400}function qb(a){sb.duration.fn[a]=function(){return this._data[a]}}function rb(a){"undefined"==typeof ender&&(tb=wb.moment,wb.moment=a?f("Accessing Moment through the global scope is deprecated, and will be removed in an upcoming release.",sb):sb)}for(var sb,tb,ub,vb="2.8.2",wb="undefined"!=typeof global?global:this,xb=Math.round,yb=Object.prototype.hasOwnProperty,zb=0,Ab=1,Bb=2,Cb=3,Db=4,Eb=5,Fb=6,Gb={},Hb=[],Ib="undefined"!=typeof module&&module.exports,Jb=/^\/?Date\((\-?\d+)/i,Kb=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,Lb=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,Mb=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,Nb=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,Ob=/\d\d?/,Pb=/\d{1,3}/,Qb=/\d{1,4}/,Rb=/[+\-]?\d{1,6}/,Sb=/\d+/,Tb=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Ub=/Z|[\+\-]\d\d:?\d\d/gi,Vb=/T/i,Wb=/[\+\-]?\d+(\.\d{1,3})?/,Xb=/\d{1,2}/,Yb=/\d/,Zb=/\d\d/,$b=/\d{3}/,_b=/\d{4}/,ac=/[+-]?\d{6}/,bc=/[+-]?\d+/,cc=/^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,dc="YYYY-MM-DDTHH:mm:ssZ",ec=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],fc=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],gc=/([\+\-]|\d\d)/gi,hc=("Date|Hours|Minutes|Seconds|Milliseconds".split("|"),{Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6}),ic={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",D:"date",w:"week",W:"isoWeek",M:"month",Q:"quarter",y:"year",DDD:"dayOfYear",e:"weekday",E:"isoWeekday",gg:"weekYear",GG:"isoWeekYear"},jc={dayofyear:"dayOfYear",isoweekday:"isoWeekday",isoweek:"isoWeek",weekyear:"weekYear",isoweekyear:"isoWeekYear"},kc={},lc={s:45,m:45,h:22,d:26,M:11},mc="DDD w W M D d".split(" "),nc="M D H h m s w W".split(" "),oc={M:function(){return this.month()+1},MMM:function(a){return this.localeData().monthsShort(this,a)},MMMM:function(a){return this.localeData().months(this,a)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(a){return this.localeData().weekdaysMin(this,a)},ddd:function(a){return this.localeData().weekdaysShort(this,a)},dddd:function(a){return this.localeData().weekdays(this,a)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return p(this.year()%100,2)},YYYY:function(){return p(this.year(),4)},YYYYY:function(){return p(this.year(),5)},YYYYYY:function(){var a=this.year(),b=a>=0?"+":"-";return b+p(Math.abs(a),6)},gg:function(){return p(this.weekYear()%100,2)},gggg:function(){return p(this.weekYear(),4)},ggggg:function(){return p(this.weekYear(),5)},GG:function(){return p(this.isoWeekYear()%100,2)},GGGG:function(){return p(this.isoWeekYear(),4)},GGGGG:function(){return p(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.localeData().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.localeData().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return A(this.milliseconds()/100)},SS:function(){return p(A(this.milliseconds()/10),2)},SSS:function(){return p(this.milliseconds(),3)},SSSS:function(){return p(this.milliseconds(),3)},Z:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+p(A(a/60),2)+":"+p(A(a)%60,2)},ZZ:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+p(A(a/60),2)+p(A(a)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()},Q:function(){return this.quarter()}},pc={},qc=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];mc.length;)ub=mc.pop(),oc[ub+"o"]=i(oc[ub],ub);for(;nc.length;)ub=nc.pop(),oc[ub+ub]=h(oc[ub],2);oc.DDDD=h(oc.DDD,3),m(j.prototype,{set:function(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(a){return this._months[a.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(a){return this._monthsShort[a.month()]},monthsParse:function(a){var b,c,d;for(this._monthsParse||(this._monthsParse=[]),b=0;12>b;b++)if(this._monthsParse[b]||(c=sb.utc([2e3,b]),d="^"+this.months(c,"")+"|^"+this.monthsShort(c,""),this._monthsParse[b]=new RegExp(d.replace(".",""),"i")),this._monthsParse[b].test(a))return b},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(a){return this._weekdays[a.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(a){return this._weekdaysShort[a.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(a){return this._weekdaysMin[a.day()]},weekdaysParse:function(a){var b,c,d;for(this._weekdaysParse||(this._weekdaysParse=[]),b=0;7>b;b++)if(this._weekdaysParse[b]||(c=sb([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY LT",LLLL:"dddd, MMMM D, YYYY LT"},longDateFormat:function(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b},isPM:function(a){return"p"===(a+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(a,b){var c=this._calendar[a];return"function"==typeof c?c.apply(b):c},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)},pastFuture:function(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)},ordinal:function(a){return this._ordinal.replace("%d",a)},_ordinal:"%d",preparse:function(a){return a},postformat:function(a){return a},week:function(a){return gb(a,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),sb=function(b,c,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._i=b,g._f=c,g._l=e,g._strict=f,g._isUTC=!1,g._pf=d(),ib(g)},sb.suppressDeprecationWarnings=!1,sb.createFromInputFallback=f("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(a){a._d=new Date(a._i)}),sb.min=function(){var a=[].slice.call(arguments,0);return jb("isBefore",a)},sb.max=function(){var a=[].slice.call(arguments,0);return jb("isAfter",a)},sb.utc=function(b,c,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._useUTC=!0,g._isUTC=!0,g._l=e,g._i=b,g._f=c,g._strict=f,g._pf=d(),ib(g).utc()},sb.unix=function(a){return sb(1e3*a)},sb.duration=function(a,b){var d,e,f,g,h=a,i=null;return sb.isDuration(a)?h={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(h={},b?h[b]=a:h.milliseconds=a):(i=Kb.exec(a))?(d="-"===i[1]?-1:1,h={y:0,d:A(i[Bb])*d,h:A(i[Cb])*d,m:A(i[Db])*d,s:A(i[Eb])*d,ms:A(i[Fb])*d}):(i=Lb.exec(a))?(d="-"===i[1]?-1:1,f=function(a){var b=a&&parseFloat(a.replace(",","."));return(isNaN(b)?0:b)*d},h={y:f(i[2]),M:f(i[3]),d:f(i[4]),h:f(i[5]),m:f(i[6]),s:f(i[7]),w:f(i[8])}):"object"==typeof h&&("from"in h||"to"in h)&&(g=r(sb(h.from),sb(h.to)),h={},h.ms=g.milliseconds,h.M=g.months),e=new l(h),sb.isDuration(a)&&c(a,"_locale")&&(e._locale=a._locale),e},sb.version=vb,sb.defaultFormat=dc,sb.ISO_8601=function(){},sb.momentProperties=Hb,sb.updateOffset=function(){},sb.relativeTimeThreshold=function(b,c){return lc[b]===a?!1:c===a?lc[b]:(lc[b]=c,!0)},sb.lang=f("moment.lang is deprecated. Use moment.locale instead.",function(a,b){return sb.locale(a,b)}),sb.locale=function(a,b){var c;return a&&(c="undefined"!=typeof b?sb.defineLocale(a,b):sb.localeData(a),c&&(sb.duration._locale=sb._locale=c)),sb._locale._abbr},sb.defineLocale=function(a,b){return null!==b?(b.abbr=a,Gb[a]||(Gb[a]=new j),Gb[a].set(b),sb.locale(a),Gb[a]):(delete Gb[a],null)},sb.langData=f("moment.langData is deprecated. Use moment.localeData instead.",function(a){return sb.localeData(a)}),sb.localeData=function(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return sb._locale;if(!u(a)){if(b=J(a))return b;a=[a]}return I(a)},sb.isMoment=function(a){return a instanceof k||null!=a&&c(a,"_isAMomentObject")},sb.isDuration=function(a){return a instanceof l};for(ub=qc.length-1;ub>=0;--ub)z(qc[ub]);sb.normalizeUnits=function(a){return x(a)},sb.invalid=function(a){var b=sb.utc(0/0);return null!=a?m(b._pf,a):b._pf.userInvalidated=!0,b},sb.parseZone=function(){return sb.apply(null,arguments).parseZone()},sb.parseTwoDigitYear=function(a){return A(a)+(A(a)>68?1900:2e3)},m(sb.fn=k.prototype,{clone:function(){return sb(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var a=sb(this).utc();return 0<a.year()&&a.year()<=9999?N(a,"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]"):N(a,"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var a=this;return[a.year(),a.month(),a.date(),a.hours(),a.minutes(),a.seconds(),a.milliseconds()]},isValid:function(){return G(this)},isDSTShifted:function(){return this._a?this.isValid()&&w(this._a,(this._isUTC?sb.utc(this._a):sb(this._a)).toArray())>0:!1},parsingFlags:function(){return m({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(a){return this.zone(0,a)},local:function(a){return this._isUTC&&(this.zone(0,a),this._isUTC=!1,a&&this.add(this._d.getTimezoneOffset(),"m")),this},format:function(a){var b=N(this,a||sb.defaultFormat);return this.localeData().postformat(b)},add:s(1,"add"),subtract:s(-1,"subtract"),diff:function(a,b,c){var d,e,f=K(a,this),g=6e4*(this.zone()-f.zone());return b=x(b),"year"===b||"month"===b?(d=432e5*(this.daysInMonth()+f.daysInMonth()),e=12*(this.year()-f.year())+(this.month()-f.month()),e+=(this-sb(this).startOf("month")-(f-sb(f).startOf("month")))/d,e-=6e4*(this.zone()-sb(this).startOf("month").zone()-(f.zone()-sb(f).startOf("month").zone()))/d,"year"===b&&(e/=12)):(d=this-f,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-g)/864e5:"week"===b?(d-g)/6048e5:d),c?e:o(e)},from:function(a,b){return sb.duration({to:this,from:a}).locale(this.locale()).humanize(!b)},fromNow:function(a){return this.from(sb(),a)},calendar:function(a){var b=a||sb(),c=K(b,this).startOf("day"),d=this.diff(c,"days",!0),e=-6>d?"sameElse":-1>d?"lastWeek":0>d?"lastDay":1>d?"sameDay":2>d?"nextDay":7>d?"nextWeek":"sameElse";return this.format(this.localeData().calendar(e,this))},isLeapYear:function(){return E(this.year())},isDST:function(){return this.zone()<this.clone().month(0).zone()||this.zone()<this.clone().month(5).zone()},day:function(a){var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=db(a,this.localeData()),this.add(a-b,"d")):b},month:nb("Month",!0),startOf:function(a){switch(a=x(a)){case"year":this.month(0);case"quarter":case"month":this.date(1);case"week":case"isoWeek":case"day":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return"week"===a?this.weekday(0):"isoWeek"===a&&this.isoWeekday(1),"quarter"===a&&this.month(3*Math.floor(this.month()/3)),this},endOf:function(a){return a=x(a),this.startOf(a).add(1,"isoWeek"===a?"week":a).subtract(1,"ms")},isAfter:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)>+sb(a).startOf(b)},isBefore:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)<+sb(a).startOf(b)},isSame:function(a,b){return b=b||"ms",+this.clone().startOf(b)===+K(a,this).startOf(b)},min:f("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(a){return a=sb.apply(null,arguments),this>a?this:a}),max:f("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(a){return a=sb.apply(null,arguments),a>this?this:a}),zone:function(a,b){var c,d=this._offset||0;return null==a?this._isUTC?d:this._d.getTimezoneOffset():("string"==typeof a&&(a=Q(a)),Math.abs(a)<16&&(a=60*a),!this._isUTC&&b&&(c=this._d.getTimezoneOffset()),this._offset=a,this._isUTC=!0,null!=c&&this.subtract(c,"m"),d!==a&&(!b||this._changeInProgress?t(this,sb.duration(d-a,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,sb.updateOffset(this,!0),this._changeInProgress=null)),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.zone(this._tzm):"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(a){return a=a?sb(a).zone():0,(this.zone()-a)%60===0},daysInMonth:function(){return B(this.year(),this.month())},dayOfYear:function(a){var b=xb((sb(this).startOf("day")-sb(this).startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")},quarter:function(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)},weekYear:function(a){var b=gb(this,this.localeData()._week.dow,this.localeData()._week.doy).year;return null==a?b:this.add(a-b,"y")},isoWeekYear:function(a){var b=gb(this,1,4).year;return null==a?b:this.add(a-b,"y")},week:function(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")},isoWeek:function(a){var b=gb(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")},weekday:function(a){var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")},isoWeekday:function(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)},isoWeeksInYear:function(){return C(this.year(),1,4)},weeksInYear:function(){var a=this.localeData()._week;return C(this.year(),a.dow,a.doy)},get:function(a){return a=x(a),this[a]()},set:function(a,b){return a=x(a),"function"==typeof this[a]&&this[a](b),this},locale:function(b){return b===a?this._locale._abbr:(this._locale=sb.localeData(b),this)},lang:f("moment().lang() is deprecated. Use moment().localeData() instead.",function(b){return b===a?this.localeData():(this._locale=sb.localeData(b),this)}),localeData:function(){return this._locale}}),sb.fn.millisecond=sb.fn.milliseconds=nb("Milliseconds",!1),sb.fn.second=sb.fn.seconds=nb("Seconds",!1),sb.fn.minute=sb.fn.minutes=nb("Minutes",!1),sb.fn.hour=sb.fn.hours=nb("Hours",!0),sb.fn.date=nb("Date",!0),sb.fn.dates=f("dates accessor is deprecated. Use date instead.",nb("Date",!0)),sb.fn.year=nb("FullYear",!0),sb.fn.years=f("years accessor is deprecated. Use year instead.",nb("FullYear",!0)),sb.fn.days=sb.fn.day,sb.fn.months=sb.fn.month,sb.fn.weeks=sb.fn.week,sb.fn.isoWeeks=sb.fn.isoWeek,sb.fn.quarters=sb.fn.quarter,sb.fn.toJSON=sb.fn.toISOString,m(sb.duration.fn=l.prototype,{_bubble:function(){var a,b,c,d=this._milliseconds,e=this._days,f=this._months,g=this._data,h=0;g.milliseconds=d%1e3,a=o(d/1e3),g.seconds=a%60,b=o(a/60),g.minutes=b%60,c=o(b/60),g.hours=c%24,e+=o(c/24),h=o(ob(e)),e-=o(pb(h)),f+=o(e/30),e%=30,h+=o(f/12),f%=12,g.days=e,g.months=f,g.years=h},abs:function(){return this._milliseconds=Math.abs(this._milliseconds),this._days=Math.abs(this._days),this._months=Math.abs(this._months),this._data.milliseconds=Math.abs(this._data.milliseconds),this._data.seconds=Math.abs(this._data.seconds),this._data.minutes=Math.abs(this._data.minutes),this._data.hours=Math.abs(this._data.hours),this._data.months=Math.abs(this._data.months),this._data.years=Math.abs(this._data.years),this},weeks:function(){return o(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*A(this._months/12)},humanize:function(a){var b=fb(this,!a,this.localeData());return a&&(b=this.localeData().pastFuture(+this,b)),this.localeData().postformat(b)},add:function(a,b){var c=sb.duration(a,b);return this._milliseconds+=c._milliseconds,this._days+=c._days,this._months+=c._months,this._bubble(),this},subtract:function(a,b){var c=sb.duration(a,b);return this._milliseconds-=c._milliseconds,this._days-=c._days,this._months-=c._months,this._bubble(),this},get:function(a){return a=x(a),this[a.toLowerCase()+"s"]()},as:function(a){var b,c;if(a=x(a),b=this._days+this._milliseconds/864e5,"month"===a||"year"===a)return c=this._months+12*ob(b),"month"===a?c:c/12;switch(b+=pb(this._months/12),a){case"week":return b/7;case"day":return b;case"hour":return 24*b;case"minute":return 24*b*60;case"second":return 24*b*60*60;case"millisecond":return 24*b*60*60*1e3;default:throw new Error("Unknown unit "+a)}},lang:sb.fn.lang,locale:sb.fn.locale,toIsoString:f("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",function(){return this.toISOString()}),toISOString:function(){var a=Math.abs(this.years()),b=Math.abs(this.months()),c=Math.abs(this.days()),d=Math.abs(this.hours()),e=Math.abs(this.minutes()),f=Math.abs(this.seconds()+this.milliseconds()/1e3);return this.asSeconds()?(this.asSeconds()<0?"-":"")+"P"+(a?a+"Y":"")+(b?b+"M":"")+(c?c+"D":"")+(d||e||f?"T":"")+(d?d+"H":"")+(e?e+"M":"")+(f?f+"S":""):"P0D"},localeData:function(){return this._locale}}),sb.duration.fn.toString=sb.duration.fn.toISOString;for(ub in hc)c(hc,ub)&&qb(ub.toLowerCase());sb.duration.fn.asMilliseconds=function(){return this.as("ms")},sb.duration.fn.asSeconds=function(){return this.as("s")},sb.duration.fn.asMinutes=function(){return this.as("m")},sb.duration.fn.asHours=function(){return this.as("h")},sb.duration.fn.asDays=function(){return this.as("d")},sb.duration.fn.asWeeks=function(){return this.as("weeks")},sb.duration.fn.asMonths=function(){return this.as("M")},sb.duration.fn.asYears=function(){return this.as("y")},sb.locale("en",{ordinal:function(a){var b=a%10,c=1===A(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),Ib?module.exports=sb:"function"==typeof define&&define.amd?(define("moment",function(a,b,c){return c.config&&c.config()&&c.config().noGlobal===!0&&(wb.moment=tb),sb}),rb(!0)):rb()}).call(this); \ No newline at end of file
diff --git a/extensions/Review/web/js/review.js b/extensions/Review/web/js/review.js
new file mode 100644
index 000000000..08ae29547
--- /dev/null
+++ b/extensions/Review/web/js/review.js
@@ -0,0 +1,210 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+var Dom = YAHOO.util.Dom;
+var Event = YAHOO.util.Event;
+
+var REVIEW = {
+ widget: false,
+ target: false,
+ fields: [],
+ use_error_for: false,
+ ispatch_override: false,
+ description_override: false,
+
+ init_review_flag: function(fid, flag_name) {
+ var idx = this.fields.push({ 'fid': fid, 'flag_name': flag_name, 'component': '' }) - 1;
+ this.flag_change(false, idx);
+ Event.addListener(fid, 'change', this.flag_change, idx);
+ },
+
+ init_mandatory: function() {
+ var form = this.find_form();
+ if (!form) return;
+ Event.addListener(form, 'submit', this.check_mandatory);
+ for (var i = 0; i < this.fields.length; i++) {
+ var field = this.fields[i];
+ // existing reviews that have empty requestee shouldn't force a
+ // reviewer to be selected
+ field.old_empty_review = Dom.get(field.fid).value == '?'
+ && Dom.get(field.flag_name).value == '';
+ if (!field.old_empty_review)
+ Dom.addClass(field.flag_name, 'required');
+ }
+ },
+
+ init_enter_bug: function() {
+ Event.addListener('component', 'change', REVIEW.component_change);
+ BUGZILLA.string['reviewer_required'] = 'A reviewer is required.';
+ this.use_error_for = true;
+ this.init_create_attachment();
+ },
+
+ init_create_attachment: function() {
+ Event.addListener('data', 'change', REVIEW.attachment_change);
+ Event.addListener('description', 'change', REVIEW.description_change);
+ Event.addListener('ispatch', 'change', REVIEW.ispatch_change);
+ },
+
+ component_change: function() {
+ for (var i = 0; i < REVIEW.fields.length; i++) {
+ REVIEW.flag_change(false, i);
+ }
+ },
+
+ attachment_change: function() {
+ var filename = Dom.get('data').value.split('/').pop().split('\\').pop();
+ var description = Dom.get('description');
+ if (description.value == '' || !REVIEW.description_override) {
+ description.value = filename;
+ }
+ if (!REVIEW.ispatch_override) {
+ Dom.get('ispatch').checked =
+ REVIEW.endsWith(filename, '.diff') || REVIEW.endsWith(filename, '.patch');
+ }
+ setContentTypeDisabledState(this.form);
+ description.select();
+ description.focus();
+ },
+
+ description_change: function() {
+ REVIEW.description_override = true;
+ },
+
+ ispatch_change: function() {
+ REVIEW.ispatch_override = true;
+ },
+
+ flag_change: function(e, field_idx) {
+ var field = REVIEW.fields[field_idx];
+ var suggestions_span = Dom.get(field.fid + '_suggestions');
+
+ // for requests only
+ if (Dom.get(field.fid).value != '?') {
+ Dom.addClass(suggestions_span, 'bz_default_hidden');
+ return;
+ }
+
+ // find selected component
+ var component = static_component || Dom.get('component').value;
+ if (!component) {
+ Dom.addClass(suggestions_span, 'bz_default_hidden');
+ return;
+ }
+
+ // init menu and events
+ if (!field.menu) {
+ field.menu = new YAHOO.widget.Menu(field.fid + '_menu');
+ field.menu.render(document.body);
+ field.menu.subscribe('click', REVIEW.suggestion_click);
+ Event.addListener(field.fid + '_suggestions_link', 'click', REVIEW.suggestions_click, field_idx)
+ }
+
+ // build review list
+ if (field.component != component) {
+ field.menu.clearContent();
+ for (var i = 0, il = review_suggestions._mentors.length; i < il; i++) {
+ REVIEW.add_menu_item(field_idx, review_suggestions._mentors[i], true);
+ }
+ if (review_suggestions[component] && review_suggestions[component].length) {
+ REVIEW.add_menu_items(field_idx, review_suggestions[component]);
+ } else if (review_suggestions._product) {
+ REVIEW.add_menu_items(field_idx, review_suggestions._product);
+ }
+ field.menu.render();
+ field.component = component;
+ }
+
+ // show (or hide) the menu
+ if (field.menu.getItem(0)) {
+ Dom.removeClass(suggestions_span, 'bz_default_hidden');
+ } else {
+ Dom.addClass(suggestions_span, 'bz_default_hidden');
+ }
+ },
+
+ add_menu_item: function(field_idx, user, is_mentor) {
+ var menu = REVIEW.fields[field_idx].menu;
+ var items = menu.getItems();
+ for (var i = 0, il = items.length; i < il; i++) {
+ if (items[i].cfg.config.url.value == '#' + user.login) {
+ return;
+ }
+ }
+ var queue = '';
+ if (user.review_count == 0) {
+ queue = 'empty queue';
+ } else {
+ queue = user.review_count + ' review' + (user.review_count == 1 ? '' : 's') + ' in queue';
+ }
+ var item = menu.addItem(
+ { text: user.identity + ' (' + queue + ')', url: '#' + user.login }
+ );
+ if (is_mentor)
+ item.cfg.setProperty('classname', 'mentor');
+ },
+
+ add_menu_items: function(field_idx, users) {
+ for (var i = 0; i < users.length; i++) {
+ if (!review_suggestions._mentor
+ || users[i].login != review_suggestions._mentor.login)
+ {
+ REVIEW.add_menu_item(field_idx, users[i]);
+ }
+ }
+ },
+
+ suggestions_click: function(e, field_idx) {
+ var field = REVIEW.fields[field_idx];
+ field.menu.cfg.setProperty('xy', Event.getXY(e));
+ field.menu.show();
+ Event.stopEvent(e);
+ REVIEW.target = field.flag_name;
+ },
+
+ suggestion_click: function(type, args) {
+ if (args[1]) {
+ Dom.get(REVIEW.target).value = decodeURIComponent(args[1].cfg.getProperty('url')).substr(1);
+ }
+ Event.stopEvent(args[0]);
+ },
+
+ check_mandatory: function(e) {
+ if (Dom.get('data') && !Dom.get('data').value
+ && Dom.get('attach_text') && !Dom.get('attach_text').value)
+ {
+ return;
+ }
+ for (var i = 0; i < REVIEW.fields.length; i++) {
+ var field = REVIEW.fields[i];
+ if (!field.old_empty_review
+ && Dom.get(field.fid).value == '?'
+ && Dom.get(field.flag_name).value == '')
+ {
+ if (REVIEW.use_error_for) {
+ _errorFor(Dom.get(REVIEW.fields[i].flag_name), 'reviewer');
+ } else {
+ alert('You must provide a reviewer for review requests.');
+ }
+ Event.stopEvent(e);
+ }
+ }
+ },
+
+ find_form: function() {
+ for (var i = 0; i < document.forms.length; i++) {
+ var action = document.forms[i].getAttribute('action');
+ if (action == 'attachment.cgi' || action == 'post_bug.cgi')
+ return document.forms[i];
+ }
+ return false;
+ },
+
+ endsWith: function(str, suffix) {
+ return str.indexOf(suffix, str.length - suffix.length) !== -1;
+ }
+};
diff --git a/extensions/Review/web/js/review_history.js b/extensions/Review/web/js/review_history.js
new file mode 100644
index 000000000..ea35edf26
--- /dev/null
+++ b/extensions/Review/web/js/review_history.js
@@ -0,0 +1,384 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+(function () {
+ 'use strict';
+
+ YUI.add('bz-review-history', function (Y) {
+ function format_duration(o) {
+ if (o.value) {
+ if (o.value < 0) {
+ return "???";
+ } else {
+ return moment.duration(o.value).humanize();
+ }
+ }
+ else {
+ return "---";
+ }
+ }
+
+ function format_attachment(o) {
+ if (o.value) {
+ return o.value.description;
+ }
+ }
+
+ function format_action(o) {
+ return o.value;
+ }
+
+ function format_setter(o) {
+ return o.value.real_name ? o.value.real_name + " <" + o.value.name + ">" : o.value.name;
+ }
+
+ function format_date(o) {
+ return o.value && Y.DataType.Date.format(o.value, {
+ format: "%Y-%m-%d"
+ });
+ }
+
+ function parse_date(str) {
+ var parts = str.split(/\D/);
+ return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]);
+ }
+
+ var flagDS, bugDS, attachmentDS, historyTable;
+ flagDS = new Y.DataSource.IO({ source: 'jsonrpc.cgi' });
+ flagDS.plug(Y.Plugin.DataSourceJSONSchema, {
+ schema: {
+ resultListLocator: 'result',
+ resultFields: [
+ { key: 'id' },
+ { key: 'requestee' },
+ { key: 'setter' },
+ { key: 'flag_id' },
+ { key: 'creation_time' },
+ { key: 'status' },
+ { key: 'bug_id' },
+ { key: 'type' },
+ { key: 'attachment_id' }
+ ]
+ }
+ });
+
+ bugDS = new Y.DataSource.IO({ source: 'jsonrpc.cgi' });
+ bugDS.plug(Y.Plugin.DataSourceJSONSchema, {
+ schema: {
+ resultListLocator: 'result.bugs',
+ resultFields: [
+ { key: 'id' },
+ { key: 'summary' }
+ ]
+ }
+ });
+
+ attachmentDS = new Y.DataSource.IO({ source: 'jsonrpc.cgi' });
+ attachmentDS.plug(Y.Plugin.DataSourceJSONSchema, {
+ schema: {
+ metaFields: { 'attachments': 'result.attachments' }
+ }
+ });
+
+ historyTable = new Y.DataTable({
+ columns: [
+ { key: 'creation_time', label: 'Created', sortable: true, formatter: format_date },
+ { key: 'attachment', label: 'Attachment', formatter: format_attachment, allowHTML: true },
+ { key: 'setter', label: 'Requester', formatter: format_setter },
+ { key: "action", label: "Action", sortable: true, allowHTML: true, formatter: format_action },
+ { key: "duration", label: "Duration", sortable: true, formatter: format_duration },
+ { key: "bug_id", label: "Bug", sortable: true, allowHTML: true,
+ formatter: '<a href="show_bug.cgi?id={value}" target="_blank">{value}</a>' },
+ { key: 'bug_summary', label: 'Summary' }
+ ]
+ });
+
+ function fetch_flag_ids(user) {
+ return new Y.Promise(function (resolve, reject) {
+ var flagIdCallback = {
+ success: function (e) {
+ var flags = e.response.results;
+ var flag_ids = flags.filter(function (flag) {
+ return flag.status == '?';
+ })
+ .map(function (flag) {
+ return flag.flag_id;
+ });
+
+ if (flag_ids.length > 0) {
+ resolve(flag_ids);
+ } else {
+ reject("No reviews found");
+ }
+ },
+ failure: function (e) {
+ reject(e.error.message);
+ }
+ };
+
+ flagDS.sendRequest({
+ request: Y.JSON.stringify({
+ version: '1.1',
+ method: 'Review.flag_activity',
+ params: {
+ type_name: 'review',
+ requestee: user,
+ include_fields: ['flag_id', 'status']
+ }
+ }),
+ cfg: {
+ method: "POST",
+ headers: { 'Content-Type': 'application/json' }
+ },
+ callback: flagIdCallback
+ });
+ });
+ }
+
+ function fetch_flags(flag_ids) {
+ return new Y.Promise(function (resolve, reject) {
+ flagDS.sendRequest({
+ request: Y.JSON.stringify({
+ version: '1.1',
+ method: 'Review.flag_activity',
+ params: { flag_ids: flag_ids }
+ }),
+ cfg: {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' }
+ },
+ callback: {
+ success: function (e) {
+ var flags = e.response.results;
+ flags.forEach(function(flag) {
+ flag.creation_time = parse_date(flag.creation_time);
+ });
+ resolve(flags.sort(function (a, b) {
+ if (a.id > b.id) return 1;
+ if (a.id < b.id) return -1;
+ return 0;
+ }));
+ },
+ failure: function (e) {
+ reject(e.error.message);
+ }
+ }
+ });
+ });
+ }
+
+ function fetch_bug_summaries(flags) {
+ return new Y.Promise(function (resolve, reject) {
+ var bug_ids = Y.Array.dedupe(flags.map(function (f) {
+ return f.bug_id;
+ }));
+
+ bugDS.sendRequest({
+ request: Y.JSON.stringify({
+ version: '1.1',
+ method: 'Bug.get',
+ params: { ids: bug_ids, include_fields: ['summary', 'id'] }
+ }),
+ cfg: {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' }
+ },
+ callback: {
+ success: function (e) {
+ var bugs = e.response.results,
+ summary = {};
+
+ bugs.forEach(function (bug) {
+ summary[bug.id] = bug.summary;
+ });
+ flags.forEach(function (flag) {
+ flag.bug_summary = summary[flag.bug_id];
+ });
+ resolve(flags);
+ },
+ failure: function (e) {
+ reject(e.error.message);
+ }
+ }
+ });
+ });
+ }
+
+ function fetch_attachment_descriptions(flags) {
+ return new Y.Promise(function (resolve, reject) {
+ var attachment_ids = Y.Array.dedupe(flags.map(function (f) {
+ return f.attachment_id;
+ }));
+
+ attachmentDS.sendRequest({
+ request: Y.JSON.stringify({
+ version: '1.1',
+ method: 'Bug.attachments',
+ params: {
+ attachment_ids: attachment_ids,
+ include_fields: ['id', 'description']
+ }
+ }),
+ cfg: {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' }
+ },
+ callback: {
+ success: function (e) {
+ var attachments = e.response.meta.attachments;
+ flags.forEach(function (flag) {
+ flag.attachment = attachments[flag.attachment_id];
+ });
+ resolve(flags);
+ },
+ failure: function (e) {
+ reject(e.error.message);
+ }
+ }
+ });
+ });
+ }
+
+ function add_historical_action(history, flag, stash, action) {
+ history.push({
+ attachment: flag.attachment,
+ bug_id: flag.bug_id,
+ bug_summary: flag.bug_summary,
+ creation_time: stash.creation_time,
+ duration: flag.creation_time - stash.creation_time,
+ setter: stash.setter,
+ action: action
+ });
+ }
+
+ function generate_history(flags, user) {
+ var history = [],
+ stash = {},
+ flag, stash_key ;
+
+ flags.forEach(function (flag) {
+ var flag_id = flag.flag_id;
+
+ switch (flag.status) {
+ case '?':
+ // If we get a ? after a + or -, we get a fresh start.
+ if (stash[flag_id] && stash[flag_id].is_complete)
+ delete stash[flag_id];
+
+ // handle untargeted review requests.
+ if (!flag.requestee)
+ flag.requestee = { id: 'the wind', name: 'the wind' };
+
+ if (stash[flag_id]) {
+ // flag was reassigned
+ if (flag.requestee.id != stash[flag_id].requestee.id) {
+ // if ? started out mine, but went to someone else.
+ if (stash[flag_id].requestee.name == user) {
+ add_historical_action(history, flag, stash[flag_id], 'reassigned to ' + flag.requestee.name);
+ stash[flag_id] = flag;
+ }
+ else {
+ // flag changed hands. Reset the creation_time and requestee
+ stash[flag_id].creation_time = flag.creation_time;
+ stash[flag_id].requestee = flag.requestee;
+ }
+ }
+ } else {
+ stash[flag_id] = flag;
+ }
+ break;
+
+ case 'X':
+ if (stash[flag_id]) {
+ // Only process if we did not get a + or a - since
+ if (!stash[flag_id].is_complete) {
+ add_historical_action(history, flag, stash[flag_id], 'cancelled');
+ }
+ delete stash[flag_id];
+ }
+ break;
+
+
+ case '+':
+ case '-':
+ // if we get a + or -, we only accept it if the requestee is the user we're interested in.
+ // we set is_complete to handle cancelations.
+ if (stash[flag_id] && stash[flag_id].requestee.name == user) {
+ add_historical_action(history, flag, stash[flag_id], "review" + flag.status);
+ stash[flag_id].is_complete = true;
+ }
+ break;
+ }
+ });
+
+ for (stash_key in stash) {
+ flag = stash[stash_key];
+ if (flag.is_complete) continue;
+ if (flag.requestee.name != user) continue;
+ history.push({
+ attachment: flag.attachment,
+ bug_id: flag.bug_id,
+ bug_summary: flag.bug_summary,
+ creation_time: flag.creation_time,
+ duration: new Date() - flag.creation_time,
+ setter: flag.setter,
+ action: 'review?'
+ });
+ }
+
+ return history;
+ }
+
+ Y.ReviewHistory = {};
+
+ Y.ReviewHistory.render = function (sel) {
+ Y.one('#history-loading').hide();
+ historyTable.render(sel);
+ historyTable.setAttrs({
+ width: "100%"
+ }, true);
+ };
+
+ Y.ReviewHistory.refresh = function (user, real_name) {
+ var caption = "Review History for " + (real_name ? real_name + ' &lt;' + user + '&gt;' : user);
+ historyTable.setAttrs({
+ caption: caption
+ });
+ historyTable.set('data', null);
+ historyTable.showMessage('Loading...');
+ fetch_flag_ids(user)
+ .then(fetch_flags)
+ .then(fetch_bug_summaries)
+ .then(fetch_attachment_descriptions)
+ .then(function (flags) {
+ return new Y.Promise(function (resolve, reject) {
+ try {
+ resolve(generate_history(flags, user));
+ }
+ catch (e) {
+ reject(e.message);
+ }
+ });
+ })
+ .then(function (history) {
+ historyTable.set('data', history);
+ historyTable.sort({
+ creation_time: 'desc'
+ });
+ }, function (message) {
+ historyTable.showMessage(message);
+ });
+ };
+
+ }, '0.0.1', {
+ requires: [
+ "node", "datatype-date", "datatable", "datatable-sort", "datatable-message", "json-stringify",
+ "datatable-datasource", "datasource-io", "datasource-jsonschema", "cookie",
+ "gallery-datatable-row-expansion-bmo", "handlebars", "escape", "promise"
+ ]
+ });
+}());
diff --git a/extensions/Review/web/styles/badge.css b/extensions/Review/web/styles/badge.css
new file mode 100644
index 000000000..e699b5825
--- /dev/null
+++ b/extensions/Review/web/styles/badge.css
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#badge {
+ background-color: #c00;
+ font-size: small;
+ font-weight: bold;
+ padding: 0 5px;
+ border-radius: 10px;
+ margin: 0 5px;
+ color: #fff !important;
+}
diff --git a/extensions/Review/web/styles/reports.css b/extensions/Review/web/styles/reports.css
new file mode 100644
index 000000000..bbbf93559
--- /dev/null
+++ b/extensions/Review/web/styles/reports.css
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#report {
+ margin-top: 1em;
+}
+
+.product_name {
+ font-weight: bold;
+ white-space: nowrap;
+}
+
+.product_name a {
+ color: inherit;
+}
+
+.product_name a:hover {
+ color: inherit;
+ text-decoration: none;
+}
+
+.component_name, .other_components {
+ padding: 0 1em;
+ white-space: nowrap;
+}
+
+.component_name:before, .other_components:before {
+ content: "\a0\a0\a0\a0";
+}
+
+.other_components {
+ font-style: italic;
+}
+
+.reviewers {
+ width: 100%;
+}
diff --git a/extensions/Review/web/styles/review.css b/extensions/Review/web/styles/review.css
new file mode 100644
index 000000000..9f5b63603
--- /dev/null
+++ b/extensions/Review/web/styles/review.css
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+.mentor {
+ font-weight: bold;
+}
diff --git a/extensions/Review/web/styles/review_history.css b/extensions/Review/web/styles/review_history.css
new file mode 100644
index 000000000..b72b2efb2
--- /dev/null
+++ b/extensions/Review/web/styles/review_history.css
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+.yui3-skin-sam .yui3-datatable-table > table {
+ width: 100%;
+}
diff --git a/extensions/SecureMail/Config.pm b/extensions/SecureMail/Config.pm
new file mode 100644
index 000000000..f1975c1c1
--- /dev/null
+++ b/extensions/SecureMail/Config.pm
@@ -0,0 +1,49 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla SecureMail Extension
+#
+# The Initial Developer of the Original Code is Mozilla.
+# Portions created by Mozilla are Copyright (C) 2008 Mozilla Corporation.
+# All Rights Reserved.
+#
+# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
+# Gervase Markham <gerv@gerv.net>
+
+package Bugzilla::Extension::SecureMail;
+use strict;
+
+use constant NAME => 'SecureMail';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'Crypt-OpenPGP',
+ module => 'Crypt::OpenPGP',
+ # 1.02 added the ability for new() to take KeyRing objects for the
+ # PubRing argument.
+ version => '1.02',
+ # 1.04 hangs - https://rt.cpan.org/Public/Bug/Display.html?id=68018
+ # blacklist => [ '1.04' ],
+ },
+ {
+ package => 'Crypt-SMIME',
+ module => 'Crypt::SMIME',
+ version => 0,
+ },
+ {
+ package => 'HTML-Tree',
+ module => 'HTML::Tree',
+ version => 0,
+ }
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/SecureMail/Extension.pm b/extensions/SecureMail/Extension.pm
new file mode 100644
index 000000000..687112955
--- /dev/null
+++ b/extensions/SecureMail/Extension.pm
@@ -0,0 +1,659 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla SecureMail Extension
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation.
+# Portions created by Mozilla are Copyright (C) 2008 Mozilla Foundation.
+# All Rights Reserved.
+#
+# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
+# Gervase Markham <gerv@gerv.net>
+
+package Bugzilla::Extension::SecureMail;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Attachment;
+use Bugzilla::Comment;
+use Bugzilla::Group;
+use Bugzilla::Object;
+use Bugzilla::User;
+use Bugzilla::Util qw(correct_urlbase trim trick_taint is_7bit_clean);
+use Bugzilla::Error;
+use Bugzilla::Mailer;
+
+use Crypt::OpenPGP::Armour;
+use Crypt::OpenPGP::KeyRing;
+use Crypt::OpenPGP;
+use Crypt::SMIME;
+use Encode;
+use HTML::Tree;
+
+our $VERSION = '0.5';
+
+use constant SECURE_NONE => 0;
+use constant SECURE_BODY => 1;
+use constant SECURE_ALL => 2;
+
+##############################################################################
+# Creating new columns
+#
+# secure_mail boolean in the 'groups' table - whether to send secure mail
+# public_key text in the 'profiles' table - stores public key
+##############################################################################
+sub install_update_db {
+ my ($self, $args) = @_;
+
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_add_column('groups', 'secure_mail',
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0});
+ $dbh->bz_add_column('profiles', 'public_key', { TYPE => 'LONGTEXT' });
+}
+
+##############################################################################
+# Maintaining new columns
+##############################################################################
+
+BEGIN {
+ *Bugzilla::Group::secure_mail = \&_group_secure_mail;
+ *Bugzilla::User::public_key = \&_user_public_key;
+}
+
+sub _group_secure_mail { return $_[0]->{'secure_mail'}; }
+
+# We want to lazy-load the public_key.
+sub _user_public_key {
+ my $self = shift;
+ if (!exists $self->{public_key}) {
+ ($self->{public_key}) = Bugzilla->dbh->selectrow_array(
+ "SELECT public_key FROM profiles WHERE userid = ?",
+ undef,
+ $self->id
+ );
+ }
+ return $self->{public_key};
+}
+
+# Make sure generic functions know about the additional fields in the user
+# and group objects.
+sub object_columns {
+ my ($self, $args) = @_;
+ my $class = $args->{'class'};
+ my $columns = $args->{'columns'};
+
+ if ($class->isa('Bugzilla::Group')) {
+ push(@$columns, 'secure_mail');
+ }
+}
+
+# Plug appropriate validators so we can check the validity of the two
+# fields created by this extension, when new values are submitted.
+sub object_validators {
+ my ($self, $args) = @_;
+ my %args = %{ $args };
+ my ($invocant, $validators) = @args{qw(class validators)};
+
+ if ($invocant->isa('Bugzilla::Group')) {
+ $validators->{'secure_mail'} = \&Bugzilla::Object::check_boolean;
+ }
+ elsif ($invocant->isa('Bugzilla::User')) {
+ $validators->{'public_key'} = sub {
+ my ($self, $value) = @_;
+ $value = trim($value) || '';
+
+ return $value if $value eq '';
+
+ if ($value =~ /PUBLIC KEY/) {
+ # PGP keys must be ASCII-armoured.
+ if (!Crypt::OpenPGP::Armour->unarmour($value)) {
+ ThrowUserError('securemail_invalid_key',
+ { errstr => Crypt::OpenPGP::Armour->errstr });
+ }
+ }
+ elsif ($value =~ /BEGIN CERTIFICATE/) {
+ # S/MIME Keys must be in PEM format (Base64-encoded X.509)
+ #
+ # Crypt::SMIME seems not to like tainted values - it claims
+ # they aren't scalars!
+ trick_taint($value);
+
+ my $smime = Crypt::SMIME->new();
+ eval {
+ $smime->setPublicKey([$value]);
+ };
+ if ($@) {
+ ThrowUserError('securemail_invalid_key',
+ { errstr => $@ });
+ }
+ }
+ else {
+ ThrowUserError('securemail_invalid_key');
+ }
+
+ return $value;
+ };
+ }
+}
+
+# When creating a 'group' object, set up the secure_mail field appropriately.
+sub object_before_create {
+ my ($self, $args) = @_;
+ my $class = $args->{'class'};
+ my $params = $args->{'params'};
+
+ if ($class->isa('Bugzilla::Group')) {
+ $params->{secure_mail} = Bugzilla->cgi->param('secure_mail');
+ }
+}
+
+# On update, make sure the updating process knows about our new columns.
+sub object_update_columns {
+ my ($self, $args) = @_;
+ my $object = $args->{'object'};
+ my $columns = $args->{'columns'};
+
+ if ($object->isa('Bugzilla::Group')) {
+ # This seems like a convenient moment to extract this value...
+ $object->set('secure_mail', Bugzilla->cgi->param('secure_mail'));
+
+ push(@$columns, 'secure_mail');
+ }
+ elsif ($object->isa('Bugzilla::User')) {
+ push(@$columns, 'public_key');
+ }
+}
+
+# Handle the setting and changing of the public key.
+sub user_preferences {
+ my ($self, $args) = @_;
+ my $tab = $args->{'current_tab'};
+ my $save = $args->{'save_changes'};
+ my $handled = $args->{'handled'};
+ my $vars = $args->{'vars'};
+ my $params = Bugzilla->input_params;
+
+ return unless $tab eq 'securemail';
+
+ # Create a new user object so we don't mess with the main one, as we
+ # don't know where it's been...
+ my $user = new Bugzilla::User(Bugzilla->user->id);
+
+ if ($save) {
+ $user->set('public_key', $params->{'public_key'});
+ $user->update();
+
+ # Send user a test email
+ if ($user->public_key) {
+ _send_test_email($user);
+ $vars->{'test_email_sent'} = 1;
+ }
+ }
+
+ $vars->{'public_key'} = $user->public_key;
+
+ # Set the 'handled' scalar reference to true so that the caller
+ # knows the panel name is valid and that an extension took care of it.
+ $$handled = 1;
+}
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my $file = $args->{'file'};
+ my $vars = $args->{'vars'};
+
+ # Bug dependency emails contain the subject of the dependent bug
+ # right before the diffs when a status has gone from open/closed
+ # or closed/open. We need to sanitize the subject of change.blocker
+ # similar to how we do referenced bugs
+ return unless
+ $file eq 'email/bugmail.html.tmpl'
+ || $file eq 'email/bugmail.txt.tmpl';
+
+ if (defined $vars->{diffs}) {
+ foreach my $change (@{ $vars->{diffs} }) {
+ next if !defined $change->{blocker};
+ if (grep($_->secure_mail, @{ $change->{blocker}->groups_in })) {
+ $change->{blocker}->{short_desc} = "(Secure bug)";
+ }
+ }
+ }
+}
+
+sub _send_test_email {
+ my ($user) = @_;
+ my $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'});
+
+ my $vars = {
+ to_user => $user->email,
+ };
+
+ my $msg = "";
+ $template->process("account/email/securemail-test.txt.tmpl", $vars, \$msg)
+ || ThrowTemplateError($template->error());
+
+ MessageToMTA($msg);
+}
+
+##############################################################################
+# Encrypting the email
+##############################################################################
+sub mailer_before_send {
+ my ($self, $args) = @_;
+
+ my $email = $args->{'email'};
+ my $body = $email->body;
+
+ # Decide whether to make secure.
+ # This is a bit of a hack; it would be nice if it were more clear
+ # what sort a particular email is.
+ my $is_bugmail = $email->header('X-Bugzilla-Status') ||
+ $email->header('X-Bugzilla-Type') eq 'request';
+ my $is_passwordmail = !$is_bugmail && ($body =~ /cfmpw.*cxlpw/s);
+ my $is_test_email = $email->header('X-Bugzilla-Type') =~ /securemail-test/ ? 1 : 0;
+ my $is_whine_email = $email->header('X-Bugzilla-Type') eq 'whine' ? 1 : 0;
+ my $encrypt_header = $email->header('X-Bugzilla-Encrypt') ? 1 : 0;
+
+ if ($is_bugmail
+ || $is_passwordmail
+ || $is_test_email
+ || $is_whine_email
+ || $encrypt_header
+ ) {
+ # Convert the email's To address into a User object
+ my $login = $email->header('To');
+ my $emailsuffix = Bugzilla->params->{'emailsuffix'};
+ $login =~ s/$emailsuffix$//;
+ my $user = new Bugzilla::User({ name => $login });
+
+ # Default to secure. (Of course, this means if this extension has a
+ # bug, lots of people are going to get bugmail falsely claiming their
+ # bugs are secure and they need to add a key...)
+ my $make_secure = SECURE_ALL;
+
+ if ($is_bugmail) {
+ # This is also a bit of a hack, but there's no header with the
+ # bug ID in. So we take the first number in the subject.
+ my ($bug_id) = ($email->header('Subject') =~ /\[\D+(\d+)\]/);
+ my $bug = new Bugzilla::Bug($bug_id);
+ if (!_should_secure_bug($bug)) {
+ $make_secure = SECURE_NONE;
+ }
+ # If the insider group has securemail enabled..
+ my $insider_group = Bugzilla::Group->new({ name => Bugzilla->params->{'insidergroup'} });
+ if ($insider_group
+ && $insider_group->secure_mail
+ && $make_secure == SECURE_NONE)
+ {
+ my $comment_is_private = Bugzilla->dbh->selectcol_arrayref(
+ "SELECT isprivate FROM longdescs WHERE bug_id=? ORDER BY bug_when",
+ undef, $bug_id);
+ # Encrypt if there are private comments on an otherwise public bug
+ while ($body =~ /[\r\n]--- Comment #(\d+)/g) {
+ my $comment_number = $1;
+ if ($comment_number && $comment_is_private->[$comment_number]) {
+ $make_secure = SECURE_BODY;
+ last;
+ }
+ }
+ # Encrypt if updating a private attachment without a comment
+ if ($email->header('X-Bugzilla-Changed-Fields')
+ && $email->header('X-Bugzilla-Changed-Fields') =~ /Attachment #(\d+)/)
+ {
+ my $attachment = Bugzilla::Attachment->new($1);
+ if ($attachment && $attachment->isprivate) {
+ $make_secure = SECURE_BODY;
+ }
+ }
+ }
+ }
+ elsif ($is_passwordmail) {
+ # Mail is made unsecure only if the user does not have a public
+ # key and is not in any security groups. So specifying a public
+ # key OR being in a security group means the mail is kept secure
+ # (but, as noted above, the check is the other way around because
+ # we default to secure).
+ if ($user &&
+ !$user->public_key &&
+ !grep($_->secure_mail, @{ $user->groups }))
+ {
+ $make_secure = SECURE_NONE;
+ }
+ }
+ elsif ($is_whine_email) {
+ # When a whine email has one or more secure bugs in the body, then
+ # encrypt the entire email body. Subject can be left alone as it
+ # comes from the whine settings.
+ $make_secure = _should_secure_whine($email) ? SECURE_BODY : SECURE_NONE;
+ }
+ elsif ($encrypt_header) {
+ # Templates or code may set the X-Bugzilla-Encrypt header to
+ # trigger encryption of emails. Remove that header from the email.
+ $email->header_set('X-Bugzilla-Encrypt');
+ }
+
+ # If finding the user fails for some reason, but we determine we
+ # should be encrypting, we want to make the mail safe. An empty key
+ # does that.
+ my $public_key = $user ? $user->public_key : '';
+
+ # Check if the new bugmail prefix should be added to the subject.
+ my $add_new = ($email->header('X-Bugzilla-Type') eq 'new' &&
+ $user &&
+ $user->settings->{'bugmail_new_prefix'}->{'value'} eq 'on') ? 1 : 0;
+
+ if ($make_secure == SECURE_NONE) {
+ # Filter the bug_links in HTML email in case the bugs the links
+ # point are "secured" bugs and the user may not be able to see
+ # the summaries.
+ _filter_bug_links($email);
+ }
+ else {
+ _make_secure($email, $public_key, $is_bugmail && $make_secure == SECURE_ALL, $add_new);
+ }
+ }
+}
+
+# Custom hook for bugzilla.mozilla.org (see bug 752400)
+sub bugmail_referenced_bugs {
+ my ($self, $args) = @_;
+ # Sanitise subjects of referenced bugs.
+ my $referenced_bugs = $args->{'referenced_bugs'};
+ # No need to sanitise subjects if the entire email will be secured.
+ return if _should_secure_bug($args->{'updated_bug'});
+ # Replace the subject if required
+ foreach my $ref (@$referenced_bugs) {
+ if (grep($_->secure_mail, @{ $ref->{'bug'}->groups_in })) {
+ $ref->{'short_desc'} = "(Secure bug)";
+ }
+ }
+}
+
+sub _should_secure_bug {
+ my ($bug) = @_;
+ # If there's a problem with the bug, err on the side of caution and mark it
+ # as secure.
+ return
+ !$bug
+ || $bug->{'error'}
+ || grep($_->secure_mail, @{ $bug->groups_in });
+}
+
+sub _should_secure_whine {
+ my ($email) = @_;
+ my $should_secure = 0;
+ $email->walk_parts(sub {
+ my $part = shift;
+ my $content_type = $part->content_type;
+ return if !$content_type || $content_type !~ /^text\/plain/;
+ my $body = $part->body;
+ my @bugids = $body =~ /Bug (\d+):/g;
+ foreach my $id (@bugids) {
+ $id = trim($id);
+ next if !$id;
+ my $bug = new Bugzilla::Bug($id);
+ if ($bug && _should_secure_bug($bug)) {
+ $should_secure = 1;
+ last;
+ }
+ }
+ });
+ return $should_secure ? 1 : 0;
+}
+
+sub _make_secure {
+ my ($email, $key, $sanitise_subject, $add_new) = @_;
+
+ # Add header showing this email has been secured
+ $email->header_set('X-Bugzilla-Secure-Email', 'Yes');
+
+ my $subject = $email->header('Subject');
+ my ($bug_id) = $subject =~ /\[\D+(\d+)\]/;
+
+ my $key_type = 0;
+ if ($key && $key =~ /PUBLIC KEY/) {
+ $key_type = 'PGP';
+ }
+ elsif ($key && $key =~ /BEGIN CERTIFICATE/) {
+ $key_type = 'S/MIME';
+ }
+
+ if ($key_type eq 'PGP') {
+ ##################
+ # PGP Encryption #
+ ##################
+
+ my $pubring = new Crypt::OpenPGP::KeyRing(Data => $key);
+ my $pgp = new Crypt::OpenPGP(PubRing => $pubring);
+
+ if (scalar $email->parts > 1) {
+ my $old_boundary = $email->{ct}{attributes}{boundary};
+ my $to_encrypt = "Content-Type: " . $email->content_type . "\n\n";
+
+ # We need to do some fix up of each part for proper encoding and then
+ # stringify all parts for encrypting. We have to retain the old
+ # boundaries as well so that the email client can reconstruct the
+ # original message properly.
+ $email->walk_parts(\&_fix_part);
+
+ $email->walk_parts(sub {
+ my ($part) = @_;
+ if ($sanitise_subject) {
+ _insert_subject($part, $subject);
+ }
+ return if $part->parts > 1; # Top-level
+ $to_encrypt .= "--$old_boundary\n" . $part->as_string . "\n";
+ });
+ $to_encrypt .= "--$old_boundary--";
+
+ # Now create the new properly formatted PGP parts containing the
+ # encrypted original message
+ my @new_parts = (
+ Email::MIME->create(
+ attributes => {
+ content_type => 'application/pgp-encrypted',
+ encoding => '7bit',
+ },
+ body => "Version: 1\n",
+ ),
+ Email::MIME->create(
+ attributes => {
+ content_type => 'application/octet-stream',
+ filename => 'encrypted.asc',
+ disposition => 'inline',
+ encoding => '7bit',
+ },
+ body => _pgp_encrypt($pgp, $to_encrypt)
+ ),
+ );
+ $email->parts_set(\@new_parts);
+ my $new_boundary = $email->{ct}{attributes}{boundary};
+ # Redo the old content type header with the new boundaries
+ # and other information needed for PGP
+ $email->header_set("Content-Type",
+ "multipart/encrypted; " .
+ "protocol=\"application/pgp-encrypted\"; " .
+ "boundary=\"$new_boundary\"");
+ }
+ else {
+ _fix_part($email);
+ if ($sanitise_subject) {
+ _insert_subject($email, $subject);
+ }
+ $email->body_set(_pgp_encrypt($pgp, $email->body));
+ }
+ }
+
+ elsif ($key_type eq 'S/MIME') {
+ #####################
+ # S/MIME Encryption #
+ #####################
+
+ $email->walk_parts(\&_fix_part);
+
+ if ($sanitise_subject) {
+ $email->walk_parts(sub { _insert_subject($_[0], $subject) });
+ }
+
+ my $smime = Crypt::SMIME->new();
+ my $encrypted;
+
+ eval {
+ $smime->setPublicKey([$key]);
+ $encrypted = $smime->encrypt($email->as_string());
+ };
+
+ if (!$@) {
+ # We can't replace the Email::MIME object, so we have to swap
+ # out its component parts.
+ my $enc_obj = new Email::MIME($encrypted);
+ $email->header_obj_set($enc_obj->header_obj());
+ $email->parts_set([]);
+ $email->body_set($enc_obj->body());
+ $email->content_type_set('application/pkcs7-mime');
+ $email->charset_set('UTF-8') if Bugzilla->params->{'utf8'};
+ }
+ else {
+ $email->body_set('Error during Encryption: ' . $@);
+ }
+ }
+ else {
+ # No encryption key provided; send a generic, safe email.
+ my $template = Bugzilla->template;
+ my $message;
+ my $vars = {
+ 'urlbase' => correct_urlbase(),
+ 'bug_id' => $bug_id,
+ 'maintainer' => Bugzilla->params->{'maintainer'}
+ };
+
+ $template->process('account/email/encryption-required.txt.tmpl',
+ $vars, \$message)
+ || ThrowTemplateError($template->error());
+
+ $email->parts_set([]);
+ $email->content_type_set('text/plain');
+ $email->body_set($message);
+ }
+
+ if ($sanitise_subject) {
+ # This is designed to still work if the admin changes the word
+ # 'bug' to something else. However, it could break if they change
+ # the format of the subject line in another way.
+ my $new = $add_new ? ' New:' : '';
+ my $product = $email->header('X-Bugzilla-Product');
+ my $component = $email->header('X-Bugzilla-Component');
+ # Note: the $bug_id is required within the parentheses in order to keep
+ # gmail's threading algorithm happy.
+ $subject =~ s/($bug_id\])\s+(.*)$/$1$new (Secure bug $bug_id in $product :: $component)/;
+ $email->header_set('Subject', $subject);
+ }
+}
+
+sub _pgp_encrypt {
+ my ($pgp, $text) = @_;
+ # "@" matches every key in the public key ring, which is fine,
+ # because there's only one key in our keyring.
+ #
+ # We use the CAST5 cipher because the Rijndael (AES) module doesn't
+ # like us for some reason I don't have time to debug fully.
+ # ("key must be an untainted string scalar")
+ my $encrypted = $pgp->encrypt(Data => $text,
+ Recipients => "@",
+ Cipher => 'CAST5',
+ Armour => 1);
+ if (!defined $encrypted) {
+ return 'Error during Encryption: ' . $pgp->errstr;
+ }
+ return $encrypted;
+}
+
+# Insert the subject into the part's body, as the subject of the message will
+# be sanitised.
+# XXX this incorrectly assumes all parts of the message are the body
+# we should only alter parts who's parent is multipart/alternative
+sub _insert_subject {
+ my ($part, $subject) = @_;
+ my $content_type = $part->content_type or return;
+ if ($content_type =~ /^text\/plain/) {
+ if (!is_7bit_clean($subject)) {
+ $part->encoding_set('quoted-printable');
+ }
+ $part->body_str_set("Subject: $subject\015\012\015\012" . $part->body_str);
+ }
+ elsif ($content_type =~ /^text\/html/) {
+ my $tree = HTML::Tree->new->parse_content($part->body_str);
+ my $body = $tree->look_down(qw(_tag body));
+ $body->unshift_content(['div', "Subject: $subject"], ['br']);
+ _set_body_from_tree($part, $tree);
+ }
+}
+
+# Copied from Bugzilla/Mailer as this extension runs before
+# this code there and Mailer.pm will no longer see the original
+# message.
+sub _fix_part {
+ my ($part) = @_;
+ return if $part->parts > 1; # Top-level
+ my $content_type = $part->content_type || '';
+ $content_type =~ /charset=['"](.+)['"]/;
+ # If no charset is defined or is the default us-ascii,
+ # then we encode the email to UTF-8 if Bugzilla has utf8 enabled.
+ # XXX - This is a hack to workaround bug 723944.
+ if (!$1 || $1 eq 'us-ascii') {
+ my $body = $part->body;
+ if (Bugzilla->params->{'utf8'}) {
+ $part->charset_set('UTF-8');
+ # encoding_set works only with bytes, not with utf8 strings.
+ my $raw = $part->body_raw;
+ if (utf8::is_utf8($raw)) {
+ utf8::encode($raw);
+ $part->body_set($raw);
+ }
+ }
+ $part->encoding_set('quoted-printable') if !is_7bit_clean($body);
+ }
+}
+
+sub _filter_bug_links {
+ my ($email) = @_;
+ $email->walk_parts(sub {
+ my $part = shift;
+ my $content_type = $part->content_type;
+ return if !$content_type || $content_type !~ /text\/html/;
+ my $tree = HTML::Tree->new->parse_content($part->body);
+ my @links = $tree->look_down( _tag => q{a}, class => qr/bz_bug_link/ );
+ my $updated = 0;
+ foreach my $link (@links) {
+ my $href = $link->attr('href');
+ my ($bug_id) = $href =~ /\Qshow_bug.cgi?id=\E(\d+)/;
+ my $bug = new Bugzilla::Bug($bug_id);
+ if ($bug && _should_secure_bug($bug)) {
+ $link->attr('title', '(secure bug)');
+ $link->attr('class', 'bz_bug_link');
+ $updated = 1;
+ }
+ }
+ if ($updated) {
+ _set_body_from_tree($part, $tree);
+ }
+ });
+}
+
+sub _set_body_from_tree {
+ my ($part, $tree) = @_;
+ $part->body_set($tree->as_HTML);
+ $part->charset_set('UTF-8') if Bugzilla->params->{'utf8'};
+ $part->encoding_set('quoted-printable');
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/SecureMail/README b/extensions/SecureMail/README
new file mode 100644
index 000000000..ac3484291
--- /dev/null
+++ b/extensions/SecureMail/README
@@ -0,0 +1,8 @@
+This extension should be placed in a directory called "SecureMail" in the
+Bugzilla extensions/ directory. After installing it, remove the file
+"disabled" (if present) and then run checksetup.pl.
+
+Instructions for user key formats:
+
+S/MIME Keys must be in PEM format - i.e. Base64-encoded text, with BEGIN CERTIFICATE
+PGP keys must be ASCII-armoured - i.e. text, with BEGIN PGP PUBLIC KEY.
diff --git a/extensions/SecureMail/template/en/default/account/email/encryption-required.txt.tmpl b/extensions/SecureMail/template/en/default/account/email/encryption-required.txt.tmpl
new file mode 100644
index 000000000..f3710bb17
--- /dev/null
+++ b/extensions/SecureMail/template/en/default/account/email/encryption-required.txt.tmpl
@@ -0,0 +1,15 @@
+This email would have contained sensitive information, but you have not set
+a PGP/GPG key or SMIME certificate in the "Secure Mail" section of your user
+preferences.
+
+[% IF bug_id %]
+In order to receive the full text of similar mails in the future, please
+go to:
+[%+ urlbase %]userprefs.cgi?tab=securemail
+and provide a key or certificate.
+
+You can see this bug's current state at:
+[%+ urlbase %]show_bug.cgi?id=[% bug_id %]
+[% ELSE %]
+You will have to contact [% maintainer %] to reset your password.
+[% END %]
diff --git a/extensions/SecureMail/template/en/default/account/email/securemail-test.txt.tmpl b/extensions/SecureMail/template/en/default/account/email/securemail-test.txt.tmpl
new file mode 100644
index 000000000..e4f4c9242
--- /dev/null
+++ b/extensions/SecureMail/template/en/default/account/email/securemail-test.txt.tmpl
@@ -0,0 +1,23 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+From: [% Param('mailfrom') %]
+To: [% to_user %]
+Subject: [% terms.Bugzilla %] SecureMail Test Email
+X-Bugzilla-Type: securemail-test
+
+Congratulations! If you can read this, then your SecureMail encryption
+key uploaded to [% terms.Bugzilla %] is working properly.
+
+To update your SecureMail preferences at any time, please go to:
+[%+ urlbase %]userprefs.cgi?tab=securemail
+
+Sincerely,
+Your Friendly [% terms.Bugzilla %] Administrator
diff --git a/extensions/SecureMail/template/en/default/account/prefs/securemail.html.tmpl b/extensions/SecureMail/template/en/default/account/prefs/securemail.html.tmpl
new file mode 100644
index 000000000..db595a23f
--- /dev/null
+++ b/extensions/SecureMail/template/en/default/account/prefs/securemail.html.tmpl
@@ -0,0 +1,40 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Corporation.
+ # Portions created by the Initial Developer are Copyright (C) 2008 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
+ #%]
+
+[% IF test_email_sent %]
+ <div id="message">
+ An encrypted test email has been sent to your address.
+ </div>
+[% END %]
+
+<p>Some [% terms.bugs %] in this [% terms.Bugzilla %] are in groups the administrator has
+deemed 'secure'. This means emails containing information about those [% terms.bugs %]
+will only be sent encrypted. Enter your PGP/GPG public key or
+SMIME certificate here to receive full update emails for such [% terms.bugs %].</p>
+
+<p>If you are a member of a secure group, or if you enter a key here, your password reset email will also be sent to you encrypted. If you are a member of a secure group and do not enter a key, you will not be able to reset your password without the assistance of an administrator.</p>
+
+<p><a href="page.cgi?id=securemail/help.html">More help is available</a>.</p>
+
+[% Hook.process('moreinfo') %]
+
+<textarea id="public_key" name="public_key" cols="72" rows="12">
+ [%- public_key FILTER html %]</textarea>
+
+<p>Submitting valid changes will automatically send an encrypted test email to your address.</p>
diff --git a/extensions/SecureMail/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl b/extensions/SecureMail/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
new file mode 100644
index 000000000..70a40e592
--- /dev/null
+++ b/extensions/SecureMail/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
@@ -0,0 +1,28 @@
+[%# -*- Mode: perl; indent-tabs-mode: nil -*-
+ #
+ # The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla SecureMail Extension
+ #
+ # The Initial Developer of the Original Code is Mozilla.
+ # Portions created by Mozilla are Copyright (C) 2008 Mozilla Corporation.
+ # All Rights Reserved.
+ #
+ # Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
+ # Gervase Markham <gerv@gerv.net>
+ #%]
+
+[% tabs = tabs.import([{
+ name => "securemail",
+ label => "Secure Mail",
+ link => "userprefs.cgi?tab=securemail",
+ saveable => 1
+ }]) %]
diff --git a/extensions/SecureMail/template/en/default/hook/admin/groups/create-field.html.tmpl b/extensions/SecureMail/template/en/default/hook/admin/groups/create-field.html.tmpl
new file mode 100644
index 000000000..27c644d02
--- /dev/null
+++ b/extensions/SecureMail/template/en/default/hook/admin/groups/create-field.html.tmpl
@@ -0,0 +1,25 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Corporation.
+ # Portions created by the Initial Developer are Copyright (C) 2008 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
+ #%]
+<tr>
+ <th>Secure Bugmail:</th>
+ <td colspan="3">
+ <input type="checkbox" id="secure_mail" name="secure_mail"
+ [% ' checked="checked"' IF group.secure_mail %]>
+ </td>
+</tr>
diff --git a/extensions/SecureMail/template/en/default/hook/admin/groups/edit-field.html.tmpl b/extensions/SecureMail/template/en/default/hook/admin/groups/edit-field.html.tmpl
new file mode 100644
index 000000000..253fed29e
--- /dev/null
+++ b/extensions/SecureMail/template/en/default/hook/admin/groups/edit-field.html.tmpl
@@ -0,0 +1,27 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Corporation.
+ # Portions created by the Initial Developer are Copyright (C) 2008 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
+ #%]
+[% IF group.is_bug_group || group.name == Param('insidergroup') %]
+ <tr>
+ <th>Secure Bugmail:</th>
+ <td>
+ <input type="checkbox" id="secure_mail" name="secure_mail"
+ [% ' checked="checked"' IF group.secure_mail %]>
+ </td>
+ </tr>
+[% END %]
diff --git a/extensions/SecureMail/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/SecureMail/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..46b093674
--- /dev/null
+++ b/extensions/SecureMail/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,27 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Corporation.
+ # Portions created by the Initial Developer are Copyright (C) 2008 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
+ #%]
+
+[% IF error == "securemail_invalid_key" %]
+ [% title = "Invalid Public Key" %]
+ We were unable to read the public key that you entered. Make sure
+ that you are entering either an ASCII-armored PGP/GPG public key,
+ including the "BEGIN PGP PUBLIC KEY BLOCK" and "END PGP PUBLIC KEY BLOCK"
+ lines, or a PEM format (Base64-encoded X.509) S/MIME key, including the
+ BEGIN CERTIFICATE and END CERTIFICATE lines.<br><br>[% errstr FILTER html %]
+[% END %]
diff --git a/extensions/SecureMail/template/en/default/pages/securemail/help.html.tmpl b/extensions/SecureMail/template/en/default/pages/securemail/help.html.tmpl
new file mode 100644
index 000000000..e6ef02927
--- /dev/null
+++ b/extensions/SecureMail/template/en/default/pages/securemail/help.html.tmpl
@@ -0,0 +1,130 @@
+[%#
+ # The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla SecureMail Extension.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation.
+ # Portions created by Mozilla are Copyright (C) 2008 Mozilla Foundation.
+ # All Rights Reserved.
+ #
+ # Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
+ # Gervase Markham <gerv@gerv.net>
+ # Dave Lawrence <dkl@mozilla.com>
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "SecureMail Help"
+%]
+
+[% terms.Bugzilla %] considers certain groups as "secure". If a [% terms.bug %] is in one of those groups, [% terms.Bugzilla %] will not send unencrypted
+email about it. To receive encrypted email rather than just a "something changed" placeholder, you must provide either
+a S/MIME or a GPG/PGP key on the <a href="[% urlbase FILTER none %]userprefs.cgi?tab=securemail">SecureMail preferences tab</a>.<br>
+<br>
+In addition, if you have uploaded a S/MIME or GPG/PGP key using the <a href="[% urlbase FILTER none %]userprefs.cgi?tab=securemail">
+SecureMail preferences tab</a>, if you request your password to be reset, [% terms.Bugzilla %] will send the reset email encrypted and you will
+be required to decrypt it to view the reset instructions.
+
+<h2>S/MIME</h2>
+
+<b>S/MIME Keys must be in PEM format - i.e. Base64-encoded text, with the first line containing BEGIN CERTIFICATE.</b></p>
+
+<p>
+S/MIME certificates can be obtained from a number of providers. You can get a free one from <a href="https://www.startssl.com/?app=12">StartCom</a>.
+Once you have it, <a href="https://www.startssl.com/?app=25#52">export it from your browser as a .p12 file and import it into your mail client</a>.
+You'll need to provide a password when you export - pick a strong one, and then back up the .p12 file somewhere safe.</p>
+
+<p>Import on Thunderbird as follows:</p>
+
+<ul>
+<li>Open Preferences in Thunderbird.</li>
+<li>Activate the Advanced pane.</li>
+<li>Activate the Certificates tab.</li>
+<li>Press the button View Certificates.</li>
+<li>Press the Import button.</li>
+<li>Open your .p12 file.</li>
+<li>Enter the password for unlocking the .p12 if asked.</li>
+</ul>
+
+<p>
+Then, you need to convert it to a .pem file. Here are two possible ways to do this.</p>
+
+<h3>Thunderbird</h3>
+
+<ul>
+<li>Open Preferences in Thunderbird.</li>
+<li>Activate the Advanced pane.</li>
+<li>Activate the Certificates tab.</li>
+<li>Press the button View Certificates.</li>
+<li>Select the line in the tree widget that represents the certificate you imported.</li>
+<li>Press the View button.</li>
+<li>Activate the Details tab.</li>
+<li>Press the Export button.</li>
+<li>Choose where to save the .pem file.</li>
+</ul>
+
+<p>Paste the contents of the .pem file into the SecureMail text field in [% terms.Bugzilla %].</p>
+
+<h3>OpenSSL</h3>
+
+<p>Or, if you have OpenSSL installed, do the following:</p>
+
+<p>
+<code>openssl pkcs12 -in certificate.p12 -out certificate.pem -nodes -nokeys</code></p>
+
+<p>
+Open the .pem file in a text editor. You can recognise the public key because
+it starts "BEGIN CERTIFICATE" and ends "END CERTIFICATE" and
+has an appropriate friendly name (e.g. "StartCom Free Certificate Member's StartCom Ltd. ID").</p>
+
+<p>Paste the contents of the .pem file into the SecureMail text field in [% terms.Bugzilla %].</p>
+
+<h2>PGP</h2>
+
+<b>PGP keys must be ASCII-armoured - i.e. text, with the first line containing BEGIN PGP PUBLIC KEY.</b></p>
+
+<p>
+If you already have your own PGP key in a keyring, skip straight to step 3. Otherwise:</p>
+
+<ol>
+
+<li>Install the GPG suite of utilities for your operating system, either using your package manager or downloaded from <a href="http://www.gnupg.org/download/index.en.html">gnupg.org</a>.</p>
+
+<li><p>Generate a private key.</p>
+
+<p><code>gpg --gen-key</code></p>
+
+<p>
+You’ll have to answer several questions:</p>
+
+<p>
+<ul>
+ <li>What kind and size of key you want; the defaults are probably good enough.</li>
+ <li>How long the key should be valid; you can safely choose a non-expiring key.</li>
+ <li>Your real name and e-mail address; these are necessary for identifying your key in a larger set of keys.</li>
+ <li>A comment for your key; the comment can be empty.</li>
+ <li>A passphrase. Whatever you do, don’t forget it! Your key, and all your encrypted files, will be useless if you do.</li>
+</ul>
+
+<li><p>Generate an ASCII version of your public key.</p>
+
+<p><code>gpg --armor --output pubkey.txt --export 'Your Name'</code></p>
+
+<p>Paste the contents of pubkey.txt into the SecureMail text field in [% terms.Bugzilla %].
+
+<li>Configure your email client to use your associated private key to decrypt the encrypted emails. For Thunderbird, you need the <a href="https://addons.mozilla.org/en-us/thunderbird/addon/enigmail/">Enigmail</a> extension.</p>
+</ol>
+
+<p>
+Further reading: <a href="http://www.madboa.com/geek/gpg-quickstart">GPG Quickstart</a>.
+
+[% PROCESS global/footer.html.tmpl %]
+
+
diff --git a/extensions/ShadowBugs/Config.pm b/extensions/ShadowBugs/Config.pm
new file mode 100644
index 000000000..6999edaf3
--- /dev/null
+++ b/extensions/ShadowBugs/Config.pm
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ShadowBugs;
+use strict;
+
+use constant NAME => 'ShadowBugs';
+use constant REQUIRED_MODULES => [];
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/ShadowBugs/Extension.pm b/extensions/ShadowBugs/Extension.pm
new file mode 100644
index 000000000..a9a1e0861
--- /dev/null
+++ b/extensions/ShadowBugs/Extension.pm
@@ -0,0 +1,99 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ShadowBugs;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Bug;
+use Bugzilla::Error;
+use Bugzilla::Field;
+use Bugzilla::User;
+
+our $VERSION = '1';
+
+BEGIN {
+ *Bugzilla::is_cf_shadow_bug_hidden = \&_is_cf_shadow_bug_hidden;
+ *Bugzilla::Bug::cf_shadow_bug_obj = \&_cf_shadow_bug_obj;
+}
+
+# Determine if the shadow-bug / shadowed-by fields are visibile on the
+# specified bug.
+sub _is_cf_shadow_bug_hidden {
+ my ($self, $bug) = @_;
+
+ # completely hide unless you're a member of the right group
+ return 1 unless Bugzilla->user->in_group('can_shadow_bugs');
+
+ my $is_public = Bugzilla::User->new()->can_see_bug($bug->id);
+ if ($is_public) {
+ # hide on public bugs, unless it's shadowed
+ my $related = $bug->related_bugs(Bugzilla->process_cache->{shadow_bug_field});
+ return 1 if !@$related;
+ }
+}
+
+sub _cf_shadow_bug_obj {
+ my ($self) = @_;
+ return unless $self->cf_shadow_bug;
+ return $self->{cf_shadow_bug_obj} ||= Bugzilla::Bug->new($self->cf_shadow_bug);
+}
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my $file = $args->{'file'};
+ my $vars = $args->{'vars'};
+
+ Bugzilla->process_cache->{shadow_bug_field} ||= Bugzilla::Field->new({ name => 'cf_shadow_bug' });
+
+ return unless Bugzilla->user->in_group('can_shadow_bugs');
+ return unless
+ $file eq 'bug/edit.html.tmpl'
+ || $file eq 'bug/show.html.tmpl'
+ || $file eq 'bug/show-header.html.tmpl';
+ my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'};
+ return unless $bug && $bug->cf_shadow_bug;
+ $vars->{is_shadow_bug} = 1;
+
+ if ($file eq 'bug/edit.html.tmpl') {
+ # load comments from other bug
+ $vars->{shadow_comments} = $bug->cf_shadow_bug_obj->comments;
+ }
+}
+
+sub bug_end_of_update {
+ my ($self, $args) = @_;
+
+ # don't allow shadowing non-public bugs
+ if (exists $args->{changes}->{cf_shadow_bug}) {
+ my ($old_id, $new_id) = @{ $args->{changes}->{cf_shadow_bug} };
+ if ($new_id) {
+ if (!Bugzilla::User->new()->can_see_bug($new_id)) {
+ ThrowUserError('illegal_shadow_bug_public', { id => $new_id });
+ }
+ }
+ }
+
+ # if a shadow bug is made public, clear the shadow_bug field
+ if (exists $args->{changes}->{bug_group}) {
+ my $bug = $args->{bug};
+ return unless my $shadow_id = $bug->cf_shadow_bug;
+ my $is_public = Bugzilla::User->new()->can_see_bug($bug->id);
+ if ($is_public) {
+ Bugzilla->dbh->do(
+ "UPDATE bugs SET cf_shadow_bug=NULL WHERE bug_id=?",
+ undef, $bug->id);
+ LogActivityEntry($bug->id, 'cf_shadow_bug', $shadow_id, '',
+ Bugzilla->user->id, $args->{timestamp});
+
+ }
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/ShadowBugs/disabled b/extensions/ShadowBugs/disabled
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/extensions/ShadowBugs/disabled
diff --git a/extensions/ShadowBugs/template/en/default/hook/bug/comments-aftercomments.html.tmpl b/extensions/ShadowBugs/template/en/default/hook/bug/comments-aftercomments.html.tmpl
new file mode 100644
index 000000000..d8dae521a
--- /dev/null
+++ b/extensions/ShadowBugs/template/en/default/hook/bug/comments-aftercomments.html.tmpl
@@ -0,0 +1,70 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS is_shadow_bug %]
+
+[% public_bug = bug.cf_shadow_bug_obj %]
+[% count = 0 %]
+[% FOREACH comment = shadow_comments %]
+ [% IF count >= start_at %]
+ [% PROCESS a_comment %]
+ [% END %]
+ [% count = count + increment %]
+[% END %]
+
+[% BLOCK a_comment %]
+ [% RETURN IF comment.is_private AND NOT (user.is_insider || user.id == comment.author.id) %]
+ [% comment_text = comment.body_full %]
+ [% RETURN IF comment_text == '' %]
+
+ <div id="pc[% count %]" class="bz_comment[% " bz_private" IF comment.is_private %]
+ shadow_bug_comment bz_default_hidden
+ [% " bz_first_comment" IF count == description %]">
+ [% IF count == description %]
+ [% class_name = "bz_first_comment_head" %]
+ [% comment_label = "Public Description" %]
+ [% ELSE %]
+ [% class_name = "bz_comment_head" %]
+ [% comment_label = "Public Comment " _ count %]
+ [% END %]
+
+ <div class="[% class_name FILTER html %]">
+ <span class="bz_comment_number">
+ <a href="show_bug.cgi?id=[% public_bug.bug_id FILTER none %]#c[% count %]">
+ [%- comment_label FILTER html %]</a>
+ </span>
+
+ <span class="bz_comment_user">
+ [% commenter_id = comment.author.id %]
+ [% UNLESS user_cache.$commenter_id %]
+ [% user_cache.$commenter_id = BLOCK %]
+ [% INCLUDE global/user.html.tmpl who = comment.author %]
+ [% END %]
+ [% END %]
+ [% user_cache.$commenter_id FILTER none %]
+ [% Hook.process('user', 'bug/comments.html.tmpl') %]
+ </span>
+
+ <span class="bz_comment_user_images">
+ [% FOREACH group = comment.author.groups_with_icon %]
+ <img src="[% group.icon_url FILTER html %]"
+ alt="[% group.name FILTER html %]"
+ title="[% group.name FILTER html %] - [% group.description FILTER html %]">
+ [% END %]
+ </span>
+
+ <span class="bz_comment_time">
+ [%+ comment.creation_ts FILTER time %]
+ </span>
+ </div>
+
+<pre class="bz_comment_text">
+ [%- comment_text FILTER quoteUrls(public_bug, comment) -%]
+</pre>
+ </div>
+[% END %]
diff --git a/extensions/ShadowBugs/template/en/default/hook/bug/edit-after_comment_textarea.html.tmpl b/extensions/ShadowBugs/template/en/default/hook/bug/edit-after_comment_textarea.html.tmpl
new file mode 100644
index 000000000..9873ea3d7
--- /dev/null
+++ b/extensions/ShadowBugs/template/en/default/hook/bug/edit-after_comment_textarea.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS is_shadow_bug %]
+
+<br>
+<a href="show_bug.cgi?id=[% bug.cf_shadow_bug FILTER none %]#comment">Add public comment</a>
+
diff --git a/extensions/ShadowBugs/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl b/extensions/ShadowBugs/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
new file mode 100644
index 000000000..8e8327ef2
--- /dev/null
+++ b/extensions/ShadowBugs/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
@@ -0,0 +1,27 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN IF Bugzilla.is_cf_shadow_bug_hidden(bug) %]
+[% field = Bugzilla.process_cache.shadow_bug_field %]
+[% shadowed_by = bug.related_bugs(field).pop %]
+<tr>
+ [% IF shadowed_by && user.can_see_bug(shadowed_by) %]
+ <th class="field_label">
+ [% field.reverse_desc FILTER html %]:
+ </th>
+ <td>
+ [% shadowed_by.id FILTER bug_link(shadowed_by, use_alias => 1) FILTER none %][% " " %]
+ </td>
+ [% ELSE %]
+ [% PROCESS bug/field.html.tmpl
+ value = bug.cf_shadow_bug
+ editable = bug.check_can_change_field(field.name, 0, 1)
+ no_tds = false
+ value_span = 2 %]
+ [% END %]
+</tr>
diff --git a/extensions/ShadowBugs/template/en/default/hook/bug/edit-custom_field.html.tmpl b/extensions/ShadowBugs/template/en/default/hook/bug/edit-custom_field.html.tmpl
new file mode 100644
index 000000000..4389b27ad
--- /dev/null
+++ b/extensions/ShadowBugs/template/en/default/hook/bug/edit-custom_field.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% field.hidden = field.name == 'cf_shadow_bug' %]
diff --git a/extensions/ShadowBugs/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/ShadowBugs/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..5786b3df6
--- /dev/null
+++ b/extensions/ShadowBugs/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF is_shadow_bug %]
+ [% style_urls.push('extensions/ShadowBugs/web/style.css') %]
+ [% javascript_urls.push('extensions/ShadowBugs/web/shadow-bugs.js') %]
+[% END %]
diff --git a/extensions/ShadowBugs/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/ShadowBugs/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..2e7695dbb
--- /dev/null
+++ b/extensions/ShadowBugs/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,14 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "illegal_shadow_bug_public" %]
+ [% title = "Invalid Shadow " _ terms.Bug %]
+ You cannot shadow [% terms.bug %] [%+ id FILTER html %] because it is not a
+ public [% terms.bug %].
+[% END %]
+
diff --git a/extensions/ShadowBugs/web/shadow-bugs.js b/extensions/ShadowBugs/web/shadow-bugs.js
new file mode 100644
index 000000000..ff320e117
--- /dev/null
+++ b/extensions/ShadowBugs/web/shadow-bugs.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+var shadow_bug = {
+ init: function() {
+ var Dom = YAHOO.util.Dom;
+ var comment_divs = Dom.getElementsByClassName('bz_comment', 'div', 'comments');
+ var comments = new Array();
+ for (var i = 0, l = comment_divs.length; i < l; i++) {
+ var time_spans = Dom.getElementsByClassName('bz_comment_time', 'span', comment_divs[i]);
+ if (!time_spans.length) continue;
+ var date = this.parse_date(time_spans[0].innerHTML);
+ if (!date) continue;
+
+ var comment = {};
+ comment.div = comment_divs[i];
+ comment.date = date;
+ comment.shadow = Dom.hasClass(comment.div, 'shadow_bug_comment');
+ comments.push(comment);
+ }
+
+ for (var i = 0, l = comments.length; i < l; i++) {
+ if (!comments[i].shadow) continue;
+ for (var j = 0, jl = comments.length; j < jl; j++) {
+ if (comments[j].shadow) continue;
+ if (comments[j].date > comments[i].date) {
+ comments[j].div.parentNode.insertBefore(comments[i].div, comments[j].div);
+ break;
+ }
+ }
+ Dom.removeClass(comments[i].div, 'bz_default_hidden');
+ }
+
+ Dom.get('comment').placeholder = 'Add non-public comment';
+ },
+
+ parse_date: function(date) {
+ var matches = date.match(/^\s*(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)/);
+ if (!matches) return;
+ return (matches[1] + matches[2] + matches[3] + matches[4] + matches[5] + matches[6]) + 0;
+ }
+};
+
+
+YAHOO.util.Event.onDOMReady(function() {
+ shadow_bug.init();
+});
diff --git a/extensions/ShadowBugs/web/style.css b/extensions/ShadowBugs/web/style.css
new file mode 100644
index 000000000..0c104130f
--- /dev/null
+++ b/extensions/ShadowBugs/web/style.css
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+.shadow_bug_comment {
+ background: transparent !important;
+}
diff --git a/extensions/SiteMapIndex/Config.pm b/extensions/SiteMapIndex/Config.pm
new file mode 100644
index 000000000..e10d6ec8b
--- /dev/null
+++ b/extensions/SiteMapIndex/Config.pm
@@ -0,0 +1,36 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Sitemap Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Everything Solved, Inc.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Max Kanat-Alexander <mkanat@bugzilla.org>
+# Dave Lawrence <dkl@mozilla.com>
+
+package Bugzilla::Extension::SiteMapIndex;
+use strict;
+
+use constant NAME => 'SiteMapIndex';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'IO-Compress-Gzip',
+ module => 'IO::Compress::Gzip',
+ version => 0,
+ }
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/SiteMapIndex/Extension.pm b/extensions/SiteMapIndex/Extension.pm
new file mode 100644
index 000000000..4cc384b48
--- /dev/null
+++ b/extensions/SiteMapIndex/Extension.pm
@@ -0,0 +1,157 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Sitemap Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Everything Solved, Inc.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Max Kanat-Alexander <mkanat@bugzilla.org>
+# Dave Lawrence <dkl@mozilla.com>
+
+package Bugzilla::Extension::SiteMapIndex;
+use strict;
+use base qw(Bugzilla::Extension);
+
+our $VERSION = '1.0';
+
+use Bugzilla::Constants qw(bz_locations ON_WINDOWS);
+use Bugzilla::Util qw(correct_urlbase get_text);
+use Bugzilla::Install::Filesystem;
+
+use Bugzilla::Extension::SiteMapIndex::Constants;
+use Bugzilla::Extension::SiteMapIndex::Util;
+
+use DateTime;
+use IO::File;
+use POSIX;
+
+#########
+# Pages #
+#########
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my ($vars, $file) = @$args{qw(vars file)};
+
+ return if $file ne 'global/header.html.tmpl';
+ return unless (exists $vars->{bug} || exists $vars->{bugs});
+ my $bugs = exists $vars->{bugs} ? $vars->{bugs} : [$vars->{bug}];
+ return if ref $bugs ne 'ARRAY';
+
+ foreach my $bug (@$bugs) {
+ if (!bug_is_ok_to_index($bug)) {
+ $vars->{sitemap_noindex} = 1;
+ last;
+ }
+ }
+}
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my $page = $args->{page_id};
+
+ if ($page =~ m{^sitemap/sitemap\.}) {
+ my $map = generate_sitemap(__PACKAGE__->NAME);
+ print Bugzilla->cgi->header('text/xml');
+ print $map;
+ exit;
+ }
+}
+
+################
+# Installation #
+################
+
+sub install_before_final_checks {
+ my ($self) = @_;
+ if (!correct_urlbase()) {
+ print STDERR get_text('sitemap_no_urlbase'), "\n";
+ return;
+ }
+ if (Bugzilla->params->{'requirelogin'}) {
+ print STDERR get_text('sitemap_requirelogin'), "\n";
+ return;
+ }
+
+ $self->_fix_robots_txt();
+}
+
+sub install_filesystem {
+ my ($self, $args) = @_;
+ my $create_dirs = $args->{'create_dirs'};
+ my $recurse_dirs = $args->{'recurse_dirs'};
+ my $htaccess = $args->{'htaccess'};
+
+ # Create the sitemap directory to store the index and sitemap files
+ my $sitemap_path = bz_locations->{'datadir'} . "/" . __PACKAGE__->NAME;
+
+ $create_dirs->{$sitemap_path} = Bugzilla::Install::Filesystem::DIR_CGI_WRITE
+ | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE;
+
+ $recurse_dirs->{$sitemap_path} = {
+ files => Bugzilla::Install::Filesystem::CGI_WRITE
+ | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE,
+ dirs => Bugzilla::Install::Filesystem::DIR_CGI_WRITE
+ | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE
+ };
+
+ # Create a htaccess file that allows the sitemap files to be served out
+ $htaccess->{"$sitemap_path/.htaccess"} = {
+ perms => Bugzilla::Install::Filesystem::WS_SERVE,
+ contents => <<EOT
+# Allow access to sitemap files created by the SiteMapIndex extension
+<FilesMatch ^sitemap.*\\.xml(.gz)?\$>
+ Allow from all
+</FilesMatch>
+Deny from all
+EOT
+ };
+}
+
+sub _fix_robots_txt {
+ my ($self) = @_;
+ my $cgi_path = bz_locations()->{'cgi_path'};
+ my $robots_file = "$cgi_path/robots.txt";
+ my $current_fh = new IO::File("$cgi_path/robots.txt", 'r');
+ if (!$current_fh) {
+ warn "$robots_file: $!";
+ return;
+ }
+
+ my $current_contents;
+ { local $/; $current_contents = <$current_fh> }
+ $current_fh->close();
+
+ return if $current_contents =~ /^Sitemap:/m;
+ my $backup_name = "$cgi_path/robots.txt.old";
+ print get_text('sitemap_fixing_robots', { current => $robots_file,
+ backup => $backup_name }), "\n";
+ rename $robots_file, $backup_name or die "backup failed: $!";
+
+ my $new_fh = new IO::File($self->package_dir . '/robots.txt', 'r');
+ $new_fh || die "Could not open new robots.txt template file: $!";
+ my $new_contents;
+ { local $/; $new_contents = <$new_fh> }
+ $new_fh->close() || die "Could not close new robots.txt template file: $!";
+
+ my $sitemap_url = correct_urlbase() . SITEMAP_URL;
+ $new_contents =~ s/SITEMAP_URL/$sitemap_url/;
+ $new_fh = new IO::File("$cgi_path/robots.txt", 'w');
+ $new_fh || die "Could not open new robots.txt file: $!";
+ print $new_fh $new_contents;
+ $new_fh->close() || die "Could not close new robots.txt file: $!";
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/SiteMapIndex/lib/Constants.pm b/extensions/SiteMapIndex/lib/Constants.pm
new file mode 100644
index 000000000..fce858121
--- /dev/null
+++ b/extensions/SiteMapIndex/lib/Constants.pm
@@ -0,0 +1,47 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Sitemap Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Everything Solved, Inc.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Max Kanat-Alexander <mkanat@bugzilla.org>
+
+package Bugzilla::Extension::SiteMapIndex::Constants;
+use strict;
+use base qw(Exporter);
+our @EXPORT = qw(
+ SITEMAP_AGE
+ SITEMAP_MAX
+ SITEMAP_DELAY
+ SITEMAP_URL
+);
+
+# This is the amount of hours a sitemap index and it's files are considered
+# valid before needing to be regenerated.
+use constant SITEMAP_AGE => 12;
+
+# This is the largest number of entries that can be in a single sitemap file,
+# per the sitemaps.org standard.
+use constant SITEMAP_MAX => 50_000;
+
+# We only show bugs that are at least 12 hours old, because if somebody
+# files a bug that's a security bug but doesn't protect it, we want to give
+# them time to fix that.
+use constant SITEMAP_DELAY => 12;
+
+use constant SITEMAP_URL => 'page.cgi?id=sitemap/sitemap.xml';
+
+1;
diff --git a/extensions/SiteMapIndex/lib/Util.pm b/extensions/SiteMapIndex/lib/Util.pm
new file mode 100644
index 000000000..5c02a5989
--- /dev/null
+++ b/extensions/SiteMapIndex/lib/Util.pm
@@ -0,0 +1,205 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Sitemap Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Everything Solved, Inc.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Max Kanat-Alexander <mkanat@bugzilla.org>
+# Dave Lawrence <dkl@mozilla.com>
+
+package Bugzilla::Extension::SiteMapIndex::Util;
+use strict;
+use base qw(Exporter);
+our @EXPORT = qw(
+ generate_sitemap
+ bug_is_ok_to_index
+);
+
+use Bugzilla::Extension::SiteMapIndex::Constants;
+
+use Bugzilla::Util qw(correct_urlbase datetime_from url_quote);
+use Bugzilla::Constants qw(bz_locations);
+
+use Scalar::Util qw(blessed);
+use IO::Compress::Gzip qw(gzip $GzipError);
+
+sub too_young_date {
+ my $hours_ago = DateTime->now(time_zone => Bugzilla->local_timezone);
+ $hours_ago->subtract(hours => SITEMAP_DELAY);
+ return $hours_ago;
+}
+
+sub bug_is_ok_to_index {
+ my ($bug) = @_;
+ return 1 unless blessed($bug) && $bug->isa('Bugzilla::Bug') && !$bug->{error};
+ my $creation_ts = datetime_from($bug->creation_ts);
+ return ($creation_ts && $creation_ts lt too_young_date()) ? 1 : 0;
+}
+
+# We put two things in the Sitemap: a list of Browse links for products,
+# and links to bugs.
+sub generate_sitemap {
+ my ($extension_name) = @_;
+
+ # If file is less than SITEMAP_AGE hours old, then read in and send to caller.
+ # If greater, then regenerate and send the new version.
+ my $index_file = bz_locations->{'datadir'} . "/$extension_name/sitemap_index.xml";
+ if (-e $index_file) {
+ my $index_mtime = (stat($index_file))[9];
+ my $index_hours = sprintf("%d", (time() - $index_mtime) / 60 / 60); # in hours
+ if ($index_hours < SITEMAP_AGE) {
+ my $index_fh = new IO::File($index_file, 'r');
+ $index_fh || die "Could not open current sitemap index: $!";
+ my $index_xml;
+ { local $/; $index_xml = <$index_fh> }
+ $index_fh->close() || die "Could not close current sitemap index: $!";
+
+ return $index_xml;
+ }
+ }
+
+ # Set the atime and mtime of the index file to the current time
+ # in case another request is made before we finish.
+ utime(undef, undef, $index_file);
+
+ # Sitemaps must never contain private data.
+ Bugzilla->logout_request();
+ my $user = Bugzilla->user;
+ my $products = $user->get_accessible_products;
+
+ my $num_bugs = SITEMAP_MAX - scalar(@$products);
+ # We do this date math outside of the database because databases
+ # usually do better with a straight comparison value.
+ my $hours_ago = too_young_date();
+
+ # We don't use Bugzilla::Bug objects, because this could be a tremendous
+ # amount of data, and we only want a little. Also, we only display
+ # bugs that are not in any group. We show the last $num_bugs
+ # most-recently-updated bugs.
+ my $dbh = Bugzilla->dbh;
+ my $bug_sth = $dbh->prepare(
+ 'SELECT bugs.bug_id, bugs.delta_ts
+ FROM bugs
+ LEFT JOIN bug_group_map ON bugs.bug_id = bug_group_map.bug_id
+ WHERE bug_group_map.bug_id IS NULL AND creation_ts < ?
+ ' . $dbh->sql_limit($num_bugs, '?'));
+
+ my $filecount = 1;
+ my $filelist = [];
+ my $offset = 0;
+
+ while (1) {
+ my $bugs = [];
+
+ $bug_sth->execute($hours_ago, $offset);
+
+ while (my ($bug_id, $delta_ts) = $bug_sth->fetchrow_array()) {
+ push(@$bugs, { bug_id => $bug_id, delta_ts => $delta_ts });
+ }
+
+ last if !@$bugs;
+
+ # We only need the product links in the first sitemap file
+ $products = [] if $filecount > 1;
+
+ push(@$filelist, _generate_sitemap_file($extension_name, $filecount, $products, $bugs));
+
+ $filecount++;
+ $offset += $num_bugs;
+ }
+
+ # Generate index file
+ return _generate_sitemap_index($extension_name, $filelist);
+}
+
+sub _generate_sitemap_index {
+ my ($extension_name, $filelist) = @_;
+
+ my $dbh = Bugzilla->dbh;
+ my $timestamp = $dbh->selectrow_array(
+ "SELECT " . $dbh->sql_date_format('NOW()', '%Y-%m-%d'));
+
+ my $index_xml = <<END;
+<?xml version="1.0" encoding="UTF-8"?>
+<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+END
+
+ foreach my $filename (@$filelist) {
+ $index_xml .= "
+ <sitemap>
+ <loc>" . correct_urlbase() . "data/$extension_name/$filename</loc>
+ <lastmod>$timestamp</lastmod>
+ </sitemap>
+";
+ }
+
+ $index_xml .= <<END;
+</sitemapindex>
+END
+
+ my $index_file = bz_locations->{'datadir'} . "/$extension_name/sitemap_index.xml";
+ my $index_fh = new IO::File($index_file, 'w');
+ $index_fh || die "Could not open new sitemap index: $!";
+ print $index_fh $index_xml;
+ $index_fh->close() || die "Could not close new sitemap index: $!";
+
+ return $index_xml;
+}
+
+sub _generate_sitemap_file {
+ my ($extension_name, $filecount, $products, $bugs) = @_;
+
+ my $bug_url = correct_urlbase() . 'show_bug.cgi?id=';
+ my $product_url = correct_urlbase() . 'describecomponents.cgi?product=';
+
+ my $sitemap_xml = <<END;
+<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+END
+
+ foreach my $product (@$products) {
+ $sitemap_xml .= "
+ <url>
+ <loc>" . $product_url . url_quote($product->name) . "</loc>
+ <changefreq>daily</changefreq>
+ <priority>0.4</priority>
+ </url>
+";
+ }
+
+ foreach my $bug (@$bugs) {
+ $sitemap_xml .= "
+ <url>
+ <loc>" . $bug_url . $bug->{bug_id} . "</loc>
+ <lastmod>" . datetime_from($bug->{delta_ts}, 'UTC')->iso8601 . 'Z' . "</lastmod>
+ </url>
+";
+ }
+
+ $sitemap_xml .= <<END;
+</urlset>
+END
+
+ # Write the compressed sitemap data to a file in the cgi root so that they can
+ # be accessed by the search engines.
+ my $filename = "sitemap$filecount.xml.gz";
+ gzip \$sitemap_xml => bz_locations->{'datadir'} . "/$extension_name/$filename"
+ || die "gzip failed: $GzipError\n";
+
+ return $filename;
+}
+
+1;
diff --git a/extensions/SiteMapIndex/robots.txt b/extensions/SiteMapIndex/robots.txt
new file mode 100644
index 000000000..74cc63074
--- /dev/null
+++ b/extensions/SiteMapIndex/robots.txt
@@ -0,0 +1,10 @@
+User-agent: *
+Disallow: /*.cgi
+Disallow: /show_bug.cgi*ctype=*
+Allow: /$
+Allow: /index.cgi
+Allow: /page.cgi
+Allow: /show_bug.cgi
+Allow: /describecomponents.cgi
+Allow: /data/SiteMapIndex/sitemap*.xml.gz
+Sitemap: SITEMAP_URL
diff --git a/extensions/SiteMapIndex/template/en/default/hook/global/header-additional_header.html.tmpl b/extensions/SiteMapIndex/template/en/default/hook/global/header-additional_header.html.tmpl
new file mode 100644
index 000000000..682f6093f
--- /dev/null
+++ b/extensions/SiteMapIndex/template/en/default/hook/global/header-additional_header.html.tmpl
@@ -0,0 +1,23 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Initial Developer of the Original Code is Everything Solved, Inc.
+ # Portions created by Everything Solved are Copyright (C) 2010
+ # Everything Solved. All Rights Reserved.
+ #
+ # The Original Code is the Bugzilla Sitemap Extension.
+ #
+ # Contributor(s):
+ # Max Kanat-Alexander <mkanat@bugzilla.org>
+ #%]
+
+[% SET meta_robots = ['noarchive'] %]
+[% meta_robots.push('noindex') IF sitemap_noindex %]
+<meta name="robots" content="[% meta_robots.join(',') FILTER html %]">
diff --git a/extensions/SiteMapIndex/template/en/default/hook/global/messages-messages.html.tmpl b/extensions/SiteMapIndex/template/en/default/hook/global/messages-messages.html.tmpl
new file mode 100644
index 000000000..0d0e9fd74
--- /dev/null
+++ b/extensions/SiteMapIndex/template/en/default/hook/global/messages-messages.html.tmpl
@@ -0,0 +1,37 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Initial Developer of the Original Code is Everything Solved, Inc.
+ # Portions created by Everything Solved are Copyright (C) 2010
+ # Everything Solved. All Rights Reserved.
+ #
+ # The Original Code is the Bugzilla Sitemap Extension.
+ #
+ # Contributor(s):
+ # Max Kanat-Alexander <mkanat@bugzilla.org>
+ #%]
+
+[% IF message_tag == "sitemap_fixing_robots" %]
+ Replacing [% current FILTER html %]. (The old version will be saved
+ as "[% backup FILTER html %]". You can delete the old version if you
+ do not need its contents.)
+
+[% ELSIF message_tag == "sitemap_requirelogin" %]
+ Not updating search engines with your sitemap, because you have the
+ "requirelogin" parameter turned on, and so search engines will not be
+ able to access your sitemap.
+
+[% ELSIF message_tag == "sitemap_no_urlbase" %]
+ You have not yet set the "urlbase" parameter. We cannot update
+ search engines and inform them about your sitemap without a
+ urlbase. Please set the "urlbase" parameter and re-run
+ checksetup.pl.
+
+[% END %]
diff --git a/extensions/Splinter/Config.pm b/extensions/Splinter/Config.pm
new file mode 100644
index 000000000..d36a28922
--- /dev/null
+++ b/extensions/Splinter/Config.pm
@@ -0,0 +1,5 @@
+package Bugzilla::Extension::Splinter;
+use strict;
+use constant NAME => 'Splinter';
+
+__PACKAGE__->NAME;
diff --git a/extensions/Splinter/Extension.pm b/extensions/Splinter/Extension.pm
new file mode 100644
index 000000000..a3d9fe181
--- /dev/null
+++ b/extensions/Splinter/Extension.pm
@@ -0,0 +1,148 @@
+package Bugzilla::Extension::Splinter;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla;
+use Bugzilla::Bug;
+use Bugzilla::Template;
+use Bugzilla::Attachment;
+use Bugzilla::BugMail;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Field;
+use Bugzilla::Util qw(trim detaint_natural);
+
+use Bugzilla::Extension::Splinter::Util;
+
+our $VERSION = '0.1';
+
+BEGIN {
+ *Bugzilla::splinter_review_base = \&get_review_base;
+ *Bugzilla::splinter_review_url = \&_get_review_url;
+}
+
+sub _get_review_url {
+ my ($class, $bug_id, $attach_id) = @_;
+ return get_review_url(Bugzilla::Bug->check({ id => $bug_id, cache => 1 }), $attach_id);
+}
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my ($vars, $page) = @$args{qw(vars page_id)};
+
+ if ($page eq 'splinter.html') {
+ my $user = Bugzilla->user;
+
+ # We can either provide just a bug id to see a list
+ # of prior reviews by the user, or just an attachment
+ # id to go directly to a review page for the attachment.
+ # If both are give they will be checked later to make
+ # sure they are connected.
+
+ my $input = Bugzilla->input_params;
+ if ($input->{'bug'}) {
+ $vars->{'bug_id'} = $input->{'bug'};
+ $vars->{'attach_id'} = $input->{'attachment'};
+ $vars->{'bug'} = Bugzilla::Bug->check({ id => $input->{'bug'}, cache => 1 });
+ }
+
+ if ($input->{'attachment'}) {
+ my $attachment = Bugzilla::Attachment->check({ id => $input->{'attachment'} });
+
+ # Check to see if the user can see the bug this attachment is connected to.
+ Bugzilla::Bug->check($attachment->bug_id);
+ if ($attachment->isprivate
+ && $user->id != $attachment->attacher->id
+ && !$user->is_insider)
+ {
+ ThrowUserError('auth_failure', {action => 'access',
+ object => 'attachment'});
+ }
+
+ # If the user provided both a bug id and an attachment id, they must
+ # be connected to each other
+ if ($input->{'bug'} && $input->{'bug'} != $attachment->bug_id) {
+ ThrowUserError('bug_attach_id_mismatch');
+ }
+
+ # The patch is going to be displayed in a HTML page and if the utf8
+ # param is enabled, we have to encode attachment data as utf8.
+ if (Bugzilla->params->{'utf8'}) {
+ $attachment->data; # load data
+ utf8::decode($attachment->{data});
+ }
+
+ $vars->{'attach_id'} = $attachment->id;
+ $vars->{'attach_data'} = $attachment->data;
+ $vars->{'attach_is_crlf'} = $attachment->{data} =~ /\012\015/ ? 1 : 0;
+ }
+
+ my $field_object = new Bugzilla::Field({ name => 'attachments.status' });
+ my $statuses;
+ if ($field_object) {
+ $statuses = [map { $_->name } @{ $field_object->legal_values }];
+ } else {
+ $statuses = [];
+ }
+ $vars->{'attachment_statuses'} = $statuses;
+ }
+}
+
+
+sub bug_format_comment {
+ my ($self, $args) = @_;
+
+ my $bug = $args->{'bug'};
+ my $regexes = $args->{'regexes'};
+ my $text = $args->{'text'};
+
+ # Add [review] link to the end of "Created attachment" comments
+ #
+ # We need to work around the way that the hook works, which is intended
+ # to avoid overlapping matches, since we *want* an overlapping match
+ # here (the normal handling of "Created attachment"), so we add in
+ # dummy text and then replace in the regular expression we return from
+ # the hook.
+ $$text =~ s~((?:^Created\ |\b)attachment\s*\#?\s*(\d+)(\s\[details\])?)
+ ~(push(@$regexes, { match => qr/__REVIEW__$2/,
+ replace => get_review_link("$2", "[review]") })) &&
+ (attachment_id_is_patch($2) ? "$1 __REVIEW__$2" : $1)
+ ~egmx;
+
+ # And linkify "Review of attachment", this is less of a workaround since
+ # there is no issue with overlap; note that there is an assumption that
+ # there is only one match in the text we are linkifying, since they all
+ # get the same link.
+ my $REVIEW_RE = qr/Review\s+of\s+attachment\s+(\d+)\s*:/;
+
+ if ($$text =~ $REVIEW_RE) {
+ my $attach_id = $1;
+ my $review_link = get_review_link($attach_id, "Review");
+ my $attach_link = Bugzilla::Template::get_attachment_link($attach_id, "attachment $attach_id");
+
+ push(@$regexes, { match => $REVIEW_RE,
+ replace => "$review_link of $attach_link:"});
+ }
+}
+
+sub config_add_panels {
+ my ($self, $args) = @_;
+
+ my $modules = $args->{panel_modules};
+ $modules->{Splinter} = "Bugzilla::Extension::Splinter::Config";
+}
+
+sub mailer_before_send {
+ my ($self, $args) = @_;
+
+ # Post-process bug mail to add review links to bug mail.
+ # It would be nice to be able to hook in earlier in the
+ # process when the email body is being formatted in the
+ # style of the bug-format_comment link for HTML but this
+ # is the only hook available as of Bugzilla-3.4.
+ add_review_links_to_email($args->{'email'});
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/Splinter/lib/Config.pm b/extensions/Splinter/lib/Config.pm
new file mode 100644
index 000000000..95b9f5dfa
--- /dev/null
+++ b/extensions/Splinter/lib/Config.pm
@@ -0,0 +1,46 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Example Plugin.
+#
+# The Initial Developer of the Original Code is Canonical Ltd.
+# Portions created by Canonical Ltd. are Copyright (C) 2008
+# Canonical Ltd. All Rights Reserved.
+#
+# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
+# Bradley Baetz <bbaetz@acm.org>
+# Owen Taylor <otaylor@redhat.com>
+
+package Bugzilla::Extension::Splinter::Config;
+
+use strict;
+use warnings;
+
+use Bugzilla::Config::Common;
+
+our $sortkey = 1350;
+
+sub get_param_list {
+ my ($class) = @_;
+
+ my @param_list = (
+ {
+ name => 'splinter_base',
+ type => 't',
+ default => 'page.cgi?id=splinter.html',
+ },
+ );
+
+ return @param_list;
+}
+
+1;
diff --git a/extensions/Splinter/lib/Util.pm b/extensions/Splinter/lib/Util.pm
new file mode 100644
index 000000000..3c77239a9
--- /dev/null
+++ b/extensions/Splinter/lib/Util.pm
@@ -0,0 +1,163 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Splinter Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Red Hat, Inc.
+# Portions created by Red Hat, Inc. are Copyright (C) 2009
+# Red Hat Inc. All Rights Reserved.
+#
+# Contributor(s):
+# Owen Taylor <otaylor@fishsoup.net>
+
+package Bugzilla::Extension::Splinter::Util;
+
+use strict;
+
+use Bugzilla;
+use Bugzilla::Util;
+
+use base qw(Exporter);
+
+@Bugzilla::Extension::Splinter::Util::EXPORT = qw(
+ attachment_is_visible
+ attachment_id_is_patch
+ get_review_base
+ get_review_url
+ get_review_link
+ add_review_links_to_email
+);
+
+# Validates an attachment ID.
+# Takes a parameter containing the ID to be validated.
+# If the second parameter is true, the attachment ID will be validated,
+# however the current user's access to the attachment will not be checked.
+# Will return false if 1) attachment ID is not a valid number,
+# 2) attachment does not exist, or 3) user isn't allowed to access the
+# attachment.
+#
+# Returns an attachment object.
+# Based on code from attachment.cgi
+sub attachment_id_is_valid {
+ my ($attach_id, $dont_validate_access) = @_;
+
+ # Validate the specified attachment id.
+ detaint_natural($attach_id) || return 0;
+
+ # Make sure the attachment exists in the database.
+ my $attachment = new Bugzilla::Attachment({ id => $attach_id, cache => 1 })
+ || return 0;
+
+ return $attachment
+ if ($dont_validate_access || attachment_is_visible($attachment));
+}
+
+# Checks if the current user can see an attachment
+# Based on code from attachment.cgi
+sub attachment_is_visible {
+ my $attachment = shift;
+
+ $attachment->isa('Bugzilla::Attachment') || return 0;
+
+ return (Bugzilla->user->can_see_bug($attachment->bug->id)
+ && (!$attachment->isprivate
+ || Bugzilla->user->id == $attachment->attacher->id
+ || Bugzilla->user->is_insider));
+}
+
+sub attachment_id_is_patch {
+ my $attach_id = shift;
+ my $attachment = attachment_id_is_valid($attach_id);
+
+ return ($attachment && $attachment->ispatch);
+}
+
+sub get_review_base {
+ my $base = Bugzilla->params->{'splinter_base'};
+ $base =~ s!/$!!;
+ my $urlbase = correct_urlbase();
+ $urlbase =~ s!/$!! if $base =~ "^/";
+ $base = $urlbase . $base;
+ return $base;
+}
+
+sub get_review_url {
+ my ($bug, $attach_id) = @_;
+ my $base = get_review_base();
+ my $bug_id = $bug->id;
+ return $base . ($base =~ /\?/ ? '&' : '?') . "bug=$bug_id&attachment=$attach_id";
+}
+
+sub get_review_link {
+ my ($attach_id, $link_text) = @_;
+
+ my $attachment = attachment_id_is_valid($attach_id);
+
+ if ($attachment && $attachment->ispatch) {
+ return "<a href='" . html_quote(get_review_url($attachment->bug, $attach_id)) .
+ "'>$link_text</a>";
+ }
+ else {
+ return $link_text;
+ }
+}
+
+sub munge_create_attachment {
+ my ($bug, $intro_text, $attach_id, $view_link) = @_;
+
+ if (attachment_id_is_patch($attach_id)) {
+ return ("$intro_text" .
+ " View: $view_link\015\012" .
+ " Review: " . get_review_url($bug, $attach_id, 1) . "\015\012");
+ }
+ else {
+ return ("$intro_text --> ($view_link)");
+ }
+}
+
+# This adds review links into a bug mail before we send it out.
+# Since this is happening after newlines have been converted into
+# RFC-2822 style \r\n, we need handle line ends carefully.
+# (\015 and \012 are used because Perl \n is platform-dependent)
+sub add_review_links_to_email {
+ my $email = shift;
+ my $body = $email->body;
+ my $new_body = 0;
+ my $bug;
+
+ if ($email->header('Subject') =~ /^\[Bug\s+(\d+)\]/
+ && Bugzilla->user->can_see_bug($1))
+ {
+ $bug = Bugzilla::Bug->new({ id => $1, cache => 1 });
+ }
+
+ return unless defined $bug;
+
+ if ($body =~ /Review\s+of\s+attachment\s+\d+\s*:/) {
+ $body =~ s~(Review\s+of\s+attachment\s+(\d+)\s*:)
+ ~"$1\015\012 --> (" . get_review_url($bug, $2, 1) . ")"
+ ~egx;
+ $new_body = 1;
+ }
+
+ if ($body =~ /Created attachment \d+\015\012 --> /) {
+ $body =~ s~(Created\ attachment\ (\d+)\015\012)
+ \ -->\ \(([^\015\012]*)\)[^\015\012]*
+ ~munge_create_attachment($bug, $1, $2, $3)
+ ~egx;
+ $new_body = 1;
+ }
+
+ $email->body_set($body) if $new_body;
+}
+
+1;
diff --git a/extensions/Splinter/template/en/default/admin/params/splinter.html.tmpl b/extensions/Splinter/template/en/default/admin/params/splinter.html.tmpl
new file mode 100644
index 000000000..b28a4bd37
--- /dev/null
+++ b/extensions/Splinter/template/en/default/admin/params/splinter.html.tmpl
@@ -0,0 +1,38 @@
+[%#
+ # The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Example Plugin.
+ #
+ # The Initial Developer of the Original Code is Canonical Ltd.
+ # Portions created by Canonical Ltd. are Copyright (C) 2008
+ # Canonical Ltd. All Rights Reserved.
+ #
+ # Contributor(s): Bradley Baetz <bbaetz@acm.org>
+ # Owen Taylor <otaylor@redhat.com>
+ #%]
+[%
+ title = "Splinter Patch Review"
+ desc = "Configure Splinter"
+%]
+
+[% param_descs = {
+ splinter_base => "This is the base URL for the Splinter patch review page; " _
+ "the default value '/page.cgi?id=splinter.html' works without " _
+ "further configuration, however you may want to internally forward " _
+ "/review to that URL in your web server's configuration and then change " _
+ "this parameter. For example, with the Apache HTTP server, you can add " _
+ "the following lines to the .htaccess for Bugzilla: " _
+ "<pre>" _
+ "RewriteEngine On\n" _
+ "RewriteRule ^review(.*) page.cgi?id=splinter.html\$1 [QSA]" _
+ "</pre>"
+ }
+%]
diff --git a/extensions/Splinter/template/en/default/hook/attachment/edit-action.html.tmpl b/extensions/Splinter/template/en/default/hook/attachment/edit-action.html.tmpl
new file mode 100644
index 000000000..7648e1d76
--- /dev/null
+++ b/extensions/Splinter/template/en/default/hook/attachment/edit-action.html.tmpl
@@ -0,0 +1,25 @@
+[%#
+ # The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Splinter Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is Red Hat, Inc.
+ # Portions created by Red Hat, Inc. are Copyright (C) 2008
+ # Red Hat, Inc. All Rights Reserved.
+ #
+ # Contributor(s): Owen Taylor <otaylor@redhat.com>
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+
+[% IF attachment.ispatch %]
+ &#x0020; |
+ <a href="[% Bugzilla.splinter_review_url(attachment.bug_id, attachment.id) FILTER none %]">Review</a>
+[% END %]
diff --git a/extensions/Splinter/template/en/default/hook/attachment/list-action.html.tmpl b/extensions/Splinter/template/en/default/hook/attachment/list-action.html.tmpl
new file mode 100644
index 000000000..ee793b192
--- /dev/null
+++ b/extensions/Splinter/template/en/default/hook/attachment/list-action.html.tmpl
@@ -0,0 +1,25 @@
+[%#
+ # The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Splinter Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is Red Hat, Inc.
+ # Portions created by Red Hat, Inc. are Copyright (C) 2008
+ # Red Hat, Inc. All Rights Reserved.
+ #
+ # Contributor(s): Owen Taylor <otaylor@redhat.com>
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+
+[% IF attachment.ispatch %]
+ &#x0020; |
+ <a href="[% Bugzilla.splinter_review_url(bugid, attachment.id) FILTER none %]">Review</a>
+[% END %]
diff --git a/extensions/Splinter/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/Splinter/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..17ef5c08f
--- /dev/null
+++ b/extensions/Splinter/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,5 @@
+[% IF error == "bug_attach_id_mismatch" %]
+ [% title = "Bug ID and Attachment ID Mismatch" %]
+ The [% terms.bug %] id and attachment id you provided
+ are not connected to each other.
+[% END %]
diff --git a/extensions/Splinter/template/en/default/hook/request/email-after_summary.txt.tmpl b/extensions/Splinter/template/en/default/hook/request/email-after_summary.txt.tmpl
new file mode 100644
index 000000000..159a63e36
--- /dev/null
+++ b/extensions/Splinter/template/en/default/hook/request/email-after_summary.txt.tmpl
@@ -0,0 +1,9 @@
+[% USE Bugzilla %]
+[% IF flag && flag.status == '?'
+ && (flag.type.name == 'review' || flag.type.name == 'feedback')
+ && attachment && attachment.ispatch %]
+
+Review
+[%+ Bugzilla.splinter_review_url(bug.bug_id, attachment.id) FILTER none %]
+[%- END %]
+
diff --git a/extensions/Splinter/template/en/default/hook/request/queue-after_column.html.tmpl b/extensions/Splinter/template/en/default/hook/request/queue-after_column.html.tmpl
new file mode 100644
index 000000000..a5fc61cea
--- /dev/null
+++ b/extensions/Splinter/template/en/default/hook/request/queue-after_column.html.tmpl
@@ -0,0 +1,4 @@
+[% IF column == 'attachment' && request.ispatch %]
+ &nbsp;
+ <a href="[% Bugzilla.splinter_review_url(request.bug_id, request.attach_id) FILTER none %]">[review]</a>
+[% END %]
diff --git a/extensions/Splinter/template/en/default/pages/splinter.html.tmpl b/extensions/Splinter/template/en/default/pages/splinter.html.tmpl
new file mode 100644
index 000000000..9b759ab6e
--- /dev/null
+++ b/extensions/Splinter/template/en/default/pages/splinter.html.tmpl
@@ -0,0 +1,282 @@
+[%#
+ # The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Splinter Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is Red Hat, Inc.
+ # Portions created by Red Hat, Inc. are Copyright (C) 2008
+ # Red Hat, Inc. All Rights Reserved.
+ #
+ # Contributor(s): Owen Taylor <otaylor@redhat.com>
+ #%]
+
+[% bodyclasses = [] %]
+[% FOREACH group = bug.groups_in %]
+ [% bodyclasses.push("bz_group_$group.name") %]
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Patch Review"
+ header = "Patch Review"
+ style_urls = [ "js/yui/assets/skins/sam/container.css",
+ "js/yui/assets/skins/sam/button.css",
+ "js/yui/assets/skins/sam/datatable.css",
+ "extensions/Splinter/web/splinter.css",
+ "skins/custom/bug_groups.css" ]
+ javascript_urls = [ "js/yui/element/element-min.js",
+ "js/yui/container/container-min.js",
+ "js/yui/button/button-min.js",
+ "js/yui/json/json-min.js",
+ "js/yui/datasource/datasource-min.js",
+ "js/yui/datatable/datatable-min.js",
+ "extensions/Splinter/web/splinter.js",
+ "js/field.js" ]
+ bodyclasses = bodyclasses
+ yui = ['autocomplete']
+%]
+
+[% can_edit = 0 %]
+
+<script type="text/javascript">
+ Splinter.configBase = '[% Bugzilla.splinter_review_base FILTER js %]';
+ Splinter.configBugUrl = '[% urlbase FILTER js %]';
+ Splinter.configHaveExtension = true;
+ Splinter.configHelp = '[% urlbase FILTER js %]page.cgi?id=splinter/help.html';
+ Splinter.configNote = '';
+ Splinter.readOnly = [% user.id FILTER none %] == 0;
+
+ Splinter.configAttachmentStatuses = [
+ [% FOREACH status = attachment_statuses %]
+ '[% status FILTER js %]',
+ [% END %]
+ ];
+
+ Splinter.bugId = Splinter.Utils.isDigits('[% bug_id FILTER js %]') ? parseInt('[% bug_id FILTER js %]') : NaN;
+ Splinter.attachmentId = Splinter.Utils.isDigits('[% attach_id FILTER html %]') ? parseInt('[% attach_id FILTER js %]') : NaN;
+
+ if (!isNaN(Splinter.bugId)) {
+ var theBug = new Splinter.Bug.Bug();
+ theBug.id = parseInt('[% bug.id FILTER js %]');
+ theBug.token = '[% update_token FILTER js %]';
+ theBug.shortDesc = Splinter.Utils.strip('[% bug.short_desc FILTER js %]');
+ theBug.creationDate = Splinter.Bug.parseDate('[% bug.creation_ts FILTER time("%Y-%m-%d %T %z") FILTER js %]');
+ theBug.reporterEmail = Splinter.Utils.strip('[% bug.reporter.email FILTER js %]');
+ theBug.reporterName = Splinter.Utils.strip('[% bug.reporter.name FILTER js %]');
+
+ [% FOREACH comment = bug.comments %]
+ [% NEXT IF comment.is_private && !user.is_insider %]
+ [% NEXT UNLESS comment.thetext.match('(?i)^\s*review\s+of\s+attachment\s+\d+\s*:') %]
+ var comment = new Splinter.Bug.Comment();
+ comment.whoName = Splinter.Utils.strip('[% comment.author.name FILTER js %]');
+ comment.whoEmail = Splinter.Utils.strip('[% comment.author.email FILTER js %]');
+ comment.date = Splinter.Bug.parseDate('[% comment.creation_ts FILTER time("%Y-%m-%d %T %z") FILTER js %]');
+ comment.text = '[% comment.thetext FILTER js %]';
+ theBug.comments.push(comment);
+ [% END %]
+
+ [% FOREACH attachment = bug.attachments %]
+ [% NEXT IF attachment.isprivate && !user.is_insider && attachment.attacher.id != user.id %]
+ [% NEXT IF !attachment.ispatch %]
+ var attachid = parseInt('[% attachment.id FILTER js %]');
+ var attachment = new Splinter.Bug.Attachment('', attachid);
+ [% IF attachment.id == attach_id && attachment.ispatch %]
+ [% flag_types = attachment.flag_types %]
+ [% can_edit = attachment.validate_can_edit %]
+ attachment.data = '[% attach_data FILTER js %]';
+ attachment.token = '[% issue_hash_token([attachment.id, attachment.modification_time]) FILTER js %]';
+ [% END %]
+ attachment.description = Splinter.Utils.strip('[% attachment.description FILTER js %]');
+ attachment.filename = Splinter.Utils.strip('[% attachment.filename FILTER js %]');
+ attachment.contenttypeentry = Splinter.Utils.strip('[% attachment.contenttypeentry FILTER js %]');
+ attachment.date = Splinter.Bug.parseDate('[% attachment.attached FILTER time("%Y-%m-%d %T %z") FILTER js %]');
+ attachment.whoName = Splinter.Utils.strip('[% attachment.attacher.name FILTER js %]');
+ attachment.whoEmail = Splinter.Utils.strip('[% attachment.attacher.email FILTER js %]');
+ attachment.isPatch = [% attachment.ispatch ? 1 : 0 %];
+ attachment.isObsolete = [% attachment.isobsolete ? 1 : 0 %];
+ attachment.isPrivate = [% attachment.isprivate ? 1 : 0 %];
+ attachment.isCRLF = [% attach_is_crlf FILTER none %];
+ theBug.attachments.push(attachment);
+ [% END %]
+
+ Splinter.theBug = theBug;
+ }
+</script>
+
+<!--[if lt IE 7]>
+<p style="border: 1px solid #880000; padding: 1em; background: #ffee88; font-size: 120%;">
+ Splinter Patch Review requires a modern browser, such as
+ <a href="http://www.firefox.com">Firefox</a>, for correct operation.
+</p>
+<![endif]-->
+
+<div id="helpful-links">
+ [% IF user.id %]
+ <a id="allReviewsLink" href="[% Bugzilla.splinter_review_base FILTER none %]">
+ [reviews]</a>
+ [% END %]
+ <a id='helpLink' target='splinterHelp'
+ href="[% urlbase FILTER none %]page.cgi?id=splinter/help.html">
+ [help]</a>
+</div>
+
+<div id="bugInfo" style="display: none;">
+ <b>[% terms.Bug %] <a id="bugLink"><span id="bugId"></span></a>:</b>
+ <span id="bugShortDesc"></span> -
+ <span id="bugReporter"></span> -
+ <span id="bugCreationDate"></span>
+</div>
+
+<div id="attachInfo" style="display:none;">
+ <span id="attachWarning"></span>
+ <b>Attachment <a id="attachLink"><span id="attachId"></span></a>:</b>
+ <span id="attachDesc"></span> -
+ <span id="attachCreator"></span> -
+ <span id="attachDate"></span>
+ [% IF feature_enabled('patch_viewer') %]
+ <a href="[% urlbase FILTER none %]attachment.cgi?id=[% attach_id FILTER uri %]&amp;action=diff"
+ target="_blank">[diff]</a>
+ [% END %]
+ <a href="[% urlbase FILTER none %]attachment.cgi?id=[% attach_id FILTER uri %]&amp;action=edit"
+ target="_blank">[details]</a>
+</div>
+
+<div id="error" style="display: none;"> </div>
+
+<div id="enterBug" style="display: none;">
+ [% terms.Bug %] to review:
+ <input id="enterBugInput" />
+ <input id="enterBugGo" type="button" value="Go" />
+ <div id="chooseReview" style="display: none;">
+ Drafts and published reviews:
+ <div id="chooseReviewTable"></div>
+ </div>
+</div>
+
+<div id="chooseAttachment" style="display: none;">
+ <div id="chooseAttachmentTable"></div>
+</div>
+
+<div id="quickHelpShow" style="display:none;">
+ <p>
+ <a href="javascript:Splinter.quickHelpToggle();" title="Show the quick help section" id="quickHelpToggle">
+ Show Quick Help</a>
+ </p>
+</div>
+
+<div id="quickHelpContent" style="display:none;">
+ <p>
+ <a href="javascript:Splinter.quickHelpToggle();" title="Hide the quick help section" id="quickHelpToggle">Close Quick Help</a>
+ </p>
+ <ul id="quickHelpList">
+ <li>From the Overview page, you can add a more generic overview comment that will appear at the beginning of your review.</li>
+ <li>To comment on a specific lines in the patch, first select the filename from the file navigation links.</li>
+ <li>Then double click the line you want to review and a comment box will appear below the line.</li>
+ <li>When the review is complete and you publish it, the overview comment and all line specific comments with their context,
+ will be combined together into a single review comment on the [% terms.bug %] report.</li>
+ <li>For more detailed instructions, read the Splinter
+ <a id='helpLink' target='splinterHelp' href="[% urlbase FILTER none %]page.cgi?id=splinter/help.html">help page</a>.
+ </li>
+ </ul>
+</div>
+
+<div id="navigationContainer" style="display: none;">
+ <b>Navigation:</b> <span id="navigation"></span>
+</div>
+
+<div id="overview" style="display: none;">
+ <div id="patchIntro"></div>
+ <div>
+ <span id="restored" style="display: none;">
+ (Restored from draft; last edited <span id="restoredLastModified"></span>)
+ </span>
+ </div>
+ [% IF user.id %]
+ <div>
+ <div id="myCommentFrame">
+ <textarea id="myComment"></textarea>
+ <div id="emptyCommentNotice">&lt;Overall Comment&gt;</div>
+ </div>
+ <div id="myPatchComments"></div>
+ <form id="publish" method="post" action="attachment.cgi" onsubmit="normalizeComments();">
+ <input type="hidden" id="publish_token" name="token" value="">
+ <input type="hidden" id="publish_action" name="action" value="update">
+ <input type="hidden" id="publish_review" name="comment" value="">
+ <input type="hidden" id="publish_attach_id" name="id" value="">
+ <input type="hidden" id="publish_attach_desc" name="description" value="">
+ <input type="hidden" id="publish_attach_filename" name="filename" value="">
+ <input type="hidden" id="publish_attach_contenttype" name="contenttypeentry" value="">
+ <input type="hidden" id="publish_attach_ispatch" name="ispatch" value="">
+ <input type="hidden" id="publish_attach_isobsolete" name="isobsolete" value="">
+ <input type="hidden" id="publish_attach_isprivate" name="isprivate" value="">
+ <div id="attachment_flags">
+ [% any_flags_requesteeble = 0 %]
+ [% FOREACH flag_type = flag_types %]
+ [% NEXT UNLESS flag_type.is_active %]
+ [% SET any_flags_requesteeble = 1 IF flag_type.is_requestable && flag_type.is_requesteeble %]
+ [% END %]
+ [% IF flag_types.size > 0 %]
+ [% PROCESS "flag/list.html.tmpl" bug_id = bug_id
+ attach_id = attach_d
+ flag_types = flag_types
+ read_only_flags = !can_edit
+ any_flags_requesteeble = any_flags_requesteeble
+ %]
+ [% END %]
+ <script>
+ [% FOREACH flag_type = flag_types %]
+ [% NEXT UNLESS flag_type.is_active %]
+ Event.addListener('flag_type-[% flag_type.id FILTER js %]', 'change',
+ function() { Splinter.flagChanged = 1;
+ Splinter.queueUpdateHaveDraft(); });
+ [% FOREACH flag = flag_type.flags %]
+ Event.addListener('flag-[% flag.id FILTER js %]', 'change',
+ function() { Splinter.flagChanged = 1;
+ Splinter.queueUpdateHaveDraft(); });
+ [% END %]
+ [% END %]
+ </script>
+ </div>
+ </form>
+ <div id="buttonBox">
+ <span id="attachmentStatusSpan">Patch Status:
+ <select id="attachmentStatus"> </select>
+ </span>
+ <input id="publishButton" type="button" value="Publish" />
+ <input id="cancelButton" type="button" value="Cancel" />
+ </div>
+ <div class="clear"></div>
+ </div>
+ [% ELSE %]
+ <div>
+ You must be logged in to review patches.
+ </div>
+ [% END %]
+ <div id="oldReviews" style="display: none;">
+ <div class="review-title">
+ Previous Reviews
+ </div>
+ </div>
+</div>
+
+<div id="splinter-files" style="display: none;">
+ <div id="file-collapse-all" style="display:none;">
+ <a href="javascript:void(0);" onclick="Splinter.toggleCollapsed('', 'none')">Collapse All</a> |
+ <a href="javascript:void(0);" onclick="Splinter.toggleCollapsed('', 'block')">Expand All</a>
+ </div>
+</div>
+
+<div id="credits">
+ Powered by <a href="http://fishsoup.net/software/splinter">Splinter</a>
+</div>
+
+<div id="saveDraftNotice" style="display: none;"></div>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/Splinter/template/en/default/pages/splinter/help.html.tmpl b/extensions/Splinter/template/en/default/pages/splinter/help.html.tmpl
new file mode 100644
index 000000000..dac513e56
--- /dev/null
+++ b/extensions/Splinter/template/en/default/pages/splinter/help.html.tmpl
@@ -0,0 +1,153 @@
+[%#
+ # The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Splinter Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is Red Hat, Inc.
+ # Portions created by Red Hat, Inc. are Copyright (C) 2008
+ # Red Hat, Inc. All Rights Reserved.
+ #
+ # Contributor(s): Owen Taylor <otaylor@redhat.com>
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Patch Review Help"
+ header = "Patch Review Help"
+%]
+
+<h2>Splinter Patch Review</h2>
+<p>
+ Splinter is an add-on for [% terms.Bugzilla %] to allow conveniently
+ reviewing patches that people have attached to
+ [%+ terms.Bugzilla %]. <a href="http://fishsoup.net/software/splinter">More
+ information about Splinter</a>.
+</p>
+<h3>The patch review view</h3>
+<p>
+ If you get to Splinter by clicking on a link next to an
+ attachment in [% terms.Bugzilla %], you are presented with the patch
+ review view. This view has a number of different pages that can
+ be switched between with the links at the top of the screen.
+ The first page is the Overview page, the other pages correspond to
+ individual files changed by the review.
+</p>
+<p>
+ On the Overview page, from top to bottom are shown:
+</p>
+<ul>
+ <li>Introductory text to the patch. For a patch that was created
+ using 'git format-patch' this will be the Git commit
+ message.</li>
+ <li>Controls for creating a new review</li>
+ <li>Previous reviews that other people have written.</li>
+</ul>
+<p>
+ The pages for each file show a two-column view of the changes.
+ The left column is the previous contents of the file,
+ the right column is the new contents of the file. (If the file
+ is an entirely new file or an entirely deleted file, only one
+ column will be shown.) Red indicates lines that have been
+ removed, green lines that have been added, and blue lines that
+ were modified.
+</p>
+<p>
+ If people have previously made comments on individual lines of
+ the patch, they will show up both summarized on the Overview
+ page and also inline when looking at the files of the patch.
+</p>
+<h3>Reviewing an existing patch</h3>
+<p>
+ There are three components to a review:
+</p>
+<ul>
+ <li>
+ An overall comment. The text area on the first page allows
+ you to enter your overall thoughts on the [% terms.bug %].
+ </li>
+ <li>
+ Detailed comments on changes within the files. To comment on a
+ line in a patch, double click on it, and a text area will open
+ beneath that comment. When you are done, click the Save button
+ to save your comment or the Cancel button to throw your
+ comment away. You can double-click on a saved comment to start
+ editing it again and make further changes.
+ </li>
+ <li>
+ A change to the attachment status. (This is specific to
+ [%+ terms.Bugzilla %] instances that have attachment status, which is a
+ non-upstream patch. It's somewhat similar to attachment flags,
+ which splinter doesn't currently support displaying or
+ changing.) This allows you to mark a patch as read to commit
+ or needing additional work. This is done by changing the
+ drop-down next to the Publish button.
+ </li>
+</ul>
+<p>
+ Once you are done writing your review, go back to Overview page
+ and click the "Publish" button to submit it as a comment on the
+ [%+ terms.bug %]. The comment will have a link back to the review page so
+ that people can see your comments with the full context.
+</p>
+<h3>Saved drafts</h3>
+<p>
+ Whenever you start making changes, a draft is automatically
+ saved. If you come back to the patch review page for the same
+ attachment, that draft will automatically be resumed. Drafts are
+ not visible to anybody else until published.
+</p>
+<p>
+ Note that saving drafts requires the your browser to have support
+ for the "DOM Storage" standard. At time of writing, this is
+ available only in a few very recent browsers, like Firefox
+ 3.5. Strict privacy protections like disabling cookies may also
+ disable DOM Storage, since it provides another mechanism for
+ sites to track information about their users.
+</p>
+<h3>Responding to someone's review</h3>
+<p>
+ A response is treated just like any other review and created the
+ same way. A couple of features are helpful when responding: you
+ can double-click on an inline comment to respond to it. And on
+ the overview page, when you click on a detailed comment, you are
+ taken directly to the original location of the comment.
+</p>
+<h3>Uploading patches for review</h3>
+<p>
+ Splinter doesn't really care how patches are provided to
+ [%+ terms.Bugzilla %], as long as they are well-formatted patches. If you are
+ using Git for version control, you can either format changes as
+ patches
+ using <a href="http://www.kernel.org/pub/software/scm/git/docs/git-format-patch.html">'git
+ format-patch</a> and attach them manually to the [% terms.bug %], or you
+ can
+ use <a href="http://fishsoup.net/software/git-bz">git-bz</a>.
+ git-bz is highly recommended; it automates most of the steps
+ that Splinter can't handle: it files new [% terms.bugs %], attaches updated
+ attachments to existing [% terms.bugs %], and closes [% terms.bugs %] when you push the
+ corresponding git commits to your central repository.
+</p>
+<h3>The [% terms.bug %] review view</h3>
+<p>
+ Splinter also has a view where it shows all patches attached to
+ the [% terms.bug %] with their status and links to review them. You are
+ taken to this page after publishing a review. You can also get
+ to this page with the [% terms.bug %] link in the upper-right corner of the
+ patch review view.
+</p>
+<h3>Your reviews</h3>
+<p>
+ Splinter can also show you a list of all your draft and
+ published reviews. Access this page with the "Your reviews"
+ link at the bottom of the [% terms.bug %] review view. In-progress drafts
+ are shown in bold.
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/Splinter/web/splinter.css b/extensions/Splinter/web/splinter.css
new file mode 100644
index 000000000..36e5fef31
--- /dev/null
+++ b/extensions/Splinter/web/splinter.css
@@ -0,0 +1,428 @@
+textarea:focus {
+ background: #f7f2d0;
+}
+
+#note {
+ background: #ffee88;
+ padding: 0.5em;
+}
+
+#error {
+ border: 1px solid black;
+ padding: 0.5em;
+ color: #bb0000;
+}
+
+#chooseReview {
+ margin-top: 1em;
+}
+
+.review-draft .review-desc, .review-draft .review-attachment {
+ font-weight: bold;
+}
+
+#bugInfo, #attachInfo {
+ margin-top: 0.5em;
+ margin-bottom: 1em;
+}
+
+#helpful-links {
+ float:right;
+}
+
+#chooseAttachment table {
+ margin-bottom: 1em;
+}
+
+#attachWarning {
+ font-weight: bold;
+ color: #c00000;
+}
+
+.attachment-draft .attachment-id, .attachment-draft .attachment-desc {
+ font-weight: bold;
+}
+
+.attachment-obsolete .attachment-desc {
+ text-decoration: line-through ;
+}
+
+#navigation {
+ color: #888888;
+}
+
+.navigation-link {
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+.navigation-link-selected {
+ color: black;
+}
+
+#haveDraftNotice {
+ float: right;
+ color: #bb0000;
+ font-weight: bold;
+}
+
+#overview {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+
+#patchIntro {
+ border: 1px solid #888888;
+ font-size: 90%;
+ margin-bottom: 1em;
+ padding: 0.5em;
+}
+
+.reviewer-box {
+ padding: 0.5em;
+}
+
+.reviewer-0 .reviewer-box {
+ border-left: 10px solid green;
+}
+
+.reviewer-1 .reviewer-box {
+ border-left: 10px solid blue;
+}
+
+.reviewer-2 .reviewer-box {
+ border-left: 10px solid red;
+}
+
+.reviewer-3 .reviewer-box {
+ border-left: 10px solid yellow;
+}
+
+.reviewer-4 .reviewer-box {
+ border-left: 10px solid purple;
+}
+
+.reviewer-5 .reviewer-box {
+ border-left: 10px solid orange;
+}
+
+.reviewer {
+ float: left;
+}
+
+.review-date {
+ float: right;
+}
+
+.review-info-bottom {
+ clear: both;
+}
+
+.review {
+ border: 1px solid black;
+ font-size: 90%;
+ margin-top: 0.25em;
+ margin-bottom: 1em;
+}
+
+.review-intro {
+ margin-top: 0.5em;
+}
+
+.review-patch-file {
+ margin-top: 0.5em;
+ font-weight: bold;
+}
+
+.review-patch-comment {
+ border: 1px solid white;
+ padding: 1px;
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+ cursor: pointer;
+}
+
+.review-patch-comment:hover {
+ border: 1px solid #8888ff;
+}
+
+.review-patch-comment .file-table {
+ width: 50%;
+}
+
+.review-patch-comment .file-table-changed {
+ width: 100%;
+}
+
+.review-patch-comment-separator {
+ margin: 0.5em;
+ border-bottom: 1px solid #888888;
+}
+
+div.review-patch-comment-text {
+ margin-left: 2em;
+}
+
+.review-patch-comment .reviewer-box {
+ border-left-width: 4px;
+}
+
+#restored {
+ color: #bb0000;
+ margin-bottom: 0.5em;
+}
+
+#myCommentFrame {
+ margin-top: 0.25em;
+ position: relative;
+ border: 1px solid black;
+ padding-right: 8px; /* compensate for child's padding */
+}
+
+#myComment {
+ border: 0px solid black;
+ padding: 4px;
+ margin: 0px;
+ width: 100%;
+ height: 10em;
+}
+
+#emptyCommentNotice {
+ position: absolute;
+ top: 4px;
+ left: 4px;
+ color: #888888;
+}
+
+#myPatchComments {
+ border: 1px solid black;
+ border-top-width: 0px;
+ padding: 0.5em;
+ font-size: 90%;
+}
+
+#buttonBox {
+ margin-top: 0.5em;
+ float: right;
+}
+
+.clear {
+ clear: both;
+}
+
+/* Used for IE <= 7, overridden for modern browsers */
+.pre-wrap {
+ white-space: pre;
+ word-wrap: break-word;
+}
+
+.pre-wrap {
+ white-space: pre-wrap;
+}
+
+#splinter-files {
+ position: relative;
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+
+.file-label {
+ margin-top: 1em;
+ margin-bottom: 0.5em;
+}
+
+.file-label-name {
+ font-weight: bold;
+}
+
+.file-label-extra {
+ font-size: 90%;
+ font-style: italic;
+}
+
+.hunk-header {
+ border: 1px solid #aaaaaa;
+}
+
+.hunk-header td {
+ background: #ddccbb;
+ font-size: 80%;
+ font-weight: bold;
+}
+
+.hunk-cell {
+ padding: 2px;
+}
+
+.old-line, .new-line {
+ font-family: "DejaVu Sans Mono", monospace;
+ font-size: 80%;
+ white-space: pre-wrap; /* CSS 3 & 2.1 */
+ white-space: -moz-pre-wrap; /* Gecko */
+ white-space: -pre-wrap; /* Opera 4-6 */
+ white-space: -o-pre-wrap; /* Opera 7 */
+}
+
+.removed-line {
+ background: #ffccaa;;
+}
+
+.added-line {
+ background: #bbffbb;
+}
+
+.changed-line {
+ background: #aaccff;
+}
+
+.file-table {
+ width: 100%;
+ border-collapse: collapse;
+ table-layout: fixed;
+}
+
+.line-number {
+ font-size: 80%;
+ text-align: right;
+ padding-right: 0.25em;
+ color: #888888;
+ -moz-user-select: none;
+}
+
+.line-number-column {
+ width: 2em;
+}
+
+.file-table-wide-numbers .line-number-column {
+ width: 3em;
+}
+
+.middle-column {
+ width: 3px;
+}
+
+.file-table-changed .comment-removed {
+ width: 50%;
+ float: left;
+}
+
+.file-table-changed .comment-changed {
+ margin-left: 25%;
+ margin-right: 25%;
+ clear: both;
+}
+
+.file-table-changed .comment-added {
+ width: 50%;
+ float: right;
+}
+
+.comment-frame {
+ border: 1px solid black;
+ margin-top: 5px;
+ margin-bottom: 5px;
+ margin-left: 2em;
+}
+
+.file-table-wide-numbers .comment-frame {
+ margin-left: 3em;
+}
+
+.comment .review-info {
+ margin-top: 0.5em;
+ font-size: 80%;
+}
+
+#commentTextFrame {
+ border: 1px solid #ffeeaa;
+ margin-bottom: 5px;
+}
+
+#commentEditor.focused #commentTextFrame {
+ border: 1px solid #8888bb;
+}
+
+#commentEditorInner {
+ background: #ffeeaa;
+ padding: 0.5em;
+ margin-left: 2em;
+}
+
+.file-table-wide-numbers #commentEditorInner {
+ margin-left: 3em;
+}
+
+#commentEditor textarea {
+ width: 100%;
+ height: 10em;
+ border: 0px;
+}
+
+#commentEditor textarea:focus {
+ background: white;
+}
+
+#commentEditorLeftButtons {
+ float: left;
+}
+
+#commentEditorLeftButtons input {
+ margin-right: 0.5em;
+}
+
+#commentEditorRightButtons {
+ float: right;
+}
+
+.comment-separator-removed {
+ clear: left;
+}
+
+.comment-separator-added {
+ clear: right;
+}
+
+#saveDraftNotice {
+ border: 1px solid black;
+ padding: 0.5em;
+ background: #ffccaa;
+ position: fixed;
+ bottom: 0px;
+ right: 0px;
+}
+
+#credits {
+ font-size: 80%;
+ color: #888888;
+ padding: 10px;
+ text-align: center;
+}
+
+#quickHelpShow a, #quickHelpContent a {
+ text-decoration: none;
+}
+
+#quickHelpContent {
+ border: 1px solid #000;
+ -moz-border-radius: 10px;
+ border-radius: 10px;
+ padding-left: 0.5em;
+ margin-bottom: 10px;
+}
+
+.file-label-collapse {
+ padding-right: 5px;
+ font-family: monospace;
+}
+
+.file-review-label {
+ font-size: 80%;
+}
+
+.file-reviewed-nav {
+ text-decoration: line-through;
+}
+
+.trailing-whitespace {
+ background: #ffaaaa;
+}
diff --git a/extensions/Splinter/web/splinter.js b/extensions/Splinter/web/splinter.js
new file mode 100644
index 000000000..b33fa778c
--- /dev/null
+++ b/extensions/Splinter/web/splinter.js
@@ -0,0 +1,2700 @@
+// Splinter - patch review add-on for Bugzilla
+// By Owen Taylor <otaylor@fishsoup.net>
+// Copyright 2009, Red Hat, Inc.
+// Licensed under MPL 1.1 or later, or GPL 2 or later
+// http://git.fishsoup.net/cgit/splinter
+// Converted to YUI by David Lawrence <dkl@mozilla.com>
+
+YAHOO.namespace('Splinter');
+
+var Dom = YAHOO.util.Dom;
+var Event = YAHOO.util.Event;
+var Splinter = YAHOO.Splinter;
+var Element = YAHOO.util.Element;
+
+Splinter.domCache = {
+ cache : [0],
+ expando : 'data' + new Date(),
+ data : function (elem) {
+ var cacheIndex = elem[Splinter.domCache.expando];
+ var nextCacheIndex = Splinter.domCache.cache.length;
+ if (!cacheIndex) {
+ cacheIndex = elem[Splinter.domCache.expando] = nextCacheIndex;
+ Splinter.domCache.cache[cacheIndex] = {};
+ }
+ return Splinter.domCache.cache[cacheIndex];
+ }
+};
+
+Splinter.Utils = {
+ assert : function(condition) {
+ if (!condition) {
+ throw new Error("Assertion failed");
+ }
+ },
+
+ assertNotReached : function() {
+ throw new Error("Assertion failed: should not be reached");
+ },
+
+ strip : function(string) {
+ return (/^\s*([\s\S]*?)\s*$/).exec(string)[1];
+ },
+
+ lstrip : function(string) {
+ return (/^\s*([\s\S]*)$/).exec(string)[1];
+ },
+
+ rstrip : function(string) {
+ return (/^([\s\S]*?)\s*$/).exec(string)[1];
+ },
+
+ formatDate : function(date, now) {
+ if (now == null) {
+ now = new Date();
+ }
+ var daysAgo = (now.getTime() - date.getTime()) / (24 * 60 * 60 * 1000);
+ if (daysAgo < 0 && now.getDate() != date.getDate()) {
+ return date.toLocaleDateString();
+ } else if (daysAgo < 1 && now.getDate() == date.getDate()) {
+ return date.toLocaleTimeString();
+ } else if (daysAgo < 7 && now.getDay() != date.getDay()) {
+ return ['Sun', 'Mon','Tue','Wed','Thu','Fri','Sat'][date.getDay()] + " " + date.toLocaleTimeString();
+ } else {
+ return date.toLocaleDateString();
+ }
+ },
+
+ preWrapLines : function(el, text) {
+ while ((m = Splinter.LINE_RE.exec(text)) != null) {
+ var div = document.createElement("div");
+ div.className = "pre-wrap";
+ div.appendChild(document.createTextNode(m[1].length == 0 ? " " : m[1]));
+ el.appendChild(div);
+ }
+ },
+
+ isDigits : function (str) {
+ return str.match(/^[0-9]+$/);
+ }
+};
+
+Splinter.Bug = {
+ TIMEZONES : {
+ CEST: '200',
+ CET: '100',
+ BST: '100',
+ GMT: '000',
+ UTC: '000',
+ EDT: '-400',
+ EST: '-500',
+ CDT: '-500',
+ CST: '-600',
+ MDT: '-600',
+ MST: '-700',
+ PDT: '-700',
+ PST: '-800'
+ },
+
+ parseDate : function(d) {
+ var m = /^\s*(\d+)-(\d+)-(\d+)\s+(\d+):(\d+)(?::(\d+))?\s+(?:([A-Z]{3,})|([-+]\d{3,}))\s*$/.exec(d);
+ if (!m) {
+ return null;
+ }
+
+ var year = parseInt(m[1], 10);
+ var month = parseInt(m[2] - 1, 10);
+ var day = parseInt(m[3], 10);
+ var hour = parseInt(m[4], 10);
+ var minute = parseInt(m[5], 10);
+ var second = m[6] ? parseInt(m[6], 10) : 0;
+
+ var tzoffset = 0;
+ if (m[7]) {
+ if (m[7] in Splinter.Bug.TIMEZONES) {
+ tzoffset = Splinter.Bug.TIMEZONES[m[7]];
+ }
+ } else {
+ tzoffset = parseInt(m[8], 10);
+ }
+
+ var unadjustedDate = new Date(Date.UTC(m[1], m[2] - 1, m[3], m[4], m[5]));
+
+ // 430 => 4:30. Easier to do this computation for only positive offsets
+ var sign = tzoffset < 0 ? -1 : 1;
+ tzoffset *= sign;
+ var adjustmentHours = Math.floor(tzoffset/100);
+ var adjustmentMinutes = tzoffset - adjustmentHours * 100;
+
+ return new Date(unadjustedDate.getTime() -
+ sign * adjustmentHours * 3600000 -
+ sign * adjustmentMinutes * 60000);
+ },
+
+ _formatWho : function(name, email) {
+ if (name && email) {
+ return name + " <" + email + ">";
+ } else if (name) {
+ return name;
+ } else {
+ return email;
+ }
+ }
+};
+
+Splinter.Bug.Attachment = function(bug, id) {
+ this._init(bug, id);
+};
+
+Splinter.Bug.Attachment.prototype = {
+ _init : function(bug, id) {
+ this.bug = bug;
+ this.id = id;
+ }
+};
+
+Splinter.Bug.Comment = function(bug) {
+ this._init(bug);
+};
+
+Splinter.Bug.Comment.prototype = {
+ _init : function(bug) {
+ this.bug = bug;
+ },
+
+ getWho : function() {
+ return Splinter.Bug._formatWho(this.whoName, this.whoEmail);
+ }
+};
+
+Splinter.Bug.Bug = function() {
+ this._init();
+};
+
+Splinter.Bug.Bug.prototype = {
+ _init : function() {
+ this.attachments = [];
+ this.comments = [];
+ },
+
+ getAttachment : function(attachmentId) {
+ var i;
+ for (i = 0; i < this.attachments.length; i++) {
+ if (this.attachments[i].id == attachmentId) {
+ return this.attachments[i];
+ }
+ }
+ return null;
+ },
+
+ getReporter : function() {
+ return Splinter.Bug._formatWho(this.reporterName, this.reporterEmail);
+ }
+};
+
+Splinter.Dialog = function() {
+ this._init.apply(this, arguments);
+};
+
+Splinter.Dialog.prototype = {
+ _init: function(prompt) {
+ this.buttons = [];
+ this.dialog = new YAHOO.widget.SimpleDialog('dialog', {
+ width: "300px",
+ fixedcenter: true,
+ visible: false,
+ modal: true,
+ draggable: false,
+ close: false,
+ hideaftersubmit: true,
+ constraintoviewport: true
+ });
+ this.dialog.setHeader(prompt);
+ },
+
+ addButton : function (label, callback, isdefault) {
+ this.buttons.push({ text : label,
+ handler : function () { this.hide(); callback(); },
+ isDefault : isdefault });
+ this.dialog.cfg.queueProperty("buttons", this.buttons);
+ },
+
+ show : function () {
+ this.dialog.render(document.body);
+ this.dialog.show();
+ }
+};
+
+Splinter.Patch = {
+ ADDED : 1 << 0,
+ REMOVED : 1 << 1,
+ CHANGED : 1 << 2,
+ NEW_NONEWLINE : 1 << 3,
+ OLD_NONEWLINE : 1 << 4,
+
+ FILE_START_RE : new RegExp(
+ '^(?:' + // start of optional header
+ '(?:Index|index|===|RCS|diff)[^\\n]*\\n' + // header
+ '(?:(?:copy|rename) from [^\\n]+\\n)?' + // git copy/rename from
+ '(?:(?:copy|rename) to [^\\n]+\\n)?' + // git copy/rename to
+ ')*' + // end of optional header
+ '\\-\\-\\-[ \\t]*(\\S+).*\\n' + // --- line
+ '\\+\\+\\+[ \\t]*(\\S+).*\\n' + // +++ line
+ '(?=@@)', // @@ line
+ 'mg'
+ ),
+ HUNK_START1_RE: /^@@[ \t]+-(\d+),(\d+)[ \t]+\+(\d+),(\d+)[ \t]+@@(.*)\n/mg, // -l,s +l,s
+ HUNK_START2_RE: /^@@[ \t]+-(\d+),(\d+)[ \t]+\+(\d+)[ \t]+@@(.*)\n/mg, // -l,s +l
+ HUNK_START3_RE: /^@@[ \t]+-(\d+)[ \t]+\+(\d+),(\d+)[ \t]+@@(.*)\n/mg, // -l +l,s
+ HUNK_START4_RE: /^@@[ \t]+-(\d+)[ \t]+\+(\d+)[ \t]+@@(.*)\n/mg, // -l +l
+ HUNK_RE : /((?:(?!--- )[ +\\-].*(?:\n|$)|(?:\n|$))*)/mg,
+
+ GIT_BINARY_RE : /^diff --git a\/(\S+).*\n(?:(new|deleted) file mode \d+\n)?(?:index.*\n)?GIT binary patch\n(delta )?/mg,
+
+ _cleanIntro : function(intro) {
+ var m;
+
+ intro = Splinter.Utils.strip(intro) + "\n\n";
+
+ // Git: remove binary diffs
+ var binary_re = /^(?:diff --git .*\n|literal \d+\n)(?:.+\n)+\n/mg;
+ m = binary_re.exec(intro);
+ while (m) {
+ intro = intro.substr(m.index + m[0].length);
+ binary_re.lastIndex = 0;
+ m = binary_re.exec(intro);
+ }
+
+ // Git: remove leading 'From <commit_id> <date>'
+ m = /^From\s+[a-f0-9]{40}.*\n/.exec(intro);
+ if (m) {
+ intro = intro.substr(m.index + m[0].length);
+ }
+
+ // Git: remove 'diff --stat' output from the end
+ m = /^---\n(?:^\s.*\n)+\s+\d+\s+files changed.*\n?(?!.)/m.exec(intro);
+ if (m) {
+ intro = intro.substr(0, m.index);
+ }
+
+ return Splinter.Utils.strip(intro);
+ }
+};
+
+Splinter.Patch.Hunk = function(oldStart, oldCount, newStart, newCount, functionLine, text) {
+ this._init(oldStart, oldCount, newStart, newCount, functionLine, text);
+};
+
+Splinter.Patch.Hunk.prototype = {
+ _init : function(oldStart, oldCount, newStart, newCount, functionLine, text) {
+ var rawlines = text.split("\n");
+ if (rawlines.length > 0 && Splinter.Utils.strip(rawlines[rawlines.length - 1]) == "") {
+ rawlines.pop(); // Remove trailing element from final \n
+ }
+
+ this.oldStart = oldStart;
+ this.oldCount = oldCount;
+ this.newStart = newStart;
+ this.newCount = newCount;
+ this.functionLine = Splinter.Utils.strip(functionLine);
+ this.comment = null;
+
+ var lines = [];
+ var totalOld = 0;
+ var totalNew = 0;
+
+ var currentStart = -1;
+ var currentOldCount = 0;
+ var currentNewCount = 0;
+
+ // A segment is a series of lines added/removed/changed with no intervening
+ // unchanged lines. We make the classification of Patch.ADDED/Patch.REMOVED/Patch.CHANGED
+ // in the flags for the entire segment
+ function startSegment() {
+ if (currentStart < 0) {
+ currentStart = lines.length;
+ }
+ }
+
+ function endSegment() {
+ if (currentStart >= 0) {
+ if (currentOldCount > 0 && currentNewCount > 0) {
+ var j;
+ for (j = currentStart; j < lines.length; j++) {
+ lines[j][2] &= ~(Splinter.Patch.ADDED | Splinter.Patch.REMOVED);
+ lines[j][2] |= Splinter.Patch.CHANGED;
+ }
+ }
+
+ currentStart = -1;
+ currentOldCount = 0;
+ currentNewCount = 0;
+ }
+ }
+
+ var i;
+ for (i = 0; i < rawlines.length; i++) {
+ var line = rawlines[i];
+ var op = line.substr(0, 1);
+ var strippedLine = line.substring(1);
+ var noNewLine = 0;
+ if (i + 1 < rawlines.length && rawlines[i + 1].substr(0, 1) == '\\') {
+ noNewLine = op == '-' ? Splinter.Patch.OLD_NONEWLINE : Splinter.Patch.NEW_NONEWLINE;
+ }
+
+ if (op == ' ') {
+ endSegment();
+ totalOld++;
+ totalNew++;
+ lines.push([strippedLine, strippedLine, 0]);
+ } else if (op == '-') {
+ totalOld++;
+ startSegment();
+ lines.push([strippedLine, null, Splinter.Patch.REMOVED | noNewLine]);
+ currentOldCount++;
+ } else if (op == '+') {
+ totalNew++;
+ startSegment();
+ if (currentStart + currentNewCount >= lines.length) {
+ lines.push([null, strippedLine, Splinter.Patch.ADDED | noNewLine]);
+ } else {
+ lines[currentStart + currentNewCount][1] = strippedLine;
+ lines[currentStart + currentNewCount][2] |= Splinter.Patch.ADDED | noNewLine;
+ }
+ currentNewCount++;
+ }
+ }
+
+ // git mail-formatted patches end with --\n<git version> like a signature
+ // This is troublesome since it looks like a subtraction at the end
+ // of last hunk of the last file. Handle this specifically rather than
+ // generically stripping excess lines to be kind to hand-edited patches
+ if (totalOld > oldCount &&
+ lines[lines.length - 1][1] == null &&
+ lines[lines.length - 1][0].substr(0, 1) == '-')
+ {
+ lines.pop();
+ currentOldCount--;
+ if (currentOldCount == 0 && currentNewCount == 0) {
+ currentStart = -1;
+ }
+ }
+
+ endSegment();
+
+ this.lines = lines;
+ },
+
+ iterate : function(cb) {
+ var i;
+ var oldLine = this.oldStart;
+ var newLine = this.newStart;
+ for (i = 0; i < this.lines.length; i++) {
+ var line = this.lines[i];
+ cb(this.location + i, oldLine, line[0], newLine, line[1], line[2], line);
+ if (line[0] != null) {
+ oldLine++;
+ }
+ if (line[1] != null) {
+ newLine++;
+ }
+ }
+ }
+};
+
+Splinter.Patch.File = function(filename, status, extra, hunks) {
+ this._init(filename, status, extra, hunks);
+};
+
+Splinter.Patch.File.prototype = {
+ _init : function(filename, status, extra, hunks) {
+ this.filename = filename;
+ this.status = status;
+ this.extra = extra;
+ this.hunks = hunks;
+ this.fileReviewed = false;
+
+ var l = 0;
+ var i;
+ for (i = 0; i < this.hunks.length; i++) {
+ var hunk = this.hunks[i];
+ hunk.location = l;
+ l += hunk.lines.length;
+ }
+ },
+
+ // A "location" is just a linear index into the lines of the patch in this file
+ getLocation : function(oldLine, newLine) {
+ var i;
+ for (i = 0; i < this.hunks.length; i++) {
+ var hunk = this.hunks[i];
+ if (oldLine != null && hunk.oldStart > oldLine) {
+ continue;
+ }
+ if (newLine != null && hunk.newStart > newLine) {
+ continue;
+ }
+
+ if ((oldLine != null && oldLine < hunk.oldStart + hunk.oldCount) ||
+ (newLine != null && newLine < hunk.newStart + hunk.newCount))
+ {
+ var location = -1;
+ hunk.iterate(function(loc, oldl, oldText, newl, newText, flags) {
+ if ((oldLine == null || oldl == oldLine) &&
+ (newLine == null || newl == newLine))
+ {
+ location = loc;
+ }
+ });
+
+ if (location != -1) {
+ return location;
+ }
+ }
+ }
+
+ throw "Bad oldLine,newLine: " + oldLine + "," + newLine;
+ },
+
+ getHunk : function(location) {
+ var i;
+ for (i = 0; i < this.hunks.length; i++) {
+ var hunk = this.hunks[i];
+ if (location >= hunk.location && location < hunk.location + hunk.lines.length) {
+ return hunk;
+ }
+ }
+
+ throw "Bad location: " + location;
+ },
+
+ toString : function() {
+ return "Splinter.Patch.File(" + this.filename + ")";
+ }
+};
+
+Splinter.Patch.Patch = function(text) {
+ this._init(text);
+};
+
+Splinter.Patch.Patch.prototype = {
+ // cf. parsing in Review.Review.parse()
+ _init : function(text) {
+ // Canonicalize newlines to simplify the following
+ if (/\r/.test(text)) {
+ text = text.replace(/(\r\n|\r|\n)/g, "\n");
+ }
+
+ this.files = [];
+
+ var m = Splinter.Patch.FILE_START_RE.exec(text);
+ var bm = Splinter.Patch.GIT_BINARY_RE.exec(text);
+ if (m == null && bm == null)
+ throw "Not a patch";
+ this.intro = m == null ? '' : Splinter.Patch._cleanIntro(text.substring(0, m.index));
+
+ // show binary files in the intro
+
+ if (bm && this.intro.length)
+ this.intro += "\n\n";
+ while (bm != null) {
+ if (bm[2]) {
+ // added or deleted file
+ this.intro += bm[2].charAt(0).toUpperCase() + bm[2].slice(1) + ' Binary File: ' + bm[1] + "\n";
+ } else {
+ // delta
+ this.intro += 'Modified Binary File: ' + bm[1] + "\n";
+ }
+ bm = Splinter.Patch.GIT_BINARY_RE.exec(text);
+ }
+
+ while (m != null) {
+ // git shows a diff between a/foo/bar.c and b/foo/bar.c or between
+ // a/foo/bar.c and /dev/null for removals and the reverse for
+ // additions.
+ var filename;
+ var status = undefined;
+ var extra = undefined;
+
+ if (/^a\//.test(m[1]) && /^b\//.test(m[2])) {
+ filename = m[2].substring(2);
+ status = Splinter.Patch.CHANGED;
+ } else if (/^a\//.test(m[1]) && /^\/dev\/null/.test(m[2])) {
+ filename = m[1].substring(2);
+ status = Splinter.Patch.REMOVED;
+ } else if (/^\/dev\/null/.test(m[1]) && /^b\//.test(m[2])) {
+ filename = m[2].substring(2);
+ status = Splinter.Patch.ADDED;
+ // Handle non-git cases as well
+ } else if (!/^\/dev\/null/.test(m[1]) && /^\/dev\/null/.test(m[2])) {
+ filename = m[1];
+ status = Splinter.Patch.REMOVED;
+ } else if (/^\/dev\/null/.test(m[1]) && !/^\/dev\/null/.test(m[2])) {
+ filename = m[2];
+ status = Splinter.Patch.ADDED;
+ } else {
+ filename = m[1];
+ }
+
+ // look for rename/copy
+ if (/^diff /.test(m[0])) {
+ // possibly git
+ var lines = m[0].split(/\n/);
+ for (var i = 0, il = lines.length; i < il && !extra; i++) {
+ var line = lines[i];
+ if (line != '' && !/^(?:diff|---|\+\+\+) /.test(line)) {
+ if (/^copy from /.test(line))
+ extra = 'copied from ' + m[1].substring(2);
+ if (/^rename from /.test(line))
+ extra = 'renamed from ' + m[1].substring(2);
+ }
+ }
+ } else if (/^=== renamed /.test(m[0])) {
+ // bzr
+ filename = m[2];
+ extra = 'renamed from ' + m[1];
+ }
+
+ var hunks = [];
+ var pos = Splinter.Patch.FILE_START_RE.lastIndex;
+ while (true) {
+ var found = false;
+ var oldStart, oldCount, newStart, newCount, context;
+
+ // -l,s +l,s
+ var re = Splinter.Patch.HUNK_START1_RE;
+ re.lastIndex = pos;
+ var m2 = re.exec(text);
+ if (m2 != null && m2.index == pos) {
+ oldStart = parseInt(m2[1], 10);
+ oldCount = parseInt(m2[2], 10);
+ newStart = parseInt(m2[3], 10);
+ newCount = parseInt(m2[4], 10);
+ context = m2[5];
+ found = true;
+ }
+
+ if (!found) {
+ // -l,s +l
+ re = Splinter.Patch.HUNK_START2_RE;
+ re.lastIndex = pos;
+ m2 = re.exec(text);
+ if (m2 != null && m2.index == pos) {
+ oldStart = parseInt(m2[1], 10);
+ oldCount = parseInt(m2[2], 10);
+ newStart = parseInt(m2[3], 10);
+ newCount = 1;
+ context = m2[4];
+ found = true;
+ }
+ }
+
+ if (!found) {
+ // -l +l,s
+ re = Splinter.Patch.HUNK_START3_RE;
+ re.lastIndex = pos;
+ m2 = re.exec(text);
+ if (m2 != null && m2.index == pos) {
+ oldStart = parseInt(m2[1], 10);
+ oldCount = 1;
+ newStart = parseInt(m2[2], 10);
+ newCount = parseInt(m2[3], 10);
+ context = m2[4];
+ found = true;
+ }
+ }
+
+ if (!found) {
+ // -l +l
+ re = Splinter.Patch.HUNK_START4_RE;
+ re.lastIndex = pos;
+ m2 = re.exec(text);
+ if (m2 != null && m2.index == pos) {
+ oldStart = parseInt(m2[1], 10);
+ oldCount = 1;
+ newStart = parseInt(m2[2], 10);
+ newCount = 1;
+ context = m2[3];
+ found = true;
+ }
+ }
+
+ if (!found)
+ break;
+
+ pos = re.lastIndex;
+ Splinter.Patch.HUNK_RE.lastIndex = pos;
+ var m3 = Splinter.Patch.HUNK_RE.exec(text);
+ if (m3 == null || m3.index != pos) {
+ break;
+ }
+
+ pos = Splinter.Patch.HUNK_RE.lastIndex;
+ hunks.push(new Splinter.Patch.Hunk(oldStart, oldCount, newStart, newCount, context, m3[1]));
+ }
+
+ if (status === undefined) {
+ // For non-Git we use assume patch was generated non-zero
+ // context and just look at the patch to detect added/removed.
+ // Bzr actually says added/removed in the diff, but SVN/CVS
+ // don't
+ if (hunks.length == 1 && hunks[0].oldCount == 0) {
+ status = Splinter.Patch.ADDED;
+ } else if (hunks.length == 1 && hunks[0].newCount == 0) {
+ status = Splinter.Patch.REMOVED;
+ } else {
+ status = Splinter.Patch.CHANGED;
+ }
+ }
+
+ this.files.push(new Splinter.Patch.File(filename, status, extra, hunks));
+
+ Splinter.Patch.FILE_START_RE.lastIndex = pos;
+ m = Splinter.Patch.FILE_START_RE.exec(text);
+ }
+ },
+
+ getFile : function(filename) {
+ var i;
+ for (i = 0; i < this.files.length; i++) {
+ if (this.files[i].filename == filename) {
+ return this.files[i];
+ }
+ }
+
+ return null;
+ }
+};
+
+Splinter.Review = {
+ _removeFromArray : function(a, element) {
+ var i;
+ for (i = 0; i < a.length; i++) {
+ if (a[i] === element) {
+ a.splice(i, 1);
+ return;
+ }
+ }
+ },
+
+ _noNewLine : function(flags, flag) {
+ return ((flags & flag) != 0) ? "\n\\ No newline at end of file" : "";
+ },
+
+ _lineInSegment : function(line) {
+ return (line[2] & (Splinter.Patch.ADDED | Splinter.Patch.REMOVED | Splinter.Patch.CHANGED)) != 0;
+ },
+
+ _compareSegmentLines : function(a, b) {
+ var op1 = a[0];
+ var op2 = b[0];
+ if (op1 == op2) {
+ return 0;
+ } else if (op1 == ' ') {
+ return -1;
+ } else if (op2 == ' ') {
+ return 1;
+ } else {
+ return op1 == '-' ? -1 : 1;
+ }
+ },
+
+ FILE_START_RE : /^:::[ \t]+(\S+)[ \t]*\n/mg,
+ HUNK_START_RE : /^@@[ \t]+(?:-(\d+),(\d+)[ \t]+)?(?:\+(\d+),(\d+)[ \t]+)?@@.*\n/mg,
+ HUNK_RE : /((?:(?!@@|:::).*\n?)*)/mg,
+ REVIEW_RE : /^\s*review\s+of\s+attachment\s+(\d+)\s*:\s*/i
+};
+
+Splinter.Review.Comment = function(file, location, type, comment) {
+ this._init(file, location, type, comment);
+};
+
+Splinter.Review.Comment.prototype = {
+ _init : function(file, location, type, comment) {
+ this.file = file;
+ this.type = type;
+ this.location = location;
+ this.comment = comment;
+ },
+
+ getHunk : function() {
+ return this.file.patchFile.getHunk(this.location);
+ },
+
+ getInReplyTo : function() {
+ var i;
+ var hunk = this.getHunk();
+ var line = hunk.lines[this.location - hunk.location];
+ for (i = 0; i < line.reviewComments.length; i++) {
+ var comment = line.reviewComments[i];
+ if (comment === this) {
+ return null;
+ }
+ if (comment.type == this.type) {
+ return comment;
+ }
+ }
+
+ return null;
+ },
+
+ remove : function() {
+ var hunk = this.getHunk();
+ var line = hunk.lines[this.location - hunk.location];
+ Splinter.Review._removeFromArray(this.file.comments, this);
+ Splinter.Review._removeFromArray(line.reviewComments, this);
+ }
+};
+
+Splinter.Review.File = function(review, patchFile) {
+ this._init(review, patchFile);
+};
+
+Splinter.Review.File.prototype = {
+ _init : function(review, patchFile) {
+ this.review = review;
+ this.patchFile = patchFile;
+ this.comments = [];
+ },
+
+ addComment : function(location, type, comment) {
+ var hunk = this.patchFile.getHunk(location);
+ var line = hunk.lines[location - hunk.location];
+ comment = new Splinter.Review.Comment(this, location, type, comment);
+ if (line.reviewComments == null) {
+ line.reviewComments = [];
+ }
+ line.reviewComments.push(comment);
+ var i;
+ for (i = 0; i <= this.comments.length; i++) {
+ if (i == this.comments.length ||
+ this.comments[i].location > location ||
+ (this.comments[i].location == location && this.comments[i].type > type)) {
+ this.comments.splice(i, 0, comment);
+ break;
+ } else if (this.comments[i].location == location &&
+ this.comments[i].type == type) {
+ throw "Two comments at the same location";
+ }
+ }
+
+ return comment;
+ },
+
+ getComment : function(location, type) {
+ var i;
+ for (i = 0; i < this.comments.length; i++) {
+ if (this.comments[i].location == location &&
+ this.comments[i].type == type)
+ {
+ return this.comments[i];
+ }
+ }
+
+ return null;
+ },
+
+ toString : function() {
+ var str = "::: " + this.patchFile.filename + "\n";
+ var first = true;
+
+ var i;
+ for (i = 0; i < this.comments.length; i++) {
+ if (first) {
+ first = false;
+ } else {
+ str += '\n';
+ }
+ var comment = this.comments[i];
+ var hunk = comment.getHunk();
+
+ // Find the range of lines we might want to show. That's everything in the
+ // same segment as the commented line, plus up two two lines of non-comment
+ // diff before.
+
+ var contextFirst = comment.location - hunk.location;
+ if (Splinter.Review._lineInSegment(hunk.lines[contextFirst])) {
+ while (contextFirst > 0 && Splinter.Review._lineInSegment(hunk.lines[contextFirst - 1])) {
+ contextFirst--;
+ }
+ }
+
+ var j;
+ for (j = 0; j < 5; j++) {
+ if (contextFirst > 0 && !Splinter.Review._lineInSegment(hunk.lines[contextFirst - 1])) {
+ contextFirst--;
+ }
+ }
+
+ // Now get the diff lines (' ', '-', '+' for that range of lines)
+
+ var patchOldStart = null;
+ var patchNewStart = null;
+ var patchOldLines = 0;
+ var patchNewLines = 0;
+ var unchangedLines = 0;
+ var patchLines = [];
+
+ function addOldLine(oldLine) {
+ if (patchOldLines == 0) {
+ patchOldStart = oldLine;
+ }
+ patchOldLines++;
+ }
+
+ function addNewLine(newLine) {
+ if (patchNewLines == 0) {
+ patchNewStart = newLine;
+ }
+ patchNewLines++;
+ }
+
+ hunk.iterate(function(loc, oldLine, oldText, newLine, newText, flags) {
+ if (loc >= hunk.location + contextFirst && loc <= comment.location) {
+ if ((flags & (Splinter.Patch.ADDED | Splinter.Patch.REMOVED | Splinter.Patch.CHANGED)) == 0) {
+ patchLines.push('> ' + oldText + Splinter.Review._noNewLine(flags, Splinter.Patch.OLD_NONEWLINE | Splinter.Patch.NEW_NONEWLINE));
+ addOldLine(oldLine);
+ addNewLine(newLine);
+ unchangedLines++;
+ } else {
+ if ((comment.type == Splinter.Patch.REMOVED
+ || comment.type == Splinter.Patch.CHANGED)
+ && oldText != null)
+ {
+ patchLines.push('> -' + oldText +
+ Splinter.Review._noNewLine(flags, Splinter.Patch.OLD_NONEWLINE));
+ addOldLine(oldLine);
+ }
+ if ((comment.type == Splinter.Patch.ADDED
+ || comment.type == Splinter.Patch.CHANGED)
+ && newText != null)
+ {
+ patchLines.push('> +' + newText +
+ Splinter.Review._noNewLine(flags, Splinter.Patch.NEW_NONEWLINE));
+ addNewLine(newLine);
+ }
+ }
+ }
+ });
+
+ // Sort them into global order ' ', '-', '+'
+ patchLines.sort(Splinter.Review._compareSegmentLines);
+
+ // Completely blank context isn't useful so remove it; however if we are commenting
+ // on blank lines at the start of a segment, we have to leave something or things break
+ while (patchLines.length > 1 && patchLines[0].match(/^\s*$/)) {
+ patchLines.shift();
+ patchOldStart++;
+ patchNewStart++;
+ patchOldLines--;
+ patchNewLines--;
+ unchangedLines--;
+ }
+
+ if (comment.type == Splinter.Patch.CHANGED) {
+ // For a CHANGED comment, we have to show the the start of the hunk - but to save
+ // in length we can trim unchanged context before it
+
+ if (patchOldLines + patchNewLines - unchangedLines > 5) {
+ var toRemove = Math.min(unchangedLines, patchOldLines + patchNewLines - unchangedLines - 5);
+ patchLines.splice(0, toRemove);
+ patchOldStart += toRemove;
+ patchNewStart += toRemove;
+ patchOldLines -= toRemove;
+ patchNewLines -= toRemove;
+ unchangedLines -= toRemove;
+ }
+
+ str += '@@ -' + patchOldStart + ',' + patchOldLines + ' +' + patchNewStart + ',' + patchNewLines + ' @@\n';
+
+ // We will use up to 10 lines more:
+ // 5 old lines or 4 old lines and a "... <N> more ... " line
+ // 5 new lines or 4 new lines and a "... <N> more ... " line
+
+ var patchRemovals = patchOldLines - unchangedLines;
+ var showPatchRemovals = patchRemovals > 5 ? 4 : patchRemovals;
+ var patchAdditions = patchNewLines - unchangedLines;
+ var showPatchAdditions = patchAdditions > 5 ? 4 : patchAdditions;
+
+ j = 0;
+ while (j < unchangedLines + showPatchRemovals) {
+ str += "> " + patchLines[j] + "\n";
+ j++;
+ }
+ if (showPatchRemovals < patchRemovals) {
+ str += "> ... " + (patchRemovals - showPatchRemovals) + " more ...\n";
+ j += patchRemovals - showPatchRemovals;
+ }
+ while (j < unchangedLines + patchRemovals + showPatchAdditions) {
+ str += "> " + patchLines[j] + "\n";
+ j++;
+ }
+ if (showPatchAdditions < patchAdditions) {
+ str += "> ... " + (patchAdditions - showPatchAdditions) + " more ...\n";
+ j += patchAdditions - showPatchAdditions;
+ }
+ } else {
+ // We limit Patch.ADDED/Patch.REMOVED comments strictly to 5 lines after the header
+ if (patchOldLines + patchNewLines - unchangedLines > 5) {
+ var toRemove = patchOldLines + patchNewLines - unchangedLines - 5;
+ patchLines.splice(0, toRemove);
+ patchOldStart += toRemove;
+ patchNewStart += toRemove;
+ patchOldLines -= toRemove;
+ patchNewLines -= toRemove;
+ }
+
+ if (comment.type == Splinter.Patch.REMOVED) {
+ str += '@@ -' + patchOldStart + ',' + patchOldLines + ' @@\n';
+ } else {
+ str += '@@ +' + patchNewStart + ',' + patchNewLines + ' @@\n';
+ }
+ str += patchLines.join("\n") + "\n";
+ }
+ str += "\n" + comment.comment + "\n";
+ }
+
+ return str;
+ }
+};
+
+Splinter.Review.Review = function(patch, who, date) {
+ this._init(patch, who, date);
+};
+
+Splinter.Review.Review.prototype = {
+ _init : function(patch, who, date) {
+ this.date = null;
+ this.patch = patch;
+ this.who = who;
+ this.date = date;
+ this.intro = null;
+ this.files = [];
+
+ var i;
+ for (i = 0; i < patch.files.length; i++) {
+ this.files.push(new Splinter.Review.File(this, patch.files[i]));
+ }
+ },
+
+ // cf. parsing in Patch.Patch._init()
+ parse : function(text) {
+ Splinter.Review.FILE_START_RE.lastIndex = 0;
+ var m = Splinter.Review.FILE_START_RE.exec(text);
+
+ var intro;
+ if (m != null) {
+ this.setIntro(text.substr(0, m.index));
+ } else{
+ this.setIntro(text);
+ return;
+ }
+
+ while (m != null) {
+ var filename = m[1];
+ var file = this.getFile(filename);
+ if (file == null) {
+ throw "Review.Review refers to filename '" + filename + "' not in reviewed Patch.";
+ }
+
+ var pos = Splinter.Review.FILE_START_RE.lastIndex;
+
+ while (true) {
+ Splinter.Review.HUNK_START_RE.lastIndex = pos;
+ var m2 = Splinter.Review.HUNK_START_RE.exec(text);
+ if (m2 == null || m2.index != pos) {
+ break;
+ }
+
+ pos = Splinter.Review.HUNK_START_RE.lastIndex;
+
+ var oldStart, oldCount, newStart, newCount;
+ if (m2[1]) {
+ oldStart = parseInt(m2[1], 10);
+ oldCount = parseInt(m2[2], 10);
+ } else {
+ oldStart = oldCount = null;
+ }
+
+ if (m2[3]) {
+ newStart = parseInt(m2[3], 10);
+ newCount = parseInt(m2[4], 10);
+ } else {
+ newStart = newCount = null;
+ }
+
+ var type;
+ if (oldStart != null && newStart != null) {
+ type = Splinter.Patch.CHANGED;
+ } else if (oldStart != null) {
+ type = Splinter.Patch.REMOVED;
+ } else if (newStart != null) {
+ type = Splinter.Patch.ADDED;
+ } else {
+ throw "Either old or new line numbers must be given";
+ }
+
+ var oldLine = oldStart;
+ var newLine = newStart;
+
+ Splinter.Review.HUNK_RE.lastIndex = pos;
+ var m3 = Splinter.Review.HUNK_RE.exec(text);
+ if (m3 == null || m3.index != pos) {
+ break;
+ }
+
+ pos = Splinter.Review.HUNK_RE.lastIndex;
+
+ var rawlines = m3[1].split("\n");
+ if (rawlines.length > 0 && rawlines[rawlines.length - 1].match('^/s+$')) {
+ rawlines.pop(); // Remove trailing element from final \n
+ }
+
+ var commentText = null;
+
+ var lastSegmentOld = 0;
+ var lastSegmentNew = 0;
+ var i;
+ for (i = 0; i < rawlines.length; i++) {
+ var line = rawlines[i];
+ var count = 1;
+ if (i < rawlines.length - 1 && rawlines[i + 1].match(/^... \d+\s+/)) {
+ var m3 = /^\.\.\.\s+(\d+)\s+/.exec(rawlines[i + 1]);
+ count += parseInt(m3[1], 10);
+ i += 1;
+ }
+ // The check for /^$/ is because if Bugzilla is line-wrapping it also
+ // strips completely whitespace lines
+ if (line.match(/^>\s+/) || line.match(/^$/)) {
+ oldLine += count;
+ newLine += count;
+ lastSegmentOld = 0;
+ lastSegmentNew = 0;
+ } else if (line.match(/^(> )?-/)) {
+ oldLine += count;
+ lastSegmentOld += count;
+ } else if (line.match(/^(> )?\+/)) {
+ newLine += count;
+ lastSegmentNew += count;
+ } else if (line.match(/^\\/)) {
+ // '\ No newline at end of file' - ignore
+ } else {
+ if (console)
+ console.log("WARNING: Bad content in hunk: " + line);
+ if (line != 'NaN more ...') {
+ // Tack onto current comment even thou it's invalid
+ if (commentText == null) {
+ commentText = line;
+ } else {
+ commentText += "\n" + line;
+ }
+ }
+ }
+
+ if ((oldStart == null || oldLine == oldStart + oldCount) &&
+ (newStart == null || newLine == newStart + newCount))
+ {
+ commentText = rawlines.slice(i + 1).join("\n");
+ break;
+ }
+ }
+
+ if (commentText == null) {
+ if (console)
+ console.log("WARNING: No comment found in hunk");
+ commentText = "";
+ }
+
+
+ var location;
+ try {
+ if (type == Splinter.Patch.CHANGED) {
+ if (lastSegmentOld >= lastSegmentNew) {
+ oldLine--;
+ }
+ if (lastSegmentOld <= lastSegmentNew) {
+ newLine--;
+ }
+ location = file.patchFile.getLocation(oldLine, newLine);
+ } else if (type == Splinter.Patch.REMOVED) {
+ oldLine--;
+ location = file.patchFile.getLocation(oldLine, null);
+ } else if (type == Splinter.Patch.ADDED) {
+ newLine--;
+ location = file.patchFile.getLocation(null, newLine);
+ }
+ } catch(e) {
+ if (console)
+ console.error(e);
+ location = 0;
+ }
+ file.addComment(location, type, Splinter.Utils.strip(commentText));
+ }
+
+ Splinter.Review.FILE_START_RE.lastIndex = pos;
+ m = Splinter.Review.FILE_START_RE.exec(text);
+ }
+ },
+
+ setIntro : function (intro) {
+ intro = Splinter.Utils.strip(intro);
+ this.intro = intro != "" ? intro : null;
+ },
+
+ getFile : function (filename) {
+ var i;
+ for (i = 0; i < this.files.length; i++) {
+ if (this.files[i].patchFile.filename == filename) {
+ return this.files[i];
+ }
+ }
+
+ return null;
+ },
+
+ // Making toString() serialize to our seriaization format is maybe a bit sketchy
+ // But the serialization format is designed to be human readable so it works
+ // pretty well.
+ toString : function () {
+ var str = '';
+ if (this.intro != null) {
+ str += Splinter.Utils.strip(this.intro);
+ str += '\n';
+ }
+
+ var first = this.intro == null;
+ var i;
+ for (i = 0; i < this.files.length; i++) {
+ var file = this.files[i];
+ if (file.comments.length > 0) {
+ if (first) {
+ first = false;
+ } else {
+ str += '\n';
+ }
+ str += file.toString();
+ }
+ }
+
+ return str;
+ }
+};
+
+Splinter.ReviewStorage = {};
+
+Splinter.ReviewStorage.LocalReviewStorage = function() {
+ this._init();
+};
+
+Splinter.ReviewStorage.LocalReviewStorage.available = function() {
+ // The try is a workaround for
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=517778
+ // where if cookies are disabled or set to ask, then the first attempt
+ // to access the localStorage property throws a security error.
+ try {
+ return 'localStorage' in window && window.localStorage != null;
+ } catch (e) {
+ return false;
+ }
+};
+
+Splinter.ReviewStorage.LocalReviewStorage.prototype = {
+ _init : function() {
+ var reviewInfosText = localStorage.splinterReviews;
+ if (reviewInfosText == null) {
+ this._reviewInfos = [];
+ } else {
+ this._reviewInfos = YAHOO.lang.JSON.parse(reviewInfosText);
+ }
+ },
+
+ listReviews : function() {
+ return this._reviewInfos;
+ },
+
+ _reviewPropertyName : function(bug, attachment) {
+ return 'splinterReview_' + bug.id + '_' + attachment.id;
+ },
+
+ loadDraft : function(bug, attachment, patch) {
+ var propertyName = this._reviewPropertyName(bug, attachment);
+ var reviewText = localStorage[propertyName];
+ if (reviewText != null) {
+ var review = new Splinter.Review.Review(patch);
+ review.parse(reviewText);
+ return review;
+ } else {
+ return null;
+ }
+ },
+
+ _findReview : function(bug, attachment) {
+ var i;
+ for (i = 0 ; i < this._reviewInfos.length; i++) {
+ if (this._reviewInfos[i].bugId == bug.id && this._reviewInfos[i].attachmentId == attachment.id) {
+ return i;
+ }
+ }
+
+ return -1;
+ },
+
+ _updateOrCreateReviewInfo : function(bug, attachment, props) {
+ var reviewIndex = this._findReview(bug, attachment);
+ var reviewInfo;
+
+ var nowTime = Date.now();
+ if (reviewIndex >= 0) {
+ reviewInfo = this._reviewInfos[reviewIndex];
+ this._reviewInfos.splice(reviewIndex, 1);
+ } else {
+ reviewInfo = {
+ bugId: bug.id,
+ bugShortDesc: bug.shortDesc,
+ attachmentId: attachment.id,
+ attachmentDescription: attachment.description,
+ creationTime: nowTime
+ };
+ }
+
+ reviewInfo.modificationTime = nowTime;
+ for (var prop in props) {
+ reviewInfo[prop] = props[prop];
+ }
+
+ this._reviewInfos.push(reviewInfo);
+ localStorage.splinterReviews = YAHOO.lang.JSON.stringify(this._reviewInfos);
+ },
+
+ _deleteReviewInfo : function(bug, attachment) {
+ var reviewIndex = this._findReview(bug, attachment);
+ if (reviewIndex >= 0) {
+ this._reviewInfos.splice(reviewIndex, 1);
+ localStorage.splinterReviews = YAHOO.lang.JSON.stringify(this._reviewInfos);
+ }
+ },
+
+ saveDraft : function(bug, attachment, review, extraProps) {
+ var propertyName = this._reviewPropertyName(bug, attachment);
+ if (!extraProps) {
+ extraProps = {};
+ }
+ extraProps.isDraft = true;
+ this._updateOrCreateReviewInfo(bug, attachment, extraProps);
+ localStorage[propertyName] = "" + review;
+ },
+
+ deleteDraft : function(bug, attachment, review) {
+ var propertyName = this._reviewPropertyName(bug, attachment);
+
+ this._deleteReviewInfo(bug, attachment);
+ delete localStorage[propertyName];
+ },
+
+ draftPublished : function(bug, attachment) {
+ var propertyName = this._reviewPropertyName(bug, attachment);
+
+ this._updateOrCreateReviewInfo(bug, attachment, { isDraft: false });
+ delete localStorage[propertyName];
+ }
+};
+
+Splinter.saveDraftNoticeTimeoutId = null;
+Splinter.navigationLinks = {};
+Splinter.reviewers = {};
+Splinter.savingDraft = false;
+Splinter.UPDATE_ATTACHMENT_SUCCESS = /<title>\s*Changes\s+Submitted/;
+Splinter.LINE_RE = /(?!$)([^\r\n]*)(?:\r\n|\r|\n|$)/g;
+
+Splinter.displayError = function (msg) {
+ var el = new Element(document.createElement('p'));
+ el.appendChild(document.createTextNode(msg));
+ Dom.get('error').appendChild(Dom.get(el));
+ Dom.setStyle('error', 'display', 'block');
+};
+
+Splinter.publishReview = function () {
+ Splinter.saveComment();
+ Splinter.theReview.setIntro(Dom.get('myComment').value);
+
+ if (Splinter.reviewStorage) {
+ Splinter.reviewStorage.draftPublished(Splinter.theBug,
+ Splinter.theAttachment);
+ }
+
+ var publish_form = Dom.get('publish');
+ var publish_token = Dom.get('publish_token');
+ var publish_attach_id = Dom.get('publish_attach_id');
+ var publish_attach_desc = Dom.get('publish_attach_desc');
+ var publish_attach_filename = Dom.get('publish_attach_filename');
+ var publish_attach_contenttype = Dom.get('publish_attach_contenttype');
+ var publish_attach_ispatch = Dom.get('publish_attach_ispatch');
+ var publish_attach_isobsolete = Dom.get('publish_attach_isobsolete');
+ var publish_attach_isprivate = Dom.get('publish_attach_isprivate');
+ var publish_attach_status = Dom.get('publish_attach_status');
+ var publish_review = Dom.get('publish_review');
+
+ publish_token.value = Splinter.theAttachment.token;
+ publish_attach_id.value = Splinter.theAttachment.id;
+ publish_attach_desc.value = Splinter.theAttachment.description;
+ publish_attach_filename.value = Splinter.theAttachment.filename;
+ publish_attach_contenttype.value = Splinter.theAttachment.contenttypeentry;
+ publish_attach_ispatch.value = Splinter.theAttachment.isPatch;
+ publish_attach_isobsolete.value = Splinter.theAttachment.isObsolete;
+ publish_attach_isprivate.value = Splinter.theAttachment.isPrivate;
+
+ // This is a "magic string" used to identify review comments
+ if (Splinter.theReview.toString()) {
+ var comment = "Review of attachment " + Splinter.theAttachment.id + ":\n" +
+ "-----------------------------------------------------------------\n\n" +
+ Splinter.theReview.toString();
+ publish_review.value = comment;
+ }
+
+ if (Splinter.theAttachment.status
+ && Dom.get('attachmentStatus').value != Splinter.theAttachment.status)
+ {
+ publish_attach_status.value = Dom.get('attachmentStatus').value;
+ }
+
+ publish_form.submit();
+};
+
+Splinter.doDiscardReview = function () {
+ if (Splinter.theAttachment.status) {
+ Dom.get('attachmentStatus').value = Splinter.theAttachment.status;
+ }
+
+ Dom.get('myComment').value = '';
+ Dom.setStyle('emptyCommentNotice', 'display', 'block');
+
+ var i;
+ for (i = 0; i < Splinter.theReview.files.length; i++) {
+ while (Splinter.theReview.files[i].comments.length > 0) {
+ Splinter.theReview.files[i].comments[0].remove();
+ }
+ }
+
+ Splinter.updateMyPatchComments();
+ Splinter.updateHaveDraft();
+ Splinter.saveDraft();
+};
+
+Splinter.discardReview = function () {
+ var dialog = new Splinter.Dialog("Really discard your changes?");
+ dialog.addButton('No', function() {}, true);
+ dialog.addButton('Yes', Splinter.doDiscardReview, false);
+ dialog.show();
+};
+
+Splinter.haveDraft = function () {
+ if (Splinter.readOnly) {
+ return false;
+ }
+
+ if (Splinter.theAttachment.status && Dom.get('attachmentStatus').value != Splinter.theAttachment.status) {
+ return true;
+ }
+
+ if (Dom.get('myComment').value != '') {
+ return true;
+ }
+
+ var i;
+ for (i = 0; i < Splinter.theReview.files.length; i++) {
+ if (Splinter.theReview.files[i].comments.length > 0) {
+ return true;
+ }
+ }
+
+ for (i = 0; i < Splinter.thePatch.files.length; i++) {
+ if (Splinter.thePatch.files[i].fileReviewed) {
+ return true;
+ }
+ }
+
+ if (Splinter.flagChanged == 1) {
+ return true;
+ }
+
+ return false;
+};
+
+Splinter.updateHaveDraft = function () {
+ clearTimeout(Splinter.updateHaveDraftTimeoutId);
+ Splinter.updateHaveDraftTimeoutId = null;
+
+ if (Splinter.haveDraft()) {
+ Dom.get('publishButton').removeAttribute('disabled');
+ Dom.get('cancelButton').removeAttribute('disabled');
+ Dom.setStyle('haveDraftNotice', 'display', 'block');
+ } else {
+ Dom.get('publishButton').setAttribute('disabled', 'true');
+ Dom.get('cancelButton').setAttribute('disabled', 'true');
+ Dom.setStyle('haveDraftNotice', 'display', 'none');
+ }
+};
+
+Splinter.queueUpdateHaveDraft = function () {
+ if (Splinter.updateHaveDraftTimeoutId == null) {
+ Splinter.updateHaveDraftTimeoutId = setTimeout(Splinter.updateHaveDraft, 0);
+ }
+};
+
+Splinter.hideSaveDraftNotice = function () {
+ clearTimeout(Splinter.saveDraftNoticeTimeoutId);
+ Splinter.saveDraftNoticeTimeoutId = null;
+ Dom.setStyle('saveDraftNotice', 'display', 'none');
+};
+
+Splinter.saveDraft = function () {
+ if (Splinter.reviewStorage == null) {
+ return;
+ }
+
+ clearTimeout(Splinter.saveDraftTimeoutId);
+ Splinter.saveDraftTimeoutId = null;
+
+ Splinter.savingDraft = true;
+ Dom.get('saveDraftNotice').innerHTML = "Saving Draft...";
+ Dom.setStyle('saveDraftNotice', 'display', 'block');
+ clearTimeout(Splinter.saveDraftNoticeTimeoutId);
+ setTimeout(Splinter.hideSaveDraftNotice, 3000);
+
+ if (Splinter.currentEditComment) {
+ Splinter.currentEditComment.comment = Splinter.Utils.strip(Dom.get("commentEditor").getElementsByTagName("textarea")[0].value);
+ // Messy, we don't want the empty comment in the saved draft, so remove it and
+ // then add it back.
+ if (!Splinter.currentEditComment.comment) {
+ Splinter.currentEditComment.remove();
+ }
+ }
+
+ Splinter.theReview.setIntro(Dom.get('myComment').value);
+
+ var draftSaved = false;
+ if (Splinter.haveDraft()) {
+ var filesReviewed = {};
+ for (var i = 0; i < Splinter.thePatch.files.length; i++) {
+ var file = Splinter.thePatch.files[i];
+ if (file.fileReviewed) {
+ filesReviewed[file.filename] = true;
+ }
+ }
+ Splinter.reviewStorage.saveDraft(Splinter.theBug, Splinter.theAttachment, Splinter.theReview,
+ { 'filesReviewed' : filesReviewed });
+ draftSaved = true;
+ } else {
+ Splinter.reviewStorage.deleteDraft(Splinter.theBug, Splinter.theAttachment, Splinter.theReview);
+ }
+
+ if (Splinter.currentEditComment && !Splinter.currentEditComment.comment) {
+ Splinter.currentEditComment = Splinter.currentEditComment.file.addComment(Splinter.currentEditComment.location,
+ Splinter.currentEditComment.type, "");
+ }
+
+ Splinter.savingDraft = false;
+ if (draftSaved) {
+ Dom.get('saveDraftNotice').innerHTML = "Saved Draft";
+ } else {
+ Splinter.hideSaveDraftNotice();
+ }
+};
+
+Splinter.queueSaveDraft = function () {
+ if (Splinter.saveDraftTimeoutId == null) {
+ Splinter.saveDraftTimeoutId = setTimeout(Splinter.saveDraft, 10000);
+ }
+};
+
+Splinter.flushSaveDraft = function () {
+ if (Splinter.saveDraftTimeoutId != null) {
+ Splinter.saveDraft();
+ }
+};
+
+Splinter.ensureCommentArea = function (row) {
+ var file = Splinter.domCache.data(row).patchFile;
+ var colSpan = file.status == Splinter.Patch.CHANGED ? 5 : 2;
+
+ if (!row.nextSibling || row.nextSibling.className != "comment-area") {
+ var tr = new Element(document.createElement('tr'));
+ Dom.addClass(tr, 'comment-area');
+ var td = new Element(document.createElement('td'));
+ Dom.setAttribute(td, 'colspan', colSpan);
+ td.appendTo(tr);
+ Dom.insertAfter(tr, row);
+ }
+
+ return row.nextSibling.firstChild;
+};
+
+Splinter.getTypeClass = function (type) {
+ switch (type) {
+ case Splinter.Patch.ADDED:
+ return "comment-added";
+ case Splinter.Patch.REMOVED:
+ return "comment-removed";
+ case Splinter.Patch.CHANGED:
+ return "comment-changed";
+ }
+
+ return null;
+};
+
+Splinter.getSeparatorClass = function (type) {
+ switch (type) {
+ case Splinter.Patch.ADDED:
+ return "comment-separator-added";
+ case Splinter.Patch.REMOVED:
+ return "comment-separator-removed";
+ }
+
+ return null;
+};
+
+Splinter.getReviewerClass = function (review) {
+ var reviewerIndex;
+ if (review == Splinter.theReview) {
+ reviewerIndex = 0;
+ } else {
+ reviewerIndex = (Splinter.reviewers[review.who] - 1) % 5 + 1;
+ }
+
+ return "reviewer-" + reviewerIndex;
+};
+
+Splinter.addCommentDisplay = function (commentArea, comment) {
+ var review = comment.file.review;
+
+ var separatorClass = Splinter.getSeparatorClass(comment.type);
+ if (separatorClass) {
+ var div = new Element(document.createElement('div'));
+ Dom.addClass(div, separatorClass);
+ Dom.addClass(div, Splinter.getReviewerClass(review));
+ div.appendTo(commentArea);
+ }
+
+ var commentDiv = new Element(document.createElement('div'));
+ Dom.addClass(commentDiv, 'comment');
+ Dom.addClass(commentDiv, Splinter.getTypeClass(comment.type));
+ Dom.addClass(commentDiv, Splinter.getReviewerClass(review));
+
+ Event.addListener(Dom.get(commentDiv), 'dblclick', function () {
+ Splinter.saveComment();
+ Splinter.insertCommentEditor(commentArea, comment.file.patchFile,
+ comment.location, comment.type);
+ });
+
+ var commentFrame = new Element(document.createElement('div'));
+ Dom.addClass(commentFrame, 'comment-frame');
+ commentFrame.appendTo(commentDiv);
+
+ var reviewerBox = new Element(document.createElement('div'));
+ Dom.addClass(reviewerBox, 'reviewer-box');
+ reviewerBox.appendTo(commentFrame);
+
+ var commentText = new Element(document.createElement('div'));
+ Dom.addClass(commentText, 'comment-text');
+ Splinter.Utils.preWrapLines(commentText, comment.comment);
+ commentText.appendTo(reviewerBox);
+
+ commentDiv.appendTo(commentArea);
+
+ if (review != Splinter.theReview) {
+ var reviewInfo = new Element(document.createElement('div'));
+ Dom.addClass(reviewInfo, 'review-info');
+
+ var reviewer = new Element(document.createElement('div'));
+ Dom.addClass(reviewer, 'reviewer');
+ reviewer.appendChild(document.createTextNode(review.who));
+ reviewer.appendTo(reviewInfo);
+
+ var reviewDate = new Element(document.createElement('div'));
+ Dom.addClass(reviewDate, 'review-date');
+ reviewDate.appendChild(document.createTextNode(Splinter.Utils.formatDate(review.date)));
+ reviewDate.appendTo(reviewInfo);
+
+ var reviewInfoBottom = new Element(document.createElement('div'));
+ Dom.addClass(reviewInfoBottom, 'review-info-bottom');
+ reviewInfoBottom.appendTo(reviewInfo);
+
+ reviewInfo.appendTo(reviewerBox);
+ }
+
+ comment.div = commentDiv;
+};
+
+Splinter.saveComment = function () {
+ var comment = Splinter.currentEditComment;
+ if (!comment) {
+ return;
+ }
+
+ var commentEditor = Dom.get('commentEditor');
+ var commentArea = commentEditor.parentNode;
+ var reviewFile = comment.file;
+
+ var hunk = comment.getHunk();
+ var line = hunk.lines[comment.location - hunk.location];
+
+ var value = Splinter.Utils.strip(commentEditor.getElementsByTagName('textarea')[0].value);
+ if (value != "") {
+ comment.comment = value;
+ Splinter.addCommentDisplay(commentArea, comment);
+ } else {
+ comment.remove();
+ }
+
+ if (line.reviewComments.length > 0) {
+ commentEditor.parentNode.removeChild(commentEditor);
+ var commentEditorSeparator = Dom.get('commentEditorSeparator');
+ if (commentEditorSeparator) {
+ commentEditorSeparator.parentNode.removeChild(commentEditorSeparator);
+ }
+ } else {
+ var parentToRemove = commentArea.parentNode;
+ commentArea.parentNode.parentNode.removeChild(parentToRemove);
+ }
+
+ Splinter.currentEditComment = null;
+ Splinter.saveDraft();
+ Splinter.queueUpdateHaveDraft();
+};
+
+Splinter.cancelComment = function (previousText) {
+ Dom.get("commentEditor").getElementsByTagName("textarea")[0].value = previousText;
+ Splinter.saveComment();
+};
+
+Splinter.deleteComment = function () {
+ Dom.get('commentEditor').getElementsByTagName('textarea')[0].value = "";
+ Splinter.saveComment();
+};
+
+Splinter.insertCommentEditor = function (commentArea, file, location, type) {
+ Splinter.saveComment();
+
+ var reviewFile = Splinter.theReview.getFile(file.filename);
+ var comment = reviewFile.getComment(location, type);
+ if (!comment) {
+ comment = reviewFile.addComment(location, type, "");
+ Splinter.queueUpdateHaveDraft();
+ }
+
+ var previousText = comment.comment;
+
+ var typeClass = Splinter.getTypeClass(type);
+ var separatorClass = Splinter.getSeparatorClass(type);
+
+ var nodes = Dom.getElementsByClassName('reviewer-0', 'div', commentArea);
+ var i;
+ for (i = 0; i < nodes.length; i++) {
+ if (separatorClass && Dom.hasClass(nodes[i], separatorClass)) {
+ nodes[i].parentNode.removeChild(nodes[i]);
+ }
+ if (Dom.hasClass(nodes[i], typeClass)) {
+ nodes[i].parentNode.removeChild(nodes[i]);
+ }
+ }
+
+ if (separatorClass) {
+ var commentEditorSeparator = new Element(document.createElement('div'));
+ commentEditorSeparator.set('id', 'commentEditorSeparator');
+ Dom.addClass(commentEditorSeparator, separatorClass);
+ commentEditorSeparator.appendTo(commentArea);
+ }
+
+ var commentEditor = new Element(document.createElement('div'));
+ Dom.setAttribute(commentEditor, 'id', 'commentEditor');
+ Dom.addClass(commentEditor, typeClass);
+ commentEditor.appendTo(commentArea);
+
+ var commentEditorInner = new Element(document.createElement('div'));
+ Dom.setAttribute(commentEditorInner, 'id', 'commentEditorInner');
+ commentEditorInner.appendTo(commentEditor);
+
+ var commentTextFrame = new Element(document.createElement('div'));
+ Dom.setAttribute(commentTextFrame, 'id', 'commentTextFrame');
+ commentTextFrame.appendTo(commentEditorInner);
+
+ var commentTextArea = new Element(document.createElement('textarea'));
+ Dom.setAttribute(commentTextArea, 'id', 'commentTextArea');
+ Dom.setAttribute(commentTextArea, 'tabindex', 1);
+ commentTextArea.appendChild(document.createTextNode(previousText));
+ commentTextArea.appendTo(commentTextFrame);
+ Event.addListener('commentTextArea', 'keydown', function (e) {
+ if (e.which == 13 && e.ctrlKey) {
+ Splinter.saveComment();
+ } else if (e.which == 27) {
+ var comment = Dom.get('commentTextArea').value;
+ if (previousText == comment || comment == '') {
+ Splinter.cancelComment(previousText);
+ }
+ } else {
+ Splinter.queueSaveDraft();
+ }
+ });
+ Event.addListener('commentTextArea', 'focusin', function () { Dom.addClass(commentEditor, 'focused'); });
+ Event.addListener('commentTextArea', 'focusout', function () { Dom.removeClass(commentEditor, 'focused'); });
+ Dom.get(commentTextArea).focus();
+
+ var commentEditorLeftButtons = new Element(document.createElement('div'));
+ commentEditorLeftButtons.set('id', 'commentEditorLeftButtons');
+ commentEditorLeftButtons.appendTo(commentEditorInner);
+
+ var commentCancel = new Element(document.createElement('input'));
+ commentCancel.set('id','commentCancel');
+ commentCancel.set('type', 'button');
+ commentCancel.set('value', 'Cancel');
+ Dom.setAttribute(commentCancel, 'tabindex', 4);
+ commentCancel.appendTo(commentEditorLeftButtons);
+ Event.addListener('commentCancel', 'click', function () { Splinter.cancelComment(previousText); });
+
+ if (previousText) {
+ var commentDelete = new Element(document.createElement('input'));
+ commentDelete.set('id','commentDelete');
+ commentDelete.set('type', 'button');
+ commentDelete.set('value', 'Delete');
+ Dom.setAttribute(commentDelete, 'tabindex', 3);
+ commentDelete.appendTo(commentEditorLeftButtons);
+ Event.addListener('commentDelete', 'click', Splinter.deleteComment);
+ }
+
+ var commentEditorRightButtons = new Element(document.createElement('div'));
+ commentEditorRightButtons.set('id', 'commentEditorRightButtons');
+ commentEditorRightButtons.appendTo(commentEditorInner);
+
+ var commentSave = new Element(document.createElement('input'));
+ commentSave.set('id','commentSave');
+ commentSave.set('type', 'button');
+ commentSave.set('value', 'Save');
+ Dom.setAttribute(commentSave, 'tabindex', 2);
+ commentSave.appendTo(commentEditorRightButtons);
+ Event.addListener('commentSave', 'click', Splinter.saveComment);
+
+ var clear = new Element(document.createElement('div'));
+ Dom.addClass(clear, 'clear');
+ clear.appendTo(commentEditorInner);
+
+ Splinter.currentEditComment = comment;
+};
+
+Splinter.insertCommentForRow = function (clickRow, clickType) {
+ var file = Splinter.domCache.data(clickRow).patchFile;
+ var clickLocation = Splinter.domCache.data(clickRow).patchLocation;
+
+ var row = clickRow;
+ var location = clickLocation;
+ var type = clickType;
+
+ Splinter.saveComment();
+ var commentArea = Splinter.ensureCommentArea(row);
+ Splinter.insertCommentEditor(commentArea, file, location, type);
+};
+
+Splinter.EL = function (element, cls, text, title) {
+ var e = document.createElement(element);
+ if (text != null) {
+ e.appendChild(document.createTextNode(text));
+ }
+ if (cls) {
+ e.className = cls;
+ }
+ if (title) {
+ Dom.setAttribute(e, 'title', title);
+ }
+
+ return e;
+};
+
+Splinter.textTD = function (cls, text, title) {
+ if (text == "") {
+ return Splinter.EL("td", cls, "\u00a0", title);
+ }
+ var m = text.match(/^(.*?)(\s+)$/);
+ if (m) {
+ var td = Splinter.EL("td", cls, m[1], title);
+ td.insertBefore(Splinter.EL("span", cls + " trailing-whitespace", m[2], title), null);
+ return td;
+ } else {
+ return Splinter.EL("td", cls, text, title);
+ }
+}
+
+Splinter.getElementPosition = function (element) {
+ var left = element.offsetLeft;
+ var top = element.offsetTop;
+ var parent = element.offsetParent;
+ while (parent && parent != document.body) {
+ left += parent.offsetLeft;
+ top += parent.offsetTop;
+ parent = parent.offsetParent;
+ }
+
+ return [left, top];
+};
+
+Splinter.scrollToElement = function (element) {
+ var windowHeight;
+ if ('innerHeight' in window) { // Not IE
+ windowHeight = window.innerHeight;
+ } else { // IE
+ windowHeight = document.documentElement.clientHeight;
+ }
+ var pos = Splinter.getElementPosition(element);
+ var yCenter = pos[1] + element.offsetHeight / 2;
+ window.scrollTo(0, yCenter - windowHeight / 2);
+};
+
+Splinter.onRowDblClick = function (e) {
+ var file = Splinter.domCache.data(this).patchFile;
+ var type;
+
+ if (file.status == Splinter.Patch.CHANGED) {
+ var pos = Splinter.getElementPosition(this);
+ var delta = e.pageX - (pos[0] + this.offsetWidth/2);
+ if (delta < - 20) {
+ type = Splinter.Patch.REMOVED;
+ } else if (delta < 20) {
+ // CHANGED comments disabled due to breakage
+ // type = Splinter.Patch.CHANGED;
+ type = Splinter.Patch.ADDED;
+ } else {
+ type = Splinter.Patch.ADDED;
+ }
+ } else {
+ type = file.status;
+ }
+
+ Splinter.insertCommentForRow(this, type);
+};
+
+Splinter.appendPatchTable = function (type, maxLine, parentDiv) {
+ var fileTableContainer = new Element(document.createElement('div'));
+ Dom.addClass(fileTableContainer, 'file-table-container');
+ fileTableContainer.appendTo(parentDiv);
+
+ var fileTable = new Element(document.createElement('table'));
+ Dom.addClass(fileTable, 'file-table');
+ fileTable.appendTo(fileTableContainer);
+
+ var colQ = new Element(document.createElement('colgroup'));
+ colQ.appendTo(fileTable);
+
+ var col1, col2;
+ if (type != Splinter.Patch.ADDED) {
+ col1 = new Element(document.createElement('col'));
+ Dom.addClass(col1, 'line-number-column');
+ Dom.setAttribute(col1, 'span', '1');
+ col1.appendTo(colQ);
+ col2 = new Element(document.createElement('col'));
+ Dom.addClass(col2, 'old-column');
+ Dom.setAttribute(col2, 'span', '1');
+ col2.appendTo(colQ);
+ }
+ if (type == Splinter.Patch.CHANGED) {
+ col1 = new Element(document.createElement('col'));
+ Dom.addClass(col1, 'middle-column');
+ Dom.setAttribute(col1, 'span', '1');
+ col1.appendTo(colQ);
+ }
+ if (type != Splinter.Patch.REMOVED) {
+ col1 = new Element(document.createElement('col'));
+ Dom.addClass(col1, 'line-number-column');
+ Dom.setAttribute(col1, 'span', '1');
+ col1.appendTo(colQ);
+ col2 = new Element(document.createElement('col'));
+ Dom.addClass(col2, 'new-column');
+ Dom.setAttribute(col2, 'span', '1');
+ col2.appendTo(colQ);
+ }
+
+ if (type == Splinter.Patch.CHANGED) {
+ Dom.addClass(fileTable, 'file-table-changed');
+ }
+
+ if (maxLine >= 1000) {
+ Dom.addClass(fileTable, "file-table-wide-numbers");
+ }
+
+ var tbody = new Element(document.createElement('tbody'));
+ tbody.appendTo(fileTable);
+
+ return tbody;
+};
+
+Splinter.appendPatchHunk = function (file, hunk, tableType, includeComments, clickable, tbody, filter) {
+ hunk.iterate(function(loc, oldLine, oldText, newLine, newText, flags, line) {
+ if (filter && !filter(loc)) {
+ return;
+ }
+
+ var tr = document.createElement("tr");
+
+ var oldStyle = "";
+ var newStyle = "";
+ if ((flags & Splinter.Patch.CHANGED) != 0) {
+ oldStyle = newStyle = "changed-line";
+ } else if ((flags & Splinter.Patch.REMOVED) != 0) {
+ oldStyle = "removed-line";
+ } else if ((flags & Splinter.Patch.ADDED) != 0) {
+ newStyle = "added-line";
+ }
+
+ var title = "Double click the line to add a review comment";
+
+ if (tableType != Splinter.Patch.ADDED) {
+ if (oldText != null) {
+ tr.appendChild(Splinter.EL("td", "line-number", oldLine.toString(), title));
+ tr.appendChild(Splinter.textTD("old-line " + oldStyle, oldText, title));
+ oldLine++;
+ } else {
+ tr.appendChild(Splinter.EL("td", "line-number"));
+ tr.appendChild(Splinter.EL("td", "old-line"));
+ }
+ }
+
+ if (tableType == Splinter.Patch.CHANGED) {
+ tr.appendChild(Splinter.EL("td", "line-middle"));
+ }
+
+ if (tableType != Splinter.Patch.REMOVED) {
+ if (newText != null) {
+ tr.appendChild(Splinter.EL("td", "line-number", newLine.toString(), title));
+ tr.appendChild(Splinter.textTD("new-line " + newStyle, newText, title));
+ newLine++;
+ } else if (tableType == Splinter.Patch.CHANGED) {
+ tr.appendChild(Splinter.EL("td", "line-number"));
+ tr.appendChild(Splinter.EL("td", "new-line"));
+ }
+ }
+
+ if (!Splinter.readOnly && clickable) {
+ Splinter.domCache.data(tr).patchFile = file;
+ Splinter.domCache.data(tr).patchLocation = loc;
+ Event.addListener(tr, 'dblclick', Splinter.onRowDblClick);
+ }
+
+ tbody.appendChild(tr);
+
+ if (includeComments && line.reviewComments != null) {
+ var k;
+ for (k = 0; k < line.reviewComments.length; k++) {
+ var commentArea = Splinter.ensureCommentArea(tr);
+ Splinter.addCommentDisplay(commentArea, line.reviewComments[k]);
+ }
+ }
+ });
+};
+
+Splinter.addPatchFile = function (file) {
+ var fileDiv = new Element(document.createElement('div'));
+ Dom.addClass(fileDiv, 'file');
+ fileDiv.appendTo(Dom.get('splinter-files'));
+ file.div = fileDiv;
+
+ var statusString;
+ switch (file.status) {
+ case Splinter.Patch.ADDED:
+ statusString = " (new file)";
+ break;
+ case Splinter.Patch.REMOVED:
+ statusString = " (removed)";
+ break;
+ case Splinter.Patch.CHANGED:
+ statusString = "";
+ break;
+ }
+
+ var fileLabel = new Element(document.createElement('div'));
+ Dom.addClass(fileLabel, 'file-label');
+ fileLabel.appendTo(fileDiv);
+
+ var fileCollapseLink = new Element(document.createElement('a'));
+ Dom.addClass(fileCollapseLink, 'file-label-collapse');
+ fileCollapseLink.appendChild(document.createTextNode('[-]'));
+ Dom.setAttribute(fileCollapseLink, 'href', 'javascript:void(0);')
+ Dom.setAttribute(fileCollapseLink, 'onclick', "Splinter.toggleCollapsed('" +
+ encodeURIComponent(file.filename) + "');");
+ Dom.setAttribute(fileCollapseLink, 'title', 'Click to expand or collapse this file table');
+ fileCollapseLink.appendTo(fileLabel);
+
+ var fileLabelName = new Element(document.createElement('span'));
+ Dom.addClass(fileLabelName, 'file-label-name');
+ fileLabelName.appendChild(document.createTextNode(file.filename));
+ fileLabelName.appendTo(fileLabel);
+
+ var fileLabelStatus = new Element(document.createElement('span'));
+ Dom.addClass(fileLabelStatus, 'file-label-status');
+ fileLabelStatus.appendChild(document.createTextNode(statusString));
+ fileLabelStatus.appendTo(fileLabel);
+
+ if (!Splinter.readOnly) {
+ var fileReviewed = new Element(document.createElement('span'));
+ Dom.addClass(fileReviewed, 'file-review');
+ Dom.setAttribute(fileReviewed, 'title', 'Indicates that a review has been completed for this file. ' +
+ 'This is for personal tracking purposes only and has no effect ' +
+ 'on the published review.');
+ fileReviewed.appendTo(fileLabel);
+
+ var fileReviewedInput = new Element(document.createElement('input'));
+ Dom.setAttribute(fileReviewedInput, 'type', 'checkbox');
+ Dom.setAttribute(fileReviewedInput, 'id', 'file-review-checkbox-' + encodeURIComponent(file.filename));
+ Dom.setAttribute(fileReviewedInput, 'onchange', "Splinter.toggleFileReviewed('" +
+ encodeURIComponent(file.filename) + "');");
+ if (file.fileReviewed) {
+ Dom.setAttribute(fileReviewedInput, 'checked', 'true');
+ }
+ fileReviewedInput.appendTo(fileReviewed);
+
+ var fileReviewedLabel = new Element(document.createElement('label'));
+ Dom.addClass(fileReviewedLabel, 'file-review-label')
+ Dom.setAttribute(fileReviewedLabel, 'for', 'file-review-checkbox-' + encodeURIComponent(file.filename));
+ fileReviewedLabel.appendChild(document.createTextNode(' Reviewed'));
+ fileReviewedLabel.appendTo(fileReviewed);
+ }
+
+ if (file.extra) {
+ var extraContainer = new Element(document.createElement('div'));
+ Dom.addClass(extraContainer, 'file-extra-container');
+ var extraMargin = new Element(document.createElement('span'));
+ Dom.addClass(extraMargin, 'file-label-collapse');
+ extraMargin.appendChild(document.createTextNode('\u00a0\u00a0\u00a0'));
+ extraMargin.appendTo(extraContainer);
+ var extraLabel = new Element(document.createElement('span'));
+ Dom.addClass(extraLabel, 'file-label-extra');
+ extraLabel.appendChild(document.createTextNode(file.extra));
+ extraLabel.appendTo(extraContainer);
+ extraContainer.appendTo(fileLabel);
+ }
+
+ if (file.hunks.length == 0)
+ return;
+
+ var lastHunk = file.hunks[file.hunks.length - 1];
+ var lastLine = Math.max(lastHunk.oldStart + lastHunk.oldCount - 1,
+ lastHunk.newStart + lastHunk.newCount - 1);
+
+ var tbody = Splinter.appendPatchTable(file.status, lastLine, fileDiv);
+
+ var i;
+ for (i = 0; i < file.hunks.length; i++) {
+ var hunk = file.hunks[i];
+ if (hunk.oldStart > 1) {
+ var hunkHeader = Splinter.EL("tr", "hunk-header");
+ tbody.appendChild(hunkHeader);
+ hunkHeader.appendChild(Splinter.EL("td")); // line number column
+ var hunkCell = Splinter.EL(
+ "td",
+ "hunk-cell",
+ "Lines " + hunk.oldStart + '-' +
+ Math.max(hunk.oldStart + hunk.oldCount - 1, hunk.newStart + hunk.newCount - 1) +
+ "\u00a0\u00a0" + hunk.functionLine
+ );
+ hunkCell.colSpan = file.status == Splinter.Patch.CHANGED ? 4 : 1;
+ hunkHeader.appendChild(hunkCell);
+ }
+
+ Splinter.appendPatchHunk(file, hunk, file.status, true, true, tbody);
+ }
+};
+
+Splinter.appendReviewComment = function (comment, parentDiv) {
+ var commentDiv = Splinter.EL("div", "review-patch-comment");
+ Event.addListener(commentDiv, 'click', function() {
+ Splinter.showPatchFile(comment.file.patchFile);
+ if (comment.file.review == Splinter.theReview) {
+ // Immediately start editing the comment again
+ var commentDivParent = Dom.getAncestorByClassName(comment.div, 'comment-area');
+ var commentArea = commentDivParent.getElementsByTagName('td')[0];
+ Splinter.insertCommentEditor(commentArea, comment.file.patchFile, comment.location, comment.type);
+ Splinter.scrollToElement(Dom.get('commentEditor'));
+ } else {
+ // Just scroll to the comment, don't start a reply yet
+ Splinter.scrollToElement(Dom.get(comment.div));
+ }
+ });
+
+ var inReplyTo = comment.getInReplyTo();
+ if (inReplyTo) {
+ var div = new Element(document.createElement('div'));
+ Dom.addClass(div, Splinter.getReviewerClass(inReplyTo.file.review));
+ div.appendTo(commentDiv);
+
+ var reviewerBox = new Element(document.createElement('div'));
+ Dom.addClass(reviewerBox, 'reviewer-box');
+ Splinter.Utils.preWrapLines(reviewerBox, inReplyTo.comment);
+ reviewerBox.appendTo(div);
+
+ var reviewPatchCommentText = new Element(document.createElement('div'));
+ Dom.addClass(reviewPatchCommentText, 'review-patch-comment-text');
+ Splinter.Utils.preWrapLines(reviewPatchCommentText, comment.comment);
+ reviewPatchCommentText.appendTo(commentDiv);
+
+ } else {
+ var hunk = comment.getHunk();
+
+ var lastLine = Math.max(hunk.oldStart + hunk.oldCount- 1,
+ hunk.newStart + hunk.newCount- 1);
+ var tbody = Splinter.appendPatchTable(comment.type, lastLine, commentDiv);
+
+ Splinter.appendPatchHunk(comment.file.patchFile, hunk, comment.type, false, false, tbody,
+ function(loc) {
+ return (loc <= comment.location && comment.location - loc < 5);
+ });
+
+ var tr = new Element(document.createElement('tr'));
+ var td = new Element(document.createElement('td'));
+ td.appendTo(tr);
+ td = new Element(document.createElement('td'));
+ Dom.addClass(td, 'review-patch-comment-text');
+ Splinter.Utils.preWrapLines(td, comment.comment);
+ td.appendTo(tr);
+ tr.appendTo(tbody);
+ }
+
+ parentDiv.appendChild(commentDiv);
+};
+
+Splinter.appendReviewComments = function (review, parentDiv) {
+ var i;
+ for (i = 0; i < review.files.length; i++) {
+ var file = review.files[i];
+
+ if (file.comments.length == 0) {
+ continue;
+ }
+
+ parentDiv.appendChild(Splinter.EL("div", "review-patch-file", file.patchFile.filename));
+ var firstComment = true;
+ var j;
+ for (j = 0; j < file.comments.length; j++) {
+ if (firstComment) {
+ firstComment = false;
+ } else {
+ parentDiv.appendChild(Splinter.EL("div", "review-patch-comment-separator"));
+ }
+
+ Splinter.appendReviewComment(file.comments[j], parentDiv);
+ }
+ }
+};
+
+Splinter.updateMyPatchComments = function () {
+ var myPatchComments = Dom.get("myPatchComments");
+ myPatchComments.innerHTML = '';
+ Splinter.appendReviewComments(Splinter.theReview, myPatchComments);
+ if (Dom.getChildren(myPatchComments).length > 0) {
+ Dom.setStyle(myPatchComments, 'display', 'block');
+ } else {
+ Dom.setStyle(myPatchComments, 'display', 'none');
+ }
+};
+
+Splinter.selectNavigationLink = function (identifier) {
+ var navigationLinks = Dom.getElementsByClassName('navigation-link');
+ var i;
+ for (i = 0; i < navigationLinks.length; i++) {
+ Dom.removeClass(navigationLinks[i], 'navigation-link-selected');
+ }
+ Dom.addClass(Splinter.navigationLinks[identifier], 'navigation-link-selected');
+};
+
+Splinter.addNavigationLink = function (identifier, title, callback, selected) {
+ var navigationDiv = Dom.get('navigation');
+ if (Dom.getChildren(navigationDiv).length > 0) {
+ navigationDiv.appendChild(document.createTextNode(' | '));
+ }
+
+ var navigationLink = new Element(document.createElement('a'));
+ Dom.addClass(navigationLink, 'navigation-link');
+ Dom.setAttribute(navigationLink, 'href', 'javascript:void(0);');
+ Dom.setAttribute(navigationLink, 'id', 'switch-' + encodeURIComponent(identifier));
+ Dom.setAttribute(navigationLink, 'title', identifier);
+ navigationLink.appendChild(document.createTextNode(title));
+ navigationLink.appendTo(navigationDiv);
+
+ // FIXME: Find out why I need to use an id here instead of just passing
+ // navigationLink to Event.addListener()
+ Event.addListener('switch-' + encodeURIComponent(identifier), 'click', function () {
+ if (!Dom.hasClass(this, 'navigation-link-selected')) {
+ callback();
+ }
+ });
+
+ if (selected) {
+ Dom.addClass(navigationLink, 'navigation-link-selected');
+ }
+
+ Splinter.navigationLinks[identifier] = navigationLink;
+};
+
+Splinter.showOverview = function () {
+ Splinter.selectNavigationLink('__OVERVIEW__');
+ Dom.setStyle('overview', 'display', 'block');
+ Dom.getElementsByClassName('file', 'div', '', function (node) {
+ Dom.setStyle(node, 'display', 'none');
+ });
+ if (!Splinter.readOnly)
+ Splinter.updateMyPatchComments();
+};
+
+Splinter.showAllFiles = function () {
+ Splinter.selectNavigationLink('__ALL__');
+ Dom.setStyle('overview', 'display', 'none');
+ Dom.setStyle('file-collapse-all', 'display', 'block');
+
+ var i;
+ for (i = 0; i < Splinter.thePatch.files.length; i++) {
+ var file = Splinter.thePatch.files[i];
+ if (!file.div) {
+ Splinter.addPatchFile(file);
+ } else {
+ Dom.setStyle(file.div, 'display', 'block');
+ }
+ }
+}
+
+Splinter.toggleCollapsed = function (filename, display) {
+ filename = decodeURIComponent(filename);
+ var i;
+ for (i = 0; i < Splinter.thePatch.files.length; i++) {
+ var file = Splinter.thePatch.files[i];
+ if (!filename || filename == file.filename) {
+ var fileTableContainer = file.div.getElementsByClassName('file-table-container')[0];
+ var fileExtraContainer = file.div.getElementsByClassName('file-extra-container')[0];
+ var fileCollapseLink = file.div.getElementsByClassName('file-label-collapse')[0];
+ if (!display) {
+ display = Dom.getStyle(fileTableContainer, 'display') == 'block' ? 'none' : 'block';
+ }
+ Dom.setStyle(fileTableContainer, 'display', display);
+ Dom.setStyle(fileExtraContainer, 'display', display);
+ fileCollapseLink.innerHTML = display == 'block' ? '[-]' : '[+]';
+ }
+ }
+}
+
+Splinter.toggleFileReviewed = function (filename) {
+ var checkbox = Dom.get('file-review-checkbox-' + filename);
+ if (checkbox) {
+ filename = decodeURIComponent(filename);
+ for (var i = 0; i < Splinter.thePatch.files.length; i++) {
+ var file = Splinter.thePatch.files[i];
+ if (file.filename == filename) {
+ file.fileReviewed = checkbox.checked;
+
+ Splinter.saveDraft();
+ Splinter.queueUpdateHaveDraft();
+
+ // Strike through file names to show review was completed
+ var fileNavLink = Dom.get('switch-' + encodeURIComponent(filename));
+ if (file.fileReviewed) {
+ Dom.addClass(fileNavLink, 'file-reviewed-nav');
+ }
+ else {
+ Dom.removeClass(fileNavLink, 'file-reviewed-nav');
+ }
+ }
+ }
+ }
+}
+
+Splinter.showPatchFile = function (file) {
+ Splinter.selectNavigationLink(file.filename);
+ Dom.setStyle('overview', 'display', 'none');
+ Dom.setStyle('file-collapse-all', 'display', 'none');
+
+ Dom.getElementsByClassName('file', 'div', '', function (node) {
+ Dom.setStyle(node, 'display', 'none');
+ });
+
+ if (file.div) {
+ Dom.setStyle(file.div, 'display', 'block');
+ } else {
+ Splinter.addPatchFile(file);
+ }
+};
+
+Splinter.addFileNavigationLink = function (file) {
+ var basename = file.filename.replace(/.*\//, "");
+ Splinter.addNavigationLink(file.filename, basename, function() {
+ Splinter.showPatchFile(file);
+ });
+};
+
+Splinter.start = function () {
+ Dom.setStyle('attachmentInfo', 'display', 'block');
+ Dom.setStyle('navigationContainer', 'display', 'block');
+ Dom.setStyle('overview', 'display', 'block');
+ Dom.setStyle('splinter-files', 'display', 'block');
+ Dom.setStyle('attachmentStatusSpan', 'display', 'none');
+
+ if (Splinter.thePatch.intro) {
+ Splinter.Utils.preWrapLines(Dom.get('patchIntro'), Splinter.thePatch.intro);
+ } else {
+ Dom.setStyle('patchIntro', 'display', 'none');
+ }
+
+ Splinter.addNavigationLink('__OVERVIEW__', "Overview", Splinter.showOverview, true);
+ Splinter.addNavigationLink('__ALL__', "All Files", Splinter.showAllFiles, false);
+
+ var i;
+ for (i = 0; i < Splinter.thePatch.files.length; i++) {
+ Splinter.addFileNavigationLink(Splinter.thePatch.files[i]);
+ }
+
+ var navigation = Dom.get('navigation');
+
+ var haveDraftNotice = new Element(document.createElement('div'));
+ Dom.setAttribute(haveDraftNotice, 'id', 'haveDraftNotice');
+ haveDraftNotice.appendChild(document.createTextNode('Draft'));
+ haveDraftNotice.appendTo(navigation);
+
+ var clear = new Element(document.createElement('div'));
+ Dom.addClass(clear, 'clear');
+ clear.appendTo(navigation);
+
+ var numReviewers = 0;
+ for (i = 0; i < Splinter.theBug.comments.length; i++) {
+ var comment = Splinter.theBug.comments[i];
+ var m = Splinter.Review.REVIEW_RE.exec(comment.text);
+
+ if (m && parseInt(m[1], 10) == Splinter.attachmentId) {
+ var review = new Splinter.Review.Review(Splinter.thePatch, comment.getWho(), comment.date);
+ review.parse(comment.text.substr(m[0].length));
+
+ var reviewerIndex;
+ if (review.who in Splinter.reviewers) {
+ reviewerIndex = Splinter.reviewers[review.who];
+ } else {
+ reviewerIndex = ++numReviewers;
+ Splinter.reviewers[review.who] = reviewerIndex;
+ }
+
+ var reviewDiv = new Element(document.createElement('div'));
+ Dom.addClass(reviewDiv, 'review');
+ Dom.addClass(reviewDiv, Splinter.getReviewerClass(review));
+ reviewDiv.appendTo(Dom.get('oldReviews'));
+
+ var reviewerBox = new Element(document.createElement('div'));
+ Dom.addClass(reviewerBox, 'reviewer-box');
+ reviewerBox.appendTo(reviewDiv);
+
+ var reviewer = new Element(document.createElement('div'));
+ Dom.addClass(reviewer, 'reviewer');
+ reviewer.appendChild(document.createTextNode(review.who));
+ reviewer.appendTo(reviewerBox);
+
+ var reviewDate = new Element(document.createElement('div'));
+ Dom.addClass(reviewDate, 'review-date');
+ reviewDate.appendChild(document.createTextNode(Splinter.Utils.formatDate(review.date)));
+ reviewDate.appendTo(reviewerBox);
+
+ var reviewInfoBottom = new Element(document.createElement('div'));
+ Dom.addClass(reviewInfoBottom, 'review-info-bottom');
+ reviewInfoBottom.appendTo(reviewerBox);
+
+ var reviewIntro = new Element(document.createElement('div'));
+ Dom.addClass(reviewIntro, 'review-intro');
+ Splinter.Utils.preWrapLines(reviewIntro, review.intro? review.intro : "");
+ reviewIntro.appendTo(reviewerBox);
+
+ Dom.setStyle('oldReviews', 'display', 'block');
+
+ Splinter.appendReviewComments(review, reviewerBox);
+ }
+ }
+
+ // We load the saved draft or create a new review *after* inserting the existing reviews
+ // so that the ordering comes out right.
+
+ if (Splinter.reviewStorage) {
+ Splinter.theReview = Splinter.reviewStorage.loadDraft(Splinter.theBug, Splinter.theAttachment, Splinter.thePatch);
+ if (Splinter.theReview) {
+ var storedReviews = Splinter.reviewStorage.listReviews();
+ Dom.setStyle('restored', 'display', 'block');
+ for (i = 0; i < storedReviews.length; i++) {
+ if (storedReviews[i].bugId == Splinter.theBug.id &&
+ storedReviews[i].attachmentId == Splinter.theAttachment.id)
+ {
+ Dom.get("restoredLastModified").innerHTML = Splinter.Utils.formatDate(new Date(storedReviews[i].modificationTime));
+ // Restore file reviewed checkboxes
+ if (storedReviews[i].filesReviewed) {
+ for (var j = 0; j < Splinter.thePatch.files.length; j++) {
+ var file = Splinter.thePatch.files[j];
+ if (storedReviews[i].filesReviewed[file.filename]) {
+ file.fileReviewed = true;
+ // Strike through file names to show that review was completed
+ var fileNavLink = Dom.get('switch-' + encodeURIComponent(file.filename));
+ Dom.addClass(fileNavLink, 'file-reviewed-nav');
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (!Splinter.theReview) {
+ Splinter.theReview = new Splinter.Review.Review(Splinter.thePatch);
+ }
+
+ if (Splinter.theReview.intro) {
+ Dom.setStyle('emptyCommentNotice', 'display', 'none');
+ }
+
+ if (!Splinter.readOnly) {
+ var myComment = Dom.get('myComment');
+ myComment.value = Splinter.theReview.intro ? Splinter.theReview.intro : "";
+ Event.addListener(myComment, 'focus', function () {
+ Dom.setStyle('emptyCommentNotice', 'display', 'none');
+ });
+ Event.addListener(myComment, 'blur', function () {
+ if (myComment.value == '') {
+ Dom.setStyle('emptyCommentNotice', 'display', 'block');
+ }
+ });
+ Event.addListener(myComment, 'keydown', function () {
+ Splinter.queueSaveDraft();
+ Splinter.queueUpdateHaveDraft();
+ });
+
+ Splinter.updateMyPatchComments();
+
+ Splinter.queueUpdateHaveDraft();
+
+ Event.addListener("publishButton", "click", Splinter.publishReview);
+ Event.addListener("cancelButton", "click", Splinter.discardReview);
+ } else {
+ Dom.setStyle('haveDraftNotice', 'display', 'none');
+ }
+};
+
+Splinter.newPageUrl = function (newBugId, newAttachmentId) {
+ var newUrl = Splinter.configBase;
+ if (newBugId != null) {
+ newUrl += (newUrl.indexOf("?") < 0) ? "?" : "&";
+ newUrl += "bug=" + escape("" + newBugId);
+ if (newAttachmentId != null) {
+ newUrl += "&attachment=" + escape("" + newAttachmentId);
+ }
+ }
+
+ return newUrl;
+};
+
+Splinter.showNote = function () {
+ var noteDiv = Dom.get("note");
+ if (noteDiv && Splinter.configNote) {
+ noteDiv.innerHTML = Splinter.configNote;
+ Dom.setStyle(noteDiv, 'display', 'block');
+ }
+};
+
+Splinter.showEnterBug = function () {
+ Splinter.showNote();
+
+ Event.addListener("enterBugGo", "click", function () {
+ var newBugId = Splinter.Utils.strip(Dom.get("enterBugInput").value);
+ document.location = Splinter.newPageUrl(newBugId);
+ });
+
+ Dom.setStyle('enterBug', 'display', 'block');
+
+ if (!Splinter.reviewStorage) {
+ return;
+ }
+
+ var storedReviews = Splinter.reviewStorage.listReviews();
+ if (storedReviews.length == 0) {
+ return;
+ }
+
+ var i;
+ var reviewData = [];
+ for (i = storedReviews.length - 1; i >= 0; i--) {
+ var reviewInfo = storedReviews[i];
+ var modificationDate = Splinter.Utils.formatDate(new Date(reviewInfo.modificationTime));
+ var extra = reviewInfo.isDraft ? "(draft)" : "";
+
+ reviewData.push([
+ reviewInfo.bugId,
+ reviewInfo.bugId + ":" + reviewInfo.attachmentId + ":" + reviewInfo.attachmentDescription,
+ modificationDate,
+ extra
+ ]);
+ }
+
+ var attachLink = function (elLiner, oRecord, oColumn, oData) {
+ var splitResult = oData.split(':', 3);
+ elLiner.innerHTML = "<a href=\"" + Splinter.newPageUrl(splitResult[0], splitResult[1]) +
+ "\">" + splitResult[1] + " - " + splitResult[2] + "</a>";
+ };
+
+ var bugLink = function (elLiner, oRecord, oColumn, oData) {
+ elLiner.innerHTML = "<a href=\"" + Splinter.newPageUrl(oData) +
+ "\">" + oData + "</a>";
+ };
+
+ dsConfig = {
+ responseType: YAHOO.util.DataSource.TYPE_JSARRAY,
+ responseSchema: { fields:["bug_id","attachment", "date", "extra"] }
+ };
+
+ var columnDefs = [
+ { key: "bug_id", label: "Bug", formatter: bugLink },
+ { key: "attachment", label: "Attachment", formatter: attachLink },
+ { key: "date", label: "Date" },
+ { key: "extra", label: "Extra" }
+ ];
+
+ var dataSource = new YAHOO.util.LocalDataSource(reviewData, dsConfig);
+ var dataTable = new YAHOO.widget.DataTable("chooseReviewTable", columnDefs, dataSource);
+
+ Dom.setStyle('chooseReview', 'display', 'block');
+};
+
+Splinter.showChooseAttachment = function () {
+ var drafts = {};
+ var published = {};
+ if (Splinter.reviewStorage) {
+ var storedReviews = Splinter.reviewStorage.listReviews();
+ var j;
+ for (j = 0; j < storedReviews.length; j++) {
+ var reviewInfo = storedReviews[j];
+ if (reviewInfo.bugId == Splinter.theBug.id) {
+ if (reviewInfo.isDraft) {
+ drafts[reviewInfo.attachmentId] = 1;
+ } else {
+ published[reviewInfo.attachmentId] = 1;
+ }
+ }
+ }
+ }
+
+ var attachData = [];
+
+ var i;
+ for (i = 0; i < Splinter.theBug.attachments.length; i++) {
+ var attachment = Splinter.theBug.attachments[i];
+
+ if (!attachment.isPatch || attachment.isObsolete) {
+ continue;
+ }
+
+ var href = Splinter.newPageUrl(Splinter.theBug.id, attachment.id);
+
+ var date = Splinter.Utils.formatDate(attachment.date);
+ var status = (attachment.status && attachment.status != 'none') ? attachment.status : '';
+
+ var extra = '';
+ if (attachment.id in drafts) {
+ extra = '(draft)';
+ } else if (attachment.id in published) {
+ extra = '(published)';
+ }
+
+ attachData.push([ attachment.id, attachment.description, attachment.date, extra ]);
+ }
+
+ var attachLink = function (elLiner, oRecord, oColumn, oData) {
+ elLiner.innerHTML = "<a href=\"" + Splinter.newPageUrl(Splinter.theBug.id, oData) +
+ "\">" + oData + "</a>";
+ };
+
+ dsConfig = {
+ responseType: YAHOO.util.DataSource.TYPE_JSARRAY,
+ responseSchema: { fields:["id","description","date", "extra"] }
+ };
+
+ var columnDefs = [
+ { key: "id", label: "ID", formatter: attachLink },
+ { key: "description", label: "Description" },
+ { key: "date", label: "Date" },
+ { key: "extra", label: "Extra" }
+ ];
+
+ var dataSource = new YAHOO.util.LocalDataSource(attachData, dsConfig);
+ var dataTable = new YAHOO.widget.DataTable("chooseAttachmentTable", columnDefs, dataSource);
+
+ Dom.setStyle('chooseAttachment', 'display', 'block');
+};
+
+Splinter.quickHelpToggle = function () {
+ var quickHelpShow = Dom.get('quickHelpShow');
+ var quickHelpContent = Dom.get('quickHelpContent');
+ var quickHelpToggle = Dom.get('quickHelpToggle');
+
+ if (quickHelpContent.style.display == 'none') {
+ quickHelpContent.style.display = 'block';
+ quickHelpShow.style.display = 'none';
+ } else {
+ quickHelpContent.style.display = 'none';
+ quickHelpShow.style.display = 'block';
+ }
+};
+
+Splinter.init = function () {
+ Splinter.showNote();
+
+ if (Splinter.ReviewStorage.LocalReviewStorage.available()) {
+ Splinter.reviewStorage = new Splinter.ReviewStorage.LocalReviewStorage();
+ }
+
+ if (Splinter.theBug == null) {
+ Splinter.showEnterBug();
+ return;
+ }
+
+ Dom.get("bugId").innerHTML = Splinter.theBug.id;
+ Dom.get("bugLink").setAttribute('href', Splinter.configBugUrl + "show_bug.cgi?id=" + Splinter.theBug.id);
+ Dom.get("bugShortDesc").innerHTML = YAHOO.lang.escapeHTML(Splinter.theBug.shortDesc);
+ Dom.get("bugReporter").appendChild(document.createTextNode(Splinter.theBug.getReporter()));
+ Dom.get("bugCreationDate").innerHTML = Splinter.Utils.formatDate(Splinter.theBug.creationDate);
+ Dom.setStyle('bugInfo', 'display', 'block');
+
+ if (Splinter.attachmentId) {
+ Splinter.theAttachment = Splinter.theBug.getAttachment(Splinter.attachmentId);
+
+ if (Splinter.theAttachment == null) {
+ Splinter.displayError("Attachment " + Splinter.attachmentId + " is not an attachment to bug " + Splinter.theBug.id);
+ }
+ else if (!Splinter.theAttachment.isPatch) {
+ Splinter.displayError("Attachment " + Splinter.attachmentId + " is not a patch");
+ Splinter.theAttachment = null;
+ }
+ }
+
+ if (Splinter.theAttachment == null) {
+ Splinter.showChooseAttachment();
+
+ } else {
+ Dom.get("attachId").innerHTML = Splinter.theAttachment.id;
+ Dom.get("attachLink").setAttribute('href', Splinter.configBugUrl + "attachment.cgi?id=" + Splinter.theAttachment.id);
+ Dom.get("attachDesc").innerHTML = YAHOO.lang.escapeHTML(Splinter.theAttachment.description);
+ Dom.get("attachCreator").appendChild(document.createTextNode(Splinter.Bug._formatWho(Splinter.theAttachment.whoName,
+ Splinter.theAttachment.whoEmail)));
+ Dom.get("attachDate").innerHTML = Splinter.Utils.formatDate(Splinter.theAttachment.date);
+ var warnings = [];
+ if (Splinter.theAttachment.isObsolete)
+ warnings.push('OBSOLETE');
+ if (Splinter.theAttachment.isCRLF)
+ warnings.push('WINDOWS PATCH');
+ if (warnings.length > 0)
+ Dom.get("attachWarning").innerHTML = warnings.join(', ');
+ Dom.setStyle('attachInfo', 'display', 'block');
+
+ Dom.setStyle('quickHelpShow', 'display', 'block');
+
+ document.title = "Patch Review of Attachment " + Splinter.theAttachment.id +
+ " for Bug " + Splinter.theBug.id;
+
+ Splinter.thePatch = new Splinter.Patch.Patch(Splinter.theAttachment.data);
+ if (Splinter.thePatch != null) {
+ Splinter.start();
+ }
+ }
+};
+
+YAHOO.util.Event.addListener(window, 'load', Splinter.init);
diff --git a/extensions/TagNewUsers/Config.pm b/extensions/TagNewUsers/Config.pm
new file mode 100644
index 000000000..c2330afc8
--- /dev/null
+++ b/extensions/TagNewUsers/Config.pm
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TagNewUsers;
+use strict;
+
+use constant NAME => 'TagNewUsers';
+use constant REQUIRED_MODULES => [ ];
+use constant OPTIONAL_MODULES => [ ];
+
+__PACKAGE__->NAME;
diff --git a/extensions/TagNewUsers/Extension.pm b/extensions/TagNewUsers/Extension.pm
new file mode 100644
index 000000000..7f12445fb
--- /dev/null
+++ b/extensions/TagNewUsers/Extension.pm
@@ -0,0 +1,263 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TagNewUsers;
+use strict;
+use base qw(Bugzilla::Extension);
+use Bugzilla::Field;
+use Bugzilla::User;
+use Bugzilla::Install::Util qw(indicate_progress);
+use Bugzilla::WebService::Util qw(filter_wants);
+use Date::Parse;
+use Scalar::Util qw(blessed);
+
+# users younger than PROFILE_AGE days will be tagged as new
+use constant PROFILE_AGE => 60;
+
+# users with fewer comments than COMMENT_COUNT will be tagged as new
+use constant COMMENT_COUNT => 25;
+
+our $VERSION = '1';
+
+#
+# install
+#
+
+sub install_update_db {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ if (!$dbh->bz_column_info('profiles', 'comment_count')) {
+ $dbh->bz_add_column('profiles', 'comment_count',
+ {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0});
+ my $sth = $dbh->prepare('UPDATE profiles SET comment_count=? WHERE userid=?');
+ my $ra = $dbh->selectall_arrayref('SELECT who,COUNT(*) FROM longdescs GROUP BY who');
+ my $count = 1;
+ my $total = scalar @$ra;
+ foreach my $ra_row (@$ra) {
+ indicate_progress({ current => $count++, total => $total, every => 25 });
+ my ($user_id, $count) = @$ra_row;
+ $sth->execute($count, $user_id);
+ }
+ }
+
+ if (!$dbh->bz_column_info('profiles', 'creation_ts')) {
+ $dbh->bz_add_column('profiles', 'creation_ts',
+ {TYPE => 'DATETIME'});
+ my $creation_date_fieldid = get_field_id('creation_ts');
+ my $sth = $dbh->prepare('UPDATE profiles SET creation_ts=? WHERE userid=?');
+ my $ra = $dbh->selectall_arrayref("
+ SELECT p.userid, a.profiles_when
+ FROM profiles p
+ LEFT JOIN profiles_activity a ON a.userid=p.userid
+ AND a.fieldid=$creation_date_fieldid
+ ");
+ my ($now) = Bugzilla->dbh->selectrow_array("SELECT NOW()");
+ my $count = 1;
+ my $total = scalar @$ra;
+ foreach my $ra_row (@$ra) {
+ indicate_progress({ current => $count++, total => $total, every => 25 });
+ my ($user_id, $when) = @$ra_row;
+ if (!$when) {
+ ($when) = $dbh->selectrow_array(
+ "SELECT bug_when FROM bugs_activity WHERE who=? ORDER BY bug_when " .
+ $dbh->sql_limit(1),
+ undef, $user_id
+ );
+ }
+ if (!$when) {
+ ($when) = $dbh->selectrow_array(
+ "SELECT bug_when FROM longdescs WHERE who=? ORDER BY bug_when " .
+ $dbh->sql_limit(1),
+ undef, $user_id
+ );
+ }
+ if (!$when) {
+ ($when) = $dbh->selectrow_array(
+ "SELECT creation_ts FROM bugs WHERE reporter=? ORDER BY creation_ts " .
+ $dbh->sql_limit(1),
+ undef, $user_id
+ );
+ }
+ if (!$when) {
+ $when = $now;
+ }
+
+ $sth->execute($when, $user_id);
+ }
+ }
+
+ if (!$dbh->bz_column_info('profiles', 'first_patch_bug_id')) {
+ $dbh->bz_add_column('profiles', 'first_patch_bug_id', {TYPE => 'INT3'});
+ my $sth_update = $dbh->prepare('UPDATE profiles SET first_patch_bug_id=? WHERE userid=?');
+ my $sth_select = $dbh->prepare(
+ 'SELECT bug_id FROM attachments WHERE submitter_id=? AND ispatch=1 ORDER BY creation_ts ' . $dbh->sql_limit(1)
+ );
+ my $ra = $dbh->selectcol_arrayref('SELECT DISTINCT submitter_id FROM attachments WHERE ispatch=1');
+ my $count = 1;
+ my $total = scalar @$ra;
+ foreach my $user_id (@$ra) {
+ indicate_progress({ current => $count++, total => $total, every => 25 });
+ $sth_select->execute($user_id);
+ my ($bug_id) = $sth_select->fetchrow_array;
+ $sth_update->execute($bug_id, $user_id);
+ }
+ }
+}
+
+#
+# objects
+#
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my ($class, $columns) = @$args{qw(class columns)};
+ if ($class->isa('Bugzilla::User')) {
+ push(@$columns, qw(comment_count creation_ts first_patch_bug_id));
+ }
+}
+
+sub object_before_create {
+ my ($self, $args) = @_;
+ my ($class, $params) = @$args{qw(class params)};
+ if ($class->isa('Bugzilla::User')) {
+ my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()");
+ $params->{comment_count} = 0;
+ $params->{creation_ts} = $timestamp;
+ } elsif ($class->isa('Bugzilla::Attachment')) {
+ if ($params->{ispatch} && !Bugzilla->user->first_patch_bug_id) {
+ Bugzilla->user->first_patch_bug_id($params->{bug}->id);
+ }
+ }
+}
+
+#
+# Bugzilla::User methods
+#
+
+BEGIN {
+ *Bugzilla::User::comment_count = \&_comment_count;
+ *Bugzilla::User::creation_ts = \&_creation_ts;
+ *Bugzilla::User::update_comment_count = \&_update_comment_count;
+ *Bugzilla::User::first_patch_bug_id = \&_first_patch_bug_id;
+ *Bugzilla::User::is_new = \&_is_new;
+ *Bugzilla::User::creation_age = \&_creation_age;
+}
+
+sub _comment_count { return $_[0]->{comment_count} }
+sub _creation_ts { return $_[0]->{creation_ts} }
+
+sub _update_comment_count {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ # no need to update this counter for users which are no longer new
+ return unless $self->is_new;
+
+ my $id = $self->id;
+ my ($count) = $dbh->selectrow_array(
+ "SELECT COUNT(*) FROM longdescs WHERE who=?",
+ undef, $id
+ );
+ return if $self->{comment_count} == $count;
+ $dbh->do(
+ 'UPDATE profiles SET comment_count=? WHERE userid=?',
+ undef, $count, $id
+ );
+ Bugzilla->memcached->clear({ table => 'profiles', id => $id });
+ $self->{comment_count} = $count;
+}
+
+sub _first_patch_bug_id {
+ my ($self, $bug_id) = @_;
+ return $self->{first_patch_bug_id} unless defined $bug_id;
+
+ Bugzilla->dbh->do(
+ 'UPDATE profiles SET first_patch_bug_id=? WHERE userid=?',
+ undef, $bug_id, $self->id
+ );
+ Bugzilla->memcached->clear({ table => 'profiles', id => $self->id });
+ $self->{first_patch_bug_id} = $bug_id;
+}
+
+sub _is_new {
+ my ($self) = @_;
+
+ if (!exists $self->{is_new}) {
+ if ($self->in_group('canconfirm')) {
+ $self->{is_new} = 0;
+ } else {
+ $self->{is_new} = ($self->comment_count <= COMMENT_COUNT)
+ || ($self->creation_age <= PROFILE_AGE);
+ }
+ }
+
+ return $self->{is_new};
+}
+
+sub _creation_age {
+ my ($self) = @_;
+
+ if (!exists $self->{creation_age}) {
+ my $age = sprintf("%.0f", (time() - str2time($self->creation_ts)) / 86400);
+ $self->{creation_age} = $age;
+ }
+
+ return $self->{creation_age};
+}
+
+#
+# hooks
+#
+
+sub bug_end_of_create {
+ Bugzilla->user->update_comment_count();
+}
+
+sub bug_end_of_update {
+ Bugzilla->user->update_comment_count();
+}
+
+sub mailer_before_send {
+ my ($self, $args) = @_;
+ my $email = $args->{email};
+
+ my ($bug_id) = ($email->header('Subject') =~ /^[^\d]+(\d+)/);
+ my $changer_login = $email->header('X-Bugzilla-Who');
+ my $changed_fields = $email->header('X-Bugzilla-Changed-Fields');
+
+ if ($bug_id
+ && $changer_login
+ && $changed_fields =~ /attachments.created/)
+ {
+ my $changer = Bugzilla::User->new({ name => $changer_login });
+ if ($changer
+ && $changer->first_patch_bug_id
+ && $changer->first_patch_bug_id == $bug_id)
+ {
+ $email->header_set('X-Bugzilla-FirstPatch' => $bug_id);
+ }
+ }
+}
+
+sub webservice_user_get {
+ my ($self, $args) = @_;
+ my ($webservice, $params, $users) = @$args{qw(webservice params users)};
+
+ return unless filter_wants($params, 'is_new');
+
+ foreach my $user (@$users) {
+ # Most of the time the hash values are XMLRPC::Data objects
+ my $email = blessed $user->{'email'} ? $user->{'email'}->value : $user->{'email'};
+ if ($email) {
+ my $user_obj = Bugzilla::User->new({ name => $email });
+ $user->{'is_new'} = $webservice->type('boolean', $user_obj->is_new ? 1 : 0);
+ }
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/TagNewUsers/template/en/default/hook/bug/comments-user.html.tmpl b/extensions/TagNewUsers/template/en/default/hook/bug/comments-user.html.tmpl
new file mode 100644
index 000000000..81cfc776a
--- /dev/null
+++ b/extensions/TagNewUsers/template/en/default/hook/bug/comments-user.html.tmpl
@@ -0,0 +1,26 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS user.in_group('canconfirm') %]
+[% IF comment.author.is_new %]
+<span class="new_user" title="
+[%- comment.author.comment_count FILTER html %] comment[% "s" IF comment.author.comment_count != 1 -%]
+, created [%
+IF comment.author.creation_age == 0 %]today[%
+ELSIF comment.author.creation_age > 365 %]more than a year ago[%
+ELSE %][% comment.author.creation_age FILTER html %] day[% "s" IF comment.author.creation_age != 1 %] ago[% END %]."
+ >
+(New to [% terms.Bugzilla %])
+</span>
+[% END %]
+[% IF comment.is_about_attachment
+ && comment.author.first_patch_bug_id == bug.id
+ && comment.attachment.ispatch
+%]
+<span class="new_user">(First Patch)</span>
+[% END %]
diff --git a/extensions/TagNewUsers/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/TagNewUsers/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..bff73e963
--- /dev/null
+++ b/extensions/TagNewUsers/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% style_urls.push('extensions/TagNewUsers/web/style.css') IF user.in_group('canconfirm') %]
diff --git a/extensions/TagNewUsers/web/style.css b/extensions/TagNewUsers/web/style.css
new file mode 100644
index 000000000..842dca02e
--- /dev/null
+++ b/extensions/TagNewUsers/web/style.css
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+.new_user {
+ color: #448844;
+}
diff --git a/extensions/TrackingFlags/Config.pm b/extensions/TrackingFlags/Config.pm
new file mode 100644
index 000000000..1854cb9fd
--- /dev/null
+++ b/extensions/TrackingFlags/Config.pm
@@ -0,0 +1,24 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TrackingFlags;
+use strict;
+
+use constant NAME => 'TrackingFlags';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'JSON-XS',
+ module => 'JSON::XS',
+ version => '2.0'
+ },
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/TrackingFlags/Extension.pm b/extensions/TrackingFlags/Extension.pm
new file mode 100644
index 000000000..a1b5a0ef6
--- /dev/null
+++ b/extensions/TrackingFlags/Extension.pm
@@ -0,0 +1,789 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TrackingFlags;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Extension::TrackingFlags::Constants;
+use Bugzilla::Extension::TrackingFlags::Flag;
+use Bugzilla::Extension::TrackingFlags::Flag::Bug;
+use Bugzilla::Extension::TrackingFlags::Admin;
+
+use Bugzilla::Bug;
+use Bugzilla::Component;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Extension::BMO::Data;
+use Bugzilla::Field;
+use Bugzilla::Install::Filesystem;
+use Bugzilla::Product;
+
+use JSON;
+
+our $VERSION = '1';
+
+BEGIN {
+ *Bugzilla::tracking_flags = \&_tracking_flags;
+ *Bugzilla::tracking_flag_names = \&_tracking_flag_names;
+}
+
+sub _tracking_flags {
+ return Bugzilla::Extension::TrackingFlags::Flag->get_all();
+}
+
+sub _tracking_flag_names {
+ return Bugzilla::Extension::TrackingFlags::Flag->get_all_names();
+}
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my $page = $args->{'page_id'};
+ my $vars = $args->{'vars'};
+
+ if ($page eq 'tracking_flags_admin_list.html') {
+ Bugzilla->user->in_group('admin')
+ || ThrowUserError('auth_failure',
+ { group => 'admin',
+ action => 'access',
+ object => 'administrative_pages' });
+ admin_list($vars);
+
+ } elsif ($page eq 'tracking_flags_admin_edit.html') {
+ Bugzilla->user->in_group('admin')
+ || ThrowUserError('auth_failure',
+ { group => 'admin',
+ action => 'access',
+ object => 'administrative_pages' });
+ admin_edit($vars);
+ }
+}
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my $file = $args->{'file'};
+ my $vars = $args->{'vars'};
+
+ if ($file eq 'bug/create/create.html.tmpl'
+ || $file eq 'bug/create/create-winqual.html.tmpl')
+ {
+ my $flags = Bugzilla::Extension::TrackingFlags::Flag->match({
+ product => $vars->{'product'}->name,
+ enter_bug => 1,
+ is_active => 1,
+ });
+
+ $vars->{tracking_flags} = $flags;
+ $vars->{tracking_flags_json} = _flags_to_json($flags);
+ $vars->{tracking_flag_types} = FLAG_TYPES;
+ }
+ elsif ($file eq 'bug/edit.html.tmpl'|| $file eq 'bug/show.xml.tmpl'
+ || $file eq 'email/bugmail.html.tmpl' || $file eq 'email/bugmail.txt.tmpl')
+ {
+ # note: bug/edit.html.tmpl doesn't support multiple bugs
+ my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'};
+
+ if ($bug && !$bug->{error}) {
+ my $flags = Bugzilla::Extension::TrackingFlags::Flag->match({
+ product => $bug->product,
+ component => $bug->component,
+ bug_id => $bug->id,
+ is_active => 1,
+ });
+
+ $vars->{tracking_flags} = $flags;
+ $vars->{tracking_flags_json} = _flags_to_json($flags);
+ }
+
+ $vars->{'tracking_flag_types'} = FLAG_TYPES;
+ }
+ elsif ($file eq 'list/edit-multiple.html.tmpl' && $vars->{'one_product'}) {
+ $vars->{'tracking_flags'} = Bugzilla::Extension::TrackingFlags::Flag->match({
+ product => $vars->{'one_product'}->name,
+ is_active => 1
+ });
+ }
+}
+
+sub _flags_to_json {
+ my ($flags) = @_;
+
+ my $json = {
+ flags => {},
+ types => [],
+ comments => {},
+ };
+
+ my %type_map = map { $_->{name} => $_ } @{ FLAG_TYPES() };
+ foreach my $flag (@$flags) {
+ my $flag_type = $flag->flag_type;
+
+ $json->{flags}->{$flag_type}->{$flag->name} = $flag->bug_flag->value;
+
+ if ($type_map{$flag_type}->{collapsed}
+ && !grep { $_ eq $flag_type } @{ $json->{types} })
+ {
+ push @{ $json->{types} }, $flag_type;
+ }
+
+ foreach my $value (@{ $flag->values }) {
+ if (defined($value->comment) && $value->comment ne '') {
+ $json->{comments}->{$flag->name}->{$value->value} = $value->comment;
+ }
+ }
+ }
+
+ return encode_json($json);
+}
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'tracking_flags'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ field_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'fielddefs',
+ COLUMN => 'id',
+ DELETE => 'CASCADE'
+ }
+ },
+ name => {
+ TYPE => 'varchar(64)',
+ NOTNULL => 1,
+ },
+ description => {
+ TYPE => 'varchar(64)',
+ NOTNULL => 1,
+ },
+ type => {
+ TYPE => 'varchar(64)',
+ NOTNULL => 1,
+ },
+ sortkey => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ DEFAULT => '0',
+ },
+ enter_bug => {
+ TYPE => 'BOOLEAN',
+ NOTNULL => 1,
+ DEFAULT => 'TRUE',
+ },
+ is_active => {
+ TYPE => 'BOOLEAN',
+ NOTNULL => 1,
+ DEFAULT => 'TRUE',
+ },
+ ],
+ INDEXES => [
+ tracking_flags_idx => {
+ FIELDS => ['name'],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'tracking_flags_values'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ tracking_flag_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'tracking_flags',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ },
+ },
+ setter_group_id => {
+ TYPE => 'INT3',
+ NOTNULL => 0,
+ REFERENCES => {
+ TABLE => 'groups',
+ COLUMN => 'id',
+ DELETE => 'SET NULL',
+ },
+ },
+ value => {
+ TYPE => 'varchar(64)',
+ NOTNULL => 1,
+ },
+ sortkey => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ DEFAULT => '0',
+ },
+ enter_bug => {
+ TYPE => 'BOOLEAN',
+ NOTNULL => 1,
+ DEFAULT => 'TRUE',
+ },
+ is_active => {
+ TYPE => 'BOOLEAN',
+ NOTNULL => 1,
+ DEFAULT => 'TRUE',
+ },
+ comment => {
+ TYPE => 'TEXT',
+ NOTNULL => 0,
+ },
+ ],
+ INDEXES => [
+ tracking_flags_values_idx => {
+ FIELDS => ['tracking_flag_id', 'value'],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'tracking_flags_bugs'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ tracking_flag_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'tracking_flags',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ },
+ },
+ bug_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE',
+ },
+ },
+ value => {
+ TYPE => 'varchar(64)',
+ NOTNULL => 1,
+ },
+ ],
+ INDEXES => [
+ tracking_flags_bugs_idx => {
+ FIELDS => ['tracking_flag_id', 'bug_id'],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'tracking_flags_visibility'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ tracking_flag_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'tracking_flags',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ },
+ },
+ product_id => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'products',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ },
+ },
+ component_id => {
+ TYPE => 'INT2',
+ NOTNULL => 0,
+ REFERENCES => {
+ TABLE => 'components',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ },
+ },
+ ],
+ INDEXES => [
+ tracking_flags_visibility_idx => {
+ FIELDS => ['tracking_flag_id', 'product_id', 'component_id'],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+}
+
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+
+ my $fk = $dbh->bz_fk_info('tracking_flags', 'field_id');
+ if ($fk and !defined $fk->{DELETE}) {
+ $fk->{DELETE} = 'CASCADE';
+ $dbh->bz_alter_fk('tracking_flags', 'field_id', $fk);
+ }
+
+ $dbh->bz_add_column(
+ 'tracking_flags',
+ 'enter_bug',
+ {
+ TYPE => 'BOOLEAN',
+ NOTNULL => 1,
+ DEFAULT => 'TRUE',
+ }
+ );
+ $dbh->bz_add_column(
+ 'tracking_flags_values',
+ 'comment',
+ {
+ TYPE => 'TEXT',
+ NOTNULL => 0,
+ },
+ );
+}
+
+sub install_filesystem {
+ my ($self, $args) = @_;
+ my $files = $args->{files};
+ my $extensions_dir = bz_locations()->{extensionsdir};
+ $files->{"$extensions_dir/TrackingFlags/bin/bulk_flag_clear.pl"} = {
+ perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE
+ };
+}
+
+sub active_custom_fields {
+ my ($self, $args) = @_;
+ my $fields = $args->{'fields'};
+ my $params = $args->{'params'};
+ my $product = $params->{'product'};
+ my $component = $params->{'component'};
+
+ # Create a hash of current fields based on field names
+ my %field_hash = map { $_->name => $_ } @$$fields;
+
+ my @tracking_flags;
+ if ($product) {
+ $params->{'product_id'} = $product->id;
+ $params->{'component_id'} = $component->id if $component;
+ $params->{'is_active'} = 1;
+ @tracking_flags = @{ Bugzilla::Extension::TrackingFlags::Flag->match($params) };
+ }
+ else {
+ @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all;
+ }
+
+ # Add tracking flags to fields hash replacing if already exists for our
+ # flag object instead of the usual Field.pm object
+ foreach my $flag (@tracking_flags) {
+ $field_hash{$flag->name} = $flag;
+ }
+
+ @$$fields = sort { $a->sortkey <=> $b->sortkey } values %field_hash;
+}
+
+sub buglist_columns {
+ my ($self, $args) = @_;
+ my $columns = $args->{columns};
+ my $dbh = Bugzilla->dbh;
+ my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all;
+ foreach my $flag (@tracking_flags) {
+ $columns->{$flag->name} = {
+ name => "COALESCE(map_" . $flag->name . ".value, '---')",
+ title => $flag->description
+ };
+ }
+}
+
+sub buglist_column_joins {
+ my ($self, $args) = @_;
+ # if there are elements in the tracking_flags array, then they have been
+ # removed from the query, so we mustn't generate joins
+ return if scalar @{ $args->{search}->{tracking_flags} };
+
+ my $column_joins = $args->{'column_joins'};
+ my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all;
+ foreach my $flag (@tracking_flags) {
+ $column_joins->{$flag->name} = {
+ as => 'map_' . $flag->name,
+ table => 'tracking_flags_bugs',
+ extra => [ 'map_' . $flag->name . '.tracking_flag_id = ' . $flag->flag_id ]
+ };
+ }
+}
+
+sub bug_create_cf_accessors {
+ my ($self, $args) = @_;
+ # Create the custom accessors for the flag values
+ my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all;
+ foreach my $flag (@tracking_flags) {
+ my $flag_name = $flag->name;
+ if (!Bugzilla::Bug->can($flag_name)) {
+ my $accessor = sub {
+ my $self = shift;
+ return $self->{$flag_name} if defined $self->{$flag_name};
+ if (!exists $self->{'_tf_bug_values_preloaded'}) {
+ # preload all values currently set for this bug
+ my $bug_values
+ = Bugzilla::Extension::TrackingFlags::Flag::Bug->match({ bug_id => $self->id });
+ foreach my $value (@$bug_values) {
+ $self->{$value->tracking_flag->name} = $value->value;
+ }
+ $self->{'_tf_bug_values_preloaded'} = 1;
+ }
+ return $self->{$flag_name} ||= '---';
+ };
+ no strict 'refs';
+ *{"Bugzilla::Bug::$flag_name"} = $accessor;
+ }
+ if (!Bugzilla::Bug->can("set_$flag_name")) {
+ my $setter = sub {
+ my ($self, $value) = @_;
+ $value = ref($value) eq 'ARRAY'
+ ? $value->[0]
+ : $value;
+ $self->set($flag_name, $value);
+ };
+ no strict 'refs';
+ *{"Bugzilla::Bug::set_$flag_name"} = $setter;
+ }
+ }
+}
+
+sub bug_editable_bug_fields {
+ my ($self, $args) = @_;
+ my $fields = $args->{'fields'};
+ my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all;
+ foreach my $flag (@tracking_flags) {
+ push(@$fields, $flag->name);
+ }
+}
+
+sub search_operator_field_override {
+ my ($self, $args) = @_;
+ my $operators = $args->{'operators'};
+ my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all;
+ foreach my $flag (@tracking_flags) {
+ $operators->{$flag->name} = {
+ _non_changed => sub {
+ _tracking_flags_search_nonchanged($flag->flag_id, @_)
+ }
+ };
+ }
+}
+
+sub _tracking_flags_search_nonchanged {
+ my ($flag_id, $search, $args) = @_;
+ my ($bugs_table, $chart_id, $joins, $value, $operator) =
+ @$args{qw(bugs_table chart_id joins value operator)};
+ my $dbh = Bugzilla->dbh;
+
+ return if ($operator =~ m/^changed/);
+
+ my $bugs_alias = "tracking_flags_bugs_$chart_id";
+ my $flags_alias = "tracking_flags_$chart_id";
+
+ my $bugs_join = {
+ table => 'tracking_flags_bugs',
+ as => $bugs_alias,
+ from => $bugs_table . ".bug_id",
+ to => "bug_id",
+ extra => [$bugs_alias . ".tracking_flag_id = $flag_id"]
+ };
+
+ push(@$joins, $bugs_join);
+
+ if ($operator eq 'isempty' or $operator eq 'isnotempty') {
+ $args->{'full_field'} = "$bugs_alias.value";
+ }
+ else {
+ $args->{'full_field'} = "COALESCE($bugs_alias.value, '---')";
+ }
+}
+
+sub bug_end_of_create {
+ my ($self, $args) = @_;
+ my $bug = $args->{'bug'};
+ my $timestamp = $args->{'timestamp'};
+ my $user = Bugzilla->user;
+
+ my $params = Bugzilla->request_cache->{tracking_flags_create_params};
+ return if !$params;
+
+ my $tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->match({
+ product => $bug->product,
+ component => $bug->component,
+ is_active => 1,
+ });
+
+ foreach my $flag (@$tracking_flags) {
+ next if !$params->{$flag->name};
+ foreach my $value (@{$flag->values}) {
+ next if $value->value ne $params->{$flag->name};
+ next if $value->value eq '---'; # do not insert if value is '---', same as empty
+ if (!$flag->can_set_value($value->value)) {
+ ThrowUserError('tracking_flags_change_denied',
+ { flag => $flag, value => $value });
+ }
+ Bugzilla::Extension::TrackingFlags::Flag::Bug->create({
+ tracking_flag_id => $flag->flag_id,
+ bug_id => $bug->id,
+ value => $value->value,
+ });
+ # Add the name/value pair to the bug object
+ $bug->{$flag->name} = $value->value;
+ }
+ }
+}
+
+sub object_end_of_set_all {
+ my ($self, $args) = @_;
+ my $object = $args->{object};
+ my $params = $args->{params};
+
+ return unless $object->isa('Bugzilla::Bug');
+
+ # Do not filter by product/component as we may be changing those
+ my $tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->match({
+ bug_id => $object->id,
+ is_active => 1,
+ });
+
+ foreach my $flag (@$tracking_flags) {
+ my $flag_name = $flag->name;
+ if (exists $params->{$flag_name}) {
+ my $value = ref($params->{$flag_name}) eq 'ARRAY'
+ ? $params->{$flag_name}->[0]
+ : $params->{$flag_name};
+ $object->set($flag_name, $value);
+ }
+ }
+}
+
+sub bug_check_can_change_field {
+ my ($self, $args) = @_;
+ my ($bug, $field, $old_value, $new_value, $priv_results)
+ = @$args{qw(bug field old_value new_value priv_results)};
+
+ return if $field !~ /^cf_/ or $old_value eq $new_value;
+ return unless my $flag = Bugzilla::Extension::TrackingFlags::Flag->new({ name => $field });
+
+ if ($flag->can_set_value($new_value)) {
+ push @$priv_results, PRIVILEGES_REQUIRED_NONE;
+ }
+ else {
+ push @$priv_results, PRIVILEGES_REQUIRED_EMPOWERED;
+ }
+}
+
+sub bug_end_of_update {
+ my ($self, $args) = @_;
+ my ($bug, $old_bug, $timestamp, $changes)
+ = @$args{qw(bug old_bug timestamp changes)};
+ my $user = Bugzilla->user;
+
+ # Do not filter by product/component as we may be changing those
+ my $tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->match({
+ bug_id => $bug->id,
+ is_active => 1,
+ });
+
+ my (@flag_changes);
+ foreach my $flag (@$tracking_flags) {
+ my $flag_name = $flag->name;
+ my $new_value = $bug->$flag_name;
+ my $old_value = $old_bug->$flag_name;
+
+ if ($new_value ne $old_value) {
+ # Do not allow if the user cannot set the old value or the new value
+ if (!$flag->can_set_value($new_value)) {
+ ThrowUserError('tracking_flags_change_denied',
+ { flag => $flag, value => $new_value });
+ }
+ push(@flag_changes, { flag => $flag,
+ added => $new_value,
+ removed => $old_value });
+ }
+ }
+
+ foreach my $change (@flag_changes) {
+ my $flag = $change->{'flag'};
+ my $added = $change->{'added'};
+ my $removed = $change->{'removed'};
+
+ if ($added eq '---') {
+ $flag->bug_flag->remove_from_db();
+ }
+ elsif ($removed eq '---') {
+ Bugzilla::Extension::TrackingFlags::Flag::Bug->create({
+ tracking_flag_id => $flag->flag_id,
+ bug_id => $bug->id,
+ value => $added,
+ });
+ }
+ else {
+ $flag->bug_flag->set_value($added);
+ $flag->bug_flag->update($timestamp);
+ }
+
+ $changes->{$flag->name} = [ $removed, $added ];
+ LogActivityEntry($bug->id, $flag->name, $removed, $added, $user->id, $timestamp);
+
+ # Update the name/value pair in the bug object
+ $bug->{$flag->name} = $added;
+ }
+}
+
+sub bug_end_of_create_validators {
+ my ($self, $args) = @_;
+ my $params = $args->{params};
+
+ # We need to stash away any params that are setting/updating tracking
+ # flags early on. Otherwise set_all or insert_create_data will complain.
+ my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all;
+ my $cache = Bugzilla->request_cache->{tracking_flags_create_params} ||= {};
+ foreach my $flag (@tracking_flags) {
+ my $flag_name = $flag->name;
+ if (defined $params->{$flag_name}) {
+ $cache->{$flag_name} = delete $params->{$flag_name};
+ }
+ }
+}
+
+sub mailer_before_send {
+ my ($self, $args) = @_;
+ my $email = $args->{email};
+
+ # Add X-Bugzilla-Tracking header or add to it
+ # if already exists
+ if ($email->header('X-Bugzilla-ID')) {
+ my $bug_id = $email->header('X-Bugzilla-ID');
+
+ my $tracking_flags
+ = Bugzilla::Extension::TrackingFlags::Flag->match({ bug_id => $bug_id });
+
+ my @set_values = ();
+ foreach my $flag (@$tracking_flags) {
+ next if $flag->bug_flag->value eq '---';
+ push(@set_values, $flag->description . ":" . $flag->bug_flag->value);
+ }
+
+ if (@set_values) {
+ my $set_values_string = join(' ', @set_values);
+ if ($email->header('X-Bugzilla-Tracking')) {
+ $set_values_string = $email->header('X-Bugzilla-Tracking') .
+ " " . $set_values_string;
+ }
+ $email->header_set('X-Bugzilla-Tracking' => $set_values_string);
+ }
+ }
+}
+
+# Purpose: generically handle generating pretty blocking/status "flags" from
+# custom field names.
+sub quicksearch_map {
+ my ($self, $args) = @_;
+ my $map = $args->{'map'};
+
+ foreach my $name (keys %$map) {
+ if ($name =~ /^cf_(blocking|tracking|status)_([a-z]+)?(\d+)?$/) {
+ my $type = $1;
+ my $product = $2;
+ my $version = $3;
+
+ if ($version) {
+ $version = join('.', split(//, $version));
+ }
+
+ my $pretty_name = $type;
+ if ($product) {
+ $pretty_name .= "-" . $product;
+ }
+ if ($version) {
+ $pretty_name .= $version;
+ }
+
+ $map->{$pretty_name} = $name;
+ }
+ }
+}
+
+sub reorg_move_component {
+ my ($self, $args) = @_;
+ my $new_product = $args->{new_product};
+ my $component = $args->{component};
+
+ Bugzilla->dbh->do(
+ "UPDATE tracking_flags_visibility SET product_id=? WHERE component_id=?",
+ undef,
+ $new_product->id, $component->id,
+ );
+}
+
+sub sanitycheck_check {
+ my ($self, $args) = @_;
+ my $status = $args->{status};
+
+ $status->('tracking_flags_check');
+
+ my ($count) = Bugzilla->dbh->selectrow_array("
+ SELECT COUNT(*)
+ FROM tracking_flags_visibility
+ INNER JOIN components ON components.id = tracking_flags_visibility.component_id
+ WHERE tracking_flags_visibility.product_id <> components.product_id
+ ");
+ if ($count) {
+ $status->('tracking_flags_alert', undef, 'alert');
+ $status->('tracking_flags_repair');
+ }
+}
+
+sub sanitycheck_repair {
+ my ($self, $args) = @_;
+ return unless Bugzilla->cgi->param('tracking_flags_repair');
+
+ my $status = $args->{'status'};
+ my $dbh = Bugzilla->dbh;
+ $status->('tracking_flags_repairing');
+
+ my $rows = $dbh->selectall_arrayref("
+ SELECT DISTINCT tracking_flags_visibility.product_id AS bad_product_id,
+ components.product_id AS good_product_id,
+ tracking_flags_visibility.component_id
+ FROM tracking_flags_visibility
+ INNER JOIN components ON components.id = tracking_flags_visibility.component_id
+ WHERE tracking_flags_visibility.product_id <> components.product_id
+ ",
+ { Slice => {} }
+ );
+ foreach my $row (@$rows) {
+ $dbh->do("
+ UPDATE tracking_flags_visibility
+ SET product_id=?
+ WHERE product_id=? AND component_id=?
+ ", undef,
+ $row->{good_product_id},
+ $row->{bad_product_id},
+ $row->{component_id},
+ );
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/TrackingFlags/bin/bulk_flag_clear.pl b/extensions/TrackingFlags/bin/bulk_flag_clear.pl
new file mode 100755
index 000000000..1eff355fe
--- /dev/null
+++ b/extensions/TrackingFlags/bin/bulk_flag_clear.pl
@@ -0,0 +1,137 @@
+#!/usr/bin/perl -w
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+
+use FindBin '$RealBin';
+use lib "$RealBin/../../..";
+use lib "$RealBin/../../../lib";
+use lib "$RealBin/../lib";
+
+BEGIN {
+ use Bugzilla;
+ Bugzilla->extensions;
+}
+
+use Bugzilla::Constants;
+use Bugzilla::Extension::TrackingFlags::Flag;
+use Bugzilla::Extension::TrackingFlags::Flag::Bug;
+use Bugzilla::User;
+
+use Getopt::Long;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my $config = {};
+GetOptions(
+ $config,
+ "trace=i",
+ "update_db",
+ "flag=s",
+ "modified_before=s",
+ "modified_after=s",
+ "value=s"
+) or exit;
+unless ($config->{flag}
+ && ($config->{modified_before}
+ || $config->{modified_after}
+ || $config->{value}))
+{
+ die <<EOF;
+$0
+ clears tracking flags matching the specified criteria.
+ the last-modified will be updated, however bugmail will not be generated.
+
+SYNTAX
+ $0 --flag <flag> (conditions) [--update_db]
+
+CONDITIONS
+ --modified_before <datetime> bug last-modified before <datetime>
+ --modified_after <datetime> bug last-modified after <datetime>
+ --value <flag value> flag = <flag value>
+
+OPTIONS
+ --update_db : by default only the impacted bugs will be listed. pass this
+ switch to update the database.
+EOF
+}
+
+# build sql
+
+my (@where, @values);
+
+my $flag = Bugzilla::Extension::TrackingFlags::Flag->check({ name => $config->{flag} });
+push @where, 'tracking_flags_bugs.tracking_flag_id = ?';
+push @values, $flag->flag_id;
+
+if ($config->{modified_before}) {
+ push @where, 'bugs.delta_ts < ?';
+ push @values, $config->{modified_before};
+}
+
+if ($config->{modified_after}) {
+ push @where, 'bugs.delta_ts > ?';
+ push @values, $config->{modified_after};
+}
+
+if ($config->{value}) {
+ push @where, 'tracking_flags_bugs.value = ?';
+ push @values, $config->{value};
+}
+
+my $sql = "
+ SELECT tracking_flags_bugs.bug_id
+ FROM tracking_flags_bugs
+ INNER JOIN bugs ON bugs.bug_id = tracking_flags_bugs.bug_id
+ WHERE (" . join(") AND (", @where) . ")
+ ORDER BY tracking_flags_bugs.bug_id
+";
+
+# execute query
+
+my $dbh = Bugzilla->dbh;
+$dbh->{TraceLevel} = $config->{trace} if $config->{trace};
+
+my $bug_ids = $dbh->selectcol_arrayref($sql, undef, @values);
+
+if (!@$bug_ids) {
+ die "no matching bugs found\n";
+}
+
+if (!$config->{update_db}) {
+ print "bugs found: ", scalar(@$bug_ids), "\n\n", join(',', @$bug_ids), "\n\n";
+ print "--update_db not provided, no changes made to the database\n";
+ exit;
+}
+
+# update bugs
+
+my $nobody = Bugzilla::User->check({ name => 'nobody@mozilla.org' });
+# put our nobody user into all groups to avoid permissions issues
+$nobody->{groups} = [Bugzilla::Group->get_all];
+Bugzilla->set_user($nobody);
+
+foreach my $bug_id (@$bug_ids) {
+ print "updating bug $bug_id\n";
+ $dbh->bz_start_transaction;
+
+ # update the bug
+ # this will deal with history for us but not send bugmail
+ my $bug = Bugzilla::Bug->check({ id => $bug_id });
+ $bug->set_all({ $flag->name => '---' });
+ $bug->update;
+
+ # update lastdiffed to skip bugmail for this change
+ $dbh->do(
+ "UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?",
+ undef,
+ $bug->id
+ );
+ $dbh->bz_commit_transaction;
+}
diff --git a/extensions/TrackingFlags/bin/migrate_tracking_flags.pl b/extensions/TrackingFlags/bin/migrate_tracking_flags.pl
new file mode 100755
index 000000000..06b3596c4
--- /dev/null
+++ b/extensions/TrackingFlags/bin/migrate_tracking_flags.pl
@@ -0,0 +1,316 @@
+#!/usr/bin/perl -w
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+# Migrate old custom field based tracking flags to the new
+# table based tracking flags
+
+use strict;
+use warnings;
+
+use FindBin '$RealBin';
+use lib "$RealBin/../../..";
+use lib "$RealBin/../../../lib";
+use lib "$RealBin/../lib";
+
+BEGIN {
+ use Bugzilla;
+ Bugzilla->extensions;
+}
+
+use Bugzilla::Constants;
+use Bugzilla::Field;
+use Bugzilla::Product;
+use Bugzilla::Component;
+use Bugzilla::Extension::BMO::Data;
+use Bugzilla::Install::Util qw(indicate_progress);
+
+use Bugzilla::Extension::TrackingFlags::Constants;
+use Bugzilla::Extension::TrackingFlags::Flag;
+use Bugzilla::Extension::TrackingFlags::Flag::Bug;
+use Bugzilla::Extension::TrackingFlags::Flag::Value;
+use Bugzilla::Extension::TrackingFlags::Flag::Visibility;
+
+use Getopt::Long;
+use Data::Dumper;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my ($dry_run, $trace) = (0, 0);
+GetOptions(
+ "dry-run" => \$dry_run,
+ "trace" => \$trace,
+) or exit;
+
+my $dbh = Bugzilla->dbh;
+
+$dbh->{TraceLevel} = 1 if $trace;
+
+my %product_cache;
+my %component_cache;
+
+sub migrate_flag_visibility {
+ my ($new_flag, $products) = @_;
+
+ # Create product/component visibility
+ foreach my $prod_name (keys %$products) {
+ $product_cache{$prod_name} ||= Bugzilla::Product->new({ name => $prod_name });
+ if (!$product_cache{$prod_name}) {
+ warn "No such product $prod_name\n";
+ next;
+ }
+
+ # If no components specified then we do Product/__any__
+ # otherwise, we enter an entry for each Product/Component
+ my $components = $products->{$prod_name};
+ if (!@$components) {
+ Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({
+ tracking_flag_id => $new_flag->flag_id,
+ product_id => $product_cache{$prod_name}->id,
+ component_id => undef
+ });
+ }
+ else {
+ foreach my $comp_name (@$components) {
+ my $comp_matches = [];
+ # If the component is a regexp, we need to find all components
+ # matching the regex and insert each individually
+ if (ref $comp_name eq 'Regexp') {
+ my $comp_re = $comp_name;
+ $comp_re =~ s/\?\-xism://;
+ $comp_re =~ s/\(//;
+ $comp_re =~ s/\)//;
+ $comp_matches = $dbh->selectcol_arrayref(
+ 'SELECT components.name FROM components
+ WHERE components.product_id = ?
+ AND ' . $dbh->sql_regexp('components.name', $dbh->quote($comp_re)) . '
+ ORDER BY components.name',
+ undef,
+ $product_cache{$prod_name}->id);
+ }
+ else {
+ $comp_matches = [ $comp_name ];
+ }
+
+ foreach my $comp_match (@$comp_matches) {
+ $component_cache{"${prod_name}:${comp_match}"}
+ ||= Bugzilla::Component->new({ name => $comp_match,
+ product => $product_cache{$prod_name} });
+ if (!$component_cache{"${prod_name}:${comp_match}"}) {
+ warn "No such product $prod_name and component $comp_match\n";
+ next;
+ }
+
+ Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({
+ tracking_flag_id => $new_flag->flag_id,
+ product_id => $product_cache{$prod_name}->id,
+ component_id => $component_cache{"${prod_name}:${comp_match}"}->id,
+ });
+ }
+ }
+ }
+ }
+}
+
+sub migrate_flag_values {
+ my ($new_flag, $field) = @_;
+
+ print "Migrating flag values...";
+
+ my %blocking_trusted_requesters
+ = %{$Bugzilla::Extension::BMO::Data::blocking_trusted_requesters};
+ my %blocking_trusted_setters
+ = %{$Bugzilla::Extension::BMO::Data::blocking_trusted_setters};
+ my %status_trusted_wanters
+ = %{$Bugzilla::Extension::BMO::Data::status_trusted_wanters};
+ my %status_trusted_setters
+ = %{$Bugzilla::Extension::BMO::Data::status_trusted_setters};
+
+ my %group_cache;
+ foreach my $value (@{ $field->legal_values }) {
+ my $group_name = 'everyone';
+
+ if ($field->name =~ /^cf_(blocking|tracking)_/) {
+ if ($value->name ne '---' && $value->name !~ '\?$') {
+ $group_name = get_setter_group($field->name, \%blocking_trusted_setters);
+ }
+ if ($value->name eq '?') {
+ $group_name = get_setter_group($field->name, \%blocking_trusted_requesters);
+ }
+ } elsif ($field->name =~ /^cf_status_/) {
+ if ($value->name eq 'wanted') {
+ $group_name = get_setter_group($field->name, \%status_trusted_wanters);
+ } elsif ($value->name ne '---' && $value->name ne '?') {
+ $group_name = get_setter_group($field->name, \%status_trusted_setters);
+ }
+ }
+
+ $group_cache{$group_name} ||= Bugzilla::Group->new({ name => $group_name });
+ $group_cache{$group_name} || die "Setter group '$group_name' does not exist";
+
+ Bugzilla::Extension::TrackingFlags::Flag::Value->create({
+ tracking_flag_id => $new_flag->flag_id,
+ value => $value->name,
+ setter_group_id => $group_cache{$group_name}->id,
+ sortkey => $value->sortkey,
+ is_active => $value->is_active
+ });
+ }
+
+ print "done.\n";
+}
+
+sub get_setter_group {
+ my ($field, $trusted) = @_;
+ my $setter_group = $trusted->{'_default'} || "";
+ foreach my $dfield (keys %$trusted) {
+ if ($field =~ $dfield) {
+ $setter_group = $trusted->{$dfield};
+ }
+ }
+ return $setter_group;
+}
+
+sub migrate_flag_bugs {
+ my ($new_flag, $field) = @_;
+
+ print "Migrating bug values...";
+
+ my $bugs = $dbh->selectall_arrayref("SELECT bug_id, " . $field->name . "
+ FROM bugs
+ WHERE " . $field->name . " != '---'
+ ORDER BY bug_id");
+ local $| = 1;
+ my $count = 1;
+ my $total = scalar @$bugs;
+ foreach my $row (@$bugs) {
+ my ($id, $value) = @$row;
+ indicate_progress({ current => $count++, total => $total, every => 25 });
+ Bugzilla::Extension::TrackingFlags::Flag::Bug->create({
+ tracking_flag_id => $new_flag->flag_id,
+ bug_id => $id,
+ value => $value,
+
+ });
+ }
+
+ print "done.\n";
+}
+
+sub migrate_flag_activity {
+ my ($new_flag, $field) = @_;
+
+ print "Migating flag activity...";
+
+ my $new_field = Bugzilla::Field->new({ name => $new_flag->name });
+ $dbh->do("UPDATE bugs_activity SET fieldid = ? WHERE fieldid = ?",
+ undef, $new_field->id, $field->id);
+
+ print "done.\n";
+}
+
+sub do_migration {
+ my $bmo_tracking_flags = $Bugzilla::Extension::BMO::Data::cf_visible_in_products;
+ my $bmo_project_flags = $Bugzilla::Extension::BMO::Data::cf_project_flags;
+ my $bmo_disabled_flags = $Bugzilla::Extension::BMO::Data::cf_disabled_flags;
+
+ my $fields = Bugzilla::Field->match({ custom => 1,
+ type => FIELD_TYPE_SINGLE_SELECT });
+
+ my @drop_columns;
+ foreach my $field (@$fields) {
+ next if $field->name !~ /^cf_(blocking|tracking|status)_/;
+
+ foreach my $field_re (keys %$bmo_tracking_flags) {
+ next if $field->name !~ $field_re;
+
+ # Create the new tracking flag if not exists
+ my $new_flag
+ = Bugzilla::Extension::TrackingFlags::Flag->new({ name => $field->name });
+
+ next if $new_flag;
+
+ print "----------------------------------\n" .
+ "Migrating custom tracking field " . $field->name . "...\n";
+
+ my $new_flag_name = $field->name . "_new"; # Temporary name til we delete the old
+
+ my $type = grep($field->name =~ $_, @$bmo_project_flags)
+ ? 'project'
+ : 'tracking';
+
+ my $is_active = grep($_ eq $field->name, @$bmo_disabled_flags) ? 0 : 1;
+
+ $new_flag = Bugzilla::Extension::TrackingFlags::Flag->create({
+ name => $new_flag_name,
+ description => $field->description,
+ type => $type,
+ sortkey => $field->sortkey,
+ is_active => $is_active,
+ enter_bug => $field->enter_bug,
+ });
+
+ migrate_flag_visibility($new_flag, $bmo_tracking_flags->{$field_re});
+
+ migrate_flag_values($new_flag, $field);
+
+ migrate_flag_bugs($new_flag, $field);
+
+ migrate_flag_activity($new_flag, $field);
+
+ push(@drop_columns, $field->name);
+
+ # Remove the old flag entry from fielddefs
+ $dbh->do("DELETE FROM fielddefs WHERE name = ?",
+ undef, $field->name);
+
+ # Rename the new flag
+ $dbh->do("UPDATE fielddefs SET name = ? WHERE name = ?",
+ undef, $field->name, $new_flag_name);
+
+ $new_flag->set_name($field->name);
+ $new_flag->update;
+
+ # more than one regex could possibly match but we only want the first one
+ last;
+ }
+ }
+
+ # Drop each custom flag's value table and the column from the bz schema object
+ if (!$dry_run && @drop_columns) {
+ print "Dropping value tables and updating bz schema object...\n";
+
+ foreach my $column (@drop_columns) {
+ # Drop the values table
+ $dbh->bz_drop_table($column);
+
+ # Drop the bugs table column from the bz schema object
+ $dbh->_bz_real_schema->delete_column('bugs', $column);
+ $dbh->_bz_store_real_schema;
+ }
+
+ # Do the one alter table to drop all columns at once
+ $dbh->do("ALTER TABLE bugs DROP COLUMN " . join(", DROP COLUMN ", @drop_columns));
+ }
+}
+
+# Start Main
+
+eval {
+ if ($dry_run) {
+ print "** dry run : no changes to the database will be made **\n";
+ $dbh->bz_start_transaction();
+ }
+ print "Starting migration...\n";
+ do_migration();
+ $dbh->bz_rollback_transaction() if $dry_run;
+ print "All done!\n";
+};
+if ($@) {
+ $dbh->bz_rollback_transaction() if $dry_run;
+ die "$@" if $@;
+}
diff --git a/extensions/TrackingFlags/lib/Admin.pm b/extensions/TrackingFlags/lib/Admin.pm
new file mode 100644
index 000000000..1bae18ef8
--- /dev/null
+++ b/extensions/TrackingFlags/lib/Admin.pm
@@ -0,0 +1,446 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TrackingFlags::Admin;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Component;
+use Bugzilla::Error;
+use Bugzilla::Group;
+use Bugzilla::Product;
+use Bugzilla::Util qw(trim detaint_natural);
+
+use Bugzilla::Extension::TrackingFlags::Constants;
+use Bugzilla::Extension::TrackingFlags::Flag;
+use Bugzilla::Extension::TrackingFlags::Flag::Bug;
+use Bugzilla::Extension::TrackingFlags::Flag::Value;
+use Bugzilla::Extension::TrackingFlags::Flag::Visibility;
+
+use JSON;
+use Scalar::Util qw(blessed);
+
+use base qw(Exporter);
+our @EXPORT = qw(
+ admin_list
+ admin_edit
+);
+
+#
+# page loading
+#
+
+sub admin_list {
+ my ($vars) = @_;
+ $vars->{show_bug_counts} = Bugzilla->input_params->{show_bug_counts};
+ $vars->{flags} = [ Bugzilla::Extension::TrackingFlags::Flag->get_all() ];
+}
+
+sub admin_edit {
+ my ($vars, $page) = @_;
+ my $input = Bugzilla->input_params;
+
+ $vars->{groups} = _groups_to_json();
+ $vars->{mode} = $input->{mode} || 'new';
+ $vars->{flag_id} = $input->{flag_id} || 0;
+ $vars->{tracking_flag_types} = FLAG_TYPES;
+
+ if ($input->{delete}) {
+ my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id})
+ || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{flag_id} });
+ $flag->remove_from_db();
+
+ $vars->{message} = 'tracking_flag_deleted';
+ $vars->{flag} = $flag;
+ $vars->{flags} = [ Bugzilla::Extension::TrackingFlags::Flag->get_all() ];
+
+ print Bugzilla->cgi->header;
+ my $template = Bugzilla->template;
+ $template->process('pages/tracking_flags_admin_list.html.tmpl', $vars)
+ || ThrowTemplateError($template->error());
+ exit;
+
+ } elsif ($input->{save}) {
+ # save
+
+ my ($flag, $values, $visibilities) = _load_from_input($input, $vars);
+ _validate($flag, $values, $visibilities);
+ my $flag_obj = _update_db($flag, $values, $visibilities);
+
+ $vars->{flag} = $flag_obj;
+ $vars->{values} = _flag_values_to_json($values);
+ $vars->{visibility} = _flag_visibility_to_json($visibilities);
+
+ if ($vars->{mode} eq 'new') {
+ $vars->{message} = 'tracking_flag_created';
+ } else {
+ $vars->{message} = 'tracking_flag_updated';
+ }
+ $vars->{mode} = 'edit';
+
+ } else {
+ # initial load
+
+ if ($vars->{mode} eq 'edit') {
+ # edit - straight load
+ my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id})
+ || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{flag_id} });
+ $vars->{flag} = $flag;
+ $vars->{values} = _flag_values_to_json($flag->values);
+ $vars->{visibility} = _flag_visibility_to_json($flag->visibility);
+ $vars->{can_delete} = !$flag->bug_count;
+
+ } elsif ($vars->{mode} eq 'copy') {
+ # copy - load the source flag
+ $vars->{mode} = 'new';
+ my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($input->{copy_from})
+ || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{copy_from} });
+
+ # increment the number at the end of the name and description
+ if ($flag->name =~ /^(\D+)(\d+)$/) {
+ $flag->set_name("$1" . ($2 + 1));
+ }
+ if ($flag->description =~ /^(\D+)(\d+)$/) {
+ $flag->set_description("$1" . ($2 + 1));
+ }
+ $flag->set_sortkey(_next_unique_sortkey($flag->sortkey));
+ $flag->set_type($flag->flag_type);
+ $flag->set_enter_bug($flag->enter_bug);
+ # always default new flags as active, even when copying an inactive one
+ $flag->set_is_active(1);
+
+ $vars->{flag} = $flag;
+ $vars->{values} = _flag_values_to_json($flag->values, 1);
+ $vars->{visibility} = _flag_visibility_to_json($flag->visibility, 1);
+ $vars->{can_delete} = 0;
+
+ } else {
+ $vars->{mode} = 'new';
+ $vars->{flag} = {
+ sortkey => 0,
+ enter_bug => 1,
+ is_active => 1,
+ };
+ $vars->{values} = _flag_values_to_json([
+ {
+ id => 0,
+ value => '---',
+ setter_group_id => '',
+ is_active => 1,
+ comment => '',
+ },
+ ]);
+ $vars->{visibility} = '';
+ $vars->{can_delete} = 0;
+ }
+ }
+}
+
+sub _load_from_input {
+ my ($input, $vars) = @_;
+
+ # flag
+
+ my $flag = {
+ id => ($input->{mode} eq 'edit' ? $input->{flag_id} : 0),
+ name => trim($input->{flag_name} || ''),
+ description => trim($input->{flag_desc} || ''),
+ sortkey => $input->{flag_sort} || 0,
+ type => trim($input->{flag_type} || ''),
+ enter_bug => $input->{flag_enter_bug} ? 1 : 0,
+ is_active => $input->{flag_active} ? 1 : 0,
+ };
+ detaint_natural($flag->{id});
+ detaint_natural($flag->{sortkey});
+ detaint_natural($flag->{enter_bug});
+ detaint_natural($flag->{is_active});
+
+ # values
+
+ my $values = decode_json($input->{values} || '[]');
+ foreach my $value (@$values) {
+ $value->{value} = '' unless exists $value->{value} && defined $value->{value};
+ $value->{setter_group_id} = '' unless $value->{setter_group_id};
+ $value->{is_active} = $value->{is_active} ? 1 : 0;
+ }
+
+ # vibility
+
+ my $visibilities = decode_json($input->{visibility} || '[]');
+ foreach my $visibility (@$visibilities) {
+ $visibility->{product} = '' unless exists $visibility->{product} && defined $visibility->{product};
+ $visibility->{component} = '' unless exists $visibility->{component} && defined $visibility->{component};
+ }
+
+ return ($flag, $values, $visibilities);
+}
+
+sub _next_unique_sortkey {
+ my ($sortkey) = @_;
+
+ my %current;
+ foreach my $flag (Bugzilla::Extension::TrackingFlags::Flag->get_all()) {
+ $current{$flag->sortkey} = 1;
+ }
+
+ $sortkey += 5;
+ $sortkey += 5 while exists $current{$sortkey};
+ return $sortkey;
+}
+
+#
+# validation
+#
+
+sub _validate {
+ my ($flag, $values, $visibilities) = @_;
+
+ # flag
+
+ my @missing;
+ push @missing, 'Field Name' if $flag->{name} eq '';
+ push @missing, 'Field Description' if $flag->{description} eq '';
+ push @missing, 'Field Sort Key' if $flag->{sortkey} eq '';
+ scalar(@missing)
+ && ThrowUserError('tracking_flags_missing_mandatory', { fields => \@missing });
+
+ $flag->{name} =~ /^cf_/
+ || ThrowUserError('tracking_flags_cf_prefix');
+
+ if ($flag->{id}) {
+ my $old_flag = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id})
+ || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $flag->{id} });
+ if ($flag->{name} ne $old_flag->name) {
+ Bugzilla::Field->new({ name => $flag->{name} })
+ && ThrowUserError('field_already_exists', { field => { name => $flag->{name} }});
+ }
+ } else {
+ Bugzilla::Field->new({ name => $flag->{name} })
+ && ThrowUserError('field_already_exists', { field => { name => $flag->{name} }});
+ }
+
+ # values
+
+ scalar(@$values)
+ || ThrowUserError('tracking_flags_missing_values');
+
+ my %seen;
+ foreach my $value (@$values) {
+ my $v = $value->{value};
+
+ $v eq ''
+ && ThrowUserError('tracking_flags_missing_value');
+
+ exists $seen{$v}
+ && ThrowUserError('tracking_flags_duplicate_value', { value => $v });
+ $seen{$v} = 1;
+
+ push @missing, "Setter for $v" if !$value->{setter_group_id};
+ }
+ scalar(@missing)
+ && ThrowUserError('tracking_flags_missing_mandatory', { fields => \@missing });
+
+ # visibility
+
+ scalar(@$visibilities)
+ || ThrowUserError('tracking_flags_missing_visibility');
+
+ %seen = ();
+ foreach my $visibility (@$visibilities) {
+ my $name = $visibility->{product} . ':' . $visibility->{component};
+
+ exists $seen{$name}
+ && ThrowUserError('tracking_flags_duplicate_visibility', { name => $name });
+
+ $visibility->{product_obj} = Bugzilla::Product->new({ name => $visibility->{product} })
+ || ThrowCodeError('tracking_flags_invalid_product', { product => $visibility->{product} });
+
+ if ($visibility->{component} ne '') {
+ $visibility->{component_obj} = Bugzilla::Component->new({ product => $visibility->{product_obj},
+ name => $visibility->{component} })
+ || ThrowCodeError('tracking_flags_invalid_component', {
+ product => $visibility->{product},
+ component_name => $visibility->{component},
+ });
+ }
+ }
+
+}
+
+#
+# database updating
+#
+
+sub _update_db {
+ my ($flag, $values, $visibilities) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->bz_start_transaction();
+ my $flag_obj = _update_db_flag($flag);
+ _update_db_values($flag_obj, $flag, $values);
+ _update_db_visibility($flag_obj, $flag, $visibilities);
+ $dbh->bz_commit_transaction();
+
+ return $flag_obj;
+}
+
+sub _update_db_flag {
+ my ($flag) = @_;
+
+ my $object_set = {
+ name => $flag->{name},
+ description => $flag->{description},
+ sortkey => $flag->{sortkey},
+ type => $flag->{type},
+ enter_bug => $flag->{enter_bug},
+ is_active => $flag->{is_active},
+ };
+
+ my $flag_obj;
+ if ($flag->{id}) {
+ # update existing flag
+ $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id})
+ || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $flag->{id} });
+ $flag_obj->set_all($object_set);
+ $flag_obj->update();
+
+ } else {
+ # create new flag
+ $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->create($object_set);
+ }
+
+ return $flag_obj;
+}
+
+sub _update_db_values {
+ my ($flag_obj, $flag, $values) = @_;
+
+ # delete
+ foreach my $current_value (@{ $flag_obj->values }) {
+ if (!grep { $_->{id} == $current_value->id } @$values) {
+ $current_value->remove_from_db();
+ }
+ }
+
+ # add/update
+ my $sortkey = 0;
+ foreach my $value (@{ $values }) {
+ $sortkey += 10;
+
+ my $object_set = {
+ value => $value->{value},
+ setter_group_id => $value->{setter_group_id},
+ is_active => $value->{is_active},
+ sortkey => $sortkey,
+ comment => $value->{comment},
+ };
+
+ if ($value->{id}) {
+ my $value_obj = Bugzilla::Extension::TrackingFlags::Flag::Value->new($value->{id})
+ || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag value', id => $flag->{id} });
+ my $old_value = $value_obj->value;
+ $value_obj->set_all($object_set);
+ $value_obj->update();
+ Bugzilla::Extension::TrackingFlags::Flag::Bug->update_all_values({
+ value_obj => $value_obj,
+ old_value => $old_value,
+ new_value => $value_obj->value,
+ });
+ } else {
+ $object_set->{tracking_flag_id} = $flag_obj->flag_id;
+ Bugzilla::Extension::TrackingFlags::Flag::Value->create($object_set);
+ }
+ }
+}
+
+sub _update_db_visibility {
+ my ($flag_obj, $flag, $visibilities) = @_;
+
+ # delete
+ foreach my $current_visibility (@{ $flag_obj->visibility }) {
+ if (!grep { $_->{id} == $current_visibility->id } @$visibilities) {
+ $current_visibility->remove_from_db();
+ }
+ }
+
+ # add
+ foreach my $visibility (@{ $visibilities }) {
+ next if $visibility->{id};
+ Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({
+ tracking_flag_id => $flag_obj->flag_id,
+ product_id => $visibility->{product_obj}->id,
+ component_id => $visibility->{component} ? $visibility->{component_obj}->id : undef,
+ });
+ }
+}
+
+#
+# serialisation
+#
+
+sub _groups_to_json {
+ my @data;
+ foreach my $group (sort { $a->name cmp $b->name } Bugzilla::Group->get_all()) {
+ push @data, {
+ id => $group->id,
+ name => $group->name,
+ };
+ }
+ return encode_json(\@data);
+}
+
+sub _flag_values_to_json {
+ my ($values, $is_copy) = @_;
+ # setting is_copy will set the id's to zero, to force new values rather
+ # than editing existing ones
+ my @data;
+ foreach my $value (@$values) {
+ push @data, {
+ id => $is_copy ? 0 : $value->{id},
+ value => $value->{value},
+ setter_group_id => $value->{setter_group_id},
+ is_active => $value->{is_active} ? JSON::true : JSON::false,
+ comment => $value->{comment} // '',
+ };
+ }
+ return encode_json(\@data);
+}
+
+sub _flag_visibility_to_json {
+ my ($visibilities, $is_copy) = @_;
+ # setting is_copy will set the id's to zero, to force new visibilites
+ # rather than editing existing ones
+ my @data;
+
+ foreach my $visibility (@$visibilities) {
+ my $product = exists $visibility->{product_id}
+ ? $visibility->product->name
+ : $visibility->{product};
+ my $component;
+ if (exists $visibility->{component_id} && $visibility->{component_id}) {
+ $component = $visibility->component->name;
+ } elsif (exists $visibility->{component}) {
+ $component = $visibility->{component};
+ } else {
+ $component = undef;
+ }
+ push @data, {
+ id => $is_copy ? 0 : $visibility->{id},
+ product => $product,
+ component => $component,
+ };
+ }
+ @data = sort {
+ lc($a->{product}) cmp lc($b->{product})
+ || lc($a->{component}) cmp lc($b->{component})
+ } @data;
+ return encode_json(\@data);
+}
+
+1;
diff --git a/extensions/TrackingFlags/lib/Constants.pm b/extensions/TrackingFlags/lib/Constants.pm
new file mode 100644
index 000000000..0b1ae3a1a
--- /dev/null
+++ b/extensions/TrackingFlags/lib/Constants.pm
@@ -0,0 +1,41 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TrackingFlags::Constants;
+
+use strict;
+use base qw(Exporter);
+
+our @EXPORT = qw(
+ FLAG_TYPES
+);
+
+sub FLAG_TYPES {
+ my @flag_types = (
+ {
+ name => 'project',
+ description => 'Project Flags',
+ collapsed => 0,
+ sortkey => 0
+ },
+ {
+ name => 'tracking',
+ description => 'Tracking Flags',
+ collapsed => 1,
+ sortkey => 1
+ },
+ {
+ name => 'blocking',
+ description => 'Blocking Flags',
+ collapsed => 1,
+ sortkey => 2
+ },
+ );
+ return [ sort { $a->{'sortkey'} <=> $b->{'sortkey'} } @flag_types ];
+}
+
+1;
diff --git a/extensions/TrackingFlags/lib/Flag.pm b/extensions/TrackingFlags/lib/Flag.pm
new file mode 100644
index 000000000..3ae7a937e
--- /dev/null
+++ b/extensions/TrackingFlags/lib/Flag.pm
@@ -0,0 +1,467 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TrackingFlags::Flag;
+
+use base qw(Bugzilla::Object);
+
+use strict;
+use warnings;
+
+use Bugzilla::Error;
+use Bugzilla::Constants;
+use Bugzilla::Util qw(detaint_natural trim);
+use Bugzilla::Config qw(SetParam write_params);
+
+use Bugzilla::Extension::TrackingFlags::Constants;
+use Bugzilla::Extension::TrackingFlags::Flag::Bug;
+use Bugzilla::Extension::TrackingFlags::Flag::Value;
+use Bugzilla::Extension::TrackingFlags::Flag::Visibility;
+
+###############################
+#### Initialization ####
+###############################
+
+use constant DB_TABLE => 'tracking_flags';
+
+use constant DB_COLUMNS => qw(
+ id
+ field_id
+ name
+ description
+ type
+ sortkey
+ enter_bug
+ is_active
+);
+
+use constant LIST_ORDER => 'sortkey';
+
+use constant UPDATE_COLUMNS => qw(
+ name
+ description
+ type
+ sortkey
+ enter_bug
+ is_active
+);
+
+use constant VALIDATORS => {
+ name => \&_check_name,
+ description => \&_check_description,
+ type => \&_check_type,
+ sortkey => \&_check_sortkey,
+ enter_bug => \&Bugzilla::Object::check_boolean,
+ is_active => \&Bugzilla::Object::check_boolean,
+};
+
+use constant UPDATE_VALIDATORS => {
+ name => \&_check_name,
+ description => \&_check_description,
+ type => \&_check_type,
+ sortkey => \&_check_sortkey,
+ enter_bug => \&Bugzilla::Object::check_boolean,
+ is_active => \&Bugzilla::Object::check_boolean,
+};
+
+###############################
+#### Methods ####
+###############################
+
+sub new {
+ my $class = shift;
+ my $param = shift;
+ my $cache = Bugzilla->request_cache;
+
+ if (!ref $param
+ && exists $cache->{'tracking_flags'}
+ && exists $cache->{'tracking_flags'}->{$param})
+ {
+ return $cache->{'tracking_flags'}->{$param};
+ }
+
+ return $class->SUPER::new($param);
+}
+
+sub create {
+ my $class = shift;
+ my $params = shift;
+ my $dbh = Bugzilla->dbh;
+ my $flag;
+
+ # Disable bug updates temporarily to avoid conflicts.
+ SetParam('disable_bug_updates', 1);
+ write_params();
+
+ eval {
+ $dbh->bz_start_transaction();
+
+ $params = $class->run_create_validators($params);
+
+ # We have to create an entry for this new flag
+ # in the fielddefs table for use elsewhere. We cannot
+ # use Bugzilla::Field->create as it will create the
+ # additional tables needed by custom fields which we
+ # do not need. Also we do this so as not to add a
+ # another column to the bugs table.
+ # We will create the entry as a custom field with a
+ # type of FIELD_TYPE_EXTENSION so Bugzilla will skip
+ # these field types in certain parts of the core code.
+ $dbh->do("INSERT INTO fielddefs
+ (name, description, sortkey, type, custom, obsolete, buglist)
+ VALUES
+ (?, ?, ?, ?, ?, ?, ?)",
+ undef,
+ $params->{'name'},
+ $params->{'description'},
+ $params->{'sortkey'},
+ FIELD_TYPE_EXTENSION,
+ 1, 0, 1);
+ $params->{'field_id'} = $dbh->bz_last_key;
+
+ $flag = $class->SUPER::create($params);
+
+ $dbh->bz_commit_transaction();
+ };
+ my $error = "$@";
+ SetParam('disable_bug_updates', 0);
+ write_params();
+ die $error if $error;
+
+ return $flag;
+}
+
+sub update {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ my $old_self = $self->new($self->flag_id);
+
+ # HACK! Bugzilla::Object::update uses hardcoded $self->id
+ # instead of $self->{ID_FIELD} so we need to reverse field_id
+ # and the real id temporarily
+ my $field_id = $self->id;
+ $self->{'field_id'} = $self->{'id'};
+
+ my $changes = $self->SUPER::update(@_);
+
+ $self->{'field_id'} = $field_id;
+
+ # Update the fielddefs entry
+ $dbh->do("UPDATE fielddefs SET name = ?, description = ? WHERE name = ?",
+ undef,
+ $self->name, $self->description, $old_self->name);
+
+ # Update request_cache
+ my $cache = Bugzilla->request_cache;
+ if (exists $cache->{'tracking_flags'}) {
+ $cache->{'tracking_flags'}->{$self->flag_id} = $self;
+ }
+
+ return $changes;
+}
+
+sub match {
+ my $class = shift;
+ my ($params) = @_;
+
+ # Use later for preload
+ my $bug_id = delete $params->{'bug_id'};
+
+ # Retrieve all flags relevant for the given product and component
+ if (!exists $params->{'id'}
+ && ($params->{'component'} || $params->{'component_id'}
+ || $params->{'product'} || $params->{'product_id'}))
+ {
+ my $visible_flags
+ = Bugzilla::Extension::TrackingFlags::Flag::Visibility->match(@_);
+ my @flag_ids = map { $_->tracking_flag_id } @$visible_flags;
+
+ delete $params->{'component'} if exists $params->{'component'};
+ delete $params->{'component_id'} if exists $params->{'component_id'};
+ delete $params->{'product'} if exists $params->{'product'};
+ delete $params->{'product_id'} if exists $params->{'product_id'};
+
+ $params->{'id'} = \@flag_ids;
+ }
+
+ # We need to return inactive flags if a value has been set
+ my $is_active_filter = delete $params->{is_active};
+
+ my $flags = $class->SUPER::match($params);
+ preload_all_the_things($flags, { bug_id => $bug_id });
+
+ if ($is_active_filter) {
+ $flags = [ grep { $_->is_active || exists $_->{bug_flag} } @$flags ];
+ }
+ return [ sort { $a->sortkey <=> $b->sortkey } @$flags ];
+}
+
+sub get_all {
+ my $self = shift;
+ my $cache = Bugzilla->request_cache;
+ if (!exists $cache->{'tracking_flags'}) {
+ my @tracking_flags = $self->SUPER::get_all(@_);
+ preload_all_the_things(\@tracking_flags);
+ my %tracking_flags_hash = map { $_->flag_id => $_ } @tracking_flags;
+ $cache->{'tracking_flags'} = \%tracking_flags_hash;
+ }
+ return sort { $a->flag_type cmp $b->flag_type || $a->sortkey <=> $b->sortkey }
+ values %{ $cache->{'tracking_flags'} };
+}
+
+# avoids the overhead of pre-loading if just the field names are required
+sub get_all_names {
+ my $self = shift;
+ my $cache = Bugzilla->request_cache;
+ if (!exists $cache->{'tracking_flags_names'}) {
+ $cache->{'tracking_flags_names'} =
+ Bugzilla->dbh->selectcol_arrayref("SELECT name FROM tracking_flags ORDER BY name");
+ }
+ return @{ $cache->{'tracking_flags_names'} };
+}
+
+sub remove_from_db {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ # Check to see if tracking_flags_bugs table has records
+ if ($self->bug_count) {
+ ThrowUserError('tracking_flag_has_contents', { flag => $self });
+ }
+
+ # Disable bug updates temporarily to avoid conflicts.
+ SetParam('disable_bug_updates', 1);
+ write_params();
+
+ eval {
+ $dbh->bz_start_transaction();
+
+ $dbh->do('DELETE FROM bugs_activity WHERE fieldid = ?', undef, $self->id);
+ $dbh->do('DELETE FROM fielddefs WHERE name = ?', undef, $self->name);
+
+ $dbh->bz_commit_transaction();
+
+ # Remove from request cache
+ my $cache = Bugzilla->request_cache;
+ if (exists $cache->{'tracking_flags'}) {
+ delete $cache->{'tracking_flags'}->{$self->flag_id};
+ }
+ };
+ my $error = "$@";
+ SetParam('disable_bug_updates', 0);
+ write_params();
+ die $error if $error;
+}
+
+sub preload_all_the_things {
+ my ($flags, $params) = @_;
+
+ my %flag_hash = map { $_->flag_id => $_ } @$flags;
+ my @flag_ids = keys %flag_hash;
+ return unless @flag_ids;
+
+ # Preload values
+ my $value_objects
+ = Bugzilla::Extension::TrackingFlags::Flag::Value->match({ tracking_flag_id => \@flag_ids });
+
+ # Now populate the tracking flags with this set of value objects.
+ foreach my $obj (@$value_objects) {
+ my $flag_id = $obj->tracking_flag_id;
+
+ # Prepopulate the tracking flag object in the value object
+ $obj->{'tracking_flag'} = $flag_hash{$flag_id};
+
+ # Prepopulate the current value objects for this tracking flag
+ $flag_hash{$flag_id}->{'values'} ||= [];
+ push(@{$flag_hash{$flag_id}->{'values'}}, $obj);
+ }
+
+ # Preload bug values if a bug_id is passed
+ if ($params && exists $params->{'bug_id'} && $params->{'bug_id'}) {
+ # We don't want to use @flag_ids here as we want all flags attached to this bug
+ # even if they are inactive.
+ my $bug_objects
+ = Bugzilla::Extension::TrackingFlags::Flag::Bug->match({ bug_id => $params->{'bug_id'} });
+ # Now populate the tracking flags with this set of objects.
+ # Also we add them to the flag hash since we want them to be visible even if
+ # they are not longer applicable to this product/component.
+ foreach my $obj (@$bug_objects) {
+ my $flag_id = $obj->tracking_flag_id;
+
+ # Load the flag object if it does not yet exist.
+ # This can happen if the bug value tracking flag
+ # is no longer visible for the product/component
+ $flag_hash{$flag_id}
+ ||= Bugzilla::Extension::TrackingFlags::Flag->new($flag_id);
+
+ # Prepopulate the tracking flag object in the bug flag object
+ $obj->{'tracking_flag'} = $flag_hash{$flag_id};
+
+ # Prepopulate the the current bug flag object for the tracking flag
+ $flag_hash{$flag_id}->{'bug_flag'} = $obj;
+ }
+ }
+
+ @$flags = values %flag_hash;
+}
+
+###############################
+#### Validators ####
+###############################
+
+sub _check_name {
+ my ($invocant, $name) = @_;
+ $name = trim($name);
+ $name || ThrowCodeError('param_required', { param => 'name' });
+ return $name;
+}
+
+sub _check_description {
+ my ($invocant, $description) = @_;
+ $description = trim($description);
+ $description || ThrowCodeError( 'param_required', { param => 'description' } );
+ return $description;
+}
+
+sub _check_type {
+ my ($invocant, $type) = @_;
+ $type = trim($type);
+ $type || ThrowCodeError( 'param_required', { param => 'type' } );
+ grep($_->{name} eq $type, @{FLAG_TYPES()})
+ || ThrowUserError('tracking_flags_invalid_flag_type', { type => $type });
+ return $type;
+}
+
+sub _check_sortkey {
+ my ($invocant, $sortkey) = @_;
+ detaint_natural($sortkey)
+ || ThrowUserError('field_invalid_sortkey', { sortkey => $sortkey });
+ return $sortkey;
+}
+
+###############################
+#### Setters ####
+###############################
+
+sub set_name { $_[0]->set('name', $_[1]); }
+sub set_description { $_[0]->set('description', $_[1]); }
+sub set_type { $_[0]->set('type', $_[1]); }
+sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
+sub set_enter_bug { $_[0]->set('enter_bug', $_[1]); }
+sub set_is_active { $_[0]->set('is_active', $_[1]); }
+
+###############################
+#### Accessors ####
+###############################
+
+sub flag_id { return $_[0]->{'id'}; }
+sub name { return $_[0]->{'name'}; }
+sub description { return $_[0]->{'description'}; }
+sub flag_type { return $_[0]->{'type'}; }
+sub sortkey { return $_[0]->{'sortkey'}; }
+sub enter_bug { return $_[0]->{'enter_bug'}; }
+sub is_active { return $_[0]->{'is_active'}; }
+
+sub values {
+ return $_[0]->{'values'} ||= Bugzilla::Extension::TrackingFlags::Flag::Value->match({
+ tracking_flag_id => $_[0]->flag_id
+ });
+}
+
+sub visibility {
+ return $_[0]->{'visibility'} ||= Bugzilla::Extension::TrackingFlags::Flag::Visibility->match({
+ tracking_flag_id => $_[0]->flag_id
+ });
+}
+
+sub can_set_value {
+ my ($self, $new_value, $user) = @_;
+ $user ||= Bugzilla->user;
+ my $new_value_obj;
+ foreach my $value (@{$self->values}) {
+ if ($value->value eq $new_value) {
+ $new_value_obj = $value;
+ last;
+ }
+ }
+ return $new_value_obj
+ && $new_value_obj->setter_group
+ && $user->in_group($new_value_obj->setter_group->name)
+ ? 1
+ : 0;
+}
+
+sub bug_flag {
+ my ($self, $bug_id) = @_;
+ # Return the current bug value object if defined unless the passed bug_id does
+ # not equal the current bug value objects id.
+ if (defined $self->{'bug_flag'}
+ && (!$bug_id || $self->{'bug_flag'}->bug->id == $bug_id))
+ {
+ return $self->{'bug_flag'};
+ }
+
+ # Flag::Bug->new will return a default bug value object if $params undefined
+ my $params = !$bug_id
+ ? undef
+ : { condition => "tracking_flag_id = ? AND bug_id = ?",
+ values => [ $self->flag_id, $bug_id ] };
+ return $self->{'bug_flag'} = Bugzilla::Extension::TrackingFlags::Flag::Bug->new($params);
+}
+
+sub bug_count {
+ my ($self) = @_;
+ return $self->{'bug_count'} if defined $self->{'bug_count'};
+ my $dbh = Bugzilla->dbh;
+ return $self->{'bug_count'} = scalar $dbh->selectrow_array("
+ SELECT COUNT(bug_id)
+ FROM tracking_flags_bugs
+ WHERE tracking_flag_id = ?",
+ undef, $self->flag_id);
+}
+
+sub activity_count {
+ my ($self) = @_;
+ return $self->{'activity_count'} if defined $self->{'activity_count'};
+ my $dbh = Bugzilla->dbh;
+ return $self->{'activity_count'} = scalar $dbh->selectrow_array("
+ SELECT COUNT(bug_id)
+ FROM bugs_activity
+ WHERE fieldid = ?",
+ undef, $self->id);
+}
+
+######################################
+# Compatibility with Bugzilla::Field #
+######################################
+
+# Here we return 'field_id' instead of the real
+# id as we want other Bugzilla code to treat this
+# as a Bugzilla::Field object in certain places.
+sub id { return $_[0]->{'field_id'}; }
+sub type { return FIELD_TYPE_EXTENSION; }
+sub legal_values { return $_[0]->values; }
+sub custom { return 1; }
+sub in_new_bugmail { return 1; }
+sub obsolete { return $_[0]->is_active ? 0 : 1; }
+sub buglist { return 1; }
+sub is_select { return 1; }
+sub is_abnormal { return 1; }
+sub is_timetracking { return 0; }
+sub visibility_field { return undef; }
+sub visibility_values { return undef; }
+sub controls_visibility_of { return undef; }
+sub value_field { return undef; }
+sub controls_values_of { return undef; }
+sub is_visible_on_bug { return 1; }
+sub is_relationship { return 0; }
+sub reverse_desc { return ''; }
+sub is_mandatory { return 0; }
+sub is_numeric { return 0; }
+
+1;
diff --git a/extensions/TrackingFlags/lib/Flag/Bug.pm b/extensions/TrackingFlags/lib/Flag/Bug.pm
new file mode 100644
index 000000000..ea382a29d
--- /dev/null
+++ b/extensions/TrackingFlags/lib/Flag/Bug.pm
@@ -0,0 +1,187 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TrackingFlags::Flag::Bug;
+
+use base qw(Bugzilla::Object);
+
+use strict;
+use warnings;
+
+use Bugzilla::Extension::TrackingFlags::Flag;
+
+use Bugzilla::Bug;
+use Bugzilla::Error;
+
+use Scalar::Util qw(blessed);
+
+###############################
+#### Initialization ####
+###############################
+
+use constant DEFAULT_FLAG_BUG => {
+ 'id' => 0,
+ 'tracking_flag_id' => 0,
+ 'bug_id' => '',
+ 'value' => '---',
+};
+
+use constant DB_TABLE => 'tracking_flags_bugs';
+
+use constant DB_COLUMNS => qw(
+ id
+ tracking_flag_id
+ bug_id
+ value
+);
+
+use constant LIST_ORDER => 'id';
+
+use constant UPDATE_COLUMNS => qw(
+ value
+);
+
+use constant VALIDATORS => {
+ tracking_flag_id => \&_check_tracking_flag,
+ value => \&_check_value,
+};
+
+use constant AUDIT_CREATES => 0;
+use constant AUDIT_UPDATES => 0;
+use constant AUDIT_REMOVES => 0;
+
+###############################
+#### Object Methods ####
+###############################
+
+sub new {
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+ my ($param) = @_;
+
+ my $self;
+ if ($param) {
+ $self = $class->SUPER::new(@_);
+ if (!$self) {
+ $self = DEFAULT_FLAG_BUG;
+ bless($self, $class);
+ }
+ }
+ else {
+ $self = DEFAULT_FLAG_BUG;
+ bless($self, $class);
+ }
+
+ return $self
+}
+
+sub match {
+ my $class = shift;
+ my $bug_flags = $class->SUPER::match(@_);
+ preload_all_the_things($bug_flags);
+ return $bug_flags;
+}
+
+sub remove_from_db {
+ my ($self) = @_;
+ $self->SUPER::remove_from_db();
+ $self->{'id'} = $self->{'tracking_flag_id'} = $self->{'bug_id'} = 0;
+ $self->{'value'} = '---';
+}
+
+sub preload_all_the_things {
+ my ($bug_flags) = @_;
+ my $cache = Bugzilla->request_cache;
+
+ # Preload tracking flag objects
+ my @tracking_flag_ids;
+ foreach my $bug_flag (@$bug_flags) {
+ if (exists $cache->{'tracking_flags'}
+ && $cache->{'tracking_flags'}->{$bug_flag->tracking_flag_id})
+ {
+ $bug_flag->{'tracking_flag'}
+ = $cache->{'tracking_flags'}->{$bug_flag->tracking_flag_id};
+ next;
+ }
+ push(@tracking_flag_ids, $bug_flag->tracking_flag_id);
+ }
+
+ return unless @tracking_flag_ids;
+
+ my $tracking_flags
+ = Bugzilla::Extension::TrackingFlags::Flag->match({ id => \@tracking_flag_ids });
+ my %tracking_flag_hash = map { $_->flag_id => $_ } @$tracking_flags;
+
+ foreach my $bug_flag (@$bug_flags) {
+ next if exists $bug_flag->{'tracking_flag'};
+ $bug_flag->{'tracking_flag'} = $tracking_flag_hash{$bug_flag->tracking_flag_id};
+ }
+}
+
+##############################
+#### Class Methods ####
+##############################
+
+sub update_all_values {
+ my ($invocant, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+ $dbh->do(
+ "UPDATE tracking_flags_bugs SET value=? WHERE tracking_flag_id=? AND value=?",
+ undef,
+ $params->{new_value},
+ $params->{value_obj}->tracking_flag_id,
+ $params->{old_value},
+ );
+}
+
+###############################
+#### Validators ####
+###############################
+
+sub _check_value {
+ my ($invocant, $value) = @_;
+ $value || ThrowCodeError('param_required', { param => 'value' });
+ return $value;
+}
+
+sub _check_tracking_flag {
+ my ($invocant, $flag) = @_;
+ if (blessed $flag) {
+ return $flag->flag_id;
+ }
+ $flag = Bugzilla::Extension::TrackingFlags::Flag->new({ id => $flag, cache => 1 })
+ || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag });
+ return $flag->flag_id;
+}
+
+###############################
+#### Setters ####
+###############################
+
+sub set_value { $_[0]->set('value', $_[1]); }
+
+###############################
+#### Accessors ####
+###############################
+
+sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; }
+sub bug_id { return $_[0]->{'bug_id'}; }
+sub value { return $_[0]->{'value'}; }
+
+sub bug {
+ return $_[0]->{'bug'} ||= Bugzilla::Bug->new({
+ id => $_[0]->bug_id, cache => 1
+ });
+}
+
+sub tracking_flag {
+ return $_[0]->{'tracking_flag'} ||= Bugzilla::Extension::TrackingFlags::Flag->new({
+ id => $_[0]->tracking_flag_id, cache => 1
+ });
+}
+
+1;
diff --git a/extensions/TrackingFlags/lib/Flag/Value.pm b/extensions/TrackingFlags/lib/Flag/Value.pm
new file mode 100644
index 000000000..964d76810
--- /dev/null
+++ b/extensions/TrackingFlags/lib/Flag/Value.pm
@@ -0,0 +1,142 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TrackingFlags::Flag::Value;
+
+use base qw(Bugzilla::Object);
+
+use strict;
+use warnings;
+
+use Bugzilla::Error;
+use Bugzilla::Group;
+use Bugzilla::Util qw(detaint_natural trim);
+use Scalar::Util qw(blessed);
+
+###############################
+#### Initialization ####
+###############################
+
+use constant DB_TABLE => 'tracking_flags_values';
+
+use constant DB_COLUMNS => qw(
+ id
+ tracking_flag_id
+ setter_group_id
+ value
+ sortkey
+ is_active
+ comment
+);
+
+use constant LIST_ORDER => 'sortkey';
+
+use constant UPDATE_COLUMNS => qw(
+ setter_group_id
+ value
+ sortkey
+ is_active
+ comment
+);
+
+use constant VALIDATORS => {
+ tracking_flag_id => \&_check_tracking_flag,
+ setter_group_id => \&_check_setter_group,
+ value => \&_check_value,
+ sortkey => \&_check_sortkey,
+ is_active => \&Bugzilla::Object::check_boolean,
+ comment => \&_check_comment,
+};
+
+###############################
+#### Validators ####
+###############################
+
+sub _check_value {
+ my ($invocant, $value) = @_;
+ defined $value || ThrowCodeError('param_required', { param => 'value' });
+ return $value;
+}
+
+sub _check_tracking_flag {
+ my ($invocant, $flag) = @_;
+ if (blessed $flag) {
+ return $flag->flag_id;
+ }
+ $flag = Bugzilla::Extension::TrackingFlags::Flag->new({ id => $flag, cache => 1 })
+ || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag });
+ return $flag->flag_id;
+}
+
+sub _check_setter_group {
+ my ($invocant, $group) = @_;
+ if (blessed $group) {
+ return $group->id;
+ }
+ $group = Bugzilla::Group->new({ id => $group, cache => 1 })
+ || ThrowCodeError('tracking_flags_invalid_param', { name => 'setter_group_id', value => $group });
+ return $group->id;
+}
+
+sub _check_sortkey {
+ my ($invocant, $sortkey) = @_;
+ detaint_natural($sortkey)
+ || ThrowUserError('field_invalid_sortkey', { sortkey => $sortkey });
+ return $sortkey;
+}
+
+sub _check_comment {
+ my ($invocant, $value) = @_;
+ return undef unless defined $value;
+ $value = trim($value);
+ return $value eq '' ? undef : $value;
+}
+
+###############################
+#### Setters ####
+###############################
+
+sub set_setter_group_id { $_[0]->set('setter_group_id', $_[1]); }
+sub set_value { $_[0]->set('value', $_[1]); }
+sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
+sub set_is_active { $_[0]->set('is_active', $_[1]); }
+sub set_comment { $_[0]->set('comment', $_[1]); }
+
+###############################
+#### Accessors ####
+###############################
+
+sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; }
+sub setter_group_id { return $_[0]->{'setter_group_id'}; }
+sub value { return $_[0]->{'value'}; }
+sub sortkey { return $_[0]->{'sortkey'}; }
+sub is_active { return $_[0]->{'is_active'}; }
+sub comment { return $_[0]->{'comment'}; }
+
+sub tracking_flag {
+ return $_[0]->{'tracking_flag'} ||= Bugzilla::Extension::TrackingFlags::Flag->new({
+ id => $_[0]->tracking_flag_id, cache => 1
+ });
+}
+
+sub setter_group {
+ if ($_[0]->setter_group_id) {
+ $_[0]->{'setter_group'} ||= Bugzilla::Group->new({
+ id => $_[0]->setter_group_id, cache => 1
+ });
+ }
+ return $_[0]->{'setter_group'};
+}
+
+########################################
+## Compatibility with Bugzilla::Field ##
+########################################
+
+sub name { return $_[0]->{'value'}; }
+sub is_visible_on_bug { return 1; }
+
+1;
diff --git a/extensions/TrackingFlags/lib/Flag/Visibility.pm b/extensions/TrackingFlags/lib/Flag/Visibility.pm
new file mode 100644
index 000000000..7600d71bd
--- /dev/null
+++ b/extensions/TrackingFlags/lib/Flag/Visibility.pm
@@ -0,0 +1,172 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TrackingFlags::Flag::Visibility;
+
+use base qw(Bugzilla::Object);
+
+use strict;
+use warnings;
+
+use Bugzilla::Error;
+use Bugzilla::Product;
+use Bugzilla::Component;
+use Scalar::Util qw(blessed);
+
+###############################
+#### Initialization ####
+###############################
+
+use constant DB_TABLE => 'tracking_flags_visibility';
+
+use constant DB_COLUMNS => qw(
+ id
+ tracking_flag_id
+ product_id
+ component_id
+);
+
+use constant LIST_ORDER => 'id';
+
+use constant UPDATE_COLUMNS => (); # imutable
+
+use constant VALIDATORS => {
+ tracking_flag_id => \&_check_tracking_flag,
+ product_id => \&_check_product,
+ component_id => \&_check_component,
+};
+
+###############################
+#### Methods ####
+###############################
+
+sub match {
+ my $class= shift;
+ my ($params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # Allow matching component and product by name
+ # (in addition to matching by ID).
+ # Borrowed from Bugzilla::Bug::match
+ my %translate_fields = (
+ product => 'Bugzilla::Product',
+ component => 'Bugzilla::Component',
+ );
+
+ foreach my $field (keys %translate_fields) {
+ my @ids;
+ # Convert names to ids. We use "exists" everywhere since people can
+ # legally specify "undef" to mean IS NULL
+ if (exists $params->{$field}) {
+ my $names = $params->{$field};
+ my $type = $translate_fields{$field};
+ my $objects = Bugzilla::Object::match($type, { name => $names });
+ push(@ids, map { $_->id } @$objects);
+ }
+ # You can also specify ids directly as arguments to this function,
+ # so include them in the list if they have been specified.
+ if (exists $params->{"${field}_id"}) {
+ my $current_ids = $params->{"${field}_id"};
+ my @id_array = ref $current_ids ? @$current_ids : ($current_ids);
+ push(@ids, @id_array);
+ }
+ # We do this "or" instead of a "scalar(@ids)" to handle the case
+ # when people passed only invalid object names. Otherwise we'd
+ # end up with a SUPER::match call with zero criteria (which dies).
+ if (exists $params->{$field} or exists $params->{"${field}_id"}) {
+ delete $params->{$field};
+ $params->{"${field}_id"} = scalar(@ids) == 1 ? [ $ids[0] ] : \@ids;
+ }
+ }
+
+ # If we aren't matching on the product, use the default matching code
+ if (!exists $params->{product_id}) {
+ return $class->SUPER::match(@_);
+ }
+
+ my @criteria = ("1=1");
+
+ if ($params->{product_id}) {
+ push(@criteria, $dbh->sql_in('product_id', $params->{'product_id'}));
+ if ($params->{component_id}) {
+ my $component_id = $params->{component_id};
+ push(@criteria, "(" . $dbh->sql_in('component_id', $params->{'component_id'}) .
+ " OR component_id IS NULL)");
+ }
+ }
+
+ my $where = join(' AND ', @criteria);
+ my $flag_ids = $dbh->selectcol_arrayref("SELECT id
+ FROM tracking_flags_visibility
+ WHERE $where");
+
+ return Bugzilla::Extension::TrackingFlags::Flag::Visibility->new_from_list($flag_ids);
+}
+
+###############################
+#### Validators ####
+###############################
+
+sub _check_tracking_flag {
+ my ($invocant, $flag) = @_;
+ if (blessed $flag) {
+ return $flag->flag_id;
+ }
+ $flag = Bugzilla::Extension::TrackingFlags::Flag->new($flag)
+ || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag });
+ return $flag->flag_id;
+}
+
+sub _check_product {
+ my ($invocant, $product) = @_;
+ if (blessed $product) {
+ return $product->id;
+ }
+ $product = Bugzilla::Product->new($product)
+ || ThrowCodeError('tracking_flags_invalid_param', { name => 'product_id', value => $product });
+ return $product->id;
+}
+
+sub _check_component {
+ my ($invocant, $component) = @_;
+ return undef unless defined $component;
+ if (blessed $component) {
+ return $component->id;
+ }
+ $component = Bugzilla::Component->new($component)
+ || ThrowCodeError('tracking_flags_invalid_param', { name => 'component_id', value => $component });
+ return $component->id;
+}
+
+###############################
+#### Accessors ####
+###############################
+
+sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; }
+sub product_id { return $_[0]->{'product_id'}; }
+sub component_id { return $_[0]->{'component_id'}; }
+
+sub tracking_flag {
+ my ($self) = @_;
+ $self->{'tracking_flag'} ||= Bugzilla::Extension::TrackingFlags::Flag->new($self->tracking_flag_id);
+ return $self->{'tracking_flag'};
+}
+
+sub product {
+ my ($self) = @_;
+ $self->{'product'} ||= Bugzilla::Product->new($self->product_id);
+ return $self->{'product'};
+}
+
+sub component {
+ my ($self) = @_;
+ return undef unless $self->component_id;
+ $self->{'component'} ||= Bugzilla::Component->new($self->component_id);
+ return $self->{'component'};
+}
+
+1;
diff --git a/extensions/TrackingFlags/template/en/default/bug/tracking_flags.html.tmpl b/extensions/TrackingFlags/template/en/default/bug/tracking_flags.html.tmpl
new file mode 100644
index 000000000..4e2c97dfa
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/bug/tracking_flags.html.tmpl
@@ -0,0 +1,62 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% FOREACH flag = flag_list %]
+ [% SET bug_id = bug.defined ? bug.id : 0 %]
+ [% SET flag_bug_value = flag.bug_flag(bug_id).value %]
+ [% NEXT IF !new_bug && (!user.id && flag_bug_value == '---') %]
+ <tr id="row_[% flag.name FILTER html %]">
+ <td [% IF new_bug %]class="field_label"[% END %]>
+ <label for="[% flag.name FILTER html %]">
+ [% IF new_bug %]
+ <a
+ [% IF help_html.${flag.name}.defined %]
+ title="[% help_html.${flag.name} FILTER txt FILTER collapse FILTER html %]"
+ class="field_help_link"
+ [% END %]
+ href="page.cgi?id=fields.html#[% flag.name FILTER uri %]">
+ [% END %]
+ [% flag.description FILTER html %]
+ [% IF new_bug %]
+ </a>
+ [% END %]:</label>
+ </td>
+ <td>
+ [% IF user.id %]
+ <input type="hidden" id="[% flag.name FILTER html %]_dirty">
+ <select id="[% flag.name FILTER html %]"
+ name="[% flag.name FILTER html %]"
+ onchange="tracking_flag_change(this)">
+ [% FOREACH value = flag.values %]
+ [% IF new_bug || value.name != flag_bug_value %]
+ [% NEXT IF !value.is_active || !flag.can_set_value(value.name) %]
+ [% END %]
+ <option value="[% value.name FILTER html %]"
+ id="v[% value.id FILTER html %]_[% flag.name FILTER html %]"
+ [% " selected" IF !new_bug && flag_bug_value == value.name %]>
+ [% value.name FILTER html %]</option>
+ [% END %]
+ </select>
+ <script type="text/javascript">
+ initHidingOptionsForIE('[% flag.name FILTER js %]');
+ </script>
+ [% IF !new_bug && user.id %]
+ <span id="ro_[% flag.name FILTER html %]" class="bz_default_hidden">
+ [% flag_bug_value FILTER html %]
+ </span>
+ [% END %]
+ [% ELSE %]
+ [% flag_bug_value FILTER html %]
+ [% END %]
+ </td>
+ </tr>
+[% END %]
+
+<script type="text/javascript">
+ TrackingFlags = [% tracking_flags_json FILTER none %];
+</script>
diff --git a/extensions/TrackingFlags/template/en/default/hook/admin/admin-end_links_right.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/admin/admin-end_links_right.html.tmpl
new file mode 100644
index 000000000..4808da069
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/admin/admin-end_links_right.html.tmpl
@@ -0,0 +1,18 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF user.in_group('admin') %]
+ <dt id="push">
+ <a href="page.cgi?id=tracking_flags_admin_list.html">Release Tracking Flags</a>
+ </dt>
+ <dd>
+ Tracking flags are special multi-value fields used to aid tracking releases
+ of Firefox, Firefox OS, Thunderbird, and other projects.
+ </dd>
+[% END %]
+
diff --git a/extensions/TrackingFlags/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl
new file mode 100644
index 000000000..71ef63c11
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl
@@ -0,0 +1,23 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF san_tag == "tracking_flags_repair" %]
+ <a href="sanitycheck.cgi?tracking_flags_repair=1&amp;token=
+ [%- issue_hash_token(['sanitycheck']) FILTER uri %]"
+ >Repair invalid product_id values in the tracking_flags_visibility table</a>
+
+[% ELSIF san_tag == "tracking_flags_check" %]
+ Checking tracking_flags_visibility table for bad values of product_id.
+
+[% ELSIF san_tag == "tracking_flags_alert" %]
+ Bad values for product_id found in the tracking_flags_visibility table.
+
+[% ELSIF san_tag == "tracking_flags_repairing" %]
+ OK, now fixing bad product_id values in the tracking_flags_visibility table.
+
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-bug_flags.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-bug_flags.html.tmpl
new file mode 100644
index 000000000..b41e1619f
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-bug_flags.html.tmpl
@@ -0,0 +1,29 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN IF NOT tracking_flags.size %]
+<td>
+ <table class="tracking_flags">
+ [% FOREACH type = tracking_flag_types %]
+ [% flag_list = [] %]
+ [% FOREACH flag = tracking_flags %]
+ [% flag_list.push(flag) IF flag.flag_type == type.name %]
+ [% END %]
+ [% IF flag_list.size %]
+ <tr>
+ <th style="text-align:right">
+ [% type.description FILTER html %]:
+ </th>
+ </tr>
+ [% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = flag_list
+ new_bug = 1 %]
+ [% END %]
+ [% END %]
+ </table>
+</td>
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-form.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-form.html.tmpl
new file mode 100644
index 000000000..59fe1d0ec
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-form.html.tmpl
@@ -0,0 +1,63 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF tracking_flags.size %]
+ [% tracking_flag_names = [] %]
+ [% FOREACH flag = tracking_flags %]
+ [% tracking_flag_names.push(flag.name) %]
+ [% END %]
+
+ <script type="text/javascript">
+ [% js_filtered_names = [] %]
+ [% FOREACH flag = tracking_flag_names %]
+ [% js_filtered = flag FILTER js %]
+ [% js_filtered_names.push(js_filtered) %]
+ [% END %]
+ var tracking_flag_names = ['[% js_filtered_names.join("','") FILTER none %]'];
+ var tracking_flags = new Array([% product.components.size %]);
+
+ [% count = 0 %]
+ [% FOREACH c = product.components %]
+ [% NEXT IF NOT c.is_active %]
+ [% tracking_flag_list = [] %]
+ [% FOREACH flag = tracking_flags %]
+ [% FOREACH v = flag.visibility %]
+ [% IF v.product_id == product.id
+ && (!v.component_id.defined || v.component_id == c.id) %]
+ [% tracking_flag_list.push(flag.name) %]
+ [% END %]
+ [% END %]
+ [% END %]
+ [% js_filtered_flags = [] %]
+ [% FOREACH flag = tracking_flag_list %]
+ [% js_filtered = flag FILTER js %]
+ [% js_filtered_flags.push(js_filtered) %]
+ [% END %]
+ tracking_flags[[% count %]] = ['[% js_filtered_flags.join("','") FILTER none %]'];
+ [% count = count + 1 %]
+ [% END %]
+
+ function update_tracking_flags () {
+ var component = document.getElementById('component');
+ // First, we disable all flags.
+ for (var i = 0; i < tracking_flag_names.length; i++) {
+ var flagField = document.getElementById(tracking_flag_names[i]);
+ flagField.disabled = true;
+ }
+ // Now enable flags available for the selected component.
+ var index = component.selectedIndex;
+ for (var i = 0; i < tracking_flags[index].length; i++) {
+ var flagField = document.getElementById(tracking_flags[index][i]);
+ flagField.disabled = false;
+ }
+ }
+
+ YAHOO.util.Event.onDOMReady(update_tracking_flags);
+ YAHOO.util.Event.addListener("component", "change", update_tracking_flags);
+ </script>
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-bug_flags_end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-bug_flags_end.html.tmpl
new file mode 100644
index 000000000..2a90cbfe3
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-bug_flags_end.html.tmpl
@@ -0,0 +1,33 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN IF NOT tracking_flags.size %]
+
+[% FOREACH type = tracking_flag_types %]
+ [% NEXT IF type.name == 'tracking' || type.name == 'project' %]
+ [% flag_list = [] %]
+ [% FOREACH flag = tracking_flags %]
+ [% flag_list.push(flag) IF flag.flag_type == type.name %]
+ [% END %]
+ [% IF flag_list.size %]
+ <tr>
+ <td>
+ <table class="tracking_flags">
+ <tr>
+ <th>
+ [% type.description FILTER html %]:
+ </th>
+ </tr>
+ [% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = flag_list
+ new_bug = 1 %]
+ </table>
+ </td>
+ </tr>
+ [% END %]
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-project_flags_end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-project_flags_end.html.tmpl
new file mode 100644
index 000000000..662bc26ee
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-project_flags_end.html.tmpl
@@ -0,0 +1,18 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN IF NOT tracking_flags.size %]
+
+[% flag_list = [] %]
+[% FOREACH flag = tracking_flags %]
+ [% NEXT IF flag.flag_type != 'project' %]
+ [% flag_list.push(flag) %]
+[% END %]
+[% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = flag_list
+ new_bug = 1 %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-tracking_flags_end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-tracking_flags_end.html.tmpl
new file mode 100644
index 000000000..69827a87a
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-tracking_flags_end.html.tmpl
@@ -0,0 +1,18 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN IF NOT tracking_flags.size %]
+
+[% flag_list = [] %]
+[% FOREACH flag = tracking_flags %]
+ [% NEXT IF flag.flag_type != 'tracking' %]
+ [% flag_list.push(flag) %]
+[% END %]
+[% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = flag_list
+ new_bug = 1 %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
new file mode 100644
index 000000000..b66bd3df4
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
@@ -0,0 +1,46 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS tracking_flags.size %]
+
+[% FOREACH type = tracking_flag_types %]
+ [% flag_list = [] %]
+ [% FOREACH flag = tracking_flags %]
+ [% flag_list.push(flag) IF flag.flag_type == type.name %]
+ [% END %]
+ [% IF flag_list.size %]
+ <tr>
+ <td class="field_label">
+ <label>[% type.description FILTER html %]:</label>
+ </td>
+ <td>
+ [% IF bug.check_can_change_field('flagtypes.name', 0, 1) %]
+ [% IF user.id && type.collapsed %]
+ <span id="edit_[% type.name FILTER html %]_flags_action">
+ (<a href="#" name="[% type.name FILTER html %]" class="edit_tracking_flags_link">edit</a>)
+ </span>
+ [% END %]
+ <table class="tracking_flags">
+ [% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = flag_list %]
+ </table>
+ [% ELSE %]
+ [% FOREACH flag = flag_list %]
+ [% NEXT IF flag.status == '---' %]
+ [% flag.description FILTER html %]: [% flag.bug_flag.value FILTER html %]<br>
+ [% END %]
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+[% END %]
+
+<script type="text/javascript">
+ TrackingFlags = [% tracking_flags_json FILTER none %];
+ hide_tracking_flags();
+</script>
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/field-editable.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/field-editable.html.tmpl
new file mode 100644
index 000000000..f598609e8
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/field-editable.html.tmpl
@@ -0,0 +1,38 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<input type="hidden" id="[% field.name FILTER html %]_dirty">
+<select id="[% field.name FILTER html %]"
+ name="[% field.name FILTER html %]">
+ [% IF allow_dont_change %]
+ <option value="[% dontchange FILTER html %]"
+ [% ' selected="selected"' IF value == dontchange %]>
+ [% dontchange FILTER html %]
+ </option>
+ [% END %]
+ [% FOREACH legal_value = field.values %]
+ [% IF legal_value.name != value %]
+ [% NEXT IF !field.can_set_value(legal_value.name) %]
+ [% NEXT IF !legal_value.is_active %]
+ [% END %]
+ <option value="[% legal_value.name FILTER html %]"
+ id="v[% legal_value.id FILTER html %] [%- field.name FILTER html %]"
+ [% IF legal_value.name == value %]
+ selected="selected"
+ [% END %]>
+ [%- display_value(field.name, legal_value.name) FILTER html ~%]
+ </option>
+ [% END %]
+</select>
+<script type="text/javascript">
+<!--
+ initHidingOptionsForIE('[% field.name FILTER js %]');
+ [%+ INCLUDE "bug/field-events.js.tmpl"
+ field = field, product = bug.product_obj %]
+//-->
+</script>
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/field-non_editable.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/field-non_editable.html.tmpl
new file mode 100644
index 000000000..8fa1f1623
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/field-non_editable.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% display_value(field.name, value) FILTER html %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..5e4ef2fcb
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,10 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% javascript_urls.push('extensions/TrackingFlags/web/js/tracking_flags.js') %]
+[% style_urls.push('extensions/TrackingFlags/web/styles/edit_bug.css') %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/global/code-error-errors.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/global/code-error-errors.html.tmpl
new file mode 100644
index 000000000..d656aac92
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/global/code-error-errors.html.tmpl
@@ -0,0 +1,27 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "tracking_flags_invalid_product" %]
+ [% title = "Invalid Product" %]
+ The product named '[% product FILTER html %]' does not exist.
+
+[% ELSIF error == "tracking_flags_invalid_component" %]
+ [% title = "Invalid Component" %]
+ The component named '[% component_name FILTER html %]' does not exist in the
+ product '[% product FILTER html %]'.
+
+[% ELSIF error == "tracking_flags_invalid_item_id" %]
+ [% title = "Invalid " _ item _ " ID" %]
+ Invalid [% item FILTER html %] ID ([% id FILTER html %]).
+
+[% ELSIF error == "tracking_flags_invalid_param" %]
+ [% title = "Invalid Parameter Provided" %]
+ An invalid parameter '[% value FILTER html %]'
+ for '[% name FILTER html %]' was provided.
+
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/global/header-start.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/global/header-start.html.tmpl
new file mode 100644
index 000000000..2bf1c75c3
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/global/header-start.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF template.name == "bug/create/create.html.tmpl" && tracking_flags.size %]
+ [% javascript_urls.push('extensions/TrackingFlags/web/js/tracking_flags.js') %]
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/global/messages-messages.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/global/messages-messages.html.tmpl
new file mode 100644
index 000000000..ce254b8cc
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/global/messages-messages.html.tmpl
@@ -0,0 +1,18 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF message_tag == 'tracking_flag_created' %]
+ The tracking flag '[% flag.name FILTER html %]' has been created.
+
+[% ELSIF message_tag == 'tracking_flag_updated' %]
+ The tracking flag '[% flag.name FILTER html %]' has been updated.
+
+[% ELSIF message_tag == "tracking_flag_deleted" %]
+ The tracking flag '[% flag.name FILTER html %]' has been deleted.
+
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..8c067a5d1
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,58 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "tracking_flags_change_denied" %]
+ [% title = "Tracking Flag Modification Denied" %]
+ You tried to update the status of the tracking flag '[% flag.name FILTER html %]'
+ [% IF value %] to '[% value FILTER html %]'[% END %].
+ Only a user with the required permissions may make this change.
+
+[% ELSIF error == "tracking_flags_missing_mandatory" %]
+ [% IF fields.size == 1 %]
+ [% title = "Missing mandatory field" %]
+ The field "[% fields.first FILTER html %]" is mandatory, and must be provided.
+ [% ELSE %]
+ [% title = "Missing mandatory fields" %]
+ The following fields are mandatory, and must be provided:
+ [%+ fields.join(', ') FILTER html %]
+ [% END %]
+
+[% ELSIF error == "tracking_flags_cf_prefix" %]
+ [% title = "Invalid flag name" %]
+ The flag name must start with 'cf_'.
+
+[% ELSIF error == "tracking_flags_missing_values" %]
+ [% title = "Missing values" %]
+ You must provide at least one value.
+
+[% ELSIF error == "tracking_flags_missing_value" %]
+ [% title = "Missing value" %]
+ You must provied the value for all values.
+
+[% ELSIF error == "tracking_flags_duplicate_value" %]
+ [% title = "Duplicate value" %]
+ The value "[% value FILTER html %]" has been provided more than once.
+
+[% ELSIF error == "tracking_flags_missing_visibility" %]
+ [% title = "Missing visibility" %]
+ You must provide at least one product for visibility.
+
+[% ELSIF error == "tracking_flags_duplicate_visibility" %]
+ [% title = "Duplicate visibility" %]
+ The visibility '[% name FILTER html %]' has been provided more than once.
+
+[% ELSIF error == "tracking_flags_invalid_flag_type" %]
+ [% title = "Invalid flag type" %]
+ The flag type '[% type FILTER html %]' is invalid.
+
+[% ELSIF error == "tracking_flag_has_contents" %]
+ [% title = "Tracking Flag Has Contents" %]
+ The tracking flag '[% flag.name FILTER html %]' cannot be deleted because
+ at least one [% terms.bug %] has a non empty value for this field.
+
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_edit.html.tmpl b/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_edit.html.tmpl
new file mode 100644
index 000000000..60406490f
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_edit.html.tmpl
@@ -0,0 +1,197 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% js_data = BLOCK %]
+var useclassification = false;
+var first_load = true;
+var last_sel = [];
+var cpts = new Array();
+[% n = 1 %]
+[% FOREACH p = user.get_selectable_products %]
+ cpts['[% n FILTER js %]'] = [
+ [%- FOREACH c = p.components %]'[% c.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
+ [% n = n+1 %]
+[% END %]
+var selected_components = [
+ [%- FOREACH c = input.component %]'[% c FILTER js %]'
+ [%- ',' UNLESS loop.last %] [%- END ~%] ];
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Release Tracking Flags"
+ javascript = js_data
+ javascript_urls = [ 'extensions/TrackingFlags/web/js/admin.js', 'js/productform.js' ]
+ style_urls = [ 'extensions/TrackingFlags/web/styles/admin.css' ]
+%]
+
+<script>
+ var groups = [% groups || '[]' FILTER none %];
+ var flag_values = [% values || '[]' FILTER none %];
+ var flag_visibility = [% visibility || '[]' FILTER none %];
+</script>
+
+<div id="edit_mode">
+ [% IF mode == 'edit' %]
+ Editing <b>[% flag.name FILTER html %]</b>.
+ [% ELSE %]
+ New flag
+ [% END %]
+</div>
+
+<form method="POST" action="page.cgi" onsubmit="return on_submit()">
+<input type="hidden" name="id" value="tracking_flags_admin_edit.html">
+<input type="hidden" name="mode" value="[% mode FILTER html %]">
+<input type="hidden" name="flag_id" value="[% flag ? flag.flag_id : 0 FILTER html %]">
+<input type="hidden" name="values" id="values" value="">
+<input type="hidden" name="visibility" id="visibility" value="">
+<input type="hidden" name="save" value="1">
+
+[%# name/desc/etc %]
+
+<table class="edit" cellspacing="0">
+
+<tr class="header">
+ <th colspan="3">Flag</th>
+</tr>
+
+<tr>
+ <th>Name</th>
+ <td><input name="flag_name" id="flag_name" value="[% flag.name FILTER html %]"></td>
+ <td class="help">database field name</td>
+</tr>
+
+<tr>
+ <th>Description</th>
+ <td><input name="flag_desc" id="flag_desc" value="[% flag.description FILTER html %]"></td>
+ <td class="help">visible name</td>
+</tr>
+
+<tr>
+ <th>Type</th>
+ <td>
+ <select name="flag_type" id="flag_type">
+ <option value=""></option>
+ [% FOREACH type = tracking_flag_types %]
+ <option value="[% type.name FILTER html %]"
+ [% " selected" IF flag.flag_type == type.name %]>
+ [% type.name FILTER html %]</option>
+ [% END %]
+ </select>
+ </td>
+ <td class="help">flag type used for grouping</td>
+</tr>
+
+<tr>
+ <th>Sort Key</th>
+ <td>
+ <input name="flag_sort" id="flag_sort" value="[% flag.sortkey FILTER html %]">
+ [
+ <a class="txt_icon" href="#" onclick="inc_field('flag_sort', 5);return false">+5</a>
+ | <a class="txt_icon" href="#" onclick="inc_field('flag_sort', -5);return false">-5</a>
+ ]
+ </td>
+</tr>
+
+<tr>
+ <th>Enter [% terms.Bug %]</th>
+ <td><input type="checkbox" name="flag_enter_bug" id="flag_enter_bug" value="1" [% "checked" IF flag.enter_bug %]></td>
+ <td class="help">can be set on [% terms.bug %] creation</td>
+</tr>
+
+<tr>
+ <th>Active</th>
+ <td><input type="checkbox" name="flag_active" id="flag_active" value="1" [% "checked" IF flag.is_active %]></td>
+</tr>
+
+[% IF mode == 'edit' %]
+ <tr>
+ <th>[% terms.Bug %] Count</th>
+ <td>[% flag.bug_count FILTER html %]</td>
+ </tr>
+[% END %]
+
+</table>
+
+[%# values %]
+
+<table id="flag_values" class="edit" cellspacing="0">
+
+<tr class="header">
+ <th colspan="4">Values</th>
+</tr>
+
+<tr>
+ <th>Value</th>
+ <th>Setter</th>
+ <th>Active</th>
+</tr>
+
+<tr>
+ <td colspan="4">
+ [ <a href="#" onclick="add_value();return false">New Value</a> ]
+ </td>
+</tr>
+
+</table>
+
+[%# visibility %]
+
+<table id="flag_visibility" class="edit" cellspacing="0">
+
+<tr class="header">
+ <th colspan="3">Visibility</th>
+</tr>
+
+<tr>
+ <th>Product</th>
+ <th>Component</th>
+</tr>
+
+<tr id="flag_visibility_add">
+ <td>
+ <select id="product" onChange="selectProduct(Dom.get('product'), Dom.get('component'), null, null, '-- Any --')">
+ <option value=""></option>
+ [% FOREACH p = user.get_selectable_products %]
+ <option value="[% p.name FILTER html %]"
+ [% " selected" IF input.product == p.name %]>
+ [% p.name FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ </td>
+ <td>
+ <select id="component">
+ </select>
+ </td>
+ <td>
+ [ <a href="#" onclick="add_visibility();return false">Add</a> ]
+ <td>
+</tr>
+
+</table>
+
+
+[%# submit %]
+
+<div>
+ <input type="submit" name="submit" id="submit" value="[% mode == 'edit' ? 'Save Changes' : 'Add' %]">
+ [% IF mode == "edit" && !flag.bug_count %]
+ <input type="hidden" name="delete" id="delete" value="">
+ <input type="submit" value="Delete Flag [% IF flag.activity_count %] and Activity[% END %]"
+ onclick="return delete_confirm('[% flag.name FILTER js FILTER html %]')">
+ [% END %]
+</div>
+
+</form>
+
+<hr>
+<p>
+Return to the <a href="page.cgi?id=tracking_flags_admin_list.html">list of Tracking Flags</a>.
+</p>
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_list.html.tmpl b/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_list.html.tmpl
new file mode 100644
index 000000000..5ea68dd98
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_list.html.tmpl
@@ -0,0 +1,73 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Release Tracking Flags"
+ style_urls = [ 'extensions/TrackingFlags/web/styles/admin.css' ]
+ javascript_urls = [ 'extensions/TrackingFlags/web/js/admin.js' ]
+%]
+
+<table id="flag_list" class="list" cellspacing="0">
+
+<tr>
+ <th>Name</th>
+ <th>Description</th>
+ <th>Type</th>
+ <th>Sort Key</th>
+ <th>Active</th>
+ [% IF show_bug_counts %]
+ <th>[% terms.Bugs %]</th>
+ [% END %]
+ <th>&nbsp;</th>
+</tr>
+
+[% FOREACH flag = flags %]
+ <tr class="flag_row
+ [% loop.count % 2 == 1 ? " odd_row" : " even_row" %]
+ [% " is_disabled" UNLESS flag.is_active %]">
+ <td [% 'class="disabled"' UNLESS flag.is_active %]>
+ <a href="page.cgi?id=tracking_flags_admin_edit.html&amp;mode=edit&amp;flag_id=[% flag.flag_id FILTER uri %]">
+ [% flag.name FILTER html %]
+ </a>
+ </td>
+ <td [% 'class="disabled"' UNLESS flag.is_active %]>
+ [% flag.description FILTER html %]
+ </td>
+ <td [% 'class="disabled"' UNLESS flag.is_active %]>
+ [% flag.flag_type FILTER html %]
+ </td>
+ <td [% 'class="disabled"' UNLESS flag.is_active %]>
+ [% flag.sortkey FILTER html %]
+ </td>
+ <td>
+ [% flag.is_active ? "Yes" : "No" %]
+ </td>
+ [% IF show_bug_counts %]
+ <td>
+ [% flag.bug_count FILTER html %]
+ </td>
+ [% END %]
+ <td>
+ <a href="page.cgi?id=tracking_flags_admin_edit.html&amp;mode=copy&amp;copy_from=[% flag.flag_id FILTER uri %]">Copy</a>
+ </td>
+ </tr>
+[% END %]
+
+</table>
+
+<div id="new_flag">
+ <a href="page.cgi?id=tracking_flags_admin_edit.html">Add Flag</a> |
+ [% IF !show_bug_counts %]
+ <a href="page.cgi?id=tracking_flags_admin_list.html&amp;show_bug_counts=1">
+ Show [% terms.bug %] counts (slower)</a> |
+ [% END %]
+ <input type="checkbox" onclick="filter_flag_list(this.checked)" id="filter">
+ <label for="filter">Show disabled flags</label>
+</div>
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/TrackingFlags/web/js/admin.js b/extensions/TrackingFlags/web/js/admin.js
new file mode 100644
index 000000000..58bdd294f
--- /dev/null
+++ b/extensions/TrackingFlags/web/js/admin.js
@@ -0,0 +1,440 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+// init
+
+var Dom = YAHOO.util.Dom;
+var Event = YAHOO.util.Event;
+
+Event.onDOMReady(function() {
+ try {
+ if (Dom.get('flag_list')) {
+ filter_flag_list(Dom.get('filter').checked);
+ }
+ else {
+ if (!JSON)
+ JSON = YAHOO.lang.JSON;
+ Event.addListener('flag_name', 'change', change_flag_name, Dom.get('flag_name'));
+ Event.addListener('flag_desc', 'change', change_string_value, Dom.get('flag_desc'));
+ Event.addListener('flag_type', 'change', change_select_value, Dom.get('flag_type'));
+ Event.addListener('flag_sort', 'change', change_int_value, Dom.get('flag_sort'));
+
+ Event.addListener('product', 'change', function() {
+ if (Dom.get('product').value == '')
+ Dom.get('component').options.length = 0;
+ });
+
+ update_flag_values();
+ update_flag_visibility();
+ tag_missing_values();
+ }
+ } catch(e) {
+ console.error(e);
+ }
+});
+
+// field
+
+function change_flag_name(e, o) {
+ change_string_value(e, o);
+ if (o.value == '')
+ return;
+ o.value = o.value.replace(/[^a-z0-9_]/g, '_');
+ if (!o.value.match(/^cf_/))
+ o.value = 'cf_' + o.value;
+ if (Dom.get('flag_desc').value == '') {
+ var desc = o.value;
+ desc = desc.replace(/^cf_/, '');
+ desc = desc.replace(/_/g, '-');
+ Dom.get('flag_desc').value = desc;
+ tag_missing_value(Dom.get('flag_desc'));
+ }
+}
+
+function inc_field(id, amount) {
+ var el = Dom.get(id);
+ el.value = el.value.match(/-?\d+/) * 1 + amount;
+ change_int_value(null, el);
+}
+
+// values
+
+function update_flag_values() {
+ // update the values table from the flag_values global
+
+ var tbl = Dom.get('flag_values');
+ if (!tbl)
+ return;
+
+ // remove current entries
+ while (tbl.rows.length > 3) {
+ tbl.deleteRow(2);
+ }
+
+ // add all entries
+
+ for (var i = 0, l = flag_values.length; i < l; i++) {
+ var value = flag_values[i];
+
+ var row = tbl.insertRow(2 + (i * 2));
+ var cell;
+
+ // value
+ cell = row.insertCell(0);
+ if (value.value == '---') {
+ cell.innerHTML = '---';
+ }
+ else {
+ var inputEl = document.createElement('input');
+ inputEl.id = 'value_' + i;
+ inputEl.type = 'text';
+ inputEl.className = 'option_value';
+ inputEl.value = value.value;
+ Event.addListener(inputEl, 'change', change_string_value, inputEl);
+ Event.addListener(inputEl, 'change', function(e, o) {
+ flag_values[o.id.match(/\d+$/)].value = o.value;
+ tag_invalid_values();
+ }, inputEl);
+ Event.addListener(inputEl, 'keyup', function(e, o) {
+ if ((e.key || e.keyCode) == 27 && o.value == '')
+ remove_value(o.id.match(/\d+$/));
+ }, inputEl);
+ cell.appendChild(inputEl);
+ }
+
+ // setter
+ cell = row.insertCell(1);
+ var selectEl = document.createElement('select');
+ selectEl.id = 'setter_' + i;
+ Event.addListener(selectEl, 'change', change_select_value, selectEl);
+ var optionEl = document.createElement('option');
+ optionEl.value = '';
+ selectEl.appendChild(optionEl);
+ for (var j = 0, m = groups.length; j < m; j++) {
+ var group = groups[j];
+ optionEl = document.createElement('option');
+ optionEl.value = group.id;
+ optionEl.innerHTML = YAHOO.lang.escapeHTML(group.name);
+ optionEl.selected = group.id == value.setter_group_id;
+ selectEl.appendChild(optionEl);
+ }
+ Event.addListener(selectEl, 'change', function(e, o) {
+ flag_values[o.id.match(/\d+$/)].setter_group_id = o.value;
+ tag_invalid_values();
+ }, selectEl);
+ cell.appendChild(selectEl);
+
+ // active
+ cell = row.insertCell(2);
+ if (value.value == '---') {
+ cell.innerHTML = 'Yes';
+ }
+ else {
+ var inputEl = document.createElement('input');
+ inputEl.type = 'checkbox';
+ inputEl.id = 'is_active_' + i;
+ inputEl.checked = value.is_active;
+ Event.addListener(inputEl, 'change', function(e, o) {
+ flag_values[o.id.match(/\d+$/)].is_active = o.checked;
+ }, inputEl);
+ cell.appendChild(inputEl);
+ }
+
+ // actions
+ cell = row.insertCell(3);
+ var html =
+ '[' +
+ (i == 0
+ ? '<span class="txt_icon">&nbsp;-&nbsp;</span>'
+ : '<a class="txt_icon" href="#" onclick="value_move_up(' + i + ');return false"> &Delta; </a>'
+ ) +
+ '|' +
+ (i == l - 1
+ ? '<span class="txt_icon">&nbsp;-&nbsp;</span>'
+ : '<a class="txt_icon" href="#" onclick="value_move_down(' + i + ');return false"> &nabla; </a>'
+ );
+ if (value.value != '---') {
+ var lbl = value.comment == '' ? 'Set Comment' : 'Edit Comment';
+ html +=
+ '|<a href="#" onclick="remove_value(' + i + ');return false">Remove</a>' +
+ '|<a href="#" onclick="toggle_value_comment(this, ' + i + ');return false">' + lbl + '</a>'
+
+ }
+ html += ' ]';
+ cell.innerHTML = html;
+
+ row = tbl.insertRow(3 + (i * 2));
+ row.className = 'bz_default_hidden';
+ row.id = 'comment_row_' + i;
+ cell = row.insertCell(0);
+ cell = row.insertCell(1);
+ cell.colSpan = 3;
+ var ta = document.createElement('textarea');
+ ta.className = 'value_comment';
+ ta.id = 'value_comment_' + i;
+ ta.rows = 5;
+ ta.value = value.comment;
+ cell.appendChild(ta);
+ Event.addListener(ta, 'blur', function(e, idx) {
+ flag_values[idx].comment = e.target.value;
+ }, i);
+ }
+
+ tag_invalid_values();
+}
+
+function tag_invalid_values() {
+ // reset
+ for (var i = 0, l = flag_values.length; i < l; i++) {
+ Dom.removeClass('value_' + i, 'admin_error');
+ }
+
+ for (var i = 0, l = flag_values.length; i < l; i++) {
+ // missing
+ if (flag_values[i].value == '')
+ Dom.addClass('value_' + i, 'admin_error');
+ if (!flag_values[i].setter_group_id)
+ Dom.addClass('setter_' + i, 'admin_error');
+
+ // duplicate values
+ for (var j = i; j < l; j++) {
+ if (i != j && flag_values[i].value == flag_values[j].value) {
+ Dom.addClass('value_' + i, 'admin_error');
+ Dom.addClass('value_' + j, 'admin_error');
+ }
+ }
+ }
+}
+
+function value_move_up(idx) {
+ if (idx == 0)
+ return;
+ var tmp = flag_values[idx];
+ flag_values[idx] = flag_values[idx - 1];
+ flag_values[idx - 1] = tmp;
+ update_flag_values();
+}
+
+function value_move_down(idx) {
+ if (idx == flag_values.length - 1)
+ return;
+ var tmp = flag_values[idx];
+ flag_values[idx] = flag_values[idx + 1];
+ flag_values[idx + 1] = tmp;
+ update_flag_values();
+}
+
+function add_value() {
+ var value = new Object();
+ value.id = 0;
+ value.value = '';
+ value.setter_group_id = '';
+ value.is_active = true;
+ var idx = flag_values.length;
+ flag_values[idx] = value;
+ update_flag_values();
+ Dom.get('value_' + idx).focus();
+}
+
+function remove_value(idx) {
+ flag_values.splice(idx, 1);
+ update_flag_values();
+}
+
+function update_value(e, o) {
+ var i = o.value.match(/\d+/);
+ flag_values[i].value = o.value;
+}
+
+function toggle_value_comment(btn, idx) {
+ var row = Dom.get('comment_row_' + idx);
+ if (Dom.hasClass(row, 'bz_default_hidden')) {
+ Dom.removeClass(row, 'bz_default_hidden');
+ btn.innerHTML = 'Hide Comment';
+ Dom.get('value_comment_' + idx).select();
+ Dom.get('value_comment_' + idx).focus();
+ } else {
+ Dom.addClass(row, 'bz_default_hidden');
+ btn.innerHTML = flag_values[idx].comment == '' ? 'Set Comment' : 'Edit Comment';
+ }
+}
+
+// visibility
+
+function update_flag_visibility() {
+ // update the visibility table from the flag_visibility global
+
+ var tbl = Dom.get('flag_visibility');
+ if (!tbl)
+ return;
+
+ // remove current entries
+ while (tbl.rows.length > 3) {
+ tbl.deleteRow(2);
+ }
+
+ // show something if there aren't any components
+
+ if (!flag_visibility.length) {
+ var row = tbl.insertRow(2);
+ var cell = row.insertCell(0);
+ cell.innerHTML = '<i class="admin_error_text">missing</i>';
+ }
+
+ // add all entries
+
+ for (var i = 0, l = flag_visibility.length; i < l; i++) {
+ var visibility = flag_visibility[i];
+
+ var row = tbl.insertRow(2 + i);
+ var cell;
+
+ // product
+ cell = row.insertCell(0);
+ cell.innerHTML = visibility.product;
+
+ // component
+ cell = row.insertCell(1);
+ cell.innerHTML = visibility.component
+ ? visibility.component
+ : '<i>-- Any --</i>';
+
+ // actions
+ cell = row.insertCell(2);
+ cell.innerHTML = '[ <a href="#" onclick="remove_visibility(' + i + ');return false">Remove</a> ]';
+ }
+}
+
+function add_visibility() {
+ // validation
+ var product = Dom.get('product').value;
+ var component = Dom.get('component').value;
+ if (!product) {
+ alert('Please select a product.');
+ return;
+ }
+
+ // don't allow duplicates
+ for (var i = 0, l = flag_visibility.length; i < l; i++) {
+ if (flag_visibility[i].product == product && flag_visibility[i].component == component) {
+ Dom.get('product').value = '';
+ Dom.get('component').options.length = 0;
+ return;
+ }
+ }
+
+ if (component == '') {
+ // if we're adding an "any" component, remove non-any components
+ for (var i = 0; i < flag_visibility.length; i++) {
+ var visibility = flag_visibility[i];
+ if (visibility.product == product) {
+ flag_visibility.splice(i, 1);
+ i--;
+ }
+ }
+ }
+ else {
+ // don't add non-any components if an "any" component exists
+ for (var i = 0, l = flag_visibility.length; i < l; i++) {
+ var visibility = flag_visibility[i];
+ if (visibility.product == product && !visibility.component)
+ return;
+ }
+ }
+
+ // add to model
+ var visibility = new Object();
+ visibility.id = 0;
+ visibility.product = product;
+ visibility.component = component;
+ flag_visibility[flag_visibility.length] = visibility;
+
+ // update ui
+ update_flag_visibility();
+ Dom.get('product').value = '';
+ Dom.get('component').options.length = 0;
+}
+
+function remove_visibility(idx) {
+ flag_visibility.splice(idx, 1);
+ update_flag_visibility();
+}
+
+// validation and submission
+
+function tag_missing_values() {
+ var els = document.getElementsByTagName('input');
+ for (var i = 0, l = els.length; i < l; i++) {
+ var el = els[i];
+ if (el.id.match(/^(flag|value)_/))
+ tag_missing_value(el);
+ }
+ tag_missing_value(Dom.get('flag_type'));
+}
+
+function tag_missing_value(el) {
+ el.value == ''
+ ? Dom.addClass(el, 'admin_error')
+ : Dom.removeClass(el, 'admin_error');
+}
+
+function delete_confirm(flag) {
+ if (confirm('Are you sure you want to delete the flag ' + flag + ' ?')) {
+ Dom.get('delete').value = 1;
+ return true;
+ }
+ else {
+ return false;
+ }
+}
+
+function on_submit() {
+ if (Dom.get('delete') && Dom.get('delete').value)
+ return;
+ // let perl manage most validation errors, because they are clearly marked
+ // the exception is an empty visibility list, so catch that here as well
+ if (!flag_visibility.length) {
+ alert('You must provide at least one product for visibility.');
+ return false;
+ }
+
+ Dom.get('values').value = JSON.stringify(flag_values);
+ Dom.get('visibility').value = JSON.stringify(flag_visibility);
+ return true;
+}
+
+// flag list
+
+function filter_flag_list(show_disabled) {
+ var rows = Dom.getElementsByClassName('flag_row', 'tr', 'flag_list');
+ for (var i = 0, l = rows.length; i < l; i++) {
+ if (Dom.hasClass(rows[i], 'is_disabled')) {
+ if (show_disabled) {
+ Dom.removeClass(rows[i], 'bz_default_hidden');
+ }
+ else {
+ Dom.addClass(rows[i], 'bz_default_hidden');
+ }
+ }
+ }
+}
+
+// utils
+
+function change_string_value(e, o) {
+ o.value = YAHOO.lang.trim(o.value);
+ tag_missing_value(o);
+}
+
+function change_int_value(e, o) {
+ o.value = o.value.match(/-?\d+/);
+ tag_missing_value(o);
+}
+
+function change_select_value(e, o) {
+ tag_missing_value(o);
+}
diff --git a/extensions/TrackingFlags/web/js/tracking_flags.js b/extensions/TrackingFlags/web/js/tracking_flags.js
new file mode 100644
index 000000000..041ae43f5
--- /dev/null
+++ b/extensions/TrackingFlags/web/js/tracking_flags.js
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+var Dom = YAHOO.util.Dom;
+
+function hide_tracking_flags() {
+ for (var i = 0, l = TrackingFlags.types.length; i < l; i++) {
+ var flag_type = TrackingFlags.types[i];
+ for (var field in TrackingFlags.flags[flag_type]) {
+ var el = Dom.get(field);
+ var value = el ? el.value : TrackingFlags.flags[flag_type][field];
+ if (el && (value != TrackingFlags.flags[flag_type][field])) {
+ show_tracking_flags(flag_type);
+ return;
+ }
+ if (value == '---') {
+ Dom.addClass('row_' + field, 'bz_default_hidden');
+ } else {
+ Dom.addClass(field, 'bz_default_hidden');
+ Dom.removeClass('ro_' + field, 'bz_default_hidden');
+ }
+ }
+ }
+}
+
+function show_tracking_flags(flag_type) {
+ Dom.addClass('edit_' + flag_type + '_flags_action', 'bz_default_hidden');
+ for (var field in TrackingFlags.flags[flag_type]) {
+ if (Dom.get(field).value == '---') {
+ Dom.removeClass('row_' + field, 'bz_default_hidden');
+ } else {
+ Dom.removeClass(field, 'bz_default_hidden');
+ Dom.addClass('ro_' + field, 'bz_default_hidden');
+ }
+ }
+}
+
+function tracking_flag_change(e) {
+ var value = e.value;
+ var prefill;
+ if (TrackingFlags.comments[e.name])
+ prefill = TrackingFlags.comments[e.name][e.value];
+ if (!prefill) {
+ var cr = document.getElementById('cr_' + e.id);
+ if (cr)
+ cr.parentElement.removeChild(cr);
+ return;
+ }
+ if (!document.getElementById('cr_' + e.id)) {
+ // create "comment required"
+ var span = document.createElement('span');
+ span.id = 'cr_' + e.id;
+ span.appendChild(document.createTextNode('('));
+ var a = document.createElement('a');
+ a.appendChild(document.createTextNode('comment required'));
+ a.href = '#';
+ a.onclick = function() {
+ var c = document.getElementById('comment');
+ c.focus();
+ c.select();
+ document.getElementById('add_comment').scrollIntoView();
+ return false;
+ };
+ span.appendChild(a);
+ span.appendChild(document.createTextNode(')'));
+ e.parentNode.appendChild(span);
+ }
+ // prefill comment
+ var commentEl = document.getElementById('comment');
+ if (!commentEl)
+ return;
+ var value = commentEl.value;
+ if (value == prefill)
+ return;
+ if (value == '') {
+ commentEl.value = prefill;
+ } else {
+ commentEl.value = prefill + "\n\n" + value;
+ }
+}
+
+YAHOO.util.Event.onDOMReady(function() {
+ var edit_tracking_links = Dom.getElementsByClassName('edit_tracking_flags_link');
+ for (var i = 0, l = edit_tracking_links.length; i < l; i++) {
+ YAHOO.util.Event.addListener(edit_tracking_links[i], 'click', function(e) {
+ e.preventDefault();
+ show_tracking_flags(this.name);
+ });
+ }
+});
diff --git a/extensions/TrackingFlags/web/styles/admin.css b/extensions/TrackingFlags/web/styles/admin.css
new file mode 100644
index 000000000..51c6ab966
--- /dev/null
+++ b/extensions/TrackingFlags/web/styles/admin.css
@@ -0,0 +1,111 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+/* list */
+
+.list {
+ border: 1px solid #888888;
+}
+
+.list td, .list th {
+ padding: 3px 10px 3px 3px;
+ border: 1px solid #888888;
+}
+
+.list .odd_row {
+ background-color: #ffffff;
+ color: #000000;
+}
+
+.list .even_row {
+ background-color: #eeeeee;
+ color: #000000;
+}
+
+.list tr:hover {
+ background-color: #ccddee;
+}
+
+
+.list th {
+ text-align: left;
+ background: #dddddd;
+}
+
+.list .disabled {
+ color: #888888;
+ text-decoration: line-through;
+}
+
+#new_flag {
+ margin: 1em 0em;
+}
+
+/* edit */
+
+.edit {
+ margin-bottom: 2em;
+}
+
+.edit .header {
+ background: #dddddd;
+}
+
+.edit .help {
+ font-style: italic;
+}
+
+.edit td, .edit th {
+ padding: 1px 5px;
+}
+
+.edit th {
+ text-align: left;
+}
+
+#edit_mode {
+ margin: 1em 0em;
+}
+
+#flag_name {
+ width: 20em;
+}
+
+#flag_desc {
+ width: 20em;
+}
+
+#flag_sort {
+ width: 10em;
+}
+
+.option_value {
+ width: 10em;
+}
+
+.value_comment {
+ width: 100%;
+}
+
+.hidden {
+ display: none;
+}
+
+.txt_icon {
+ font-family: monospace;
+}
+
+.admin_error {
+ border: 1px solid red;
+ box-shadow: 0px 0px 4px #ff0000;
+ -webkit-box-shadow: 0px 0px 4px #ff0000;
+ -moz-box-shadow: 0px 0px 4px #ff0000;
+}
+
+.admin_error_text {
+ color: #cc0000;
+}
diff --git a/extensions/TrackingFlags/web/styles/edit_bug.css b/extensions/TrackingFlags/web/styles/edit_bug.css
new file mode 100644
index 000000000..132a6a1ca
--- /dev/null
+++ b/extensions/TrackingFlags/web/styles/edit_bug.css
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+.tracking_flags {
+ width: auto !important;
+}
+
+.tracking_flags .field_label {
+ font-weight: normal !important;
+}
+
+#Create .tracking_flags th {
+ text-align: left;
+}
diff --git a/extensions/TryAutoLand/Config.pm b/extensions/TryAutoLand/Config.pm
new file mode 100644
index 000000000..8b299183b
--- /dev/null
+++ b/extensions/TryAutoLand/Config.pm
@@ -0,0 +1,19 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TryAutoLand;
+use strict;
+
+use constant NAME => 'TryAutoLand';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/TryAutoLand/Extension.pm b/extensions/TryAutoLand/Extension.pm
new file mode 100644
index 000000000..40dbb70d9
--- /dev/null
+++ b/extensions/TryAutoLand/Extension.pm
@@ -0,0 +1,323 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TryAutoLand;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Bug;
+use Bugzilla::Attachment;
+use Bugzilla::User;
+use Bugzilla::Util qw(trick_taint diff_arrays);
+use Bugzilla::Error;
+
+use Bugzilla::Extension::TryAutoLand::Constants;
+
+our $VERSION = '0.01';
+
+BEGIN {
+ *Bugzilla::Bug::autoland_branches = \&_autoland_branches;
+ *Bugzilla::Bug::autoland_try_syntax = \&_autoland_try_syntax;
+ *Bugzilla::Attachment::autoland_checked = \&_autoland_attachment_checked;
+ *Bugzilla::Attachment::autoland_who = \&_autoland_attachment_who;
+ *Bugzilla::Attachment::autoland_status = \&_autoland_attachment_status;
+ *Bugzilla::Attachment::autoland_status_when = \&_autoland_attachment_status_when;
+ *Bugzilla::Attachment::autoland_update_status = \&_autoland_attachment_update_status;
+ *Bugzilla::Attachment::autoland_remove = \&_autoland_attachment_remove;
+}
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'autoland_branches'} = {
+ FIELDS => [
+ bug_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ REFERENCES => {
+ TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE'
+ }
+ },
+ branches => {
+ TYPE => 'VARCHAR(255)',
+ NOTNULL => 1
+ },
+ try_syntax => {
+ TYPE => 'VARCHAR(255)',
+ NOTNULL => 1,
+ DEFAULT => "''",
+ }
+ ],
+ };
+
+ $args->{'schema'}->{'autoland_attachments'} = {
+ FIELDS => [
+ attach_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ REFERENCES => {
+ TABLE => 'attachments',
+ COLUMN => 'attach_id',
+ DELETE => 'CASCADE'
+ },
+ },
+ who => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ },
+ },
+ status => {
+ TYPE => 'varchar(64)',
+ NOTNULL => 1
+ },
+ status_when => {
+ TYPE => 'DATETIME',
+ NOTNULL => 1,
+ },
+ ],
+ };
+}
+
+sub install_update_db {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ if (!$dbh->bz_column_info('autoland_branches', 'try_syntax')) {
+ $dbh->bz_add_column('autoland_branches', 'try_syntax', {
+ TYPE => 'VARCHAR(255)',
+ NOTNULL => 1,
+ DEFAULT => "''",
+ });
+ }
+}
+
+sub _autoland_branches {
+ my $self = shift;
+ return $self->{'autoland_branches'} if exists $self->{'autoland_branches'};
+ _preload_bug_data($self);
+ return $self->{'autoland_branches'};
+}
+
+sub _autoland_try_syntax {
+ my $self = shift;
+ return $self->{'autoland_try_syntax'} if exists $self->{'autoland_try_syntax'};
+ _preload_bug_data($self);
+ return $self->{'autoland_try_syntax'};
+}
+
+sub _preload_bug_data {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $result = $dbh->selectrow_hashref("SELECT branches, try_syntax FROM autoland_branches
+ WHERE bug_id = ?", { Slice => {} }, $self->id);
+ if ($result) {
+ $self->{'autoland_branches'} = $result->{'branches'};
+ $self->{'autoland_try_syntax'} = $result->{'try_syntax'};
+ }
+ else {
+ $self->{'autoland_branches'} = undef;
+ $self->{'autoland_try_syntax'} = undef;
+ }
+}
+
+sub _autoland_attachment_checked {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+ return $self->{'autoland_checked'} if exists $self->{'autoland_checked'};
+ my $result = $dbh->selectrow_hashref("SELECT who, status, status_when
+ FROM autoland_attachments
+ WHERE attach_id = ?", { Slice => {} }, $self->id);
+ if ($result) {
+ $self->{'autoland_checked'} = 1;
+ $self->{'autoland_who'} = Bugzilla::User->new($result->{'who'});
+ $self->{'autoland_status'} = $result->{'status'};
+ $self->{'autoland_status_when'} = $result->{'status_when'};
+ }
+ else {
+ $self->{'autoland_checked'} = 0;
+ $self->{'autoland_who'} = undef;
+ $self->{'autoland_status'} = undef;
+ $self->{'autoland_status_when'} = undef;
+ }
+ return $self->{'autoland_checked'};
+}
+
+sub _autoland_attachment_who {
+ my $self = shift;
+ return undef if !$self->autoland_checked;
+ return $self->{'autoland_who'};
+}
+
+sub _autoland_attachment_status {
+ my $self = shift;
+ return undef if !$self->autoland_checked;
+ return $self->{'autoland_status'};
+}
+
+sub _autoland_attachment_status_when {
+ my $self = shift;
+ return undef if !$self->autoland_checked;
+ return $self->{'autoland_status_when'};
+}
+
+sub _autoland_attachment_update_status {
+ my ($self, $status) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ return undef if !$self->autoland_checked;
+
+ grep($_ eq $status, VALID_STATUSES)
+ || ThrowUserError('autoland_invalid_status',
+ { status => $status,
+ valid => [ VALID_STATUSES ] });
+
+ if ($self->autoland_status ne $status) {
+ my $timestamp = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)");
+ trick_taint($status);
+ $dbh->do("UPDATE autoland_attachments SET status = ?, status_when = ?
+ WHERE attach_id = ?", undef, $status, $timestamp, $self->id);
+ $self->{'autoland_status'} = $status;
+ $self->{'autoland_status_when'} = $timestamp;
+ }
+
+ return 1;
+}
+
+sub _autoland_attachment_remove {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ return undef if !$self->autoland_checked;
+ $dbh->do("DELETE FROM autoland_attachments WHERE attach_id = ?", undef, $self->id);
+ delete $self->{'autoland_checked'};
+ delete $self->{'autoland_who'};
+ delete $self->{'autoland_status'};
+ delete $self->{'autoland_status_when'};
+}
+
+sub object_end_of_update {
+ my ($self, $args) = @_;
+ my $object = $args->{'object'};
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+ my $cgi = Bugzilla->cgi;
+ my $params = Bugzilla->input_params;
+
+ return if !$user->in_group('autoland');
+
+ if ($object->isa('Bugzilla::Bug')) {
+ # First make any needed changes to the branches and try_syntax fields
+ my $bug_id = $object->bug_id;
+ my $bug_result = $dbh->selectrow_hashref("SELECT branches, try_syntax
+ FROM autoland_branches
+ WHERE bug_id = ?",
+ { Slice => {} }, $bug_id);
+
+ my $old_branches = '';
+ my $old_try_syntax = '';
+ if ($bug_result) {
+ $old_branches = $bug_result->{'branches'};
+ $old_try_syntax = $bug_result->{'try_syntax'};
+ }
+
+ my $new_branches = $params->{'autoland_branches'} || '';
+ my $new_try_syntax = $params->{'autoland_try_syntax'} || '';
+
+ my $set_attachments = [];
+ if (ref $params->{'autoland_attachments'}) {
+ $set_attachments = $params->{'autoland_attachments'};
+ } elsif ($params->{'autoland_attachments'}) {
+ $set_attachments = [ $params->{'autoland_attachments'} ];
+ }
+
+ # Check for required values
+ (!$new_branches && @{$set_attachments})
+ && ThrowUserError('autoland_empty_branches');
+ ($new_branches && !$new_try_syntax)
+ && ThrowUserError('autoland_empty_try_syntax');
+
+ trick_taint($new_branches);
+ if (!$new_branches && $old_branches) {
+ $dbh->do("DELETE FROM autoland_branches WHERE bug_id = ?",
+ undef, $bug_id);
+ }
+ elsif ($new_branches && !$old_branches) {
+ $dbh->do("INSERT INTO autoland_branches (bug_id, branches)
+ VALUES (?, ?)", undef, $bug_id, $new_branches);
+ }
+ elsif ($old_branches ne $new_branches) {
+ $dbh->do("UPDATE autoland_branches SET branches = ? WHERE bug_id = ?",
+ undef, $new_branches, $bug_id);
+ }
+
+ trick_taint($new_try_syntax);
+ if (($old_try_syntax ne $new_try_syntax) && $new_branches) {
+ $dbh->do("UPDATE autoland_branches SET try_syntax = ? WHERE bug_id = ?",
+ undef, $new_try_syntax, $bug_id);
+ }
+
+ # Next make any changes needed to each of the attachments.
+ # 1. If an attachment is checked it has a row in the table, if
+ # there is no row in the table it is not checked.
+ # 2. Do not allow changes to checked state if status == 'running' or status == 'waiting'
+ my $check_attachments = ref $params->{'defined_autoland_attachments'}
+ ? $params->{'defined_autoland_attachments'}
+ : [ $params->{'defined_autoland_attachments'} ];
+ my ($removed_attachments) = diff_arrays($check_attachments, $set_attachments);
+ foreach my $attachment (@{$object->attachments}) {
+ next if !$attachment->ispatch;
+ my $attach_id = $attachment->id;
+
+ my $checked = (grep $_ == $attach_id, @$set_attachments) ? 1 : 0;
+ my $unchecked = (grep $_ == $attach_id, @$removed_attachments) ? 1 : 0;
+ my $old_checked = $dbh->selectrow_array("SELECT 1 FROM autoland_attachments
+ WHERE attach_id = ?", undef, $attach_id) || 0;
+
+ next if $checked && $old_checked;
+
+ if ($unchecked && $old_checked && $attachment->autoland_status =~ /^(failed|success)$/) {
+ $dbh->do("DELETE FROM autoland_attachments WHERE attach_id = ?", undef, $attach_id);
+ }
+ elsif ($checked && !$old_checked) {
+ $dbh->do("INSERT INTO autoland_attachments (attach_id, who, status, status_when)
+ VALUES (?, ?, 'waiting', now())", undef, $attach_id, $user->id);
+ }
+ }
+
+ }
+}
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my $file = $args->{'file'};
+ my $vars = $args->{'vars'};
+
+ # in the header we just need to set the var to ensure the css gets included
+ if ($file eq 'bug/show-header.html.tmpl' && Bugzilla->user->in_group('autoland') ) {
+ $vars->{'autoland'} = 1;
+ }
+
+ if ($file eq 'bug/edit.html.tmpl') {
+ $vars->{'autoland_default_try_syntax'} = DEFAULT_TRY_SYNTAX;
+ }
+}
+
+sub webservice {
+ my ($self, $args) = @_;
+
+ my $dispatch = $args->{dispatch};
+ $dispatch->{TryAutoLand} = "Bugzilla::Extension::TryAutoLand::WebService";
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/TryAutoLand/bin/TryAutoLand.getBugs.pl b/extensions/TryAutoLand/bin/TryAutoLand.getBugs.pl
new file mode 100755
index 000000000..5d05831a8
--- /dev/null
+++ b/extensions/TryAutoLand/bin/TryAutoLand.getBugs.pl
@@ -0,0 +1,60 @@
+#!/usr/bin/perl -w
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use XMLRPC::Lite;
+use Data::Dumper;
+use HTTP::Cookies;
+
+###################################
+# Need to login first #
+###################################
+
+my $username = shift;
+my $password = shift;
+
+my $cookie_jar = new HTTP::Cookies( file => "/tmp/lwp_cookies.dat" );
+
+my $rpc = new XMLRPC::Lite;
+
+$rpc->proxy('http://fedora/726193/xmlrpc.cgi');
+
+$rpc->encoding('UTF-8');
+
+$rpc->transport->cookie_jar($cookie_jar);
+
+my $call = $rpc->call( 'User.login',
+ { login => $username, password => $password } );
+
+if ( $call->faultstring ) {
+ print $call->faultstring . "\n";
+ exit;
+}
+
+# Save the cookies in the cookie file
+$rpc->transport->cookie_jar->extract_cookies(
+ $rpc->transport->http_response );
+$rpc->transport->cookie_jar->save;
+
+print "Successfully logged in.\n";
+
+###################################
+# Main call here #
+###################################
+
+$call = $rpc->call('TryAutoLand.getBugs', { status => [] });
+
+my $result = "";
+if ( $call->faultstring ) {
+ print $call->faultstring . "\n";
+ exit;
+}
+else {
+ $result = $call->result;
+}
+
+print Dumper($result);
diff --git a/extensions/TryAutoLand/bin/TryAutoLand.updateStatus.pl b/extensions/TryAutoLand/bin/TryAutoLand.updateStatus.pl
new file mode 100755
index 000000000..4a8f92089
--- /dev/null
+++ b/extensions/TryAutoLand/bin/TryAutoLand.updateStatus.pl
@@ -0,0 +1,65 @@
+#!/usr/bin/perl -w
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use XMLRPC::Lite;
+use Data::Dumper;
+use HTTP::Cookies;
+
+###################################
+# Need to login first #
+###################################
+
+my $username = shift;
+my $password = shift;
+
+my $cookie_jar = new HTTP::Cookies( file => "/tmp/lwp_cookies.dat" );
+
+my $rpc = new XMLRPC::Lite;
+
+$rpc->proxy('http://fedora/726193/xmlrpc.cgi');
+
+$rpc->encoding('UTF-8');
+
+$rpc->transport->cookie_jar($cookie_jar);
+
+my $call = $rpc->call( 'User.login',
+ { login => $username, password => $password } );
+
+if ( $call->faultstring ) {
+ print $call->faultstring . "\n";
+ exit;
+}
+
+# Save the cookies in the cookie file
+$rpc->transport->cookie_jar->extract_cookies(
+ $rpc->transport->http_response );
+$rpc->transport->cookie_jar->save;
+
+print "Successfully logged in.\n";
+
+###################################
+# Main call here #
+###################################
+
+my $attach_id = shift;
+my $action = shift;
+my $status = shift;
+
+$call = $rpc->call('TryAutoLand.update',
+ { attach_id => $attach_id, action => $action, status => $status });
+
+my $result = "";
+if ( $call->faultstring ) {
+ print $call->faultstring . "\n";
+ exit;
+}
+else {
+ $result = $call->result;
+}
+
+print Dumper($result);
diff --git a/extensions/TryAutoLand/bin/TryAutoLand.updateStatus_json.pl b/extensions/TryAutoLand/bin/TryAutoLand.updateStatus_json.pl
new file mode 100755
index 000000000..f39b55229
--- /dev/null
+++ b/extensions/TryAutoLand/bin/TryAutoLand.updateStatus_json.pl
@@ -0,0 +1,65 @@
+#!/usr/bin/perl -w
+
+use JSON::RPC::Client;
+use Data::Dumper;
+use HTTP::Cookies;
+
+###################################
+# Need to login first #
+###################################
+
+my $username = shift;
+my $password = shift;
+
+my $cookie_jar = HTTP::Cookies->new( file => "/tmp/lwp_cookies.dat" );
+
+my $rpc = new JSON::RPC::Client;
+
+$rpc->ua->ssl_opts(verify_hostname => 0);
+
+my $uri = "http://fedora/726193/jsonrpc.cgi";
+
+#$rpc->ua->cookie_jar($cookie_jar);
+
+#my $result = $rpc->call($uri, { method => 'User.login', params =>
+# { login => $username, password => $password } });
+
+#if ($result) {
+# if ($result->is_error) {
+# print "Error : ", $result->error_message;
+# exit;
+# }
+# else {
+# print "Successfully logged in.\n";
+# }
+#}
+#else {
+# print $rpc->status_line;
+#}
+
+###################################
+# Main call here #
+###################################
+
+my $attach_id = shift;
+my $action = shift;
+my $status = shift;
+
+$result = $rpc->call($uri, { method => 'TryAutoLand.update',
+ params => { attach_id => $attach_id,
+ action => $action,
+ status => $status,
+ Bugzilla_login => $username,
+ Bugzilla_password => $password } });
+
+if ($result) {
+ if ($result->is_error) {
+ print "Error : ", $result->error_message;
+ exit;
+ }
+}
+else {
+ print $rpc->status_line;
+}
+
+print Dumper($result->result);
diff --git a/extensions/TryAutoLand/lib/Constants.pm b/extensions/TryAutoLand/lib/Constants.pm
new file mode 100644
index 000000000..53bad630a
--- /dev/null
+++ b/extensions/TryAutoLand/lib/Constants.pm
@@ -0,0 +1,31 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TryAutoLand::Constants;
+
+use strict;
+
+use base qw(Exporter);
+
+our @EXPORT = qw(
+ VALID_STATUSES
+ WEBSERVICE_USER
+ DEFAULT_TRY_SYNTAX
+);
+
+use constant VALID_STATUSES => qw(
+ waiting
+ running
+ failed
+ success
+);
+
+use constant WEBSERVICE_USER => 'autoland-try@mozilla.bugs';
+
+use constant DEFAULT_TRY_SYNTAX => '-b do -p all -u none -t none';
+
+1;
diff --git a/extensions/TryAutoLand/lib/WebService.pm b/extensions/TryAutoLand/lib/WebService.pm
new file mode 100644
index 000000000..1088386dd
--- /dev/null
+++ b/extensions/TryAutoLand/lib/WebService.pm
@@ -0,0 +1,189 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TryAutoLand::WebService;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::WebService);
+
+use Bugzilla::Error;
+use Bugzilla::Util qw(trick_taint);
+
+use Bugzilla::Extension::TryAutoLand::Constants;
+
+use constant READ_ONLY => qw(
+ getBugs
+);
+
+# TryAutoLand.getBugs
+# Params: status - List of statuses to filter attachments (only 'waiting' is default)
+# Returns: List of bugs, each being a hash of data needed by the AutoLand polling server
+# Params
+# [ { bug_id => $bug_id1, attachments => [ $attach_id1, $attach_id2 ] }, branches => $branchListFromTextField ... ]
+
+sub getBugs {
+ my ($self, $params) = @_;
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+ my %bugs;
+
+ if ($user->login ne WEBSERVICE_USER) {
+ ThrowUserError("auth_failure", { action => "access",
+ object => "autoland_attachments" });
+ }
+
+ my $status_where = "AND status = 'waiting'";
+ my $status_values = [];
+ if (exists $params->{'status'}) {
+ my $statuses = ref $params->{'status'}
+ ? $params->{'status'}
+ : [ $params->{'status'} ];
+ foreach my $status (@$statuses) {
+ if (grep($_ eq $status, VALID_STATUSES)) {
+ trick_taint($status);
+ push(@$status_values, $status);
+ }
+ }
+ if (@$status_values) {
+ my @qmarks = ("?") x @$status_values;
+ $status_where = "AND " . $dbh->sql_in('status', \@qmarks);
+ }
+
+ }
+
+ my $attachments = $dbh->selectall_arrayref("
+ SELECT attachments.bug_id,
+ attachments.attach_id,
+ autoland_attachments.who,
+ autoland_attachments.status,
+ autoland_attachments.status_when
+ FROM attachments, autoland_attachments
+ WHERE attachments.attach_id = autoland_attachments.attach_id
+ $status_where
+ ORDER BY attachments.bug_id",
+ undef, @$status_values);
+
+ foreach my $row (@$attachments) {
+ my ($bug_id, $attach_id, $al_who, $al_status, $al_status_when) = @$row;
+
+ my $al_user = Bugzilla::User->new($al_who);
+
+ # Silent Permission checks
+ next if !$user->can_see_bug($bug_id);
+ my $attachment = Bugzilla::Attachment->new($attach_id);
+ next if !$attachment
+ || $attachment->isobsolete
+ || ($attachment->isprivate && !$user->is_insider);
+
+ $bugs{$bug_id} = {} if !exists $bugs{$bug_id};
+
+ if (!$bugs{$bug_id}{'branches'}) {
+ my $bug_result = $dbh->selectrow_hashref("SELECT branches, try_syntax
+ FROM autoland_branches
+ WHERE bug_id = ?",
+ undef, $bug_id);
+ $bugs{$bug_id}{'branches'} = $bug_result->{'branches'};
+ $bugs{$bug_id}{'try_syntax'} = $bug_result->{'try_syntax'};
+ }
+
+ $bugs{$bug_id}{'attachments'} = [] if !exists $bugs{$bug_id}{'attachments'};
+
+ push(@{$bugs{$bug_id}{'attachments'}}, {
+ id => $self->type('int', $attach_id),
+ who => $self->type('string', $al_user->login),
+ status => $self->type('string', $al_status),
+ status_when => $self->type('dateTime', $al_status_when),
+ });
+ }
+
+ return [
+ map
+ { { bug_id => $_, attachments => $bugs{$_}{'attachments'},
+ branches => $bugs{$_}{'branches'}, try_syntax => $bugs{$_}{'try_syntax'} } }
+ keys %bugs
+ ];
+}
+
+# TryAutoLand.update({ attach_id => $attach_id, action => $action, status => $status })
+# Let's BMO know if a patch has landed or not and BMO will update the auto_land table accordingly
+# If $action eq 'status', $status will be a predetermined set of status values -- when waiting,
+# the UI for submitting autoland will be locked and once complete status update occurs or the
+# mapping is removed, the UI can be unlocked for the $attach_id
+# Allowed statuses: waiting, running, failed, or success
+#
+# If $action eq 'remove', the attach_id will be removed from the mapping table and the UI
+# will be unlocked for the $attach_id.
+
+sub update {
+ my ($self, $params) = @_;
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+
+ if ($user->login ne WEBSERVICE_USER) {
+ ThrowUserError("auth_failure", { action => "modify",
+ object => "autoland_attachments" });
+ }
+
+ foreach my $param ('attach_id', 'action') {
+ defined $params->{$param}
+ || ThrowCodeError('param_required',
+ { param => $param });
+ }
+
+ my $action = delete $params->{'action'};
+ my $attach_id = delete $params->{'attach_id'};
+ my $status = delete $params->{'status'};
+
+ if ($action eq 'status' && !$status) {
+ ThrowCodeError('param_required', { param => 'status' });
+ }
+
+ grep($_ eq $action, ('remove', 'status'))
+ || ThrowUserError('autoland_update_invalid_action',
+ { action => $action,
+ valid => ["remove", "status"] });
+
+ my $attachment = Bugzilla::Attachment->new($attach_id);
+ $attachment
+ || ThrowUserError('autoland_invalid_attach_id',
+ { attach_id => $attach_id });
+
+ # Loud Permission checks
+ if (!$user->can_see_bug($attachment->bug_id)) {
+ ThrowUserError("bug_access_denied", { bug_id => $attachment->bug_id });
+ }
+ if ($attachment->isprivate && !$user->is_insider) {
+ ThrowUserError('auth_failure', { action => 'access',
+ object => 'attachment',
+ attach_id => $attachment->id });
+ }
+
+ $attachment->autoland_checked
+ || ThrowUserError('autoland_invalid_attach_id',
+ { attach_id => $attach_id });
+
+ if ($action eq 'status') {
+ # Update the status
+ $attachment->autoland_update_status($status);
+
+ return {
+ id => $self->type('int', $attachment->id),
+ who => $self->type('string', $attachment->autoland_who->login),
+ status => $self->type('string', $attachment->autoland_status),
+ status_when => $self->type('dateTime', $attachment->autoland_status_when),
+ };
+ }
+ elsif ($action eq 'remove') {
+ $attachment->autoland_remove();
+ }
+
+ return {};
+}
+
+1;
diff --git a/extensions/TryAutoLand/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl b/extensions/TryAutoLand/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
new file mode 100644
index 000000000..ed6224afe
--- /dev/null
+++ b/extensions/TryAutoLand/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
@@ -0,0 +1,101 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF user.in_group('autoland') %]
+ [% autoland_attachments = [] %]
+ [% autoland_waiting = 0 %]
+ [% autoland_running = 0 %]
+ [% autoland_finished = 0 %]
+ [% FOREACH attachment = bug.attachments %]
+ [% NEXT IF attachment.isprivate && !user.is_insider && attachment.attacher.id != user.id %]
+ [% NEXT IF attachment.isobsolete %]
+ [% NEXT IF !attachment.ispatch %]
+ [% autoland_attachments.push(attachment) %]
+ [% IF attachment.autoland_checked %]
+ [% IF attachment.autoland_status == 'waiting' %]
+ [% autoland_waiting = autoland_waiting + 1 %]
+ [% END %]
+ [% IF attachment.autoland_status == 'running' %]
+ [% autoland_running = autoland_running + 1 %]
+ [% END %]
+ [% IF attachment.autoland_status == 'success' || attachment.autoland_status == 'failed' %]
+ [% autoland_finished = autoland_finished + 1 %]
+ [% END %]
+ [% END %]
+ [% END %]
+ [% IF autoland_attachments.size %]
+ <tr>
+ <th class="field_label field_land_autoland">
+ <a title="[% help_html.autoland FILTER txt FILTER collapse FILTER html %]"
+ class="field_help_link" href="https://wiki.mozilla.org/Build:Autoland">
+ AutoLand:</a>
+ </th>
+ <td>
+ <span id="autoland_edit_container">
+ (<a href="#" id="autoland_edit_action">edit</a>)
+ Total: [% autoland_attachments.size FILTER html %] -
+ <span class="autoland_waiting">Waiting:</span> [% autoland_waiting FILTER html %] -
+ <span class="autoland_running">Running:</span> [% autoland_running FILTER html %] -
+ <span class="autoland_success">Finished:</span> [% autoland_finished FILTER html %]
+ </span>
+ <div id="autoland_edit_input">
+ Branches (required):<br>
+ <input type="text" id="autoland_branches" name="autoland_branches"
+ value="[% bug.autoland_branches FILTER html %]" size="40"
+ class="text_input"><br>
+ Try Syntax (required): (Default: [% autoland_default_try_syntax FILTER html %])<br>
+ <input type="text" id="autoland_try_syntax" name="autoland_try_syntax"
+ value="[% bug.autoland_try_syntax || autoland_default_try_syntax FILTER html %]" size="40"
+ class="text_input"><br>
+ Patches:
+ <br>
+ <table id="autoland_edit_table">
+ [% FOREACH attachment = autoland_attachments %]
+ <tr>
+ <td>
+ [% IF attachment.autoland_checked %]
+ <input type="hidden" name="defined_autoland_attachments"
+ value="[% attachment.id FILTER html %]">
+ [% END %]
+ <input type="checkbox" name="autoland_attachments" value="[% attachment.id FILTER html %]"
+ [% ' checked="checked"' IF attachment.autoland_checked %]
+ [% IF attachment.autoland_status == 'running' || attachment.autoland_status == 'waiting' %]
+ disabled="disabled"
+ [% END %]>
+ </td>
+ <td>
+ <span title="[% attachment.description FILTER html %]">
+ [% attachment.filename FILTER html %]
+ </span>
+ <td>
+ [% IF attachment.autoland_checked %]
+ <span class="autoland_[% attachment.autoland_status FILTER html %]">
+ [% attachment.autoland_status FILTER html %]
+ </span>
+ [% END %]
+ </td>
+ <td>
+ [% IF attachment.autoland_checked %]
+ [% attachment.autoland_status_when FILTER time('%Y-%m-%d %H:%M') %]
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ </table>
+ </div>
+ <script type="text/javascript">
+ hideEditableField('autoland_edit_container',
+ 'autoland_edit_input',
+ 'autoland_edit_action',
+ '',
+ '');
+ </script>
+ </td>
+ </tr>
+ [% END %]
+[% END %]
diff --git a/extensions/TryAutoLand/template/en/default/hook/bug/field-help-end.none.tmpl b/extensions/TryAutoLand/template/en/default/hook/bug/field-help-end.none.tmpl
new file mode 100644
index 000000000..899db60c4
--- /dev/null
+++ b/extensions/TryAutoLand/template/en/default/hook/bug/field-help-end.none.tmpl
@@ -0,0 +1,15 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%
+ vars.help_html.autoland =
+ "TryAutoLand is a BMO extension that allows integration with the $terms.Bugzilla
+ AutoLanding system. Select patches on a $terms.bug will be picked up
+ automatically and landed on the try build server for specified branches.
+ Results of the try build will be sent back to the bug report as comments."
+%]
diff --git a/extensions/TryAutoLand/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/TryAutoLand/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..c61f478ea
--- /dev/null
+++ b/extensions/TryAutoLand/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF autoland %]
+ [% style_urls.push('extensions/TryAutoLand/web/style.css') %]
+[% END %]
diff --git a/extensions/TryAutoLand/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl b/extensions/TryAutoLand/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
new file mode 100644
index 000000000..50a1e48d5
--- /dev/null
+++ b/extensions/TryAutoLand/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF object == 'autoland_attachments' %]
+ AutoLand attachments
+[% END %]
diff --git a/extensions/TryAutoLand/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/TryAutoLand/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..c12950dcf
--- /dev/null
+++ b/extensions/TryAutoLand/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,33 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "autoland_invalid_status" %]
+ [% title = "AutoLand Invalid Status" %]
+ The status '[% status FILTER html %]' is not a valid
+ status for the AutoLand extension. Valid statuses
+ are [% valid.join(', ') FILTER html %].
+
+[% ELSIF error == "autoland_invalid_attach_id" %]
+ [% title = "AutoLand Invalid Attachment ID" %]
+ The attachment id '[% attach_id FILTER html %]' is not
+ a valid id for the AutoLand extension.
+
+[% ELSIF error == "autoland_empty_try_syntax" %]
+ [% title = "AutoLand Empty Try Syntax" %]
+ You cannot have a value for Branches and have an empty Try Syntax value.
+
+[% ELSIF error == "autoland_empty_branches" %]
+ [% title = "AutoLand Empty Branches" %]
+ You cannot check one or more patches for AutoLanding and have an empty
+ Branches value.
+
+[% ELSIF error == "autoland_update_invalid_action" %]
+ [% title = "AutoLand Update Invalid Action" %]
+ The action '[% action FILTER html %]' is not a valid action.
+ Valid actions are [% valid.join(', ') FILTER html %].
+[% END %]
diff --git a/extensions/TryAutoLand/web/style.css b/extensions/TryAutoLand/web/style.css
new file mode 100644
index 000000000..99409c0c0
--- /dev/null
+++ b/extensions/TryAutoLand/web/style.css
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+.autoland_waiting {
+ color: blue;
+}
+
+.autoland_running {
+ color: orange;
+}
+
+.autoland_failed {
+ color: red;
+}
+
+.autoland_success {
+ color: green;
+}
diff --git a/extensions/TypeSniffer/Config.pm b/extensions/TypeSniffer/Config.pm
new file mode 100644
index 000000000..6ad03b362
--- /dev/null
+++ b/extensions/TypeSniffer/Config.pm
@@ -0,0 +1,40 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the TypeSniffer Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Gervase Markham <gerv@mozilla.org>
+
+package Bugzilla::Extension::TypeSniffer;
+use strict;
+
+use constant NAME => 'TypeSniffer';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'File-MimeInfo',
+ module => 'File::MimeInfo::Magic',
+ version => '0'
+ },
+ {
+ package => 'IO-stringy',
+ module => 'IO::Scalar',
+ version => '0'
+ },
+];
+
+__PACKAGE__->NAME; \ No newline at end of file
diff --git a/extensions/TypeSniffer/Extension.pm b/extensions/TypeSniffer/Extension.pm
new file mode 100644
index 000000000..c593b76e8
--- /dev/null
+++ b/extensions/TypeSniffer/Extension.pm
@@ -0,0 +1,100 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the TypeSniffer Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Gervase Markham <gerv@mozilla.org>
+
+package Bugzilla::Extension::TypeSniffer;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use File::MimeInfo::Magic;
+use IO::Scalar;
+
+our $VERSION = '1';
+
+# These extensions override/supplement File::MimeInfo::Magic's detection.
+our %EXTENSION_OVERRIDES = (
+ '.lang' => 'text/plain',
+);
+
+################################################################################
+# This extension uses magic to guess MIME types for data where the browser has
+# told us it's application/octet-stream (probably because there's no file
+# extension, or it's a text type with a non-.txt file extension).
+################################################################################
+sub attachment_process_data {
+ my ($self, $args) = @_;
+ my $attributes = $args->{'attributes'};
+ my $params = Bugzilla->input_params;
+
+ # If we have autodetected application/octet-stream from the Content-Type
+ # header, let's have a better go using a sniffer.
+ if ($params->{'contenttypemethod'} &&
+ $params->{'contenttypemethod'} eq 'autodetect' &&
+ $attributes->{'mimetype'} eq 'application/octet-stream')
+ {
+ my $filename = $attributes->{'filename'} . '';
+
+ # Check for an override first
+ if ($filename =~ /^.+(\..+$)/) {
+ my $ext = lc($1);
+ if (exists $EXTENSION_OVERRIDES{$ext}) {
+ $attributes->{'mimetype'} = $EXTENSION_OVERRIDES{$ext};
+ return;
+ }
+ }
+
+ # Then try file extension detection
+ my $mimetype = mimetype($filename);
+ if ($mimetype) {
+ $attributes->{'mimetype'} = $mimetype;
+ return;
+ }
+
+ # data attribute can be either scalar data or filehandle
+ # bugzilla.org/docs/3.6/en/html/api/Bugzilla/Attachment.html#create
+ my $fh = $attributes->{'data'};
+ if (!ref($fh)) {
+ my $data = $attributes->{'data'};
+ $fh = new IO::Scalar \$data;
+ }
+ else {
+ # CGI.pm sends us an Fh that isn't actually an IO::Handle, but
+ # has a method for getting an actual handle out of it.
+ if (!$fh->isa('IO::Handle')) {
+ $fh = $fh->handle;
+ # ->handle returns an literal IO::Handle, even though the
+ # underlying object is a file. So we rebless it to be a proper
+ # IO::File object so that we can call ->seek on it and so on.
+ # Just in case CGI.pm fixes this some day, we check ->isa first.
+ if (!$fh->isa('IO::File')) {
+ bless $fh, 'IO::File';
+ }
+ }
+ }
+
+ $mimetype = mimetype($fh);
+ $fh->seek(0, 0);
+ if ($mimetype) {
+ $attributes->{'mimetype'} = $mimetype;
+ }
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/UserProfile/Config.pm b/extensions/UserProfile/Config.pm
new file mode 100644
index 000000000..99dca9e02
--- /dev/null
+++ b/extensions/UserProfile/Config.pm
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::UserProfile;
+use strict;
+
+use constant NAME => 'UserProfile';
+use constant REQUIRED_MODULES => [ ];
+use constant OPTIONAL_MODULES => [ ];
+
+__PACKAGE__->NAME;
diff --git a/extensions/UserProfile/Extension.pm b/extensions/UserProfile/Extension.pm
new file mode 100644
index 000000000..8671ba755
--- /dev/null
+++ b/extensions/UserProfile/Extension.pm
@@ -0,0 +1,554 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::UserProfile;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Constants;
+use Bugzilla::Extension::UserProfile::TimeAgo qw(time_ago);
+use Bugzilla::Extension::UserProfile::Util;
+use Bugzilla::Install::Filesystem;
+use Bugzilla::User;
+use Bugzilla::Util qw(datetime_from);
+use Email::Address;
+use Scalar::Util qw(blessed);
+
+our $VERSION = '1';
+
+#
+# user methods
+#
+
+BEGIN {
+ *Bugzilla::User::last_activity_ts = \&_user_last_activity_ts;
+ *Bugzilla::User::set_last_activity_ts = \&_user_set_last_activity_ts;
+ *Bugzilla::User::last_statistics_ts = \&_user_last_statistics_ts;
+ *Bugzilla::User::clear_last_statistics_ts = \&_user_clear_last_statistics_ts;
+ *Bugzilla::User::address = \&_user_address;
+}
+
+sub _user_last_activity_ts { $_[0]->{last_activity_ts} }
+sub _user_last_statistics_ts { $_[0]->{last_statistics_ts} }
+sub _user_address { Email::Address->new(undef, $_[0]->email) }
+
+sub _user_set_last_activity_ts {
+ my ($self, $value) = @_;
+ $self->set('last_activity_ts', $_[1]);
+
+ # we update the database directly to avoid audit_log entries
+ Bugzilla->dbh->do(
+ "UPDATE profiles SET last_activity_ts = ? WHERE userid = ?",
+ undef,
+ $value, $self->id);
+ Bugzilla->memcached->clear({ table => 'profiles', id => $self->id });
+}
+
+sub _user_clear_last_statistics_ts {
+ my ($self) = @_;
+ $self->set('last_statistics_ts', undef);
+
+ # we update the database directly to avoid audit_log entries
+ Bugzilla->dbh->do(
+ "UPDATE profiles SET last_statistics_ts = NULL WHERE userid = ?",
+ undef,
+ $self->id);
+ Bugzilla->memcached->clear({ table => 'profiles', id => $self->id });
+}
+
+#
+# hooks
+#
+
+sub bug_after_create {
+ my ($self, $args) = @_;
+ $self->_bug_touched($args);
+}
+
+sub bug_after_update {
+ my ($self, $args) = @_;
+ $self->_bug_touched($args);
+}
+
+sub _bug_touched {
+ my ($self, $args) = @_;
+ my $bug = $args->{bug};
+
+ my $user = Bugzilla->user;
+ my ($assigned_to, $qa_contact);
+
+ # bug update
+ if (exists $args->{changes}) {
+ return unless
+ scalar(keys %{ $args->{changes} })
+ || exists $args->{bug}->{added_comments};
+
+ # if the assignee or qa-contact is changed to someone other than the
+ # current user, update them
+ if (exists $args->{changes}->{assigned_to}
+ && $args->{changes}->{assigned_to}->[1] ne $user->login)
+ {
+ $assigned_to = $bug->assigned_to;
+ }
+ if (exists $args->{changes}->{qa_contact}
+ && ($args->{changes}->{qa_contact}->[1] || '') ne $user->login)
+ {
+ $qa_contact = $bug->qa_contact;
+ }
+
+ # if the product is changed, we need to recount everyone involved with
+ # this bug
+ if (exists $args->{changes}->{product}) {
+ tag_for_recount_from_bug($bug->id);
+ }
+
+ }
+ # new bug
+ else {
+ # if the assignee or qa-contact is created set to someone other than
+ # the current user, update them
+ if ($bug->assigned_to->id != $user->id) {
+ $assigned_to = $bug->assigned_to;
+ }
+ if ($bug->qa_contact && $bug->qa_contact->id != $user->id) {
+ $qa_contact = $bug->qa_contact;
+ }
+ }
+
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
+
+ # update user's last_activity_ts
+ eval {
+ $user->set_last_activity_ts($args->{timestamp});
+ $self->_recalc_remove($user);
+ };
+ if ($@) {
+ warn $@;
+ $self->_recalc_insert($user);
+ }
+
+ # clear the last_statistics_ts for assignee/qa-contact to force a recount
+ # at the next poll
+ if ($assigned_to) {
+ eval {
+ $assigned_to->clear_last_statistics_ts();
+ $self->_recalc_remove($assigned_to);
+ };
+ if ($@) {
+ warn $@;
+ $self->_recalc_insert($assigned_to);
+ }
+ }
+ if ($qa_contact) {
+ eval {
+ $qa_contact->clear_last_statistics_ts();
+ $self->_recalc_remove($qa_contact);
+ };
+ if ($@) {
+ warn $@;
+ $self->_recalc_insert($qa_contact);
+ }
+ }
+
+ $dbh->bz_commit_transaction();
+}
+
+sub _recalc_insert {
+ my ($self, $user) = @_;
+ Bugzilla->dbh->do(
+ "INSERT IGNORE INTO profiles_statistics_recalc SET user_id=?",
+ undef, $user->id
+ );
+}
+
+sub _recalc_remove {
+ my ($self, $user) = @_;
+ Bugzilla->dbh->do(
+ "DELETE FROM profiles_statistics_recalc WHERE user_id=?",
+ undef, $user->id
+ );
+}
+
+sub object_end_of_create {
+ my ($self, $args) = @_;
+ $self->_object_touched($args);
+}
+
+sub object_end_of_update {
+ my ($self, $args) = @_;
+ $self->_object_touched($args);
+}
+
+sub _object_touched {
+ my ($self, $args) = @_;
+ my $object = $args->{object}
+ or return;
+ return if exists $args->{changes} && !scalar(keys %{ $args->{changes} });
+
+ if ($object->isa('Bugzilla::Attachment')) {
+ # if an attachment is created or updated, that counts as user activity
+ my $user = Bugzilla->user;
+ my $timestamp = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ eval {
+ $user->set_last_activity_ts($timestamp);
+ $self->_recalc_remove($user);
+ };
+ if ($@) {
+ warn $@;
+ $self->_recalc_insert($user);
+ }
+ }
+ elsif ($object->isa('Bugzilla::Product') && exists $args->{changes}->{name}) {
+ # if a product is renamed by an admin, rename in the
+ # profiles_statistics_products table
+ Bugzilla->dbh->do(
+ "UPDATE profiles_statistics_products SET product=? where product=?",
+ undef,
+ $args->{changes}->{name}->[1], $args->{changes}->{name}->[0],
+ );
+ }
+}
+
+sub reorg_move_bugs {
+ my ($self, $args) = @_;
+ my $bug_ids = $args->{bug_ids};
+ printf "Touching user profile data for %s bugs.\n", scalar(@$bug_ids);
+ my $count = 0;
+ foreach my $bug_id (@$bug_ids) {
+ $count += tag_for_recount_from_bug($bug_id);
+ }
+ print "Updated $count users.\n";
+}
+
+sub merge_users_before {
+ my ($self, $args) = @_;
+ my ($old_id, $new_id) = @$args{qw(old_id new_id)};
+ # when users are merged, we have to delete all the statistics for both users
+ # we'll recalcuate the stats after the merge
+ print "deleting user profile statistics for $old_id and $new_id\n";
+ my $dbh = Bugzilla->dbh;
+ foreach my $table (qw( profiles_statistics profiles_statistics_status profiles_statistics_products )) {
+ $dbh->do("DELETE FROM $table WHERE " . $dbh->sql_in('user_id', [ $old_id, $new_id ]));
+ }
+}
+
+sub merge_users_after {
+ my ($self, $args) = @_;
+ my $new_id = $args->{new_id};
+ print "generating user profile statistics $new_id\n";
+ update_statistics_by_user($new_id);
+}
+
+sub webservice_user_get {
+ my ($self, $args) = @_;
+ my ($service, $users) = @$args{qw(webservice users)};
+
+ my $dbh = Bugzilla->dbh;
+ my $ids = [
+ map { blessed($_->{id}) ? $_->{id}->value : $_->{id} }
+ grep { exists $_->{id} }
+ @$users
+ ];
+ return unless @$ids;
+ my $timestamps = $dbh->selectall_hashref(
+ "SELECT userid,last_activity_ts FROM profiles WHERE " . $dbh->sql_in('userid', $ids),
+ 'userid',
+ );
+ foreach my $user (@$users) {
+ my $id = blessed($user->{id}) ? $user->{id}->value : $user->{id};
+ $user->{last_activity} = $service->type('dateTime', $timestamps->{$id}->{last_activity_ts});
+ }
+}
+
+sub template_before_create {
+ my ($self, $args) = @_;
+ $args->{config}->{FILTERS}->{timeago} = sub {
+ my ($time_str) = @_;
+ return time_ago(datetime_from($time_str, 'UTC'));
+ };
+}
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my ($vars, $page) = @$args{qw(vars page_id)};
+ return unless $page eq 'user_profile.html';
+ my $user = Bugzilla->user;
+
+ # determine user to display
+ my ($target, $login);
+ my $input = Bugzilla->input_params;
+ if (my $user_id = $input->{user_id}) {
+ # load from user_id
+ $user_id = 0 if $user_id =~ /\D/;
+ $target = Bugzilla::User->check({ id => $user_id });
+ } else {
+ # loading from login name requires authentication
+ Bugzilla->login(LOGIN_REQUIRED);
+ $login = $input->{login};
+ if (!$login) {
+ # show current user's profile by default
+ $target = $user;
+ } else {
+ my $limit = Bugzilla->params->{'maxusermatches'} + 1;
+ my $users = Bugzilla::User::match($login, $limit, 1);
+ if (scalar(@$users) == 1) {
+ # always allow singular matches without confirmation
+ $target = $users->[0];
+ } else {
+ Bugzilla::User::match_field({ 'login' => {'type' => 'single'} });
+ $target = Bugzilla::User->check($login);
+ }
+ }
+ }
+ $login ||= $target->login;
+
+ # load statistics into $vars
+ my $dbh = Bugzilla->switch_to_shadow_db;
+
+ my $stats = $dbh->selectall_hashref(
+ "SELECT name, count
+ FROM profiles_statistics
+ WHERE user_id = ?",
+ "name",
+ undef,
+ $target->id,
+ );
+ map { $stats->{$_} = $stats->{$_}->{count} } keys %$stats;
+
+ my $statuses = $dbh->selectall_hashref(
+ "SELECT status, count
+ FROM profiles_statistics_status
+ WHERE user_id = ?",
+ "status",
+ undef,
+ $target->id,
+ );
+ map { $statuses->{$_} = $statuses->{$_}->{count} } keys %$statuses;
+
+ my $products = $dbh->selectall_arrayref(
+ "SELECT product, count
+ FROM profiles_statistics_products
+ WHERE user_id = ?
+ ORDER BY product = '', count DESC",
+ { Slice => {} },
+ $target->id,
+ );
+
+ # ensure there's always an "other" product entry
+ my ($other_product) = grep { $_->{product} eq '' } @$products;
+ if (!$other_product) {
+ $other_product = { product => '', count => 0 };
+ push @$products, $other_product;
+ }
+
+ # load product objects and validate product visibility
+ foreach my $product (@$products) {
+ next if $product->{product} eq '';
+ my $product_obj = Bugzilla::Product->new({ name => $product->{product} });
+ if (!$product_obj || !$user->can_see_product($product_obj->name)) {
+ # products not accessible to current user are moved into "other"
+ $other_product->{count} += $product->{count};
+ $product->{count} = 0;
+ } else {
+ $product->{product} = $product_obj;
+ }
+ }
+
+ # set other's name, and remove empty products
+ $other_product->{product} = { name => 'Other' };
+ $products = [ grep { $_->{count} } @$products ];
+
+ $vars->{stats} = $stats;
+ $vars->{statuses} = $statuses;
+ $vars->{products} = $products;
+ $vars->{login} = $login;
+ $vars->{target} = $target;
+}
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my ($class, $columns) = @$args{qw(class columns)};
+ if ($class->isa('Bugzilla::User')) {
+ push(@$columns, qw(last_activity_ts last_statistics_ts));
+ }
+}
+
+sub object_update_columns {
+ my ($self, $args) = @_;
+ my ($object, $columns) = @$args{qw(object columns)};
+ if ($object->isa('Bugzilla::User')) {
+ push(@$columns, qw(last_activity_ts last_statistics_ts));
+ }
+}
+
+#
+# installation
+#
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'profiles_statistics'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE',
+ }
+ },
+ name => {
+ TYPE => 'VARCHAR(30)',
+ NOTNULL => 1,
+ },
+ count => {
+ TYPE => 'INT',
+ NOTNULL => 1,
+ },
+ ],
+ INDEXES => [
+ profiles_statistics_name_idx => {
+ FIELDS => [ 'user_id', 'name' ],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'profiles_statistics_status'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE',
+ }
+ },
+ status => {
+ TYPE => 'VARCHAR(64)',
+ NOTNULL => 1,
+ },
+ count => {
+ TYPE => 'INT',
+ NOTNULL => 1,
+ },
+ ],
+ INDEXES => [
+ profiles_statistics_status_idx => {
+ FIELDS => [ 'user_id', 'status' ],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'profiles_statistics_products'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE',
+ }
+ },
+ product => {
+ TYPE => 'VARCHAR(64)',
+ NOTNULL => 1,
+ },
+ count => {
+ TYPE => 'INT',
+ NOTNULL => 1,
+ },
+ ],
+ INDEXES => [
+ profiles_statistics_products_idx => {
+ FIELDS => [ 'user_id', 'product' ],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'profiles_statistics_recalc'} = {
+ FIELDS => [
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE',
+ }
+ },
+ ],
+ INDEXES => [
+ profiles_statistics_recalc_idx => {
+ FIELDS => [ 'user_id' ],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'profiles_statistics_recalc'} = {
+ FIELDS => [
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE',
+ }
+ },
+ ],
+ INDEXES => [
+ profiles_statistics_recalc_idx => {
+ FIELDS => [ 'user_id' ],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+}
+
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_add_column('profiles', 'last_activity_ts', { TYPE => 'DATETIME' });
+ $dbh->bz_add_column('profiles', 'last_statistics_ts', { TYPE => 'DATETIME' });
+}
+
+sub install_filesystem {
+ my ($self, $args) = @_;
+ my $files = $args->{'files'};
+ my $extensions_dir = bz_locations()->{'extensionsdir'};
+ my $script_name = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/update.pl";
+ $files->{$script_name} = {
+ perms => Bugzilla::Install::Filesystem::WS_EXECUTE
+ };
+ $script_name = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/migrate.pl";
+ $files->{$script_name} = {
+ perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE
+ };
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/UserProfile/bin/migrate.pl b/extensions/UserProfile/bin/migrate.pl
new file mode 100755
index 000000000..147edef9c
--- /dev/null
+++ b/extensions/UserProfile/bin/migrate.pl
@@ -0,0 +1,43 @@
+#!/usr/bin/perl
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+$| = 1;
+
+use FindBin qw($Bin);
+use lib "$Bin/../../..";
+
+use Bugzilla;
+BEGIN { Bugzilla->extensions() }
+
+use Bugzilla::Constants;
+use Bugzilla::Extension::UserProfile::Util;
+use Bugzilla::Install::Util qw(indicate_progress);
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+my $dbh = Bugzilla->dbh;
+
+my $user_ids = $dbh->selectcol_arrayref(
+ "SELECT userid
+ FROM profiles
+ WHERE last_activity_ts IS NULL
+ ORDER BY userid"
+);
+
+my ($current, $total) = (1, scalar(@$user_ids));
+foreach my $user_id (@$user_ids) {
+ indicate_progress({ current => $current++, total => $total, every => 25 });
+ my $ts = last_user_activity($user_id);
+ next unless $ts;
+ $dbh->do(
+ "UPDATE profiles SET last_activity_ts = ? WHERE userid = ?",
+ undef,
+ $ts, $user_id);
+}
diff --git a/extensions/UserProfile/bin/update.pl b/extensions/UserProfile/bin/update.pl
new file mode 100755
index 000000000..2a4997aee
--- /dev/null
+++ b/extensions/UserProfile/bin/update.pl
@@ -0,0 +1,81 @@
+#!/usr/bin/perl
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+
+use FindBin qw($Bin);
+use lib "$Bin/../../..";
+
+use Bugzilla;
+BEGIN { Bugzilla->extensions() }
+
+use Bugzilla::Constants;
+use Bugzilla::Extension::UserProfile::Util;
+use Bugzilla::User;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+my $dbh = Bugzilla->dbh;
+my $user_ids;
+my $verbose = grep { $_ eq '-v' } @ARGV;
+
+$user_ids = $dbh->selectcol_arrayref(
+ "SELECT user_id
+ FROM profiles_statistics_recalc
+ ORDER BY user_id",
+ { Slice => {} }
+);
+
+if (@$user_ids) {
+ print "recalculating last_user_activity\n";
+ my ($count, $total) = (0, scalar(@$user_ids));
+ foreach my $user_id (@$user_ids) {
+ if ($verbose) {
+ $count++;
+ my $login = user_id_to_login($user_id);
+ print "$count/$total $login ($user_id)\n";
+ }
+ $dbh->do(
+ "UPDATE profiles
+ SET last_activity_ts = ?,
+ last_statistics_ts = NULL
+ WHERE userid = ?",
+ undef,
+ last_user_activity($user_id),
+ $user_id
+ );
+ Bugzilla->memcached->clear({ table => 'profiles', id => $user_id });
+ }
+ $dbh->do(
+ "DELETE FROM profiles_statistics_recalc WHERE " . $dbh->sql_in('user_id', $user_ids)
+ );
+}
+
+$user_ids = $dbh->selectcol_arrayref(
+ "SELECT userid
+ FROM profiles
+ WHERE last_activity_ts IS NOT NULL
+ AND (last_statistics_ts IS NULL
+ OR last_activity_ts > last_statistics_ts)
+ ORDER BY userid",
+ { Slice => {} }
+);
+
+if (@$user_ids) {
+ $verbose && print "updating statistics\n";
+ my ($count, $total) = (0, scalar(@$user_ids));
+ foreach my $user_id (@$user_ids) {
+ if ($verbose) {
+ $count++;
+ my $login = user_id_to_login($user_id);
+ print "$count/$total $login ($user_id)\n";
+ }
+ update_statistics_by_user($user_id);
+ }
+}
diff --git a/extensions/UserProfile/lib/TimeAgo.pm b/extensions/UserProfile/lib/TimeAgo.pm
new file mode 100644
index 000000000..d20f0edf5
--- /dev/null
+++ b/extensions/UserProfile/lib/TimeAgo.pm
@@ -0,0 +1,179 @@
+package Bugzilla::Extension::UserProfile::TimeAgo;
+
+use strict;
+use utf8;
+use DateTime;
+use Carp;
+use Exporter qw(import);
+
+use if $ENV{ARCH_64BIT}, 'integer';
+
+our @EXPORT_OK = qw(time_ago);
+
+our $VERSION = '0.06';
+
+my @ranges = (
+ [ -1, 'in the future' ],
+ [ 60, 'just now' ],
+ [ 900, 'a few minutes ago'], # 15*60
+ [ 3000, 'less than an hour ago'], # 50*60
+ [ 4500, 'about an hour ago'], # 75*60
+ [ 7200, 'more than an hour ago'], # 2*60*60
+ [ 21600, 'several hours ago'], # 6*60*60
+ [ 86400, 'today', sub { # 24*60*60
+ my $time = shift;
+ my $now = shift;
+ if ( $time->day < $now->day
+ or $time->month < $now->month
+ or $time->year < $now->year
+ ) {
+ return 'yesterday'
+ }
+ if ($time->hour < 5) {
+ return 'tonight'
+ }
+ if ($time->hour < 10) {
+ return 'this morning'
+ }
+ if ($time->hour < 15) {
+ return 'today'
+ }
+ if ($time->hour < 19) {
+ return 'this afternoon'
+ }
+ return 'this evening'
+ }],
+ [ 172800, 'yesterday'], # 2*24*60*60
+ [ 604800, 'this week'], # 7*24*60*60
+ [ 1209600, 'last week'], # 2*7*24*60*60
+ [ 2678400, 'this month', sub { # 31*24*60*60
+ my $time = shift;
+ my $now = shift;
+ if ($time->year == $now->year and $time->month == $now->month) {
+ return 'this month'
+ }
+ return 'last month'
+ }],
+ [ 5356800, 'last month'], # 2*31*24*60*60
+ [ 24105600, 'several months ago'], # 9*31*24*60*60
+ [ 31536000, 'about a year ago'], # 365*24*60*60
+ [ 34214400, 'last year'], # (365+31)*24*60*60
+ [ 63072000, 'more than a year ago'], # 2*365*24*60*60
+ [ 283824000, 'several years ago'], # 9*365*24*60*60
+ [ 315360000, 'about a decade ago'], # 10*365*24*60*60
+ [ 630720000, 'last decade'], # 20*365*24*60*60
+ [ 2838240000, 'several decades ago'], # 90*365*24*60*60
+ [ 3153600000, 'about a century ago'], # 100*365*24*60*60
+ [ 6307200000, 'last century'], # 200*365*24*60*60
+ [ 6622560000, 'more than a century ago'], # 210*365*24*60*60
+ [ 28382400000, 'several centuries ago'], # 900*365*24*60*60
+ [ 31536000000, 'about a millenium ago'], # 1000*365*24*60*60
+ [ 63072000000, 'more than a millenium ago'], # 2000*365*24*60*60
+);
+
+sub time_ago {
+ my ($time, $now) = @_;
+
+ if (not defined $time or not $time->isa('DateTime')) {
+ croak('DateTime::Duration::Fuzzy::time_ago needs a DateTime object as first parameter')
+ }
+ if (not defined $now) {
+ $now = DateTime->now();
+ }
+ if (not $now->isa('DateTime')) {
+ croak('Invalid second parameter provided to DateTime::Duration::Fuzzy::time_ago; it must be a DateTime object if provided')
+ }
+
+ my $dur = $now->subtract_datetime_absolute($time)->in_units('seconds');
+
+ foreach my $range ( @ranges ) {
+ if ( $dur <= $range->[0] ) {
+ if ( $range->[2] ) {
+ return $range->[2]->($time, $now)
+ }
+ return $range->[1]
+ }
+ }
+
+ return 'millenia ago'
+}
+
+1
+
+__END__
+
+=head1 NAME
+
+DateTime::Duration::Fuzzy -- express dates as fuzzy human-friendly strings
+
+=head1 SYNOPSIS
+
+ use DateTime::Duration::Fuzzy qw(time_ago);
+ use DateTime;
+
+ my $now = DateTime->new(
+ year => 2010, month => 12, day => 12,
+ hour => 19, minute => 59,
+ );
+ my $then = DateTime->new(
+ year => 2010, month => 12, day => 12,
+ hour => 15,
+ );
+ print time_ago($then, $now);
+ # outputs 'several hours ago'
+
+ print time_ago($then);
+ # $now taken from C<time> function
+
+=head1 DESCRIPTION
+
+DateTime::Duration::Fuzzy is inspired from the timeAgo jQuery module
+L<http://timeago.yarp.com/>.
+
+It takes two DateTime objects -- first one representing a moment in the past
+and second optional one representine the present, and returns a human-friendly
+fuzzy expression of the time gone.
+
+=head2 functions
+
+=over 4
+
+=item time_ago($then, $now)
+
+The only exportable function.
+
+First obligatory parameter is a DateTime object.
+
+Second optional parameter is also a DateTime object.
+If it's not provided, then I<now> as the C<time> function returns is
+substituted.
+
+Returns a string expression of the interval between the two DateTime
+objects, like C<several hours ago>, C<yesterday> or <last century>.
+
+=back
+
+=head2 performance
+
+On 64bit machines, it is asvisable to 'use integer', which makes
+the calculations faster. You can turn this on by setting the
+C<ARCH_64BIT> environmental variable to a true value.
+
+If you do this on a 32bit machine, you will get wrong results for
+intervals starting with "several decades ago".
+
+=head1 AUTHOR
+
+Jan Oldrich Kruza, C<< <sixtease at cpan.org> >>
+
+=head1 LICENSE AND COPYRIGHT
+
+Copyright 2010 Jan Oldrich Kruza.
+
+This program is free software; you can redistribute it and/or modify it
+under the terms of either: the GNU General Public License as published
+by the Free Software Foundation; or the Artistic License.
+
+See http://dev.perl.org/licenses/ for more information.
+
+=cut
diff --git a/extensions/UserProfile/lib/Util.pm b/extensions/UserProfile/lib/Util.pm
new file mode 100644
index 000000000..71d0e6501
--- /dev/null
+++ b/extensions/UserProfile/lib/Util.pm
@@ -0,0 +1,387 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::UserProfile::Util;
+
+use strict;
+use warnings;
+
+use base qw(Exporter);
+our @EXPORT = qw( update_statistics_by_user
+ tag_for_recount_from_bug
+ last_user_activity );
+
+use Bugzilla;
+
+sub update_statistics_by_user {
+ my ($user_id) = @_;
+
+ # run all our queries on the slaves
+
+ my $dbh = Bugzilla->switch_to_shadow_db();
+
+ my $now = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+
+ # grab the current values
+
+ my $last_statistics_ts = _get_last_statistics_ts($user_id);
+
+ my $statistics = _get_stats($user_id, 'profiles_statistics', 'name');
+ my $by_status = _get_stats($user_id, 'profiles_statistics_status', 'status');
+ my $by_product = _get_stats($user_id, 'profiles_statistics_products', 'product');
+
+ # bugs filed
+ _update_statistics($statistics, 'bugs_filed', [ $user_id ], <<EOF);
+ SELECT COUNT(*)
+ FROM bugs
+ WHERE bugs.reporter = ?
+EOF
+
+ # comments made
+ _update_statistics($statistics, 'comments', [ $user_id ], <<EOF);
+ SELECT COUNT(*)
+ FROM longdescs
+ WHERE who = ?
+EOF
+
+ # commented on
+ _update_statistics($statistics, 'commented_on', [ $user_id ], <<EOF);
+ SELECT COUNT(*) FROM (
+ SELECT longdescs.bug_id
+ FROM longdescs
+ WHERE who = ?
+ GROUP BY longdescs.bug_id
+ ) AS temp
+EOF
+
+ # confirmed
+ _update_statistics($statistics, 'confirmed', [ $user_id, _field_id('bug_status') ], <<EOF);
+ SELECT COUNT(*)
+ FROM bugs_activity
+ WHERE who = ?
+ AND fieldid = ?
+ AND removed = 'UNCONFIRMED'
+ AND added = 'NEW'
+EOF
+
+ # patches submitted
+ _update_statistics($statistics, 'patches', [ $user_id ], <<EOF);
+ SELECT COUNT(*)
+ FROM attachments
+ WHERE submitter_id = ?
+ AND (ispatch = 1
+ OR mimetype = 'text/x-github-pull-request'
+ OR mimetype = 'text/x-review-board-request')
+EOF
+
+ # patches reviewed
+ _update_statistics($statistics, 'reviews', [ $user_id ], <<EOF);
+ SELECT COUNT(*)
+ FROM flags
+ INNER JOIN attachments ON attachments.attach_id = flags.attach_id
+ WHERE setter_id = ?
+ AND (attachments.ispatch = 1
+ OR attachments.mimetype = 'text/x-github-pull-request'
+ OR attachments.mimetype = 'text/x-review-board-request')
+ AND status IN ('+', '-')
+EOF
+
+ # assigned to
+ _update_statistics($statistics, 'assigned', [ $user_id ], <<EOF);
+ SELECT COUNT(*)
+ FROM bugs
+ WHERE assigned_to = ?
+EOF
+
+ # qa contact
+ _update_statistics($statistics, 'qa_contact', [ $user_id ], <<EOF);
+ SELECT COUNT(*)
+ FROM bugs
+ WHERE qa_contact = ?
+EOF
+
+ # bugs touched
+ _update_statistics($statistics, 'touched', [ $user_id, $user_id], <<EOF);
+ SELECT COUNT(*) FROM (
+ SELECT bugs_activity.bug_id
+ FROM bugs_activity
+ WHERE who = ?
+ GROUP BY bugs_activity.bug_id
+ UNION
+ SELECT longdescs.bug_id
+ FROM longdescs
+ WHERE who = ?
+ GROUP BY longdescs.bug_id
+ ) temp
+EOF
+
+ # activity by status/resolution, and product
+ _activity_by_status($by_status, $user_id);
+ _activity_by_product($by_product, $user_id);
+
+ # if nothing is dirty, no need to do anything else
+ if ($last_statistics_ts) {
+ return unless _has_dirty($statistics)
+ || _has_dirty($by_status)
+ || _has_dirty($by_product);
+ }
+
+ # switch back to the main db for updating
+
+ $dbh = Bugzilla->switch_to_main_db();
+ $dbh->bz_start_transaction();
+
+ # commit updated statistics
+
+ _set_stats($statistics, $user_id, 'profiles_statistics', 'name')
+ if _has_dirty($statistics);
+ _set_stats($by_status, $user_id, 'profiles_statistics_status', 'status')
+ if _has_dirty($by_status);
+ _set_stats($by_product, $user_id, 'profiles_statistics_products', 'product')
+ if _has_dirty($by_product);
+
+ # update the user's last_statistics_ts
+ _set_last_statistics_ts($user_id, $now);
+
+ $dbh->bz_commit_transaction();
+}
+
+sub tag_for_recount_from_bug {
+ my ($bug_id) = @_;
+ my $dbh = Bugzilla->dbh;
+ # get a list of all users associated with this bug
+ my $user_ids = $dbh->selectcol_arrayref(<<EOF, undef, $bug_id, _field_id('cc'), $bug_id);
+ SELECT DISTINCT user_id
+ FROM (
+ SELECT DISTINCT who AS user_id
+ FROM bugs_activity
+ WHERE bug_id = ?
+ AND fieldid <> ?
+ UNION ALL
+ SELECT DISTINCT who AS user_id
+ FROM longdescs
+ WHERE bug_id = ?
+ ) tmp
+EOF
+ # clear last_statistics_ts
+ $dbh->do(
+ "UPDATE profiles SET last_statistics_ts=NULL WHERE " . $dbh->sql_in('userid', $user_ids)
+ );
+ foreach my $id (@$user_ids) {
+ Bugzilla->memcached->clear({ table => 'profiles', id => $id });
+ }
+ return scalar(@$user_ids);
+}
+
+sub last_user_activity {
+ # last comment, or change to a bug (excluding CC changes)
+ my ($user_id) = @_;
+ return Bugzilla->dbh->selectrow_array(<<EOF, undef, $user_id, $user_id, _field_id('cc'));
+ SELECT MAX(bug_when)
+ FROM (
+ SELECT MAX(bug_when) AS bug_when
+ FROM longdescs
+ WHERE who = ?
+ UNION ALL
+ SELECT MAX(bug_when) AS bug_when
+ FROM bugs_activity
+ WHERE who = ?
+ AND fieldid <> ?
+ ) tmp
+EOF
+}
+
+# for performance reasons hit the db directly rather than using the user object
+
+sub _get_last_statistics_ts {
+ my ($user_id) = @_;
+ return Bugzilla->dbh->selectrow_array(
+ "SELECT last_statistics_ts FROM profiles WHERE userid = ?",
+ undef, $user_id
+ );
+}
+
+sub _set_last_statistics_ts {
+ my ($user_id, $timestamp) = @_;
+ Bugzilla->dbh->do(
+ "UPDATE profiles SET last_statistics_ts = ? WHERE userid = ?",
+ undef,
+ $timestamp, $user_id,
+ );
+ Bugzilla->memcached->clear({ table => 'profiles', id => $user_id });
+}
+
+sub _update_statistics {
+ my ($statistics, $name, $values, $sql) = @_;
+ my ($count) = Bugzilla->dbh->selectrow_array($sql, undef, @$values);
+ if (!exists $statistics->{$name}) {
+ $statistics->{$name} = {
+ id => 0,
+ count => $count,
+ dirty => 1,
+ };
+ } elsif ($statistics->{$name}->{count} != $count) {
+ $statistics->{$name}->{count} = $count;
+ $statistics->{$name}->{dirty} = 1;
+ };
+}
+
+sub _activity_by_status {
+ my ($by_status, $user_id) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # we actually track both status and resolution changes as statuses
+ my @values = ($user_id, _field_id('bug_status'), $user_id, _field_id('resolution'));
+ my $rows = $dbh->selectall_arrayref(<<EOF, { Slice => {} }, @values);
+ SELECT added AS status, COUNT(*) AS count
+ FROM bugs_activity
+ WHERE who = ?
+ AND fieldid = ?
+ GROUP BY added
+ UNION ALL
+ SELECT CONCAT('RESOLVED/', added) AS status, COUNT(*) AS count
+ FROM bugs_activity
+ WHERE who = ?
+ AND fieldid = ?
+ AND added != ''
+ GROUP BY added
+EOF
+
+ foreach my $row (@$rows) {
+ my $status = $row->{status};
+ if (!exists $by_status->{$status}) {
+ $by_status->{$status} = {
+ id => 0,
+ count => $row->{count},
+ dirty => 1,
+ };
+ } elsif ($by_status->{$status}->{count} != $row->{count}) {
+ $by_status->{$status}->{count} = $row->{count};
+ $by_status->{$status}->{dirty} = 1;
+ }
+ }
+}
+
+sub _activity_by_product {
+ my ($by_product, $user_id) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my %products;
+
+ # changes
+ my $rows = $dbh->selectall_arrayref(<<EOF, { Slice => {} }, $user_id);
+ SELECT products.name AS product, count(*) AS count
+ FROM bugs_activity
+ INNER JOIN bugs ON bugs.bug_id = bugs_activity.bug_id
+ INNER JOIN products ON products.id = bugs.product_id
+ WHERE who = ?
+ GROUP BY bugs.product_id
+EOF
+ map { $products{$_->{product}} += $_->{count} } @$rows;
+
+ # comments
+ $rows = $dbh->selectall_arrayref(<<EOF, { Slice => {} }, $user_id);
+ SELECT products.name AS product, count(*) AS count
+ FROM longdescs
+ INNER JOIN bugs ON bugs.bug_id = longdescs.bug_id
+ INNER JOIN products ON products.id = bugs.product_id
+ WHERE who = ?
+ GROUP BY bugs.product_id
+EOF
+ map { $products{$_->{product}} += $_->{count} } @$rows;
+
+ # store only the top 10 and 'other' (which is an empty string)
+ my @sorted = sort { $products{$b} <=> $products{$a} } keys %products;
+ my @other;
+ @other = splice(@sorted, 10) if scalar(@sorted) > 10;
+ map { $products{''} += $products{$_} } @other;
+ push @sorted, '' if $products{''};
+
+ # update by_product
+ foreach my $product (@sorted) {
+ if (!exists $by_product->{$product}) {
+ $by_product->{$product} = {
+ id => 0,
+ count => $products{$product},
+ dirty => 1,
+ };
+ } elsif ($by_product->{$product}->{count} != $products{$product}) {
+ $by_product->{$product}->{count} = $products{$product};
+ $by_product->{$product}->{dirty} = 1;
+ }
+ }
+ foreach my $product (keys %$by_product) {
+ if (!grep { $_ eq $product } @sorted) {
+ delete $by_product->{$product};
+ }
+ }
+}
+
+our $_field_id_cache;
+sub _field_id {
+ my ($name) = @_;
+ if (!$_field_id_cache) {
+ my $rows = Bugzilla->dbh->selectall_arrayref("SELECT id, name FROM fielddefs");
+ foreach my $row (@$rows) {
+ $_field_id_cache->{$row->[1]} = $row->[0];
+ }
+ }
+ return $_field_id_cache->{$name};
+}
+
+sub _get_stats {
+ my ($user_id, $table, $name_field) = @_;
+ my $result = {};
+ my $rows = Bugzilla->dbh->selectall_arrayref(
+ "SELECT * FROM $table WHERE user_id = ?",
+ { Slice => {} },
+ $user_id,
+ );
+ foreach my $row (@$rows) {
+ unless (defined $row->{$name_field}) {
+ print "$user_id $table $name_field\n";
+ die;
+ }
+ $result->{$row->{$name_field}} = {
+ id => $row->{id},
+ count => $row->{count},
+ dirty => 0,
+ }
+ }
+ return $result;
+}
+
+sub _set_stats {
+ my ($statistics, $user_id, $table, $name_field) = @_;
+ my $dbh = Bugzilla->dbh;
+ foreach my $name (keys %$statistics) {
+ next unless $statistics->{$name}->{dirty};
+ if ($statistics->{$name}->{id}) {
+ $dbh->do(
+ "UPDATE $table SET count = ? WHERE user_id = ? AND $name_field = ?",
+ undef,
+ $statistics->{$name}->{count}, $user_id, $name,
+ );
+ } else {
+ $dbh->do(
+ "INSERT INTO $table(user_id, $name_field, count) VALUES (?, ?, ?)",
+ undef,
+ $user_id, $name, $statistics->{$name}->{count},
+ );
+ }
+ }
+}
+
+sub _has_dirty {
+ my ($statistics) = @_;
+ foreach my $name (keys %$statistics) {
+ return 1 if $statistics->{$name}->{dirty};
+ }
+ return 0;
+}
+
+1;
diff --git a/extensions/UserProfile/template/en/default/hook/account/prefs/account-field.html.tmpl b/extensions/UserProfile/template/en/default/hook/account/prefs/account-field.html.tmpl
new file mode 100644
index 000000000..f2e3aad01
--- /dev/null
+++ b/extensions/UserProfile/template/en/default/hook/account/prefs/account-field.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<a href="user_profile?login=[% user.login FILTER uri %]">
+ [% terms.Bugzilla %] User Profile
+</a><br><hr>
diff --git a/extensions/UserProfile/template/en/default/pages/user_profile.html.tmpl b/extensions/UserProfile/template/en/default/pages/user_profile.html.tmpl
new file mode 100644
index 000000000..810a974ec
--- /dev/null
+++ b/extensions/UserProfile/template/en/default/pages/user_profile.html.tmpl
@@ -0,0 +1,300 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% IF user.id %]
+ [% filtered_identity = target.identity FILTER html %]
+[% ELSE %]
+ [% filtered_identity = target.name || target.address.user FILTER html %]
+[% END %]
+[% PROCESS global/header.html.tmpl
+ title = "User Profile: $filtered_identity"
+ style_urls = [ "extensions/UserProfile/web/styles/user_profile.css" ]
+ yui = [ 'autocomplete' ]
+ javascript_urls = [ "js/field.js" ]
+%]
+
+<table id="user_profile_table">
+
+[% IF user.id %]
+ <tr>
+ <td>&nbsp;</td>
+ <th>Search</th>
+ <td colspan="2">
+ <form action="user_profile">
+ [% INCLUDE global/userselect.html.tmpl
+ id => "login"
+ name => "login"
+ value => login
+ size => 40
+ emptyok => 0
+ %]
+ &nbsp;&nbsp;<input type="submit" value="Show">
+ </form>
+ </td>
+ </tr>
+
+ <tr>
+ <td colspan="4" class="separator"><hr></td>
+ </tr>
+[% END %]
+
+<tr>
+ <td rowspan="[% user.id ? 6 : 5 %]" id="gravatar-container">
+ [% IF user.gravatar %]
+ <img id="gravatar" src="[% target.gravatar(256) FILTER none %]" width="128" height="128"><br>
+ [% IF target.id == user.id %]
+ <a href="http://gravatar.com/">Change my image</a>
+ [% END %]
+ [% ELSE %]
+ &nbsp;
+ [% END %]
+ </td>
+ <th>Name</th>
+ <td colspan="2">
+ [% target.name || target.address.user FILTER html %]
+ [% IF target.id == user.id %]
+ <span style="font-size: x-small;">(<a href="userprefs.cgi?tab=account">change</a>)</span>
+ [% END %]
+ </td>
+</tr>
+
+[% IF user.id %]
+ <tr>
+ <th>Email</th>
+ <td colspan="2">
+ <a href="mailto:[% target.login FILTER uri %]">[% target.login FILTER html %]</a>
+ [% " (disabled)" UNLESS target.is_enabled %]
+ </td>
+ </tr>
+[% END %]
+
+<tr>
+ <td>&nbsp;</td>
+</tr>
+
+[%# user.creation_ts is added by the TagNewUsers extension %]
+[% IF target.can('creation_ts') %]
+ <tr>
+ <th>Created</th>
+ <td colspan="2">
+ [% target.creation_ts FILTER time %] ([% target.creation_ts FILTER timeago FILTER html %])
+ </td>
+ </tr>
+[% END %]
+
+<tr>
+ <th>Last activity</th>
+ <td colspan="2">
+ [% IF user.id %]
+ <a href="page.cgi?id=user_activity.html&amp;action=run&amp;who=[% target.login FILTER uri %]">
+ [% END %]
+ [% target.last_activity_ts FILTER time %]
+ [% "</a>" IF user.id %]
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+</tr>
+
+[%# request counters provided by the Review extension %]
+[% IF target.can("review_count")
+ && (
+ stats.reviews
+ || (
+ target.review_request_count
+ || target.feedback_request_count
+ || target.needinfo_request_count
+ )
+ )
+%]
+ <tr>
+ <td colspan="4" class="separator"><hr></td>
+ </tr>
+ <tr>
+ <td>Review Queue</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <th>Review requests</th>
+ <td class="numeric">
+ [% IF user.id %]
+ <a href="request.cgi?action=queue&amp;type=review&amp;requestee=[% target.login FILTER uri %]&amp;group=type"
+ target="_blank">
+ [% END %]
+ [% target.review_request_count FILTER html %]
+ [% "</a>" IF user.id %]
+ </td>
+ [% IF user.id %]
+ <td>
+ (<a href="page.cgi?id=review_history.html&amp;requestee=[% target.login FILTER uri %]">Review History</a>)
+ </td>
+ [% END %]
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <th>Feedback requests</th>
+ <td class="numeric">
+ [% IF user.id %]
+ <a href="request.cgi?action=queue&amp;type=feedback&amp;requestee=[% target.login FILTER uri %]&amp;group=type"
+ target="_blank">
+ [% END %]
+ [% target.feedback_request_count FILTER html %]
+ [% "</a>" IF user.id %]
+ </td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <th>Needinfo requests</th>
+ <td class="numeric">
+ [% IF user.id %]
+ <a href="request.cgi?action=queue&amp;type=needinfo&amp;requestee=[% target.login FILTER uri %]&amp;group=type"
+ target="_blank">
+ [% END %]
+ [% target.needinfo_request_count FILTER html %]
+ [% "</a>" IF user.id %]
+ </td>
+ </tr>
+[% END %]
+
+<tr>
+ <td colspan="4" class="separator"><hr></td>
+</tr>
+<tr>
+ <td>User Statistics</td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <th>[% terms.Bugs %] filed</th>
+ <td class="numeric">
+ [% IF user.id %]
+ <a href="buglist.cgi?query_format=advanced&amp;emailtype1=exact&amp;emailreporter1=1&amp;email1=[% target.login FILTER uri %]"
+ target="_blank">
+ [% END %]
+ [% stats.bugs_filed || 0 FILTER html %]
+ [% "</a>" IF user.id %]
+ </td>
+</tr>
+<tr>
+ <td>&nbsp;</td>
+ <th>Comments made</th>
+ <td class="numeric">[% stats.comments || 0 FILTER html %]</td>
+</tr>
+<tr>
+ <td>&nbsp;</td>
+ <th>Assigned to</th>
+ <td class="numeric">
+ [% IF user.id %]
+ <a href="buglist.cgi?query_format=advanced&amp;emailtype1=exact&amp;emailassigned_to1=1&amp;email1=[% target.login FILTER uri %]"
+ target="_blank">
+ [% END %]
+ [% stats.assigned || 0 FILTER html %]
+ [% "</a>" IF user.id %]
+ </td>
+</tr>
+<tr>
+ <td>&nbsp;</td>
+ <th>Commented on</th>
+ <td class="numeric">
+ [% IF user.id %]
+ <a href="buglist.cgi?query_format=advanced&amp;emailtype1=exact&amp;emaillongdesc1=1&amp;email1=[% target.login FILTER uri %]"
+ target="_blank">
+ [% END %]
+ [% stats.commented_on || 0 FILTER html %]
+ [% "</a>" IF user.id %]
+ </td>
+</tr>
+<tr>
+ <td>&nbsp;</td>
+ <th>QA-Contact</th>
+ <td class="numeric">
+ [% IF user.id %]
+ <a href="buglist.cgi?query_format=advanced&amp;emailtype1=exact&amp;emailqa_contact1=1&amp;email1=[% target.login FILTER uri %]"
+ target="_blank">
+ [% END %]
+ [% stats.qa_contact || 0 FILTER html %]
+ [% "</a>" IF user.id %]
+ </td>
+</tr>
+<tr>
+ <td>&nbsp;</td>
+ <th>Patches submitted</th>
+ <td class="numeric">[% stats.patches || 0 FILTER html %]</td>
+</tr>
+<tr>
+ <td>&nbsp;</td>
+ <th>Patches reviewed</th>
+ <td class="numeric">[% stats.reviews || 0 FILTER html %]</td>
+</tr>
+<tr>
+ <td>&nbsp;</td>
+ <th>[% terms.Bugs %] poked</th>
+ <td class="numeric">[% stats.touched || 0 FILTER html %]</td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <th>Statuses changed</th>
+ <td colspan="2">
+ RESOLVED ([% statuses.item('RESOLVED') || 0 FILTER html %]),
+ FIXED ([% statuses.item('RESOLVED/FIXED') || 0 FILTER html %]),
+ VERIFIED ([% statuses.item('VERIFIED') || 0 FILTER html %]),
+ INVALID ([% statuses.item('RESOLVED/INVALID') || 0 FILTER html %])
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <th>Activity by product</th>
+ <td colspan="2">
+ [% FOREACH p = products %]
+ <span class="product_span">
+ [% IF p.product.id %]
+ <a href="describecomponents.cgi?product=[% p.product.name FILTER uri %]"
+ target="_blank">
+ [% END %]
+ [% p.product.name FILTER html %] ([% p.count || 0 FILTER html %])
+ [% "</a>" IF p.product.id %]
+ [% "," UNLESS loop.last ~%]
+ </span>
+ [%+ END %]
+ </td>
+</tr>
+
+<tr>
+ <td colspan="3">
+ <div id="what">
+ <a href="https://wiki.mozilla.org/BMO/User_profile_fields" target="_blank">
+ What do these fields mean?
+ </a>
+ </div>
+
+ <div id="updated">
+ This information is updated daily
+ </div>
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td>&nbsp;</td>
+ <td>&nbsp;</td>
+ <td width="100%">&nbsp;</td>
+</tr>
+
+</table>
+
+[% PROCESS global/footer.html.tmpl %]
+
diff --git a/extensions/UserProfile/web/styles/user_profile.css b/extensions/UserProfile/web/styles/user_profile.css
new file mode 100644
index 000000000..ef1f71dd9
--- /dev/null
+++ b/extensions/UserProfile/web/styles/user_profile.css
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#login_autocomplete {
+ float: left;
+}
+
+#user_profile_table th {
+ text-align: right;
+ padding-right: 1em;
+ vertical-align: middle;
+ white-space: nowrap;
+}
+
+#user_profile_table .numeric {
+ text-align: right;
+}
+
+#user_profile_table .product_span {
+ white-space: nowrap;
+}
+
+#updated {
+ font-style: italic;
+ font-size: x-small;
+}
+
+#gravatar-container {
+ text-align: center;
+ font-size: x-small;
+ vertical-align: top;
+ padding-right: 15px;
+}
+
+#gravatar {
+ -moz-box-shadow: 2px 2px 5px #888;
+ -webkit-box-shadow: 2px 2px 5px #888;
+ box-shadow: 2px 2px 5px #888;
+ margin-bottom: 5px;
+}
+
+#what {
+ margin-top: 1em;
+}
diff --git a/extensions/UserStory/Config.pm b/extensions/UserStory/Config.pm
new file mode 100644
index 000000000..8649c71cf
--- /dev/null
+++ b/extensions/UserStory/Config.pm
@@ -0,0 +1,21 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::UserStory;
+use strict;
+
+use constant NAME => 'UserStory';
+use constant REQUIRED_MODULES => [
+ {
+ package => 'Text-Diff',
+ module => 'Text::Diff',
+ version => 0,
+ },
+];
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/UserStory/Extension.pm b/extensions/UserStory/Extension.pm
new file mode 100644
index 000000000..2053a0097
--- /dev/null
+++ b/extensions/UserStory/Extension.pm
@@ -0,0 +1,102 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::UserStory;
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+our $VERSION = '1';
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Extension::UserStory::Constants;
+use Bugzilla::Extension::BMO::FakeBug;
+
+use Text::Diff;
+
+BEGIN {
+ *Bugzilla::Bug::user_story_visible = \&_bug_user_story_visible;
+ *Bugzilla::Extension::BMO::FakeBug::user_story_visible = \&_bug_user_story_visible;
+}
+
+sub _bug_user_story_visible {
+ my ($self) = @_;
+ if (!exists $self->{user_story_visible}) {
+ # Visible by default
+ $self->{user_story_visible} = 1;
+ my ($product, $component) = ($self->product, $self->component);
+ my $exclude_components = [];
+ if (exists USER_STORY_EXCLUDE->{$product}) {
+ $exclude_components = USER_STORY_EXCLUDE->{$product};
+ if (scalar(@$exclude_components) == 0
+ || ($component && grep { $_ eq $component } @$exclude_components))
+ {
+ $self->{user_story_visible} = 0;
+ }
+ }
+ $self->{user_story_exclude_components} = $exclude_components;
+ }
+ return ($self->{user_story_visible}, $self->{user_story_exclude_components});
+}
+
+# ensure user is allowed to edit the story
+sub bug_check_can_change_field {
+ my ($self, $args) = @_;
+ my ($bug, $field, $priv_results) = @$args{qw(bug field priv_results)};
+ return unless $field eq 'cf_user_story';
+ if (!Bugzilla->user->in_group(USER_STORY_GROUP)) {
+ push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ }
+}
+
+# store just a diff of the changes in the bugs_activity table
+sub bug_update_before_logging {
+ my ($self, $args) = @_;
+ my $changes = $args->{changes};
+ return unless exists $changes->{cf_user_story};
+ my $diff = diff(
+ \$changes->{cf_user_story}->[0],
+ \$changes->{cf_user_story}->[1],
+ {
+ CONTEXT => 0,
+ },
+ );
+ $changes->{cf_user_story} = [ '', $diff ];
+}
+
+# stop inline-history from displaying changes to the user story
+sub inline_history_activtiy {
+ my ($self, $args) = @_;
+ foreach my $activity (@{ $args->{activity} }) {
+ foreach my $change (@{ $activity->{changes} }) {
+ if ($change->{fieldname} eq 'cf_user_story') {
+ $change->{removed} = '';
+ $change->{added} = '(updated)';
+ }
+ }
+ }
+}
+
+# create cf_user_story field
+sub install_update_db {
+ my ($self, $args) = @_;
+ return if Bugzilla::Field->new({ name => 'cf_user_story'});
+ Bugzilla::Field->create({
+ name => 'cf_user_story',
+ description => 'User Story',
+ type => FIELD_TYPE_TEXTAREA,
+ mailhead => 0,
+ enter_bug => 0,
+ obsolete => 0,
+ custom => 1,
+ buglist => 0,
+ });
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/UserStory/lib/Constants.pm b/extensions/UserStory/lib/Constants.pm
new file mode 100644
index 000000000..d09b28fef
--- /dev/null
+++ b/extensions/UserStory/lib/Constants.pm
@@ -0,0 +1,29 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::UserStory::Constants;
+
+use strict;
+use warnings;
+
+use base qw(Exporter);
+
+our @EXPORT = qw( USER_STORY_EXCLUDE USER_STORY_GROUP );
+
+# Group allowed to set/edit the user story field
+use constant USER_STORY_GROUP => 'editbugs';
+
+# Exclude showing the user story field for these products/components.
+# Examples:
+# Don't show User Story on any Firefox OS component:
+# 'Firefox OS' => [],
+# Don't show User Story on Developer Tools component, visible on all other
+# Firefox components
+# 'Firefox' => ['Developer Tools'],
+use constant USER_STORY_EXCLUDE => { };
+
+1;
diff --git a/extensions/UserStory/template/en/default/hook/bug/comments-comment_banner.html.tmpl b/extensions/UserStory/template/en/default/hook/bug/comments-comment_banner.html.tmpl
new file mode 100644
index 000000000..6a7770066
--- /dev/null
+++ b/extensions/UserStory/template/en/default/hook/bug/comments-comment_banner.html.tmpl
@@ -0,0 +1,73 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS bug.user_story_visible.0 %]
+[% RETURN IF user.id == 0 && bug.cf_user_story == "" %]
+[% can_edit_story = bug.check_can_change_field('cf_user_story', 0, 1) %]
+
+<div class="user_story">
+ <script type="text/javascript">
+ function userStoryComment() {
+ var commenttext = "(Commenting on User Story)\n";
+ var text_elem = document.getElementById('user_story');
+ var commenttext = commenttext + wrapReplyText(text_elem.value);
+ var textarea = document.getElementById('comment');
+ if (textarea.value != commenttext) {
+ textarea.value += commenttext;
+ }
+ textarea.focus();
+ }
+ </script>
+ <div id="user_story_header">
+ <b>User Story</b>
+ [% IF can_edit_story %]
+ <span id="user_story_edit">
+ (<a href="javascript:void(0)" id="user_story_edit_action" >edit</a>)
+ </span>
+ [% END %]
+ [% IF user.id
+ && bug.cf_user_story != ""
+ && bug.check_can_change_field('longdesc', 0, 1) %]
+ <span id="user_story_comment">
+ [<a class="bz_reply_link" href="#user_story_comment"
+ onclick="userStoryComment(); return false;"
+ >comment</a>]
+ </span>
+ [% END %]
+ </div>
+
+ [% IF bug.cf_user_story != "" %]
+ <div id="user_story_readonly" class="bz_comment">
+ <pre class="bz_comment_text">
+ [%- bug.cf_user_story FILTER quoteUrls(bug) -%]
+ </pre>
+ </div>
+ [% ELSE %]
+ <br id="user_story_readonly">
+ [% END %]
+
+ [% IF can_edit_story %]
+ <div id="user_story_edit_container" class="bz_default_hidden">
+ [% INCLUDE global/textarea.html.tmpl
+ name = 'cf_user_story'
+ id = 'user_story'
+ minrows = 10
+ maxrows = 10
+ cols = constants.COMMENT_COLS
+ defaultcontent = bug.cf_user_story %]
+ </div>
+ <script type="text/javascript">
+ YAHOO.util.Event.addListener('user_story_edit_action', 'click', function() {
+ YAHOO.util.Dom.addClass('user_story_edit', 'bz_default_hidden');
+ YAHOO.util.Dom.addClass('user_story_readonly', 'bz_default_hidden');
+ YAHOO.util.Dom.removeClass('user_story_edit_container', 'bz_default_hidden');
+ YAHOO.util.Dom.get('user_story').focus();
+ });
+ </script>
+ [% END %]
+</div>
diff --git a/extensions/UserStory/template/en/default/hook/bug/create/create-after_custom_fields.html.tmpl b/extensions/UserStory/template/en/default/hook/bug/create/create-after_custom_fields.html.tmpl
new file mode 100644
index 000000000..04c7f3c04
--- /dev/null
+++ b/extensions/UserStory/template/en/default/hook/bug/create/create-after_custom_fields.html.tmpl
@@ -0,0 +1,84 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS default.user_story_visible.0 && default.check_can_change_field('cf_user_story', 0, 1) %]
+
+</tbody>
+<tbody id="cf_user_story_container" class="expert_fields bz_default_hidden">
+ <tr>
+ <th class="field_label">
+ <label for="cf_user_story">User Story:</label>
+ </th>
+ <td colspan="3">
+ <div id="user_story_header">
+ <span id="user_story_edit">
+ (<a href="javascript:void(0)" id="user_story_edit_action" >edit</a>)
+ </span>
+ </div>
+ <div id="user_story_edit_container" class="bz_default_hidden">
+ [% user_story_default = cloned_bug ? cloned_bug.cf_user_story : "" %]
+ [% INCLUDE global/textarea.html.tmpl
+ name = 'cf_user_story'
+ id = 'user_story'
+ minrows = 10
+ maxrows = 10
+ cols = constants.COMMENT_COLS
+ disabled = 1
+ defaultcontent = user_story_default
+ %]
+ </div>
+ <script type="text/javascript">
+ var user_story_exclude_components = [];
+ [% FOREACH c = default.user_story_visible.1 %]
+ user_story_exclude_components.push('[% c FILTER js %]');
+ [% END %]
+ function toggleUserStory() {
+ if (YAHOO.util.Dom.get('user_story').value != '') {
+ hideUserStoryEdit();
+ }
+ if (user_story_exclude_components.length == 0) {
+ YAHOO.util.Dom.removeClass('cf_user_story_container', 'bz_default_hidden');
+ YAHOO.util.Dom.get('user_story').disabled = false;
+ return;
+ }
+ var index = -1;
+ var form = document.Create;
+ if (form.component.type == 'select-one') {
+ index = form.component.selectedIndex;
+ } else if (form.component.type == 'hidden') {
+ // Assume there is only one component in the list
+ index = 0;
+ }
+ if (index != -1) {
+ for (var i = 0, l = user_story_exclude_components.length; i < l; i++) {
+ if (user_story_exclude_components[i] == components[index]) {
+ YAHOO.util.Dom.addClass('cf_user_story_container', 'bz_default_hidden');
+ YAHOO.util.Dom.get('user_story').disabled = true;
+ return;
+ }
+ else {
+ YAHOO.util.Dom.removeClass('cf_user_story_container', 'bz_default_hidden');
+ YAHOO.util.Dom.get('user_story').disabled = false;
+ }
+ }
+ }
+ }
+ function hideUserStoryEdit() {
+ YAHOO.util.Dom.addClass('user_story_edit', 'bz_default_hidden');
+ YAHOO.util.Dom.addClass('user_story_readonly', 'bz_default_hidden');
+ YAHOO.util.Dom.removeClass('user_story_edit_container', 'bz_default_hidden');
+ }
+ YAHOO.util.Event.addListener('component', 'change', toggleUserStory);
+ YAHOO.util.Event.addListener('user_story_edit_action', 'click', function() {
+ hideUserStoryEdit();
+ YAHOO.util.Dom.get('user_story').focus();
+ });
+ toggleUserStory();
+ </script>
+ </td>
+ </tr>
diff --git a/extensions/UserStory/template/en/default/hook/bug/create/create-custom_field.html.tmpl b/extensions/UserStory/template/en/default/hook/bug/create/create-custom_field.html.tmpl
new file mode 100644
index 000000000..4d809e4a2
--- /dev/null
+++ b/extensions/UserStory/template/en/default/hook/bug/create/create-custom_field.html.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%# user story gets custom handling %]
+[% IF field.name == 'cf_user_story' %]
+ [% field.hidden = 1 %]
+[% END %]
diff --git a/extensions/UserStory/template/en/default/hook/bug/edit-custom_field.html.tmpl b/extensions/UserStory/template/en/default/hook/bug/edit-custom_field.html.tmpl
new file mode 100644
index 000000000..2e8762dbe
--- /dev/null
+++ b/extensions/UserStory/template/en/default/hook/bug/edit-custom_field.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF field.name == 'cf_user_story' %]
+ [% field.hidden = 1 %]
+[% END %]
diff --git a/extensions/UserStory/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/UserStory/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..abdbe865e
--- /dev/null
+++ b/extensions/UserStory/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% style_urls.push("extensions/UserStory/web/style/user_story.css") %]
diff --git a/extensions/UserStory/web/style/user_story.css b/extensions/UserStory/web/style/user_story.css
new file mode 100644
index 000000000..f1a457f75
--- /dev/null
+++ b/extensions/UserStory/web/style/user_story.css
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+.user_story {
+ width: 50em;
+}
+
+.skin-Mozilla .user_story {
+ width: 65em;
+}
+
+textarea#user_story {
+ width: 100%;
+}
+
+#user_story_comment {
+ float: right;
+}
+
+#user_story_readonly {
+ border: 1px solid black;
+ border-radius: 4px;
+}
+
+.skin-standard #user_story_readonly {
+ padding: 2px;
+}
+
+.skin-Mozilla #user_story_readonly {
+ border: none;
+ border-radius: 0px;
+}
+
+.skin-Mozilla #user_story_readonly .bz_comment_text {
+ border: 1px solid darkgrey;
+ border-radius: 4px;
+}
diff --git a/extensions/Voting/Extension.pm b/extensions/Voting/Extension.pm
index a5a3bc11b..0b79d3d21 100644
--- a/extensions/Voting/Extension.pm
+++ b/extensions/Voting/Extension.pm
@@ -38,7 +38,7 @@ use Bugzilla::User;
use Bugzilla::Util qw(detaint_natural);
use Bugzilla::Token;
-use List::Util qw(min);
+use List::Util qw(min sum);
use constant VERSION => BUGZILLA_VERSION;
use constant DEFAULT_VOTES_PER_BUG => 1;
@@ -47,6 +47,10 @@ use constant DEFAULT_VOTES_PER_BUG => 1;
use constant CMT_POPULAR_VOTES => 3;
use constant REL_VOTER => 4;
+BEGIN {
+ *Bugzilla::Bug::user_votes = \&_bug_user_votes;
+}
+
################
# Installation #
################
@@ -122,6 +126,15 @@ sub install_update_db {
# Objects #
###########
+sub _bug_user_votes {
+ my ($self) = @_;
+ return $self->{'user_votes'} if exists $self->{'user_votes'};
+ $self->{'user_votes'} = Bugzilla->dbh->selectrow_array(
+ "SELECT vote_count FROM votes WHERE bug_id = ? AND who = ?",
+ undef, $self->id, Bugzilla->user->id);
+ return $self->{'user_votes'};
+}
+
sub object_columns {
my ($self, $args) = @_;
my ($class, $columns) = @$args{qw(class columns)};
@@ -204,7 +217,7 @@ sub bug_end_of_update {
# If some votes have been removed, RemoveVotes() returns
# a list of messages to send to voters.
@msgs = _remove_votes($bug->id, 0, 'votes_bug_moved');
- _confirm_if_vote_confirmed($bug->id);
+ _confirm_if_vote_confirmed($bug);
foreach my $msg (@msgs) {
MessageToMTA($msg);
@@ -406,9 +419,10 @@ sub _page_user {
}
# If a bug_id is given, and we're editing, we'll add it to the votes list.
-
+
my $bug_id = $input->{bug_id};
- my $bug = Bugzilla::Bug->check($bug_id) if $bug_id;
+ $bug_id = $bug_id->[0] if ref($bug_id) eq 'ARRAY';
+ my $bug = Bugzilla::Bug->check({ id => $bug_id, cache => 1 }) if $bug_id;
my $who_id = $input->{user_id} || $user->id;
# Logged-out users must specify a user_id.
@@ -437,52 +451,38 @@ sub _page_user {
foreach my $product (@{ $user->get_selectable_products }) {
next unless ($product->{votesperuser} > 0);
- my @bugs;
- my @bug_ids;
- my $total = 0;
- my $onevoteonly = 0;
-
my $vote_list =
- $dbh->selectall_arrayref('SELECT votes.bug_id, votes.vote_count,
- bugs.short_desc
- FROM votes
- INNER JOIN bugs
- ON votes.bug_id = bugs.bug_id
- WHERE votes.who = ?
- AND bugs.product_id = ?
- ORDER BY votes.bug_id',
- undef, ($who->id, $product->id));
-
- foreach (@$vote_list) {
- my ($id, $count, $summary) = @$_;
- $total += $count;
-
- # Next if user can't see this bug. So, the totals will be correct
- # and they can see there are votes 'missing', but not on what bug
- # they are. This seems a reasonable compromise; the alternative is
- # to lie in the totals.
- next if !$user->can_see_bug($id);
-
- push (@bugs, { id => $id,
- summary => $summary,
- count => $count });
- push (@bug_ids, $id);
- push (@all_bug_ids, $id);
- }
+ $dbh->selectall_arrayref('SELECT votes.bug_id, votes.vote_count
+ FROM votes
+ INNER JOIN bugs
+ ON votes.bug_id = bugs.bug_id
+ WHERE votes.who = ?
+ AND bugs.product_id = ?',
+ undef, ($who->id, $product->id));
+
+ my %votes = map { $_->[0] => $_->[1] } @$vote_list;
+ my @bug_ids = sort keys %votes;
+ # Exclude bugs that the user can no longer see.
+ @bug_ids = @{ $user->visible_bugs(\@bug_ids) };
+ next unless scalar @bug_ids;
+
+ push(@all_bug_ids, @bug_ids);
+ my @bugs = @{ Bugzilla::Bug->new_from_list(\@bug_ids) };
+ $_->{count} = $votes{$_->id} foreach @bugs;
+ # We include votes from bugs that the user can no longer see.
+ my $total = sum(values %votes) || 0;
+ my $onevoteonly = 0;
$onevoteonly = 1 if (min($product->{votesperuser},
$product->{maxvotesperbug}) == 1);
- # Only add the product for display if there are any bugs in it.
- if ($#bugs > -1) {
- push (@products, { name => $product->name,
- bugs => \@bugs,
- bug_ids => \@bug_ids,
- onevoteonly => $onevoteonly,
- total => $total,
- maxvotes => $product->{votesperuser},
- maxperbug => $product->{maxvotesperbug} });
- }
+ push(@products, { name => $product->name,
+ bugs => \@bugs,
+ bug_ids => \@bug_ids,
+ onevoteonly => $onevoteonly,
+ total => $total,
+ maxvotes => $product->{votesperuser},
+ maxperbug => $product->{maxvotesperbug} });
}
if ($canedit && $bug) {
@@ -516,6 +516,7 @@ sub _update_votes {
# IDs and the field values are the number of votes.
my @buglist = grep {/^\d+$/} keys %$input;
+ my (%bugs, %votes);
# If no bugs are in the buglist, let's make sure the user gets notified
# that their votes will get nuked if they continue.
@@ -531,20 +532,23 @@ sub _update_votes {
exit;
}
}
+ else {
+ $user->visible_bugs(\@buglist);
+ my $bugs_obj = Bugzilla::Bug->new_from_list(\@buglist);
+ $bugs{$_->id} = $_ foreach @$bugs_obj;
+ }
- # Call check() on each bug ID to make sure it is a positive
- # integer representing an existing bug that the user is authorized
- # to access, and make sure the number of votes submitted is also
- # a non-negative integer (a series of digits not preceded by a
- # minus sign).
- my (%votes, @bugs);
+ # Call check_is_visible() on each bug to make sure it is an existing bug
+ # that the user is authorized to access, and make sure the number of votes
+ # submitted is also an integer.
foreach my $id (@buglist) {
- my $bug = Bugzilla::Bug->check($id);
- push(@bugs, $bug);
- $id = $bug->id;
- $votes{$id} = $input->{$id};
- detaint_natural($votes{$id})
- || ThrowUserError("voting_must_be_nonnegative");
+ my $bug = $bugs{$id}
+ or ThrowUserError('bug_id_does_not_exist', { bug_id => $id });
+ $bug->check_is_visible;
+ $id = $bug->id;
+ $votes{$id} = $input->{$id};
+ detaint_natural($votes{$id})
+ || ThrowUserError("voting_must_be_nonnegative");
}
my $token = $cgi->param('token');
@@ -557,10 +561,10 @@ sub _update_votes {
# If the user is voting for bugs, make sure they aren't overstuffing
# the ballot box.
- if (scalar @bugs) {
+ if (scalar @buglist) {
my (%prodcount, %products);
- foreach my $bug (@bugs) {
- my $bug_id = $bug->id;
+ foreach my $bug_id (keys %bugs) {
+ my $bug = $bugs{$bug_id};
my $prod = $bug->product;
$products{$prod} ||= $bug->product_obj;
$prodcount{$prod} ||= 0;
@@ -584,56 +588,65 @@ sub _update_votes {
}
}
- # Update the user's votes in the database. If the user did not submit
- # any votes, they may be using a form with checkboxes to remove all their
- # votes (checkboxes are not submitted along with other form data when
- # they are not checked, and Bugzilla uses them to represent single votes
- # for products that only allow one vote per bug). In that case, we still
- # need to clear the user's votes from the database.
- my %affected;
+ # Update the user's votes in the database.
$dbh->bz_start_transaction();
- # Take note of, and delete the user's old votes from the database.
- my $bug_list = $dbh->selectcol_arrayref('SELECT bug_id FROM votes
+ my $old_list = $dbh->selectall_arrayref('SELECT bug_id, vote_count FROM votes
WHERE who = ?', undef, $who);
- foreach my $id (@$bug_list) {
- $affected{$id} = 1;
- }
- $dbh->do('DELETE FROM votes WHERE who = ?', undef, $who);
+ my %old_votes = map { $_->[0] => $_->[1] } @$old_list;
my $sth_insertVotes = $dbh->prepare('INSERT INTO votes (who, bug_id, vote_count)
VALUES (?, ?, ?)');
+ my $sth_updateVotes = $dbh->prepare('UPDATE votes SET vote_count = ?
+ WHERE bug_id = ? AND who = ?');
- # Insert the new values in their place
- foreach my $id (@buglist) {
- if ($votes{$id} > 0) {
+ my %affected = map { $_ => 1 } (@buglist, keys %old_votes);
+ my @deleted_votes;
+
+ foreach my $id (keys %affected) {
+ if (!$votes{$id}) {
+ push(@deleted_votes, $id);
+ next;
+ }
+ if ($votes{$id} == ($old_votes{$id} || 0)) {
+ delete $affected{$id};
+ next;
+ }
+ # We use 'defined' in case 0 was accidentally stored in the DB.
+ if (defined $old_votes{$id}) {
+ $sth_updateVotes->execute($votes{$id}, $id, $who);
+ }
+ else {
$sth_insertVotes->execute($who, $id, $votes{$id});
}
- $affected{$id} = 1;
+ }
+
+ if (@deleted_votes) {
+ $dbh->do('DELETE FROM votes WHERE who = ? AND ' .
+ $dbh->sql_in('bug_id', \@deleted_votes), undef, $who);
}
# Update the cached values in the bugs table
- print $cgi->header();
my @updated_bugs = ();
my $sth_getVotes = $dbh->prepare("SELECT SUM(vote_count) FROM votes
WHERE bug_id = ?");
- my $sth_updateVotes = $dbh->prepare("UPDATE bugs SET votes = ?
- WHERE bug_id = ?");
+ $sth_updateVotes = $dbh->prepare('UPDATE bugs SET votes = ? WHERE bug_id = ?');
foreach my $id (keys %affected) {
$sth_getVotes->execute($id);
my $v = $sth_getVotes->fetchrow_array || 0;
$sth_updateVotes->execute($v, $id);
- my $confirmed = _confirm_if_vote_confirmed($id);
+ my $confirmed = _confirm_if_vote_confirmed($bugs{$id} || $id);
push (@updated_bugs, $id) if $confirmed;
}
$dbh->bz_commit_transaction();
+ print $cgi->header() if scalar @updated_bugs;
$vars->{'type'} = "votes";
$vars->{'title_tag'} = 'change_votes';
foreach my $bug_id (@updated_bugs) {
@@ -844,7 +857,7 @@ sub _remove_votes {
# confirm a bug has been reduced, check if the bug is now confirmed.
sub _confirm_if_vote_confirmed {
my $id = shift;
- my $bug = new Bugzilla::Bug($id);
+ my $bug = ref $id ? $id : new Bugzilla::Bug({ id => $id, cache => 1 });
my $ret = 0;
if (!$bug->everconfirmed
diff --git a/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl b/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl
index f73ffaebd..b57a5cb27 100644
--- a/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl
+++ b/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl
@@ -29,6 +29,9 @@
[% ELSE %]
votes
[% END %]</a>
+ [% IF bug.user_votes %]
+ including you
+ [% END %]
[% END %]
(<a href="page.cgi?id=voting/user.html&amp;bug_id=
[%- bug.id FILTER uri %]#vote_
diff --git a/extensions/Voting/template/en/default/pages/voting/user.html.tmpl b/extensions/Voting/template/en/default/pages/voting/user.html.tmpl
index 61eaf8491..627011fd4 100644
--- a/extensions/Voting/template/en/default/pages/voting/user.html.tmpl
+++ b/extensions/Voting/template/en/default/pages/voting/user.html.tmpl
@@ -109,8 +109,7 @@
</tr>
[% FOREACH bug = product.bugs %]
- <tr [% IF bug.id == this_bug.id && canedit %]
- class="bz_bug_being_voted_on" [% END %]>
+ <tr [% IF bug.id == this_bug.id && canedit %] class="bz_bug_being_voted_on"[% END %]>
<td>
[% IF bug.id == this_bug.id && canedit %]
[% IF product.onevoteonly %]
@@ -120,25 +119,25 @@
[% END %]
[%- END %]
</td>
- <td align="right"><a name="vote_[% bug.id FILTER html %]">
+ <td align="right"><a name="vote_[% bug.id FILTER none %]">
[% IF canedit %]
[% IF product.onevoteonly %]
- <input type="checkbox" name="[% bug.id FILTER html %]" value="1"
- [% " checked" IF bug.count %] id="bug_[% bug.id FILTER html %]">
+ <input type="checkbox" name="[% bug.id FILTER none %]" value="1"
+ [% " checked" IF bug.count %] id="bug_[% bug.id FILTER none %]">
[% ELSE %]
- <input name="[% bug.id FILTER html %]" value="[% bug.count FILTER html %]"
- size="2" id="bug_[% bug.id FILTER html %]">
+ <input name="[% bug.id FILTER none %]" value="[% bug.count FILTER html %]"
+ size="2" id="bug_[% bug.id FILTER none %]">
[% END %]
[% ELSE %]
[% bug.count FILTER html %]
[% END %]
</a></td>
<td align="center">
- [% bug.id FILTER bug_link(bug) FILTER none %]
+ [% PROCESS bug/link.html.tmpl bug = bug, link_text = bug.id %]
</td>
<td>
- [% bug.summary FILTER html %]
- (<a href="page.cgi?id=voting/bug.html&amp;bug_id=[% bug.id FILTER uri %]">Show Votes</a>)
+ [% bug.short_desc FILTER html %]
+ (<a href="page.cgi?id=voting/bug.html&amp;bug_id=[% bug.id FILTER none %]">Show Votes</a>)
</td>
</tr>
[% END %]
diff --git a/extensions/ZPushNotify/Config.pm b/extensions/ZPushNotify/Config.pm
new file mode 100644
index 000000000..e65169d41
--- /dev/null
+++ b/extensions/ZPushNotify/Config.pm
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ZPushNotify;
+use strict;
+
+use constant NAME => 'ZPushNotify';
+use constant REQUIRED_MODULES => [];
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/ZPushNotify/Extension.pm b/extensions/ZPushNotify/Extension.pm
new file mode 100644
index 000000000..6e8ab4d27
--- /dev/null
+++ b/extensions/ZPushNotify/Extension.pm
@@ -0,0 +1,110 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ZPushNotify;
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+our $VERSION = '1';
+
+use Bugzilla;
+
+#
+# insert into the notifications table
+#
+
+sub _notify {
+ my ($bug_id, $delta_ts) = @_;
+ Bugzilla->dbh->do(
+ "REPLACE INTO push_notify(bug_id, delta_ts) VALUES(?, ?)",
+ undef,
+ $bug_id, $delta_ts
+ );
+}
+
+#
+# object hooks
+#
+
+sub object_end_of_update {
+ my ($self, $args) = @_;
+ return unless Bugzilla->params->{enable_simple_push};
+ return unless scalar keys %{ $args->{changes} };
+ return unless my $object = $args->{object};
+ if ($object->isa('Bugzilla::Attachment')) {
+ _notify($object->bug->id, $object->bug->delta_ts);
+ }
+}
+
+sub object_before_delete {
+ my ($self, $args) = @_;
+ return unless Bugzilla->params->{enable_simple_push};
+ return unless my $object = $args->{object};
+ if ($object->isa('Bugzilla::Attachment')) {
+ _notify($object->bug->id, $object->bug->delta_ts);
+ }
+}
+
+sub bug_end_of_update_delta_ts {
+ my ($self, $args) = @_;
+ return unless Bugzilla->params->{enable_simple_push};
+ _notify($args->{bug_id}, $args->{timestamp});
+}
+
+sub bug_end_of_create {
+ my ($self, $args) = @_;
+ return unless Bugzilla->params->{enable_simple_push};
+ _notify($args->{bug}->id, $args->{timestamp});
+}
+
+#
+# schema / param
+#
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'push_notify'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'INTSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ bug_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE'
+ },
+ },
+ delta_ts => {
+ TYPE => 'DATETIME',
+ NOTNULL => 1,
+ },
+ ],
+ INDEXES => [
+ push_notify_idx => {
+ FIELDS => [ 'bug_id' ],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+}
+
+sub config_modify_panels {
+ my ($self, $args) = @_;
+ push @{ $args->{panels}->{advanced}->{params} }, {
+ name => 'enable_simple_push',
+ type => 'b',
+ default => 0,
+ };
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/ZPushNotify/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl b/extensions/ZPushNotify/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl
new file mode 100644
index 000000000..e6c171916
--- /dev/null
+++ b/extensions/ZPushNotify/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF panel.name == "advanced" %]
+ [% panel.param_descs.enable_simple_push =
+ 'When enabled bug changes will result in an entry in the <code>push_notify</code> table'
+ %]
+[% END -%]
diff --git a/images/buggie.png b/images/buggie.png
new file mode 100644
index 000000000..454a080a8
--- /dev/null
+++ b/images/buggie.png
Binary files differ
diff --git a/importxml.pl b/importxml.pl
index 50f639fc5..19be9a61a 100755
--- a/importxml.pl
+++ b/importxml.pl
@@ -1039,6 +1039,15 @@ sub process_bug {
push(@query, $custom_field);
push(@values, $value);
}
+ } elsif ($field->type == FIELD_TYPE_DATE) {
+ eval { $value = Bugzilla::Bug->_check_date_field($value); };
+ if ($@) {
+ $err .= "Skipping illegal value \"$value\" in $custom_field.\n" ;
+ }
+ else {
+ push(@query, $custom_field);
+ push(@values, $value);
+ }
} else {
$err .= "Type of custom field $custom_field is an unhandled FIELD_TYPE: " .
$field->type . "\n";
@@ -1208,7 +1217,7 @@ sub process_bug {
$c->{isprivate}, $c->{thetext}, 0);
}
$sth_comment->execute($id, $exporterid, $timestamp, 0, $comments, $worktime);
- Bugzilla::Bug->new($id)->_sync_fulltext('new_bug');
+ Bugzilla::Bug->new($id)->_sync_fulltext( new_bug => 1);
# Add this bug to each group of which its product is a member.
my $sth_group = $dbh->prepare("INSERT INTO bug_group_map (bug_id, group_id)
diff --git a/jobqueue.pl b/jobqueue.pl
index 775fe8dd6..d7d6fd775 100755
--- a/jobqueue.pl
+++ b/jobqueue.pl
@@ -59,7 +59,8 @@ jobqueue.pl - Runs jobs in the background for Bugzilla.
restart Stops a running jobqueue if one is running, and then
starts a new one.
once Checks the job queue once, executes the first item found (if
- any) and then exits
+ any, up to a limit of 1000 items) and then exits
+ onepass Checks the job queue, executes all items found, and then exits
check Report the current status of the daemon.
install On some *nix systems, this automatically installs and
configures jobqueue.pl as a system service so that it will
diff --git a/js/TUI.js b/js/TUI.js
index 34a79dc16..2dee8ab2e 100644
--- a/js/TUI.js
+++ b/js/TUI.js
@@ -69,8 +69,14 @@ function TUI_hide_default(className) {
function _TUI_toggle_control_link(className) {
var link = document.getElementById(className + "_controller");
if (!link) return;
- var original_text = link.innerHTML;
- link.innerHTML = TUI_alternates[className];
+ var original_text;
+ if (link.nodeName == 'INPUT') {
+ original_text = link.value;
+ link.value = TUI_alternates[className];
+ } else {
+ original_text = link.innerHTML;
+ link.innerHTML = TUI_alternates[className];
+ }
TUI_alternates[className] = original_text;
}
diff --git a/js/attachment.js b/js/attachment.js
index 7861164b1..f967f64d3 100644
--- a/js/attachment.js
+++ b/js/attachment.js
@@ -40,13 +40,12 @@ function updateCommentPrivacy(checkbox) {
}
}
-function setContentTypeDisabledState(form)
-{
+function setContentTypeDisabledState(form) {
var isdisabled = false;
if (form.ispatch.checked)
isdisabled = true;
- for (var i=0 ; i<form.contenttypemethod.length ; i++)
+ for (var i = 0; i < form.contenttypemethod.length; i++)
form.contenttypemethod[i].disabled = isdisabled;
form.contenttypeselection.disabled = isdisabled;
@@ -55,9 +54,8 @@ function setContentTypeDisabledState(form)
function TextFieldHandler() {
var field_text = document.getElementById("attach_text");
- var greyfields = new Array("data", "ispatch", "autodetect",
- "list", "manual", "contenttypeselection",
- "contenttypeentry");
+ var greyfields = new Array("data", "autodetect", "list", "manual",
+ "contenttypeselection", "contenttypeentry");
var i, thisfield;
if (field_text.value.match(/^\s*$/)) {
for (i = 0; i < greyfields.length; i++) {
diff --git a/js/bug.js b/js/bug.js
index 06ef03da1..9237f7241 100644
--- a/js/bug.js
+++ b/js/bug.js
@@ -129,3 +129,49 @@ YAHOO.bugzilla.dupTable = {
[dt, data.product_name]);
}
};
+
+(function(){
+ 'use strict';
+ var JSON = YAHOO.lang.JSON;
+
+ YAHOO.bugzilla.bugUserLastVisit = {
+ update: function(bug_ids) {
+ var args = JSON.stringify({
+ version: "1.1",
+ method: 'BugUserLastVisit.update',
+ params: { ids: bug_ids },
+ });
+ var callbacks = {
+ failure: function(res) {
+ if (console)
+ console.log("failed to update last visited: "
+ + res.responseText);
+ },
+ };
+
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ YAHOO.util.Connect.asyncRequest('POST', 'jsonrpc.cgi', callbacks,
+ args)
+ },
+
+ get: function(done) {
+ var args = JSON.stringify({
+ version: "1.1",
+ method: 'BugUserLastVisit.get',
+ params: { },
+ });
+ var callbacks = {
+ success: function(res) { done(JSON.parse(res.responseText)) },
+ failure: function(res) {
+ if (console)
+ console.log("failed to get last visited: "
+ + res.responseText);
+ },
+ };
+
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ YAHOO.util.Connect.asyncRequest('POST', 'jsonrpc.cgi', callbacks,
+ args)
+ },
+ };
+})();
diff --git a/js/comment-tagging.js b/js/comment-tagging.js
new file mode 100644
index 000000000..c110eb00e
--- /dev/null
+++ b/js/comment-tagging.js
@@ -0,0 +1,389 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+var Dom = YAHOO.util.Dom;
+
+YAHOO.bugzilla.commentTagging = {
+ ctag_div : false,
+ ctag_add : false,
+ counter : 0,
+ min_len : 3,
+ max_len : 24,
+ tags_by_no: {},
+ nos_by_tag: {},
+ current_id: 0,
+ current_no: -1,
+ can_edit : false,
+ pending : {},
+
+ label : '',
+ min_len_error: '',
+ max_len_error: '',
+
+ init : function(can_edit) {
+ this.can_edit = can_edit;
+ this.ctag_div = Dom.get('bz_ctag_div');
+ this.ctag_add = Dom.get('bz_ctag_add');
+ YAHOO.util.Event.on(this.ctag_add, 'keypress', this.onKeyPress);
+ YAHOO.util.Event.onDOMReady(function() {
+ YAHOO.bugzilla.commentTagging.updateCollapseControls();
+ });
+ if (!can_edit) return;
+
+ var ds = new YAHOO.util.XHRDataSource("jsonrpc.cgi");
+ ds.connTimeout = 30000;
+ ds.connMethodPost = true;
+ ds.connXhrMode = "cancelStaleRequests";
+ ds.maxCacheEntries = 5;
+ ds.responseSchema = {
+ metaFields : { error: "error", jsonRpcId: "id"},
+ resultsList : "result"
+ };
+
+ var ac = new YAHOO.widget.AutoComplete('bz_ctag_add', 'bz_ctag_autocomp', ds);
+ ac.maxResultsDisplayed = 7;
+ ac.generateRequest = function(query) {
+ query = YAHOO.lang.trim(query);
+ YAHOO.bugzilla.commentTagging.last_query = query;
+ YAHOO.bugzilla.commentTagging.counter = YAHOO.bugzilla.commentTagging.counter + 1;
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ return YAHOO.lang.JSON.stringify({
+ method : "Bug.search_comment_tags",
+ id : YAHOO.bugzilla.commentTagging.counter,
+ params : [ { query : query, limit : 10 } ]
+ });
+ };
+ ac.minQueryLength = this.min_len;
+ ac.autoHighlight = false;
+ ac.typeAhead = true;
+ ac.queryDelay = 0.5;
+ ac.dataReturnEvent.subscribe(function(type, args) {
+ args[0].autoHighlight = args[2].length == 1;
+ });
+ },
+
+ toggle : function(comment_id, comment_no) {
+ if (!this.ctag_div) return;
+ var tags_container = Dom.get('ct_' + comment_no);
+
+ if (this.current_id == comment_id) {
+ // hide
+ this.current_id = 0;
+ this.current_no = -1;
+ Dom.addClass(this.ctag_div, 'bz_default_hidden');
+ this.hideError();
+ window.focus();
+
+ } else {
+ // show or move
+ this.rpcRefresh(comment_id, comment_no);
+ this.current_id = comment_id;
+ this.current_no = comment_no;
+ this.ctag_add.value = '';
+ tags_container.parentNode.insertBefore(this.ctag_div, tags_container);
+ Dom.removeClass(this.ctag_div, 'bz_default_hidden');
+ Dom.removeClass(tags_container.parentNode, 'bz_default_hidden');
+ var comment = Dom.get('comment_text_' + comment_no);
+ if (Dom.hasClass(comment, 'collapsed')) {
+ var link = Dom.get('comment_link_' + comment_no);
+ expand_comment(link, comment, comment_no);
+ }
+ window.setTimeout(function() {
+ YAHOO.bugzilla.commentTagging.ctag_add.focus();
+ }, 50);
+ }
+ },
+
+ hideInput : function() {
+ if (this.current_id != 0) {
+ var comment_no = this.current_no;
+ this.toggle(this.current_id, this.current_no);
+ this.hideEmpty(comment_no);
+ }
+ this.hideError();
+ },
+
+ hideEmpty : function(comment_no) {
+ if (Dom.get('ct_' + comment_no).children.length == 0) {
+ Dom.addClass('comment_tag_' + comment_no, 'bz_default_hidden');
+ }
+ },
+
+ showError : function(comment_id, comment_no, error) {
+ var bz_ctag_error = Dom.get('bz_ctag_error');
+ var tags_container = Dom.get('ct_' + comment_no);
+ tags_container.parentNode.appendChild(bz_ctag_error, tags_container);
+ Dom.get('bz_ctag_error_msg').innerHTML = YAHOO.lang.escapeHTML(error);
+ Dom.removeClass(bz_ctag_error, 'bz_default_hidden');
+ },
+
+ hideError : function() {
+ Dom.addClass('bz_ctag_error', 'bz_default_hidden');
+ },
+
+ onKeyPress : function(evt) {
+ evt = evt || window.event;
+ var charCode = evt.charCode || evt.keyCode;
+ if (evt.keyCode == 27) {
+ // escape
+ YAHOO.bugzilla.commentTagging.hideInput();
+ YAHOO.util.Event.stopEvent(evt);
+
+ } else if (evt.keyCode == 13) {
+ // return
+ YAHOO.util.Event.stopEvent(evt);
+ var tags = YAHOO.bugzilla.commentTagging.ctag_add.value.split(/[ ,]/);
+ var comment_id = YAHOO.bugzilla.commentTagging.current_id;
+ var comment_no = YAHOO.bugzilla.commentTagging.current_no;
+ try {
+ YAHOO.bugzilla.commentTagging.add(comment_id, comment_no, tags);
+ YAHOO.bugzilla.commentTagging.hideInput();
+ } catch(e) {
+ YAHOO.bugzilla.commentTagging.showError(comment_id, comment_no, e.message);
+ }
+ }
+ },
+
+ showTags : function(comment_id, comment_no, tags) {
+ // remove existing tags
+ var tags_container = Dom.get('ct_' + comment_no);
+ while (tags_container.hasChildNodes()) {
+ tags_container.removeChild(tags_container.lastChild);
+ }
+ // add tags
+ if (tags != '') {
+ if (typeof(tags) == 'string') {
+ tags = tags.split(',');
+ }
+ for (var i = 0, l = tags.length; i < l; i++) {
+ tags_container.appendChild(this.buildTagHtml(comment_id, comment_no, tags[i]));
+ }
+ }
+ // update tracking array
+ this.tags_by_no['c' + comment_no] = tags;
+ this.updateCollapseControls();
+ },
+
+ updateCollapseControls : function() {
+ var container = Dom.get('comment_tags_collapse_expand_container');
+ if (!container) return;
+ // build list of tags
+ this.nos_by_tag = {};
+ for (var id in this.tags_by_no) {
+ if (this.tags_by_no.hasOwnProperty(id)) {
+ for (var i = 0, l = this.tags_by_no[id].length; i < l; i++) {
+ var tag = this.tags_by_no[id][i].toLowerCase();
+ if (!this.nos_by_tag.hasOwnProperty(tag)) {
+ this.nos_by_tag[tag] = [];
+ }
+ this.nos_by_tag[tag].push(id);
+ }
+ }
+ }
+ var tags = [];
+ for (var tag in this.nos_by_tag) {
+ if (this.nos_by_tag.hasOwnProperty(tag)) {
+ tags.push(tag);
+ }
+ }
+ tags.sort();
+ if (tags.length) {
+ var div = document.createElement('div');
+ div.appendChild(document.createTextNode(this.label));
+ var ul = document.createElement('ul');
+ ul.id = 'comment_tags_collapse_expand';
+ div.appendChild(ul);
+ for (var i = 0, l = tags.length; i < l; i++) {
+ var tag = tags[i];
+ var li = document.createElement('li');
+ ul.appendChild(li);
+ var a = document.createElement('a');
+ li.appendChild(a);
+ Dom.setAttribute(a, 'href', '#');
+ YAHOO.util.Event.addListener(a, 'click', function(evt, tag) {
+ YAHOO.bugzilla.commentTagging.toggleCollapse(tag);
+ YAHOO.util.Event.stopEvent(evt);
+ }, tag);
+ li.appendChild(document.createTextNode(' (' + this.nos_by_tag[tag].length + ')'));
+ a.innerHTML = tag;
+ }
+ while (container.hasChildNodes()) {
+ container.removeChild(container.lastChild);
+ }
+ container.appendChild(div);
+ } else {
+ while (container.hasChildNodes()) {
+ container.removeChild(container.lastChild);
+ }
+ }
+ },
+
+ toggleCollapse : function(tag) {
+ var nos = this.nos_by_tag[tag];
+ if (!nos) return;
+ toggle_all_comments('collapse');
+ for (var i = 0, l = nos.length; i < l; i++) {
+ var comment_no = nos[i].match(/\d+$/)[0];
+ var comment = Dom.get('comment_text_' + comment_no);
+ var link = Dom.get('comment_link_' + comment_no);
+ expand_comment(link, comment, comment_no);
+ }
+ },
+
+ buildTagHtml : function(comment_id, comment_no, tag) {
+ var el = document.createElement('span');
+ Dom.setAttribute(el, 'id', 'ct_' + comment_no + '_' + tag);
+ Dom.addClass(el, 'bz_comment_tag');
+ if (this.can_edit) {
+ var a = document.createElement('a');
+ Dom.setAttribute(a, 'href', '#');
+ YAHOO.util.Event.addListener(a, 'click', function(evt, args) {
+ YAHOO.bugzilla.commentTagging.remove(args[0], args[1], args[2])
+ YAHOO.util.Event.stopEvent(evt);
+ }, [comment_id, comment_no, tag]);
+ a.appendChild(document.createTextNode('x'));
+ el.appendChild(a);
+ el.appendChild(document.createTextNode("\u00a0"));
+ }
+ el.appendChild(document.createTextNode(tag));
+ return el;
+ },
+
+ add : function(comment_id, comment_no, add_tags) {
+ // build list of current tags from html
+ var tags = new Array();
+ var spans = Dom.getElementsByClassName('bz_comment_tag', undefined, 'ct_' + comment_no);
+ for (var i = 0, l = spans.length; i < l; i++) {
+ tags.push(spans[i].textContent.substr(2));
+ }
+ // add new tags
+ var new_tags = new Array();
+ for (var i = 0, l = add_tags.length; i < l; i++) {
+ var tag = YAHOO.lang.trim(add_tags[i]);
+ // validation
+ if (tag == '')
+ continue;
+ if (tag.length < YAHOO.bugzilla.commentTagging.min_len)
+ throw new Error(this.min_len_error)
+ if (tag.length > YAHOO.bugzilla.commentTagging.max_len)
+ throw new Error(this.max_len_error)
+ // append new tag
+ if (bz_isValueInArrayIgnoreCase(tags, tag))
+ continue;
+ new_tags.push(tag);
+ tags.push(tag);
+ }
+ tags.sort();
+ // update
+ this.showTags(comment_id, comment_no, tags);
+ this.rpcUpdate(comment_id, comment_no, new_tags, undefined);
+ },
+
+ remove : function(comment_id, comment_no, tag) {
+ var el = Dom.get('ct_' + comment_no + '_' + tag);
+ if (el) {
+ el.parentNode.removeChild(el);
+ this.rpcUpdate(comment_id, comment_no, undefined, [ tag ]);
+ this.hideEmpty(comment_no);
+ }
+ },
+
+ // If multiple updates are triggered quickly, overlapping refresh events
+ // are generated. We ignore all events except the last one.
+ incPending : function(comment_id) {
+ if (this.pending['c' + comment_id] == undefined) {
+ this.pending['c' + comment_id] = 1;
+ } else {
+ this.pending['c' + comment_id]++;
+ }
+ },
+
+ decPending : function(comment_id) {
+ if (this.pending['c' + comment_id] != undefined)
+ this.pending['c' + comment_id]--;
+ },
+
+ hasPending : function(comment_id) {
+ return this.pending['c' + comment_id] != undefined
+ && this.pending['c' + comment_id] > 0;
+ },
+
+ rpcRefresh : function(comment_id, comment_no, noRefreshOnError) {
+ this.incPending(comment_id);
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ YAHOO.util.Connect.asyncRequest('POST', 'jsonrpc.cgi',
+ {
+ success: function(res) {
+ YAHOO.bugzilla.commentTagging.decPending(comment_id);
+ data = YAHOO.lang.JSON.parse(res.responseText);
+ if (data.error) {
+ YAHOO.bugzilla.commentTagging.handleRpcError(
+ comment_id, comment_no, data.error.message, noRefreshOnError);
+ return;
+ }
+
+ if (!YAHOO.bugzilla.commentTagging.hasPending(comment_id))
+ YAHOO.bugzilla.commentTagging.showTags(
+ comment_id, comment_no, data.result.comments[comment_id].tags);
+ },
+ failure: function(res) {
+ YAHOO.bugzilla.commentTagging.decPending(comment_id);
+ YAHOO.bugzilla.commentTagging.handleRpcError(
+ comment_id, comment_no, res.responseText, noRefreshOnError);
+ }
+ },
+ YAHOO.lang.JSON.stringify({
+ version: "1.1",
+ method: 'Bug.comments',
+ params: {
+ comment_ids: [ comment_id ],
+ include_fields: [ 'tags' ]
+ }
+ })
+ );
+ },
+
+ rpcUpdate : function(comment_id, comment_no, add, remove) {
+ this.incPending(comment_id);
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ YAHOO.util.Connect.asyncRequest('POST', 'jsonrpc.cgi',
+ {
+ success: function(res) {
+ YAHOO.bugzilla.commentTagging.decPending(comment_id);
+ data = YAHOO.lang.JSON.parse(res.responseText);
+ if (data.error) {
+ YAHOO.bugzilla.commentTagging.handleRpcError(comment_id, comment_no, data.error.message);
+ return;
+ }
+
+ if (!YAHOO.bugzilla.commentTagging.hasPending(comment_id))
+ YAHOO.bugzilla.commentTagging.showTags(comment_id, comment_no, data.result);
+ },
+ failure: function(res) {
+ YAHOO.bugzilla.commentTagging.decPending(comment_id);
+ YAHOO.bugzilla.commentTagging.handleRpcError(comment_id, comment_no, res.responseText);
+ }
+ },
+ YAHOO.lang.JSON.stringify({
+ version: "1.1",
+ method: 'Bug.update_comment_tags',
+ params: {
+ comment_id: comment_id,
+ add: add,
+ remove: remove
+ }
+ })
+ );
+ },
+
+ handleRpcError : function(comment_id, comment_no, message, noRefreshOnError) {
+ YAHOO.bugzilla.commentTagging.showError(comment_id, comment_no, message);
+ if (!noRefreshOnError) {
+ YAHOO.bugzilla.commentTagging.rpcRefresh(comment_id, comment_no, true);
+ }
+ }
+}
diff --git a/js/comments.js b/js/comments.js
index e7163a0fd..12bc00d46 100644
--- a/js/comments.js
+++ b/js/comments.js
@@ -38,11 +38,11 @@ function updateCommentPrivacy(checkbox, id) {
function toggle_comment_display(link, comment_id) {
var comment = document.getElementById('comment_text_' + comment_id);
- var re = new RegExp(/\bcollapsed\b/);
- if (comment.className.match(re))
- expand_comment(link, comment);
- else
- collapse_comment(link, comment);
+ if (YAHOO.util.Dom.hasClass(comment, 'collapsed')) {
+ expand_comment(link, comment, comment_id);
+ } else {
+ collapse_comment(link, comment, comment_id);
+ }
}
function toggle_all_comments(action) {
@@ -55,24 +55,31 @@ function toggle_all_comments(action) {
var comment = comments[i];
if (!comment)
continue;
-
- var id = comments[i].id.match(/\d*$/);
+ var id = comment.id.match(/^comment_text_(\d*)$/);
+ if (!id)
+ continue;
+ id = id[1];
var link = document.getElementById('comment_link_' + id);
- if (action == 'collapse')
- collapse_comment(link, comment);
- else
- expand_comment(link, comment);
+ if (action == 'collapse') {
+ collapse_comment(link, comment, id);
+ } else {
+ expand_comment(link, comment, id);
+ }
}
}
-function collapse_comment(link, comment) {
+function collapse_comment(link, comment, comment_id) {
link.innerHTML = "[+]";
YAHOO.util.Dom.addClass(comment, 'collapsed');
+ YAHOO.util.Dom.addClass('comment_tag_' + comment_id, 'collapsed');
}
-function expand_comment(link, comment) {
+function expand_comment(link, comment, comment_id) {
link.innerHTML = "[-]";
+ YAHOO.util.Dom.addClass('cr' + comment_id, 'collapsed');
+ YAHOO.util.Dom.removeClass('c' + comment_id, 'bz_default_collapsed');
YAHOO.util.Dom.removeClass(comment, 'collapsed');
+ YAHOO.util.Dom.removeClass('comment_tag_' + comment_id, 'collapsed');
}
function wrapReplyText(text) {
@@ -125,11 +132,12 @@ function wrapReplyText(text) {
/* This way, we are sure that browsers which do not support JS
* won't display this link */
-function addCollapseLink(count, title) {
+function addCollapseLink(count, collapsed, title) {
document.write(' <a href="#" class="bz_collapse_comment"' +
' id="comment_link_' + count +
'" onclick="toggle_comment_display(this, ' + count +
- '); return false;" title="' + title + '">[-]<\/a> ');
+ '); return false;" title="' + title + '">[' +
+ (collapsed ? '+' : '&minus;') + ']<\/a> ');
}
function goto_add_comments( anchor ){
@@ -143,3 +151,30 @@ function goto_add_comments( anchor ){
},10);
return false;
}
+
+if (typeof Node == 'undefined') {
+ /* MSIE doesn't define Node, so provide a compatibility object */
+ window.Node = {
+ TEXT_NODE: 3,
+ ENTITY_REFERENCE_NODE: 5
+ };
+}
+
+/* Concatenates all text from element's childNodes. This is used
+ * instead of innerHTML because we want the actual text (and
+ * innerText is non-standard).
+ */
+function getText(element) {
+ var child, text = "";
+ for (var i=0; i < element.childNodes.length; i++) {
+ child = element.childNodes[i];
+ var type = child.nodeType;
+ if (type == Node.TEXT_NODE || type == Node.ENTITY_REFERENCE_NODE) {
+ text += child.nodeValue;
+ } else {
+ /* recurse into nodes of other types */
+ text += getText(child);
+ }
+ }
+ return text;
+}
diff --git a/js/create_bug.js b/js/create_bug.js
new file mode 100644
index 000000000..62d24a642
--- /dev/null
+++ b/js/create_bug.js
@@ -0,0 +1,116 @@
+function toggleAdvancedFields() {
+ TUI_toggle_class('expert_fields');
+ var elements = YAHOO.util.Dom.getElementsByClassName('expert_fields');
+ if (YAHOO.util.Dom.hasClass(elements[0], TUI_HIDDEN_CLASS)) {
+ handleWantsBugFlags(false);
+ }
+}
+
+function handleWantsBugFlags(wants) {
+ if (wants) {
+ hideElementById('bug_flags_false');
+ showElementById('bug_flags_true');
+ }
+ else {
+ showElementById('bug_flags_false');
+ hideElementById('bug_flags_true');
+ clearBugFlagFields();
+ }
+}
+
+function clearBugFlagFields() {
+ var flags_table;
+ flags_table = document.getElementById('bug_flags');
+ if (flags_table) {
+ var selects = flags_table.getElementsByTagName('select');
+ for (var i = 0, il = selects.length; i < il; i++) {
+ if (selects[i].value != 'X') {
+ selects[i].value = 'X';
+ toggleRequesteeField(selects[i]);
+ }
+ }
+ }
+ flags_table = document.getElementById('bug_tracking_flags');
+ if (flags_table) {
+ var selects = flags_table.getElementsByTagName('select');
+ for (var i = 0, il = selects.length; i < il; i++) {
+ selects[i].value = '---';
+ }
+ }
+}
+
+YAHOO.util.Event.onDOMReady(function() {
+ function set_width(id, width) {
+ var el = document.getElementById(id);
+ if (!el) return;
+ el.style.width = width + 'px';
+ }
+
+ // force field widths
+
+ var width = document.getElementById('short_desc').clientWidth + 'px';
+ var el;
+
+ el = document.getElementById('comment');
+ el.style.width = width;
+
+ el = document.getElementById('cf_crash_signature');
+ if (el) el.style.width = width;
+
+ // show the bug flags if a flag is set
+
+ var flag_set = false;
+ var flags_table;
+ flags_table = document.getElementById('bug_flags');
+ if (flags_table) {
+ var selects = flags_table.getElementsByTagName('select');
+ for (var i = 0, il = selects.length; i < il; i++) {
+ if (selects[i].value != 'X') {
+ flag_set = true;
+ break;
+ }
+ }
+ }
+ if (!flag_set) {
+ flags_table = document.getElementById('bug_tracking_flags');
+ if (flags_table) {
+ var selects = flags_table.getElementsByTagName('select');
+ for (var i = 0, il = selects.length; i < il; i++) {
+ if (selects[i].value != '---') {
+ flag_set = true;
+ break;
+ }
+ }
+ }
+ }
+
+ if (flag_set) {
+ hideElementById('bug_flags_false');
+ showElementById('bug_flags_true');
+ } else {
+ hideElementById('bug_flags_true');
+ showElementById('bug_flags_false');
+ }
+ showElementById('btn_no_bug_flags')
+});
+
+function take_bug(user) {
+ var el = Dom.get('assigned_to');
+ el.value = user;
+ el.focus();
+ el.select();
+ assignee_change(user);
+ return false;
+}
+
+function assignee_change(user) {
+ var el = Dom.get('take_bug');
+ if (!el) return;
+ el.style.display = Dom.get('assigned_to').value == user ? 'none' : '';
+}
+
+function init_take_handler(user) {
+ YAHOO.util.Event.addListener(
+ 'assigned_to', 'change', function() { assignee_change(user); });
+ assignee_change(user);
+}
diff --git a/js/custom-search.js b/js/custom-search.js
index 73897035d..e5c172d3b 100644
--- a/js/custom-search.js
+++ b/js/custom-search.js
@@ -40,7 +40,7 @@ function custom_search_new_row() {
var row = document.getElementById('custom_search_last_row');
var clone = row.cloneNode(true);
- _cs_fix_ids(clone);
+ _cs_fix_row_ids(clone);
// We only want one copy of the buttons, in the new row. So the old
// ones get deleted.
@@ -55,13 +55,28 @@ function custom_search_new_row() {
// Always make sure there's only one row with this id.
row.id = null;
row.parentNode.appendChild(clone);
+ cs_reconfigure(row);
fix_query_string(row);
return clone;
}
+var _cs_source_any_all;
function custom_search_open_paren() {
var row = document.getElementById('custom_search_last_row');
+ // create a copy of j_top and use that as the source, so we can modify
+ // j_top if required
+ if (!_cs_source_any_all) {
+ var j_top = document.getElementById('j_top');
+ _cs_source_any_all = j_top.cloneNode(true);
+ }
+
+ // find the parent any/all select, and remove the grouped option
+ var structure = _cs_build_structure(row);
+ var old_id = _cs_get_row_id(row);
+ var parent_j = document.getElementById(_cs_get_join(structure, 'f' + old_id));
+ _cs_remove_and_g(parent_j);
+
// If there's an "Any/All" select in this row, it needs to stay as
// part of the parent paren set.
var old_any_all = _remove_any_all(row);
@@ -78,21 +93,20 @@ function custom_search_open_paren() {
var not_for_paren = new_not[0].cloneNode(true);
// Preserve the values when modifying the row.
- var id = _cs_fix_ids(row, true);
+ var id = _cs_fix_row_ids(row, true);
var prev_id = id - 1;
var paren_row = row.cloneNode(false);
paren_row.id = null;
paren_row.innerHTML = '(<input type="hidden" name="f' + prev_id
- + '" value="OP">';
+ + '" id="f' + prev_id + '" value="OP">';
paren_row.insertBefore(not_for_paren, paren_row.firstChild);
row.parentNode.insertBefore(paren_row, row);
-
+
// New paren set needs a new "Any/All" select.
var any_all_container = document.createElement('div');
YAHOO.util.Dom.addClass(any_all_container, ANY_ALL_SELECT_CLASS);
- var j_top = document.getElementById('j_top');
- var any_all = j_top.cloneNode(true);
+ var any_all = _cs_source_any_all.cloneNode(true);
any_all.name = 'j' + prev_id;
any_all.id = any_all.name;
any_all_container.appendChild(any_all);
@@ -104,6 +118,7 @@ function custom_search_open_paren() {
YAHOO.util.Dom.setStyle(row, 'margin-left', new_margin + 'em');
YAHOO.util.Dom.removeClass('cp_container', 'bz_default_hidden');
+ cs_reconfigure(any_all_container);
fix_query_string(any_all_container);
}
@@ -112,7 +127,7 @@ function custom_search_close_paren() {
// We need to up the new row's id by one more, because we're going
// to insert a "CP" before it.
- var id = _cs_fix_ids(new_row);
+ var id = _cs_fix_row_ids(new_row);
var margin = YAHOO.util.Dom.getStyle(new_row, 'margin-left');
var int_match = margin.match(/\d+/);
@@ -122,7 +137,7 @@ function custom_search_close_paren() {
var paren_row = new_row.cloneNode(false);
paren_row.id = null;
paren_row.innerHTML = ')<input type="hidden" name="f' + (id - 1)
- + '" value="CP">';
+ + '" id="f' + (id - 1) + '" value="CP">';
new_row.parentNode.insertBefore(paren_row, new_row);
@@ -130,6 +145,7 @@ function custom_search_close_paren() {
YAHOO.util.Dom.addClass('cp_container', 'bz_default_hidden');
}
+ cs_reconfigure(new_row);
fix_query_string(new_row);
}
@@ -172,19 +188,17 @@ function redirect_html4_browsers() {
document.location = url;
}
-function _cs_fix_ids(parent, preserve_values) {
+function _cs_fix_row_ids(row, preserve_values) {
// Update the label of the checkbox.
- var label = YAHOO.util.Dom.getElementBy(function() { return true },
- 'label', parent);
+ var label = YAHOO.util.Dom.getElementBy(function() { return true }, 'label', row);
var id_match = label.htmlFor.match(/\d+$/);
var id = parseInt(id_match[0]) + 1;
label.htmlFor = label.htmlFor.replace(/\d+$/, id);
- // Sets all the inputs in the parent back to their default
+ // Sets all the inputs in the row back to their default
// and fixes their id.
var fields =
- YAHOO.util.Dom.getElementsByClassName('custom_search_form_field', null,
- parent);
+ YAHOO.util.Dom.getElementsByClassName('custom_search_form_field', null, row);
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
@@ -196,15 +210,141 @@ function _cs_fix_ids(parent, preserve_values) {
field.value = '';
}
}
-
- // Update the numeric id for the new row.
+
+ // Update the numeric id for the row.
field.name = field.name.replace(/\d+$/, id);
field.id = field.name;
}
-
+
return id;
}
+function _cs_build_structure(form_member) {
+ // build a map of the structure of the custom fields
+ var form = YAHOO.util.Dom.getAncestorByTagName(form_member, 'form');
+ var last_id = _get_last_cs_row_id(form);
+ var structure = [ 'j_top' ];
+ var nested = [ structure ];
+ for (var id = 1; id <= last_id; id++) {
+ var f = form['f' + id];
+ if (!f || !f.parentNode.parentNode) continue;
+
+ if (f.value == 'OP') {
+ var j = [ 'j' + id ];
+ nested[nested.length - 1].push(j);
+ nested.push(j);
+ continue;
+ } else if (f.value == 'CP') {
+ nested.pop();
+ continue;
+ } else {
+ nested[nested.length - 1].push('f' + id);
+ }
+ }
+ return structure;
+}
+
+function cs_reconfigure(form_member) {
+ var structure = _cs_build_structure(form_member);
+ _cs_add_listeners(structure);
+ _cs_trigger_j_listeners(structure);
+ fix_query_string(form_member);
+
+ var j = _cs_get_join(structure, 'f' + _get_last_cs_row_id());
+ document.getElementById('op_button').disabled = document.getElementById(j).value == 'AND_G';
+}
+
+function _cs_add_listeners(parents) {
+ for (var i = 0, l = parents.length; i < l; i++) {
+ if (typeof(parents[i]) == 'object') {
+ // nested
+ _cs_add_listeners(parents[i]);
+ } else if (i == 0) {
+ // joiner
+ YAHOO.util.Event.removeListener(parents[i], 'change', _cs_j_change);
+ YAHOO.util.Event.addListener(parents[i], 'change', _cs_j_change, parents);
+ } else {
+ // field
+ YAHOO.util.Event.removeListener(parents[i], 'change', _cs_f_change);
+ YAHOO.util.Event.addListener(parents[i], 'change', _cs_f_change, parents);
+ }
+ }
+}
+
+function _cs_trigger_j_listeners(fields) {
+ var has_children = false;
+ for (var i = 0, l = fields.length; i < l; i++) {
+ if (typeof(fields[i]) == 'undefined') {
+ continue;
+ } else if (typeof(fields[i]) == 'object') {
+ // nested
+ _cs_trigger_j_listeners(fields[i]);
+ has_children = true;
+ } else if (i == 0) {
+ _cs_j_change(undefined, fields);
+ }
+ }
+ if (has_children) {
+ _cs_remove_and_g(document.getElementById(fields[0]));
+ }
+}
+
+function _cs_get_join(parents, field) {
+ for (var i = 0, l = parents.length; i < l; i++) {
+ if (typeof(parents[i]) == 'object') {
+ // nested
+ var result = _cs_get_join(parents[i], field);
+ if (result) return result;
+ } else if (parents[i] == field) {
+ return parents[0];
+ }
+ }
+ return false;
+}
+
+function _cs_remove_and_g(join_field) {
+ var index = bz_optionIndex(join_field, 'AND_G');
+ join_field.options[index] = null;
+ join_field.options[bz_optionIndex(join_field, 'AND')].innerHTML = cs_and_label;
+ join_field.options[bz_optionIndex(join_field, 'OR')].innerHTML = cs_or_label;
+}
+
+function _cs_j_change(evt, fields, field) {
+ var j = document.getElementById(fields[0]);
+ if (j && j.value == 'AND_G') {
+ for (var i = 1, l = fields.length; i < l; i++) {
+ if (typeof(fields[i]) == 'object') continue;
+ if (!field) {
+ field = document.getElementById(fields[i]).value;
+ } else {
+ document.getElementById(fields[i]).value = field;
+ }
+ }
+ if (evt) {
+ fix_query_string(j);
+ }
+ if ('f' + _get_last_cs_row_id() == fields[fields.length - 1]) {
+ document.getElementById('op_button').style.display = 'none';
+ }
+ } else {
+ document.getElementById('op_button').style.display = '';
+ }
+}
+
+function _cs_f_change(evt, args) {
+ var field = YAHOO.util.Event.getTarget(evt);
+ _cs_j_change(evt, args, field.value);
+}
+
+function _get_last_cs_row_id() {
+ return _cs_get_row_id('custom_search_last_row');
+}
+
+function _cs_get_row_id(row) {
+ var label = YAHOO.util.Dom.getElementBy(function() { return true }, 'label', row);
+ return parseInt(label.htmlFor.match(/\d+$/)[0]);
+}
+
function _remove_any_all(parent) {
var any_all = YAHOO.util.Dom.getElementsByClassName(
ANY_ALL_SELECT_CLASS, null, parent);
diff --git a/js/field.js b/js/field.js
index 07433b2a5..286390ed1 100644
--- a/js/field.js
+++ b/js/field.js
@@ -219,14 +219,14 @@ function setupEditLink(id) {
}
/* Hide input/select fields and show the text with (edit) next to it */
-function hideEditableField( container, input, action, field_id, original_value, new_value ) {
+function hideEditableField( container, input, action, field_id, original_value, new_value, hide_input ) {
YAHOO.util.Dom.removeClass(container, 'bz_default_hidden');
YAHOO.util.Dom.addClass(input, 'bz_default_hidden');
YAHOO.util.Event.addListener(action, 'click', showEditableField,
new Array(container, input, field_id, new_value));
if(field_id != ""){
YAHOO.util.Event.addListener(window, 'load', checkForChangedFieldValues,
- new Array(container, input, field_id, original_value));
+ new Array(container, input, field_id, original_value, hide_input ));
}
}
@@ -255,6 +255,8 @@ function showEditableField (e, ContainerInputArray) {
inputs.push(document.getElementById(ContainerInputArray[2]));
} else {
inputs = inputArea.getElementsByTagName('input');
+ if ( inputs.length == 0 )
+ inputs = inputArea.getElementsByTagName('textarea');
}
if ( inputs.length > 0 ) {
// Change the first field's value to ContainerInputArray[2]
@@ -274,7 +276,7 @@ function showEditableField (e, ContainerInputArray) {
}
// focus on the first field, this makes it easier to edit
inputs[0].focus();
- if ( type == "input" ) {
+ if ( type == "input" || type == "textarea" ) {
inputs[0].select();
}
}
@@ -288,7 +290,7 @@ function showEditableField (e, ContainerInputArray) {
*
* var e: the event
* var ContainerInputArray: An array containing the (edit) and text area and the input being displayed
- * var ContainerInputArray[0]: the conainer that will be hidden usually shows the (edit) text
+ * var ContainerInputArray[0]: the container that will be hidden usually shows the (edit) text
* var ContainerInputArray[1]: the input area and label that will be displayed
* var ContainerInputArray[2]: the field that is on the page, might get changed by browser autocomplete
* var ContainerInputArray[3]: the original value from the page loading.
@@ -298,8 +300,11 @@ function checkForChangedFieldValues(e, ContainerInputArray ) {
var el = document.getElementById(ContainerInputArray[2]);
var unhide = false;
if ( el ) {
- if ( el.value != ContainerInputArray[3] ||
- ( el.value == "" && el.id != "alias") ) {
+ if ( !ContainerInputArray[4]
+ && (el.value != ContainerInputArray[3]
+ || (el.value == "" && el.id != "alias" && el.id != "qa_contact" && el.id != "bug_mentors")) )
+ {
+
unhide = true;
}
else {
@@ -308,7 +313,7 @@ function checkForChangedFieldValues(e, ContainerInputArray ) {
if ( set_default ) {
if(set_default.checked){
unhide = true;
- }
+ }
}
}
}
@@ -341,13 +346,19 @@ function showPeopleOnChange( field_id_list ) {
}
}
-function assignToDefaultOnChange(field_id_list) {
- showPeopleOnChange( field_id_list );
- for(var i = 0; i < field_id_list.length; i++) {
- YAHOO.util.Event.addListener( field_id_list[i],'change', setDefaultCheckbox,
- 'set_default_assignee');
- YAHOO.util.Event.addListener( field_id_list[i],'change',setDefaultCheckbox,
- 'set_default_qa_contact');
+function assignToDefaultOnChange(field_id_list, default_assignee, default_qa_contact) {
+ showPeopleOnChange(field_id_list);
+ for(var i = 0, l = field_id_list.length; i < l; i++) {
+ YAHOO.util.Event.addListener(field_id_list[i], 'change', function(evt, defaults) {
+ if (document.getElementById('assigned_to').value == defaults[0]) {
+ setDefaultCheckbox(evt, 'set_default_assignee');
+ }
+ if (document.getElementById('qa_contact')
+ && document.getElementById('qa_contact').value == defaults[1])
+ {
+ setDefaultCheckbox(evt, 'set_default_qa_contact');
+ }
+ }, [default_assignee, default_qa_contact]);
}
}
@@ -444,7 +455,7 @@ function setResolutionToDuplicate(e, duplicate_or_move_bug_status) {
YAHOO.util.Event.preventDefault(e);
}
-function setDefaultCheckbox(e, field_id ) {
+function setDefaultCheckbox(e, field_id) {
var el = document.getElementById(field_id);
var elLabel = document.getElementById(field_id + "_label");
if( el && elLabel ) {
@@ -714,9 +725,11 @@ YAHOO.bugzilla.userAutocomplete = {
},
debug_helper : function ( ){
/* used to help debug any errors that might happen */
+ /*
if( typeof(console) !== 'undefined' && console != null && arguments.length > 0 ){
console.log("debug helper info:", arguments);
}
+ */
return true;
},
init_ds : function(){
@@ -792,3 +805,138 @@ YAHOO.bugzilla.keywordAutocomplete = {
});
}
};
+
+/**
+ * Force the browser to honour the selected option when a page is refreshed,
+ * but only if the user hasn't explicitly selected a different option.
+ */
+function initDirtyFieldTracking() {
+ // old IE versions don't provide the information we need to make this fix work
+ // however they aren't affected by this issue, so it's ok to ignore them
+ if (YAHOO.env.ua.ie > 0 && YAHOO.env.ua.ie <= 8) return;
+ var selects = document.getElementById('changeform').getElementsByTagName('select');
+ for (var i = 0, l = selects.length; i < l; i++) {
+ var el = selects[i];
+ var el_dirty = document.getElementById(el.name + '_dirty');
+ if (!el_dirty) continue;
+ if (!el_dirty.value) {
+ var preSelected = bz_preselectedOptions(el);
+ if (!el.multiple) {
+ preSelected.selected = true;
+ } else {
+ el.selectedIndex = -1;
+ for (var j = 0, m = preSelected.length; j < m; j++) {
+ preSelected[j].selected = true;
+ }
+ }
+ }
+ YAHOO.util.Event.on(el, "change", function(e) {
+ var el = e.target || e.srcElement;
+ var preSelected = bz_preselectedOptions(el);
+ var currentSelected = bz_selectedOptions(el);
+ var isDirty = false;
+ if (!el.multiple) {
+ isDirty = preSelected.index != currentSelected.index;
+ } else {
+ if (preSelected.length != currentSelected.length) {
+ isDirty = true;
+ } else {
+ for (var i = 0, l = preSelected.length; i < l; i++) {
+ if (currentSelected[i].index != preSelected[i].index) {
+ isDirty = true;
+ break;
+ }
+ }
+ }
+ }
+ document.getElementById(el.name + '_dirty').value = isDirty ? '1' : '';
+ });
+ }
+}
+
+/**
+ * Comment preview
+ */
+
+var last_comment_text = '';
+
+function show_comment_preview(bug_id) {
+ var Dom = YAHOO.util.Dom;
+ var comment = document.getElementById('comment');
+ var preview = document.getElementById('comment_preview');
+
+ if (!comment || !preview) return;
+ if (Dom.hasClass('comment_preview_tab', 'active_comment_tab')) return;
+
+ preview.style.width = (comment.clientWidth - 4) + 'px';
+ preview.style.height = comment.offsetHeight + 'px';
+
+ var comment_tab = document.getElementById('comment_tab');
+ Dom.addClass(comment, 'bz_default_hidden');
+ Dom.removeClass(comment_tab, 'active_comment_tab');
+ comment_tab.setAttribute('aria-selected', 'false');
+
+ var preview_tab = document.getElementById('comment_preview_tab');
+ Dom.removeClass(preview, 'bz_default_hidden');
+ Dom.addClass(preview_tab, 'active_comment_tab');
+ preview_tab.setAttribute('aria-selected', 'true');
+
+ Dom.addClass('comment_preview_error', 'bz_default_hidden');
+
+ if (last_comment_text == comment.value)
+ return;
+
+ Dom.addClass('comment_preview_text', 'bz_default_hidden');
+ Dom.removeClass('comment_preview_loading', 'bz_default_hidden');
+
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ YAHOO.util.Connect.asyncRequest('POST', 'jsonrpc.cgi',
+ {
+ success: function(res) {
+ data = YAHOO.lang.JSON.parse(res.responseText);
+ if (data.error) {
+ Dom.addClass('comment_preview_loading', 'bz_default_hidden');
+ Dom.removeClass('comment_preview_error', 'bz_default_hidden');
+ Dom.get('comment_preview_error').innerHTML =
+ YAHOO.lang.escapeHTML(data.error.message);
+ } else {
+ document.getElementById('comment_preview_text').innerHTML = data.result.html;
+ Dom.addClass('comment_preview_loading', 'bz_default_hidden');
+ Dom.removeClass('comment_preview_text', 'bz_default_hidden');
+ last_comment_text = comment.value;
+ }
+ },
+ failure: function(res) {
+ Dom.addClass('comment_preview_loading', 'bz_default_hidden');
+ Dom.removeClass('comment_preview_error', 'bz_default_hidden');
+ Dom.get('comment_preview_error').innerHTML =
+ YAHOO.lang.escapeHTML(res.responseText);
+ }
+ },
+ YAHOO.lang.JSON.stringify({
+ version: "1.1",
+ method: 'Bug.render_comment',
+ params: {
+ id: bug_id,
+ text: comment.value
+ }
+ })
+ );
+}
+
+function show_comment_edit() {
+ var comment = document.getElementById('comment');
+ var preview = document.getElementById('comment_preview');
+ if (!comment || !preview) return;
+ if (YAHOO.util.Dom.hasClass(comment, 'active_comment_tab')) return;
+
+ var preview_tab = document.getElementById('comment_preview_tab');
+ YAHOO.util.Dom.addClass(preview, 'bz_default_hidden');
+ YAHOO.util.Dom.removeClass(preview_tab, 'active_comment_tab');
+ preview_tab.setAttribute('aria-selected', 'false');
+
+ var comment_tab = document.getElementById('comment_tab');
+ YAHOO.util.Dom.removeClass(comment, 'bz_default_hidden');
+ YAHOO.util.Dom.addClass(comment_tab, 'active_comment_tab');
+ comment_tab.setAttribute('aria-selected', 'true');
+}
diff --git a/js/global.js b/js/global.js
index b62d7b9a7..1aac910c3 100644
--- a/js/global.js
+++ b/js/global.js
@@ -16,8 +16,6 @@
*
*/
-var mini_login_constants;
-
function show_mini_login_form( suffix ) {
var login_link = document.getElementById('login_link' + suffix);
var login_form = document.getElementById('mini_login' + suffix);
@@ -67,13 +65,10 @@ function hide_forgot_form( suffix ) {
function init_mini_login_form( suffix ) {
var mini_login = document.getElementById('Bugzilla_login' + suffix );
var mini_password = document.getElementById('Bugzilla_password' + suffix );
- var mini_dummy = document.getElementById(
- 'Bugzilla_password_dummy' + suffix);
+ var mini_dummy = document.getElementById('Bugzilla_password_dummy' + suffix);
// If the login and password are blank when the page loads, we display
// "login" and "password" in the boxes by default.
if (mini_login.value == "" && mini_password.value == "") {
- mini_login.value = mini_login_constants.login;
- YAHOO.util.Dom.addClass(mini_login, "bz_mini_login_help");
YAHOO.util.Dom.addClass(mini_password, 'bz_default_hidden');
YAHOO.util.Dom.removeClass(mini_dummy, 'bz_default_hidden');
}
@@ -82,33 +77,15 @@ function init_mini_login_form( suffix ) {
}
}
-// Clear the words "login" and "password" from the form when you click
-// in one of the boxes. We clear them both when you click in either box
-// so that the browser's password-autocomplete can work.
-function mini_login_on_focus( suffix ) {
- var mini_login = document.getElementById('Bugzilla_login' + suffix );
- var mini_password = document.getElementById('Bugzilla_password' + suffix );
- var mini_dummy = document.getElementById(
- 'Bugzilla_password_dummy' + suffix);
-
- YAHOO.util.Dom.removeClass(mini_login, "bz_mini_login_help");
- if (mini_login.value == mini_login_constants.login) {
- mini_login.value = '';
- }
- YAHOO.util.Dom.removeClass(mini_password, 'bz_default_hidden');
- YAHOO.util.Dom.addClass(mini_dummy, 'bz_default_hidden');
-}
-
function check_mini_login_fields( suffix ) {
var mini_login = document.getElementById('Bugzilla_login' + suffix );
var mini_password = document.getElementById('Bugzilla_password' + suffix );
- if( (mini_login.value != "" && mini_password.value != "")
- && mini_login.value != mini_login_constants.login )
- {
- return true;
+ if (mini_login.value != "" && mini_password.value != "") {
+ return true;
+ } else {
+ window.alert("You must provide the email address and password before logging in.");
+ return false;
}
- window.alert( mini_login_constants.warning );
- return false;
}
function set_language( value ) {
diff --git a/js/instant-search.js b/js/instant-search.js
new file mode 100644
index 000000000..a3f051f2f
--- /dev/null
+++ b/js/instant-search.js
@@ -0,0 +1,201 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+var Dom = YAHOO.util.Dom;
+var Event = YAHOO.util.Event;
+
+Event.onDOMReady(function() {
+ YAHOO.bugzilla.instantSearch.onInit();
+ if (YAHOO.bugzilla.instantSearch.getContent().length >= 4) {
+ YAHOO.bugzilla.instantSearch.doSearch(YAHOO.bugzilla.instantSearch.getContent());
+ } else {
+ YAHOO.bugzilla.instantSearch.reset();
+ }
+ Dom.get('content').focus();
+});
+
+YAHOO.bugzilla.instantSearch = {
+ counter: 0,
+ dataTable: null,
+ dataTableColumns: null,
+ elContent: null,
+ elList: null,
+ currentSearchQuery: '',
+ currentSearchProduct: '',
+
+ onInit: function() {
+ YAHOO.util.Connect.setDefaultPostHeader('application/json; charset=UTF-8');
+
+ this.elContent = Dom.get('content');
+ this.elList = Dom.get('results');
+
+ Event.addListener(this.elContent, 'keyup', this.onContentKeyUp);
+ Event.addListener(Dom.get('product'), 'change', this.onProductChange);
+ },
+
+ setLabels: function(labels) {
+ this.dataTableColumns = [
+ { key: "id", label: labels.id, formatter: this.formatId },
+ { key: "summary", label: labels.summary, formatter: "text" },
+ { key: "component", label: labels.component, formatter: "text" },
+ { key: "status", label: labels.status, formatter: this.formatStatus },
+ ];
+ },
+
+ initDataTable: function() {
+ var dataSource = new YAHOO.util.XHRDataSource("jsonrpc.cgi");
+ dataSource.connTimeout = 15000;
+ dataSource.connMethodPost = true;
+ dataSource.connXhrMode = "cancelStaleRequests";
+ dataSource.maxCacheEntries = 3;
+ dataSource.responseSchema = {
+ resultsList : "result.bugs",
+ metaFields : { error: "error", jsonRpcId: "id" }
+ };
+ // DataSource can't understand a JSON-RPC error response, so
+ // we have to modify the result data if we get one.
+ dataSource.doBeforeParseData =
+ function(oRequest, oFullResponse, oCallback) {
+ if (oFullResponse.error) {
+ oFullResponse.result = {};
+ oFullResponse.result.bugs = [];
+ if (console)
+ console.error("JSON-RPC error:", oFullResponse.error);
+ }
+ return oFullResponse;
+ };
+ dataSource.subscribe('dataErrorEvent',
+ function() {
+ YAHOO.bugzilla.instantSearch.currentSearchQuery = '';
+ }
+ );
+
+ this.dataTable = new YAHOO.widget.DataTable(
+ 'results',
+ this.dataTableColumns,
+ dataSource,
+ {
+ initialLoad: false,
+ MSG_EMPTY: 'No matching bugs found.',
+ MSG_ERROR: 'An error occurred while searching for bugs, please try again.'
+ }
+ );
+ },
+
+ formatId: function(el, oRecord, oColumn, oData) {
+ el.innerHTML = '<a href="show_bug.cgi?id=' + oData + '" target="_blank">' + oData + '</a>';
+ },
+
+ formatStatus: function(el, oRecord, oColumn, oData) {
+ var resolution = oRecord.getData('resolution');
+ var bugStatus = display_value('bug_status', oData);
+ if (resolution) {
+ el.innerHTML = bugStatus + ' ' + display_value('resolution', resolution);
+ } else {
+ el.innerHTML = bugStatus;
+ }
+ },
+
+ reset: function() {
+ Dom.addClass(this.elList, 'hidden');
+ this.elList.innerHTML = '';
+ this.currentSearchQuery = '';
+ this.currentSearchProduct = '';
+ },
+
+ onContentKeyUp: function(e) {
+ clearTimeout(YAHOO.bugzilla.instantSearch.lastTimeout);
+ YAHOO.bugzilla.instantSearch.lastTimeout = setTimeout(function() {
+ YAHOO.bugzilla.instantSearch.doSearch(YAHOO.bugzilla.instantSearch.getContent()) },
+ 600);
+ },
+
+ onProductChange: function(e) {
+ YAHOO.bugzilla.instantSearch.doSearch(YAHOO.bugzilla.instantSearch.getContent());
+ },
+
+ doSearch: function(query) {
+ if (query.length < 4)
+ return;
+
+ // don't query if we already have the results (or they are pending)
+ var product = Dom.get('product').value;
+ if (YAHOO.bugzilla.instantSearch.currentSearchQuery == query &&
+ YAHOO.bugzilla.instantSearch.currentSearchProduct == product)
+ return;
+ YAHOO.bugzilla.instantSearch.currentSearchQuery = query;
+ YAHOO.bugzilla.instantSearch.currentSearchProduct = product;
+
+ // initialise the datatable as late as possible
+ YAHOO.bugzilla.instantSearch.initDataTable();
+
+ try {
+ // run the search
+ Dom.removeClass(YAHOO.bugzilla.instantSearch.elList, 'hidden');
+
+ YAHOO.bugzilla.instantSearch.dataTable.showTableMessage(
+ 'Searching...&nbsp;&nbsp;&nbsp;' +
+ '<img src="extensions/GuidedBugEntry/web/images/throbber.gif"' +
+ ' width="16" height="11">',
+ YAHOO.widget.DataTable.CLASS_LOADING
+ );
+ var jsonObject = {
+ version: "1.1",
+ method: "Bug.possible_duplicates",
+ id: ++YAHOO.bugzilla.instantSearch.counter,
+ params: {
+ product: YAHOO.bugzilla.instantSearch.getProduct(),
+ summary: query,
+ limit: 20,
+ include_fields: [ "id", "summary", "status", "resolution", "component" ]
+ }
+ };
+
+ YAHOO.bugzilla.instantSearch.dataTable.getDataSource().sendRequest(
+ YAHOO.lang.JSON.stringify(jsonObject),
+ {
+ success: YAHOO.bugzilla.instantSearch.onSearchResults,
+ failure: YAHOO.bugzilla.instantSearch.onSearchResults,
+ scope: YAHOO.bugzilla.instantSearch.dataTable,
+ argument: YAHOO.bugzilla.instantSearch.dataTable.getState()
+ }
+ );
+
+ } catch(err) {
+ if (console)
+ console.error(err.message);
+ }
+ },
+
+ onSearchResults: function(sRequest, oResponse, oPayload) {
+ YAHOO.bugzilla.instantSearch.dataTable.onDataReturnInitializeTable(sRequest, oResponse, oPayload);
+ },
+
+ getContent: function() {
+ var content = YAHOO.lang.trim(this.elContent.value);
+ // work around chrome bug
+ if (content == YAHOO.bugzilla.instantSearch.elContent.getAttribute('placeholder')) {
+ return '';
+ } else {
+ return content;
+ }
+ },
+
+ getProduct: function() {
+ var result = [];
+ var name = Dom.get('product').value;
+ result.push(name);
+ if (products[name] && products[name].related) {
+ for (var i = 0, n = products[name].related.length; i < n; i++) {
+ result.push(products[name].related[i]);
+ }
+ }
+ return result;
+ }
+
+};
+
diff --git a/js/util.js b/js/util.js
index 6dcabbbc9..78b64b516 100644
--- a/js/util.js
+++ b/js/util.js
@@ -37,7 +37,8 @@ function bz_findPosX(obj)
if (obj.offsetParent) {
while (obj) {
- curleft += obj.offsetLeft;
+ if (getComputedStyle(obj).position != 'relative')
+ curleft += obj.offsetLeft;
obj = obj.offsetParent;
}
}
@@ -61,7 +62,8 @@ function bz_findPosY(obj)
if (obj.offsetParent) {
while (obj) {
- curtop += obj.offsetTop;
+ if (getComputedStyle(obj).position != 'relative')
+ curtop += obj.offsetTop;
obj = obj.offsetParent;
}
}
@@ -132,6 +134,7 @@ function bz_overlayBelow(item, parent) {
item.style.position = 'absolute';
item.style.left = elemX + "px";
item.style.top = elemY + elemH + 1 + "px";
+ item.style.zIndex = 999;
}
/**
@@ -143,10 +146,7 @@ function bz_overlayBelow(item, parent) {
*/
function bz_isValueInArray(aArray, aValue)
{
- var run = 0;
- var len = aArray.length;
-
- for ( ; run < len; run++) {
+ for (var run = 0, len = aArray.length ; run < len; run++) {
if (aArray[run] == aValue) {
return true;
}
@@ -156,6 +156,26 @@ function bz_isValueInArray(aArray, aValue)
}
/**
+ * Checks if a specified value is in the specified array by performing a
+ * case-insensitive comparison.
+ *
+ * @param aArray Array to search for the value.
+ * @param aValue Value to search from the array.
+ * @return Boolean; true if value is found in the array and false if not.
+ */
+function bz_isValueInArrayIgnoreCase(aArray, aValue)
+{
+ var re = new RegExp(aValue.replace(/([^A-Za-z0-9])/g, "\\$1"), 'i');
+ for (var run = 0, len = aArray.length ; run < len; run++) {
+ if (aArray[run].match(re)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
* Create wanted options in a select form control.
*
* @param aSelect Select form control to manipulate.
@@ -202,6 +222,55 @@ function bz_populateSelectFromArray(aSelect, aArray) {
}
/**
+ * Returns all Option elements that are selected in a <select>,
+ * as an array. Returns an empty array if nothing is selected.
+ *
+ * @param aSelect The select you want the selected values of.
+ */
+function bz_selectedOptions(aSelect) {
+ // HTML 5
+ if (aSelect.selectedOptions) {
+ return aSelect.selectedOptions;
+ }
+
+ var start_at = aSelect.selectedIndex;
+ if (start_at == -1) return [];
+ var first_selected = aSelect.options[start_at];
+ if (!aSelect.multiple) return first_selected;
+ // selectedIndex is specified as being the "first selected item",
+ // so we can start from there.
+ var selected = [first_selected];
+ var options_length = aSelect.options.length;
+ // We start after first_selected
+ for (var i = start_at + 1; i < options_length; i++) {
+ var this_option = aSelect.options[i];
+ if (this_option.selected) selected.push(this_option);
+ }
+ return selected;
+}
+
+/**
+ * Returns all Option elements that have the "selected" attribute, as an array.
+ * Returns an empty array if nothing is selected.
+ *
+ * @param aSelect The select you want the pre-selected values of.
+ */
+function bz_preselectedOptions(aSelect) {
+ var options = aSelect.options;
+ var selected = new Array();
+ for (var i = 0, l = options.length; i < l; i++) {
+ var attributes = options[i].attributes;
+ for (var j = 0, m = attributes.length; j < m; j++) {
+ if (attributes[j].name == 'selected') {
+ if (!aSelect.multiple) return options[i];
+ selected.push(options[i]);
+ }
+ }
+ }
+ return selected;
+}
+
+/**
* Tells you whether or not a particular value is selected in a select,
* whether it's a multi-select or a single-select. The check is
* case-sensitive.
diff --git a/js/yui3/align-plugin/align-plugin-min.js b/js/yui3/align-plugin/align-plugin-min.js
new file mode 100644
index 000000000..3282c65e0
--- /dev/null
+++ b/js/yui3/align-plugin/align-plugin-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("align-plugin",function(e,t){function s(e){e.host&&(this._host=e.host)}var n="offsetWidth",r="offsetHeight",i=i;s.prototype={to:function(t,o,u,a){this._syncArgs=e.Array(arguments),t.top===i&&(t=e.one(t).get("region"));if(t){var f=[t.left,t.top],l=[t.width,t.height],c=s.points,h=this._host,p=null,d=h.getAttrs([r,n]),v=[0-d[n],0-d[r]],m=o?c[o.charAt(0)]:p,g=o&&o!=="cc"?c[o.charAt(1)]:p,y=u?c[u.charAt(0)]:p,b=u&&u!=="cc"?c[u.charAt(1)]:p;m&&(f=m(f,l,o)),g&&(f=g(f,l,o)),y&&(f=y(f,v,u)),b&&(f=b(f,v,u)),f&&h&&h.setXY(f),this._resize(a)}return this},sync:function(){return this.to.apply(this,this._syncArgs),this},_resize:function(t){var n=this._handle;t&&!n?this._handle=e.on("resize",this._onresize,window,this):!t&&n&&n.detach()},_onresize:function(){var e=this;setTimeout(function(){e.sync()})},center:function(e,t){return this.to(e,"cc","cc",t),this},destroy:function(){var e=this._handle;e&&e.detach()}},s.points={t:function(e,t){return e},r:function(e,t){return[e[0]+t[0],e[1]]},b:function(e,t){return[e[0],e[1]+t[1]]},l:function(e,t){return e},c:function(e,t,n){var r=n[0]==="t"||n[0]==="b"?0:1,i,s;return n==="cc"?i=[e[0]+t[0]/2,e[1]+t[1]/2]:(s=e[r]+t[r]/2,i=r?[e[0],s]:[s,e[1]]),i}},s.NAME="Align",s.NS="align",s.prototype.constructor=s,e.namespace("Plugin"),e.Plugin.Align=s},"3.17.2",{requires:["node-screen","node-pluginhost"]});
diff --git a/js/yui3/anim-base/anim-base-min.js b/js/yui3/anim-base/anim-base-min.js
new file mode 100644
index 000000000..7f550eddc
--- /dev/null
+++ b/js/yui3/anim-base/anim-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("anim-base",function(e,t){var n="running",r="startTime",i="elapsedTime",s="start",o="tween",u="end",a="node",f="paused",l="reverse",c="iterationCount",h=Number,p={},d;e.Anim=function(){e.Anim.superclass.constructor.apply(this,arguments),e.Anim._instances[e.stamp(this)]=this},e.Anim.NAME="anim",e.Anim._instances={},e.Anim.RE_DEFAULT_UNIT=/^width|height|top|right|bottom|left|margin.*|padding.*|border.*$/i,e.Anim.DEFAULT_UNIT="px",e.Anim.DEFAULT_EASING=function(e,t,n,r){return n*e/r+t},e.Anim._intervalTime=20,e.Anim.behaviors={left:{get:function(e,t){return e._getOffset(t)}}},e.Anim.behaviors.top=e.Anim.behaviors.left,e.Anim.DEFAULT_SETTER=function(t,n,r,i,s,o,u,a){var f=t._node,l=f._node,c=u(s,h(r),h(i)-h(r),o);l?"style"in l&&(n in l.style||n in e.DOM.CUSTOM_STYLES)?(a=a||"",f.setStyle(n,c+a)):"attributes"in l&&n in l.attributes?f.setAttribute(n,c):n in l&&(l[n]=c):f.set?f.set(n,c):n in f&&(f[n]=c)},e.Anim.DEFAULT_GETTER=function(t,n){var r=t._node,i=r._node,s="";return i?"style"in i&&(n in i.style||n in e.DOM.CUSTOM_STYLES)?s=r.getComputedStyle(n):"attributes"in i&&n in i.attributes?s=r.getAttribute(n):n in i&&(s=i[n]):r.get?s=r.get(n):n in r&&(s=r[n]),s},e.Anim.ATTRS={node:{setter:function(t){return t&&(typeof t=="string"||t.nodeType)&&(t=e.one(t)),this._node=t,!t,t}},duration:{value:1},easing:{value:e.Anim.DEFAULT_EASING,setter:function(t){if(typeof t=="string"&&e.Easing)return e.Easing[t]}},from:{},to:{},startTime:{value:0,readOnly:!0},elapsedTime:{value:0,readOnly:!0},running:{getter:function(){return!!p[e.stamp(this)]},value:!1,readOnly:!0},iterations:{value:1},iterationCount:{value:0,readOnly:!0},direction:{value:"normal"},paused:{readOnly:!0,value:!1},reverse:{value:!1}},e.Anim.run=function(){var t=e.Anim._instances,n;for(n in t)t[n].run&&t[n].run()},e.Anim.pause=function(){for(var t in p)p[t].pause&&p[t].pause();e.Anim._stopTimer()},e.Anim.stop=function(){for(var t in p)p[t].stop&&p[t].stop();e.Anim._stopTimer()},e.Anim._startTimer=function(){d||(d=setInterval(e.Anim._runFrame,e.Anim._intervalTime))},e.Anim._stopTimer=function(){clearInterval(d),d=0},e.Anim._runFrame=function(){var t=!0,n;for(n in p)p[n]._runFrame&&(t=!1,p[n]._runFrame());t&&e.Anim._stopTimer()},e.Anim.RE_UNITS=/^(-?\d*\.?\d*){1}(em|ex|px|in|cm|mm|pt|pc|%)*$/;var v={run:function(){return this.get(f)?this._resume():this.get(n)||this._start(),this},pause:function(){return this.get(n)&&this._pause(),this},stop:function(e){return(this.get(n)||this.get(f))&&this._end(e),this},_added:!1,_start:function(){this._set(r,new Date-this.get(i)),this._actualFrames=0,this.get(f)||this._initAnimAttr(),p[e.stamp(this)]=this,e.Anim._startTimer(),this.fire(s)},_pause:function(){this._set(r,null),this._set(f,!0),delete p[e.stamp(this)],this.fire("pause")},_resume:function(){this._set(f,!1),p[e.stamp(this)]=this,this._set(r,new Date-this.get(i)),e.Anim._startTimer(),this.fire("resume")},_end:function(t){var n=this.get("duration")*1e3;t&&this._runAttrs(n,n,this.get(l)),this._set(r,null),this._set(i,0),this._set(f,!1),delete p[e.stamp(this)],this.fire(u,{elapsed:this.get(i)})},_runFrame:function(){var e=this._runtimeAttr.duration,t=new Date-this.get(r),n=this.get(l),s=t>=e;this._runAttrs(t,e,n),this._actualFrames+=1,this._set(i,t),this.fire(o),s&&this._lastFrame()},_runAttrs:function(t,n,r){var i=this._runtimeAttr,s=e.Anim.behaviors,o=i.easing,u=n,a=!1,f,l,c;t>=n&&(a=!0),r&&(t=n-t,u=0);for(c in i)i[c].to&&(f=i[c],l=c in s&&"set"in s[c]?s[c].set:e.Anim.DEFAULT_SETTER,a?l(this,c,f.from,f.to,u,n,o,f.unit):l(this,c,f.from,f.to,t,n,o,f.unit))},_lastFrame:function(){var e=this.get("iterations"),t=this.get(c);t+=1,e==="infinite"||t<e?(this.get("direction")==="alternate"&&this.set(l,!this.get(l)),this.fire("iteration")):(t=0,this._end()),this._set(r,new Date),this._set(c,t)},_initAnimAttr:function(){var t=this.get("from")||{},n=this.get("to")||{},r={duration:this.get("duration")*1e3,easing:this.get("easing")},i=e.Anim.behaviors,s=this.get(a),o,u,f;e.each(n,function(n,a){typeof n=="function"&&(n=n.call(this,s)),u=t[a],u===undefined?u=a in i&&"get"in i[a]?i[a].get(this,a):e.Anim.DEFAULT_GETTER(this,a):typeof u=="function"&&(u=u.call(this,s));var l=e.Anim.RE_UNITS.exec(u),c=e.Anim.RE_UNITS.exec(n);u=l?l[1]:u,f=c?c[1]:n,o=c?c[2]:l?l[2]:"",!o&&e.Anim.RE_DEFAULT_UNIT.test(a)&&(o=e.Anim.DEFAULT_UNIT);if(!u||!f){e.error('invalid "from" or "to" for "'+a+'"',"Anim");return}r[a]={from:e.Lang.isObject(u)?e.clone(u):u,to:f,unit:o}},this),this._runtimeAttr=r},_getOffset:function(e){var t=this._node,n=t.getComputedStyle(e),r=e==="left"?"getX":"getY",i=e==="left"?"setX":"setY",s;return n==="auto"&&(s=t.getStyle("position"),s==="absolute"||s==="fixed"?(n=t[r](),t[i](n)):n=0),n},destructor:function(){delete e.Anim._instances[e.stamp(this)]}};e.extend(e.Anim,e.Base,v)},"3.17.2",{requires:["base-base","node-style","color-base"]});
diff --git a/js/yui3/anim-color/anim-color-min.js b/js/yui3/anim-color/anim-color-min.js
new file mode 100644
index 000000000..a1c078e36
--- /dev/null
+++ b/js/yui3/anim-color/anim-color-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("anim-color",function(e,t){var n=Number;e.Anim.getUpdatedColorValue=function(t,r,i,s,o){return t=e.Color.re_RGB.exec(e.Color.toRGB(t)),r=e.Color.re_RGB.exec(e.Color.toRGB(r)),(!t||t.length<3||!r||r.length<3)&&e.error("invalid from or to passed to color behavior"),"rgb("+[Math.floor(o(i,n(t[1]),n(r[1])-n(t[1]),s)),Math.floor(o(i,n(t[2]),n(r[2])-n(t[2]),s)),Math.floor(o(i,n(t[3]),n(r[3])-n(t[3]),s))].join(", ")+")"},e.Anim.behaviors.color={set:function(t,n,r,i,s,o,u){t._node.setStyle(n,e.Anim.getUpdatedColorValue(r,i,s,o,u))},get:function(e,t){var n=e._node.getComputedStyle(t);return n=n==="transparent"?"rgb(255, 255, 255)":n,n}},e.each(["backgroundColor","borderColor","borderTopColor","borderRightColor","borderBottomColor","borderLeftColor"],function(t){e.Anim.behaviors[t]=e.Anim.behaviors.color})},"3.17.2",{requires:["anim-base"]});
diff --git a/js/yui3/anim-curve/anim-curve-min.js b/js/yui3/anim-curve/anim-curve-min.js
new file mode 100644
index 000000000..e54dcb174
--- /dev/null
+++ b/js/yui3/anim-curve/anim-curve-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("anim-curve",function(e,t){e.Anim.behaviors.curve={set:function(t,n,r,i,s,o,u){r=r.slice.call(r),i=i.slice.call(i);var a=u(s,0,100,o)/100;i.unshift(r),t._node.setXY(e.Anim.getBezier(i,a))},get:function(e){return e._node.getXY()}},e.Anim.getBezier=function(e,t){var n=e.length,r=[],i,s;for(i=0;i<n;++i)r[i]=[e[i][0],e[i][1]];for(s=1;s<n;++s)for(i=0;i<n-s;++i)r[i][0]=(1-t)*r[i][0]+t*r[parseInt(i+1,10)][0],r[i][1]=(1-t)*r[i][1]+t*r[parseInt(i+1,10)][1];return[r[0][0],r[0][1]]}},"3.17.2",{requires:["anim-xy"]});
diff --git a/js/yui3/anim-easing/anim-easing-min.js b/js/yui3/anim-easing/anim-easing-min.js
new file mode 100644
index 000000000..00fa2209e
--- /dev/null
+++ b/js/yui3/anim-easing/anim-easing-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("anim-easing",function(e,t){var n={easeNone:function(e,t,n,r){return n*e/r+t},easeIn:function(e,t,n,r){return n*(e/=r)*e+t},easeOut:function(e,t,n,r){return-n*(e/=r)*(e-2)+t},easeBoth:function(e,t,n,r){return(e/=r/2)<1?n/2*e*e+t:-n/2*(--e*(e-2)-1)+t},easeInStrong:function(e,t,n,r){return n*(e/=r)*e*e*e+t},easeOutStrong:function(e,t,n,r){return-n*((e=e/r-1)*e*e*e-1)+t},easeBothStrong:function(e,t,n,r){return(e/=r/2)<1?n/2*e*e*e*e+t:-n/2*((e-=2)*e*e*e-2)+t},elasticIn:function(e,t,n,r,i,s){var o;return e===0?t:(e/=r)===1?t+n:(s||(s=r*.3),!i||i<Math.abs(n)?(i=n,o=s/4):o=s/(2*Math.PI)*Math.asin(n/i),-(i*Math.pow(2,10*(e-=1))*Math.sin((e*r-o)*2*Math.PI/s))+t)},elasticOut:function(e,t,n,r,i,s){var o;return e===0?t:(e/=r)===1?t+n:(s||(s=r*.3),!i||i<Math.abs(n)?(i=n,o=s/4):o=s/(2*Math.PI)*Math.asin(n/i),i*Math.pow(2,-10*e)*Math.sin((e*r-o)*2*Math.PI/s)+n+t)},elasticBoth:function(e,t,n,r,i,s){var o;return e===0?t:(e/=r/2)===2?t+n:(s||(s=r*.3*1.5),!i||i<Math.abs(n)?(i=n,o=s/4):o=s/(2*Math.PI)*Math.asin(n/i),e<1?-0.5*i*Math.pow(2,10*(e-=1))*Math.sin((e*r-o)*2*Math.PI/s)+t:i*Math.pow(2,-10*(e-=1))*Math.sin((e*r-o)*2*Math.PI/s)*.5+n+t)},backIn:function(e,t,n,r,i){return i===undefined&&(i=1.70158),e===r&&(e-=.001),n*(e/=r)*e*((i+1)*e-i)+t},backOut:function(e,t,n,r,i){return typeof i=="undefined"&&(i=1.70158),n*((e=e/r-1)*e*((i+1)*e+i)+1)+t},backBoth:function(e,t,n,r,i){return typeof i=="undefined"&&(i=1.70158),(e/=r/2)<1?n/2*e*e*(((i*=1.525)+1)*e-i)+t:n/2*((e-=2)*e*(((i*=1.525)+1)*e+i)+2)+t},bounceIn:function(t,n,r,i){return r-e.Easing.bounceOut(i-t,0,r,i)+n},bounceOut:function(e,t,n,r){return(e/=r)<1/2.75?n*7.5625*e*e+t:e<2/2.75?n*(7.5625*(e-=1.5/2.75)*e+.75)+t:e<2.5/2.75?n*(7.5625*(e-=2.25/2.75)*e+.9375)+t:n*(7.5625*(e-=2.625/2.75)*e+.984375)+t},bounceBoth:function(t,n,r,i){return t<i/2?e.Easing.bounceIn(t*2,0,r,i)*.5+n:e.Easing.bounceOut(t*2-i,0,r,i)*.5+r*.5+n}};e.Easing=n},"3.17.2",{requires:["anim-base"]});
diff --git a/js/yui3/anim-node-plugin/anim-node-plugin-min.js b/js/yui3/anim-node-plugin/anim-node-plugin-min.js
new file mode 100644
index 000000000..122b4a59a
--- /dev/null
+++ b/js/yui3/anim-node-plugin/anim-node-plugin-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("anim-node-plugin",function(e,t){var n=function(t){t=t?e.merge(t):{},t.node=t.host,n.superclass.constructor.apply(this,arguments)};n.NAME="nodefx",n.NS="fx",e.extend(n,e.Anim),e.namespace("Plugin"),e.Plugin.NodeFX=n},"3.17.2",{requires:["node-pluginhost","anim-base"]});
diff --git a/js/yui3/anim-scroll/anim-scroll-min.js b/js/yui3/anim-scroll/anim-scroll-min.js
new file mode 100644
index 000000000..535cf2ffe
--- /dev/null
+++ b/js/yui3/anim-scroll/anim-scroll-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("anim-scroll",function(e,t){var n=Number;e.Anim.behaviors.scroll={set:function(e,t,r,i,s,o,u){var a=e._node,f=[u(s,n(r[0]),n(i[0])-n(r[0]),o),u(s,n(r[1]),n(i[1])-n(r[1]),o)];f[0]&&a.set("scrollLeft",f[0]),f[1]&&a.set("scrollTop",f[1])},get:function(e){var t=e._node;return[t.get("scrollLeft"),t.get("scrollTop")]}}},"3.17.2",{requires:["anim-base"]});
diff --git a/js/yui3/anim-shape/anim-shape-min.js b/js/yui3/anim-shape/anim-shape-min.js
new file mode 100644
index 000000000..ba7cc0095
--- /dev/null
+++ b/js/yui3/anim-shape/anim-shape-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("anim-shape",function(e,t){var n=Number,r,i,s="color",o="stops",u="type",a=function(t,r,i,o,u,a){var f=0,l=e.Anim.getUpdatedColorValue,c,h,p,d=i.length,v=[],m;for(;f<d;f+=1){c=i[f],h=r[f],m={};for(p in c)c.hasOwnProperty(p)&&(p===s?m[p]=e.Color.toHex(l(e.Color.toHex(h[p]),e.Color.toHex(c[p]),o,u,a)):m[p]=a(o,n(h[p]),n(c[p])-n(h[p]),u));v.push(m)}return v},f={set:function(t,r,i,f,l,c,h){var p,d={},v=e.Anim.getUpdatedColorValue,m=a;for(p in f)if(f.hasOwnProperty(p)&&p!==u)switch(p){case s:d[p]=v(i[p],f[p],l,c,h);break;case o:d[p]=m(t,i[p],f[p],l,c,h);break;default:d[p]=h(l,n(i[p]),n(f[p])-n(i[p]),c)}t._node.set(r,d)}};e.Anim.behaviors.fill=f,e.Anim.behaviors.stroke=f,e.Anim.behaviors.transform={set:function(e,t,s,o,u,a,f){var l=e._node,c="",h,p,d,v,m=0,g,y,b;o=r,b=r.length;for(;m<b;++m){d=o[m].concat(),v=s[m].concat(),h=d.shift(),p=v.shift(),y=d.length,c+=h+"(";for(g=0;g<y;++g)c+=f(u,n(v[g]),n(d[g])-n(v[g]),a),g<y-1&&(c+=", ");c+=");"}c&&l.set("transform",c),l._transform=i},get:function(t){var n=t._node,s=n.matrix,o=t.get("to").transform,u=n.get("transform"),a=e.MatrixUtil.getTransformArray(o),f=u?e.MatrixUtil.getTransformArray(u):null,l,c,h,p,d;if(a)if(!f||f.length<1){f=[],h=a.length;for(c=0;c<h;++c)p=a[c][0],f[c]=e.MatrixUtil.getTransformFunctionArray(p);r=a,d=f}else if(e.MatrixUtil.compareTransformSequence(a,f))r=a,d=f;else{l=new e.Matrix,h=a.length;for(c=0;c<h;++c)p=a[c].shift(),p=p==="matrix"?"multiply":p,l[p].apply(l,a[c]);r=l.decompose(),d=s.decompose()}return i=o,d}}},"3.17.2",{requires:["anim-base","anim-easing","anim-color","matrix"]});
diff --git a/js/yui3/anim-xy/anim-xy-min.js b/js/yui3/anim-xy/anim-xy-min.js
new file mode 100644
index 000000000..5ea5cc323
--- /dev/null
+++ b/js/yui3/anim-xy/anim-xy-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("anim-xy",function(e,t){var n=Number;e.Anim.behaviors.xy={set:function(e,t,r,i,s,o,u){e._node.setXY([u(s,n(r[0]),n(i[0])-n(r[0]),o),u(s,n(r[1]),n(i[1])-n(r[1]),o)])},get:function(e){return e._node.getXY()}}},"3.17.2",{requires:["anim-base","node-screen"]});
diff --git a/js/yui3/app-base/app-base-min.js b/js/yui3/app-base/app-base-min.js
new file mode 100644
index 000000000..2715ad2c3
--- /dev/null
+++ b/js/yui3/app-base/app-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("app-base",function(e,t){var n=e.Lang,r=e.Object,i=e.PjaxBase,s=e.Router,o=e.View,u=e.ClassNameManager.getClassName,a=e.config.win,f;f=e.Base.create("app",e.Base,[o,s,i],{views:{},initializer:function(t){function i(t,r){n[r]=e.merge(n[r],t)}t||(t={});var n={};r.each(this.views,i),r.each(t.views,i),this.views=n,this._viewInfoMap={},this.after("activeViewChange",e.bind("_afterActiveViewChange",this)),this.get("serverRouting")||this._pjaxBindUI()},createView:function(t,i){var s=this.getViewInfo(t),u=s&&s.type||o,a,f;return a=n.isString(u)?r.getValue(e,u.split(".")):u,f=new a(i),this._viewInfoMap[e.stamp(f,!0)]=s,f},getViewInfo:function(t){return n.isString(t)?this.views[t]:t&&this._viewInfoMap[e.stamp(t,!0)]},render:function(){var t=e.App.CLASS_NAMES,n=this.get("container"),r=this.get("viewContainer"),i=this.get("activeView"),s=i&&i.get("container"),o=n.compareTo(r);return n.addClass(t.app),r.addClass(t.views),i&&!r.contains(s)&&r.appendChild(s),!n.contains(r)&&!o&&n.appendChild(r),this},showView:function(t,r,i,s){var o,u;return i||(i={}),s?i=e.merge(i,{callback:s}):n.isFunction(i)&&(i={callback:i}),n.isString(t)&&(o=this.getViewInfo(t),o&&o.preserve&&o.instance?(t=o.instance,this._viewInfoMap[e.stamp(t,!0)]=o):(t=this.createView(t,r),u=!0)),i.update&&!u&&t.setAttrs(r),"render"in i?i.render&&t.render():u&&t.render(),this._set("activeView",t,{options:i})},_attachView:function(e,t){if(!e)return;var n=this.getViewInfo(e),r=this.get("viewContainer");e.addTarget(this),n&&(n.instance=e),r[t?"prepend":"append"](e.get("container"))},_destroyContainer:function(){var t=e.App.CLASS_NAMES,n=this.get("container"),r=this.get("viewContainer"),i=n.compareTo(r);if(e.one("body").compareTo(n)){this.detachEvents(),n.removeClass(t.app),i?n.removeClass(t.views):r.remove(!0);return}r.remove(!0),i||n.remove(!0)},_detachView:function(t){if(!t)return;var n=this.getViewInfo(t)||{};n.preserve?t.remove():(t.destroy({remove:!0}),delete this._viewInfoMap[e.stamp(t,!0)],t===n.instance&&delete n.instance),t.removeTarget(this)},_getRequest:function(){var e=s.prototype._getRequest.apply(this,arguments);return e.app=this,e},_getViewContainer:function(e){return!e&&!this._viewContainer&&(e=this._viewContainer=this.create(),this._set("viewContainer",e)),e},_initHtml5:function(){return this.get("serverRouting")===!1?!1:s.html5},_isChildView:function(e,t){var n=this.getViewInfo(e),r=this.getViewInfo(t);return n&&r?this.getViewInfo(n.parent)===r:!1},_isParentView:function(e,t){var n=this.getViewInfo(e),r=this.getViewInfo(t);return n&&r?this.getViewInfo(r.parent)===n:!1},_navigate:function(t,n){return this.get("serverRouting")||(n=e.merge({force:!0},n)),i.prototype._navigate.call(this,t,n)},_save:function(t,n){var r;return this.get("serverRouting")&&!this.get("html5")?this._hasSameOrigin(t)?(a&&(r=this._joinURL(t||""),n?a.location.replace(r):a.location=r),this):(e.error("Security error: The new URL must be of the same origin as the current URL."),this):s.prototype._save.apply(this,arguments)},_uiSetActiveView:function(e,t,n){n||(n={});var r=n.callback,i=this._isChildView(e,t),s=!i&&this._isParentView(e,t),o=!!n.prepend||s;if(e===t)return r&&r.call(this,e);this._attachView(e,o),this._detachView(t),r&&r.call(this,e)},_afterActiveViewChange:function(e){this._uiSetActiveView(e.newVal,e.prevVal,e.options)}},{ATTRS:{activeView:{value:null,readOnly:!0},container:{valueFn:function(){return e.one("body")}},html5:{valueFn:"_initHtml5"},linkSelector:{value:"a"},serverRouting:{valueFn:function(){return e.App.serverRouting},writeOnce:"initOnly"},viewContainer:{getter:"_getViewContainer",setter:e.one,writeOnce:!0}},_NON_ATTRS_CFG:["views"]}),e.namespace("App").Base=f,e.App=e.mix(e.Base.create("app",f,[]),e.App,!0),e.App.CLASS_NAMES={app:u("app"),views:u("app","views")}},"3.17.2",{requires:["classnamemanager","pjax-base","router","view"]});
diff --git a/js/yui3/app-content/app-content-min.js b/js/yui3/app-content/app-content-min.js
new file mode 100644
index 000000000..2ad00f92f
--- /dev/null
+++ b/js/yui3/app-content/app-content-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("app-content",function(e,t){function r(){n.apply(this,arguments)}var n=e.PjaxContent;r.route=["loadContent","_contentRoute"],r.prototype={showContent:function(t,n,r){t=e.one(t),typeof n=="function"&&(n={callback:n},r=null),n=e.merge({render:!1},n);var i=n.view||"",s=typeof i=="string"?i:i.name,o=typeof i!="string"?i.config:{},u=this.getViewInfo(s),a,f,l,c;return delete n.view,t&&t.isFragment()&&t.get("childNodes").size()===1&&(t=t.get("firstChild")),t&&t.get("nodeType")===1?a=t:(l=u&&u.type||e.View,c=typeof l=="string"?e.Object.getValue(e,l.split(".")):l,f=c.prototype.containerTemplate,a=e.Node.create(f),a.append(t)),o=e.merge(o,{container:a}),this.showView(s,o,n,r)},_contentRoute:function(t,n,r){var i=n.content,s=e.config.doc,o;if(!i||!i.node)return r();i.title&&s&&(o=this.onceAfter("activeViewChange",function(){s.title=i.title})),this.showContent(i.node),o&&o.detach(),r()}},r.ATTRS=e.Attribute.protectAttrs(n.ATTRS),e.mix(r,n,!1,null,1),e.App.Content=r,e.Base.mix(e.App,[r])},"3.17.2",{requires:["app-base","pjax-content"]});
diff --git a/js/yui3/app-transitions-css/app-transitions-css-min.css b/js/yui3/app-transitions-css/app-transitions-css-min.css
new file mode 100644
index 000000000..c06ffbd66
--- /dev/null
+++ b/js/yui3/app-transitions-css/app-transitions-css-min.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-app-transitioning .yui3-app-views,.yui3-app-views.yui3-app-transitioning{overflow-x:hidden;position:relative;white-space:nowrap;letter-spacing:-0.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.yui3-app-transitioning .yui3-app-views,.yui3-app-views.yui3-app-transitioning{word-spacing:-0.43em}.yui3-app-transitioning .yui3-app-views>*,.yui3-app-views.yui3-app-transitioning>*{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;width:100%;white-space:normal;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}#yui3-css-stamp.app-transitions-css{display:none}
diff --git a/js/yui3/app-transitions-css/app-transitions-css.css b/js/yui3/app-transitions-css/app-transitions-css.css
new file mode 100644
index 000000000..ae22befda
--- /dev/null
+++ b/js/yui3/app-transitions-css/app-transitions-css.css
@@ -0,0 +1,40 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-app-transitioning .yui3-app-views,
+.yui3-app-views.yui3-app-transitioning {
+ overflow-x: hidden;
+ position: relative;
+ white-space: nowrap;
+ letter-spacing: -0.31em; /* webkit: collapse white-space between units */
+ text-rendering: optimizespeed; /* Webkit: fixes text-rendering: optimizeLegibility */
+}
+/* Opera as of 12 on Windows needs word-spacing.
+ The ".opera-only" selector is used to prevent actual prefocus styling
+ and is not required in markup.
+*/
+.opera-only :-o-prefocus,
+.yui3-app-transitioning .yui3-app-views,
+.yui3-app-views.yui3-app-transitioning {
+ word-spacing: -0.43em;
+}
+.yui3-app-transitioning .yui3-app-views > *,
+.yui3-app-views.yui3-app-transitioning > * {
+ display: inline-block;
+ letter-spacing: normal;
+ word-spacing: normal;
+ vertical-align: top;
+ text-rendering: auto;
+ width: 100%;
+ white-space: normal;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+/* YUI CSS Detection Stamp */
+#yui3-css-stamp.app-transitions-css { display: none; }
diff --git a/js/yui3/app-transitions-native/app-transitions-native-min.js b/js/yui3/app-transitions-native/app-transitions-native-min.js
new file mode 100644
index 000000000..1d00a6d30
--- /dev/null
+++ b/js/yui3/app-transitions-native/app-transitions-native-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("app-transitions-native",function(e,t){function r(){}var n=e.App.Transitions;r.prototype={initializer:function(){this._transitioning=!1,this._viewTransitionQueue=[],e.Do.before(this._queueActiveView,this,"_uiSetActiveView")},_dequeueActiveView:function(){var t=this._viewTransitionQueue,n=t.shift(),r;n&&(t.length&&(r=e.merge(n[2],{transition:!1}),n.splice(2,1,r)),this._uiTransitionActiveView.apply(this,n))},_getFx:function(e,t,r){var i=n.FX,s=this.get("transitions");return r===!1||!s?null:r?i[r]:this._isChildView(e,t)?i[s.toChild]:this._isParentView(e,t)?i[s.toParent]:i[s.navigate]},_queueActiveView:function(){var t=e.Array(arguments,0,!0);return this._viewTransitionQueue.push(t),this._transitioning||this._dequeueActiveView(),new e.Do.Prevent},_uiTransitionActiveView:function(t,n,r){function p(){return this._detachView(n),s.removeClass(o),i&&i.call(this,t),this._transitioning=!1,this._dequeueActiveView()}r||(r={});var i=r.callback,s,o,u,a,f,l,c,h;if(t===n)return i&&i.call(this,t),this._transitioning=!1,this._dequeueActiveView();l=this._getFx(t,n,r.transition),u=this._isChildView(t,n),a=!u&&this._isParentView(t,n),f=!!r.prepend||a;if(!l)return this._attachView(t,f),this._detachView(n),i&&i.call(this,t),this._transitioning=!1,this._dequeueActiveView();this._transitioning=!0,s=this.get("container"),o=e.App.CLASS_NAMES.transitioning,s.addClass(o),this._attachView(t,f),h=new e.Parallel({context:this}),c={crossView:!!n&&!!t,prepended:f},t&&l.viewIn&&t.get("container").transition(l.viewIn,c,h.add()),n&&l.viewOut&&n.get("container").transition(l.viewOut,c,h.add()),h.done(p)}},e.mix(e.Transition.fx,{"app:fadeIn":{opacity:1,duration:.3,on:{start:function(e){var t={opacity:0},n=e.config;n.crossView&&!n.prepended&&(t.transform="translateX(-100%)"),this.setStyles(t)},end:function(){this.setStyle("transform","translateX(0)")}}},"app:fadeOut":{opacity:0,duration:.3,on:{start:function(e){var t={opacity:1},n=e.config;n.crossView&&n.prepended&&(t.transform="translateX(-100%)"),this.setStyles(t)},end:function(){this.setStyle("transform","translateX(0)")}}},"app:slideLeft":{duration:.3,transform:"translateX(-100%)",on:{start:function(){this.setStyles({opacity:1,transform:"translateX(0%)"})},end:function(){this.setStyle("transform","translateX(0)")}}},"app:slideRight":{duration:.3,transform:"translateX(0)",on:{start:function(){this.setStyles({opacity:1,transform:"translateX(-100%)"})},end:function(){this.setStyle("transform","translateX(0)")}}}}),e.App.TransitionsNative=r,e.Base.mix(e.App,[r])},"3.17.2",{requires:["app-transitions","app-transitions-css","parallel","transition"]});
diff --git a/js/yui3/app-transitions/app-transitions-min.js b/js/yui3/app-transitions/app-transitions-min.js
new file mode 100644
index 000000000..219fb9892
--- /dev/null
+++ b/js/yui3/app-transitions/app-transitions-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("app-transitions",function(e,t){function n(){}n.ATTRS={transitions:{setter:"_setTransitions",value:!1}},n.FX={fade:{viewIn:"app:fadeIn",viewOut:"app:fadeOut"},slideLeft:{viewIn:"app:slideLeft",viewOut:"app:slideLeft"},slideRight:{viewIn:"app:slideRight",viewOut:"app:slideRight"}},n.prototype={transitions:{navigate:"fade",toChild:"slideLeft",toParent:"slideRight"},_setTransitions:function(t){var n=this.transitions;return t&&t===!0?e.merge(n):t}},e.App.Transitions=n,e.Base.mix(e.App,[n]),e.mix(e.App.CLASS_NAMES,{transitioning:e.ClassNameManager.getClassName("app","transitioning")})},"3.17.2",{requires:["app-base"]});
diff --git a/js/yui3/array-extras/array-extras-min.js b/js/yui3/array-extras/array-extras-min.js
new file mode 100644
index 000000000..2b73c9258
--- /dev/null
+++ b/js/yui3/array-extras/array-extras-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("array-extras",function(e,t){var n=e.Array,r=e.Lang,i=Array.prototype;n.lastIndexOf=r._isNative(i.lastIndexOf)?function(e,t,n){return n||n===0?e.lastIndexOf(t,n):e.lastIndexOf(t)}:function(e,t,n){var r=e.length,i=r-1;if(n||n===0)i=Math.min(n<0?r+n:n,r);if(i>-1&&r>0)for(;i>-1;--i)if(i in e&&e[i]===t)return i;return-1},n.unique=function(e,t){var n=0,r=e.length,i=[],s,o,u,a;e:for(;n<r;n++){a=e[n];for(s=0,u=i.length;s<u;s++){o=i[s];if(t){if(t.call(e,a,o,n,e))continue e}else if(a===o)continue e}i.push(a)}return i},n.filter=r._isNative(i.filter)?function(e,t,n){return i.filter.call(e,t,n)}:function(e,t,n){var r=0,i=e.length,s=[],o;for(;r<i;++r)r in e&&(o=e[r],t.call(n,o,r,e)&&s.push(o));return s},n.reject=function(e,t,r){return n.filter(e,function(e,n,i){return!t.call(r,e,n,i)})},n.every=r._isNative(i.every)?function(e,t,n){return i.every.call(e,t,n)}:function(e,t,n){for(var r=0,i=e.length;r<i;++r)if(r in e&&!t.call(n,e[r],r,e))return!1;return!0},n.map=r._isNative(i.map)?function(e,t,n){return i.map.call(e,t,n)}:function(e,t,n){var r=0,s=e.length,o=i.concat.call(e);for(;r<s;++r)r in e&&(o[r]=t.call(n,e[r],r,e));return o},n.reduce=r._isNative(i.reduce)?function(e,t,n,r){return i.reduce.call(e,function(e,t,i,s){return n.call(r,e,t,i,s)},t)}:function(e,t,n,r){var i=0,s=e.length,o=t;for(;i<s;++i)i in e&&(o=n.call(r,o,e[i],i,e));return o},n.find=function(e,t,n){for(var r=0,i=e.length;r<i;r++)if(r in e&&t.call(n,e[r],r,e))return e[r];return null},n.grep=function(e,t){return n.filter(e,function(e,n){return t.test(e)})},n.partition=function(e,t,r){var i={matches:[],rejects:[]};return n.each(e,function(n,s){var u=t.call(r,n,s,e)?i.matches:i.rejects;u.push(n)}),i},n.zip=function(e,t){var r=[];return n.each(e,function(e,n){r.push([e,t[n]])}),r},n.flatten=function(e){var t=[],i,s,o;if(!e)return t;for(i=0,s=e.length;i<s;++i)o=e[i],r.isArray(o)?t.push.apply(t,n.flatten(o)):t.push(o);return t}},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/array-invoke/array-invoke-min.js b/js/yui3/array-invoke/array-invoke-min.js
new file mode 100644
index 000000000..a15c459df
--- /dev/null
+++ b/js/yui3/array-invoke/array-invoke-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("array-invoke",function(e,t){e.Array.invoke=function(t,n){var r=e.Array(arguments,2,!0),i=e.Lang.isFunction,s=[];return e.Array.each(e.Array(t),function(e,t){e&&i(e[n])&&(s[t]=e[n].apply(e,r))}),s}},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/arraylist-add/arraylist-add-min.js b/js/yui3/arraylist-add/arraylist-add-min.js
new file mode 100644
index 000000000..2363fe66a
--- /dev/null
+++ b/js/yui3/arraylist-add/arraylist-add-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("arraylist-add",function(e,t){e.mix(e.ArrayList.prototype,{add:function(t,n){var r=this._items;return e.Lang.isNumber(n)?r.splice(n,0,t):r.push(t),this},remove:function(e,t,n){n=n||this.itemsAreEqual;for(var r=this._items.length-1;r>=0;--r)if(n.call(this,e,this.item(r))){this._items.splice(r,1);if(!t)break}return this},itemsAreEqual:function(e,t){return e===t}})},"3.17.2",{requires:["arraylist"]});
diff --git a/js/yui3/arraylist-filter/arraylist-filter-min.js b/js/yui3/arraylist-filter/arraylist-filter-min.js
new file mode 100644
index 000000000..29f6c9d96
--- /dev/null
+++ b/js/yui3/arraylist-filter/arraylist-filter-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("arraylist-filter",function(e,t){e.mix(e.ArrayList.prototype,{filter:function(t){var n=[];return e.Array.each(this._items,function(e,r){e=this.item(r),t(e)&&n.push(e)},this),new this.constructor(n)}})},"3.17.2",{requires:["arraylist"]});
diff --git a/js/yui3/arraylist/arraylist-min.js b/js/yui3/arraylist/arraylist-min.js
new file mode 100644
index 000000000..47ec9b06b
--- /dev/null
+++ b/js/yui3/arraylist/arraylist-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("arraylist",function(e,t){function s(t){t!==undefined?this._items=e.Lang.isArray(t)?t:n(t):this._items=this._items||[]}var n=e.Array,r=n.each,i;i={item:function(e){return this._items[e]},each:function(e,t){return r(this._items,function(n,r){n=this.item(r),e.call(t||n,n,r,this)},this),this},some:function(e,t){return n.some(this._items,function(n,r){return n=this.item(r),e.call(t||n,n,r,this)},this)},indexOf:function(e){return n.indexOf(this._items,e)},size:function(){return this._items.length},isEmpty:function(){return!this.size()},toJSON:function(){return this._items}},i._item=i.item,e.mix(s.prototype,i),e.mix(s,{addMethod:function(e,t){t=n(t),r(t,function(t){e[t]=function(){var e=n(arguments,0,!0),i=[];return r(this._items,function(n,r){n=this._item(r);var s=n[t].apply(n,e);s!==undefined&&s!==n&&(i[r]=s)},this),i.length?i:this}})}}),e.ArrayList=s},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/arraysort/arraysort-min.js b/js/yui3/arraysort/arraysort-min.js
new file mode 100644
index 000000000..d4e0f434c
--- /dev/null
+++ b/js/yui3/arraysort/arraysort-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("arraysort",function(e,t){var n=e.Lang,r=n.isValue,i=n.isString,s=e.ArraySort={compare:function(e,t,n){return r(e)?r(t)?(i(e)&&(e=e.toLowerCase()),i(t)&&(t=t.toLowerCase()),e<t?n?1:-1:e>t?n?-1:1:0):-1:r(t)?1:0},naturalCompare:function(e,t,n){e+="",t+="";if(!n||!n.caseSensitive)e=e.toLowerCase(),t=t.toLowerCase();var r=s._splitAlphaNum(e),i=s._splitAlphaNum(t),o=Math.min(r.length,i.length),u=0,a,f,l;for(l=0;l<o;l++){a=r[l],f=i[l];if(a!==f){u=a-f,u||(u=a>f?1:-1);break}}return u||(u=e.length-t.length),n&&n.descending?-u:u},_splitAlphaNum:function(e){var t=[],n=/(\d+|\D+)/g,r;while(r=n.exec(e))t.push(r[1]);return t}}},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/assets/skin/audio-light/skin.css b/js/yui3/assets/skin/audio-light/skin.css
new file mode 100644
index 000000000..ba899e32d
--- /dev/null
+++ b/js/yui3/assets/skin/audio-light/skin.css
@@ -0,0 +1,197 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/audio/thumb-x.png */
+
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+}
+
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail {
+ height: 35px;
+ background-position: 0 7px;
+}
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-thumb {
+ height: 35px;
+ width: 19px;
+}
+
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 13px;
+ left: -5px;
+ width: 5px;
+ top: 7px;
+}
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 13px;
+ right: -5px;
+ width: 5px;
+ top: 7px;
+}
+
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -3px;
+}
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -43px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/audio/thumb-y.png */
+
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+}
+
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail {
+ width: 35px;
+ background-position: 7px 0;
+}
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-thumb {
+ width: 35px;
+ height: 19px;
+}
+
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 13px;
+ top: -5px;
+ height: 5px;
+ left: 7px;
+}
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 13px;
+ bottom: -5px;
+ height: 5px;
+ left: 7px;
+}
+
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-thumb-image {
+ left: -3px;
+ top: 0;
+}
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -43px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/audio/thumb-x.png */
+
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+}
+
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail {
+ height: 35px;
+ background-position: 0 7px;
+}
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-thumb {
+ height: 35px;
+ width: 19px;
+}
+
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 13px;
+ left: -5px;
+ width: 5px;
+ top: 7px;
+}
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 13px;
+ right: -5px;
+ width: 5px;
+ top: 7px;
+}
+
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -3px;
+}
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -43px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/audio/thumb-y.png */
+
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+}
+
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail {
+ width: 35px;
+ background-position: 7px 0;
+}
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-thumb {
+ width: 35px;
+ height: 19px;
+}
+
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 13px;
+ top: -5px;
+ height: 5px;
+ left: 7px;
+}
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 13px;
+ bottom: -5px;
+ height: 5px;
+ left: 7px;
+}
+
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-thumb-image {
+ left: -3px;
+ top: 0;
+}
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -43px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
diff --git a/js/yui3/assets/skin/audio/skin.css b/js/yui3/assets/skin/audio/skin.css
new file mode 100644
index 000000000..7177f067a
--- /dev/null
+++ b/js/yui3/assets/skin/audio/skin.css
@@ -0,0 +1,197 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/audio/thumb-x.png */
+
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+}
+
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail {
+ height: 35px;
+ background-position: 0 7px;
+}
+.yui3-skin-audio .yui3-slider-x .yui3-slider-thumb {
+ height: 35px;
+ width: 19px;
+}
+
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 13px;
+ left: -5px;
+ width: 5px;
+ top: 7px;
+}
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 13px;
+ right: -5px;
+ width: 5px;
+ top: 7px;
+}
+
+.yui3-skin-audio .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -3px;
+}
+.yui3-skin-audio .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -43px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/audio/thumb-y.png */
+
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+}
+
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail {
+ width: 35px;
+ background-position: 7px 0;
+}
+.yui3-skin-audio .yui3-slider-y .yui3-slider-thumb {
+ width: 35px;
+ height: 19px;
+}
+
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 13px;
+ top: -5px;
+ height: 5px;
+ left: 7px;
+}
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 13px;
+ bottom: -5px;
+ height: 5px;
+ left: 7px;
+}
+
+.yui3-skin-audio .yui3-slider-y .yui3-slider-thumb-image {
+ left: -3px;
+ top: 0;
+}
+.yui3-skin-audio .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -43px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/audio/thumb-x.png */
+
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+}
+
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail {
+ height: 35px;
+ background-position: 0 7px;
+}
+.yui3-skin-audio .yui3-slider-x .yui3-slider-thumb {
+ height: 35px;
+ width: 19px;
+}
+
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 13px;
+ left: -5px;
+ width: 5px;
+ top: 7px;
+}
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 13px;
+ right: -5px;
+ width: 5px;
+ top: 7px;
+}
+
+.yui3-skin-audio .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -3px;
+}
+.yui3-skin-audio .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -43px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/audio/thumb-y.png */
+
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+}
+
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail {
+ width: 35px;
+ background-position: 7px 0;
+}
+.yui3-skin-audio .yui3-slider-y .yui3-slider-thumb {
+ width: 35px;
+ height: 19px;
+}
+
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 13px;
+ top: -5px;
+ height: 5px;
+ left: 7px;
+}
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 13px;
+ bottom: -5px;
+ height: 5px;
+ left: 7px;
+}
+
+.yui3-skin-audio .yui3-slider-y .yui3-slider-thumb-image {
+ left: -3px;
+ top: 0;
+}
+.yui3-skin-audio .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -43px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
diff --git a/js/yui3/assets/skin/capsule-dark/skin.css b/js/yui3/assets/skin/capsule-dark/skin.css
new file mode 100644
index 000000000..3113729e4
--- /dev/null
+++ b/js/yui3/assets/skin/capsule-dark/skin.css
@@ -0,0 +1,197 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/capsule-dark/thumb-x.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/capsule-dark/thumb-x-line.png */
+
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+ /* alternate: rail-x-dots.png */
+ /* alternate: rail-x-lines.png */
+}
+
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail {
+ height: 25px;
+}
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-thumb {
+ height: 30px;
+ width: 14px;
+}
+
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -10px;
+}
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -50px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/capsule-dark/thumb-y.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/capsule-dark/thumb-y-line.png */
+
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+ /* alternate: rail-y-dots.png */
+ /* alternate: rail-y-lines.png */
+}
+
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail {
+ width: 25px;
+}
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-thumb {
+ width: 30px;
+ height: 14px;
+}
+
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-thumb-image {
+ left: -10px;
+ top: 0;
+}
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -50px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/capsule-dark/thumb-x.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/capsule-dark/thumb-x-line.png */
+
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+ /* alternate: rail-x-dots.png */
+ /* alternate: rail-x-lines.png */
+}
+
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail {
+ height: 25px;
+}
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-thumb {
+ height: 30px;
+ width: 14px;
+}
+
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -10px;
+}
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -50px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/capsule-dark/thumb-y.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/capsule-dark/thumb-y-line.png */
+
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+ /* alternate: rail-y-dots.png */
+ /* alternate: rail-y-lines.png */
+}
+
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail {
+ width: 25px;
+}
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-thumb {
+ width: 30px;
+ height: 14px;
+}
+
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-thumb-image {
+ left: -10px;
+ top: 0;
+}
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -50px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
diff --git a/js/yui3/assets/skin/capsule/skin.css b/js/yui3/assets/skin/capsule/skin.css
new file mode 100644
index 000000000..1e755e585
--- /dev/null
+++ b/js/yui3/assets/skin/capsule/skin.css
@@ -0,0 +1,201 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/capsule/thumb-x.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/capsule/thumb-x-line.png */
+
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+ background-repeat: repeat-x;
+ /* alternate: rail-x-dots.png */
+ /* alternate: rail-x-lines.png */
+}
+
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail {
+ height: 25px;
+}
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-thumb {
+ height: 30px;
+ width: 14px;
+}
+
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -10px;
+}
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -50px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/capsule/thumb-y.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/capsule/thumb-y-line.png */
+
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+ background-repeat: repeat-y;
+ /* alternate: rail-y-dots.png */
+ /* alternate: rail-y-lines.png */
+}
+
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail {
+ width: 25px;
+}
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-thumb {
+ width: 30px;
+ height: 14px;
+}
+
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-thumb-image {
+ left: -10px;
+ top: 0;
+}
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -50px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/capsule/thumb-x.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/capsule/thumb-x-line.png */
+
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+ background-repeat: repeat-x;
+ /* alternate: rail-x-dots.png */
+ /* alternate: rail-x-lines.png */
+}
+
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail {
+ height: 25px;
+}
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-thumb {
+ height: 30px;
+ width: 14px;
+}
+
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -10px;
+}
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -50px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/capsule/thumb-y.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/capsule/thumb-y-line.png */
+
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+ background-repeat: repeat-y;
+ /* alternate: rail-y-dots.png */
+ /* alternate: rail-y-lines.png */
+}
+
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail {
+ width: 25px;
+}
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-thumb {
+ width: 30px;
+ height: 14px;
+}
+
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-thumb-image {
+ left: -10px;
+ top: 0;
+}
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -50px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
diff --git a/js/yui3/assets/skin/night/skin.css b/js/yui3/assets/skin/night/skin.css
new file mode 100644
index 000000000..9cf5dd661
--- /dev/null
+++ b/js/yui3/assets/skin/night/skin.css
@@ -0,0 +1,1815 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night .yui3-scrollview-scrollbar {
+ -webkit-transform: translate3d(0, 0, 0);
+ -moz-transform: translate(0, 0);
+}
+
+.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-first,
+.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-middle,
+.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-last {
+ border-radius:3px;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ background-color:#808080;
+ opacity:0.3;
+ filter:alpha(opacity=30); /*IE*/
+
+
+/* background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAABCAYAAAD9yd/wAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABJJREFUeNpiZGBgSGPAAgACDAAIkABoFyloZQAAAABJRU5ErkJggg==);
+*/
+}
+
+.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-first,
+.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-last {
+ border-bottom-right-radius:0;
+ border-bottom-left-radius:0;
+
+ -webkit-border-bottom-right-radius:0;
+ -webkit-border-bottom-left-radius:0;
+
+ -moz-border-radius-bottomright:0;
+ -moz-border-radius-bottomleft:0;
+}
+.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-last {
+
+ border-radius:0;
+ border-bottom-right-radius:3px;
+ border-bottom-left-radius:3px;
+
+ -webkit-border-radius:0;
+ -webkit-border-bottom-right-radius:3px;
+ -webkit-border-bottom-left-radius:3px;
+ -webkit-transform: translate3d(0, 0, 0);
+
+ -moz-border-radius:0;
+ -moz-border-radius-bottomright:3px;
+ -moz-border-radius-bottomleft:3px;
+ -moz-transform: translate(0, 0);
+}
+
+.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-middle {
+ border-radius:0;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+
+ -webkit-transform: translate3d(0,0,0) scaleY(1);
+ -webkit-transform-origin: 0 0;
+
+ -moz-transform: translate(0,0) scaleY(1);
+ -moz-transform-origin: 0 0;
+}
+
+.yui3-skin-night .yui3-scrollview-scrollbar-horiz .yui3-scrollview-first,
+.yui3-skin-night .yui3-scrollview-scrollbar-horiz .yui3-scrollview-last {
+ border-top-right-radius: 0;
+ border-bottom-left-radius: 3px;
+
+ -webkit-border-top-right-radius: 0;
+ -webkit-border-bottom-left-radius: 3px;
+
+ -moz-border-radius-topright: 0;
+ -moz-border-radius-bottomleft: 3px;
+}
+
+.yui3-skin-night .yui3-scrollview-scrollbar-horiz .yui3-scrollview-last {
+ border-bottom-left-radius: 0;
+ border-top-right-radius: 3px;
+
+ -webkit-border-bottom-left-radius: 0;
+ -webkit-border-top-right-radius: 3px;
+
+ -moz-border-radius-bottomleft: 0;
+ -moz-border-radius-topright: 3px;
+}
+
+.yui3-skin-night .yui3-scrollview-scrollbar-horiz .yui3-scrollview-middle {
+ -webkit-transform: translate3d(0,0,0) scaleX(1);
+ -webkit-transform-origin: 0 0;
+
+ -moz-transform: translate(0,0) scaleX(1);
+ -moz-transform-origin: 0 0;
+}
+
+.yui3-skin-night .yui3-scrollview-scrollbar-vert-basic .yui3-scrollview-child,
+.yui3-skin-night .yui3-scrollview-scrollbar-horiz-basic .yui3-scrollview-child {
+ background-color: #aaa;
+ background-image: none;
+}
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-resize-handle-inner-r,
+.yui3-resize-handle-inner-l,
+.yui3-resize-handle-inner-t,
+.yui3-resize-handle-inner-b,
+.yui3-resize-handle-inner-tr,
+.yui3-resize-handle-inner-br,
+.yui3-resize-handle-inner-tl,
+.yui3-resize-handle-inner-bl {
+ background-repeat: no-repeat;
+ background: url(arrows.png) no-repeat 0 0;
+ display: block;
+ height: 15px;
+ overflow: hidden;
+ text-indent: -99999em;
+ width: 15px;
+}
+
+.yui3-resize-handle-inner-br {
+ background-position: -30px 0;
+ bottom: -2px;
+ right: -2px;
+}
+
+.yui3-resize-handle-inner-tr {
+ background-position: -58px 0;
+ bottom: 0;
+ right: -2px;
+
+}
+
+.yui3-resize-handle-inner-bl {
+ background-position: -75px 0;
+ bottom: -2px;
+ right: -2px;
+}
+
+.yui3-resize-handle-inner-tl {
+ background-position: -47px 0;
+ bottom: 0;
+ right: -2px;
+
+}
+
+.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-t {
+ background-position: -15px 0;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night .yui3-calendar-day-highlighted {
+ background-color: #555555;
+}
+
+.yui3-skin-night .yui3-calendar-day-selected.yui3-calendar-day-highlighted {
+ background-color: #777777;
+}
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night{
+ background-color:#000;
+ font-family: HelveticaNeue,arial,helvetica,clean,sans-serif;
+ color:#fff;
+}
+
+.yui3-skin-night .yui3-overlay-content ul, ol, li {
+ margin: 0;
+ padding: 0;
+ list-style:none;
+ zoom:1;
+}
+.yui3-skin-night .yui3-overlay-content li{
+ *float:left;
+}
+.yui3-skin-night .yui3-overlay-content {
+ background-color:#6d6e6e;
+ -moz-box-shadow:0 0 17px rgba(0,0,0,0.58);
+ -webkit-box-shadow:0 0 17px rgba(0,0,0,0.58);
+ box-shadow:0 0 17px rgba(0,0,0,0.58);
+ -moz-border-radius:7px;
+ -webkit-border-radius:7px;
+ border-radius:7px;
+}
+
+.yui3-skin-night .yui3-overlay-content .yui3-widget-hd {
+ background-color:#6d6e6e;
+ -moz-border-radius:7px 7px 0 0;
+ -webkit-border-radius:7px 7px 0 0;
+ border-radius:7px 7px 0 0;
+ color: #fff;
+
+ margin:0;
+ padding:20px 22px 0;
+ font-size:147%;
+}
+
+
+/*** background of bd and ft ***/
+.yui3-skin-night .yui3-overlay-content .yui3-widget-bd {
+ padding:11px 22px 17px;
+ font-size:92%;
+ /*margin-bottom:-1px; this was needed on ipad to close gap between ft and bd*/
+}
+.yui3-skin-night .yui3-overlay .yui3-widget-bd li{
+ margin: 0.04em;
+}
+/*.yui3-skin-night .yui3-widget-bd li:last-child{
+ margin-bottom:0.4em;
+}*/
+.yui3-skin-night .yui3-overlay-content .yui3-widget-ft{
+ background-color:#575858;
+ border-top:solid 1px #494a4a;
+ -moz-border-radius:0 0 7px 7px;
+ -webkit-border-radius:0 0 7px 7px;
+ border-radius:0 0 7px 7px;
+ padding:17px 25px 20px;
+ text-align:center;
+ /*font-size:92%;*/
+}
+/* For Buttons */
+
+.yui3-skin-night .yui3-overlay-content .yui3-widget-ft li {
+ margin:3px;
+ display:inline-block;
+}
+.yui3-skin-night .yui3-overlay-content .yui3-widget-ft li a{
+ border:solid 1px #1B1C1C;
+ border-radius: 6px;
+
+ -moz-box-shadow: 0 1px #677478;
+ -webkit-box-shadow: 0 1px #677478;
+ box-shadow: 0 1px #677478;
+
+ text-shadow: 0 -1px 0 rgba(0,0,0,0.7);
+ font-size:85%;
+ text-align:center;
+ color: #fff;
+ padding: 6px 28px;
+ background-color:#2B2D2D;
+ background: -moz-linear-gradient(
+ 0% 100% 90deg,
+ #242526 0%,
+ #3b3c3d 96%,
+ #2C2D2F 100%
+ );
+ background: -webkit-gradient(
+ linear,
+ left bottom,
+ left top,
+ from(#242526),
+ color-stop(0.96, #3b3c3d),
+ to(#2C2D2F)
+ );
+}
+.yui3-skin-night .yui3-overlay .yui3-widget-ft li:first-child {
+ margin-left:0;
+}
+
+.yui3-skin-night .yui3-overlay .yui3-widget-ft li:last-child {
+ margin-right:0;
+}
+
+.yui3-skin-night .yui3-overlay .yui3-widget-ft li:last-child a {
+ border:solid 1px #520E00;
+ -moz-box-shadow: 0 1px #7D5D57;
+ -webkit-box-shadow: 0 1px #7D5D57;
+ box-shadow: 0 1px #7D5D57;
+ background-color:#901704;
+ background: -moz-linear-gradient(
+ 100% 0% 270deg,
+ #ab1c0b,
+ #7b1400
+ );
+ background: -webkit-gradient(
+ linear,
+ left top,
+ left bottom,
+ from(#ab1c0b),
+ to(#7b1400)
+ );
+ margin-right: 0;
+}
+
+
+/*
+.yui3-skin-night .yui3-overlay .yui3-widget-ft li{
+ display:inline-block;
+ -moz-border-radius:6px;
+ -webkit-border-radius:6px;
+ border-radius:6px;
+ background-color:#252525;
+ background: -moz-linear-gradient(
+ 100% 0% 270deg,
+ #3f3f3f,
+ #0e0e0e
+ );
+ background: -webkit-gradient(
+ linear,
+ left top,
+ left bottom,
+ from(#3f3f3f),
+ to(#0e0e0e)
+ );
+
+ line-height:32px;
+ width:100px;
+ margin:0px 5px 0px 5px;
+}
+.yui3-skin-night .yui3-overlay .yui3-widget-ft li:first-child {
+ margin-left:0px;
+}
+.yui3-skin-night .yui3-overlay .yui3-widget-ft li:last-child {
+ background-color:#901704;
+ background: -moz-linear-gradient(
+ 100% 0% 270deg,
+ #ab1c0b,
+ #7b1400
+ );
+ background: -webkit-gradient(
+ linear,
+ left top,
+ left bottom,
+ from(#ab1c0b),
+ to(#7b1400)
+ );
+ margin-right: 0px;
+}
+*/
+#yui3-widget-mask{
+ background-color:#000;
+ opacity:0.5;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Vertical menus and submenus */
+
+.yui3-skin-night .yui3-menu-content,
+.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-content {
+
+ font-size: 100%;
+ line-height: 2.25; /* 18px 1.5*/
+ *line-height: 1.45; /* For IE */
+ border: solid 1px #303030;
+ background: #151515;
+ /*padding: 3px 0;*/
+
+}
+
+.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-content {
+
+ font-size: 100%;
+
+}
+
+
+/* Horizontal menus */
+.yui3-skin-night .yui3-menu-horizontal .yui3-menu-content {
+
+ line-height: 2; /* ~24px */
+ *line-height: 1.9; /* For IE */
+ background-color:#3b3c3d;
+ background: -moz-linear-gradient(
+ 0% 100% 90deg,
+ #242526 0%,
+ #3b3c3d 96%,
+ #2C2D2F 100%
+ );
+ background: -webkit-gradient(
+ linear,
+ left bottom,
+ left top,
+ from(#242526),
+ color-stop(0.96, #3b3c3d),
+ to(#2C2D2F)
+ );
+ padding: 0;
+
+}
+
+
+.yui3-skin-night .yui3-menu ul,
+.yui3-skin-night .yui3-menu ul ul {
+
+ margin-top: 3px;
+ padding-top: 3px;
+ border-top: solid 1px #303030;
+
+}
+
+.yui3-skin-night .yui3-menu ul.first-of-type {
+
+ border: 0;
+ margin: 0;
+ padding: 0;
+
+}
+
+.yui3-skin-night .yui3-menu-horizontal ul {
+
+ padding: 0;
+ margin: 0;
+ border: 0;
+
+}
+
+
+.yui3-skin-night .yui3-menu li,
+.yui3-skin-night .yui3-menu .yui3-menu li {
+
+ /*
+ For and IE 6 (Strict Mode and Quirks Mode) and IE 7 (Quirks Mode only):
+ Used to collapse superfluous white space between <li> elements that is
+ triggered by the "display" property of the <a> elements being set to
+ "block" by node-menunav-core.css file.
+ */
+
+ _border-bottom: solid 1px #151515;
+
+}
+
+.yui3-skin-night .yui3-menu-horizontal li {
+
+ _border-bottom: 0;
+
+}
+
+.yui3-skin-night .yui3-menubuttonnav li {
+
+ border-right: solid 1px #ccc;
+
+}
+
+.yui3-skin-night .yui3-splitbuttonnav li {
+
+ border-right: solid 1px #303030;
+
+}
+
+.yui3-skin-night .yui3-menubuttonnav li li,
+.yui3-skin-night .yui3-splitbuttonnav li li {
+
+ border-right: 0;
+
+}
+
+
+/* Menuitems and menu labels */
+
+
+.yui3-skin-night .yui3-menu-label,
+.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-label,
+.yui3-skin-night .yui3-menuitem-content,
+.yui3-skin-night .yui3-menu .yui3-menu .yui3-menuitem-content {
+
+ /*padding: 0 20px;*/
+ padding: 0 1em;
+ /*background-color: #2F3030;*/
+
+
+
+
+
+
+ color: #fff;
+ text-decoration: none;
+ cursor: default;
+
+ /*
+ Necessary specify values for border, position and margin to override
+ values specified in the selectors that follow.
+ */
+
+ float: none;
+ border: 0;
+ margin: 0;
+
+}
+
+.yui3-skin-night .yui3-menu-horizontal .yui3-menu-label,
+.yui3-skin-night .yui3-menu-horizontal .yui3-menuitem-content {
+
+ padding: 0 10px;
+ border-style: solid;
+ border-color: #303030;
+ border-width: 1px 0;
+ margin: -1px 0;
+
+ float: left; /* Ensures that menu labels clear floated descendents.
+ Also gets negative margins working in IE 7
+ (Strict Mode). */
+ width: auto;
+
+}
+
+.yui3-skin-night .yui3-menu-label,
+.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-label {
+
+ background: url(vertical-menu-submenu-indicator.png) right center no-repeat;
+
+}
+
+.yui3-skin-night .yui3-menu-horizontal .yui3-menu-label {
+
+ background: none;
+
+}
+
+.yui3-skin-night .yui3-menubuttonnav .yui3-menu-label,
+.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label {
+
+ background-image: none;
+
+}
+
+.yui3-skin-night .yui3-menubuttonnav .yui3-menu-label {
+
+ padding-right: 0;
+
+}
+
+.yui3-skin-night .yui3-menubuttonnav .yui3-menu-label em {
+
+ font-style: normal;
+ padding-right: 20px;
+ display: block;
+ background: url(horizontal-menu-submenu-indicator.png) right center no-repeat;
+
+}
+
+
+.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label {
+
+ padding: 0;
+
+}
+
+.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label a {
+
+ float: left;
+ width: auto;
+ color: #fff;
+ text-decoration: none;
+ cursor: default;
+ padding: 0 5px 0 10px;
+
+}
+
+.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label .yui3-menu-toggle {
+
+ padding: 0; /* Overide padding applied by the preceeding rule. */
+ border-left: solid 1px #303030;
+ width: 15px;
+ overflow: hidden;
+ text-indent: -1000px;
+ background: url(horizontal-menu-submenu-indicator.png) 3px center no-repeat;
+
+}
+
+
+/* Selected menuitem */
+
+.yui3-skin-night .yui3-menu-label-active,
+.yui3-skin-night .yui3-menu-label-menuvisible,
+.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-label-active,
+.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-label-menuvisible {
+
+ background-color: #292a2a;
+
+}
+
+.yui3-skin-night .yui3-menuitem-active .yui3-menuitem-content,
+.yui3-skin-night .yui3-menu .yui3-menu .yui3-menuitem-active .yui3-menuitem-content {
+
+ background-image: none;
+ background-color: #292a2a;
+ background: -moz-linear-gradient(
+ 0% 100% 90deg,
+ #252626 0%,
+ #333434 100%
+ );
+ background: -webkit-gradient(
+ linear,
+ left top,
+ left bottom,
+ from(#333434),
+ to(#252626)
+ );
+
+ /*
+ Undo values set for "border-left-width" and "margin-left" when the root
+ menu has a class of "yui-menubuttonnav" or "yui-splitbuttonnav" applied.
+ */
+
+ border-left-width: 0;
+ margin-left: 0;
+
+}
+
+.yui3-skin-night .yui3-menu-horizontal .yui3-menu-label-active,
+.yui3-skin-night .yui3-menu-horizontal .yui3-menuitem-active .yui3-menuitem-content,
+.yui3-skin-night .yui3-menu-horizontal .yui3-menu-label-menuvisible {
+
+ border-color: #303030;
+ background-color:#555658;
+ background: -moz-linear-gradient(
+ 0% 100% 90deg,
+ #343536 0%,
+ #555658 96%,
+ #3E3F41 100%
+ );
+ background: -webkit-gradient(
+ linear,
+ left bottom,
+ left top,
+ from(#343536),
+ color-stop(0.96, #555658),
+ to(#3E3F41)
+ );
+
+}
+
+.yui3-skin-night .yui3-menubuttonnav .yui3-menu-label-active,
+.yui3-skin-night .yui3-menubuttonnav .yui3-menuitem-active .yui3-menuitem-content,
+.yui3-skin-night .yui3-menubuttonnav .yui3-menu-label-menuvisible,
+.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label-active,
+.yui3-skin-night .yui3-splitbuttonnav .yui3-menuitem-active .yui3-menuitem-content,
+.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label-menuvisible {
+
+ border-left-width: 1px;
+ margin-left: -1px;
+
+}
+
+.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label-menuvisible {
+
+ border-color: #303030;
+ background: transparent;
+
+}
+
+.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label-menuvisible .yui3-menu-toggle {
+
+ border-color: #303030;
+ background-color: #505050;
+
+}
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night .yui3-calendar-content {
+ padding:10px;
+ color: #CBCBCB;
+ border: 1px solid #303030;
+ background: #151515; /* Old browsers */
+ background: -moz-linear-gradient(top, #222222 0%, #151515 100%); /* FF3.6+ */
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#222222), color-stop(100%,#151515)); /* Chrome,Safari4+ */
+ background: -webkit-linear-gradient(top, #222222 0%,#151515 100%); /* Chrome10+,Safari5.1+ */
+ background: -o-linear-gradient(top, #222222 0%,#151515 100%); /* Opera11.10+ */
+ background: -ms-linear-gradient(top, #222222 0%,#151515 100%); /* IE10+ */
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#222222', endColorstr='#151515',GradientType=0 ); /* IE6-9 */
+ background: linear-gradient(top, #222222 0%,#151515 100%); /* W3C */
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+
+.yui3-skin-night .yui3-calendar-grid {
+ padding:5px;
+ border-collapse: collapse;
+}
+
+.yui3-skin-night .yui3-calendar-header {
+ padding-bottom:10px;
+}
+
+.yui3-skin-night .yui3-calendar-header-label {
+ margin: 0;
+ font-size: 1em;
+ font-weight: bold;
+ text-align: center;
+ width: 100%;
+}
+
+.yui3-skin-night .yui3-calendar-day,
+.yui3-skin-night .yui3-calendar-prevmonth-day,
+.yui3-skin-night .yui3-calendar-nextmonth-day {
+ padding:5px;
+ border: 1px solid #151515;
+ background: #262727;
+ text-align:center;
+}
+
+.yui3-skin-night .yui3-calendar-day:hover {
+ background: #383939;
+ color: #FFFFFF;
+}
+
+.yui3-skin-night .yui3-calendar-selection-disabled,
+.yui3-skin-night .yui3-calendar-selection-disabled:hover {
+ background: #151515;
+ color: #596060;
+}
+
+.yui3-skin-night .yui3-calendar-weekday {
+ color: #4F4F4F;
+ font-weight: bold;
+ text-align: center;
+}
+
+.yui3-skin-night .yui3-calendar-prevmonth-day, .yui3-skin-night .yui3-calendar-nextmonth-day {
+ color: #4F4F4F;
+}
+
+.yui3-skin-night .yui3-calendar-day {
+ font-weight: bold;
+}
+
+.yui3-skin-night .yui3-calendar-day-selected {
+ background-color: #505151;
+ color: #fff;
+}
+
+.yui3-skin-night .yui3-calendar-left-grid {
+ margin-right:1em;
+}
+
+[dir="rtl"] .yui3-skin-night .yui3-calendar-left-grid,
+.yui3-skin-night [dir="rtl"] .yui3-calendar-left-grid {
+ margin-right: auto;
+ margin-left: 1em;
+}
+
+.yui3-skin-sam .yui3-calendar-right-grid {
+ margin-left:1em;
+}
+
+[dir="rtl"] .yui3-skin-night .yui3-calendar-right-grid,
+.yui3-skin-night [dir="rtl"] .yui3-calendar-right-grid {
+ margin-left: auto;
+ margin-right: 1em;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night [for=ac-input] { /* autocomplete label color */
+ color: #CBCBCB;
+}
+
+.yui3-skin-night .yui3-aclist-content {
+ font-size: 100%;
+ background-color: #151515;
+ color: #ccc;
+ border: 1px solid #303030;
+ -moz-box-shadow: 0 0 17px rgba(0,0,0,0.58);
+ -webkit-box-shadow: 0 0 17px rgba(0,0,0,0.58);
+ box-shadow: 0 0 17px rgba(0,0,0,0.58);
+}
+
+.yui3-skin-night .yui3-aclist-item-active {
+ background-color: #2F3030;
+ background: -moz-linear-gradient(
+ 0% 100% 90deg,
+ #252626 0%,
+ #333434 100%
+ );
+ background: -webkit-gradient(
+ linear,
+ left top,
+ left bottom,
+ from(#333434),
+ to(#252626)
+ );
+}
+.yui3-skin-night .yui3-aclist-item-hover {
+ background-color: #262727;
+ background: -moz-linear-gradient(
+ 0% 100% 90deg,
+ #202121 0%,
+ #282929 100%
+ );
+ background: -webkit-gradient(
+ linear,
+ left top,
+ left bottom,
+ from(#282929),
+ to(#202121)
+ );
+}
+
+.yui3-skin-night .yui3-aclist-item {
+ padding: 0 1em; /*0.4em 1em 0.6em*/
+ line-height: 2.25;
+}
+.yui3-skin-night .yui3-aclist-item-active { outline: none; }
+.yui3-skin-night .yui3-highlight { color:#EFEFEF; }
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-datatable-scroll-columns {
+ border-collapse: separate;
+ border-spacing: 0;
+ font-family: HelveticaNeue,arial,helvetica,clean,sans-serif;
+ margin: 0;
+ padding: 0;
+ top: 0;
+ left: 0;
+}
+
+.yui3-skin-sam .yui3-datatable-scroll-columns .yui3-datatable-header {
+ padding: 0;
+}
+
+.yui3-skin-sam .yui3-datatable-x-scroller,
+.yui3-skin-sam .yui3-datatable-y-scroller-container {
+ border: 1px solid #303030;
+ border-left-color: #323434;
+}
+
+.yui3-skin-sam .yui3-datatable-scrollable-x .yui3-datatable-y-scroller-container,
+.yui3-skin-sam .yui3-datatable-x-scroller .yui3-datatable-table,
+.yui3-skin-sam .yui3-datatable-y-scroller .yui3-datatable-table {
+ border: 0 none;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night .yui3-calendarnav-prevmonth, .yui3-skin-night .yui3-calendarnav-nextmonth {
+ width: 0;
+ height: 0;
+ padding: 0;
+ margin: 0;
+ border: 10px solid transparent;
+ position: absolute;
+ /* ie6 height fix */
+ font-size: 0;
+ line-height: 0;
+ /* ie6 transparent fix */
+ _border-left-color: black;
+ _border-top-color: black;
+ _border-right-color: black;
+ _border-bottom-color: black;
+ _filter: chroma(color=black);
+}
+
+.yui3-skin-night .yui3-calendarnav-prevmonth:hover,
+[dir="rtl"] .yui3-skin-night .yui3-calendarnav-nextmonth:hover,
+.yui3-skin-night [dir="rtl"] .yui3-calendarnav-nextmonth:hover {
+ border-right-color: #0066CC;
+}
+
+.yui3-skin-night .yui3-calendarnav-nextmonth:hover,
+[dir="rtl"] .yui3-skin-night .yui3-calendarnav-prevmonth:hover,
+.yui3-skin-night [dir="rtl"] .yui3-calendarnav-prevmonth:hover {
+ border-left-color: #0066CC;
+}
+
+.yui3-skin-night .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,
+.yui3-skin-night .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover,
+[dir="rtl"] .yui3-skin-night .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,
+.yui3-skin-night [dir="rtl"] .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,
+[dir="rtl"] .yui3-skin-night .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover,
+.yui3-skin-night [dir="rtl"] .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover {
+ cursor: default;
+ border-right-color: #CCCCCC;
+ border-left-color: transparent;
+}
+.yui3-skin-night .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,
+.yui3-skin-night .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover,
+[dir="rtl"] .yui3-skin-night .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,
+.yui3-skin-night [dir="rtl"] .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,
+[dir="rtl"] .yui3-skin-night .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover,
+.yui3-skin-night [dir="rtl"] .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover {
+ cursor: default;
+ border-left-color: #CCCCCC;
+ border-right-color: transparent;
+}
+
+.yui3-skin-night .yui3-calendarnav-prevmonth {
+ border-right-color: #FFFFFF;
+ left: 0;
+ margin-left: -10px;
+}
+
+.yui3-skin-night .yui3-calendarnav-nextmonth {
+ border-left-color: #FFFFFF;
+ right: 0;
+ margin-right: -10px;
+}
+
+[dir="rtl"] .yui3-skin-night .yui3-calendarnav-prevmonth,
+.yui3-skin-night [dir="rtl"] .yui3-calendarnav-prevmonth {
+ left: auto;
+ right: 0;
+ border-left-color: #FFFFFF;
+ border-right-color: transparent;
+}
+
+[dir="rtl"] .yui3-skin-night .yui3-calendarnav-nextmonth,
+.yui3-skin-night [dir="rtl"] .yui3-calendarnav-nextmonth {
+ left: 0;
+ right: auto;
+ border-right-color: #FFFFFF;
+ border-left-color: transparent;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night .yui3-tabview-panel{
+ background-color:#333333;
+ color:#808080;
+ padding:1px;
+}
+.yui3-skin-night .yui3-tab-panel p{
+ margin:10px;
+}
+.yui3-skin-night .yui3-tabview-list {
+ background-color:#0f0f0f;
+ border-top:1px solid #000;
+ text-align:center;
+ height:46px;
+ background: -moz-linear-gradient(
+ 0% 100% 90deg,
+ #0f0f0f 0%,
+ #1e1e1e 96%,
+ #292929 100%
+ );
+ background: -webkit-gradient(
+ linear,
+ left bottom,
+ left top,
+ from(#0f0f0f),
+ color-stop(0.96, #1e1e1e),
+ to(#292929)
+ );
+}
+
+.yui3-skin-night .yui3-tabview-list li {
+ margin-top:8px;
+}
+.yui3-skin-night .yui3-tabview-list li a{
+ border:solid 1px #0c0c0c;
+ border-right-style:none;
+
+ -moz-box-shadow: 0 1px #222222;
+ -webkit-box-shadow: 0 1px #222222;
+ box-shadow: 0 1px #222222;
+
+ text-shadow: 0 -1px 0 rgba(0,0,0,0.7);
+ font-size:85%;
+ text-align:center;
+ color: #fff;
+ padding: 6px 28px;
+ background-color:#555658;
+ background: -moz-linear-gradient(
+ 0% 100% 90deg,
+ #343536 0%,
+ #555658 96%,
+ #3E3F41 100%
+ );
+ background: -webkit-gradient(
+ linear,
+ left bottom,
+ left top,
+ from(#343536),
+ color-stop(0.96, #555658),
+ to(#3E3F41)
+ );
+}
+.yui3-skin-night .yui3-tabview-list li.yui3-tab-selected a {
+ background-color:#2B2D2D;
+ background: -moz-linear-gradient(
+ 0% 100% 90deg,
+ #242526 0%,
+ #3b3c3d 96%,
+ #2C2D2F 100%
+ );
+ background: -webkit-gradient(
+ linear,
+ left bottom,
+ left top,
+ from(#242526),
+ color-stop(0.96, #3b3c3d),
+ to(#2C2D2F)
+ );
+}
+.yui3-skin-night .yui3-tabview-list li:first-child a{
+ -moz-border-radius:6px 0 0 6px;
+ -webkit-border-radius:6px 0 0 6px;
+ border-radius:6px 0 0 6px;
+}
+.yui3-skin-night .yui3-tabview-list li:last-child a{
+ border-right-style:solid;
+ -moz-border-radius:0 6px 6px 0;
+ -webkit-border-radius:0 6px 6px 0;
+ border-radius:0 6px 6px 0;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* basic skin styles */
+.yui3-skin-night .yui3-datatable {
+ color:#8E8E8E;
+ font-family: HelveticaNeue,arial,helvetica,clean,sans-serif;
+}
+.yui3-skin-night .yui3-datatable-table {
+ border: 1px solid #323434;
+ border-collapse: separate;
+ border-spacing: 0;
+ color: #8E8E8E;
+ margin: 0;
+ padding: 0;
+}
+
+.yui3-skin-night .yui3-datatable-caption {
+ color: #474747;
+ font: italic 85%/1 HelveticaNeue,arial,helvetica,clean,sans-serif;
+ padding: 1em 0;
+ text-align: center;
+}
+
+.yui3-skin-night .yui3-datatable-cell,
+.yui3-skin-night .yui3-datatable-header {
+ border-left: 1px solid #303030;/* inner column border */
+ border-width: 0 0 0 1px;
+ font-size: inherit;
+ margin: 0;
+ overflow: visible; /*to make ths where the title is really long work*/
+ padding: 4px 10px 4px 10px; /* cell padding */
+}
+.yui3-skin-night .yui3-datatable-cell:first-child,
+.yui3-skin-night .yui3-datatable-first-header {
+ border-left-width: 0;
+}
+
+.yui3-skin-night .yui3-datatable-header {
+ /* header gradient */
+ background-color:#3b3c3d;
+
+ background: -moz-linear-gradient(
+ 0% 100% 90deg,
+ #242526 0%,
+ #3b3c3d 96%,
+ #2C2D2F 100%
+ );
+ background: -webkit-gradient(
+ linear,
+ left bottom,
+ left top,
+ from(#242526),
+ color-stop(0.96, #3b3c3d),
+ to(#2C2D2F)
+ );
+ color: #eee;
+ font-weight: normal;
+ text-align: left;
+ vertical-align: bottom;
+ white-space: nowrap;
+}
+
+/*
+striping:
+ even - #0e0e0e (darkest)
+ odd - #1d1e1e (lighter)
+*/
+.yui3-skin-night .yui3-datatable-cell {
+ background-color: transparent;
+}
+.yui3-skin-night .yui3-datatable-even .yui3-datatable-cell {
+ background-color: #0e0e0e;
+}
+.yui3-skin-night .yui3-datatable-odd .yui3-datatable-cell {
+ background-color: #1d1e1e;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night .yui3-datatable-sortable-column {
+ cursor: pointer;
+}
+
+.yui3-skin-night .yui3-datatable-columns .yui3-datatable-sorted,
+.yui3-skin-night .yui3-datatable-sortable-column:hover {
+ background-color: #4D4E4F;
+ *background: #505152 url(../../../../assets/skins/night/sprite.png) repeat-x 0 -100px;
+
+ background-image: -webkit-gradient(
+ linear,
+ 0 0,
+ 0 100%,
+ from(rgba(255,255,255, 0.2)),
+ color-stop(40%, rgba(255,255,255, 0.1)),
+ color-stop(80%, rgba(255,255,255, 0.01)),
+ to(transparent));
+
+ background-image: -webkit-linear-gradient(
+ rgba(255,255,255, 0.2),
+ rgba(255,255,255, 0.1) 40%,
+ rgba(255,255,255, 0.01) 80%,
+ transparent);
+
+ background-image: -moz-linear-gradient(
+ top,
+ rgba(255,255,255, 0.2),
+ rgba(255,255,255, 0.1) 40%,
+ rgba(255,255,255, 0.01) 80%,
+ transparent);
+
+ background-image: -ms-linear-gradient(
+ rgba(255,255,255, 0.2),
+ rgba(255,255,255, 0.1) 40%,
+ rgba(255,255,255, 0.01) 80%,
+ transparent);
+
+ background-image: -o-linear-gradient(
+ rgba(255,255,255, 0.2),
+ rgba(255,255,255, 0.1) 40%,
+ rgba(255,255,255, 0.01) 80%,
+ transparent);
+
+ background-image: linear-gradient(
+ rgba(255,255,255, 0.2),
+ rgba(255,255,255, 0.1) 40%,
+ rgba(255,255,255, 0.01) 80%,
+ transparent);
+}
+
+.yui3-skin-night .yui3-datatable-sort-liner {
+ display: block;
+ height: 100%;
+ position: relative;
+ padding-right: 15px;
+ position: relative;
+}
+
+.yui3-skin-night .yui3-datatable-sort-indicator {
+ position: absolute;
+ right: 0;
+ bottom: .5ex;
+ width: 7px;
+ height: 10px;
+ background: url(sort-arrow-sprite.png) no-repeat 0 0;
+ _background: url(sort-arrow-sprite-ie.png) no-repeat 0 0;
+ overflow: hidden;
+}
+
+.yui3-skin-night .yui3-datatable-sorted .yui3-datatable-sort-indicator {
+ background-position: 0 -10px;
+}
+.yui3-skin-night .yui3-datatable-sorted-desc .yui3-datatable-sort-indicator {
+ background-position: 0 -20px;
+}
+
+.yui3-skin-night .yui3-datatable-data .yui3-datatable-even .yui3-datatable-sorted {
+ background-color: #262626;
+ color: #B3B2B2;
+}
+
+.yui3-skin-night .yui3-datatable-data .yui3-datatable-odd .yui3-datatable-sorted {
+ background-color: #393A3A;
+ color: #CBCBCB;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night .yui3-datatable .yui3-datatable-highlight-row td {
+ background-color: #38383f;
+}
+
+.yui3-skin-night .yui3-datatable tr .yui3-datatable-highlight-col {
+ background-color: #38383f;
+}
+
+.yui3-skin-night .yui3-datatable tr .yui3-datatable-highlight-cell {
+ background-color: #38383f;
+}
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night .yui3-widget-mask {
+ background-color: black;
+ zoom: 1;
+ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(opacity=40)";
+ filter: alpha(opacity=40);
+ opacity: 0.4;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night .yui3-panel {
+ color: #FFFFFF;
+ font-family: HelveticaNeue, arial, helvetica, clean, sans-serif;
+}
+
+.yui3-skin-night .yui3-panel-content {
+ background: #6D6E6E;
+ -webkit-box-shadow: 0 0 20px #000;
+ -moz-box-shadow: 0 0 20px #000;
+ box-shadow: 0 0 20px #000;
+ border: 1px solid black;
+ -webkit-border-radius: 7px;
+ -moz-border-radius: 7px;
+ border-radius: 7px;
+}
+.yui3-skin-night .yui3-panel .yui3-widget-hd {
+ padding: 11px 57px 11px 22px; /* Room for close button. */
+ min-height: 17px; /* For the close button */
+ _height: 17px; /* IE6 */
+ -webkit-border-top-left-radius: 7px;
+ -webkit-border-top-right-radius: 7px;
+ -moz-border-radius-topleft: 7px;
+ -moz-border-radius-topright: 7px;
+ border-top-left-radius: 7px;
+ border-top-right-radius: 7px;
+ font-weight: bold;
+ color: white;
+ background-color: #555658;
+ background: -moz-linear-gradient(
+ 0% 100% 90deg,
+ #343536 0%,
+ #555658 96%,
+ #3E3F41 100%
+ );
+ background: -webkit-gradient(
+ linear,
+ left bottom,
+ left top,
+ from(#343536),
+ color-stop(0.96, #555658),
+ to(#3E3F41)
+ );
+}
+.yui3-skin-night .yui3-panel .yui3-widget-hd .yui3-widget-buttons {
+ padding: 11px;
+}
+.yui3-skin-night .yui3-panel .yui3-widget-bd {
+ padding: 11px 22px 17px;
+}
+.yui3-skin-night .yui3-panel .yui3-widget-ft {
+ background-color: #575858;
+ border-top: 1px solid #494A4A;
+ padding: 6px 16px 8px;
+ text-align: center;
+ -webkit-border-bottom-right-radius: 7px;
+ -webkit-border-bottom-left-radius: 7px;
+ -moz-border-radius-bottomright: 7px;
+ -moz-border-radius-bottomleft: 7px;
+ border-bottom-right-radius: 7px;
+ border-bottom-left-radius: 7px;
+}
+.yui3-skin-night .yui3-panel .yui3-widget-ft .yui3-widget-buttons {
+ bottom: 0;
+ position: relative;
+ right: auto;
+ width: 100%;
+ text-align: center;
+ padding-bottom: 0;
+ margin-left: -5px;
+ margin-right: -5px;
+}
+.yui3-skin-night .yui3-panel .yui3-widget-ft .yui3-button {
+ margin: 5px;
+}
+
+/*
+Support for icon-based [x] "close" button in the header.
+
+Nicolas Gallagher: "CSS image replacement with pseudo-elements (NIR)"
+http://nicolasgallagher.com/css-image-replacement-with-pseudo-elements/
+*/
+.yui3-skin-night .yui3-panel .yui3-widget-hd .yui3-button-close {
+ /* Reset base button styles */
+ background: transparent;
+ filter: none;
+ border: none;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+
+ /* Structure */
+ width: 22px;
+ height: 17px;
+ padding: 0;
+ overflow: hidden;
+ vertical-align: top;
+ /* IE < 8 :( */
+ *font-size: 0;
+ *line-height: 0;
+ *letter-spacing: -1000px;
+ *color: #86A5EC;
+ *background: url(sprite_icons.png) no-repeat center 3px;
+}
+.yui3-skin-night .yui3-panel .yui3-widget-hd .yui3-button-close:hover {
+ background-color: #333;
+}
+.yui3-skin-night .yui3-panel .yui3-widget-hd .yui3-button-close:before {
+ /*
+ Displays the [x] icon in place of the "Close" text.
+ Note: The `width` of this pseudo element is the same as its "host" element.
+ */
+ content: url(sprite_icons.png);
+ display: inline-block;
+ text-align: center;
+ font-size: 0;
+ line-height: 0;
+ width: 22px;
+ margin: 3px 0 0 1px;
+}
+.yui3-skin-night .yui3-panel-hidden .yui3-widget-hd .yui3-button-close {
+ /* Required for IE > 7 to deal with pseudo :before element */
+ display: none;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night .yui3-scrollview {
+ -webkit-tap-highlight-color: rgba(0,0,0,0);
+}
+
+.yui3-skin-night .yui3-scrollview{
+ color:#fff;
+ background-color:#000;
+}
+
+.yui3-skin-night .yui3-scrollview-vert .yui3-scrollview-content {
+ /*border:1px solid #303030; If the ScrollView needs a border add it here */
+ border-top:0;
+ background-color:#000;
+ font-family: HelveticaNeue,arial,helvetica,clean,sans-serif;
+ color:#fff;
+}
+
+/* For IE 6/7 - needs a background color (above) to pick up events, and zoom, to fill the UL */
+.yui3-skin-night .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-item {
+ *zoom:1;
+}
+
+/* For IE7 - needs zoom, otherwise clipped content is not rendered */
+.yui3-skin-night .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-list {
+ *zoom:1;
+ list-style:none; /*need these since reset is not required*/
+ padding:0; /*need these since reset is not required*/
+ margin:0; /*need these since reset is not required*/
+}
+
+.yui3-skin-night .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-item {
+ border-bottom: 1px solid #303030;
+ padding: 15px 20px 16px;
+ font-size: 100%;
+ font-weight: bold;
+ background-color:#151515;
+ cursor:pointer;
+}
+
+.yui3-skin-night .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-list.selected{
+ background-color:#2C2D2E;
+ background: -moz-linear-gradient(
+ 0% 100% 90deg,
+ #252626 0%,
+ #333434 100%
+ );
+ background: -webkit-gradient(
+ linear,
+ left top,
+ left bottom,
+ from(#333434),
+ to(#252626)
+ );
+ border-top:solid 1px #4b4b4b;
+ border-bottom:solid 1px #3e3f3f;
+ margin-top:-1px;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/sam-dark/thumb-x.png */
+
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+ /* alternate: rail-x-lines.png */
+}
+
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail {
+ height: 26px;
+}
+.yui3-skin-night .yui3-slider-x .yui3-slider-thumb {
+ height: 26px;
+ width: 21px;
+}
+
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-night .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -10px;
+}
+.yui3-skin-night .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -50px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/sam-dark/thumb-y.png */
+
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+ /* alternate: rail-y-lines.png */
+}
+
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail {
+ width: 26px;
+}
+.yui3-skin-night .yui3-slider-y .yui3-slider-thumb {
+ width: 26px;
+ height: 15px;
+}
+
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-night .yui3-slider-y .yui3-slider-thumb-image {
+ left: -10px;
+ top: 0;
+}
+.yui3-skin-night .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -50px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/night/thumb-x.png */
+
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+ /* alternate: rail-x-lines.png */
+}
+
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail {
+ height: 25px;
+}
+.yui3-skin-night .yui3-slider-x .yui3-slider-thumb {
+ height: 26px;
+ width: 21px;
+}
+
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 20px;
+ left: -5px;
+ width: 5px;
+}
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 20px;
+ right: -5px;
+ width: 5px;
+}
+
+.yui3-skin-night .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -10px;
+}
+.yui3-skin-night .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -50px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/night/thumb-y.png */
+
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+ /* alternate: rail-y-lines.png */
+}
+
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail {
+ width: 25px;
+}
+.yui3-skin-night .yui3-slider-y .yui3-slider-thumb {
+ width: 26px;
+ height: 21px;
+}
+
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 20px;
+ top: -5px;
+ height: 5px;
+}
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 20px;
+ bottom: -5px;
+ height: 5px;
+}
+
+.yui3-skin-night .yui3-slider-y .yui3-slider-thumb-image {
+ left: -10px;
+ top: 0;
+}
+.yui3-skin-night .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -50px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-paginator {
+ background: white url(../../../../assets/skins/night/sprite.png) repeat-x 0 0;
+ background-image: -webkit-linear-gradient(transparent 40%, hsla(0, 0%, 0%, 0.21));
+ background-image: -moz-linear-gradient(top, transparent 40%, hsla(0, 0%, 0%, 0.21));
+ background-image: -ms-linear-gradient(transparent 40%, hsla(0, 0%, 0%, 0.21));
+ background-image: -o-linear-gradient(transparent 40%, hsla(0, 0%, 0%, 0.21));
+ background-image: linear-gradient(transparent 40%, hsla(0, 0%, 0%, 0.21));
+}
+
+.yui3-datatable-paginator .yui3-datatable-paginator-control {
+ color: #242D42;
+}
+
+.yui3-datatable-paginator .yui3-datatable-paginator-control-first:hover,
+.yui3-datatable-paginator .yui3-datatable-paginator-control-last:hover,
+.yui3-datatable-paginator .yui3-datatable-paginator-control-prev:hover,
+.yui3-datatable-paginator .yui3-datatable-paginator-control-next:hover {
+ box-shadow: 0 1px 2px #292442;
+}
+
+.yui3-datatable-paginator .yui3-datatable-paginator-control-first:active,
+.yui3-datatable-paginator .yui3-datatable-paginator-control-last:active,
+.yui3-datatable-paginator .yui3-datatable-paginator-control-prev:active,
+.yui3-datatable-paginator .yui3-datatable-paginator-control-next:active {
+ box-shadow: inset 0 1px 1px #292442;
+ background: #E0DEED;
+ background: hsla(250, 30%, 90%, 0.3);
+}
+
+.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled,
+.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled:hover {
+ color: #BDC7DB;
+ border-color: transparent;
+ box-shadow: none;
+}
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night .yui3-dial {
+ color:#fff;
+}
+
+.yui3-skin-night .yui3-dial-handle{ /*container. top left corner used for trig positioning*/
+ background:#439EDE;
+ opacity:0.3;
+ -moz-box-shadow:1px 1px 1px rgba(0, 0, 0, 0.9) inset;
+ -webkit-box-shadow:1px 1px 1px rgba(0, 0, 0, 0.9) inset; /*Chrome 7/Win bug*/
+ box-shadow:1px 1px 1px rgba(0, 0, 0, 0.9) inset;
+ cursor:pointer;
+ font-size:1px;
+}
+.yui3-skin-night .yui3-dial-ring {
+ background:#595B5B;
+ background:-moz-linear-gradient(0% 100% 315deg, #5E6060, #2D2E2F);
+ background:-webkit-gradient(linear, 50% 0%, 100% 100%, from(#636666), to(#424344));
+ -moz-box-shadow:1px 1px 2px rgba(0, 0, 0, 0.7) inset;
+ -webkit-box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.7) inset; /*Chrome 7/Win bug*/
+ box-shadow:1px 1px 5px rgba(0, 0, 0, 0.4) inset;
+}
+.yui3-skin-night .yui3-dial-center-button{
+ -moz-box-shadow:-1px -1px 2px rgba(0, 0, 0, 0.3) inset, 1px 1px 2px rgba(0, 0, 0, 0.5);
+ -webkit-box-shadow: -1px -1px 2px rgba(0, 0, 0, 0.3) inset, 1px 1px 2px rgba(0, 0, 0, 0.5); /*Chrome 7/Win bug*/
+ box-shadow:-1px -1px 2px rgba(0, 0, 0, 0.3) inset, 1px 1px 2px rgba(0, 0, 0, 0.5);
+ background:#DDDBD4;
+ background:-moz-radial-gradient(
+ 30% 30% 0deg,
+ circle farthest-side,
+ #999C9C 24%,
+ #898989 41%,
+ #535555 87%)
+ repeat scroll 0 0 transparent;
+ background:-webkit-gradient(
+ radial, 15 15, 15, 30 30, 40,
+ from(#999C9C),
+ to(#535555),
+ color-stop(.2,#898989));
+ cursor:pointer;
+ opacity:0.7;
+ /*text-align:center;*/
+}
+.yui3-skin-night .yui3-dial-reset-string{
+ color:#fff;
+ font-size:72%;
+ text-decoration:none;
+}
+.yui3-skin-night .yui3-dial-label{
+ color:#CBCBCB;
+ margin-bottom:0.8em;
+}
+.yui3-skin-night .yui3-dial-value-string{
+ margin-left:0.5em;
+ color:#DCDCDC;
+ font-size:130%;
+}
+.yui3-skin-night .yui3-dial-value {
+ visibility:hidden;
+ position:absolute;
+ top:0;
+ left:102%;
+ width:4em;
+}
+.yui3-skin-night .yui3-dial-north-mark{
+ position:absolute;
+ border-left:2px solid #434343;
+ height:5px;
+ left:50%;
+ top:-7px;
+ font-size:1px;
+}
+.yui3-skin-night .yui3-dial-marker {
+ background-color:#A0D8FF;
+ opacity:0.2;
+ font-size:1px;
+}
+.yui3-skin-night .yui3-dial-marker-max-min{
+ background-color:#FF0404;
+ opacity:0.6;
+}
+.yui3-skin-night .yui3-dial-ring-vml,
+.yui3-skin-night .yui3-dial-center-button-vml,
+.yui3-skin-night .yui3-dial-marker v\:oval.yui3-dial-marker-max-min,
+.yui3-skin-night v\:oval.yui3-dial-marker-max-min,
+.yui3-skin-night .yui3-dial-marker-vml,
+.yui3-skin-night .yui3-dial-handle-vml {
+ background: none;
+ opacity:1;
+}
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night .yui3-datatable-message-content {
+ background-color: #0e0e0e;
+ border: 0 none;
+ border-bottom: 1px solid #303030;
+ padding: 4px 10px;
+}
diff --git a/js/yui3/assets/skin/round-dark/skin.css b/js/yui3/assets/skin/round-dark/skin.css
new file mode 100644
index 000000000..f96969b28
--- /dev/null
+++ b/js/yui3/assets/skin/round-dark/skin.css
@@ -0,0 +1,193 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/round-dark/thumb-x.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/round-dark/thumb-x-grip.png */
+
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+}
+
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail {
+ height: 25px;
+ background-position: 0 3px;
+}
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-thumb {
+ height: 26px;
+ width: 24px;
+}
+
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -17px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -37px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -7px;
+}
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -47px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/round-dark/thumb-y.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/round-dark/thumb-y-grip.png */
+
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+}
+
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail {
+ width: 25px;
+ background-position: 3px 0;
+}
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-thumb {
+ width: 26px;
+ height: 24px;
+}
+
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -17px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -37px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-thumb-image {
+ top: 0;
+ left: -7px;
+}
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-thumb-shadow {
+ top: 0;
+ left: -47px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/round-dark/thumb-x.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/round-dark/thumb-x-grip.png */
+
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+}
+
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail {
+ height: 25px;
+ background-position: 0 3px;
+}
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-thumb {
+ height: 26px;
+ width: 24px;
+}
+
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -17px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -37px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -7px;
+}
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -47px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/round-dark/thumb-y.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/round-dark/thumb-y-grip.png */
+
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+}
+
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail {
+ width: 25px;
+ background-position: 3px 0;
+}
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-thumb {
+ width: 26px;
+ height: 24px;
+}
+
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -17px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -37px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-thumb-image {
+ top: 0;
+ left: -7px;
+}
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-thumb-shadow {
+ top: 0;
+ left: -47px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+}
diff --git a/js/yui3/assets/skin/round/skin.css b/js/yui3/assets/skin/round/skin.css
new file mode 100644
index 000000000..3afb724d6
--- /dev/null
+++ b/js/yui3/assets/skin/round/skin.css
@@ -0,0 +1,193 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/round/thumb-x.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/round/thumb-x-grip.png */
+
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+}
+
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail {
+ height: 25px;
+ background-position: 0 3px;
+}
+.yui3-skin-round .yui3-slider-x .yui3-slider-thumb {
+ height: 26px;
+ width: 24px;
+}
+
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -17px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -37px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-round .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -7px;
+}
+.yui3-skin-round .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -47px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/round/thumb-y.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/round/thumb-y-grip.png */
+
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+}
+
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail {
+ width: 25px;
+ background-position: 3px 0;
+}
+.yui3-skin-round .yui3-slider-y .yui3-slider-thumb {
+ width: 26px;
+ height: 24px;
+}
+
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -17px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -37px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-round .yui3-slider-y .yui3-slider-thumb-image {
+ top: 0;
+ left: -8px;
+}
+.yui3-skin-round .yui3-slider-y .yui3-slider-thumb-shadow {
+ top: 0;
+ left: -48px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/round/thumb-x.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/round/thumb-x-grip.png */
+
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+}
+
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail {
+ height: 25px;
+ background-position: 0 3px;
+}
+.yui3-skin-round .yui3-slider-x .yui3-slider-thumb {
+ height: 26px;
+ width: 24px;
+}
+
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -17px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -37px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-round .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -7px;
+}
+.yui3-skin-round .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -47px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/round/thumb-y.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/round/thumb-y-grip.png */
+
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+}
+
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail {
+ width: 25px;
+ background-position: 3px 0;
+}
+.yui3-skin-round .yui3-slider-y .yui3-slider-thumb {
+ width: 26px;
+ height: 24px;
+}
+
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -17px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -37px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-round .yui3-slider-y .yui3-slider-thumb-image {
+ top: 0;
+ left: -8px;
+}
+.yui3-skin-round .yui3-slider-y .yui3-slider-thumb-shadow {
+ top: 0;
+ left: -48px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+}
diff --git a/js/yui3/assets/skin/sam-dark/skin.css b/js/yui3/assets/skin/sam-dark/skin.css
new file mode 100644
index 000000000..d6d43fda7
--- /dev/null
+++ b/js/yui3/assets/skin/sam-dark/skin.css
@@ -0,0 +1,189 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/sam-dark/thumb-x.png */
+
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+ /* alternate: rail-x-lines.png */
+}
+
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail {
+ height: 26px;
+}
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-thumb {
+ height: 26px;
+ width: 15px;
+}
+
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -10px;
+}
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -50px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/sam-dark/thumb-y.png */
+
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+ /* alternate: rail-y-lines.png */
+}
+
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail {
+ width: 26px;
+}
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-thumb {
+ width: 26px;
+ height: 15px;
+}
+
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-thumb-image {
+ left: -10px;
+ top: 0;
+}
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -50px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/sam-dark/thumb-x.png */
+
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+ /* alternate: rail-x-lines.png */
+}
+
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail {
+ height: 26px;
+}
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-thumb {
+ height: 26px;
+ width: 15px;
+}
+
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -10px;
+}
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -50px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/sam-dark/thumb-y.png */
+
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+ /* alternate: rail-y-lines.png */
+}
+
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail {
+ width: 26px;
+}
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-thumb {
+ width: 26px;
+ height: 15px;
+}
+
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-thumb-image {
+ left: -10px;
+ top: 0;
+}
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -50px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
diff --git a/js/yui3/assets/skin/sam/skin.css b/js/yui3/assets/skin/sam/skin.css
new file mode 100644
index 000000000..80d01413f
--- /dev/null
+++ b/js/yui3/assets/skin/sam/skin.css
@@ -0,0 +1,1727 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-scrollview-scrollbar {
+ -webkit-transform: translate3d(0, 0, 0);
+ -moz-transform: translate(0, 0);
+}
+
+.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-first,
+.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-middle,
+.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-last {
+ border-radius:3px;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAABCAYAAAD9yd/wAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABJJREFUeNpiZGBgSGPAAgACDAAIkABoFyloZQAAAABJRU5ErkJggg==);
+}
+
+.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-first,
+.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-last {
+ border-bottom-right-radius:0;
+ border-bottom-left-radius:0;
+
+ -webkit-border-bottom-right-radius:0;
+ -webkit-border-bottom-left-radius:0;
+
+ -moz-border-radius-bottomright:0;
+ -moz-border-radius-bottomleft:0;
+}
+
+.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-last {
+ border-radius:0;
+ border-bottom-right-radius:3px;
+ border-bottom-left-radius:3px;
+
+ -webkit-border-radius:0;
+ -webkit-border-bottom-right-radius:3px;
+ -webkit-border-bottom-left-radius:3px;
+ -webkit-transform: translate3d(0, 0, 0);
+
+ -moz-border-radius:0;
+ -moz-border-radius-bottomright:3px;
+ -moz-border-radius-bottomleft:3px;
+ -moz-transform: translate(0, 0);
+}
+
+.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-middle {
+ border-radius:0;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+
+ -webkit-transform: translate3d(0,0,0) scaleY(1);
+ -webkit-transform-origin-y: 0;
+
+ -moz-transform: translate(0,0) scaleY(1);
+ -moz-transform-origin: 0 0;
+}
+
+.yui3-skin-sam .yui3-scrollview-scrollbar-horiz .yui3-scrollview-first,
+.yui3-skin-sam .yui3-scrollview-scrollbar-horiz .yui3-scrollview-last {
+ border-top-right-radius: 0;
+ border-bottom-left-radius: 3px;
+
+ -webkit-border-top-right-radius: 0;
+ -webkit-border-bottom-left-radius: 3px;
+
+ -moz-border-radius-topright: 0;
+ -moz-border-radius-bottomleft: 3px;
+}
+
+.yui3-skin-sam .yui3-scrollview-scrollbar-horiz .yui3-scrollview-last {
+ border-bottom-left-radius: 0;
+ border-top-right-radius: 3px;
+
+ -webkit-border-bottom-left-radius: 0;
+ -webkit-border-top-right-radius: 3px;
+
+ -moz-border-radius-bottomleft: 0;
+ -moz-border-radius-topright: 3px;
+}
+
+.yui3-skin-sam .yui3-scrollview-scrollbar-horiz .yui3-scrollview-middle {
+ -webkit-transform: translate3d(0,0,0) scaleX(1);
+ -webkit-transform-origin: 0 0;
+
+ -moz-transform: translate(0,0) scaleX(1);
+ -moz-transform-origin: 0 0;
+}
+
+.yui3-skin-sam .yui3-scrollview-scrollbar-vert-basic .yui3-scrollview-child,
+.yui3-skin-sam .yui3-scrollview-scrollbar-horiz-basic .yui3-scrollview-child {
+ background-color: #aaa;
+ background-image: none;
+}
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-resize-handle-inner-r,
+.yui3-resize-handle-inner-l,
+.yui3-resize-handle-inner-t,
+.yui3-resize-handle-inner-b,
+.yui3-resize-handle-inner-tr,
+.yui3-resize-handle-inner-br,
+.yui3-resize-handle-inner-tl,
+.yui3-resize-handle-inner-bl {
+ background-repeat: no-repeat;
+ background: url(arrows.png) no-repeat 0 0;
+ display: block;
+ height: 15px;
+ overflow: hidden;
+ text-indent: -99999em;
+ width: 15px;
+}
+
+.yui3-resize-handle-inner-br {
+ background-position: -30px 0;
+ bottom: -2px;
+ right: -2px;
+}
+
+.yui3-resize-handle-inner-tr {
+ background-position: -58px 0;
+ bottom: 0;
+ right: -2px;
+
+}
+
+.yui3-resize-handle-inner-bl {
+ background-position: -75px 0;
+ bottom: -2px;
+ right: -2px;
+}
+
+.yui3-resize-handle-inner-tl {
+ background-position: -47px 0;
+ bottom: 0;
+ right: -2px;
+
+}
+
+.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-t {
+ background-position: -15px 0;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-calendar-day-highlighted {
+ background-color: #DCDEF5;
+}
+
+.yui3-skin-sam .yui3-calendar-day-selected.yui3-calendar-day-highlighted {
+ background-color: #758FBB;
+}
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Vertical menus and submenus */
+
+.yui3-skin-sam .yui3-menu-content,
+.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-content {
+
+ font-size: 93%; /* 12px */
+ line-height: 1.5; /* 18px */
+ *line-height: 1.45; /* For IE */
+ border: solid 1px #808080;
+ background: #fff;
+ padding: 3px 0;
+
+}
+
+.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-content {
+
+ font-size: 100%;
+
+}
+
+
+/* Horizontal menus */
+
+.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-content {
+
+ line-height: 2; /* ~24px */
+ *line-height: 1.9; /* For IE */
+ background: url(../../../../assets/skins/sam/sprite.png) repeat-x 0 0;
+ padding: 0;
+
+}
+
+
+.yui3-skin-sam .yui3-menu ul,
+.yui3-skin-sam .yui3-menu ul ul {
+
+ margin-top: 3px;
+ padding-top: 3px;
+ border-top: solid 1px #ccc;
+
+}
+
+.yui3-skin-sam .yui3-menu ul.first-of-type {
+
+ border: 0;
+ margin: 0;
+ padding: 0;
+
+}
+
+.yui3-skin-sam .yui3-menu-horizontal ul {
+
+ padding: 0;
+ margin: 0;
+ border: 0;
+
+}
+
+
+.yui3-skin-sam .yui3-menu li,
+.yui3-skin-sam .yui3-menu .yui3-menu li {
+
+ /*
+ For and IE 6 (Strict Mode and Quirks Mode) and IE 7 (Quirks Mode only):
+ Used to collapse superfluous white space between <li> elements that is
+ triggered by the "display" property of the <a> elements being set to
+ "block" by node-menunav-core.css file.
+ */
+
+ _border-bottom: solid 1px #fff;
+
+}
+
+.yui3-skin-sam .yui3-menu-horizontal li {
+
+ _border-bottom: 0;
+
+}
+
+.yui3-skin-sam .yui3-menubuttonnav li {
+
+ border-right: solid 1px #ccc;
+
+}
+
+.yui3-skin-sam .yui3-splitbuttonnav li {
+
+ border-right: solid 1px #808080;
+
+}
+
+.yui3-skin-sam .yui3-menubuttonnav li li,
+.yui3-skin-sam .yui3-splitbuttonnav li li {
+
+ border-right: 0;
+
+}
+
+
+/* Menuitems and menu labels */
+
+
+.yui3-skin-sam .yui3-menu-label,
+.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label,
+.yui3-skin-sam .yui3-menuitem-content,
+.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menuitem-content {
+
+ /*padding: 0 20px;*/
+ padding: 0 1em;
+ color: #000;
+ text-decoration: none;
+ cursor: default;
+
+ /*
+ Necessary specify values for border, position and margin to override
+ values specified in the selectors that follow.
+ */
+
+ float: none;
+ border: 0;
+ margin: 0;
+
+}
+
+.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label,
+.yui3-skin-sam .yui3-menu-horizontal .yui3-menuitem-content {
+
+ padding: 0 10px;
+ border-style: solid;
+ border-color: #808080;
+ border-width: 1px 0;
+ margin: -1px 0;
+
+ float: left; /* Ensures that menu labels clear floated descendents.
+ Also gets negative margins working in IE 7
+ (Strict Mode). */
+ width: auto;
+
+}
+
+.yui3-skin-sam .yui3-menu-label,
+.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label {
+
+ background: url(vertical-menu-submenu-indicator.png) right center no-repeat;
+
+}
+
+.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label {
+
+ background: url(../../../../assets/skins/sam/sprite.png) repeat-x 0 0;
+
+}
+
+.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label,
+.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label {
+
+ background-image: none;
+
+}
+
+.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label {
+
+ padding-right: 0;
+
+}
+
+.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label em {
+
+ font-style: normal;
+ padding-right: 20px;
+ display: block;
+ background: url(horizontal-menu-submenu-indicator.png) right center no-repeat;
+
+}
+
+
+.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label {
+
+ padding: 0;
+
+}
+
+.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label a {
+
+ float: left;
+ width: auto;
+ color: #000;
+ text-decoration: none;
+ cursor: default;
+ padding: 0 5px 0 10px;
+
+}
+
+.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label .yui3-menu-toggle {
+
+ padding: 0; /* Overide padding applied by the preceeding rule. */
+ border-left: solid 1px #ccc;
+ width: 15px;
+ overflow: hidden;
+ text-indent: -1000px;
+ background: url(horizontal-menu-submenu-indicator.png) 3px center no-repeat;
+
+}
+
+
+/* Selected menuitem */
+
+.yui3-skin-sam .yui3-menu-label-active,
+.yui3-skin-sam .yui3-menu-label-menuvisible,
+.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label-active,
+.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label-menuvisible {
+
+ background-color: #B3D4FF;
+
+}
+
+.yui3-skin-sam .yui3-menuitem-active .yui3-menuitem-content,
+.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menuitem-active .yui3-menuitem-content {
+
+ background-image: none;
+ background-color: #B3D4FF;
+
+ /*
+ Undo values set for "border-left-width" and "margin-left" when the root
+ menu has a class of "yui-menubuttonnav" or "yui-splitbuttonnav" applied.
+ */
+
+ border-left-width: 0;
+ margin-left: 0;
+
+}
+
+.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label-active,
+.yui3-skin-sam .yui3-menu-horizontal .yui3-menuitem-active .yui3-menuitem-content,
+.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label-menuvisible {
+
+ border-color: #7D98B8;
+ background: url(../../../../assets/skins/sam/sprite.png) repeat-x 0 -1700px;
+
+}
+
+.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label-active,
+.yui3-skin-sam .yui3-menubuttonnav .yui3-menuitem-active .yui3-menuitem-content,
+.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label-menuvisible,
+.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-active,
+.yui3-skin-sam .yui3-splitbuttonnav .yui3-menuitem-active .yui3-menuitem-content,
+.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-menuvisible {
+
+ border-left-width: 1px;
+ margin-left: -1px;
+
+}
+
+.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-menuvisible {
+
+ border-color: #808080;
+ background: transparent;
+
+}
+
+.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-menuvisible .yui3-menu-toggle {
+
+ border-color: #7D98B8;
+ background: url(horizontal-menu-submenu-toggle.png) left center no-repeat;
+
+}
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-calendar-content {
+ padding:10px;
+ color: #000000;
+ border: 1px solid gray;
+ background: #f2f2f2; /* Old browsers */
+ background: -moz-linear-gradient(top, #f9f9f9 0%, #f2f2f2 100%); /* FF3.6+ */
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f9f9f9), color-stop(100%,#f2f2f2)); /* Chrome,Safari4+ */
+ background: -webkit-linear-gradient(top, #f9f9f9 0%,#f2f2f2 100%); /* Chrome10+,Safari5.1+ */
+ background: -o-linear-gradient(top, #f9f9f9 0%,#f2f2f2 100%); /* Opera11.10+ */
+ background: -ms-linear-gradient(top, #f9f9f9 0%,#f2f2f2 100%); /* IE10+ */
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f9f9f9', endColorstr='#f2f2f2',GradientType=0 ); /* IE6-9 */
+ background: linear-gradient(top, #f9f9f9 0%,#f2f2f2 100%); /* W3C */
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+.yui3-skin-sam .yui3-calendar-grid {
+ padding:5px;
+ border-collapse: collapse;
+}
+
+.yui3-skin-sam .yui3-calendar-header {
+ padding-bottom:10px;
+}
+
+.yui3-skin-sam .yui3-calendar-header-label {
+ margin: 0;
+ font-size: 1em;
+ font-weight: bold;
+ text-align: center;
+ width: 100%;
+}
+
+.yui3-skin-sam .yui3-calendar-day,
+.yui3-skin-sam .yui3-calendar-prevmonth-day,
+.yui3-skin-sam .yui3-calendar-nextmonth-day {
+ padding:5px;
+ border: 1px solid #CCCCCC;
+ background: #FFFFFF;
+ text-align:center;
+}
+
+
+.yui3-skin-sam .yui3-calendar-day:hover {
+ background: #0066CC;
+ color: #FFFFFF;
+}
+
+.yui3-skin-sam .yui3-calendar-selection-disabled,
+.yui3-skin-sam .yui3-calendar-selection-disabled:hover {
+ color: #A6A6A6;
+ background: #CCCCCC;
+}
+
+.yui3-skin-sam .yui3-calendar-weekday {
+ font-weight: bold;
+}
+
+.yui3-skin-sam .yui3-calendar-prevmonth-day, .yui3-skin-sam .yui3-calendar-nextmonth-day {
+ color: #A6A6A6;
+}
+
+.yui3-skin-sam .yui3-calendar-day {
+ font-weight: bold;
+}
+
+.yui3-skin-sam .yui3-calendar-day-selected {
+ background-color: #B3D4FF;
+ color: #000000;
+}
+
+.yui3-skin-sam .yui3-calendar-left-grid {
+ margin-right:1em;
+}
+
+[dir="rtl"] .yui3-skin-sam .yui3-calendar-left-grid,
+.yui3-skin-sam [dir="rtl"] .yui3-calendar-left-grid {
+ margin-right: auto;
+ margin-left: 1em;
+}
+
+.yui3-skin-sam .yui3-calendar-right-grid {
+ margin-left:1em;
+}
+
+[dir="rtl"] .yui3-skin-sam .yui3-calendar-right-grid,
+.yui3-skin-sam [dir="rtl"] .yui3-calendar-right-grid {
+ margin-left: auto;
+ margin-right: 1em;
+}
+
+.yui3-skin-sam .yui3-calendar-day-highlighted {
+ background-color: #DCDEF5;
+}
+
+.yui3-skin-sam .yui3-calendar-day-selected.yui3-calendar-day-highlighted {
+ background-color: #758FBB;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-aclist-content {
+ background: #fff;
+ border: 1px solid #afafaf;
+ -moz-box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.58);
+ -webkit-box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.58);
+ box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.58);
+}
+
+.yui3-skin-sam .yui3-aclist-item-hover {
+ background: #bfdaff;
+}
+
+.yui3-skin-sam .yui3-aclist-item-active {
+ background: #2647a0;
+ color: #fff;
+ outline: none;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-datatable-scroll-columns {
+ border-collapse: separate;
+ border-spacing: 0;
+ font-family: arial, sans-serif;
+ margin: 0;
+ padding: 0;
+ top: 0;
+ left: 0;
+}
+
+.yui3-skin-sam .yui3-datatable-scroll-columns .yui3-datatable-header {
+ padding: 0;
+}
+
+.yui3-skin-sam .yui3-datatable-x-scroller,
+.yui3-skin-sam .yui3-datatable-y-scroller-container {
+ border: 1px solid #cbcbcb;
+}
+
+.yui3-skin-sam .yui3-datatable-scrollable-x .yui3-datatable-y-scroller-container,
+.yui3-skin-sam .yui3-datatable-x-scroller .yui3-datatable-table,
+.yui3-skin-sam .yui3-datatable-y-scroller .yui3-datatable-table {
+ border: 0 none;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-console-separate {
+ position:absolute;
+ right:1em;
+ top:1em;
+ z-index:999;
+}
+
+.yui3-skin-sam .yui3-console-inline {
+ /* xbrowser inline-block styles */
+ display: -moz-inline-stack; /* FF2 */
+ display: inline-block;
+ *display: inline; /* IE 7- (with zoom) */
+ zoom: 1;
+ vertical-align: top;
+}
+.yui3-skin-sam .yui3-console-inline .yui3-console-content {
+ position: relative;
+}
+
+.yui3-skin-sam .yui3-console-content {
+ background: #777;
+ _background: #D8D8DA url(bg.png) repeat-x 0 0;
+ font: normal 13px/1.3 Arial, sans-serif;
+ text-align: left;
+
+ border: 1px solid #777;
+ border-radius: 10px;
+ -moz-border-radius: 10px;
+ -webkit-border-radius: 10px;
+}
+
+.yui3-skin-sam .yui3-console-hd,
+.yui3-skin-sam .yui3-console-bd,
+.yui3-skin-sam .yui3-console-ft {
+ position: relative;
+}
+
+.yui3-skin-sam .yui3-console-hd,
+.yui3-skin-sam .yui3-console-ft .yui3-console-controls {
+ text-align: right;
+}
+
+.yui3-skin-sam .yui3-console-hd {
+ background: #D8D8DA url(bg.png) repeat-x 0 0;
+ padding: 1ex;
+
+ border: 1px solid transparent;
+ _border: 0 none;
+ border-top-right-radius: 10px;
+ border-top-left-radius: 10px;
+ -moz-border-radius-topright: 10px;
+ -moz-border-radius-topleft: 10px;
+ -webkit-border-top-right-radius: 10px;
+ -webkit-border-top-left-radius: 10px;
+}
+
+.yui3-skin-sam .yui3-console-bd {
+ background: #fff;
+ border-top: 1px solid #777;
+ border-bottom: 1px solid #777;
+ color: #000;
+ font-size: 11px;
+ overflow: auto;
+ overflow-x: auto;
+ overflow-y: scroll;
+ _width: 100%;
+}
+
+.yui3-skin-sam .yui3-console-ft {
+ background: #D8D8DA url(bg.png) repeat-x 0 0;
+
+ border: 1px solid transparent;
+ _border: 0 none;
+ border-bottom-right-radius: 10px;
+ border-bottom-left-radius: 10px;
+ -moz-border-radius-bottomright: 10px;
+ -moz-border-radius-bottomleft: 10px;
+ -webkit-border-bottom-right-radius: 10px;
+ -webkit-border-bottom-left-radius: 10px;
+}
+
+.yui3-skin-sam .yui3-console-controls {
+ padding: 4px 1ex;
+ zoom: 1;
+}
+
+.yui3-skin-sam .yui3-console-title {
+ color: #000;
+ display: inline;
+ float: left;
+ font-weight: bold;
+ font-size: 13px;
+ height: 24px;
+ line-height: 24px;
+ margin: 0;
+ padding-left: 1ex;
+}
+
+.yui3-skin-sam .yui3-console-pause-label {
+ float: left;
+}
+.yui3-skin-sam .yui3-console-button {
+ line-height: 1.3;
+}
+
+.yui3-skin-sam .yui3-console-collapsed .yui3-console-bd,
+.yui3-skin-sam .yui3-console-collapsed .yui3-console-ft {
+ display: none;
+}
+.yui3-skin-sam .yui3-console-content.yui3-console-collapsed {
+ -webkit-border-radius: 0;
+}
+.yui3-skin-sam .yui3-console-collapsed .yui3-console-hd {
+ border-radius: 10px;
+ -moz-border-radius: 10px;
+ -webkit-border-radius: 0;
+}
+
+/* Log entries */
+.yui3-skin-sam .yui3-console-entry {
+ border-bottom: 1px solid #aaa;
+ min-height: 32px;
+ _height: 32px;
+}
+
+.yui3-skin-sam .yui3-console-entry-meta {
+ margin: 0;
+ overflow: hidden;
+}
+
+.yui3-skin-sam .yui3-console-entry-content {
+ margin: 0;
+ padding: 0 1ex;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+.yui3-skin-sam .yui3-console-entry-meta .yui3-console-entry-src {
+ color: #000;
+ font-style: italic;
+ font-weight: bold;
+ float: right;
+ margin: 2px 5px 0 0;
+}
+.yui3-skin-sam .yui3-console-entry-meta .yui3-console-entry-time {
+ color: #777;
+ padding-left: 1ex;
+}
+.yui3-skin-sam .yui3-console-entry-warn .yui3-console-entry-meta .yui3-console-entry-time {
+ color: #555;
+}
+
+.yui3-skin-sam .yui3-console-entry-info .yui3-console-entry-meta .yui3-console-entry-cat,
+.yui3-skin-sam .yui3-console-entry-warn .yui3-console-entry-meta .yui3-console-entry-cat,
+.yui3-skin-sam .yui3-console-entry-error .yui3-console-entry-meta .yui3-console-entry-cat {
+ display: none;
+}
+.yui3-skin-sam .yui3-console-entry-warn {
+ background: #aee url(warn_error.png) no-repeat -15px 15px;
+}
+.yui3-skin-sam .yui3-console-entry-error {
+ background: #ffa url(warn_error.png) no-repeat 5px -24px;
+ color: #900;
+}
+.yui3-skin-sam .yui3-console-entry-warn .yui3-console-entry-content,
+.yui3-skin-sam .yui3-console-entry-error .yui3-console-entry-content {
+ padding-left: 24px;
+}
+.yui3-skin-sam .yui3-console-entry-cat {
+ text-transform: uppercase;
+ padding: 1px 4px;
+ background-color: #ccc;
+}
+.yui3-skin-sam .yui3-console-entry-info .yui3-console-entry-cat {
+ background-color: #ac2;
+}
+.yui3-skin-sam .yui3-console-entry-warn .yui3-console-entry-cat {
+ background-color: #e81;
+}
+.yui3-skin-sam .yui3-console-entry-error .yui3-console-entry-cat {
+ background-color: #b00;
+ color: #fff;
+}
+
+.yui3-skin-sam .yui3-console-hidden { display: none; }
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-calendarnav-prevmonth, .yui3-skin-sam .yui3-calendarnav-nextmonth {
+ width: 0;
+ height: 0;
+ padding: 0;
+ margin: 0;
+ border: 10px solid transparent;
+ position: absolute;
+ /* ie6 height fix */
+ font-size: 0;
+ line-height: 0;
+ /* ie6 transparent fix */
+ _border-left-color: white;
+ _border-top-color: white;
+ _border-right-color: white;
+ _border-bottom-color: white;
+ _filter: chroma(color=white);
+}
+
+.yui3-skin-sam .yui3-calendarnav-prevmonth:hover,
+[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-nextmonth:hover,
+.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-nextmonth:hover {
+ border-right-color: #0066CC;
+}
+
+.yui3-skin-sam .yui3-calendarnav-nextmonth:hover,
+[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-prevmonth:hover,
+.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-prevmonth:hover {
+ border-left-color: #0066CC;
+}
+
+.yui3-skin-sam .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,
+.yui3-skin-sam .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover,
+[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,
+.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,
+[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover,
+.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover {
+ cursor: default;
+ border-right-color: #CCCCCC;
+ border-left-color: transparent;
+}
+.yui3-skin-sam .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,
+.yui3-skin-sam .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover,
+[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,
+.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,
+[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover,
+.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover {
+ cursor: default;
+ border-left-color: #CCCCCC;
+ border-right-color: transparent;
+}
+
+.yui3-skin-sam .yui3-calendarnav-prevmonth {
+ border-right-color: #000000;
+ left: 0;
+ margin-left: -10px;
+}
+
+.yui3-skin-sam .yui3-calendarnav-nextmonth {
+ border-left-color: #000000;
+ right: 0;
+ margin-right: -10px;
+}
+
+[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-prevmonth,
+.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-prevmonth {
+ left: auto;
+ right: 0;
+ border-left-color: #000000;
+ border-right-color: transparent;
+}
+
+[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-nextmonth,
+.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-nextmonth {
+ left: 0;
+ right: auto;
+ border-right-color: #000000;
+ border-left-color: transparent;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* .yui-navset defaults to .yui-navset-top */
+.yui3-skin-sam .yui3-tabview-list {
+ border:solid #2647a0; /* color between tab list and content */
+ border-width:0 0 5px;
+ zoom:1;
+}
+
+.yui3-skin-sam .yui3-tab {
+ margin:0 0.2em 0 0;
+ padding:1px 0 0; /* gecko: make room for overflow */
+ zoom:1;
+}
+
+.yui3-skin-sam .yui3-tab-selected {
+ margin-bottom:-1px; /* for overlap (mapped to tabview-list border-width) */
+}
+
+.yui3-skin-sam .yui3-tab-label {
+ background:#d8d8d8 url(../../../../assets/skins/sam/sprite.png) repeat-x; /* tab background */
+ border:solid #a3a3a3;
+ border-width: 1px 1px 0 1px;
+ color:#000;
+ cursor:pointer;
+ font-size:85%;
+ padding:0.3em .75em;
+ text-decoration:none;
+}
+
+.yui3-skin-sam .yui3-tab-label:hover,
+.yui3-skin-sam .yui3-tab-label:focus {
+ background:#bfdaff url(../../../../assets/skins/sam/sprite.png) repeat-x left -1300px; /* hovered tab background */
+ outline:0;
+}
+
+.yui3-skin-sam .yui3-tab-selected .yui3-tab-label,
+.yui3-skin-sam .yui3-tab-selected .yui3-tab-label:focus,
+.yui3-skin-sam .yui3-tab-selected .yui3-tab-label:hover { /* no hover effect for selected */
+ background:#2647a0 url(../../../../assets/skins/sam/sprite.png) repeat-x left -1400px; /* selected tab background */
+ color:#fff;
+}
+
+.yui3-skin-sam .yui3-tab-selected .yui3-tab-label {
+ padding:0.4em 0.75em; /* raise selected tab */
+}
+
+.yui3-skin-sam .yui3-tab-selected .yui3-tab-label {
+ border-color:#243356; /* selected tab border color */
+}
+
+.yui3-skin-sam .yui3-tabview-panel {
+ background:#edf5ff; /* content background color */
+}
+
+.yui3-skin-sam .yui3-tabview-panel {
+ border:1px solid #808080; /* content border */
+ border-top-color:#243356; /* different border color */
+ padding:0.25em 0.5em; /* content padding */
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* basic skin styles */
+.yui3-skin-sam .yui3-datatable-table {
+ margin: 0;
+ padding: 0;
+ font-family: arial, sans-serif;
+ border-collapse: separate;
+ border-spacing: 0;
+ /* IE7- don't support border-spacing, but separate is needed for more
+ * reliable dimension calculation for scrollable's header width sync,
+ * and it doesn't hurt to have the declaration here.
+ */
+ /**border-collapse: collapse;*/
+ border: 1px solid #cbcbcb;
+}
+
+.yui3-skin-sam .yui3-datatable-caption {
+ color: #000;
+ font: italic 85%/1 arial, sans-serif;
+ padding: 1em 0;
+ text-align: center;
+}
+
+.yui3-skin-sam .yui3-datatable-cell,
+.yui3-skin-sam .yui3-datatable-header {
+ border-left: 1px solid #cbcbcb;/* inner column border */
+ border-width: 0 0 0 1px;
+ font-size: inherit;
+ margin: 0;
+ overflow: visible; /*to make ths where the title is really long work*/
+ padding: 4px 10px 4px 10px; /* cell padding */
+}
+.yui3-skin-sam .yui3-datatable-cell:first-child,
+.yui3-skin-sam .yui3-datatable-first-header {
+ border-left-width: 0px;
+}
+
+.yui3-skin-sam .yui3-datatable-header {
+ /* header gradient */
+ background: #fff url(../../../../assets/skins/sam/sprite.png) repeat-x 0 0;
+
+ background-image:-webkit-linear-gradient(transparent 40%, rgba(0,0,0,0.21));
+ background-image:-moz-linear-gradient(top,transparent 40%,rgba(0,0,0,0.21));
+ background-image: -ms-linear-gradient(transparent 40%, rgba(0,0,0,0.21));
+ background-image: -o-linear-gradient(transparent 40%, rgba(0,0,0,0.21));
+ background-image: linear-gradient(transparent 40%, rgba(0,0,0,0.21));
+ /* Not using an IE gradient because it doesn't support color stops */
+
+ color: #000;
+ font-weight: normal;
+ text-align: left;
+ text-shadow: 0 1px 1px #fff;
+ vertical-align: bottom;
+ white-space: nowrap;
+}
+
+/*
+striping:
+ even - #fff (white)
+ odd - #edf5ff (light blue)
+*/
+.yui3-skin-sam .yui3-datatable-cell {
+ background-color: transparent;
+}
+.yui3-skin-sam .yui3-datatable-even .yui3-datatable-cell {
+ background-color: #fff;
+}
+.yui3-skin-sam .yui3-datatable-odd .yui3-datatable-cell {
+ background-color: #edf5ff;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-datatable-sortable-column {
+ cursor: pointer;
+}
+
+.yui3-skin-sam .yui3-datatable-columns .yui3-datatable-sorted,
+.yui3-skin-sam .yui3-datatable-sortable-column:hover {
+ *background: #c1c4c8 url(../../../../assets/skins/sam/sprite.png) repeat-x 0 -100px;
+ background-color: #f1f2f3;
+}
+
+.yui3-skin-sam .yui3-datatable-sort-liner {
+ display: block;
+ height: 100%;
+ position: relative;
+ padding-right: 15px;
+ position: relative;
+}
+
+.yui3-skin-sam .yui3-datatable-sort-indicator {
+ position: absolute;
+ right: 0;
+ bottom: .5ex;
+ width: 7px;
+ height: 10px;
+ background: url(sort-arrow-sprite.png) no-repeat 0 0;
+ _background: url(sort-arrow-sprite-ie.png) no-repeat 0 0;
+ overflow: hidden;
+}
+
+.yui3-skin-sam .yui3-datatable-sorted .yui3-datatable-sort-indicator {
+ background-position: 0 -10px;
+}
+.yui3-skin-sam .yui3-datatable-sorted-desc .yui3-datatable-sort-indicator {
+ background-position: 0 -20px;
+}
+
+.yui3-skin-sam .yui3-datatable-data .yui3-datatable-even .yui3-datatable-sorted {
+ background-color: #edf5ff;
+}
+
+.yui3-skin-sam .yui3-datatable-data .yui3-datatable-odd .yui3-datatable-sorted {
+ background-color: #dbeaff;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-datatable .yui3-datatable-highlight-row td {
+ background-color: #fef2cd;
+}
+
+.yui3-skin-sam .yui3-datatable tr .yui3-datatable-highlight-col {
+ background-color: #fef2cd;
+}
+
+.yui3-skin-sam .yui3-datatable tr .yui3-datatable-highlight-cell {
+ background-color: #fef2cd;
+}
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-widget-mask {
+ background-color: black;
+ zoom: 1;
+ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(opacity=40)";
+ filter: alpha(opacity=40);
+ opacity: 0.4;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-panel-content {
+ -webkit-box-shadow: 0 0 5px #333;
+ -moz-box-shadow: 0 0 5px #333;
+ box-shadow: 0 0 5px #333;
+ border: 1px solid black;
+ background: white;
+}
+.yui3-skin-sam .yui3-panel .yui3-widget-hd {
+ padding: 8px 28px 8px 8px; /* Room for close button. */
+ min-height: 13px; /* For the close button */
+ _height: 13px; /* IE6 */
+ color: white;
+ background-color: #3961c5;
+ background: -moz-linear-gradient(
+ 0% 100% 90deg,
+ #2647a0 7%,
+ #3d67ce 50%,
+ #426fd9 100%
+ );
+ background: -webkit-gradient(
+ linear,
+ left bottom,
+ left top,
+ from(#2647a0),
+ color-stop(0.07, #2647a0),
+ color-stop(0.5, #3d67ce),
+ to(#426fd9)
+ );
+ /*
+ TODO: Add support for IE and W3C gradients
+ */
+}
+.yui3-skin-sam .yui3-panel .yui3-widget-hd .yui3-widget-buttons {
+ padding: 8px;
+}
+.yui3-skin-sam .yui3-panel .yui3-widget-bd {
+ padding: 10px;
+}
+.yui3-skin-sam .yui3-panel .yui3-widget-ft {
+ background: #EDF5FF;
+ padding: 8px;
+ text-align: right;
+}
+.yui3-skin-sam .yui3-panel .yui3-widget-ft .yui3-button {
+ margin-left: 8px;
+}
+
+/*
+Support for icon-based [x] "close" button in the header.
+
+Nicolas Gallagher: "CSS image replacement with pseudo-elements (NIR)"
+http://nicolasgallagher.com/css-image-replacement-with-pseudo-elements/
+*/
+.yui3-skin-sam .yui3-panel .yui3-widget-hd .yui3-button-close {
+ /* Reset base button styles */
+ background: transparent;
+ filter: none;
+ border: none;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+
+ /* Structure */
+ width: 13px;
+ height: 13px;
+ padding: 0;
+ overflow: hidden;
+ vertical-align: top;
+ /* IE < 8 :( */
+ *font-size: 0;
+ *line-height: 0;
+ *letter-spacing: -1000px;
+ *color: #86A5EC;
+ *background: url(sprite_icons.png) no-repeat 1px 1px;
+}
+.yui3-skin-sam .yui3-panel .yui3-widget-hd .yui3-button-close:before {
+ /*
+ Displays the [x] icon in place of the "Close" text.
+ Note: The `width` of this pseudo element is the same as its "host" element.
+ */
+ content: url(sprite_icons.png);
+ display: inline-block;
+ text-align: center;
+ font-size: 0;
+ line-height: 0;
+ width: 13px;
+ margin: 1px 0 0 1px;
+}
+.yui3-skin-sam .yui3-panel-hidden .yui3-widget-hd .yui3-button-close {
+ /* Required for IE > 7 to deal with pseudo :before element */
+ display: none;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-console-ft .yui3-console-filters-categories,
+.yui3-skin-sam .yui3-console-ft .yui3-console-filters-sources {
+ text-align: left;
+ padding: 5px 0;
+ border: 1px inset;
+ margin: 0 2px;
+}
+.yui3-skin-sam .yui3-console-ft .yui3-console-filters-categories {
+ background: #fff;
+ border-bottom: 2px ridge;
+}
+.yui3-skin-sam .yui3-console-ft .yui3-console-filters-sources {
+ background: #fff;
+ margin-bottom: 2px;
+
+ border-top: 0 none;
+ border-bottom-right-radius: 10px;
+ border-bottom-left-radius: 10px;
+ -moz-border-radius-bottomright: 10px;
+ -moz-border-radius-bottomleft: 10px;
+ -webkit-border-bottom-right-radius: 10px;
+ -webkit-border-bottom-left-radius: 10px;
+}
+.yui3-skin-sam .yui3-console-filter-label {
+ white-space: nowrap;
+ margin-left: 1ex;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-scrollview {
+ -webkit-tap-highlight-color: rgba(255,255,255,0);
+}
+
+.yui3-skin-sam .yui3-scrollview{
+ background-color: white;
+}
+/* For IE 6/7 - needs a background color (above) to pick up events, and zoom, to fill the UL */
+.yui3-skin-sam .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-item {
+ *zoom:1;
+}
+
+/* For IE7 - needs zoom, otherwise clipped content is not rendered */
+.yui3-skin-sam .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-list {
+ *zoom:1;
+ list-style:none; /*need these since reset is not required*/
+ padding:0; /*need these since reset is not required*/
+ margin:0; /*need these since reset is not required*/
+}
+
+.yui3-skin-sam .yui3-scrollview-vert .yui3-scrollview-content {
+ /*border:1px solid #303030; If the ScrollView needs a border add it here */
+ border-top:0;
+ background-color:white;
+ font-family: HelveticaNeue,arial,helvetica,clean,sans-serif;
+ color:black;
+}
+
+.yui3-skin-sam .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-item {
+ border-bottom: 1px solid #303030;
+ padding: 15px 20px 16px;
+ font-size: 100%;
+ font-weight: bold;
+ background-color:white;
+ cursor:pointer;
+}
+
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* empty */
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/sam/thumb-x.png */
+
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+ /* alternate: rail-x-lines.png */
+}
+
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail {
+ height: 26px;
+}
+.yui3-skin-sam .yui3-slider-x .yui3-slider-thumb {
+ height: 26px;
+ width: 15px;
+}
+
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-sam .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -10px;
+}
+.yui3-skin-sam .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -50px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/sam/thumb-y.png */
+
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+ /* alternate: rail-y-lines.png */
+}
+
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail {
+ width: 26px;
+}
+.yui3-skin-sam .yui3-slider-y .yui3-slider-thumb {
+ width: 26px;
+ height: 15px;
+}
+
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-sam .yui3-slider-y .yui3-slider-thumb-image {
+ left: -10px;
+ top: 0;
+}
+.yui3-skin-sam .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -50px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/sam/thumb-x.png */
+
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+ /* alternate: rail-x-lines.png */
+}
+
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail {
+ height: 26px;
+}
+.yui3-skin-sam .yui3-slider-x .yui3-slider-thumb {
+ height: 26px;
+ width: 15px;
+}
+
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-sam .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -10px;
+}
+.yui3-skin-sam .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -50px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/sam/thumb-y.png */
+
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+ /* alternate: rail-y-lines.png */
+}
+
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail {
+ width: 26px;
+}
+.yui3-skin-sam .yui3-slider-y .yui3-slider-thumb {
+ width: 26px;
+ height: 15px;
+}
+
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-sam .yui3-slider-y .yui3-slider-thumb-image {
+ left: -10px;
+ top: 0;
+}
+.yui3-skin-sam .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -50px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-scrollview {
+ -webkit-tap-highlight-color: rgba(255,255,255,0);
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-paginator {
+ background: white url(../../../../assets/skins/sam/sprite.png) repeat-x 0 0;
+ background-image: -webkit-linear-gradient(transparent 40%, hsla(0, 0%, 0%, 0.21));
+ background-image: -moz-linear-gradient(top, transparent 40%, hsla(0, 0%, 0%, 0.21));
+ background-image: -ms-linear-gradient(transparent 40%, hsla(0, 0%, 0%, 0.21));
+ background-image: -o-linear-gradient(transparent 40%, hsla(0, 0%, 0%, 0.21));
+ background-image: linear-gradient(transparent 40%, hsla(0, 0%, 0%, 0.21));
+ border-color: #cbcbcb;
+}
+
+.yui3-datatable-paginator .yui3-datatable-paginator-control {
+ color: #242D42;
+}
+
+.yui3-datatable-paginator .yui3-datatable-paginator-control-first:hover,
+.yui3-datatable-paginator .yui3-datatable-paginator-control-last:hover,
+.yui3-datatable-paginator .yui3-datatable-paginator-control-prev:hover,
+.yui3-datatable-paginator .yui3-datatable-paginator-control-next:hover {
+ box-shadow: 0 1px 2px #292442;
+}
+
+.yui3-datatable-paginator .yui3-datatable-paginator-control-first:active,
+.yui3-datatable-paginator .yui3-datatable-paginator-control-last:active,
+.yui3-datatable-paginator .yui3-datatable-paginator-control-prev:active,
+.yui3-datatable-paginator .yui3-datatable-paginator-control-next:active {
+ box-shadow: inset 0 1px 1px #292442;
+ background: #E0DEED;
+ background: hsla(250, 30%, 90%, 0.3);
+}
+
+.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled,
+.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled:hover {
+ color: #BDC7DB;
+ border-color: transparent;
+ box-shadow: none;
+}
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Reset base Console skin styles */
+.yui3-skin-sam .yui3-testconsole .yui3-console-content,
+.yui3-skin-sam .yui3-testconsole .yui3-console-bd,
+.yui3-skin-sam .yui3-testconsole .yui3-console-entry,
+.yui3-skin-sam .yui3-testconsole .yui3-console-ft,
+.yui3-skin-sam .yui3-testconsole .yui3-console-ft .yui3-console-filters-categories,
+.yui3-skin-sam .yui3-testconsole .yui3-console-ft .yui3-console-filters-sources,
+.yui3-skin-sam .yui3-testconsole .yui3-console-hd {
+ background: none;
+ border: none;
+ -moz-border-radius: 0;
+ -webkit-border-radius: 0;
+ border-radius: 0;
+}
+
+.yui3-skin-sam .yui3-testconsole-content,
+.yui3-skin-sam .yui3-testconsole .yui3-console-bd {
+ color: #333;
+ font: 13px/1.4 Helvetica, 'DejaVu Sans', 'Bitstream Vera Sans', Arial, sans-serif;
+}
+
+.yui3-skin-sam .yui3-testconsole-content {
+ border: 1px solid #afafaf;
+}
+
+.yui3-skin-sam .yui3-testconsole .yui3-console-entry {
+ border-bottom: 1px solid #eaeaea;
+ font-family: Menlo, Inconsolata, Consolas, 'DejaVu Mono', 'Bitstream Vera Sans Mono', monospace;
+ font-size: 11px;
+}
+
+.yui3-skin-sam .yui3-testconsole .yui3-console-ft {
+ border-top: 1px solid;
+}
+
+.yui3-skin-sam .yui3-testconsole .yui3-console-hd {
+ border-bottom: 1px solid;
+ *zoom: 1;
+}
+
+.yui3-skin-sam .yui3-testconsole.yui3-console-collapsed .yui3-console-hd {
+ border: none;
+}
+
+.yui3-skin-sam .yui3-testconsole .yui3-console-ft,
+.yui3-skin-sam .yui3-testconsole .yui3-console-hd {
+ border-color: #cfcfcf;
+}
+
+.yui3-skin-sam .yui3-testconsole .yui3-testconsole-entry-fail {
+ background-color: #FFE0E0;
+ border-bottom-color: #FFC5C4;
+}
+
+.yui3-skin-sam .yui3-testconsole .yui3-testconsole-entry-pass {
+ background-color: #ECFFEA;
+ border-bottom-color: #D1FFCC;
+}
+
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-dial-handle{ /*container. top left corner used for trig positioning*/
+ background:#6C3A3A;
+ opacity:0.3;
+ -moz-box-shadow:1px 1px 1px rgba(0, 0, 0, 0.9) inset;
+ /*-webkit-box-shadow:1px 1px 1px rgba(0, 0, 0, 0.9) inset; Chrome 7/Win bug*/
+ cursor:pointer;
+ font-size:1px;
+}
+.yui3-skin-sam .yui3-dial-ring {
+ background:#BEBDB7;
+ background:-moz-linear-gradient(100% 100% 135deg, #7B7A6D, #FFFFFF);
+ background:-webkit-gradient(linear, left top, right bottom, from(#FFFFFF), to(#7B7A6D));
+ box-shadow:1px 1px 5px rgba(0, 0, 0, 0.4) inset;
+ -webkit-box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.4) inset; /*Chrome 7/Win bug*/
+ -moz-box-shadow:1px 1px 5px rgba(0, 0, 0, 0.4) inset;
+}
+.yui3-skin-sam .yui3-dial-center-button{
+ box-shadow:-1px -1px 2px rgba(0, 0, 0, 0.3) inset, 1px 1px 2px rgba(0, 0, 0, 0.5);
+ -moz-box-shadow:-1px -1px 2px rgba(0, 0, 0, 0.3) inset, 1px 1px 2px rgba(0, 0, 0, 0.5);
+ /*-webkit-box-shadow: -1px -1px 2px rgba(0, 0, 0, 0.3) inset, 1px 1px 2px rgba(0, 0, 0, 0.5); Chrome 7/Win bug*/
+ background:#DDDBD4;
+ background:-moz-radial-gradient(30% 30% 0deg, circle farthest-side, #FBFBF9 24%, #F2F0EA 41%, #D3D0C3 83%) repeat scroll 0 0 transparent;
+ background:-webkit-gradient(radial, 15 15, 15, 30 30, 40, from(#FBFBF9), to(#D3D0C3), color-stop(.2,#F2F0EA));
+ cursor:pointer;
+ opacity:0.7;
+ /*text-align:center;*/
+}
+.yui3-skin-sam .yui3-dial-reset-string{
+ color:#676767;
+ font-size:85%;
+ text-decoration:underline;
+}
+.yui3-skin-sam .yui3-dial-label{
+ color:#808080;
+ margin-bottom:0.8em;
+}
+.yui3-skin-sam .yui3-dial-value-string{
+ margin-left:0.5em;
+ color:#000;
+ font-size:130%;
+}
+.yui3-skin-sam .yui3-dial-value {
+ visibility:hidden;
+ position:absolute;
+ top:0;
+ left:102%;
+ width:4em;
+}
+.yui3-skin-sam .yui3-dial-north-mark{
+ position:absolute;
+ border-left:2px solid #ccc;
+ height:5px;
+ width:10px;
+ left:50%;
+ top:-7px;
+ font-size:1px;
+}
+.yui3-skin-sam .yui3-dial-marker {
+ background-color:#000;
+ opacity:0.2;
+ font-size:1px;
+}
+.yui3-skin-sam .yui3-dial-marker-max-min{
+ background-color:#AB3232;
+ opacity:0.6;
+}
+.yui3-skin-sam .yui3-dial-ring-vml,
+.yui3-skin-sam .yui3-dial-center-button-vml,
+.yui3-skin-sam .yui3-dial-marker v\:oval.yui3-dial-marker-max-min,
+.yui3-skin-sam v\:oval.yui3-dial-marker-max-min,
+.yui3-skin-sam .yui3-dial-marker-vml,
+.yui3-skin-sam .yui3-dial-handle-vml {
+ background: none;
+ opacity:1;
+}
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-datatable-message-content {
+ border: 0 none;
+ border-bottom: 1px solid #cbcbcb;
+ padding: 4px 10px;
+}
diff --git a/js/yui3/assets/skins/audio-light/rail-x.png b/js/yui3/assets/skins/audio-light/rail-x.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/audio-light/rail-x.png
diff --git a/js/yui3/assets/skins/audio-light/rail-y.png b/js/yui3/assets/skins/audio-light/rail-y.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/audio-light/rail-y.png
diff --git a/js/yui3/assets/skins/audio-light/slider-base.css b/js/yui3/assets/skins/audio-light/slider-base.css
new file mode 100644
index 000000000..52bd5c38a
--- /dev/null
+++ b/js/yui3/assets/skins/audio-light/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail,.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x}.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail{height:35px;background-position:0 7px}.yui3-skin-audio-light .yui3-slider-x .yui3-slider-thumb{height:35px;width:19px}.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -20px;height:13px;left:-5px;width:5px;top:7px}.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -40px;height:13px;right:-5px;width:5px;top:7px}.yui3-skin-audio-light .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-3px}.yui3-skin-audio-light .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-43px}.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail,.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y}.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail{width:35px;background-position:7px 0}.yui3-skin-audio-light .yui3-slider-y .yui3-slider-thumb{width:35px;height:19px}.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-20px 0;width:13px;top:-5px;height:5px;left:7px}.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-40px 0;width:13px;bottom:-5px;height:5px;left:7px}.yui3-skin-audio-light .yui3-slider-y .yui3-slider-thumb-image{left:-3px;top:0}.yui3-skin-audio-light .yui3-slider-y .yui3-slider-thumb-shadow{left:-43px;opacity:.15;filter:alpha(opacity=15);top:0}#yui3-css-stamp.skin-audio-light-slider-base{display:none}
diff --git a/js/yui3/assets/skins/audio-light/thumb-x.png b/js/yui3/assets/skins/audio-light/thumb-x.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/audio-light/thumb-x.png
diff --git a/js/yui3/assets/skins/audio-light/thumb-y.png b/js/yui3/assets/skins/audio-light/thumb-y.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/audio-light/thumb-y.png
diff --git a/js/yui3/assets/skins/audio/rail-x.png b/js/yui3/assets/skins/audio/rail-x.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/audio/rail-x.png
diff --git a/js/yui3/assets/skins/audio/rail-y.png b/js/yui3/assets/skins/audio/rail-y.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/audio/rail-y.png
diff --git a/js/yui3/assets/skins/audio/slider-base.css b/js/yui3/assets/skins/audio/slider-base.css
new file mode 100644
index 000000000..81df7a805
--- /dev/null
+++ b/js/yui3/assets/skins/audio/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-audio .yui3-slider-x .yui3-slider-rail,.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x}.yui3-skin-audio .yui3-slider-x .yui3-slider-rail{height:35px;background-position:0 7px}.yui3-skin-audio .yui3-slider-x .yui3-slider-thumb{height:35px;width:19px}.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -20px;height:13px;left:-5px;width:5px;top:7px}.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -40px;height:13px;right:-5px;width:5px;top:7px}.yui3-skin-audio .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-3px}.yui3-skin-audio .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-43px}.yui3-skin-audio .yui3-slider-y .yui3-slider-rail,.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y}.yui3-skin-audio .yui3-slider-y .yui3-slider-rail{width:35px;background-position:7px 0}.yui3-skin-audio .yui3-slider-y .yui3-slider-thumb{width:35px;height:19px}.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-20px 0;width:13px;top:-5px;height:5px;left:7px}.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-40px 0;width:13px;bottom:-5px;height:5px;left:7px}.yui3-skin-audio .yui3-slider-y .yui3-slider-thumb-image{left:-3px;top:0}.yui3-skin-audio .yui3-slider-y .yui3-slider-thumb-shadow{left:-43px;opacity:.15;filter:alpha(opacity=15);top:0}#yui3-css-stamp.skin-audio-slider-base{display:none}
diff --git a/js/yui3/assets/skins/audio/thumb-x.png b/js/yui3/assets/skins/audio/thumb-x.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/audio/thumb-x.png
diff --git a/js/yui3/assets/skins/audio/thumb-y.png b/js/yui3/assets/skins/audio/thumb-y.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/audio/thumb-y.png
diff --git a/js/yui3/assets/skins/capsule-dark/rail-x-dots.png b/js/yui3/assets/skins/capsule-dark/rail-x-dots.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule-dark/rail-x-dots.png
diff --git a/js/yui3/assets/skins/capsule-dark/rail-x-lines.png b/js/yui3/assets/skins/capsule-dark/rail-x-lines.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule-dark/rail-x-lines.png
diff --git a/js/yui3/assets/skins/capsule-dark/rail-x.png b/js/yui3/assets/skins/capsule-dark/rail-x.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule-dark/rail-x.png
diff --git a/js/yui3/assets/skins/capsule-dark/rail-y-dots.png b/js/yui3/assets/skins/capsule-dark/rail-y-dots.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule-dark/rail-y-dots.png
diff --git a/js/yui3/assets/skins/capsule-dark/rail-y-lines.png b/js/yui3/assets/skins/capsule-dark/rail-y-lines.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule-dark/rail-y-lines.png
diff --git a/js/yui3/assets/skins/capsule-dark/rail-y.png b/js/yui3/assets/skins/capsule-dark/rail-y.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule-dark/rail-y.png
diff --git a/js/yui3/assets/skins/capsule-dark/slider-base.css b/js/yui3/assets/skins/capsule-dark/slider-base.css
new file mode 100644
index 000000000..803b3590f
--- /dev/null
+++ b/js/yui3/assets/skins/capsule-dark/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail,.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x}.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail{height:25px}.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-thumb{height:30px;width:14px}.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -20px;height:20px;left:-2px;width:5px}.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -40px;height:20px;right:-2px;width:5px}.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-10px}.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-50px}.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail,.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y}.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail{width:25px}.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-thumb{width:30px;height:14px}.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-20px 0;width:20px;top:-2px;height:5px}.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-40px 0;width:20px;bottom:-2px;height:5px}.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-thumb-image{left:-10px;top:0}.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-thumb-shadow{left:-50px;opacity:.15;filter:alpha(opacity=15);top:0}#yui3-css-stamp.skin-capsule-dark-slider-base{display:none}
diff --git a/js/yui3/assets/skins/capsule-dark/thumb-x-line.png b/js/yui3/assets/skins/capsule-dark/thumb-x-line.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule-dark/thumb-x-line.png
diff --git a/js/yui3/assets/skins/capsule-dark/thumb-x.png b/js/yui3/assets/skins/capsule-dark/thumb-x.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule-dark/thumb-x.png
diff --git a/js/yui3/assets/skins/capsule-dark/thumb-y-line.png b/js/yui3/assets/skins/capsule-dark/thumb-y-line.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule-dark/thumb-y-line.png
diff --git a/js/yui3/assets/skins/capsule-dark/thumb-y.png b/js/yui3/assets/skins/capsule-dark/thumb-y.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule-dark/thumb-y.png
diff --git a/js/yui3/assets/skins/capsule/rail-x-dots.png b/js/yui3/assets/skins/capsule/rail-x-dots.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule/rail-x-dots.png
diff --git a/js/yui3/assets/skins/capsule/rail-x-lines.png b/js/yui3/assets/skins/capsule/rail-x-lines.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule/rail-x-lines.png
diff --git a/js/yui3/assets/skins/capsule/rail-x.png b/js/yui3/assets/skins/capsule/rail-x.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule/rail-x.png
diff --git a/js/yui3/assets/skins/capsule/rail-y-dots.png b/js/yui3/assets/skins/capsule/rail-y-dots.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule/rail-y-dots.png
diff --git a/js/yui3/assets/skins/capsule/rail-y-lines.png b/js/yui3/assets/skins/capsule/rail-y-lines.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule/rail-y-lines.png
diff --git a/js/yui3/assets/skins/capsule/rail-y.png b/js/yui3/assets/skins/capsule/rail-y.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule/rail-y.png
diff --git a/js/yui3/assets/skins/capsule/slider-base.css b/js/yui3/assets/skins/capsule/slider-base.css
new file mode 100644
index 000000000..03b50f955
--- /dev/null
+++ b/js/yui3/assets/skins/capsule/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail,.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x;background-repeat:repeat-x}.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail{height:25px}.yui3-skin-capsule .yui3-slider-x .yui3-slider-thumb{height:30px;width:14px}.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -20px;height:20px;left:-2px;width:5px}.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -40px;height:20px;right:-2px;width:5px}.yui3-skin-capsule .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-10px}.yui3-skin-capsule .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-50px}.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail,.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y;background-repeat:repeat-y}.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail{width:25px}.yui3-skin-capsule .yui3-slider-y .yui3-slider-thumb{width:30px;height:14px}.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-20px 0;width:20px;top:-2px;height:5px}.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-40px 0;width:20px;bottom:-2px;height:5px}.yui3-skin-capsule .yui3-slider-y .yui3-slider-thumb-image{left:-10px;top:0}.yui3-skin-capsule .yui3-slider-y .yui3-slider-thumb-shadow{left:-50px;opacity:.15;filter:alpha(opacity=15);top:0}#yui3-css-stamp.skin-capsule-slider-base{display:none}
diff --git a/js/yui3/assets/skins/capsule/thumb-x-line.png b/js/yui3/assets/skins/capsule/thumb-x-line.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule/thumb-x-line.png
diff --git a/js/yui3/assets/skins/capsule/thumb-x.png b/js/yui3/assets/skins/capsule/thumb-x.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule/thumb-x.png
diff --git a/js/yui3/assets/skins/capsule/thumb-y-line.png b/js/yui3/assets/skins/capsule/thumb-y-line.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule/thumb-y-line.png
diff --git a/js/yui3/assets/skins/capsule/thumb-y-lines.png b/js/yui3/assets/skins/capsule/thumb-y-lines.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule/thumb-y-lines.png
diff --git a/js/yui3/assets/skins/capsule/thumb-y.png b/js/yui3/assets/skins/capsule/thumb-y.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/capsule/thumb-y.png
diff --git a/js/yui3/assets/skins/night/arrows.png b/js/yui3/assets/skins/night/arrows.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/night/arrows.png
diff --git a/js/yui3/assets/skins/night/autocomplete-list.css b/js/yui3/assets/skins/night/autocomplete-list.css
new file mode 100644
index 000000000..623b4fc75
--- /dev/null
+++ b/js/yui3/assets/skins/night/autocomplete-list.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-aclist{position:absolute;z-index:1}.yui3-aclist-hidden{visibility:hidden}.yui3-aclist-aria{left:-9999px;position:absolute}.yui3-aclist-list{list-style:none;margin:0;overflow:hidden;padding:0}.yui3-aclist-item{cursor:pointer;list-style:none;padding:2px 5px}.yui3-aclist-item-active{outline:#afafaf dotted thin}.yui3-skin-night [for=ac-input]{color:#cbcbcb}.yui3-skin-night .yui3-aclist-content{font-size:100%;background-color:#151515;color:#ccc;border:1px solid #303030;-moz-box-shadow:0 0 17px rgba(0,0,0,0.58);-webkit-box-shadow:0 0 17px rgba(0,0,0,0.58);box-shadow:0 0 17px rgba(0,0,0,0.58)}.yui3-skin-night .yui3-aclist-item-active{background-color:#2f3030;background:-moz-linear-gradient(0% 100% 90deg,#252626 0,#333434 100%);background:-webkit-gradient(linear,left top,left bottom,from(#333434),to(#252626))}.yui3-skin-night .yui3-aclist-item-hover{background-color:#262727;background:-moz-linear-gradient(0% 100% 90deg,#202121 0,#282929 100%);background:-webkit-gradient(linear,left top,left bottom,from(#282929),to(#202121))}.yui3-skin-night .yui3-aclist-item{padding:0 1em;line-height:2.25}.yui3-skin-night .yui3-aclist-item-active{outline:0}.yui3-skin-night .yui3-highlight{color:#efefef}#yui3-css-stamp.skin-night-autocomplete-list{display:none}
diff --git a/js/yui3/assets/skins/night/calendar-base.css b/js/yui3/assets/skins/night/calendar-base.css
new file mode 100644
index 000000000..27c7d0635
--- /dev/null
+++ b/js/yui3/assets/skins/night/calendar-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-calendar-pane{width:100%}.yui3-calendar-grid{width:100%}.yui3-calendar-column-hidden,.yui3-calendar-hidden{display:none}.yui3-skin-night .yui3-calendar-content{padding:10px;color:#cbcbcb;border:1px solid #303030;background:#151515;background:-moz-linear-gradient(top,#222 0,#151515 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#222),color-stop(100%,#151515));background:-webkit-linear-gradient(top,#222 0,#151515 100%);background:-o-linear-gradient(top,#222 0,#151515 100%);background:-ms-linear-gradient(top,#222 0,#151515 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#222222',endColorstr='#151515',GradientType=0);background:linear-gradient(top,#222 0,#151515 100%);-moz-border-radius:5px;border-radius:5px}.yui3-skin-night .yui3-calendar-grid{padding:5px;border-collapse:collapse}.yui3-skin-night .yui3-calendar-header{padding-bottom:10px}.yui3-skin-night .yui3-calendar-header-label{margin:0;font-size:1em;font-weight:bold;text-align:center;width:100%}.yui3-skin-night .yui3-calendar-day,.yui3-skin-night .yui3-calendar-prevmonth-day,.yui3-skin-night .yui3-calendar-nextmonth-day{padding:5px;border:1px solid #151515;background:#262727;text-align:center}.yui3-skin-night .yui3-calendar-day:hover{background:#383939;color:#fff}.yui3-skin-night .yui3-calendar-selection-disabled,.yui3-skin-night .yui3-calendar-selection-disabled:hover{background:#151515;color:#596060}.yui3-skin-night .yui3-calendar-weekday{color:#4f4f4f;font-weight:bold;text-align:center}.yui3-skin-night .yui3-calendar-prevmonth-day,.yui3-skin-night .yui3-calendar-nextmonth-day{color:#4f4f4f}.yui3-skin-night .yui3-calendar-day{font-weight:bold}.yui3-skin-night .yui3-calendar-day-selected{background-color:#505151;color:#fff}.yui3-skin-night .yui3-calendar-left-grid{margin-right:1em}[dir="rtl"] .yui3-skin-night .yui3-calendar-left-grid,.yui3-skin-night [dir="rtl"] .yui3-calendar-left-grid{margin-right:auto;margin-left:1em}.yui3-skin-sam .yui3-calendar-right-grid{margin-left:1em}[dir="rtl"] .yui3-skin-night .yui3-calendar-right-grid,.yui3-skin-night [dir="rtl"] .yui3-calendar-right-grid{margin-left:auto;margin-right:1em}#yui3-css-stamp.skin-night-calendar-base{display:none}
diff --git a/js/yui3/assets/skins/night/calendar.css b/js/yui3/assets/skins/night/calendar.css
new file mode 100644
index 000000000..e4c98d013
--- /dev/null
+++ b/js/yui3/assets/skins/night/calendar.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-calendar-column-hidden,.yui3-calendar-hidden{display:none}.yui3-calendar-day{cursor:pointer}.yui3-calendar-selection-disabled{cursor:default}.yui3-calendar-prevmonth-day{cursor:default}.yui3-calendar-nextmonth-day{cursor:default}.yui3-calendar-content:hover .yui3-calendar-day,.yui3-calendar-content:hover .yui3-calendar-prevmonth-day,.yui3-calendar-content:hover .yui3-calendar-nextmonth-day{-moz-user-select:none}.yui3-skin-night .yui3-calendar-day-highlighted{background-color:#555}.yui3-skin-night .yui3-calendar-day-selected.yui3-calendar-day-highlighted{background-color:#777}#yui3-css-stamp.skin-night-calendar{display:none}
diff --git a/js/yui3/assets/skins/night/calendarnavigator.css b/js/yui3/assets/skins/night/calendarnavigator.css
new file mode 100644
index 000000000..395aa419a
--- /dev/null
+++ b/js/yui3/assets/skins/night/calendarnavigator.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-calendar-header{text-align:center;position:relative;width:100%}.yui3-calendar-header-label{display:inline}.yui3-calendarnav-prevmonth{cursor:pointer}.yui3-calendarnav-nextmonth{cursor:pointer}.yui3-skin-night .yui3-calendarnav-prevmonth,.yui3-skin-night .yui3-calendarnav-nextmonth{width:0;height:0;padding:0;margin:0;border:10px solid transparent;position:absolute;font-size:0;line-height:0;_border-left-color:black;_border-top-color:black;_border-right-color:black;_border-bottom-color:black;_filter:chroma(color=black)}.yui3-skin-night .yui3-calendarnav-prevmonth:hover,[dir="rtl"] .yui3-skin-night .yui3-calendarnav-nextmonth:hover,.yui3-skin-night [dir="rtl"] .yui3-calendarnav-nextmonth:hover{border-right-color:#06c}.yui3-skin-night .yui3-calendarnav-nextmonth:hover,[dir="rtl"] .yui3-skin-night .yui3-calendarnav-prevmonth:hover,.yui3-skin-night [dir="rtl"] .yui3-calendarnav-prevmonth:hover{border-left-color:#06c}.yui3-skin-night .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,.yui3-skin-night .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover,[dir="rtl"] .yui3-skin-night .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,.yui3-skin-night [dir="rtl"] .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,[dir="rtl"] .yui3-skin-night .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover,.yui3-skin-night [dir="rtl"] .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover{cursor:default;border-right-color:#ccc;border-left-color:transparent}.yui3-skin-night .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,.yui3-skin-night .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover,[dir="rtl"] .yui3-skin-night .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,.yui3-skin-night [dir="rtl"] .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,[dir="rtl"] .yui3-skin-night .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover,.yui3-skin-night [dir="rtl"] .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover{cursor:default;border-left-color:#ccc;border-right-color:transparent}.yui3-skin-night .yui3-calendarnav-prevmonth{border-right-color:#fff;left:0;margin-left:-10px}.yui3-skin-night .yui3-calendarnav-nextmonth{border-left-color:#fff;right:0;margin-right:-10px}[dir="rtl"] .yui3-skin-night .yui3-calendarnav-prevmonth,.yui3-skin-night [dir="rtl"] .yui3-calendarnav-prevmonth{left:auto;right:0;border-left-color:#fff;border-right-color:transparent}[dir="rtl"] .yui3-skin-night .yui3-calendarnav-nextmonth,.yui3-skin-night [dir="rtl"] .yui3-calendarnav-nextmonth{left:0;right:auto;border-right-color:#fff;border-left-color:transparent}#yui3-css-stamp.skin-night-calendarnavigator{display:none}
diff --git a/js/yui3/assets/skins/night/datatable-base.css b/js/yui3/assets/skins/night/datatable-base.css
new file mode 100644
index 000000000..5808f35d2
--- /dev/null
+++ b/js/yui3/assets/skins/night/datatable-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-table{empty-cells:show}.yui3-skin-night .yui3-datatable{color:#8e8e8e;font-family:HelveticaNeue,arial,helvetica,clean,sans-serif}.yui3-skin-night .yui3-datatable-table{border:1px solid #323434;border-collapse:separate;border-spacing:0;color:#8e8e8e;margin:0;padding:0}.yui3-skin-night .yui3-datatable-caption{color:#474747;font:italic 85%/1 HelveticaNeue,arial,helvetica,clean,sans-serif;padding:1em 0;text-align:center}.yui3-skin-night .yui3-datatable-cell,.yui3-skin-night .yui3-datatable-header{border-left:1px solid #303030;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:4px 10px 4px 10px}.yui3-skin-night .yui3-datatable-cell:first-child,.yui3-skin-night .yui3-datatable-first-header{border-left-width:0}.yui3-skin-night .yui3-datatable-header{background-color:#3b3c3d;background:-moz-linear-gradient(0% 100% 90deg,#242526 0,#3b3c3d 96%,#2c2d2f 100%);background:-webkit-gradient(linear,left bottom,left top,from(#242526),color-stop(0.96,#3b3c3d),to(#2c2d2f));color:#eee;font-weight:normal;text-align:left;vertical-align:bottom;white-space:nowrap}.yui3-skin-night .yui3-datatable-cell{background-color:transparent}.yui3-skin-night .yui3-datatable-even .yui3-datatable-cell{background-color:#0e0e0e}.yui3-skin-night .yui3-datatable-odd .yui3-datatable-cell{background-color:#1d1e1e}#yui3-css-stamp.skin-night-datatable-base{display:none}
diff --git a/js/yui3/assets/skins/night/datatable-highlight.css b/js/yui3/assets/skins/night/datatable-highlight.css
new file mode 100644
index 000000000..c8a498da2
--- /dev/null
+++ b/js/yui3/assets/skins/night/datatable-highlight.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable tr td{-webkit-transition:background-color .05s ease-in;-moz-transition:background-color .05s ease-in;-o-transition:background-color .05s ease-in;transition:background-color .05s ease-in}.yui3-datatable .yui3-datatable-highlight-row td{-webkit-transition:background-color .1s ease-out;-moz-transition:background-color .1s ease-out;-o-transition:background-color .1s ease-out;transition:background-color .1s ease-out}.yui3-datatable tr .yui3-datatable-highlight-col{-webkit-transition:background-color .1s ease-out;-moz-transition:background-color .1s ease-out;-o-transition:background-color .1s ease-out;transition:background-color .1s ease-out}.yui3-datatable tr .yui3-datatable-highlight-cell{-webkit-transition:background-color .1s ease-out;-moz-transition:background-color .1s ease-out;-o-transition:background-color .1s ease-out;transition:background-color .1s ease-out}.yui3-skin-night .yui3-datatable .yui3-datatable-highlight-row td{background-color:#38383f}.yui3-skin-night .yui3-datatable tr .yui3-datatable-highlight-col{background-color:#38383f}.yui3-skin-night .yui3-datatable tr .yui3-datatable-highlight-cell{background-color:#38383f}#yui3-css-stamp.skin-night-datatable-highlight{display:none}
diff --git a/js/yui3/assets/skins/night/datatable-message.css b/js/yui3/assets/skins/night/datatable-message.css
new file mode 100644
index 000000000..58d56bdea
--- /dev/null
+++ b/js/yui3/assets/skins/night/datatable-message.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-message{display:none}.yui3-datatable-message-visible .yui3-datatable-message{display:block;display:table-row-group}.yui3-skin-night .yui3-datatable-message-content{background-color:#0e0e0e;border:0 none;border-bottom:1px solid #303030;padding:4px 10px}#yui3-css-stamp.skin-night-datatable-message{display:none}
diff --git a/js/yui3/assets/skins/night/datatable-paginator.css b/js/yui3/assets/skins/night/datatable-paginator.css
new file mode 100644
index 000000000..00137b837
--- /dev/null
+++ b/js/yui3/assets/skins/night/datatable-paginator.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-paginator-wrapper{border:0;padding:0}.yui3-datatable-paginator{padding:3px;white-space:nowrap}.yui3-datatable-paginator .yui3-paginator-content{position:relative}.yui3-datatable-paginator .yui3-paginator-page-select{position:absolute;right:0;top:0}.yui3-datatable-paginator .yui3-datatable-paginator-group{display:inline-block;zoom:1;*display:inline}.yui3-datatable-paginator .yui3-datatable-paginator-control{display:inline-block;zoom:1;*display:inline;margin:0 3px;padding:0 .2em;text-align:center;text-decoration:none;line-height:1.5;border:1px solid transparent;border-radius:3px;background:transparent}.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled,.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled:hover{cursor:default}.yui3-datatable-paginator .yui3-datatable-paginator-group input{width:3em}.yui3-datatable-paginator form{text-align:center;margin:0 2em}.yui3-datatable-paginator .yui3-datatable-paginator-per-page{text-align:right}.yui3-datatable-paginator{background:white url(../../../../assets/skins/night/sprite.png) repeat-x 0 0;background-image:-webkit-linear-gradient(transparent 40%,hsla(0,0%,0%,0.21));background-image:-moz-linear-gradient(top,transparent 40%,hsla(0,0%,0%,0.21));background-image:-ms-linear-gradient(transparent 40%,hsla(0,0%,0%,0.21));background-image:-o-linear-gradient(transparent 40%,hsla(0,0%,0%,0.21));background-image:linear-gradient(transparent 40%,hsla(0,0%,0%,0.21))}.yui3-datatable-paginator .yui3-datatable-paginator-control{color:#242d42}.yui3-datatable-paginator .yui3-datatable-paginator-control-first:hover,.yui3-datatable-paginator .yui3-datatable-paginator-control-last:hover,.yui3-datatable-paginator .yui3-datatable-paginator-control-prev:hover,.yui3-datatable-paginator .yui3-datatable-paginator-control-next:hover{box-shadow:0 1px 2px #292442}.yui3-datatable-paginator .yui3-datatable-paginator-control-first:active,.yui3-datatable-paginator .yui3-datatable-paginator-control-last:active,.yui3-datatable-paginator .yui3-datatable-paginator-control-prev:active,.yui3-datatable-paginator .yui3-datatable-paginator-control-next:active{box-shadow:inset 0 1px 1px #292442;background:#e0deed;background:hsla(250,30%,90%,0.3)}.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled,.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled:hover{color:#bdc7db;border-color:transparent;box-shadow:none}#yui3-css-stamp.skin-night-datatable-paginator{display:none}
diff --git a/js/yui3/assets/skins/night/datatable-scroll.css b/js/yui3/assets/skins/night/datatable-scroll.css
new file mode 100644
index 000000000..50e5bbde7
--- /dev/null
+++ b/js/yui3/assets/skins/night/datatable-scroll.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-scrollable-x{_overflow-x:hidden;_position:relative}.yui3-datatable-scrollable-y,.yui3-datatable-scrollable-y .yui3-datatable-x-scroller{_overflow-y:hidden;_position:relative}.yui3-datatable-y-scroller-container{overflow-x:hidden;position:relative}.yui3-datatable-scrollable-y .yui3-datatable-content{position:relative}.yui3-datatable-scrollable-y .yui3-datatable-table .yui3-datatable-columns{visibility:hidden}.yui3-datatable-scroll-columns{position:absolute;width:100%;z-index:2}.yui3-datatable-y-scroller,.yui3-datatable-scrollable-x .yui3-datatable-caption-table{width:100%}.yui3-datatable-x-scroller{position:relative;overflow-x:scroll;overflow-y:hidden}.yui3-datatable-scrollable-y .yui3-datatable-y-scroller{position:relative;overflow-x:hidden;overflow-y:scroll;z-index:1;-webkit-overflow-scrolling:touch}.yui3-datatable-scrollbar{position:absolute;overflow-x:hidden;overflow-y:scroll;z-index:2}.yui3-datatable-scrollbar div{position:absolute;width:1px;visibility:hidden}.yui3-skin-sam .yui3-datatable-scroll-columns{border-collapse:separate;border-spacing:0;font-family:HelveticaNeue,arial,helvetica,clean,sans-serif;margin:0;padding:0;top:0;left:0}.yui3-skin-sam .yui3-datatable-scroll-columns .yui3-datatable-header{padding:0}.yui3-skin-sam .yui3-datatable-x-scroller,.yui3-skin-sam .yui3-datatable-y-scroller-container{border:1px solid #303030;border-left-color:#323434}.yui3-skin-sam .yui3-datatable-scrollable-x .yui3-datatable-y-scroller-container,.yui3-skin-sam .yui3-datatable-x-scroller .yui3-datatable-table,.yui3-skin-sam .yui3-datatable-y-scroller .yui3-datatable-table{border:0 none}#yui3-css-stamp.skin-night-datatable-scroll{display:none}
diff --git a/js/yui3/assets/skins/night/datatable-sort.css b/js/yui3/assets/skins/night/datatable-sort.css
new file mode 100644
index 000000000..e755d381d
--- /dev/null
+++ b/js/yui3/assets/skins/night/datatable-sort.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-sortable-column{z-index:1}.yui3-datatable-sortable-column:focus,.yui3-datatable-sortable-column:active{z-index:2}.yui3-datatable-sort-liner{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.yui3-skin-night .yui3-datatable-sortable-column{cursor:pointer}.yui3-skin-night .yui3-datatable-columns .yui3-datatable-sorted,.yui3-skin-night .yui3-datatable-sortable-column:hover{background-color:#4d4e4f;*background:#505152 url(../../../../assets/skins/night/sprite.png) repeat-x 0 -100px;background-image:-webkit-gradient(linear,0 0,0 100%,from(rgba(255,255,255,0.2)),color-stop(40%,rgba(255,255,255,0.1)),color-stop(80%,rgba(255,255,255,0.01)),to(transparent));background-image:-webkit-linear-gradient(rgba(255,255,255,0.2),rgba(255,255,255,0.1) 40%,rgba(255,255,255,0.01) 80%,transparent);background-image:-moz-linear-gradient(top,rgba(255,255,255,0.2),rgba(255,255,255,0.1) 40%,rgba(255,255,255,0.01) 80%,transparent);background-image:-ms-linear-gradient(rgba(255,255,255,0.2),rgba(255,255,255,0.1) 40%,rgba(255,255,255,0.01) 80%,transparent);background-image:-o-linear-gradient(rgba(255,255,255,0.2),rgba(255,255,255,0.1) 40%,rgba(255,255,255,0.01) 80%,transparent);background-image:linear-gradient(rgba(255,255,255,0.2),rgba(255,255,255,0.1) 40%,rgba(255,255,255,0.01) 80%,transparent)}.yui3-skin-night .yui3-datatable-sort-liner{display:block;height:100%;position:relative;padding-right:15px;position:relative}.yui3-skin-night .yui3-datatable-sort-indicator{position:absolute;right:0;bottom:.5ex;width:7px;height:10px;background:url(sort-arrow-sprite.png) no-repeat 0 0;_background:url(sort-arrow-sprite-ie.png) no-repeat 0 0;overflow:hidden}.yui3-skin-night .yui3-datatable-sorted .yui3-datatable-sort-indicator{background-position:0 -10px}.yui3-skin-night .yui3-datatable-sorted-desc .yui3-datatable-sort-indicator{background-position:0 -20px}.yui3-skin-night .yui3-datatable-data .yui3-datatable-even .yui3-datatable-sorted{background-color:#262626;color:#b3b2b2}.yui3-skin-night .yui3-datatable-data .yui3-datatable-odd .yui3-datatable-sorted{background-color:#393a3a;color:#cbcbcb}#yui3-css-stamp.skin-night-datatable-sort{display:none}
diff --git a/js/yui3/assets/skins/night/dial.css b/js/yui3/assets/skins/night/dial.css
new file mode 100644
index 000000000..dfc5ce22f
--- /dev/null
+++ b/js/yui3/assets/skins/night/dial.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+v\:oval,v\:shadow,v\:fill{behavior:url(#default#VML);display:inline-block;zoom:1;*display:inline}.yui3-dial{position:relative;display:-moz-inline-stack;display:inline-block;zoom:1;*display:inline}.yui3-dial-content,.yui3-dial-ring{position:relative}.yui3-dial-handle,.yui3-dial-marker,.yui3-dial-center-button,.yui3-dial-reset-string,.yui3-dial-handle-vml,.yui3-dial-marker-vml,.yui3-dial-center-button-vml,.yui3-dial-ring-vml v\:oval,.yui3-dial-center-button-vml v\:oval{position:absolute}.yui3-dial-center-button-vml v\:oval{font-size:1px;top:0;left:0}.yui3-dial-content .yui3-dial-ring .yui3-dial-hidden v\:oval,.yui3-dial-content .yui3-dial-ring .yui3-dial-hidden{opacity:0;filter:alpha(opacity=0)}.yui3-skin-night .yui3-dial{color:#fff}.yui3-skin-night .yui3-dial-handle{background:#439ede;opacity:.3;-moz-box-shadow:1px 1px 1px rgba(0,0,0,0.9) inset;-webkit-box-shadow:1px 1px 1px rgba(0,0,0,0.9) inset;box-shadow:1px 1px 1px rgba(0,0,0,0.9) inset;cursor:pointer;font-size:1px}.yui3-skin-night .yui3-dial-ring{background:#595b5b;background:-moz-linear-gradient(0% 100% 315deg,#5e6060,#2d2e2f);background:-webkit-gradient(linear,50% 0,100% 100%,from(#636666),to(#424344));-moz-box-shadow:1px 1px 2px rgba(0,0,0,0.7) inset;-webkit-box-shadow:1px 1px 3px rgba(0,0,0,0.7) inset;box-shadow:1px 1px 5px rgba(0,0,0,0.4) inset}.yui3-skin-night .yui3-dial-center-button{-moz-box-shadow:-1px -1px 2px rgba(0,0,0,0.3) inset,1px 1px 2px rgba(0,0,0,0.5);-webkit-box-shadow:-1px -1px 2px rgba(0,0,0,0.3) inset,1px 1px 2px rgba(0,0,0,0.5);box-shadow:-1px -1px 2px rgba(0,0,0,0.3) inset,1px 1px 2px rgba(0,0,0,0.5);background:#dddbd4;background:-moz-radial-gradient(30% 30% 0deg,circle farthest-side,#999c9c 24%,#898989 41%,#535555 87%) repeat scroll 0 0 transparent;background:-webkit-gradient(radial,15 15,15,30 30,40,from(#999c9c),to(#535555),color-stop(.2,#898989));cursor:pointer;opacity:.7}.yui3-skin-night .yui3-dial-reset-string{color:#fff;font-size:72%;text-decoration:none}.yui3-skin-night .yui3-dial-label{color:#cbcbcb;margin-bottom:.8em}.yui3-skin-night .yui3-dial-value-string{margin-left:.5em;color:#dcdcdc;font-size:130%}.yui3-skin-night .yui3-dial-value{visibility:hidden;position:absolute;top:0;left:102%;width:4em}.yui3-skin-night .yui3-dial-north-mark{position:absolute;border-left:2px solid #434343;height:5px;left:50%;top:-7px;font-size:1px}.yui3-skin-night .yui3-dial-marker{background-color:#a0d8ff;opacity:.2;font-size:1px}.yui3-skin-night .yui3-dial-marker-max-min{background-color:#ff0404;opacity:.6}.yui3-skin-night .yui3-dial-ring-vml,.yui3-skin-night .yui3-dial-center-button-vml,.yui3-skin-night .yui3-dial-marker v\:oval.yui3-dial-marker-max-min,.yui3-skin-night v\:oval.yui3-dial-marker-max-min,.yui3-skin-night .yui3-dial-marker-vml,.yui3-skin-night .yui3-dial-handle-vml{background:0;opacity:1}#yui3-css-stamp.skin-night-dial{display:none}
diff --git a/js/yui3/assets/skins/night/horizontal-menu-submenu-indicator.png b/js/yui3/assets/skins/night/horizontal-menu-submenu-indicator.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/night/horizontal-menu-submenu-indicator.png
diff --git a/js/yui3/assets/skins/night/node-menunav.css b/js/yui3/assets/skins/night/node-menunav.css
new file mode 100644
index 000000000..9fdbee6b0
--- /dev/null
+++ b/js/yui3/assets/skins/night/node-menunav.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-menu .yui3-menu{position:absolute;z-index:1}.yui3-menu .yui3-shim{position:absolute;top:0;left:0;z-index:-1;opacity:0;filter:alpha(opacity=0);border:0;margin:0;padding:0;height:100%;width:100%}.yui3-menu-hidden{top:-10000px;left:-10000px;visibility:hidden}.yui3-menu li{list-style-type:none}.yui3-menu ul,.yui3-menu li{margin:0;padding:0}.yui3-menu-label,.yui3-menuitem-content{text-align:left;white-space:nowrap;display:block}.yui3-menu-horizontal li{float:left;width:auto}.yui3-menu-horizontal li li{float:none}.yui3-menu-horizontal ul{*zoom:1}.yui3-menu-horizontal ul ul{*zoom:normal}.yui3-menu-horizontal>.yui3-menu-content>ul:after{content:"";display:block;clear:both;line-height:0;font-size:0;visibility:hidden}.yui3-menu-content{*zoom:1}.yui3-menu-hidden .yui3-menu-content{*zoom:normal}.yui3-menuitem-content,.yui3-menu-label{_zoom:1}.yui3-menu-hidden .yui3-menuitem-content,.yui3-menu-hidden .yui3-menu-label{_zoom:normal}.yui3-skin-night .yui3-menu-content,.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-content{font-size:100%;line-height:2.25;*line-height:1.45;border:solid 1px #303030;background:#151515}.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-content{font-size:100%}.yui3-skin-night .yui3-menu-horizontal .yui3-menu-content{line-height:2;*line-height:1.9;background-color:#3b3c3d;background:-moz-linear-gradient(0% 100% 90deg,#242526 0,#3b3c3d 96%,#2c2d2f 100%);background:-webkit-gradient(linear,left bottom,left top,from(#242526),color-stop(0.96,#3b3c3d),to(#2c2d2f));padding:0}.yui3-skin-night .yui3-menu ul,.yui3-skin-night .yui3-menu ul ul{margin-top:3px;padding-top:3px;border-top:solid 1px #303030}.yui3-skin-night .yui3-menu ul.first-of-type{border:0;margin:0;padding:0}.yui3-skin-night .yui3-menu-horizontal ul{padding:0;margin:0;border:0}.yui3-skin-night .yui3-menu li,.yui3-skin-night .yui3-menu .yui3-menu li{_border-bottom:solid 1px #151515}.yui3-skin-night .yui3-menu-horizontal li{_border-bottom:0}.yui3-skin-night .yui3-menubuttonnav li{border-right:solid 1px #ccc}.yui3-skin-night .yui3-splitbuttonnav li{border-right:solid 1px #303030}.yui3-skin-night .yui3-menubuttonnav li li,.yui3-skin-night .yui3-splitbuttonnav li li{border-right:0}.yui3-skin-night .yui3-menu-label,.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-label,.yui3-skin-night .yui3-menuitem-content,.yui3-skin-night .yui3-menu .yui3-menu .yui3-menuitem-content{padding:0 1em;color:#fff;text-decoration:none;cursor:default;float:none;border:0;margin:0}.yui3-skin-night .yui3-menu-horizontal .yui3-menu-label,.yui3-skin-night .yui3-menu-horizontal .yui3-menuitem-content{padding:0 10px;border-style:solid;border-color:#303030;border-width:1px 0;margin:-1px 0;float:left;width:auto}.yui3-skin-night .yui3-menu-label,.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-label{background:url(vertical-menu-submenu-indicator.png) right center no-repeat}.yui3-skin-night .yui3-menu-horizontal .yui3-menu-label{background:0}.yui3-skin-night .yui3-menubuttonnav .yui3-menu-label,.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label{background-image:none}.yui3-skin-night .yui3-menubuttonnav .yui3-menu-label{padding-right:0}.yui3-skin-night .yui3-menubuttonnav .yui3-menu-label em{font-style:normal;padding-right:20px;display:block;background:url(horizontal-menu-submenu-indicator.png) right center no-repeat}.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label{padding:0}.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label a{float:left;width:auto;color:#fff;text-decoration:none;cursor:default;padding:0 5px 0 10px}.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label .yui3-menu-toggle{padding:0;border-left:solid 1px #303030;width:15px;overflow:hidden;text-indent:-1000px;background:url(horizontal-menu-submenu-indicator.png) 3px center no-repeat}.yui3-skin-night .yui3-menu-label-active,.yui3-skin-night .yui3-menu-label-menuvisible,.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-label-active,.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-label-menuvisible{background-color:#292a2a}.yui3-skin-night .yui3-menuitem-active .yui3-menuitem-content,.yui3-skin-night .yui3-menu .yui3-menu .yui3-menuitem-active .yui3-menuitem-content{background-image:none;background-color:#292a2a;background:-moz-linear-gradient(0% 100% 90deg,#252626 0,#333434 100%);background:-webkit-gradient(linear,left top,left bottom,from(#333434),to(#252626));border-left-width:0;margin-left:0}.yui3-skin-night .yui3-menu-horizontal .yui3-menu-label-active,.yui3-skin-night .yui3-menu-horizontal .yui3-menuitem-active .yui3-menuitem-content,.yui3-skin-night .yui3-menu-horizontal .yui3-menu-label-menuvisible{border-color:#303030;background-color:#555658;background:-moz-linear-gradient(0% 100% 90deg,#343536 0,#555658 96%,#3e3f41 100%);background:-webkit-gradient(linear,left bottom,left top,from(#343536),color-stop(0.96,#555658),to(#3e3f41))}.yui3-skin-night .yui3-menubuttonnav .yui3-menu-label-active,.yui3-skin-night .yui3-menubuttonnav .yui3-menuitem-active .yui3-menuitem-content,.yui3-skin-night .yui3-menubuttonnav .yui3-menu-label-menuvisible,.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label-active,.yui3-skin-night .yui3-splitbuttonnav .yui3-menuitem-active .yui3-menuitem-content,.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label-menuvisible{border-left-width:1px;margin-left:-1px}.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label-menuvisible{border-color:#303030;background:transparent}.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label-menuvisible .yui3-menu-toggle{border-color:#303030;background-color:#505050}#yui3-css-stamp.skin-night-node-menunav{display:none}
diff --git a/js/yui3/assets/skins/night/overlay.css b/js/yui3/assets/skins/night/overlay.css
new file mode 100644
index 000000000..04b9df09a
--- /dev/null
+++ b/js/yui3/assets/skins/night/overlay.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-overlay{position:absolute}.yui3-overlay-hidden{visibility:hidden}.yui3-widget-tmp-forcesize .yui3-overlay-content{overflow:hidden!important}.yui3-skin-night{background-color:#000;font-family:HelveticaNeue,arial,helvetica,clean,sans-serif;color:#fff}.yui3-skin-night .yui3-overlay-content ul,ol,li{margin:0;padding:0;list-style:none;zoom:1}.yui3-skin-night .yui3-overlay-content li{*float:left}.yui3-skin-night .yui3-overlay-content{background-color:#6d6e6e;-moz-box-shadow:0 0 17px rgba(0,0,0,0.58);-webkit-box-shadow:0 0 17px rgba(0,0,0,0.58);box-shadow:0 0 17px rgba(0,0,0,0.58);-moz-border-radius:7px;-webkit-border-radius:7px;border-radius:7px}.yui3-skin-night .yui3-overlay-content .yui3-widget-hd{background-color:#6d6e6e;-moz-border-radius:7px 7px 0 0;-webkit-border-radius:7px 7px 0 0;border-radius:7px 7px 0 0;color:#fff;margin:0;padding:20px 22px 0;font-size:147%}.yui3-skin-night .yui3-overlay-content .yui3-widget-bd{padding:11px 22px 17px;font-size:92%}.yui3-skin-night .yui3-overlay .yui3-widget-bd li{margin:.04em}.yui3-skin-night .yui3-overlay-content .yui3-widget-ft{background-color:#575858;border-top:solid 1px #494a4a;-moz-border-radius:0 0 7px 7px;-webkit-border-radius:0 0 7px 7px;border-radius:0 0 7px 7px;padding:17px 25px 20px;text-align:center}.yui3-skin-night .yui3-overlay-content .yui3-widget-ft li{margin:3px;display:inline-block}.yui3-skin-night .yui3-overlay-content .yui3-widget-ft li a{border:solid 1px #1b1c1c;border-radius:6px;-moz-box-shadow:0 1px #677478;-webkit-box-shadow:0 1px #677478;box-shadow:0 1px #677478;text-shadow:0 -1px 0 rgba(0,0,0,0.7);font-size:85%;text-align:center;color:#fff;padding:6px 28px;background-color:#2b2d2d;background:-moz-linear-gradient(0% 100% 90deg,#242526 0,#3b3c3d 96%,#2c2d2f 100%);background:-webkit-gradient(linear,left bottom,left top,from(#242526),color-stop(0.96,#3b3c3d),to(#2c2d2f))}.yui3-skin-night .yui3-overlay .yui3-widget-ft li:first-child{margin-left:0}.yui3-skin-night .yui3-overlay .yui3-widget-ft li:last-child{margin-right:0}.yui3-skin-night .yui3-overlay .yui3-widget-ft li:last-child a{border:solid 1px #520e00;-moz-box-shadow:0 1px #7d5d57;-webkit-box-shadow:0 1px #7d5d57;box-shadow:0 1px #7d5d57;background-color:#901704;background:-moz-linear-gradient(100% 0 270deg,#ab1c0b,#7b1400);background:-webkit-gradient(linear,left top,left bottom,from(#ab1c0b),to(#7b1400));margin-right:0}#yui3-widget-mask{background-color:#000;opacity:.5}#yui3-css-stamp.skin-night-overlay{display:none}
diff --git a/js/yui3/assets/skins/night/panel.css b/js/yui3/assets/skins/night/panel.css
new file mode 100644
index 000000000..05e526045
--- /dev/null
+++ b/js/yui3/assets/skins/night/panel.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-panel{position:absolute}.yui3-panel-hidden{visibility:hidden}.yui3-widget-tmp-forcesize .yui3-panel-content{overflow:hidden!important}.yui3-panel .yui3-widget-hd{position:relative}.yui3-panel .yui3-widget-hd .yui3-widget-buttons{position:absolute;top:0;right:0}.yui3-panel .yui3-widget-ft .yui3-widget-buttons{display:inline-block;*display:inline;zoom:1}.yui3-skin-night .yui3-panel{color:#fff;font-family:HelveticaNeue,arial,helvetica,clean,sans-serif}.yui3-skin-night .yui3-panel-content{background:#6d6e6e;-webkit-box-shadow:0 0 20px #000;-moz-box-shadow:0 0 20px #000;box-shadow:0 0 20px #000;border:1px solid black;-webkit-border-radius:7px;-moz-border-radius:7px;border-radius:7px}.yui3-skin-night .yui3-panel .yui3-widget-hd{padding:11px 57px 11px 22px;min-height:17px;_height:17px;-webkit-border-top-left-radius:7px;-webkit-border-top-right-radius:7px;-moz-border-radius-topleft:7px;-moz-border-radius-topright:7px;border-top-left-radius:7px;border-top-right-radius:7px;font-weight:bold;color:white;background-color:#555658;background:-moz-linear-gradient(0% 100% 90deg,#343536 0,#555658 96%,#3e3f41 100%);background:-webkit-gradient(linear,left bottom,left top,from(#343536),color-stop(0.96,#555658),to(#3e3f41))}.yui3-skin-night .yui3-panel .yui3-widget-hd .yui3-widget-buttons{padding:11px}.yui3-skin-night .yui3-panel .yui3-widget-bd{padding:11px 22px 17px}.yui3-skin-night .yui3-panel .yui3-widget-ft{background-color:#575858;border-top:1px solid #494a4a;padding:6px 16px 8px;text-align:center;-webkit-border-bottom-right-radius:7px;-webkit-border-bottom-left-radius:7px;-moz-border-radius-bottomright:7px;-moz-border-radius-bottomleft:7px;border-bottom-right-radius:7px;border-bottom-left-radius:7px}.yui3-skin-night .yui3-panel .yui3-widget-ft .yui3-widget-buttons{bottom:0;position:relative;right:auto;width:100%;text-align:center;padding-bottom:0;margin-left:-5px;margin-right:-5px}.yui3-skin-night .yui3-panel .yui3-widget-ft .yui3-button{margin:5px}.yui3-skin-night .yui3-panel .yui3-widget-hd .yui3-button-close{background:transparent;filter:none;border:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;width:22px;height:17px;padding:0;overflow:hidden;vertical-align:top;*font-size:0;*line-height:0;*letter-spacing:-1000px;*color:#86a5ec;*background:url(sprite_icons.png) no-repeat center 3px}.yui3-skin-night .yui3-panel .yui3-widget-hd .yui3-button-close:hover{background-color:#333}.yui3-skin-night .yui3-panel .yui3-widget-hd .yui3-button-close:before{content:url(sprite_icons.png);display:inline-block;text-align:center;font-size:0;line-height:0;width:22px;margin:3px 0 0 1px}.yui3-skin-night .yui3-panel-hidden .yui3-widget-hd .yui3-button-close{display:none}#yui3-css-stamp.skin-night-panel{display:none}
diff --git a/js/yui3/assets/skins/night/rail-x-lines.png b/js/yui3/assets/skins/night/rail-x-lines.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/night/rail-x-lines.png
diff --git a/js/yui3/assets/skins/night/rail-x.png b/js/yui3/assets/skins/night/rail-x.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/night/rail-x.png
diff --git a/js/yui3/assets/skins/night/rail-y-lines.png b/js/yui3/assets/skins/night/rail-y-lines.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/night/rail-y-lines.png
diff --git a/js/yui3/assets/skins/night/rail-y.png b/js/yui3/assets/skins/night/rail-y.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/night/rail-y.png
diff --git a/js/yui3/assets/skins/night/resize-base.css b/js/yui3/assets/skins/night/resize-base.css
new file mode 100644
index 000000000..f0a5b41fe
--- /dev/null
+++ b/js/yui3/assets/skins/night/resize-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-resize,.yui3-resize-wrapper{z-index:0;zoom:1}.yui3-resize-handle{position:absolute;display:block;z-index:100;zoom:1}.yui3-resize-proxy{position:absolute;border:1px dashed #000;position:absolute;z-index:10000}.yui3-resize-hidden-handles .yui3-resize-handle{opacity:0;filter:alpha(opacity=0)}.yui3-resize-handle-t,.yui3-resize-handle-b{width:100%;left:0;height:6px}.yui3-resize-handle-l,.yui3-resize-handle-r{height:100%;top:0;width:6px}.yui3-resize-handle-t{cursor:n-resize;top:0}.yui3-resize-handle-b{cursor:s-resize;bottom:0}.yui3-resize-handle-l{cursor:w-resize;left:0}.yui3-resize-handle-r{cursor:e-resize;right:0}.yui3-resize-handle-inner{position:absolute;zoom:1}@media only screen and (min-device-width :320px) and (max-device-width :480px){.yui3-resize-handle-inner:after{content:"";width:40px;height:40px;position:absolute}.yui3-resize-handle-inner-r,.yui3-resize-handle-inner-l,.yui3-resize-handle-inner-t,.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-tr,.yui3-resize-handle-inner-br,.yui3-resize-handle-inner-tl,.yui3-resize-handle-inner-bl{overflow:visible!important}.yui3-resize-handle-inner-r:after{top:-12px;right:0}.yui3-resize-handle-inner-l:after{top:-12px;left:0}.yui3-resize-handle-inner-t:after{top:0;left:-12px}.yui3-resize-handle-inner-b:after{bottom:0;left:-12px}.yui3-resize-handle-inner-tr:after{top:0;right:0}.yui3-resize-handle-inner-br:after{bottom:0;right:0}.yui3-resize-handle-inner-tl:after{top:0;left:0}.yui3-resize-handle-inner-bl:after{bottom:0;left:0}}@media only screen and (min-device-width :768px) and (max-device-width :1024px){.yui3-resize-handle-inner:after{content:"";width:30px;height:30px;position:absolute}.yui3-resize-handle-inner-r,.yui3-resize-handle-inner-l,.yui3-resize-handle-inner-t,.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-tr,.yui3-resize-handle-inner-br,.yui3-resize-handle-inner-tl,.yui3-resize-handle-inner-bl{overflow:visible!important}.yui3-resize-handle-inner-r:after{top:-6px;right:0}.yui3-resize-handle-inner-l:after{top:-6px;left:0}.yui3-resize-handle-inner-t:after{top:0;left:-6px}.yui3-resize-handle-inner-b:after{bottom:0;left:-6px}.yui3-resize-handle-inner-tr:after{top:0;right:0}.yui3-resize-handle-inner-br:after{bottom:0;right:0}.yui3-resize-handle-inner-tl:after{top:0;left:0}.yui3-resize-handle-inner-bl:after{bottom:0;left:0}}.yui3-resize-handle-inner-t,.yui3-resize-handle-inner-b{margin-left:-8px;left:50%}.yui3-resize-handle-inner-l,.yui3-resize-handle-inner-r{margin-top:-8px;top:50%}.yui3-resize-handle-inner-t{top:-4px}.yui3-resize-handle-inner-b{bottom:-4px}.yui3-resize-handle-inner-l{left:-4px}.yui3-resize-handle-inner-r{right:-4px}.yui3-resize-handle-tr,.yui3-resize-handle-br,.yui3-resize-handle-tl,.yui3-resize-handle-bl{height:15px;width:15px;z-index:200}.yui3-resize-handle-tr{cursor:ne-resize;top:0;right:0}.yui3-resize-handle-tl{cursor:nw-resize;top:0;left:0}.yui3-resize-handle-br{cursor:se-resize;bottom:0;right:0}.yui3-resize-handle-bl{cursor:sw-resize;bottom:0;left:0}.yui3-resize-handle-inner-r,.yui3-resize-handle-inner-l,.yui3-resize-handle-inner-t,.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-tr,.yui3-resize-handle-inner-br,.yui3-resize-handle-inner-tl,.yui3-resize-handle-inner-bl{background-repeat:no-repeat;background:url(arrows.png) no-repeat 0 0;display:block;height:15px;overflow:hidden;text-indent:-99999em;width:15px}.yui3-resize-handle-inner-br{background-position:-30px 0;bottom:-2px;right:-2px}.yui3-resize-handle-inner-tr{background-position:-58px 0;bottom:0;right:-2px}.yui3-resize-handle-inner-bl{background-position:-75px 0;bottom:-2px;right:-2px}.yui3-resize-handle-inner-tl{background-position:-47px 0;bottom:0;right:-2px}.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-t{background-position:-15px 0}#yui3-css-stamp.skin-night-resize-base{display:none}
diff --git a/js/yui3/assets/skins/night/scrollview-base.css b/js/yui3/assets/skins/night/scrollview-base.css
new file mode 100644
index 000000000..ba1572531
--- /dev/null
+++ b/js/yui3/assets/skins/night/scrollview-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-scrollview{position:relative;overflow:hidden;-webkit-user-select:none;-moz-user-select:none}.yui3-scrollview-hidden{display:none}.yui3-scrollview-content{position:relative}#yui3-css-stamp.skin-night-scrollview-base{display:none}
diff --git a/js/yui3/assets/skins/night/scrollview-list.css b/js/yui3/assets/skins/night/scrollview-list.css
new file mode 100644
index 000000000..7e2fe81bb
--- /dev/null
+++ b/js/yui3/assets/skins/night/scrollview-list.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night .yui3-scrollview{-webkit-tap-highlight-color:rgba(0,0,0,0)}.yui3-skin-night .yui3-scrollview{color:#fff;background-color:#000}.yui3-skin-night .yui3-scrollview-vert .yui3-scrollview-content{border-top:0;background-color:#000;font-family:HelveticaNeue,arial,helvetica,clean,sans-serif;color:#fff}.yui3-skin-night .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-item{*zoom:1}.yui3-skin-night .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-list{*zoom:1;list-style:none;padding:0;margin:0}.yui3-skin-night .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-item{border-bottom:1px solid #303030;padding:15px 20px 16px;font-size:100%;font-weight:bold;background-color:#151515;cursor:pointer}.yui3-skin-night .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-list.selected{background-color:#2c2d2e;background:-moz-linear-gradient(0% 100% 90deg,#252626 0,#333434 100%);background:-webkit-gradient(linear,left top,left bottom,from(#333434),to(#252626));border-top:solid 1px #4b4b4b;border-bottom:solid 1px #3e3f3f;margin-top:-1px}#yui3-css-stamp.skin-night-scrollview-list{display:none}
diff --git a/js/yui3/assets/skins/night/scrollview-scrollbars.css b/js/yui3/assets/skins/night/scrollview-scrollbars.css
new file mode 100644
index 000000000..0ad911acb
--- /dev/null
+++ b/js/yui3/assets/skins/night/scrollview-scrollbars.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-scrollview-scrollbar{opacity:1;position:absolute;width:6px;height:10px}.yui3-scrollview-scrollbar{top:0;right:1px}.yui3-scrollview-scrollbar-horiz{top:auto;height:8px;width:20px;bottom:1px;left:0}.yui3-scrollview-scrollbar .yui3-scrollview-child{position:absolute;right:0;display:block;width:100%;height:4px}.yui3-scrollview-scrollbar .yui3-scrollview-first{top:0}.yui3-scrollview-scrollbar .yui3-scrollview-last{top:0}.yui3-scrollview-scrollbar .yui3-scrollview-middle{position:absolute;top:4px;height:1px}.yui3-scrollview-scrollbar-horiz .yui3-scrollview-child{display:-moz-inline-stack;display:inline-block;zoom:1;*display:inline;top:0;left:0;bottom:auto;right:auto}.yui3-scrollview-scrollbar-horiz .yui3-scrollview-first,.yui3-scrollview-scrollbar-horiz .yui3-scrollview-last{width:4px;height:6px}.yui3-scrollview-scrollbar-horiz .yui3-scrollview-middle{top:0;left:4px;width:1px;height:6px}.yui3-scrollview-scrollbar-vert-basic{height:auto}.yui3-scrollview-scrollbar-vert-basic .yui3-scrollview-child{position:static;_overflow:hidden;_line-height:4px}.yui3-scrollview-scrollbar-horiz-basic{width:auto;white-space:nowrap;line-height:6px;_overflow:hidden}.yui3-scrollview-scrollbar-horiz-basic .yui3-scrollview-child{position:static;padding:0;margin:0;top:auto;left:auto;right:auto;bottom:auto}.yui3-skin-night .yui3-scrollview-scrollbar{-webkit-transform:translate3d(0,0,0);-moz-transform:translate(0,0)}.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-first,.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-middle,.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-last{border-radius:3px;-webkit-border-radius:3px;-moz-border-radius:3px;background-color:#808080;opacity:.3;filter:alpha(opacity=30)}.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-first,.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-last{border-bottom-right-radius:0;border-bottom-left-radius:0;-webkit-border-bottom-right-radius:0;-webkit-border-bottom-left-radius:0;-moz-border-radius-bottomright:0;-moz-border-radius-bottomleft:0}.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-last{border-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;-webkit-border-radius:0;-webkit-border-bottom-right-radius:3px;-webkit-border-bottom-left-radius:3px;-webkit-transform:translate3d(0,0,0);-moz-border-radius:0;-moz-border-radius-bottomright:3px;-moz-border-radius-bottomleft:3px;-moz-transform:translate(0,0)}.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-middle{border-radius:0;-webkit-border-radius:0;-moz-border-radius:0;-webkit-transform:translate3d(0,0,0) scaleY(1);-webkit-transform-origin:0 0;-moz-transform:translate(0,0) scaleY(1);-moz-transform-origin:0 0}.yui3-skin-night .yui3-scrollview-scrollbar-horiz .yui3-scrollview-first,.yui3-skin-night .yui3-scrollview-scrollbar-horiz .yui3-scrollview-last{border-top-right-radius:0;border-bottom-left-radius:3px;-webkit-border-top-right-radius:0;-webkit-border-bottom-left-radius:3px;-moz-border-radius-topright:0;-moz-border-radius-bottomleft:3px}.yui3-skin-night .yui3-scrollview-scrollbar-horiz .yui3-scrollview-last{border-bottom-left-radius:0;border-top-right-radius:3px;-webkit-border-bottom-left-radius:0;-webkit-border-top-right-radius:3px;-moz-border-radius-bottomleft:0;-moz-border-radius-topright:3px}.yui3-skin-night .yui3-scrollview-scrollbar-horiz .yui3-scrollview-middle{-webkit-transform:translate3d(0,0,0) scaleX(1);-webkit-transform-origin:0 0;-moz-transform:translate(0,0) scaleX(1);-moz-transform-origin:0 0}.yui3-skin-night .yui3-scrollview-scrollbar-vert-basic .yui3-scrollview-child,.yui3-skin-night .yui3-scrollview-scrollbar-horiz-basic .yui3-scrollview-child{background-color:#aaa;background-image:none}#yui3-css-stamp.skin-night-scrollview-scrollbars{display:none}
diff --git a/js/yui3/assets/skins/night/slider-base.css b/js/yui3/assets/skins/night/slider-base.css
new file mode 100644
index 000000000..9aa9ebb90
--- /dev/null
+++ b/js/yui3/assets/skins/night/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-night .yui3-slider-x .yui3-slider-rail,.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x}.yui3-skin-night .yui3-slider-x .yui3-slider-rail{height:25px}.yui3-skin-night .yui3-slider-x .yui3-slider-thumb{height:26px;width:21px}.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -20px;height:20px;left:-5px;width:5px}.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -40px;height:20px;right:-5px;width:5px}.yui3-skin-night .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-10px}.yui3-skin-night .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-50px}.yui3-skin-night .yui3-slider-y .yui3-slider-rail,.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y}.yui3-skin-night .yui3-slider-y .yui3-slider-rail{width:25px}.yui3-skin-night .yui3-slider-y .yui3-slider-thumb{width:26px;height:21px}.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-20px 0;width:20px;top:-5px;height:5px}.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-40px 0;width:20px;bottom:-5px;height:5px}.yui3-skin-night .yui3-slider-y .yui3-slider-thumb-image{left:-10px;top:0}.yui3-skin-night .yui3-slider-y .yui3-slider-thumb-shadow{left:-50px;opacity:.15;filter:alpha(opacity=15);top:0}#yui3-css-stamp.skin-night-slider-base{display:none}
diff --git a/js/yui3/assets/skins/night/sort-arrow-sprite-ie.png b/js/yui3/assets/skins/night/sort-arrow-sprite-ie.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/night/sort-arrow-sprite-ie.png
diff --git a/js/yui3/assets/skins/night/sort-arrow-sprite.png b/js/yui3/assets/skins/night/sort-arrow-sprite.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/night/sort-arrow-sprite.png
diff --git a/js/yui3/assets/skins/night/sprite_icons.png b/js/yui3/assets/skins/night/sprite_icons.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/night/sprite_icons.png
diff --git a/js/yui3/assets/skins/night/tabview.css b/js/yui3/assets/skins/night/tabview.css
new file mode 100644
index 000000000..82eafa1b1
--- /dev/null
+++ b/js/yui3/assets/skins/night/tabview.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-tab-panel{display:none}.yui3-tab-panel-selected{display:block}.yui3-tabview-list,.yui3-tab{margin:0;padding:0;list-style:none}.yui3-tabview{position:relative}.yui3-tabview,.yui3-tabview-list,.yui3-tabview-panel,.yui3-tab,.yui3-tab-panel{zoom:1}.yui3-tab{display:inline-block;*display:inline;vertical-align:bottom;cursor:pointer}.yui3-tab-label{display:block;display:inline-block;padding:6px 10px;position:relative;text-decoration:none;vertical-align:bottom}.yui3-skin-night .yui3-tabview-panel{background-color:#333;color:#808080;padding:1px}.yui3-skin-night .yui3-tab-panel p{margin:10px}.yui3-skin-night .yui3-tabview-list{background-color:#0f0f0f;border-top:1px solid #000;text-align:center;height:46px;background:-moz-linear-gradient(0% 100% 90deg,#0f0f0f 0,#1e1e1e 96%,#292929 100%);background:-webkit-gradient(linear,left bottom,left top,from(#0f0f0f),color-stop(0.96,#1e1e1e),to(#292929))}.yui3-skin-night .yui3-tabview-list li{margin-top:8px}.yui3-skin-night .yui3-tabview-list li a{border:solid 1px #0c0c0c;border-right-style:none;-moz-box-shadow:0 1px #222;-webkit-box-shadow:0 1px #222;box-shadow:0 1px #222;text-shadow:0 -1px 0 rgba(0,0,0,0.7);font-size:85%;text-align:center;color:#fff;padding:6px 28px;background-color:#555658;background:-moz-linear-gradient(0% 100% 90deg,#343536 0,#555658 96%,#3e3f41 100%);background:-webkit-gradient(linear,left bottom,left top,from(#343536),color-stop(0.96,#555658),to(#3e3f41))}.yui3-skin-night .yui3-tabview-list li.yui3-tab-selected a{background-color:#2b2d2d;background:-moz-linear-gradient(0% 100% 90deg,#242526 0,#3b3c3d 96%,#2c2d2f 100%);background:-webkit-gradient(linear,left bottom,left top,from(#242526),color-stop(0.96,#3b3c3d),to(#2c2d2f))}.yui3-skin-night .yui3-tabview-list li:first-child a{-moz-border-radius:6px 0 0 6px;-webkit-border-radius:6px 0 0 6px;border-radius:6px 0 0 6px}.yui3-skin-night .yui3-tabview-list li:last-child a{border-right-style:solid;-moz-border-radius:0 6px 6px 0;-webkit-border-radius:0 6px 6px 0;border-radius:0 6px 6px 0}#yui3-css-stamp.skin-night-tabview{display:none}
diff --git a/js/yui3/assets/skins/night/thumb-x.png b/js/yui3/assets/skins/night/thumb-x.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/night/thumb-x.png
diff --git a/js/yui3/assets/skins/night/thumb-y.png b/js/yui3/assets/skins/night/thumb-y.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/night/thumb-y.png
diff --git a/js/yui3/assets/skins/night/vertical-menu-submenu-indicator.png b/js/yui3/assets/skins/night/vertical-menu-submenu-indicator.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/night/vertical-menu-submenu-indicator.png
diff --git a/js/yui3/assets/skins/night/widget-base.css b/js/yui3/assets/skins/night/widget-base.css
new file mode 100644
index 000000000..89eb3fd39
--- /dev/null
+++ b/js/yui3/assets/skins/night/widget-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-widget-hidden{display:none}.yui3-widget-content{overflow:hidden}.yui3-widget-content-expanded{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;height:100%}.yui3-widget-tmp-forcesize{overflow:hidden!important}#yui3-css-stamp.skin-night-widget-base{display:none}
diff --git a/js/yui3/assets/skins/night/widget-modality.css b/js/yui3/assets/skins/night/widget-modality.css
new file mode 100644
index 000000000..1b9a6373d
--- /dev/null
+++ b/js/yui3/assets/skins/night/widget-modality.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night .yui3-widget-mask{background-color:black;zoom:1;-ms-filter:"alpha(opacity=40)";filter:alpha(opacity=40);opacity:.4}#yui3-css-stamp.skin-night-widget-modality{display:none}
diff --git a/js/yui3/assets/skins/night/widget-stack.css b/js/yui3/assets/skins/night/widget-stack.css
new file mode 100644
index 000000000..69c57e0dd
--- /dev/null
+++ b/js/yui3/assets/skins/night/widget-stack.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-widget-stacked .yui3-widget-shim{opacity:0;filter:alpha(opacity=0);position:absolute;border:0;top:0;left:0;padding:0;margin:0;z-index:-1;width:100%;height:100%;_width:0;_height:0}#yui3-css-stamp.skin-night-widget-stack{display:none}
diff --git a/js/yui3/assets/skins/round-dark/rail-x.png b/js/yui3/assets/skins/round-dark/rail-x.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/round-dark/rail-x.png
diff --git a/js/yui3/assets/skins/round-dark/rail-y.png b/js/yui3/assets/skins/round-dark/rail-y.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/round-dark/rail-y.png
diff --git a/js/yui3/assets/skins/round-dark/slider-base.css b/js/yui3/assets/skins/round-dark/slider-base.css
new file mode 100644
index 000000000..7bcf5e383
--- /dev/null
+++ b/js/yui3/assets/skins/round-dark/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail,.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x}.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail{height:25px;background-position:0 3px}.yui3-skin-round-dark .yui3-slider-x .yui3-slider-thumb{height:26px;width:24px}.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -17px;height:20px;left:-2px;width:5px}.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -37px;height:20px;right:-2px;width:5px}.yui3-skin-round-dark .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-7px}.yui3-skin-round-dark .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-47px}.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail,.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y}.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail{width:25px;background-position:3px 0}.yui3-skin-round-dark .yui3-slider-y .yui3-slider-thumb{width:26px;height:24px}.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-17px 0;width:20px;top:-2px;height:5px}.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-37px 0;width:20px;bottom:-2px;height:5px}.yui3-skin-round-dark .yui3-slider-y .yui3-slider-thumb-image{top:0;left:-7px}.yui3-skin-round-dark .yui3-slider-y .yui3-slider-thumb-shadow{top:0;left:-47px;opacity:.15;filter:alpha(opacity=15)}#yui3-css-stamp.skin-round-dark-slider-base{display:none}
diff --git a/js/yui3/assets/skins/round-dark/thumb-x-grip.png b/js/yui3/assets/skins/round-dark/thumb-x-grip.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/round-dark/thumb-x-grip.png
diff --git a/js/yui3/assets/skins/round-dark/thumb-x.png b/js/yui3/assets/skins/round-dark/thumb-x.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/round-dark/thumb-x.png
diff --git a/js/yui3/assets/skins/round-dark/thumb-y-grip.png b/js/yui3/assets/skins/round-dark/thumb-y-grip.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/round-dark/thumb-y-grip.png
diff --git a/js/yui3/assets/skins/round-dark/thumb-y.png b/js/yui3/assets/skins/round-dark/thumb-y.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/round-dark/thumb-y.png
diff --git a/js/yui3/assets/skins/round/rail-x.png b/js/yui3/assets/skins/round/rail-x.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/round/rail-x.png
diff --git a/js/yui3/assets/skins/round/rail-y.png b/js/yui3/assets/skins/round/rail-y.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/round/rail-y.png
diff --git a/js/yui3/assets/skins/round/slider-base.css b/js/yui3/assets/skins/round/slider-base.css
new file mode 100644
index 000000000..a3cec7072
--- /dev/null
+++ b/js/yui3/assets/skins/round/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-round .yui3-slider-x .yui3-slider-rail,.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x}.yui3-skin-round .yui3-slider-x .yui3-slider-rail{height:25px;background-position:0 3px}.yui3-skin-round .yui3-slider-x .yui3-slider-thumb{height:26px;width:24px}.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -17px;height:20px;left:-2px;width:5px}.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -37px;height:20px;right:-2px;width:5px}.yui3-skin-round .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-7px}.yui3-skin-round .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-47px}.yui3-skin-round .yui3-slider-y .yui3-slider-rail,.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y}.yui3-skin-round .yui3-slider-y .yui3-slider-rail{width:25px;background-position:3px 0}.yui3-skin-round .yui3-slider-y .yui3-slider-thumb{width:26px;height:24px}.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-17px 0;width:20px;top:-2px;height:5px}.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-37px 0;width:20px;bottom:-2px;height:5px}.yui3-skin-round .yui3-slider-y .yui3-slider-thumb-image{top:0;left:-8px}.yui3-skin-round .yui3-slider-y .yui3-slider-thumb-shadow{top:0;left:-48px;opacity:.15;filter:alpha(opacity=15)}#yui3-css-stamp.skin-round-slider-base{display:none}
diff --git a/js/yui3/assets/skins/round/thumb-x-grip.png b/js/yui3/assets/skins/round/thumb-x-grip.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/round/thumb-x-grip.png
diff --git a/js/yui3/assets/skins/round/thumb-x.png b/js/yui3/assets/skins/round/thumb-x.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/round/thumb-x.png
diff --git a/js/yui3/assets/skins/round/thumb-y-grip.png b/js/yui3/assets/skins/round/thumb-y-grip.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/round/thumb-y-grip.png
diff --git a/js/yui3/assets/skins/round/thumb-y.png b/js/yui3/assets/skins/round/thumb-y.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/round/thumb-y.png
diff --git a/js/yui3/assets/skins/sam-dark/rail-x-lines.png b/js/yui3/assets/skins/sam-dark/rail-x-lines.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/sam-dark/rail-x-lines.png
diff --git a/js/yui3/assets/skins/sam-dark/rail-x.png b/js/yui3/assets/skins/sam-dark/rail-x.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/sam-dark/rail-x.png
diff --git a/js/yui3/assets/skins/sam-dark/rail-y-lines.png b/js/yui3/assets/skins/sam-dark/rail-y-lines.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/sam-dark/rail-y-lines.png
diff --git a/js/yui3/assets/skins/sam-dark/rail-y.png b/js/yui3/assets/skins/sam-dark/rail-y.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/sam-dark/rail-y.png
diff --git a/js/yui3/assets/skins/sam-dark/slider-base.css b/js/yui3/assets/skins/sam-dark/slider-base.css
new file mode 100644
index 000000000..ea12b363d
--- /dev/null
+++ b/js/yui3/assets/skins/sam-dark/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail,.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x}.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail{height:26px}.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-thumb{height:26px;width:15px}.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -20px;height:20px;left:-2px;width:5px}.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -40px;height:20px;right:-2px;width:5px}.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-10px}.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-50px}.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail,.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y}.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail{width:26px}.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-thumb{width:26px;height:15px}.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-20px 0;width:20px;top:-2px;height:5px}.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-40px 0;width:20px;bottom:-2px;height:5px}.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-thumb-image{left:-10px;top:0}.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-thumb-shadow{left:-50px;opacity:.15;filter:alpha(opacity=15);top:0}#yui3-css-stamp.skin-sam-dark-slider-base{display:none}
diff --git a/js/yui3/assets/skins/sam-dark/thumb-x.png b/js/yui3/assets/skins/sam-dark/thumb-x.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/sam-dark/thumb-x.png
diff --git a/js/yui3/assets/skins/sam-dark/thumb-y.png b/js/yui3/assets/skins/sam-dark/thumb-y.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/js/yui3/assets/skins/sam-dark/thumb-y.png
diff --git a/js/yui3/assets/skins/sam/arrows.png b/js/yui3/assets/skins/sam/arrows.png
new file mode 100644
index 000000000..2942681f4
--- /dev/null
+++ b/js/yui3/assets/skins/sam/arrows.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/autocomplete-list.css b/js/yui3/assets/skins/sam/autocomplete-list.css
new file mode 100644
index 000000000..1788ecdbc
--- /dev/null
+++ b/js/yui3/assets/skins/sam/autocomplete-list.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-aclist{position:absolute;z-index:1}.yui3-aclist-hidden{visibility:hidden}.yui3-aclist-aria{left:-9999px;position:absolute}.yui3-aclist-list{list-style:none;margin:0;overflow:hidden;padding:0}.yui3-aclist-item{cursor:pointer;list-style:none;padding:2px 5px}.yui3-aclist-item-active{outline:#afafaf dotted thin}.yui3-skin-sam .yui3-aclist-content{background:#fff;border:1px solid #afafaf;-moz-box-shadow:1px 1px 4px rgba(0,0,0,0.58);-webkit-box-shadow:1px 1px 4px rgba(0,0,0,0.58);box-shadow:1px 1px 4px rgba(0,0,0,0.58)}.yui3-skin-sam .yui3-aclist-item-hover{background:#bfdaff}.yui3-skin-sam .yui3-aclist-item-active{background:#2647a0;color:#fff;outline:0}#yui3-css-stamp.skin-sam-autocomplete-list{display:none}
diff --git a/js/yui3/assets/skins/sam/bg.png b/js/yui3/assets/skins/sam/bg.png
new file mode 100644
index 000000000..fd11e03de
--- /dev/null
+++ b/js/yui3/assets/skins/sam/bg.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/calendar-base.css b/js/yui3/assets/skins/sam/calendar-base.css
new file mode 100644
index 000000000..009b7d8c6
--- /dev/null
+++ b/js/yui3/assets/skins/sam/calendar-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-calendar-pane{width:100%}.yui3-calendar-grid{width:100%}.yui3-calendar-column-hidden,.yui3-calendar-hidden{display:none}.yui3-skin-sam .yui3-calendar-content{padding:10px;color:#000;border:1px solid gray;background:#f2f2f2;background:-moz-linear-gradient(top,#f9f9f9 0,#f2f2f2 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#f9f9f9),color-stop(100%,#f2f2f2));background:-webkit-linear-gradient(top,#f9f9f9 0,#f2f2f2 100%);background:-o-linear-gradient(top,#f9f9f9 0,#f2f2f2 100%);background:-ms-linear-gradient(top,#f9f9f9 0,#f2f2f2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f9f9f9',endColorstr='#f2f2f2',GradientType=0);background:linear-gradient(top,#f9f9f9 0,#f2f2f2 100%);-moz-border-radius:5px;border-radius:5px}.yui3-skin-sam .yui3-calendar-grid{padding:5px;border-collapse:collapse}.yui3-skin-sam .yui3-calendar-header{padding-bottom:10px}.yui3-skin-sam .yui3-calendar-header-label{margin:0;font-size:1em;font-weight:bold;text-align:center;width:100%}.yui3-skin-sam .yui3-calendar-day,.yui3-skin-sam .yui3-calendar-prevmonth-day,.yui3-skin-sam .yui3-calendar-nextmonth-day{padding:5px;border:1px solid #ccc;background:#fff;text-align:center}.yui3-skin-sam .yui3-calendar-day:hover{background:#06c;color:#fff}.yui3-skin-sam .yui3-calendar-selection-disabled,.yui3-skin-sam .yui3-calendar-selection-disabled:hover{color:#a6a6a6;background:#ccc}.yui3-skin-sam .yui3-calendar-weekday{font-weight:bold}.yui3-skin-sam .yui3-calendar-prevmonth-day,.yui3-skin-sam .yui3-calendar-nextmonth-day{color:#a6a6a6}.yui3-skin-sam .yui3-calendar-day{font-weight:bold}.yui3-skin-sam .yui3-calendar-day-selected{background-color:#b3d4ff;color:#000}.yui3-skin-sam .yui3-calendar-left-grid{margin-right:1em}[dir="rtl"] .yui3-skin-sam .yui3-calendar-left-grid,.yui3-skin-sam [dir="rtl"] .yui3-calendar-left-grid{margin-right:auto;margin-left:1em}.yui3-skin-sam .yui3-calendar-right-grid{margin-left:1em}[dir="rtl"] .yui3-skin-sam .yui3-calendar-right-grid,.yui3-skin-sam [dir="rtl"] .yui3-calendar-right-grid{margin-left:auto;margin-right:1em}.yui3-skin-sam .yui3-calendar-day-highlighted{background-color:#dcdef5}.yui3-skin-sam .yui3-calendar-day-selected.yui3-calendar-day-highlighted{background-color:#758fbb}#yui3-css-stamp.skin-sam-calendar-base{display:none}
diff --git a/js/yui3/assets/skins/sam/calendar.css b/js/yui3/assets/skins/sam/calendar.css
new file mode 100644
index 000000000..14930c898
--- /dev/null
+++ b/js/yui3/assets/skins/sam/calendar.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-calendar-column-hidden,.yui3-calendar-hidden{display:none}.yui3-calendar-day{cursor:pointer}.yui3-calendar-selection-disabled{cursor:default}.yui3-calendar-prevmonth-day{cursor:default}.yui3-calendar-nextmonth-day{cursor:default}.yui3-calendar-content:hover .yui3-calendar-day,.yui3-calendar-content:hover .yui3-calendar-prevmonth-day,.yui3-calendar-content:hover .yui3-calendar-nextmonth-day{-moz-user-select:none}.yui3-skin-sam .yui3-calendar-day-highlighted{background-color:#dcdef5}.yui3-skin-sam .yui3-calendar-day-selected.yui3-calendar-day-highlighted{background-color:#758fbb}#yui3-css-stamp.skin-sam-calendar{display:none}
diff --git a/js/yui3/assets/skins/sam/calendarnavigator.css b/js/yui3/assets/skins/sam/calendarnavigator.css
new file mode 100644
index 000000000..f514f76ca
--- /dev/null
+++ b/js/yui3/assets/skins/sam/calendarnavigator.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-calendar-header{text-align:center;position:relative;width:100%}.yui3-calendar-header-label{display:inline}.yui3-calendarnav-prevmonth{cursor:pointer}.yui3-calendarnav-nextmonth{cursor:pointer}.yui3-skin-sam .yui3-calendarnav-prevmonth,.yui3-skin-sam .yui3-calendarnav-nextmonth{width:0;height:0;padding:0;margin:0;border:10px solid transparent;position:absolute;font-size:0;line-height:0;_border-left-color:white;_border-top-color:white;_border-right-color:white;_border-bottom-color:white;_filter:chroma(color=white)}.yui3-skin-sam .yui3-calendarnav-prevmonth:hover,[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-nextmonth:hover,.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-nextmonth:hover{border-right-color:#06c}.yui3-skin-sam .yui3-calendarnav-nextmonth:hover,[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-prevmonth:hover,.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-prevmonth:hover{border-left-color:#06c}.yui3-skin-sam .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,.yui3-skin-sam .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover,[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover,.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover{cursor:default;border-right-color:#ccc;border-left-color:transparent}.yui3-skin-sam .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,.yui3-skin-sam .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover,[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover,.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover{cursor:default;border-left-color:#ccc;border-right-color:transparent}.yui3-skin-sam .yui3-calendarnav-prevmonth{border-right-color:#000;left:0;margin-left:-10px}.yui3-skin-sam .yui3-calendarnav-nextmonth{border-left-color:#000;right:0;margin-right:-10px}[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-prevmonth,.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-prevmonth{left:auto;right:0;border-left-color:#000;border-right-color:transparent}[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-nextmonth,.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-nextmonth{left:0;right:auto;border-right-color:#000;border-left-color:transparent}#yui3-css-stamp.skin-sam-calendarnavigator{display:none}
diff --git a/js/yui3/assets/skins/sam/console-filters.css b/js/yui3/assets/skins/sam/console-filters.css
new file mode 100644
index 000000000..1230e6343
--- /dev/null
+++ b/js/yui3/assets/skins/sam/console-filters.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-console-ft .yui3-console-filters-categories,.yui3-skin-sam .yui3-console-ft .yui3-console-filters-sources{text-align:left;padding:5px 0;border:1px inset;margin:0 2px}.yui3-skin-sam .yui3-console-ft .yui3-console-filters-categories{background:#fff;border-bottom:2px ridge}.yui3-skin-sam .yui3-console-ft .yui3-console-filters-sources{background:#fff;margin-bottom:2px;border-top:0 none;border-bottom-right-radius:10px;border-bottom-left-radius:10px;-moz-border-radius-bottomright:10px;-moz-border-radius-bottomleft:10px;-webkit-border-bottom-right-radius:10px;-webkit-border-bottom-left-radius:10px}.yui3-skin-sam .yui3-console-filter-label{white-space:nowrap;margin-left:1ex}#yui3-css-stamp.skin-sam-console-filters{display:none}
diff --git a/js/yui3/assets/skins/sam/console.css b/js/yui3/assets/skins/sam/console.css
new file mode 100644
index 000000000..a471b45ec
--- /dev/null
+++ b/js/yui3/assets/skins/sam/console.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-console-separate{position:absolute;right:1em;top:1em;z-index:999}.yui3-skin-sam .yui3-console-inline{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:top}.yui3-skin-sam .yui3-console-inline .yui3-console-content{position:relative}.yui3-skin-sam .yui3-console-content{background:#777;_background:#d8d8da url(bg.png) repeat-x 0 0;font:normal 13px/1.3 Arial,sans-serif;text-align:left;border:1px solid #777;border-radius:10px;-moz-border-radius:10px;-webkit-border-radius:10px}.yui3-skin-sam .yui3-console-hd,.yui3-skin-sam .yui3-console-bd,.yui3-skin-sam .yui3-console-ft{position:relative}.yui3-skin-sam .yui3-console-hd,.yui3-skin-sam .yui3-console-ft .yui3-console-controls{text-align:right}.yui3-skin-sam .yui3-console-hd{background:#d8d8da url(bg.png) repeat-x 0 0;padding:1ex;border:1px solid transparent;_border:0 none;border-top-right-radius:10px;border-top-left-radius:10px;-moz-border-radius-topright:10px;-moz-border-radius-topleft:10px;-webkit-border-top-right-radius:10px;-webkit-border-top-left-radius:10px}.yui3-skin-sam .yui3-console-bd{background:#fff;border-top:1px solid #777;border-bottom:1px solid #777;color:#000;font-size:11px;overflow:auto;overflow-x:auto;overflow-y:scroll;_width:100%}.yui3-skin-sam .yui3-console-ft{background:#d8d8da url(bg.png) repeat-x 0 0;border:1px solid transparent;_border:0 none;border-bottom-right-radius:10px;border-bottom-left-radius:10px;-moz-border-radius-bottomright:10px;-moz-border-radius-bottomleft:10px;-webkit-border-bottom-right-radius:10px;-webkit-border-bottom-left-radius:10px}.yui3-skin-sam .yui3-console-controls{padding:4px 1ex;zoom:1}.yui3-skin-sam .yui3-console-title{color:#000;display:inline;float:left;font-weight:bold;font-size:13px;height:24px;line-height:24px;margin:0;padding-left:1ex}.yui3-skin-sam .yui3-console-pause-label{float:left}.yui3-skin-sam .yui3-console-button{line-height:1.3}.yui3-skin-sam .yui3-console-collapsed .yui3-console-bd,.yui3-skin-sam .yui3-console-collapsed .yui3-console-ft{display:none}.yui3-skin-sam .yui3-console-content.yui3-console-collapsed{-webkit-border-radius:0}.yui3-skin-sam .yui3-console-collapsed .yui3-console-hd{border-radius:10px;-moz-border-radius:10px;-webkit-border-radius:0}.yui3-skin-sam .yui3-console-entry{border-bottom:1px solid #aaa;min-height:32px;_height:32px}.yui3-skin-sam .yui3-console-entry-meta{margin:0;overflow:hidden}.yui3-skin-sam .yui3-console-entry-content{margin:0;padding:0 1ex;white-space:pre-wrap;word-wrap:break-word}.yui3-skin-sam .yui3-console-entry-meta .yui3-console-entry-src{color:#000;font-style:italic;font-weight:bold;float:right;margin:2px 5px 0 0}.yui3-skin-sam .yui3-console-entry-meta .yui3-console-entry-time{color:#777;padding-left:1ex}.yui3-skin-sam .yui3-console-entry-warn .yui3-console-entry-meta .yui3-console-entry-time{color:#555}.yui3-skin-sam .yui3-console-entry-info .yui3-console-entry-meta .yui3-console-entry-cat,.yui3-skin-sam .yui3-console-entry-warn .yui3-console-entry-meta .yui3-console-entry-cat,.yui3-skin-sam .yui3-console-entry-error .yui3-console-entry-meta .yui3-console-entry-cat{display:none}.yui3-skin-sam .yui3-console-entry-warn{background:#aee url(warn_error.png) no-repeat -15px 15px}.yui3-skin-sam .yui3-console-entry-error{background:#ffa url(warn_error.png) no-repeat 5px -24px;color:#900}.yui3-skin-sam .yui3-console-entry-warn .yui3-console-entry-content,.yui3-skin-sam .yui3-console-entry-error .yui3-console-entry-content{padding-left:24px}.yui3-skin-sam .yui3-console-entry-cat{text-transform:uppercase;padding:1px 4px;background-color:#ccc}.yui3-skin-sam .yui3-console-entry-info .yui3-console-entry-cat{background-color:#ac2}.yui3-skin-sam .yui3-console-entry-warn .yui3-console-entry-cat{background-color:#e81}.yui3-skin-sam .yui3-console-entry-error .yui3-console-entry-cat{background-color:#b00;color:#fff}.yui3-skin-sam .yui3-console-hidden{display:none}#yui3-css-stamp.skin-sam-console{display:none}
diff --git a/js/yui3/assets/skins/sam/datatable-base.css b/js/yui3/assets/skins/sam/datatable-base.css
new file mode 100644
index 000000000..8ce156884
--- /dev/null
+++ b/js/yui3/assets/skins/sam/datatable-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-table{empty-cells:show}.yui3-skin-sam .yui3-datatable-table{margin:0;padding:0;font-family:arial,sans-serif;border-collapse:separate;border-spacing:0;border:1px solid #cbcbcb}.yui3-skin-sam .yui3-datatable-caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.yui3-skin-sam .yui3-datatable-cell,.yui3-skin-sam .yui3-datatable-header{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:4px 10px 4px 10px}.yui3-skin-sam .yui3-datatable-cell:first-child,.yui3-skin-sam .yui3-datatable-first-header{border-left-width:0}.yui3-skin-sam .yui3-datatable-header{background:#fff url(../../../../assets/skins/sam/sprite.png) repeat-x 0 0;background-image:-webkit-linear-gradient(transparent 40%,rgba(0,0,0,0.21));background-image:-moz-linear-gradient(top,transparent 40%,rgba(0,0,0,0.21));background-image:-ms-linear-gradient(transparent 40%,rgba(0,0,0,0.21));background-image:-o-linear-gradient(transparent 40%,rgba(0,0,0,0.21));background-image:linear-gradient(transparent 40%,rgba(0,0,0,0.21));color:#000;font-weight:normal;text-align:left;text-shadow:0 1px 1px #fff;vertical-align:bottom;white-space:nowrap}.yui3-skin-sam .yui3-datatable-cell{background-color:transparent}.yui3-skin-sam .yui3-datatable-even .yui3-datatable-cell{background-color:#fff}.yui3-skin-sam .yui3-datatable-odd .yui3-datatable-cell{background-color:#edf5ff}#yui3-css-stamp.skin-sam-datatable-base{display:none}
diff --git a/js/yui3/assets/skins/sam/datatable-highlight.css b/js/yui3/assets/skins/sam/datatable-highlight.css
new file mode 100644
index 000000000..5a220d4d1
--- /dev/null
+++ b/js/yui3/assets/skins/sam/datatable-highlight.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable tr td{-webkit-transition:background-color .05s ease-in;-moz-transition:background-color .05s ease-in;-o-transition:background-color .05s ease-in;transition:background-color .05s ease-in}.yui3-datatable .yui3-datatable-highlight-row td{-webkit-transition:background-color .1s ease-out;-moz-transition:background-color .1s ease-out;-o-transition:background-color .1s ease-out;transition:background-color .1s ease-out}.yui3-datatable tr .yui3-datatable-highlight-col{-webkit-transition:background-color .1s ease-out;-moz-transition:background-color .1s ease-out;-o-transition:background-color .1s ease-out;transition:background-color .1s ease-out}.yui3-datatable tr .yui3-datatable-highlight-cell{-webkit-transition:background-color .1s ease-out;-moz-transition:background-color .1s ease-out;-o-transition:background-color .1s ease-out;transition:background-color .1s ease-out}.yui3-skin-sam .yui3-datatable .yui3-datatable-highlight-row td{background-color:#fef2cd}.yui3-skin-sam .yui3-datatable tr .yui3-datatable-highlight-col{background-color:#fef2cd}.yui3-skin-sam .yui3-datatable tr .yui3-datatable-highlight-cell{background-color:#fef2cd}#yui3-css-stamp.skin-sam-datatable-highlight{display:none}
diff --git a/js/yui3/assets/skins/sam/datatable-message.css b/js/yui3/assets/skins/sam/datatable-message.css
new file mode 100644
index 000000000..6fdaf71fb
--- /dev/null
+++ b/js/yui3/assets/skins/sam/datatable-message.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-message{display:none}.yui3-datatable-message-visible .yui3-datatable-message{display:block;display:table-row-group}.yui3-skin-sam .yui3-datatable-message-content{border:0 none;border-bottom:1px solid #cbcbcb;padding:4px 10px}#yui3-css-stamp.skin-sam-datatable-message{display:none}
diff --git a/js/yui3/assets/skins/sam/datatable-paginator.css b/js/yui3/assets/skins/sam/datatable-paginator.css
new file mode 100644
index 000000000..daf4c8dab
--- /dev/null
+++ b/js/yui3/assets/skins/sam/datatable-paginator.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-paginator-wrapper{border:0;padding:0}.yui3-datatable-paginator{padding:3px;white-space:nowrap}.yui3-datatable-paginator .yui3-paginator-content{position:relative}.yui3-datatable-paginator .yui3-paginator-page-select{position:absolute;right:0;top:0}.yui3-datatable-paginator .yui3-datatable-paginator-group{display:inline-block;zoom:1;*display:inline}.yui3-datatable-paginator .yui3-datatable-paginator-control{display:inline-block;zoom:1;*display:inline;margin:0 3px;padding:0 .2em;text-align:center;text-decoration:none;line-height:1.5;border:1px solid transparent;border-radius:3px;background:transparent}.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled,.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled:hover{cursor:default}.yui3-datatable-paginator .yui3-datatable-paginator-group input{width:3em}.yui3-datatable-paginator form{text-align:center;margin:0 2em}.yui3-datatable-paginator .yui3-datatable-paginator-per-page{text-align:right}.yui3-datatable-paginator{background:white url(../../../../assets/skins/sam/sprite.png) repeat-x 0 0;background-image:-webkit-linear-gradient(transparent 40%,hsla(0,0%,0%,0.21));background-image:-moz-linear-gradient(top,transparent 40%,hsla(0,0%,0%,0.21));background-image:-ms-linear-gradient(transparent 40%,hsla(0,0%,0%,0.21));background-image:-o-linear-gradient(transparent 40%,hsla(0,0%,0%,0.21));background-image:linear-gradient(transparent 40%,hsla(0,0%,0%,0.21));border-color:#cbcbcb}.yui3-datatable-paginator .yui3-datatable-paginator-control{color:#242d42}.yui3-datatable-paginator .yui3-datatable-paginator-control-first:hover,.yui3-datatable-paginator .yui3-datatable-paginator-control-last:hover,.yui3-datatable-paginator .yui3-datatable-paginator-control-prev:hover,.yui3-datatable-paginator .yui3-datatable-paginator-control-next:hover{box-shadow:0 1px 2px #292442}.yui3-datatable-paginator .yui3-datatable-paginator-control-first:active,.yui3-datatable-paginator .yui3-datatable-paginator-control-last:active,.yui3-datatable-paginator .yui3-datatable-paginator-control-prev:active,.yui3-datatable-paginator .yui3-datatable-paginator-control-next:active{box-shadow:inset 0 1px 1px #292442;background:#e0deed;background:hsla(250,30%,90%,0.3)}.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled,.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled:hover{color:#bdc7db;border-color:transparent;box-shadow:none}#yui3-css-stamp.skin-sam-datatable-paginator{display:none}
diff --git a/js/yui3/assets/skins/sam/datatable-scroll.css b/js/yui3/assets/skins/sam/datatable-scroll.css
new file mode 100644
index 000000000..9e41f42cc
--- /dev/null
+++ b/js/yui3/assets/skins/sam/datatable-scroll.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-scrollable-x{_overflow-x:hidden;_position:relative}.yui3-datatable-scrollable-y,.yui3-datatable-scrollable-y .yui3-datatable-x-scroller{_overflow-y:hidden;_position:relative}.yui3-datatable-y-scroller-container{overflow-x:hidden;position:relative}.yui3-datatable-scrollable-y .yui3-datatable-content{position:relative}.yui3-datatable-scrollable-y .yui3-datatable-table .yui3-datatable-columns{visibility:hidden}.yui3-datatable-scroll-columns{position:absolute;width:100%;z-index:2}.yui3-datatable-y-scroller,.yui3-datatable-scrollable-x .yui3-datatable-caption-table{width:100%}.yui3-datatable-x-scroller{position:relative;overflow-x:scroll;overflow-y:hidden}.yui3-datatable-scrollable-y .yui3-datatable-y-scroller{position:relative;overflow-x:hidden;overflow-y:scroll;z-index:1;-webkit-overflow-scrolling:touch}.yui3-datatable-scrollbar{position:absolute;overflow-x:hidden;overflow-y:scroll;z-index:2}.yui3-datatable-scrollbar div{position:absolute;width:1px;visibility:hidden}.yui3-skin-sam .yui3-datatable-scroll-columns{border-collapse:separate;border-spacing:0;font-family:arial,sans-serif;margin:0;padding:0;top:0;left:0}.yui3-skin-sam .yui3-datatable-scroll-columns .yui3-datatable-header{padding:0}.yui3-skin-sam .yui3-datatable-x-scroller,.yui3-skin-sam .yui3-datatable-y-scroller-container{border:1px solid #cbcbcb}.yui3-skin-sam .yui3-datatable-scrollable-x .yui3-datatable-y-scroller-container,.yui3-skin-sam .yui3-datatable-x-scroller .yui3-datatable-table,.yui3-skin-sam .yui3-datatable-y-scroller .yui3-datatable-table{border:0 none}#yui3-css-stamp.skin-sam-datatable-scroll{display:none}
diff --git a/js/yui3/assets/skins/sam/datatable-sort.css b/js/yui3/assets/skins/sam/datatable-sort.css
new file mode 100644
index 000000000..9326f550d
--- /dev/null
+++ b/js/yui3/assets/skins/sam/datatable-sort.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-sortable-column{z-index:1}.yui3-datatable-sortable-column:focus,.yui3-datatable-sortable-column:active{z-index:2}.yui3-datatable-sort-liner{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.yui3-skin-sam .yui3-datatable-sortable-column{cursor:pointer}.yui3-skin-sam .yui3-datatable-columns .yui3-datatable-sorted,.yui3-skin-sam .yui3-datatable-sortable-column:hover{*background:#c1c4c8 url(../../../../assets/skins/sam/sprite.png) repeat-x 0 -100px;background-color:#f1f2f3}.yui3-skin-sam .yui3-datatable-sort-liner{display:block;height:100%;position:relative;padding-right:15px;position:relative}.yui3-skin-sam .yui3-datatable-sort-indicator{position:absolute;right:0;bottom:.5ex;width:7px;height:10px;background:url(sort-arrow-sprite.png) no-repeat 0 0;_background:url(sort-arrow-sprite-ie.png) no-repeat 0 0;overflow:hidden}.yui3-skin-sam .yui3-datatable-sorted .yui3-datatable-sort-indicator{background-position:0 -10px}.yui3-skin-sam .yui3-datatable-sorted-desc .yui3-datatable-sort-indicator{background-position:0 -20px}.yui3-skin-sam .yui3-datatable-data .yui3-datatable-even .yui3-datatable-sorted{background-color:#edf5ff}.yui3-skin-sam .yui3-datatable-data .yui3-datatable-odd .yui3-datatable-sorted{background-color:#dbeaff}#yui3-css-stamp.skin-sam-datatable-sort{display:none}
diff --git a/js/yui3/assets/skins/sam/dial.css b/js/yui3/assets/skins/sam/dial.css
new file mode 100644
index 000000000..86b3da09c
--- /dev/null
+++ b/js/yui3/assets/skins/sam/dial.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+v\:oval,v\:shadow,v\:fill{behavior:url(#default#VML);display:inline-block;zoom:1;*display:inline}.yui3-dial{position:relative;display:-moz-inline-stack;display:inline-block;zoom:1;*display:inline}.yui3-dial-content,.yui3-dial-ring{position:relative}.yui3-dial-handle,.yui3-dial-marker,.yui3-dial-center-button,.yui3-dial-reset-string,.yui3-dial-handle-vml,.yui3-dial-marker-vml,.yui3-dial-center-button-vml,.yui3-dial-ring-vml v\:oval,.yui3-dial-center-button-vml v\:oval{position:absolute}.yui3-dial-center-button-vml v\:oval{font-size:1px;top:0;left:0}.yui3-dial-content .yui3-dial-ring .yui3-dial-hidden v\:oval,.yui3-dial-content .yui3-dial-ring .yui3-dial-hidden{opacity:0;filter:alpha(opacity=0)}.yui3-skin-sam .yui3-dial-handle{background:#6c3a3a;opacity:.3;-moz-box-shadow:1px 1px 1px rgba(0,0,0,0.9) inset;cursor:pointer;font-size:1px}.yui3-skin-sam .yui3-dial-ring{background:#bebdb7;background:-moz-linear-gradient(100% 100% 135deg,#7b7a6d,#fff);background:-webkit-gradient(linear,left top,right bottom,from(#fff),to(#7b7a6d));box-shadow:1px 1px 5px rgba(0,0,0,0.4) inset;-webkit-box-shadow:1px 1px 5px rgba(0,0,0,0.4) inset;-moz-box-shadow:1px 1px 5px rgba(0,0,0,0.4) inset}.yui3-skin-sam .yui3-dial-center-button{box-shadow:-1px -1px 2px rgba(0,0,0,0.3) inset,1px 1px 2px rgba(0,0,0,0.5);-moz-box-shadow:-1px -1px 2px rgba(0,0,0,0.3) inset,1px 1px 2px rgba(0,0,0,0.5);background:#dddbd4;background:-moz-radial-gradient(30% 30% 0deg,circle farthest-side,#fbfbf9 24%,#f2f0ea 41%,#d3d0c3 83%) repeat scroll 0 0 transparent;background:-webkit-gradient(radial,15 15,15,30 30,40,from(#fbfbf9),to(#d3d0c3),color-stop(.2,#f2f0ea));cursor:pointer;opacity:.7}.yui3-skin-sam .yui3-dial-reset-string{color:#676767;font-size:85%;text-decoration:underline}.yui3-skin-sam .yui3-dial-label{color:#808080;margin-bottom:.8em}.yui3-skin-sam .yui3-dial-value-string{margin-left:.5em;color:#000;font-size:130%}.yui3-skin-sam .yui3-dial-value{visibility:hidden;position:absolute;top:0;left:102%;width:4em}.yui3-skin-sam .yui3-dial-north-mark{position:absolute;border-left:2px solid #ccc;height:5px;width:10px;left:50%;top:-7px;font-size:1px}.yui3-skin-sam .yui3-dial-marker{background-color:#000;opacity:.2;font-size:1px}.yui3-skin-sam .yui3-dial-marker-max-min{background-color:#ab3232;opacity:.6}.yui3-skin-sam .yui3-dial-ring-vml,.yui3-skin-sam .yui3-dial-center-button-vml,.yui3-skin-sam .yui3-dial-marker v\:oval.yui3-dial-marker-max-min,.yui3-skin-sam v\:oval.yui3-dial-marker-max-min,.yui3-skin-sam .yui3-dial-marker-vml,.yui3-skin-sam .yui3-dial-handle-vml{background:0;opacity:1}#yui3-css-stamp.skin-sam-dial{display:none}
diff --git a/js/yui3/assets/skins/sam/dt-arrow-dn.png b/js/yui3/assets/skins/sam/dt-arrow-dn.png
new file mode 100644
index 000000000..9c42b8331
--- /dev/null
+++ b/js/yui3/assets/skins/sam/dt-arrow-dn.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/dt-arrow-up.png b/js/yui3/assets/skins/sam/dt-arrow-up.png
new file mode 100644
index 000000000..07e237512
--- /dev/null
+++ b/js/yui3/assets/skins/sam/dt-arrow-up.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/horizontal-menu-submenu-indicator.png b/js/yui3/assets/skins/sam/horizontal-menu-submenu-indicator.png
new file mode 100644
index 000000000..a2482ac79
--- /dev/null
+++ b/js/yui3/assets/skins/sam/horizontal-menu-submenu-indicator.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/horizontal-menu-submenu-toggle.png b/js/yui3/assets/skins/sam/horizontal-menu-submenu-toggle.png
new file mode 100644
index 000000000..4379f817d
--- /dev/null
+++ b/js/yui3/assets/skins/sam/horizontal-menu-submenu-toggle.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/node-flick.css b/js/yui3/assets/skins/sam/node-flick.css
new file mode 100644
index 000000000..7e9615540
--- /dev/null
+++ b/js/yui3/assets/skins/sam/node-flick.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-flick{position:relative;overflow:hidden}.yui3-flick-content{position:relative}#yui3-css-stamp.skin-sam-node-flick{display:none}
diff --git a/js/yui3/assets/skins/sam/node-menunav.css b/js/yui3/assets/skins/sam/node-menunav.css
new file mode 100644
index 000000000..5e78d5abf
--- /dev/null
+++ b/js/yui3/assets/skins/sam/node-menunav.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-menu .yui3-menu{position:absolute;z-index:1}.yui3-menu .yui3-shim{position:absolute;top:0;left:0;z-index:-1;opacity:0;filter:alpha(opacity=0);border:0;margin:0;padding:0;height:100%;width:100%}.yui3-menu-hidden{top:-10000px;left:-10000px;visibility:hidden}.yui3-menu li{list-style-type:none}.yui3-menu ul,.yui3-menu li{margin:0;padding:0}.yui3-menu-label,.yui3-menuitem-content{text-align:left;white-space:nowrap;display:block}.yui3-menu-horizontal li{float:left;width:auto}.yui3-menu-horizontal li li{float:none}.yui3-menu-horizontal ul{*zoom:1}.yui3-menu-horizontal ul ul{*zoom:normal}.yui3-menu-horizontal>.yui3-menu-content>ul:after{content:"";display:block;clear:both;line-height:0;font-size:0;visibility:hidden}.yui3-menu-content{*zoom:1}.yui3-menu-hidden .yui3-menu-content{*zoom:normal}.yui3-menuitem-content,.yui3-menu-label{_zoom:1}.yui3-menu-hidden .yui3-menuitem-content,.yui3-menu-hidden .yui3-menu-label{_zoom:normal}.yui3-skin-sam .yui3-menu-content,.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-content{font-size:93%;line-height:1.5;*line-height:1.45;border:solid 1px #808080;background:#fff;padding:3px 0}.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-content{font-size:100%}.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-content{line-height:2;*line-height:1.9;background:url(../../../../assets/skins/sam/sprite.png) repeat-x 0 0;padding:0}.yui3-skin-sam .yui3-menu ul,.yui3-skin-sam .yui3-menu ul ul{margin-top:3px;padding-top:3px;border-top:solid 1px #ccc}.yui3-skin-sam .yui3-menu ul.first-of-type{border:0;margin:0;padding:0}.yui3-skin-sam .yui3-menu-horizontal ul{padding:0;margin:0;border:0}.yui3-skin-sam .yui3-menu li,.yui3-skin-sam .yui3-menu .yui3-menu li{_border-bottom:solid 1px #fff}.yui3-skin-sam .yui3-menu-horizontal li{_border-bottom:0}.yui3-skin-sam .yui3-menubuttonnav li{border-right:solid 1px #ccc}.yui3-skin-sam .yui3-splitbuttonnav li{border-right:solid 1px #808080}.yui3-skin-sam .yui3-menubuttonnav li li,.yui3-skin-sam .yui3-splitbuttonnav li li{border-right:0}.yui3-skin-sam .yui3-menu-label,.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label,.yui3-skin-sam .yui3-menuitem-content,.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menuitem-content{padding:0 1em;color:#000;text-decoration:none;cursor:default;float:none;border:0;margin:0}.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label,.yui3-skin-sam .yui3-menu-horizontal .yui3-menuitem-content{padding:0 10px;border-style:solid;border-color:#808080;border-width:1px 0;margin:-1px 0;float:left;width:auto}.yui3-skin-sam .yui3-menu-label,.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label{background:url(vertical-menu-submenu-indicator.png) right center no-repeat}.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label{background:url(../../../../assets/skins/sam/sprite.png) repeat-x 0 0}.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label,.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label{background-image:none}.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label{padding-right:0}.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label em{font-style:normal;padding-right:20px;display:block;background:url(horizontal-menu-submenu-indicator.png) right center no-repeat}.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label{padding:0}.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label a{float:left;width:auto;color:#000;text-decoration:none;cursor:default;padding:0 5px 0 10px}.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label .yui3-menu-toggle{padding:0;border-left:solid 1px #ccc;width:15px;overflow:hidden;text-indent:-1000px;background:url(horizontal-menu-submenu-indicator.png) 3px center no-repeat}.yui3-skin-sam .yui3-menu-label-active,.yui3-skin-sam .yui3-menu-label-menuvisible,.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label-active,.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label-menuvisible{background-color:#b3d4ff}.yui3-skin-sam .yui3-menuitem-active .yui3-menuitem-content,.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menuitem-active .yui3-menuitem-content{background-image:none;background-color:#b3d4ff;border-left-width:0;margin-left:0}.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label-active,.yui3-skin-sam .yui3-menu-horizontal .yui3-menuitem-active .yui3-menuitem-content,.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label-menuvisible{border-color:#7d98b8;background:url(../../../../assets/skins/sam/sprite.png) repeat-x 0 -1700px}.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label-active,.yui3-skin-sam .yui3-menubuttonnav .yui3-menuitem-active .yui3-menuitem-content,.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label-menuvisible,.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-active,.yui3-skin-sam .yui3-splitbuttonnav .yui3-menuitem-active .yui3-menuitem-content,.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-menuvisible{border-left-width:1px;margin-left:-1px}.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-menuvisible{border-color:#808080;background:transparent}.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-menuvisible .yui3-menu-toggle{border-color:#7d98b8;background:url(horizontal-menu-submenu-toggle.png) left center no-repeat}#yui3-css-stamp.skin-sam-node-menunav{display:none}
diff --git a/js/yui3/assets/skins/sam/overlay.css b/js/yui3/assets/skins/sam/overlay.css
new file mode 100644
index 000000000..11d130bec
--- /dev/null
+++ b/js/yui3/assets/skins/sam/overlay.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-overlay{position:absolute}.yui3-overlay-hidden{visibility:hidden}.yui3-widget-tmp-forcesize .yui3-overlay-content{overflow:hidden!important}#yui3-css-stamp.skin-sam-overlay{display:none}
diff --git a/js/yui3/assets/skins/sam/panel.css b/js/yui3/assets/skins/sam/panel.css
new file mode 100644
index 000000000..a2cd74652
--- /dev/null
+++ b/js/yui3/assets/skins/sam/panel.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-panel{position:absolute}.yui3-panel-hidden{visibility:hidden}.yui3-widget-tmp-forcesize .yui3-panel-content{overflow:hidden!important}.yui3-panel .yui3-widget-hd{position:relative}.yui3-panel .yui3-widget-hd .yui3-widget-buttons{position:absolute;top:0;right:0}.yui3-panel .yui3-widget-ft .yui3-widget-buttons{display:inline-block;*display:inline;zoom:1}.yui3-skin-sam .yui3-panel-content{-webkit-box-shadow:0 0 5px #333;-moz-box-shadow:0 0 5px #333;box-shadow:0 0 5px #333;border:1px solid black;background:white}.yui3-skin-sam .yui3-panel .yui3-widget-hd{padding:8px 28px 8px 8px;min-height:13px;_height:13px;color:white;background-color:#3961c5;background:-moz-linear-gradient(0% 100% 90deg,#2647a0 7%,#3d67ce 50%,#426fd9 100%);background:-webkit-gradient(linear,left bottom,left top,from(#2647a0),color-stop(0.07,#2647a0),color-stop(0.5,#3d67ce),to(#426fd9))}.yui3-skin-sam .yui3-panel .yui3-widget-hd .yui3-widget-buttons{padding:8px}.yui3-skin-sam .yui3-panel .yui3-widget-bd{padding:10px}.yui3-skin-sam .yui3-panel .yui3-widget-ft{background:#edf5ff;padding:8px;text-align:right}.yui3-skin-sam .yui3-panel .yui3-widget-ft .yui3-button{margin-left:8px}.yui3-skin-sam .yui3-panel .yui3-widget-hd .yui3-button-close{background:transparent;filter:none;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;width:13px;height:13px;padding:0;overflow:hidden;vertical-align:top;*font-size:0;*line-height:0;*letter-spacing:-1000px;*color:#86a5ec;*background:url(sprite_icons.png) no-repeat 1px 1px}.yui3-skin-sam .yui3-panel .yui3-widget-hd .yui3-button-close:before{content:url(sprite_icons.png);display:inline-block;text-align:center;font-size:0;line-height:0;width:13px;margin:1px 0 0 1px}.yui3-skin-sam .yui3-panel-hidden .yui3-widget-hd .yui3-button-close{display:none}#yui3-css-stamp.skin-sam-panel{display:none}
diff --git a/js/yui3/assets/skins/sam/rail-x-lines.png b/js/yui3/assets/skins/sam/rail-x-lines.png
new file mode 100644
index 000000000..45c84288f
--- /dev/null
+++ b/js/yui3/assets/skins/sam/rail-x-lines.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/rail-x.png b/js/yui3/assets/skins/sam/rail-x.png
new file mode 100644
index 000000000..b99e1049e
--- /dev/null
+++ b/js/yui3/assets/skins/sam/rail-x.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/rail-y-lines.png b/js/yui3/assets/skins/sam/rail-y-lines.png
new file mode 100644
index 000000000..841c97088
--- /dev/null
+++ b/js/yui3/assets/skins/sam/rail-y-lines.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/rail-y.png b/js/yui3/assets/skins/sam/rail-y.png
new file mode 100644
index 000000000..2bec78ab6
--- /dev/null
+++ b/js/yui3/assets/skins/sam/rail-y.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/resize-base.css b/js/yui3/assets/skins/sam/resize-base.css
new file mode 100644
index 000000000..5a83e6e8c
--- /dev/null
+++ b/js/yui3/assets/skins/sam/resize-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-resize,.yui3-resize-wrapper{z-index:0;zoom:1}.yui3-resize-handle{position:absolute;display:block;z-index:100;zoom:1}.yui3-resize-proxy{position:absolute;border:1px dashed #000;position:absolute;z-index:10000}.yui3-resize-hidden-handles .yui3-resize-handle{opacity:0;filter:alpha(opacity=0)}.yui3-resize-handle-t,.yui3-resize-handle-b{width:100%;left:0;height:6px}.yui3-resize-handle-l,.yui3-resize-handle-r{height:100%;top:0;width:6px}.yui3-resize-handle-t{cursor:n-resize;top:0}.yui3-resize-handle-b{cursor:s-resize;bottom:0}.yui3-resize-handle-l{cursor:w-resize;left:0}.yui3-resize-handle-r{cursor:e-resize;right:0}.yui3-resize-handle-inner{position:absolute;zoom:1}@media only screen and (min-device-width :320px) and (max-device-width :480px){.yui3-resize-handle-inner:after{content:"";width:40px;height:40px;position:absolute}.yui3-resize-handle-inner-r,.yui3-resize-handle-inner-l,.yui3-resize-handle-inner-t,.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-tr,.yui3-resize-handle-inner-br,.yui3-resize-handle-inner-tl,.yui3-resize-handle-inner-bl{overflow:visible!important}.yui3-resize-handle-inner-r:after{top:-12px;right:0}.yui3-resize-handle-inner-l:after{top:-12px;left:0}.yui3-resize-handle-inner-t:after{top:0;left:-12px}.yui3-resize-handle-inner-b:after{bottom:0;left:-12px}.yui3-resize-handle-inner-tr:after{top:0;right:0}.yui3-resize-handle-inner-br:after{bottom:0;right:0}.yui3-resize-handle-inner-tl:after{top:0;left:0}.yui3-resize-handle-inner-bl:after{bottom:0;left:0}}@media only screen and (min-device-width :768px) and (max-device-width :1024px){.yui3-resize-handle-inner:after{content:"";width:30px;height:30px;position:absolute}.yui3-resize-handle-inner-r,.yui3-resize-handle-inner-l,.yui3-resize-handle-inner-t,.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-tr,.yui3-resize-handle-inner-br,.yui3-resize-handle-inner-tl,.yui3-resize-handle-inner-bl{overflow:visible!important}.yui3-resize-handle-inner-r:after{top:-6px;right:0}.yui3-resize-handle-inner-l:after{top:-6px;left:0}.yui3-resize-handle-inner-t:after{top:0;left:-6px}.yui3-resize-handle-inner-b:after{bottom:0;left:-6px}.yui3-resize-handle-inner-tr:after{top:0;right:0}.yui3-resize-handle-inner-br:after{bottom:0;right:0}.yui3-resize-handle-inner-tl:after{top:0;left:0}.yui3-resize-handle-inner-bl:after{bottom:0;left:0}}.yui3-resize-handle-inner-t,.yui3-resize-handle-inner-b{margin-left:-8px;left:50%}.yui3-resize-handle-inner-l,.yui3-resize-handle-inner-r{margin-top:-8px;top:50%}.yui3-resize-handle-inner-t{top:-4px}.yui3-resize-handle-inner-b{bottom:-4px}.yui3-resize-handle-inner-l{left:-4px}.yui3-resize-handle-inner-r{right:-4px}.yui3-resize-handle-tr,.yui3-resize-handle-br,.yui3-resize-handle-tl,.yui3-resize-handle-bl{height:15px;width:15px;z-index:200}.yui3-resize-handle-tr{cursor:ne-resize;top:0;right:0}.yui3-resize-handle-tl{cursor:nw-resize;top:0;left:0}.yui3-resize-handle-br{cursor:se-resize;bottom:0;right:0}.yui3-resize-handle-bl{cursor:sw-resize;bottom:0;left:0}.yui3-resize-handle-inner-r,.yui3-resize-handle-inner-l,.yui3-resize-handle-inner-t,.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-tr,.yui3-resize-handle-inner-br,.yui3-resize-handle-inner-tl,.yui3-resize-handle-inner-bl{background-repeat:no-repeat;background:url(arrows.png) no-repeat 0 0;display:block;height:15px;overflow:hidden;text-indent:-99999em;width:15px}.yui3-resize-handle-inner-br{background-position:-30px 0;bottom:-2px;right:-2px}.yui3-resize-handle-inner-tr{background-position:-58px 0;bottom:0;right:-2px}.yui3-resize-handle-inner-bl{background-position:-75px 0;bottom:-2px;right:-2px}.yui3-resize-handle-inner-tl{background-position:-47px 0;bottom:0;right:-2px}.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-t{background-position:-15px 0}#yui3-css-stamp.skin-sam-resize-base{display:none}
diff --git a/js/yui3/assets/skins/sam/scrollview-base.css b/js/yui3/assets/skins/sam/scrollview-base.css
new file mode 100644
index 000000000..ffa5f7d26
--- /dev/null
+++ b/js/yui3/assets/skins/sam/scrollview-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-scrollview{position:relative;overflow:hidden;-webkit-user-select:none;-moz-user-select:none}.yui3-scrollview-hidden{display:none}.yui3-scrollview-content{position:relative}.yui3-skin-sam .yui3-scrollview{-webkit-tap-highlight-color:rgba(255,255,255,0)}#yui3-css-stamp.skin-sam-scrollview-base{display:none}
diff --git a/js/yui3/assets/skins/sam/scrollview-list.css b/js/yui3/assets/skins/sam/scrollview-list.css
new file mode 100644
index 000000000..3cd9ed811
--- /dev/null
+++ b/js/yui3/assets/skins/sam/scrollview-list.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-scrollview{-webkit-tap-highlight-color:rgba(255,255,255,0)}.yui3-skin-sam .yui3-scrollview{background-color:white}.yui3-skin-sam .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-item{*zoom:1}.yui3-skin-sam .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-list{*zoom:1;list-style:none;padding:0;margin:0}.yui3-skin-sam .yui3-scrollview-vert .yui3-scrollview-content{border-top:0;background-color:white;font-family:HelveticaNeue,arial,helvetica,clean,sans-serif;color:black}.yui3-skin-sam .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-item{border-bottom:1px solid #303030;padding:15px 20px 16px;font-size:100%;font-weight:bold;background-color:white;cursor:pointer}#yui3-css-stamp.skin-sam-scrollview-list{display:none}
diff --git a/js/yui3/assets/skins/sam/scrollview-scrollbars.css b/js/yui3/assets/skins/sam/scrollview-scrollbars.css
new file mode 100644
index 000000000..63c18b87e
--- /dev/null
+++ b/js/yui3/assets/skins/sam/scrollview-scrollbars.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-scrollview-scrollbar{opacity:1;position:absolute;width:6px;height:10px}.yui3-scrollview-scrollbar{top:0;right:1px}.yui3-scrollview-scrollbar-horiz{top:auto;height:8px;width:20px;bottom:1px;left:0}.yui3-scrollview-scrollbar .yui3-scrollview-child{position:absolute;right:0;display:block;width:100%;height:4px}.yui3-scrollview-scrollbar .yui3-scrollview-first{top:0}.yui3-scrollview-scrollbar .yui3-scrollview-last{top:0}.yui3-scrollview-scrollbar .yui3-scrollview-middle{position:absolute;top:4px;height:1px}.yui3-scrollview-scrollbar-horiz .yui3-scrollview-child{display:-moz-inline-stack;display:inline-block;zoom:1;*display:inline;top:0;left:0;bottom:auto;right:auto}.yui3-scrollview-scrollbar-horiz .yui3-scrollview-first,.yui3-scrollview-scrollbar-horiz .yui3-scrollview-last{width:4px;height:6px}.yui3-scrollview-scrollbar-horiz .yui3-scrollview-middle{top:0;left:4px;width:1px;height:6px}.yui3-scrollview-scrollbar-vert-basic{height:auto}.yui3-scrollview-scrollbar-vert-basic .yui3-scrollview-child{position:static;_overflow:hidden;_line-height:4px}.yui3-scrollview-scrollbar-horiz-basic{width:auto;white-space:nowrap;line-height:6px;_overflow:hidden}.yui3-scrollview-scrollbar-horiz-basic .yui3-scrollview-child{position:static;padding:0;margin:0;top:auto;left:auto;right:auto;bottom:auto}.yui3-skin-sam .yui3-scrollview-scrollbar{-webkit-transform:translate3d(0,0,0);-moz-transform:translate(0,0)}.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-first,.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-middle,.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-last{border-radius:3px;-webkit-border-radius:3px;-moz-border-radius:3px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAABCAYAAAD9yd/wAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABJJREFUeNpiZGBgSGPAAgACDAAIkABoFyloZQAAAABJRU5ErkJggg==)}.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-first,.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-last{border-bottom-right-radius:0;border-bottom-left-radius:0;-webkit-border-bottom-right-radius:0;-webkit-border-bottom-left-radius:0;-moz-border-radius-bottomright:0;-moz-border-radius-bottomleft:0}.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-last{border-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;-webkit-border-radius:0;-webkit-border-bottom-right-radius:3px;-webkit-border-bottom-left-radius:3px;-webkit-transform:translate3d(0,0,0);-moz-border-radius:0;-moz-border-radius-bottomright:3px;-moz-border-radius-bottomleft:3px;-moz-transform:translate(0,0)}.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-middle{border-radius:0;-webkit-border-radius:0;-moz-border-radius:0;-webkit-transform:translate3d(0,0,0) scaleY(1);-webkit-transform-origin-y:0;-moz-transform:translate(0,0) scaleY(1);-moz-transform-origin:0 0}.yui3-skin-sam .yui3-scrollview-scrollbar-horiz .yui3-scrollview-first,.yui3-skin-sam .yui3-scrollview-scrollbar-horiz .yui3-scrollview-last{border-top-right-radius:0;border-bottom-left-radius:3px;-webkit-border-top-right-radius:0;-webkit-border-bottom-left-radius:3px;-moz-border-radius-topright:0;-moz-border-radius-bottomleft:3px}.yui3-skin-sam .yui3-scrollview-scrollbar-horiz .yui3-scrollview-last{border-bottom-left-radius:0;border-top-right-radius:3px;-webkit-border-bottom-left-radius:0;-webkit-border-top-right-radius:3px;-moz-border-radius-bottomleft:0;-moz-border-radius-topright:3px}.yui3-skin-sam .yui3-scrollview-scrollbar-horiz .yui3-scrollview-middle{-webkit-transform:translate3d(0,0,0) scaleX(1);-webkit-transform-origin:0 0;-moz-transform:translate(0,0) scaleX(1);-moz-transform-origin:0 0}.yui3-skin-sam .yui3-scrollview-scrollbar-vert-basic .yui3-scrollview-child,.yui3-skin-sam .yui3-scrollview-scrollbar-horiz-basic .yui3-scrollview-child{background-color:#aaa;background-image:none}#yui3-css-stamp.skin-sam-scrollview-scrollbars{display:none}
diff --git a/js/yui3/assets/skins/sam/slider-base.css b/js/yui3/assets/skins/sam/slider-base.css
new file mode 100644
index 000000000..9664d8892
--- /dev/null
+++ b/js/yui3/assets/skins/sam/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-sam .yui3-slider-x .yui3-slider-rail,.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x}.yui3-skin-sam .yui3-slider-x .yui3-slider-rail{height:26px}.yui3-skin-sam .yui3-slider-x .yui3-slider-thumb{height:26px;width:15px}.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -20px;height:20px;left:-2px;width:5px}.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -40px;height:20px;right:-2px;width:5px}.yui3-skin-sam .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-10px}.yui3-skin-sam .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-50px}.yui3-skin-sam .yui3-slider-y .yui3-slider-rail,.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y}.yui3-skin-sam .yui3-slider-y .yui3-slider-rail{width:26px}.yui3-skin-sam .yui3-slider-y .yui3-slider-thumb{width:26px;height:15px}.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-20px 0;width:20px;top:-2px;height:5px}.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-40px 0;width:20px;bottom:-2px;height:5px}.yui3-skin-sam .yui3-slider-y .yui3-slider-thumb-image{left:-10px;top:0}.yui3-skin-sam .yui3-slider-y .yui3-slider-thumb-shadow{left:-50px;opacity:.15;filter:alpha(opacity=15);top:0}#yui3-css-stamp.skin-sam-slider-base{display:none}
diff --git a/js/yui3/assets/skins/sam/sort-arrow-sprite-ie.png b/js/yui3/assets/skins/sam/sort-arrow-sprite-ie.png
new file mode 100644
index 000000000..f1f6576ab
--- /dev/null
+++ b/js/yui3/assets/skins/sam/sort-arrow-sprite-ie.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/sort-arrow-sprite.png b/js/yui3/assets/skins/sam/sort-arrow-sprite.png
new file mode 100644
index 000000000..47b8b1550
--- /dev/null
+++ b/js/yui3/assets/skins/sam/sort-arrow-sprite.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/sprite.png b/js/yui3/assets/skins/sam/sprite.png
new file mode 100644
index 000000000..321bc3f25
--- /dev/null
+++ b/js/yui3/assets/skins/sam/sprite.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/sprite_icons.gif b/js/yui3/assets/skins/sam/sprite_icons.gif
new file mode 100644
index 000000000..fa26094f6
--- /dev/null
+++ b/js/yui3/assets/skins/sam/sprite_icons.gif
Binary files differ
diff --git a/js/yui3/assets/skins/sam/sprite_icons.png b/js/yui3/assets/skins/sam/sprite_icons.png
new file mode 100644
index 000000000..89944ba0f
--- /dev/null
+++ b/js/yui3/assets/skins/sam/sprite_icons.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/tabview.css b/js/yui3/assets/skins/sam/tabview.css
new file mode 100644
index 000000000..7e45d2375
--- /dev/null
+++ b/js/yui3/assets/skins/sam/tabview.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-tab-panel{display:none}.yui3-tab-panel-selected{display:block}.yui3-tabview-list,.yui3-tab{margin:0;padding:0;list-style:none}.yui3-tabview{position:relative}.yui3-tabview,.yui3-tabview-list,.yui3-tabview-panel,.yui3-tab,.yui3-tab-panel{zoom:1}.yui3-tab{display:inline-block;*display:inline;vertical-align:bottom;cursor:pointer}.yui3-tab-label{display:block;display:inline-block;padding:6px 10px;position:relative;text-decoration:none;vertical-align:bottom}.yui3-skin-sam .yui3-tabview-list{border:solid #2647a0;border-width:0 0 5px;zoom:1}.yui3-skin-sam .yui3-tab{margin:0 .2em 0 0;padding:1px 0 0;zoom:1}.yui3-skin-sam .yui3-tab-selected{margin-bottom:-1px}.yui3-skin-sam .yui3-tab-label{background:#d8d8d8 url(../../../../assets/skins/sam/sprite.png) repeat-x;border:solid #a3a3a3;border-width:1px 1px 0 1px;color:#000;cursor:pointer;font-size:85%;padding:.3em .75em;text-decoration:none}.yui3-skin-sam .yui3-tab-label:hover,.yui3-skin-sam .yui3-tab-label:focus{background:#bfdaff url(../../../../assets/skins/sam/sprite.png) repeat-x left -1300px;outline:0}.yui3-skin-sam .yui3-tab-selected .yui3-tab-label,.yui3-skin-sam .yui3-tab-selected .yui3-tab-label:focus,.yui3-skin-sam .yui3-tab-selected .yui3-tab-label:hover{background:#2647a0 url(../../../../assets/skins/sam/sprite.png) repeat-x left -1400px;color:#fff}.yui3-skin-sam .yui3-tab-selected .yui3-tab-label{padding:.4em .75em}.yui3-skin-sam .yui3-tab-selected .yui3-tab-label{border-color:#243356}.yui3-skin-sam .yui3-tabview-panel{background:#edf5ff}.yui3-skin-sam .yui3-tabview-panel{border:1px solid #808080;border-top-color:#243356;padding:.25em .5em}#yui3-css-stamp.skin-sam-tabview{display:none}
diff --git a/js/yui3/assets/skins/sam/test-console.css b/js/yui3/assets/skins/sam/test-console.css
new file mode 100644
index 000000000..9a992bdbe
--- /dev/null
+++ b/js/yui3/assets/skins/sam/test-console.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-testconsole .yui3-console-entry{min-height:inherit;padding:5px}.yui3-testconsole .yui3-console-controls{display:none}.yui3-skin-sam .yui3-testconsole .yui3-console-content,.yui3-skin-sam .yui3-testconsole .yui3-console-bd,.yui3-skin-sam .yui3-testconsole .yui3-console-entry,.yui3-skin-sam .yui3-testconsole .yui3-console-ft,.yui3-skin-sam .yui3-testconsole .yui3-console-ft .yui3-console-filters-categories,.yui3-skin-sam .yui3-testconsole .yui3-console-ft .yui3-console-filters-sources,.yui3-skin-sam .yui3-testconsole .yui3-console-hd{background:0;border:0;-moz-border-radius:0;-webkit-border-radius:0;border-radius:0}.yui3-skin-sam .yui3-testconsole-content,.yui3-skin-sam .yui3-testconsole .yui3-console-bd{color:#333;font:13px/1.4 Helvetica,'DejaVu Sans','Bitstream Vera Sans',Arial,sans-serif}.yui3-skin-sam .yui3-testconsole-content{border:1px solid #afafaf}.yui3-skin-sam .yui3-testconsole .yui3-console-entry{border-bottom:1px solid #eaeaea;font-family:Menlo,Inconsolata,Consolas,'DejaVu Mono','Bitstream Vera Sans Mono',monospace;font-size:11px}.yui3-skin-sam .yui3-testconsole .yui3-console-ft{border-top:1px solid}.yui3-skin-sam .yui3-testconsole .yui3-console-hd{border-bottom:1px solid;*zoom:1}.yui3-skin-sam .yui3-testconsole.yui3-console-collapsed .yui3-console-hd{border:0}.yui3-skin-sam .yui3-testconsole .yui3-console-ft,.yui3-skin-sam .yui3-testconsole .yui3-console-hd{border-color:#cfcfcf}.yui3-skin-sam .yui3-testconsole .yui3-testconsole-entry-fail{background-color:#ffe0e0;border-bottom-color:#ffc5c4}.yui3-skin-sam .yui3-testconsole .yui3-testconsole-entry-pass{background-color:#ecffea;border-bottom-color:#d1ffcc}#yui3-css-stamp.skin-sam-test-console{display:none}
diff --git a/js/yui3/assets/skins/sam/thumb-x.png b/js/yui3/assets/skins/sam/thumb-x.png
new file mode 100644
index 000000000..4d4bcbd48
--- /dev/null
+++ b/js/yui3/assets/skins/sam/thumb-x.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/thumb-y.png b/js/yui3/assets/skins/sam/thumb-y.png
new file mode 100644
index 000000000..0b17a0ed2
--- /dev/null
+++ b/js/yui3/assets/skins/sam/thumb-y.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/vertical-menu-submenu-indicator.png b/js/yui3/assets/skins/sam/vertical-menu-submenu-indicator.png
new file mode 100644
index 000000000..cfc46b8ac
--- /dev/null
+++ b/js/yui3/assets/skins/sam/vertical-menu-submenu-indicator.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/warn_error.png b/js/yui3/assets/skins/sam/warn_error.png
new file mode 100644
index 000000000..8b0db799e
--- /dev/null
+++ b/js/yui3/assets/skins/sam/warn_error.png
Binary files differ
diff --git a/js/yui3/assets/skins/sam/widget-base.css b/js/yui3/assets/skins/sam/widget-base.css
new file mode 100644
index 000000000..388fd00aa
--- /dev/null
+++ b/js/yui3/assets/skins/sam/widget-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-widget-hidden{display:none}.yui3-widget-content{overflow:hidden}.yui3-widget-content-expanded{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;height:100%}.yui3-widget-tmp-forcesize{overflow:hidden!important}#yui3-css-stamp.skin-sam-widget-base{display:none}
diff --git a/js/yui3/assets/skins/sam/widget-modality.css b/js/yui3/assets/skins/sam/widget-modality.css
new file mode 100644
index 000000000..28e44a10e
--- /dev/null
+++ b/js/yui3/assets/skins/sam/widget-modality.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-widget-mask{background-color:black;zoom:1;-ms-filter:"alpha(opacity=40)";filter:alpha(opacity=40);opacity:.4}#yui3-css-stamp.skin-sam-widget-modality{display:none}
diff --git a/js/yui3/assets/skins/sam/widget-stack.css b/js/yui3/assets/skins/sam/widget-stack.css
new file mode 100644
index 000000000..989ac5bcb
--- /dev/null
+++ b/js/yui3/assets/skins/sam/widget-stack.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-widget-stacked .yui3-widget-shim{opacity:0;filter:alpha(opacity=0);position:absolute;border:0;top:0;left:0;padding:0;margin:0;z-index:-1;width:100%;height:100%;_width:0;_height:0}#yui3-css-stamp.skin-sam-widget-stack{display:none}
diff --git a/js/yui3/async-queue/async-queue-min.js b/js/yui3/async-queue/async-queue-min.js
new file mode 100644
index 000000000..76dba50ac
--- /dev/null
+++ b/js/yui3/async-queue/async-queue-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("async-queue",function(e,t){e.AsyncQueue=function(){this._init(),this.add.apply(this,arguments)};var n=e.AsyncQueue,r="execute",i="shift",s="promote",o="remove",u=e.Lang.isObject,a=e.Lang.isFunction;n.defaults=e.mix({autoContinue:!0,iterations:1,timeout:10,until:function(){return this.iterations|=0,this.iterations<=0}},e.config.queueDefaults||{}),e.extend(n,e.EventTarget,{_running:!1,_init:function(){e.EventTarget.call(this,{prefix:"queue",emitFacade:!0}),this._q=[],this.defaults={},this._initEvents()},_initEvents:function(){this.publish({execute:{defaultFn:this._defExecFn,emitFacade:!0},shift:{defaultFn:this._defShiftFn,emitFacade:!0},add:{defaultFn:this._defAddFn,emitFacade:!0},promote:{defaultFn:this._defPromoteFn,emitFacade:!0},remove:{defaultFn:this._defRemoveFn,emitFacade:!0}})},next:function(){var e;while(this._q.length){e=this._q[0]=this._prepare(this._q[0]);if(!e||!e.until())break;this.fire(i,{callback:e}),e=null}return e||null},_defShiftFn:function(e){this.indexOf(e.callback)===0&&this._q.shift()},_prepare:function(t){if(a(t)&&t._prepared)return t;var r=e.merge(n.defaults,{context:this,args:[],_prepared:!0},this.defaults,a(t)?{fn:t}:t),i=e.bind(function(){i._running||i.iterations--,a(i.fn)&&i.fn.apply(i.context||e,e.Array(i.args))},this);return e.mix(i,r)},run:function(){var e,t=!0;if(this._executing)return this._running=!0,this;for(e=this.next();e&&!this.isRunning();e=this.next()){t=e.timeout<0?this._execute(e):this._schedule(e);if(!t)break}return e||this.fire("complete"),this},_execute:function(e){this._running=e._running=!0,this._executing=e,e.iterations--,this.fire(r,{callback:e});var t=this._running&&e.autoContinue;return this._running=e._running=!1,this._executing=!1,t},_schedule:function(t){return this._running=e.later(t.timeout,this,function(){this._execute(t)&&this.run()}),!1},isRunning:function(){return!!this._running},_defExecFn:function(e){e.callback()},add:function(){return this.fire("add",{callbacks:e.Array(arguments,0,!0)}),this},_defAddFn:function(t){var n=this._q,r=[];e.Array.each(t.callbacks,function(e){u(e)&&(n.push(e),r.push(e))}),t.added=r},pause:function(){return this._running&&u(this._running)&&this._running.cancel(),this._running=!1,this},stop:function(){return this._q=[],this._running&&u(this._running)&&(this._running.cancel(),this._running=!1),this._executing||this.run(),this},indexOf:function(e){var t=0,n=this._q.length,r;for(;t<n;++t){r=this._q[t];if(r===e||r.id===e)return t}return-1},getCallback:function(e){var t=this.indexOf(e);return t>-1?this._q[t]:null},promote:function(e){var t={callback:e},n;return this.isRunning()?n=this.after(i,function(){this.fire(s,t),n.detach()},this):this.fire(s,t),this},_defPromoteFn:function(e){var t=this.indexOf(e.callback),n=t>-1?this._q.splice(t,1)[0]:null;e.promoted=n,n&&this._q.unshift(n)},remove:function(e){var t={callback:e},n;return this.isRunning()?n=this.after(i,function(){this.fire(o,t),n.detach()},this):this.fire(o,t),this},_defRemoveFn:function(e){var t=this.indexOf(e.callback);e.removed=t>-1?this._q.splice(t,1)[0]:null},size:function(){return this.isRunning()||this.next(),this._q.length}})},"3.17.2",{requires:["event-custom"]});
diff --git a/js/yui3/attribute-base/attribute-base-min.js b/js/yui3/attribute-base/attribute-base-min.js
new file mode 100644
index 000000000..32eae7235
--- /dev/null
+++ b/js/yui3/attribute-base/attribute-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("attribute-base",function(e,t){function n(){e.AttributeCore.apply(this,arguments),e.AttributeObservable.apply(this,arguments),e.AttributeExtras.apply(this,arguments)}e.mix(n,e.AttributeCore,!1,null,1),e.mix(n,e.AttributeExtras,!1,null,1),e.mix(n,e.AttributeObservable,!0,null,1),n.INVALID_VALUE=e.AttributeCore.INVALID_VALUE,n._ATTR_CFG=e.AttributeCore._ATTR_CFG.concat(e.AttributeObservable._ATTR_CFG),n.protectAttrs=e.AttributeCore.protectAttrs,e.Attribute=n},"3.17.2",{requires:["attribute-core","attribute-observable","attribute-extras"]});
diff --git a/js/yui3/attribute-complex/attribute-complex-min.js b/js/yui3/attribute-complex/attribute-complex-min.js
new file mode 100644
index 000000000..5d2eb598f
--- /dev/null
+++ b/js/yui3/attribute-complex/attribute-complex-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("attribute-complex",function(e,t){var n=e.Attribute;n.Complex=function(){},n.Complex.prototype={_normAttrVals:n.prototype._normAttrVals,_getAttrInitVal:n.prototype._getAttrInitVal},e.AttributeComplex=n.Complex},"3.17.2",{requires:["attribute-base"]});
diff --git a/js/yui3/attribute-core/attribute-core-min.js b/js/yui3/attribute-core/attribute-core-min.js
new file mode 100644
index 000000000..38a64a98e
--- /dev/null
+++ b/js/yui3/attribute-core/attribute-core-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("attribute-core",function(e,t){function b(e,t,n){this._yuievt=null,this._initAttrHost(e,t,n)}e.State=function(){this.data={}},e.State.prototype={add:function(e,t,n){var r=this.data[e];r||(r=this.data[e]={}),r[t]=n},addAll:function(e,t){var n=this.data[e],r;n||(n=this.data[e]={});for(r in t)t.hasOwnProperty(r)&&(n[r]=t[r])},remove:function(e,t){var n=this.data[e];n&&delete n[t]},removeAll:function(t,n){var r;n?e.each(n,function(e,n){this.remove(t,typeof n=="string"?n:e)},this):(r=this.data,t in r&&delete r[t])},get:function(e,t){var n=this.data[e];if(n)return n[t]},getAll:function(e,t){var n=this.data[e],r,i;if(t)i=n;else if(n){i={};for(r in n)n.hasOwnProperty(r)&&(i[r]=n[r])}return i}};var n=e.Object,r=e.Lang,i=".",s="getter",o="setter",u="readOnly",a="writeOnce",f="initOnly",l="validator",c="value",h="valueFn",p="lazyAdd",d="added",v="_bypassProxy",m="initValue",g="lazy",y;b.INVALID_VALUE={},y=b.INVALID_VALUE,b._ATTR_CFG=[o,s,l,c,h,a,u,p,v],b.protectAttrs=function(t){if(t){t=e.merge(t);for(var n in t)t.hasOwnProperty(n)&&(t[n]=e.merge(t[n]))}return t},b.prototype={_initAttrHost:function(t,n,r){this._state=new e.State,this._initAttrs(t,n,r)},addAttr:function(e,t,n){var r=this,i=r._state,s=i.data,o,u,a;t=t||{},p in t&&(n=t[p]),u=i.get(e,d);if(n&&!u)i.data[e]={lazy:t,added:!0};else if(!u||t.isLazyAdd)a=c in t,a&&(o=t.value,t.value=undefined),t.added=!0,t.initializing=!0,s[e]=t,a&&r.set(e,o),t.initializing=!1;return r},attrAdded:function(e){return!!this._state.get(e,d)},get:function(e){return this._getAttr(e)},_isLazyAttr:function(e){return this._state.get(e,g)},_addLazyAttr:function(e,t){var n=this._state;t=t||n.get(e,g),t&&(n.data[e].lazy=undefined,t.isLazyAdd=!0,this.addAttr(e,t))},set:function(e,t,n){return this._setAttr(e,t,n)},_set:function(e,t,n){return this._setAttr(e,t,n,!0)},_setAttr:function(t,r,s,o){var u=!0,a=this._state,l=this._stateProxy,c=this._tCfgs,h,p,d,v,m,g,y;return t.indexOf(i)!==-1&&(d=t,v=t.split(i),t=v.shift()),c&&c[t]&&this._addOutOfOrder(t,c[t]),h=a.data[t]||{},h.lazy&&(h=h.lazy,this._addLazyAttr(t,h)),p=h.value===undefined,l&&t in l&&!h._bypassProxy&&(p=!1),g=h.writeOnce,y=h.initializing,!p&&!o&&(g&&(u=!1),h.readOnly&&(u=!1)),!y&&!o&&g===f&&(u=!1),u&&(p||(m=this.get(t)),v&&(r=n.setValue(e.clone(m),v,r),r===undefined&&(u=!1)),u&&(!this._fireAttrChange||y?this._setAttrVal(t,d,m,r,s,h):this._fireAttrChange(t,d,m,r,s,h))),this},_addOutOfOrder:function(e,t){var n={};n[e]=t,delete this._tCfgs[e],this._addAttrs(n,this._tVals)},_getAttr:function(e){var t=e,r=this._tCfgs,s,o,u,a;return e.indexOf(i)!==-1&&(s=e.split(i),e=s.shift()),r&&r[e]&&this._addOutOfOrder(e,r[e]),a=this._state.data[e]||{},a.lazy&&(a=a.lazy,this._addLazyAttr(e,a)),u=this._getStateVal(e,a),o=a.getter,o&&!o.call&&(o=this[o]),u=o?o.call(this,u,t):u,u=s?n.getValue(u,s):u,u},_getStateVal:function(e,t){var n=this._stateProxy;return t||(t=this._state.getAll(e)||{}),n&&e in n&&!t._bypassProxy?n[e]:t.value},_setStateVal:function(e,t){var n=this._stateProxy;n&&e in n&&!this._state.get(e,v)?n[e]=t:this._state.add(e,c,t)},_setAttrVal:function(e,t,n,i,s,o){var u=this,a=!0,f=o||this._state.data[e]||{},l=f.validator,c=f.setter,h=f.initializing,p=this._getStateVal(e,f),d=t||e,v,g;return l&&(l.call||(l=this[l]),l&&(g=l.call(u,i,d,s),!g&&h&&(i=f.defaultValue,g=!0))),!l||g?(c&&(c.call||(c=this[c]),c&&(v=c.call(u,i,d,s),v===y?h?i=f.defaultValue:a=!1:v!==undefined&&(i=v))),a&&(!t&&i===p&&!r.isObject(i)?a=!1:(m in f||(f.initValue=i),u._setStateVal(e,i)))):a=!1,a},setAttrs:function(e,t){return this._setAttrs(e,t)},_setAttrs:function(e,t){var n;for(n in e)e.hasOwnProperty(n)&&this.set(n,e[n],t);return this},getAttrs:function(e){return this._getAttrs(e)},_getAttrs:function(e){var t={},r,i,s,o=e===!0;if(!e||o)e=n.keys(this._state.data);for(i=0,s=e.length;i<s;i++){r=e[i];if(!o||this._getStateVal(r)!=this._state.get(r,m))t[r]=this.get(r)}return t},addAttrs:function(e,t,n){return e&&(this._tCfgs=e,this._tVals=t?this._normAttrVals(t):null,this._addAttrs(e,this._tVals,n),this._tCfgs=this._tVals=null),this},_addAttrs:function(e,t,n){var r=this._tCfgs,i=this._tVals,s,o,u;for(s in e)e.hasOwnProperty(s)&&(o=e[s],o.defaultValue=o.value,u=this._getAttrInitVal(s,o,i),u!==undefined&&(o.value=u),r[s]&&(r[s]=undefined),this.addAttr(s,o,n))},_protectAttrs:b.protectAttrs,_normAttrVals:function(e){var t,n,r,s,o,u;if(!e)return null;t={};for(u in e)e.hasOwnProperty(u)&&(u.indexOf(i)!==-1?(r=u.split(i),s=r.shift(),n=n||{},o=n[s]=n[s]||[],o[o.length]={path:r,value:e[u]}):t[u]=e[u]);return{simple:t,complex:n}},_getAttrInitVal:function(e,t,r){var i=t.value,s=t.valueFn,o,u=!1,a=t.readOnly,f,l,c,h,p,d,v;!a&&r&&(f=r.simple,f&&f.hasOwnProperty(e)&&(i=f[e],u=!0)),s&&!u&&(s.call||(s=this[s]),s&&(o=s.call(this,e),i=o));if(!a&&r){l=r.complex;if(l&&l.hasOwnProperty(e)&&i!==undefined&&i!==null){v=l[e];for(c=0,h=v.length;c<h;++c)p=v[c].path,d=v[c].value,n.setValue(i,p,d)}}return i},_initAttrs:function(t,n,r){t=t||this.constructor.ATTRS;var i=e.Base,s=e.BaseCore,o=i&&e.instanceOf(this,i),u=!o&&s&&e.instanceOf(this,s);t&&!o&&!u&&this.addAttrs(e.AttributeCore.protectAttrs(t),n,r)}},e.AttributeCore=b},"3.17.2",{requires:["oop"]});
diff --git a/js/yui3/attribute-extras/attribute-extras-min.js b/js/yui3/attribute-extras/attribute-extras-min.js
new file mode 100644
index 000000000..1de3af954
--- /dev/null
+++ b/js/yui3/attribute-extras/attribute-extras-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("attribute-extras",function(e,t){function o(){}var n="broadcast",r="published",i="initValue",s={readOnly:1,writeOnce:1,getter:1,broadcast:1};o.prototype={modifyAttr:function(e,t){var i=this,o,u;if(i.attrAdded(e)){i._isLazyAttr(e)&&i._addLazyAttr(e),u=i._state;for(o in t)s[o]&&t.hasOwnProperty(o)&&(u.add(e,o,t[o]),o===n&&u.remove(e,r))}},removeAttr:function(e){this._state.removeAll(e)},reset:function(t){var n=this;return t?(n._isLazyAttr(t)&&n._addLazyAttr(t),n.set(t,n._state.get(t,i))):e.Object.each(n._state.data,function(e,t){n.reset(t)}),n},_getAttrCfg:function(t){var n,r=this._state;return t?n=r.getAll(t)||{}:(n={},e.each(r.data,function(e,t){n[t]=r.getAll(t)})),n}},e.AttributeExtras=o},"3.17.2",{requires:["oop"]});
diff --git a/js/yui3/attribute-observable/attribute-observable-min.js b/js/yui3/attribute-observable/attribute-observable-min.js
new file mode 100644
index 000000000..1ae4d0107
--- /dev/null
+++ b/js/yui3/attribute-observable/attribute-observable-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("attribute-observable",function(e,t){function s(){this._ATTR_E_FACADE={},n.call(this,{emitFacade:!0})}var n=e.EventTarget,r="Change",i="broadcast";s._ATTR_CFG=[i],s.prototype={set:function(e,t,n){return this._setAttr(e,t,n)},_set:function(e,t,n){return this._setAttr(e,t,n,!0)},setAttrs:function(e,t){return this._setAttrs(e,t)},_setAttrs:function(e,t){var n;for(n in e)e.hasOwnProperty(n)&&this.set(n,e[n],t);return this},_fireAttrChange:function(t,n,i,s,o,u){var a=this,f=this._getFullType(t+r),l=a._state,c,h,p;u||(u=l.data[t]||{}),u.published||(p=a._publish(f),p.emitFacade=!0,p.defaultTargetOnly=!0,p.defaultFn=a._defAttrChangeFn,h=u.broadcast,h!==undefined&&(p.broadcast=h),u.published=!0),o?(c=e.merge(o),c._attrOpts=o):c=a._ATTR_E_FACADE,c.attrName=t,c.subAttrName=n,c.prevVal=i,c.newVal=s,a._hasPotentialSubscribers(f)?a.fire(f,c):this._setAttrVal(t,n,i,s,o,u)},_defAttrChangeFn:function(e,t){var n=e._attrOpts;n&&delete e._attrOpts,this._setAttrVal(e.attrName,e.subAttrName,e.prevVal,e.newVal,n)?t||(e.newVal=this.get(e.attrName)):t||e.stopImmediatePropagation()}},e.mix(s,n,!1,null,1),e.AttributeObservable=s,e.AttributeEvents=s},"3.17.2",{requires:["event-custom"]});
diff --git a/js/yui3/autocomplete-base/autocomplete-base-min.js b/js/yui3/autocomplete-base/autocomplete-base-min.js
new file mode 100644
index 000000000..996266c5b
--- /dev/null
+++ b/js/yui3/autocomplete-base/autocomplete-base-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("autocomplete-base",function(e,t){function T(){}var n=e.Escape,r=e.Lang,i=e.Array,s=e.Object,o=r.isFunction,u=r.isString,a=r.trim,f=e.Attribute.INVALID_VALUE,l="_functionValidator",c="_sourceSuccess",h="allowBrowserAutocomplete",p="inputNode",d="query",v="queryDelimiter",m="requestTemplate",g="results",y="resultListLocator",b="value",w="valueChange",E="clear",S=d,x=g;T.prototype={initializer:function(){e.before(this._bindUIACBase,this,"bindUI"),e.before(this._syncUIACBase,this,"syncUI"),this.publish(E,{defaultFn:this._defClearFn}),this.publish(S,{defaultFn:this._defQueryFn}),this.publish(x,{defaultFn:this._defResultsFn})},destructor:function(){this._acBaseEvents&&this._acBaseEvents.detach(),delete this._acBaseEvents,delete this._cache,delete this._inputNode,delete this._rawSource},clearCache:function(){return this._cache&&(this._cache={}),this},sendRequest:function(t,n){var r,i=this.get("source");return t||t===""?this._set(d,t):t=this.get(d)||"",i&&(n||(n=this.get(m)),r=n?n.call(this,t):t,i.sendRequest({query:t,request:r,callback:{success:e.bind(this._onResponse,this,t)}})),this},_bindUIACBase:function(){var t=this.get(p),n=t&&t.tokenInput;n&&(t=n.get(p),this._set("tokenInput",n));if(!t){e.error("No inputNode specified.");return}this._inputNode=t,this._acBaseEvents=new e.EventHandle([t.on(w,this._onInputValueChange,this),t.on("blur",this._onInputBlur,this),this.after(h+"Change",this._syncBrowserAutocomplete),this.after("sourceTypeChange",this._afterSourceTypeChange),this.after(w,this._afterValueChange)])},_syncUIACBase:function(){this._syncBrowserAutocomplete(),this.set(b,this.get(p).get(b))},_createArraySource:function(e){var t=this;return{type:"array",sendRequest:function(n){t[c](e.concat(),n)}}},_createFunctionSource:function(e){var t=this;return{type:"function",sendRequest:function(n){function i(e){t[c](e||[],n)}var r;(r=e(n.query,i))&&i(r)}}},_createObjectSource:function(e){var t=this;return{type:"object",sendRequest:function(n){var r=n.query;t[c](s.owns(e,r)?e[r]:[],n)}}},_functionValidator:function(e){return e===null||o(e)},_getObjectValue:function(e,t){if(!e)return;for(var n=0,r=t.length;e&&n<r;n++)e=e[t[n]];return e},_parseResponse:function(e,t,r){var i={data:r,query:e,results:[]},s=this.get(y),o=[],u=t&&t.results,a,f,l,c,h,p,d,v,m,g,b;u&&s&&(u=s.call(this,u));if(u&&u.length){a=this.get("resultFilters"),b=this.get("resultTextLocator");for(p=0,d=u.length;p<d;++p)m=u[p],g=b?b.call(this,m):m.toString(),o.push({display:n.html(g),raw:m,text:g});for(p=0,d=a.length;p<d;++p){o=a[p].call(this,e,o.concat());if(!o)return;if(!o.length)break}if(o.length){l=this.get("resultFormatter"),h=this.get("resultHighlighter"),v=this.get("maxResults"),v&&v>0&&o.length>v&&(o.length=v);if(h){c=h.call(this,e,o.concat());if(!c)return;for(p=0,d=c.length;p<d;++p)m=o[p],m.highlighted=c[p],m.display=m.highlighted}if(l){f=l.call(this,e,o.concat());if(!f)return;for(p=0,d=f.length;p<d;++p)o[p].display=f[p]}}}i.results=o,this.fire(x,i)},_parseValue:function(e){var t=this.get(v);return t&&(e=e.split(t),e=e[e.length-1]),r.trimLeft(e)},_setEnableCache:function(e){this._cache=e?{}:null},_setLocator:function(e){if(this[l](e))return e;var t=this;return e=e.toString().split("."),function(n){return n&&t._getObjectValue(n,e)}},_setRequestTemplate:function(e){return this[l](e)?e:(e=e.toString(),function(t){return r.sub(e,{query:encodeURIComponent(t)})})},_setResultFilters:function(t){var n,s;return t===null?[]:(n=e.AutoCompleteFilters,s=function(e){return o(e)?e:u(e)&&n&&o(n[e])?n[e]:!1},r.isArray(t)?(t=i.map(t,s),i.every(t,function(e){return!!e})?t:f):(t=s(t),t?[t]:f))},_setResultHighlighter:function(t){var n;return this[l](t)?t:(n=e.AutoCompleteHighlighters,u(t)&&n&&o(n[t])?n[t]:f)},_setSource:function(t){var n=this.get("sourceType")||r.type(t),i;return t&&o(t.sendRequest)||t===null||n==="datasource"?(this._rawSource=t,t):(i=T.SOURCE_TYPES[n])?(this._rawSource=t,r.isString(i)?this[i](t):i(t)):(e.error("Unsupported source type '"+n+"'. Maybe autocomplete-sources isn't loaded?"),f)},_sourceSuccess:function(e,t){t.callback.success({data:e,response:{results:e}})},_syncBrowserAutocomplete:function(){var e=this.get(p);e.get("nodeName").toLowerCase()==="input"&&e.setAttribute("autocomplete",this.get(h)?"on":"off")},_updateValue:function(e){var t=this.get(v),n,s,o;e=r.trimLeft(e),t&&(n=a(t),o=i.map(a(this.get(b)).split(t),a),s=o.length,s>1&&(o[s-1]=e,e=o.join(n+" ")),e=e+n+" "),this.set(b,e)},_afterSourceTypeChange:function(e){this._rawSource&&this.set("source",this._rawSource)},_afterValueChange:function(e){var t=e.newVal,n=this,r=e.src===T.UI_SRC,i,s,o,u;r||n._inputNode.set(b,t),o=n.get("minQueryLength"),u=n._parseValue(t)||"",o>=0&&u.length>=o?r?(i=n.get("queryDelay"),s=function(){n.fire(S,{inputValue:t,query:u,src:e.src})},i?(clearTimeout(n._delay),n._delay=setTimeout(s,i)):s()):n._set(d,u):(clearTimeout(n._delay),n.fire(E,{prevVal:e.prevVal?n._parseValue(e.prevVal):null,src:e.src}))},_onInputBlur:function(e){var t=this.get(v),n,i,s;if(t&&!this.get("allowTrailingDelimiter")){t=r.trimRight(t),s=i=this._inputNode.get(b);if(t)while((i=r.trimRight(i))&&(n=i.length-t.length)&&i.lastIndexOf(t)===n)i=i.substring(0,n);else i=r.trimRight(i);i!==s&&this.set(b,i)}},_onInputValueChange:function(e){var t=e.newVal;t!==this.get(b)&&this.set(b,t,{src:T.UI_SRC})},_onResponse:function(e,t){e===(this.get(d)||"")&&this._parseResponse(e||"",t.response,t.data)},_defClearFn:function(){this._set(d,null),this._set(g,[])},_defQueryFn:function(e){this.sendRequest(e.query)},_defResultsFn:function(e){this._set(g,e[g])}},T.ATTRS={allowBrowserAutocomplete:{value:!1},allowTrailingDelimiter:{value:!1},enableCache:{lazyAdd:!1,setter:"_setEnableCache",value:!0},inputNode:{setter:e.one,writeOnce:"initOnly"},maxResults:{value:0},minQueryLength:{value:1},query:{readOnly:!0,value:null},queryDelay:{value:100},queryDelimiter:{value:null},requestTemplate:{setter:"_setRequestTemplate",value:null},resultFilters:{setter:"_setResultFilters",value:[]},resultFormatter
+:{validator:l,value:null},resultHighlighter:{setter:"_setResultHighlighter",value:null},resultListLocator:{setter:"_setLocator",value:null},results:{readOnly:!0,value:[]},resultTextLocator:{setter:"_setLocator",value:null},source:{setter:"_setSource",value:null},sourceType:{value:null},tokenInput:{readOnly:!0},value:{value:""}},T._buildCfg={aggregates:["SOURCE_TYPES"],statics:["UI_SRC"]},T.SOURCE_TYPES={array:"_createArraySource","function":"_createFunctionSource",object:"_createObjectSource"},T.UI_SRC=e.Widget&&e.Widget.UI_SRC||"ui",e.AutoCompleteBase=T},"3.17.2",{optional:["autocomplete-sources"],requires:["array-extras","base-build","escape","event-valuechange","node-base"]});
diff --git a/js/yui3/autocomplete-filters-accentfold/autocomplete-filters-accentfold-min.js b/js/yui3/autocomplete-filters-accentfold/autocomplete-filters-accentfold-min.js
new file mode 100644
index 000000000..6ad5de2d5
--- /dev/null
+++ b/js/yui3/autocomplete-filters-accentfold/autocomplete-filters-accentfold-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("autocomplete-filters-accentfold",function(e,t){var n=e.Text.AccentFold,r=e.Text.WordBreak,i=e.Array,s=e.Object;e.mix(e.namespace("AutoCompleteFilters"),{charMatchFold:function(e,t){if(!e)return t;var r=i.unique(n.fold(e).split(""));return i.filter(t,function(e){var t=n.fold(e.text);return i.every(r,function(e){return t.indexOf(e)!==-1})})},phraseMatchFold:function(e,t){return e?(e=n.fold(e),i.filter(t,function(t){return n.fold(t.text).indexOf(e)!==-1})):t},startsWithFold:function(e,t){return e?(e=n.fold(e),i.filter(t,function(t){return n.fold(t.text).indexOf(e)===0})):t},subWordMatchFold:function(e,t){if(!e)return t;var s=r.getUniqueWords(n.fold(e));return i.filter(t,function(e){var t=n.fold(e.text);return i.every(s,function(e){return t.indexOf(e)!==-1})})},wordMatchFold:function(e,t){if(!e)return t;var o=r.getUniqueWords(n.fold(e));return i.filter(t,function(e){var t=i.hash(r.getUniqueWords(n.fold(e.text)));return i.every(o,function(e){return s.owns(t,e)})})}})},"3.17.2",{requires:["array-extras","text-accentfold","text-wordbreak"]});
diff --git a/js/yui3/autocomplete-filters/autocomplete-filters-min.js b/js/yui3/autocomplete-filters/autocomplete-filters-min.js
new file mode 100644
index 000000000..5a9cc94dd
--- /dev/null
+++ b/js/yui3/autocomplete-filters/autocomplete-filters-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("autocomplete-filters",function(e,t){var n=e.Array,r=e.Object,i=e.Text.WordBreak,s=e.mix(e.namespace("AutoCompleteFilters"),{charMatch:function(e,t,r){if(!e)return t;var i=n.unique((r?e:e.toLowerCase()).split(""));return n.filter(t,function(e){return e=e.text,r||(e=e.toLowerCase()),n.every(i,function(t){return e.indexOf(t)!==-1})})},charMatchCase:function(e,t){return s.charMatch(e,t,!0)},phraseMatch:function(e,t,r){return e?(r||(e=e.toLowerCase()),n.filter(t,function(t){return(r?t.text:t.text.toLowerCase()).indexOf(e)!==-1})):t},phraseMatchCase:function(e,t){return s.phraseMatch(e,t,!0)},startsWith:function(e,t,r){return e?(r||(e=e.toLowerCase()),n.filter(t,function(t){return(r?t.text:t.text.toLowerCase()).indexOf(e)===0})):t},startsWithCase:function(e,t){return s.startsWith(e,t,!0)},subWordMatch:function(e,t,r){if(!e)return t;var s=i.getUniqueWords(e,{ignoreCase:!r});return n.filter(t,function(e){var t=r?e.text:e.text.toLowerCase();return n.every(s,function(e){return t.indexOf(e)!==-1})})},subWordMatchCase:function(e,t){return s.subWordMatch(e,t,!0)},wordMatch:function(e,t,s){if(!e)return t;var o={ignoreCase:!s},u=i.getUniqueWords(e,o);return n.filter(t,function(e){var t=n.hash(i.getUniqueWords(e.text,o));return n.every(u,function(e){return r.owns(t,e)})})},wordMatchCase:function(e,t){return s.wordMatch(e,t,!0)}})},"3.17.2",{requires:["array-extras","text-wordbreak"]});
diff --git a/js/yui3/autocomplete-highlighters-accentfold/autocomplete-highlighters-accentfold-min.js b/js/yui3/autocomplete-highlighters-accentfold/autocomplete-highlighters-accentfold-min.js
new file mode 100644
index 000000000..932ed27a1
--- /dev/null
+++ b/js/yui3/autocomplete-highlighters-accentfold/autocomplete-highlighters-accentfold-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("autocomplete-highlighters-accentfold",function(e,t){var n=e.Highlight,r=e.Array;e.mix(e.namespace("AutoCompleteHighlighters"),{charMatchFold:function(e,t){var i=r.unique(e.split(""));return r.map(t,function(e){return n.allFold(e.text,i)})},phraseMatchFold:function(e,t){return r.map(t,function(t){return n.allFold(t.text,[e])})},startsWithFold:function(e,t){return r.map(t,function(t){return n.allFold(t.text,[e],{startsWith:!0})})},subWordMatchFold:function(t,i){var s=e.Text.WordBreak.getUniqueWords(t);return r.map(i,function(e){return n.allFold(e.text,s)})},wordMatchFold:function(e,t){return r.map(t,function(t){return n.wordsFold(t.text,e)})}})},"3.17.2",{requires:["array-extras","highlight-accentfold"]});
diff --git a/js/yui3/autocomplete-highlighters/autocomplete-highlighters-min.js b/js/yui3/autocomplete-highlighters/autocomplete-highlighters-min.js
new file mode 100644
index 000000000..358290101
--- /dev/null
+++ b/js/yui3/autocomplete-highlighters/autocomplete-highlighters-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("autocomplete-highlighters",function(e,t){var n=e.Array,r=e.Highlight,i=e.mix(e.namespace("AutoCompleteHighlighters"),{charMatch:function(e,t,i){var s=n.unique((i?e:e.toLowerCase()).split(""));return n.map(t,function(e){return r.all(e.text,s,{caseSensitive:i})})},charMatchCase:function(e,t){return i.charMatch(e,t,!0)},phraseMatch:function(e,t,i){return n.map(t,function(t){return r.all(t.text,[e],{caseSensitive:i})})},phraseMatchCase:function(e,t){return i.phraseMatch(e,t,!0)},startsWith:function(e,t,i){return n.map(t,function(t){return r.all(t.text,[e],{caseSensitive:i,startsWith:!0})})},startsWithCase:function(e,t){return i.startsWith(e,t,!0)},subWordMatch:function(t,i,s){var o=e.Text.WordBreak.getUniqueWords(t,{ignoreCase:!s});return n.map(i,function(e){return r.all(e.text,o,{caseSensitive:s})})},subWordMatchCase:function(e,t){return i.subWordMatch(e,t,!0)},wordMatch:function(e,t,i){return n.map(t,function(t){return r.words(t.text,e,{caseSensitive:i})})},wordMatchCase:function(e,t){return i.wordMatch(e,t,!0)}})},"3.17.2",{requires:["array-extras","highlight-base"]});
diff --git a/js/yui3/autocomplete-list-keys/autocomplete-list-keys-min.js b/js/yui3/autocomplete-list-keys/autocomplete-list-keys-min.js
new file mode 100644
index 000000000..a75ce7979
--- /dev/null
+++ b/js/yui3/autocomplete-list-keys/autocomplete-list-keys-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("autocomplete-list-keys",function(e,t){function u(){e.before(this._bindKeys,this,"bindUI"),this._initKeys()}var n=40,r=13,i=27,s=9,o=38;u.prototype={_initKeys:function(){var e={},t={};e[n]=this._keyDown,t[r]=this._keyEnter,t[i]=this._keyEsc,t[s]=this._keyTab,t[o]=this._keyUp,this._keys=e,this._keysVisible=t},destructor:function(){this._unbindKeys()},_bindKeys:function(){this._keyEvents=this._inputNode.on("keydown",this._onInputKey,this)},_unbindKeys:function(){this._keyEvents&&this._keyEvents.detach(),this._keyEvents=null},_keyDown:function(){this.get("visible")?this._activateNextItem():this.show()},_keyEnter:function(e){var t=this.get("activeItem");if(!t)return!1;this.selectItem(t,e)},_keyEsc:function(){this.hide()},_keyTab:function(e){var t;if(this.get("tabSelect")){t=this.get("activeItem");if(t)return this.selectItem(t,e),!0}return!1},_keyUp:function(){this._activatePrevItem()},_onInputKey:function(e){var t,n=e.keyCode;this._lastInputKey=n,this.get("results").length&&(t=this._keys[n],!t&&this.get("visible")&&(t=this._keysVisible[n]),t&&t.call(this,e)!==!1&&e.preventDefault())}},e.Base.mix(e.AutoCompleteList,[u])},"3.17.2",{requires:["autocomplete-list","base-build"]});
diff --git a/js/yui3/autocomplete-list/assets/autocomplete-list-core.css b/js/yui3/autocomplete-list/assets/autocomplete-list-core.css
new file mode 100644
index 000000000..e79cb8e57
--- /dev/null
+++ b/js/yui3/autocomplete-list/assets/autocomplete-list-core.css
@@ -0,0 +1,34 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-aclist {
+ position: absolute;
+ z-index: 1;
+}
+
+.yui3-aclist-hidden { visibility: hidden; }
+
+.yui3-aclist-aria {
+ /* Hide from sighted users, show to screen readers. */
+ left: -9999px;
+ position: absolute;
+}
+
+.yui3-aclist-list {
+ list-style: none;
+ margin: 0;
+ overflow: hidden;
+ padding: 0;
+}
+
+.yui3-aclist-item {
+ cursor: pointer;
+ list-style: none;
+ padding: 2px 5px;
+}
+
+.yui3-aclist-item-active { outline: #afafaf dotted thin; }
diff --git a/js/yui3/autocomplete-list/assets/skins/night/autocomplete-list.css b/js/yui3/autocomplete-list/assets/skins/night/autocomplete-list.css
new file mode 100644
index 000000000..623b4fc75
--- /dev/null
+++ b/js/yui3/autocomplete-list/assets/skins/night/autocomplete-list.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-aclist{position:absolute;z-index:1}.yui3-aclist-hidden{visibility:hidden}.yui3-aclist-aria{left:-9999px;position:absolute}.yui3-aclist-list{list-style:none;margin:0;overflow:hidden;padding:0}.yui3-aclist-item{cursor:pointer;list-style:none;padding:2px 5px}.yui3-aclist-item-active{outline:#afafaf dotted thin}.yui3-skin-night [for=ac-input]{color:#cbcbcb}.yui3-skin-night .yui3-aclist-content{font-size:100%;background-color:#151515;color:#ccc;border:1px solid #303030;-moz-box-shadow:0 0 17px rgba(0,0,0,0.58);-webkit-box-shadow:0 0 17px rgba(0,0,0,0.58);box-shadow:0 0 17px rgba(0,0,0,0.58)}.yui3-skin-night .yui3-aclist-item-active{background-color:#2f3030;background:-moz-linear-gradient(0% 100% 90deg,#252626 0,#333434 100%);background:-webkit-gradient(linear,left top,left bottom,from(#333434),to(#252626))}.yui3-skin-night .yui3-aclist-item-hover{background-color:#262727;background:-moz-linear-gradient(0% 100% 90deg,#202121 0,#282929 100%);background:-webkit-gradient(linear,left top,left bottom,from(#282929),to(#202121))}.yui3-skin-night .yui3-aclist-item{padding:0 1em;line-height:2.25}.yui3-skin-night .yui3-aclist-item-active{outline:0}.yui3-skin-night .yui3-highlight{color:#efefef}#yui3-css-stamp.skin-night-autocomplete-list{display:none}
diff --git a/js/yui3/autocomplete-list/assets/skins/sam/autocomplete-list.css b/js/yui3/autocomplete-list/assets/skins/sam/autocomplete-list.css
new file mode 100644
index 000000000..1788ecdbc
--- /dev/null
+++ b/js/yui3/autocomplete-list/assets/skins/sam/autocomplete-list.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-aclist{position:absolute;z-index:1}.yui3-aclist-hidden{visibility:hidden}.yui3-aclist-aria{left:-9999px;position:absolute}.yui3-aclist-list{list-style:none;margin:0;overflow:hidden;padding:0}.yui3-aclist-item{cursor:pointer;list-style:none;padding:2px 5px}.yui3-aclist-item-active{outline:#afafaf dotted thin}.yui3-skin-sam .yui3-aclist-content{background:#fff;border:1px solid #afafaf;-moz-box-shadow:1px 1px 4px rgba(0,0,0,0.58);-webkit-box-shadow:1px 1px 4px rgba(0,0,0,0.58);box-shadow:1px 1px 4px rgba(0,0,0,0.58)}.yui3-skin-sam .yui3-aclist-item-hover{background:#bfdaff}.yui3-skin-sam .yui3-aclist-item-active{background:#2647a0;color:#fff;outline:0}#yui3-css-stamp.skin-sam-autocomplete-list{display:none}
diff --git a/js/yui3/autocomplete-list/autocomplete-list-min.js b/js/yui3/autocomplete-list/autocomplete-list-min.js
new file mode 100644
index 000000000..675e6ab32
--- /dev/null
+++ b/js/yui3/autocomplete-list/autocomplete-list-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("autocomplete-list",function(e,t){var n=e.Lang,r=e.Node,i=e.Array,s=e.UA.ie&&e.UA.ie<7,o=9,u="_CLASS_ITEM",a="_CLASS_ITEM_ACTIVE",f="_CLASS_ITEM_HOVER",l="_SELECTOR_ITEM",c="activeItem",h="alwaysShowList",p="circular",d="hoveredItem",v="id",m="item",g="list",y="result",b="results",w="visible",E="width",S="select",x=e.Base.create("autocompleteList",e.Widget,[e.AutoCompleteBase,e.WidgetPosition,e.WidgetPositionAlign],{ARIA_TEMPLATE:"<div/>",ITEM_TEMPLATE:"<li/>",LIST_TEMPLATE:"<ul/>",UI_EVENTS:function(){var t=e.merge(e.Node.DOM_EVENTS);return delete t.valuechange,delete t.valueChange,t}(),initializer:function(){var t=this.get("inputNode");if(!t){e.error("No inputNode specified.");return}this._inputNode=t,this._listEvents=[],this.DEF_PARENT_NODE=t.get("parentNode"),this[u]=this.getClassName(m),this[a]=this.getClassName(m,"active"),this[f]=this.getClassName(m,"hover"),this[l]="."+this[u],this.publish(S,{defaultFn:this._defSelectFn})},destructor:function(){while(this._listEvents.length)this._listEvents.pop().detach();this._ariaNode&&this._ariaNode.remove().destroy(!0)},bindUI:function(){this._bindInput(),this._bindList()},renderUI:function(){var t=this._createAriaNode(),n=this.get("boundingBox"),r=this.get("contentBox"),i=this._inputNode,o=this._createListNode(),u=i.get("parentNode");i.addClass(this.getClassName("input")).setAttrs({"aria-autocomplete":g,"aria-expanded":!1,"aria-owns":o.get("id")}),u.append(t),s&&n.plug(e.Plugin.Shim),this._ariaNode=t,this._boundingBox=n,this._contentBox=r,this._listNode=o,this._parentNode=u},syncUI:function(){this._syncResults(),this._syncVisibility()},hide:function(){return this.get(h)?this:this.set(w,!1)},selectItem:function(e,t){if(e){if(!e.hasClass(this[u]))return this}else{e=this.get(c);if(!e)return this}return this.fire(S,{itemNode:e,originEvent:t||null,result:e.getData(y)}),this},_activateNextItem:function(){var e=this.get(c),t;return e?t=e.next(this[l])||(this.get(p)?null:e):t=this._getFirstItemNode(),this.set(c,t),this},_activatePrevItem:function(){var e=this.get(c),t=e?e.previous(this[l]):this.get(p)&&this._getLastItemNode();return this.set(c,t||null),this},_add:function(t){var r=[];return i.each(n.isArray(t)?t:[t],function(e){r.push(this._createItemNode(e).setData(y,e))},this),r=e.all(r),this._listNode.append(r.toFrag()),r},_ariaSay:function(e,t){var r=this.get("strings."+e);this._ariaNode.set("text",t?n.sub(r,t):r)},_bindInput:function(){var e=this._inputNode,t,n,r;this.get("align")===null&&(r=this.get("tokenInput"),t=r&&r.get("boundingBox")||e,this.set("align",{node:t,points:["tl","bl"]}),!this.get(E)&&(n=t.get("offsetWidth"))&&this.set(E,n)),this._listEvents=this._listEvents.concat([e.after("blur",this._afterListInputBlur,this),e.after("focus",this._afterListInputFocus,this)])},_bindList:function(){this._listEvents=this._listEvents.concat([e.one("doc").after("click",this._afterDocClick,this),e.one("win").after("windowresize",this._syncPosition,this),this.after({mouseover:this._afterMouseOver,mouseout:this._afterMouseOut,activeItemChange:this._afterActiveItemChange,alwaysShowListChange:this._afterAlwaysShowListChange,hoveredItemChange:this._afterHoveredItemChange,resultsChange:this._afterResultsChange,visibleChange:this._afterVisibleChange}),this._listNode.delegate("click",this._onItemClick,this[l],this)])},_clear:function(){this.set(c,null),this._set(d,null),this._listNode.get("children").remove(!0)},_createAriaNode:function(){var e=r.create(this.ARIA_TEMPLATE);return e.addClass(this.getClassName("aria")).setAttrs({"aria-live":"polite",role:"status"})},_createItemNode:function(t){var n=r.create(this.ITEM_TEMPLATE);return n.addClass(this[u]).setAttrs({id:e.stamp(n),role:"option"}).setAttribute("data-text",t.text).append(t.display)},_createListNode:function(){var t=this.get("listNode")||r.create(this.LIST_TEMPLATE);return t.addClass(this.getClassName(g)).setAttrs({id:e.stamp(t),role:"listbox"}),this._set("listNode",t),this.get("contentBox").append(t),t},_getFirstItemNode:function(){return this._listNode.one(this[l])},_getLastItemNode:function(){return this._listNode.one(this[l]+":last-child")},_syncPosition:function(){this._syncUIPosAlign(),this._syncShim()},_syncResults:function(e){e||(e=this.get(b)),this._clear(),e.length&&(this._add(e),this._ariaSay("items_available")),this._syncPosition(),this.get("activateFirstItem")&&!this.get(c)&&this.set(c,this._getFirstItemNode())},_syncShim:s?function(){var e=this._boundingBox.shim;e&&e.sync()}:function(){},_syncVisibility:function(t){this.get(h)&&(t=!0,this.set(w,t)),typeof t=="undefined"&&(t=this.get(w)),this._inputNode.set("aria-expanded",t),this._boundingBox.set("aria-hidden",!t),t?this._syncPosition():(this.set(c,null),this._set(d,null),this._boundingBox.get("offsetWidth")),e.UA.ie===7&&e.one("body").addClass("yui3-ie7-sucks").removeClass("yui3-ie7-sucks")},_afterActiveItemChange:function(t){var n=this._inputNode,r=t.newVal,i=t.prevVal,s;i&&i._node&&i.removeClass(this[a]),r?(r.addClass(this[a]),n.set("aria-activedescendant",r.get(v))):n.removeAttribute("aria-activedescendant"),this.get("scrollIntoView")&&(s=r||n,(!s.inRegion(e.DOM.viewportRegion(),!0)||!s.inRegion(this._contentBox,!0))&&s.scrollIntoView())},_afterAlwaysShowListChange:function(e){this.set(w,e.newVal||this.get(b).length>0)},_afterDocClick:function(e){var t=this._boundingBox,n=e.target;n!==this._inputNode&&n!==t&&!n.ancestor("#"+t.get("id"),!0)&&this.hide()},_afterHoveredItemChange:function(e){var t=e.newVal,n=e.prevVal;n&&n.removeClass(this[f]),t&&t.addClass(this[f])},_afterListInputBlur:function(){this._listInputFocused=!1,this.get(w)&&!this._mouseOverList&&(this._lastInputKey!==o||!this.get("tabSelect")||!this.get(c))&&this.hide()},_afterListInputFocus:function(){this._listInputFocused=!0},_afterMouseOver:function(e){var t=e.domEvent.target.ancestor(this[l],!0);this._mouseOverList=!0,t&&this._set(d,t)},_afterMouseOut:function(){this._mouseOverList=!1,this._set(d,null)},_afterResultsChange:function(e){this._syncResults(e.newVal
+),this.get(h)||this.set(w,!!e.newVal.length)},_afterVisibleChange:function(e){this._syncVisibility(!!e.newVal)},_onItemClick:function(e){var t=e.currentTarget;this.set(c,t),this.selectItem(t,e)},_defSelectFn:function(e){var t=e.result.text;this._inputNode.focus(),this._updateValue(t),this._ariaSay("item_selected",{item:t}),this.hide()}},{ATTRS:{activateFirstItem:{value:!1},activeItem:{setter:e.one,value:null},alwaysShowList:{value:!1},circular:{value:!0},hoveredItem:{readOnly:!0,value:null},listNode:{writeOnce:"initOnly",value:null},scrollIntoView:{value:!1},strings:{valueFn:function(){return e.Intl.get("autocomplete-list")}},tabSelect:{value:!0},visible:{value:!1}},CSS_PREFIX:e.ClassNameManager.getClassName("aclist")});e.AutoCompleteList=x,e.AutoComplete=x},"3.17.2",{lang:["en","es","hu","it"],requires:["autocomplete-base","event-resize","node-screen","selector-css3","shim-plugin","widget","widget-position","widget-position-align"],skinnable:!0});
diff --git a/js/yui3/autocomplete-list/lang/autocomplete-list.js b/js/yui3/autocomplete-list/lang/autocomplete-list.js
new file mode 100644
index 000000000..d58cc5cf6
--- /dev/null
+++ b/js/yui3/autocomplete-list/lang/autocomplete-list.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/autocomplete-list",function(e){e.Intl.add("autocomplete-list","",{item_selected:"{item} selected.",items_available:"Suggestions are available. Use the up and down arrow keys to select suggestions."})},"3.17.2");
diff --git a/js/yui3/autocomplete-list/lang/autocomplete-list_en.js b/js/yui3/autocomplete-list/lang/autocomplete-list_en.js
new file mode 100644
index 000000000..da6a4069f
--- /dev/null
+++ b/js/yui3/autocomplete-list/lang/autocomplete-list_en.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/autocomplete-list_en",function(e){e.Intl.add("autocomplete-list","en",{item_selected:"{item} selected.",items_available:"Suggestions are available. Use up and down arrows to select."})},"3.17.2");
diff --git a/js/yui3/autocomplete-list/lang/autocomplete-list_es.js b/js/yui3/autocomplete-list/lang/autocomplete-list_es.js
new file mode 100644
index 000000000..d58a5f67a
--- /dev/null
+++ b/js/yui3/autocomplete-list/lang/autocomplete-list_es.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/autocomplete-list_es",function(e){e.Intl.add("autocomplete-list","es",{item_selected:"{item} seleccionado.",items_available:"Hay sugerencias disponibles. Use flecha arriba y abajo para seleccionar."})},"3.17.2");
diff --git a/js/yui3/autocomplete-list/lang/autocomplete-list_hu.js b/js/yui3/autocomplete-list/lang/autocomplete-list_hu.js
new file mode 100644
index 000000000..8127316b3
--- /dev/null
+++ b/js/yui3/autocomplete-list/lang/autocomplete-list_hu.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/autocomplete-list_hu",function(e){e.Intl.add("autocomplete-list","hu",{item_selected:"{item} kiv\u00e1lasztva.",items_available:"Javaslatok \u00e1llnak rendelkez\u00e9sre. Haszn\u00e1lja a fel \u00e9s le nyilakat a v\u00e1laszt\u00e1shoz."})},"3.17.2");
diff --git a/js/yui3/autocomplete-list/lang/autocomplete-list_it.js b/js/yui3/autocomplete-list/lang/autocomplete-list_it.js
new file mode 100644
index 000000000..bb31e9372
--- /dev/null
+++ b/js/yui3/autocomplete-list/lang/autocomplete-list_it.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/autocomplete-list_it",function(e){e.Intl.add("autocomplete-list","it",{item_selected:"{item} selezionato.",items_available:"Sono disponibili suggerimenti. Usa la freccia su e gi\u00f9 per selezionare."})},"3.17.2");
diff --git a/js/yui3/autocomplete-plugin/autocomplete-plugin-min.js b/js/yui3/autocomplete-plugin/autocomplete-plugin-min.js
new file mode 100644
index 000000000..2c3006b25
--- /dev/null
+++ b/js/yui3/autocomplete-plugin/autocomplete-plugin-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("autocomplete-plugin",function(e,t){function r(e){e.inputNode=e.host,!e.render&&e.render!==!1&&(e.render=!0),r.superclass.constructor.apply(this,arguments)}var n=e.Plugin;e.extend(r,e.AutoCompleteList,{},{NAME:"autocompleteListPlugin",NS:"ac",CSS_PREFIX:e.ClassNameManager.getClassName("aclist")}),n.AutoComplete=r,n.AutoCompleteList=r},"3.17.2",{requires:["autocomplete-list","node-pluginhost"]});
diff --git a/js/yui3/autocomplete-sources/autocomplete-sources-min.js b/js/yui3/autocomplete-sources/autocomplete-sources-min.js
new file mode 100644
index 000000000..7232b2957
--- /dev/null
+++ b/js/yui3/autocomplete-sources/autocomplete-sources-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("autocomplete-sources",function(e,t){var n=e.AutoCompleteBase,r=e.Lang,i="_sourceSuccess",s="maxResults",o="requestTemplate",u="resultListLocator";e.mix(n.prototype,{_YQL_SOURCE_REGEX:/^(?:select|set|use)\s+/i,_beforeCreateObjectSource:function(t){return t instanceof e.Node&&t.get("nodeName").toLowerCase()==="select"?this._createSelectSource(t):e.JSONPRequest&&t instanceof e.JSONPRequest?this._createJSONPSource(t):this._createObjectSource(t)},_createIOSource:function(t){function a(n){var o=n.request;if(r._cache&&o in r._cache){r[i](r._cache[o],n);return}s&&s.isInProgress()&&s.abort(),s=e.io(r._getXHRUrl(t,n),{on:{success:function(t,s){var u;try{u=e.JSON.parse(s.responseText)}catch(a){e.error("JSON parse error",a)}u&&(r._cache&&(r._cache[o]=u),r[i](u,n))}}})}var n={type:"io"},r=this,s,o,u;return n.sendRequest=function(t){o=t;if(u)return;u=!0,e.use("io-base","json-parse",function(){n.sendRequest=a,a(o)})},n},_createJSONPSource:function(t){function u(e){var n=e.request,s=e.query;if(r._cache&&n in r._cache){r[i](r._cache[n],e);return}t._config.on.success=function(t){r._cache&&(r._cache[n]=t),r[i](t,e)},t.send(s)}var n={type:"jsonp"},r=this,s,o;return n.sendRequest=function(i){s=i;if(o)return;o=!0,e.use("jsonp",function(){t instanceof e.JSONPRequest||(t=new e.JSONPRequest(t,{format:e.bind(r._jsonpFormatter,r)})),n.sendRequest=u,u(s)})},n},_createSelectSource:function(e){var t=this;return{type:"select",sendRequest:function(n){var r=[];e.get("options").each(function(e){r.push({html:e.get("innerHTML"),index:e.get("index"),node:e,selected:e.get("selected"),text:e.get("text"),value:e.get("value")})}),t[i](r,n)}}},_createStringSource:function(e){return this._YQL_SOURCE_REGEX.test(e)?this._createYQLSource(e):e.indexOf("{callback}")!==-1?this._createJSONPSource(e):this._createIOSource(e)},_createYQLSource:function(t){function c(o){var u=o.query,a=n.get("yqlEnv"),f=n.get(s),c,h,p;p=r.sub(t,{maxResults:f>0?f:1e3,request:o.request,query:u});if(n._cache&&p in n._cache){n[i](n._cache[p],o);return}c=function(e){n._cache&&(n._cache[p]=e),n[i](e,o)},h={proto:n.get("yqlProtocol")},l?(l._callback=c,l._opts=h,l._params.q=p,a&&(l._params.env=a)):l=new e.YQLRequest(p,{on:{success:c},allowCache:!1},a?{env:a}:null,h),l.send()}var n=this,o={type:"yql"},a,f,l;return n.get(u)||n.set(u,n._defaultYQLLocator),o.sendRequest=function(t){a=t,f||(f=!0,e.use("yql",function(){o.sendRequest=c,c(a)}))},o},_defaultYQLLocator:function(t){var n=t&&t.query&&t.query.results,i;return n&&r.isObject(n)?(i=e.Object.values(n)||[],n=i.length===1?i[0]:i,r.isArray(n)||(n=[n])):n=[],n},_getXHRUrl:function(e,t){var n=this.get(s);return t.query!==t.request&&(e+=t.request),r.sub(e,{maxResults:n>0?n:1e3,query:encodeURIComponent(t.query)})},_jsonpFormatter:function(e,t,n){var i=this.get(s),u=this.get(o);return u&&(e+=u(n)),r.sub(e,{callback:t,maxResults:i>0?i:1e3,query:encodeURIComponent(n)})}}),e.mix(n.ATTRS,{yqlEnv:{value:null},yqlProtocol:{value:"http"}}),e.mix(n.SOURCE_TYPES,{io:"_createIOSource",jsonp:"_createJSONPSource",object:"_beforeCreateObjectSource",select:"_createSelectSource",string:"_createStringSource",yql:"_createYQLSource"},!0)},"3.17.2",{optional:["io-base","json-parse","jsonp","yql"],requires:["autocomplete-base"]});
diff --git a/js/yui3/axis-base/axis-base-min.js b/js/yui3/axis-base/axis-base-min.js
new file mode 100644
index 000000000..b428fe35d
--- /dev/null
+++ b/js/yui3/axis-base/axis-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("axis-base",function(e,t){function r(){}var n=e.Lang;r.ATTRS={styles:{getter:function(){return this._styles=this._styles||this._getDefaultStyles(),this._styles},setter:function(e){this._styles=this._setStyles(e)}},graphic:{}},r.NAME="renderer",r.prototype={_styles:null,_setStyles:function(e){var t=this.get("styles");return this._mergeStyles(e,t)},_mergeStyles:function(t,r){r||(r={});var i=e.merge(r,{});return e.Object.each(t,function(e,t){r.hasOwnProperty(t)&&n.isObject(e)&&!n.isFunction(e)&&!n.isArray(e)?i[t]=this._mergeStyles(e,r[t]):i[t]=e},this),i},_copyObject:function(e){var t={},r,i;for(r in e)e.hasOwnProperty(r)&&(i=e[r],typeof i=="object"&&!n.isArray(i)?t[r]=this._copyObject(i):t[r]=i);return t},_getDefaultStyles:function(){return{padding:{top:0,right:0,bottom:0,left:0}}}},e.augment(r,e.Attribute),e.Renderer=r,e.AxisBase=e.Base.create("axisBase",e.Base,[e.Renderer],{initializer:function(){this.after("minimumChange",e.bind(this._keyChangeHandler,this)),this.after("maximumChange",e.bind(this._keyChangeHandler,this)),this.after("keysChange",this._keyChangeHandler),this.after("dataProviderChange",this._dataProviderChangeHandler)},getOrigin:function(){return this.get("minimum")},_dataProviderChangeHandler:function(){var e=this.get("keyCollection").concat(),t=this.get("keys"),n;if(t)for(n in t)t.hasOwnProperty(n)&&delete t[n];e&&e.length&&this.set("keys",e)},_updateMinAndMax:function(){},GUID:"yuibaseaxis",_type:null,_setMaximum:null,_setMinimum:null,_data:null,_updateTotalDataFlag:!0,_dataReady:!1,addKey:function(e){this.set("keys",e)},_getKeyArray:function(e,t){var n=0,r,i=[],s=t.length;for(;n<s;++n)r=t[n],i[n]=r[e];return i},_updateTotalData:function(){var e=this.get("keys"),t;this._data=[];for(t in e)e.hasOwnProperty(t)&&(this._data=this._data.concat(e[t]));this._updateTotalDataFlag=!1},removeKey:function(e){var t=this.get("keys");t.hasOwnProperty(e)&&(delete t[e],this._keyChangeHandler())},getKeyValueAt:function(e,t){var r=NaN,i=this.get("keys");return i[e]&&n.isNumber(parseFloat(i[e][t]))&&(r=i[e][t]),parseFloat(r)},getDataByKey:function(e){var t,r,i,s,o=this.get("keys");if(n.isArray(e)){t={},i=e.length;for(r=0;r<i;r+=1)s=e[r],o[s]&&(t[s]=this.getDataByKey(s))}else o[e]?t=o[e]:t=null;return t},getTotalMajorUnits:function(){var e,t=this.get("styles").majorUnit;return e=t.count,e},getEdgeOffset:function(e,t){var n;return this.get("calculateEdgeOffset")?n=t/e/2:n=0,n},_keyChangeHandler:function(){this._updateMinAndMax(),this._updateTotalDataFlag=!0,this.fire("dataUpdate")},_getDefaultStyles:function(){var e={majorUnit:{determinant:"count",count:11,distance:75}};return e},_maximumGetter:function(){var e=this.get("dataMaximum"),t=this.get("minimum");return t===0&&e===0&&(e=10),n.isNumber(this._setMaximum)&&(e=this._setMaximum),parseFloat(e)},_maximumSetter:function(e){return this._setMaximum=parseFloat(e),e},_minimumGetter:function(){var e=this.get("dataMinimum");return n.isNumber(this._setMinimum)&&(e=this._setMinimum),parseFloat(e)},_minimumSetter:function(e){return this._setMinimum=parseFloat(e),e},_getSetMax:function(){return n.isNumber(this._setMaximum)},_getCoordsFromValues:function(e,t,n,r,i,s){var o,u=[],a=r.length;for(o=0;o<a;o+=1)u.push(this._getCoordFromValue.apply(this,[e,t,n,r[o],i,s]));return u},_getDataValuesByCount:function(e,t,n){var r=[],i=t,s=e-1,o=n-t,u=o/s,a;for(a=0;a<s;a+=1)r.push(i),i+=u;return r.push(n),r},_getSetMin:function(){return n.isNumber(this._setMinimum)}},{ATTRS:{calculateEdgeOffset:{value:!1},labelFunction:{valueFn:function(){return this.formatLabel}},keys:{value:{},setter:function(e){var t={},r,i,s=this.get("dataProvider");if(n.isArray(e)){i=e.length;for(r=0;r<i;++r)t[e[r]]=this._getKeyArray(e[r],s)}else if(n.isString(e))t=this.get("keys"),t[e]=this._getKeyArray(e,s);else for(r in e)e.hasOwnProperty(r)&&(t[r]=this._getKeyArray(r,s));return this._updateTotalDataFlag=!0,t}},type:{readOnly:!0,getter:function(){return this._type}},dataProvider:{setter:function(e){return e}},dataMaximum:{getter:function(){return n.isNumber(this._dataMaximum)||this._updateMinAndMax(),this._dataMaximum}},maximum:{lazyAdd:!1,getter:"_maximumGetter",setter:"_maximumSetter"},dataMinimum:{getter:function(){return n.isNumber(this._dataMinimum)||this._updateMinAndMax(),this._dataMinimum}},minimum:{lazyAdd:!1,getter:"_minimumGetter",setter:"_minimumSetter"},setMax:{readOnly:!0,getter:"_getSetMax"},setMin:{readOnly:!0,getter:"_getSetMin"},data:{getter:function(){return(!this._data||this._updateTotalDataFlag)&&this._updateTotalData(),this._data}},keyCollection:{getter:function(){var e=this.get("keys"),t,n=[];for(t in e)e.hasOwnProperty(t)&&n.push(t);return n},readOnly:!0},labelFunctionScope:{}}})},"3.17.2",{requires:["classnamemanager","datatype-number","datatype-date","base","event-custom"]});
diff --git a/js/yui3/axis-category-base/axis-category-base-min.js b/js/yui3/axis-category-base/axis-category-base-min.js
new file mode 100644
index 000000000..29bfc9a34
--- /dev/null
+++ b/js/yui3/axis-category-base/axis-category-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("axis-category-base",function(e,t){function r(){}var n=e.Lang;r.NAME="categoryImpl",r.ATTRS={labelFormat:{value:null},calculateEdgeOffset:{value:!0}},r.prototype={formatLabel:function(e){return e},_indices:null,GUID:"yuicategoryaxis",_type:"category",_updateMinAndMax:function(){this._dataMaximum=Math.max(this.get("data").length-1,0),this._dataMinimum=0},_getKeyArray:function(e,t){var n=0,r,i=[],s=[],o=t.length;this._indices||(this._indices={});for(;n<o;++n)r=t[n],i[n]=n,s[n]=r[e];return this._indices[e]=i,s},getDataByKey:function(e){this._indices||this.get("keys");var t=this._indices;return t&&t[e]?t[e]:null},getTotalMajorUnits:function(){return this.get("data").length},_getCoordFromValue:function(e,t,r,i,s){var o,u,a;return n.isNumber(i)?(o=t-e,u=r/o,a=(i-e)*u,a=s+a):a=NaN,a},getKeyValueAt:function(e,t){var n=NaN,r=this.get("keys");return r[e]&&r[e][t]&&(n=r[e][t]),n}},e.CategoryImpl=r,e.CategoryAxisBase=e.Base.create("categoryAxisBase",e.AxisBase,[e.CategoryImpl])},"3.17.2",{requires:["axis-base"]});
diff --git a/js/yui3/axis-category/axis-category-min.js b/js/yui3/axis-category/axis-category-min.js
new file mode 100644
index 000000000..aae696534
--- /dev/null
+++ b/js/yui3/axis-category/axis-category-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("axis-category",function(e,t){var n=e.Lang;e.CategoryAxis=e.Base.create("categoryAxis",e.Axis,[e.CategoryImpl],{getMinimumValue:function(){var e=this.get("data"),t=e[0];return t},getMaximumValue:function(){var e=this.get("data"),t=e.length-1,n=e[t];return n},_getLabelByIndex:function(e){var t,n=this.get("data");return t=n[e],t},_getLabelData:function(t,r,i,s,o,u,a,f,l){var c,h,p=[],d=[],v,m,g=this.get("data"),y=u;l=l||g;for(h=0;h<f;h+=1)c=l[h],m=e.Array.indexOf(g,c),n.isNumber(m)&&m>-1&&(v={},v[r]=t,v[i]=this._getCoordFromValue(s,o,a,m,y),p.push(v),d.push(c));return{points:p,values:d}}})},"3.17.2",{requires:["axis","axis-category-base"]});
diff --git a/js/yui3/axis-numeric-base/axis-numeric-base-min.js b/js/yui3/axis-numeric-base/axis-numeric-base-min.js
new file mode 100644
index 000000000..ed7a798c7
--- /dev/null
+++ b/js/yui3/axis-numeric-base/axis-numeric-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("axis-numeric-base",function(e,t){function r(){}var n=e.Lang;r.NAME="numericImpl",r.ATTRS={alwaysShowZero:{value:!0},labelFormat:{value:{prefix:"",thousandsSeparator:"",decimalSeparator:"",decimalPlaces:"0",suffix:""}},roundingMethod:{value:"niceNumber"},scaleType:{value:"linear"}},r.prototype={initializer:function(){this.after("alwaysShowZeroChange",this._keyChangeHandler),this.after("roundingMethodChange",this._keyChangeHandler),this.after("scaleTypeChange",this._keyChangeHandler)},formatLabel:function(t,n){return n?e.DataType.Number.format(t,n):t},getTotalByKey:function(e){var t=0,n=this.getDataByKey(e),r=0,i,s=n?n.length:0;for(;r<s;++r)i=parseFloat(n[r]),isNaN(i)||(t+=i);return t},getOrigin:function(){var e=0,t=this.get("minimum"),n=this.get("maximum");return e=Math.max(e,t),e=Math.min(e,n),e},_type:"numeric",_getMinimumUnit:function(e,t,n){return this._getNiceNumber(Math.ceil((e-t)/n))},_getNiceNumber:function(e){var t=e,n=Math.ceil(Math.log(t)*.4342944819032518),r=Math.pow(10,n),i;return r/2>=t?(i=Math.floor((r/2-t)/(Math.pow(10,n-1)/2)),t=r/2-i*Math.pow(10,n-1)/2):t=r,isNaN(t)?e:t},_updateMinAndMax:function(){var e=this.get("data"),t,n,r,i,s=0,o=this.get("setMax"),u=this.get("setMin");if(!o||!u){if(e&&e.length&&e.length>0){r=e.length;for(;s<r;s++){i=e[s];if(isNaN(i)){t=o?this._setMaximum:t,n=u?this._setMinimum:n;continue}u?n=this._setMinimum:n===undefined?n=i:n=Math.min(i,n),o?t=this._setMaximum:t===undefined?t=i:t=Math.max(i,t),this._actualMaximum=t,this._actualMinimum=n}}this.get("scaleType")!=="logarithmic"?this._roundMinAndMax(n,t,u,o):(this._dataMaximum=t,this._dataMinimum=n)}},_roundMinAndMax:function(e,t,n,r){var i,s,o=e>=0,u=t>0,a,f,l,c,h,p,d,v=this.getTotalMajorUnits()-1,m=this.get("alwaysShowZero"),g=this.get("roundingMethod"),y=(t-e)/v>=1;if(g)if(g==="niceNumber"){i=this._getMinimumUnit(t,e,v);if(o&&u)(m||e<i)&&!n?(e=0,i=this._getMinimumUnit(t,e,v)):e=this._roundDownToNearest(e,i),r?m||(e=t-i*v):n?t=e+i*v:t=this._roundUpToNearest(t,i);else if(u&&!o)if(m){c=Math.round(v/(-1*e/t+1)),c=Math.max(Math.min(c,v-1),1),h=v-c,p=Math.ceil(t/c),d=Math.floor(e/h)*-1;if(n){while(d<p&&h>=0)h--,c++,p=Math.ceil(t/c),d=Math.floor(e/h)*-1;h>0?t=d*c:t=e+i*v}else if(r){while(p<d&&c>=0)h++,c--,d=Math.floor(e/h)*-1,p=Math.ceil(t/c);c>0?e=p*h*-1:e=t-i*v}else i=Math.max(p,d),i=this._getNiceNumber(i),t=i*c,e=i*h*-1}else r?e=t-i*v:n?t=e+i*v:(e=this._roundDownToNearest(e,i),t=this._roundUpToNearest(t,i));else n?m?t=0:t=e+i*v:r?e=t-i*v:m||t===0||t+i>0?(t=0,i=this._getMinimumUnit(t,e,v),e=t-i*v):(e=this._roundDownToNearest(e,i),t=this._roundUpToNearest(t,i))}else g==="auto"?o&&u?((m||e<(t-e)/v)&&!n&&(e=0),i=(t-e)/v,y?(i=Math.ceil(i),t=e+i*v):t=e+Math.ceil(i*v*1e5)/1e5):u&&!o?m?(c=Math.round(v/(-1*e/t+1)),c=Math.max(Math.min(c,v-1),1),h=v-c,y?(p=Math.ceil(t/c),d=Math.floor(e/h)*-1,i=Math.max(p,d),t=i*c,e=i*h*-1):(p=t/c,d=e/h*-1,i=Math.max(p,d),t=Math.ceil(i*c*1e5)/1e5,e=Math.ceil(i*h*1e5)/1e5*-1)):(i=(t-e)/v,y&&(i=Math.ceil(i)),e=Math.round(this._roundDownToNearest(e,i)*1e5)/1e5,t=Math.round(this._roundUpToNearest(t,i)*1e5)/1e5):(i=(t-e)/v,y&&(i=Math.ceil(i)),m||t===0||t+i>0?(t=0,i=(t-e)/v,y?(Math.ceil(i),e=t-i*v):e=t-Math.ceil(i*v*1e5)/1e5):(e=this._roundDownToNearest(e,i),t=this._roundUpToNearest(t,i))):!isNaN(g)&&isFinite(g)&&(i=g,s=i*v,a=t-e>s,l=this._roundDownToNearest(e,i),f=this._roundUpToNearest(t,i),r?e=t-s:n?t=e+s:o&&u?(m||l<=0?e=0:e=l,t=e+s):u&&!o?(e=l,t=f):(m||f>=0?t=0:t=f,e=t-s));this._dataMaximum=t,this._dataMinimum=e},_roundToNearest:function(e,t){t=t||1;var n=Math.round(this._roundToPrecision(e/t,10))*t;return this._roundToPrecision(n,10)},_roundUpToNearest:function(e,t){return t=t||1,Math.ceil(this._roundToPrecision(e/t,10))*t},_roundDownToNearest:function(e,t){return t=t||1,Math.floor(this._roundToPrecision(e/t,10))*t},_getCoordFromValue:function(e,t,r,i,s,o){var u,a,f,l=n.isNumber;return i=parseFloat(i),l(i)?(this.get("scaleType")==="logarithmic"&&e>0&&(e=Math.log(e),t=Math.log(t),i=Math.log(i)),u=t-e,a=r/u,f=(i-e)*a,f=o?s-f:s+f):f=NaN,f},_roundToPrecision:function(e,t){t=t||0;var n=Math.pow(10,t);return Math.round(n*e)/n}},e.NumericImpl=r,e.NumericAxisBase=e.Base.create("numericAxisBase",e.AxisBase,[e.NumericImpl])},"3.17.2",{requires:["axis-base"]});
diff --git a/js/yui3/axis-numeric/axis-numeric-min.js b/js/yui3/axis-numeric/axis-numeric-min.js
new file mode 100644
index 000000000..42c10206a
--- /dev/null
+++ b/js/yui3/axis-numeric/axis-numeric-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("axis-numeric",function(e,t){var n=e.Lang;e.NumericAxis=e.Base.create("numericAxis",e.Axis,[e.NumericImpl],{_getLabelByIndex:function(e,t){var n=this.get("minimum"),r=this.get("maximum"),i=(r-n)/(t-1),s,o=this.get("roundingMethod");return t-=1,e===0?s=n:e===t?s=r:(s=e*i,o==="niceNumber"&&(s=this._roundToNearest(s,i)),s+=n),parseFloat(s)},_getLabelData:function(e,t,n,r,i,s,o,u,a){var f,l,c=[],h=[],p,d=t==="x",v=d?o+s:s;a=a||this._getDataValuesByCount(u,r,i);for(l=0;l<u;l+=1)f=parseFloat(a[l]),f<=i&&f>=r&&(p={},p[t]=e,p[n]=this._getCoordFromValue(r,i,o,f,v,d),c.push(p),h.push(f));return{points:c,values:h}},_hasDataOverflow:function(){var e,t,r;return this.get("setMin")||this.get("setMax")?!0:(e=this.get("roundingMethod"),t=this._actualMinimum,r=this._actualMaximum,n.isNumber(e)&&(n.isNumber(r)&&r>this._dataMaximum||n.isNumber(t)&&t<this._dataMinimum)?!0:!1)}})},"3.17.2",{requires:["axis","axis-numeric-base"]});
diff --git a/js/yui3/axis-stacked-base/axis-stacked-base-min.js b/js/yui3/axis-stacked-base/axis-stacked-base-min.js
new file mode 100644
index 000000000..5a6cf6f9e
--- /dev/null
+++ b/js/yui3/axis-stacked-base/axis-stacked-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("axis-stacked-base",function(e,t){function n(){}n.NAME="stackedImpl",n.prototype={_type:"stacked",_updateMinAndMax:function(){var e=0,t=0,n=0,r=0,i=0,s=0,o,u,a=this.get("keys"),f=this.get("setMin"),l=this.get("setMax");for(o in a)a.hasOwnProperty(o)&&(i=Math.max(i,a[o].length));for(;s<i;++s){n=0,r=0;for(o in a)if(a.hasOwnProperty(o)){u=a[o][s];if(isNaN(u))continue;u>=0?n+=u:r+=u}n>0?e=Math.max(e,n):e=Math.max(e,r),r<0?t=Math.min(t,r):t=Math.min(t,n)}this._actualMaximum=e,this._actualMinimum=t,l&&(e=this._setMaximum),f&&(t=this._setMinimum),this._roundMinAndMax(t,e,f,l)}},e.StackedImpl=n,e.StackedAxisBase=e.Base.create("stackedAxisBase",e.NumericAxisBase,[e.StackedImpl])},"3.17.2",{requires:["axis-numeric-base"]});
diff --git a/js/yui3/axis-stacked/axis-stacked-min.js b/js/yui3/axis-stacked/axis-stacked-min.js
new file mode 100644
index 000000000..a62892d4a
--- /dev/null
+++ b/js/yui3/axis-stacked/axis-stacked-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("axis-stacked",function(e,t){e.StackedAxis=e.Base.create("stackedAxis",e.NumericAxis,[e.StackedImpl])},"3.17.2",{requires:["axis-numeric","axis-stacked-base"]});
diff --git a/js/yui3/axis-time-base/axis-time-base-min.js b/js/yui3/axis-time-base/axis-time-base-min.js
new file mode 100644
index 000000000..942474d1d
--- /dev/null
+++ b/js/yui3/axis-time-base/axis-time-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("axis-time-base",function(e,t){function r(){}var n=e.Lang;r.NAME="timeImpl",r.ATTRS={labelFormat:{value:"%b %d, %y"}},r.prototype={_type:"time",_maximumGetter:function(){var e=this._getNumber(this._setMaximum);return n.isNumber(e)||(e=this._getNumber(this.get("dataMaximum"))),parseFloat(e)},_maximumSetter:function(e){return this._setMaximum=this._getNumber(e),e},_minimumGetter:function(){var e=this._getNumber(this._setMinimum);return n.isNumber(e)||(e=this._getNumber(this.get("dataMinimum"))),parseFloat(e)},_minimumSetter:function(e){return this._setMinimum=this._getNumber(e),e},_getSetMax:function(){var e=this._getNumber(this._setMaximum);return n.isNumber(e)},_getSetMin:function(){var e=this._getNumber(this._setMinimum);return n.isNumber(e)},formatLabel:function(t,n){return t=e.DataType.Date.parse(t),n?e.DataType.Date.format(t,{format:n}):t},GUID:"yuitimeaxis",_dataType:"time",_getKeyArray:function(e,t){var r,i=[],s=0,o,u=t.length;for(;s<u;++s)r=t[s][e],n.isDate(r)?o=r.valueOf():(o=new Date(r),n.isDate(o)?o=o.valueOf():n.isNumber(r)?o=r:n.isNumber(parseFloat(r))?o=parseFloat(r):(typeof r!="string"&&(r=r),o=(new Date(r)).valueOf())),i[s]=o;return i},_updateMinAndMax:function(){var e=this.get("data"),t=0,n=0,r,i,s;if(e&&e.length&&e.length>0){r=e.length,t=n=e[0];if(r>1)for(s=1;s<r;s++){i=e[s];if(isNaN(i))continue;t=Math.max(i,t),n=Math.min(i,n)}}this._dataMaximum=t,this._dataMinimum=n},_getCoordFromValue:function(e,t,r,i,s){var o,u,a,f=n.isNumber;return i=this._getNumber(i),f(i)?(o=t-e,u=r/o,a=(i-e)*u,a=s+a):a=NaN,a},_getNumber:function(e){return n.isDate(e)?e=e.valueOf():!n.isNumber(e)&&e&&(e=(new Date(e)).valueOf()),e}},e.TimeImpl=r,e.TimeAxisBase=e.Base.create("timeAxisBase",e.AxisBase,[e.TimeImpl])},"3.17.2",{requires:["axis-base"]});
diff --git a/js/yui3/axis-time/axis-time-min.js b/js/yui3/axis-time/axis-time-min.js
new file mode 100644
index 000000000..ab80108ef
--- /dev/null
+++ b/js/yui3/axis-time/axis-time-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("axis-time",function(e,t){e.TimeAxis=e.Base.create("timeAxis",e.Axis,[e.TimeImpl],{_getLabelByIndex:function(e,t){var n=this.get("minimum"),r=this.get("maximum"),i,s;return t-=1,i=(r-n)/t*e,s=n+i,s},_getLabelData:function(e,t,n,r,i,s,o,u,a){var f,l,c=[],h=[],p,d=s;a=a||this._getDataValuesByCount(u,r,i);for(l=0;l<u;l+=1)f=this._getNumber(a[l]),f<=i&&f>=r&&(p={},p[t]=e,p[n]=this._getCoordFromValue(r,i,o,f,d),c.push(p),h.push(f));return{points:c,values:h}}})},"3.17.2",{requires:["axis","axis-time-base"]});
diff --git a/js/yui3/axis/axis-min.js b/js/yui3/axis/axis-min.js
new file mode 100644
index 000000000..835a1a635
--- /dev/null
+++ b/js/yui3/axis/axis-min.js
@@ -0,0 +1,12 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("axis",function(e,t){var n=e.config,r=n.doc,i=e.Lang,s=i.isString,o=e.DOM,u,a,f,l;u=function(){},u.prototype={_getDefaultMargins:function(){return{top:0,left:0,right:4,bottom:0}},setTickOffsets:function(){var e=this,t=e.get("styles").majorTicks,n=t.length,r=n*.5,i=t.display;e.set("topTickOffset",0),e.set("bottomTickOffset",0);switch(i){case"inside":e.set("rightTickOffset",n),e.set("leftTickOffset",0);break;case"outside":e.set("rightTickOffset",0),e.set("leftTickOffset",n);break;case"cross":e.set("rightTickOffset",r),e.set("leftTickOffset",r);break;default:e.set("rightTickOffset",0),e.set("leftTickOffset",0)}},drawTick:function(e,t,n){var r=this,i=r.get("styles"),s=i.padding,o=n.length,u={x:s.left,y:t.y},a={x:o+s.left,y:t.y};r.drawLine(e,u,a)},getLineStart:function(){var e=this.get("styles"),t=e.padding,n=e.majorTicks,r=n.length,i=n.display,s={x:t.left,y:0};return i==="outside"?s.x+=r:i==="cross"&&(s.x+=r/2),s},getLabelPoint:function(e){return{x:e.x-this.get("leftTickOffset"),y:e.y}},updateMaxLabelSize:function(e,t){var n=this,r=this._labelRotationProps,i=r.rot,s=r.absRot,o=r.sinRadians,u=r.cosRadians,a;i===0?a=e:s===90?a=t:a=u*e+o*t,n._maxLabelSize=Math.max(n._maxLabelSize,a)},getExplicitlySized:function(e){if(this._explicitWidth){var t=this,n=t._explicitWidth,r=t._totalTitleSize,i=t.get("leftTickOffset"),s=e.label.margin.right;return t._maxLabelSize=n-(i+s+r),!0}return!1},positionTitle:function(e){var t=this,n=t._titleBounds,r=t.get("styles").title.margin,i=t._titleRotationProps,s=n.right-n.left,o=e.offsetWidth,u=e.offsetHeight,a=o*-0.5+s*.5,f=t.get("height")*.5-u*.5;i.labelWidth=o,i.labelHeight=u,r&&r.left&&(a+=r.left),i.x=a,i.y=f,i.transformOrigin=[.5,.5],t._rotate(e,i)},positionLabel:function(e,t,n,r){var i=this,s=parseFloat(n.label.offset),o=i.get("leftTickOffset"),u=this._totalTitleSize,a=t.x+u-o,f=t.y,l=this._labelRotationProps,c=l.rot,h=l.absRot,p=i._maxLabelSize,d=this._labelWidths[r],v=this._labelHeights[r];c===0?(a-=d,f-=v*s):c===90?(a-=d*.5,f=f+d/2-d*s):c===-90?(a-=d*.5,f=f-v+d/2-d*s):(a-=d+v*h/360,f-=v*s),l.labelWidth=d,l.labelHeight=v,l.x=Math.round(p+a),l.y=Math.round(f),this._rotate(e,l)},_setRotationCoords:function(e){var t=e.rot,n=e.absRot,r,i,s=e.labelWidth,o=e.labelHeight;t===0?(r=s,i=o*.5):t===90?(i=0,r=s*.5):t===-90?(r=s*.5,i=o):(r=s+o*n/360,i=o*.5),e.x-=r,e.y-=i},_getTransformOrigin:function(e){var t;return e===0?t=[0,0]:e===90?t=[.5,0]:e===-90?t=[.5,1]:t=[1,.5],t},offsetNodeForTick:function(){},setCalculatedSize:function(){var e=this,t=this.get("graphic"),n=e.get("styles"),r=n.label,i=e.get("leftTickOffset"),s=e._maxLabelSize,o=this._totalTitleSize,u=Math.round(o+i+s+r.margin.right);this._explicitWidth&&(u=this._explicitWidth),this.set("calculatedWidth",u),t.set("x",u-i)}},e.LeftAxisLayout=u,a=function(){},a.prototype={_getDefaultMargins:function(){return{top:0,left:4,right:0,bottom:0}},setTickOffsets:function(){var e=this,t=e.get("styles").majorTicks,n=t.length,r=n*.5,i=t.display;e.set("topTickOffset",0),e.set("bottomTickOffset",0);switch(i){case"inside":e.set("leftTickOffset",n),e.set("rightTickOffset",0);break;case"outside":e.set("leftTickOffset",0),e.set("rightTickOffset",n);break;case"cross":e.set("rightTickOffset",r),e.set("leftTickOffset",r);break;default:e.set("leftTickOffset",0),e.set("rightTickOffset",0)}},drawTick:function(e,t,n){var r=this,i=r.get("styles"),s=i.padding,o=n.length,u={x:s.left,y:t.y},a={x:s.left+o,y:t.y};r.drawLine(e,u,a)},getLineStart:function(){var e=this,t=e.get("styles"),n=t.padding,r=t.majorTicks,i=r.length,s=r.display,o={x:n.left,y:n.top};return s==="inside"?o.x+=i:s==="cross"&&(o.x+=i/2),o},getLabelPoint:function(e){return{x:e.x+this.get("rightTickOffset"),y:e.y}},updateMaxLabelSize:function(e,t){var n=this,r=this._labelRotationProps,i=r.rot,s=r.absRot,o=r.sinRadians,u=r.cosRadians,a;i===0?a=e:s===90?a=t:a=u*e+o*t,n._maxLabelSize=Math.max(n._maxLabelSize,a)},getExplicitlySized:function(e){if(this._explicitWidth){var t=this,n=t._explicitWidth,r=this._totalTitleSize,i=t.get("rightTickOffset"),s=e.label.margin.right;return t._maxLabelSize=n-(i+s+r),!0}return!1},positionTitle:function(e){var t=this,n=t._titleBounds,r=t.get("styles").title.margin,i=t._titleRotationProps,s=e.offsetWidth,o=e.offsetHeight,u=n.right-n.left,a=this.get("width")-s*.5-u*.5,f=t.get("height")*.5-o*.5;i.labelWidth=s,i.labelHeight=o,r&&r.right&&(a-=r.left),i.x=a,i.y=f,i.transformOrigin=[.5,.5],t._rotate(e,i)},positionLabel:function(e,t,n,r){var i=this,s=parseFloat(n.label.offset),o=i.get("rightTickOffset"),u=n.label,a=0,f=t.x,l=t.y,c=this._labelRotationProps,h=c.rot,p=c.absRot,d=this._labelWidths[r],v=this._labelHeights[r];u.margin&&u.margin.left&&(a=u.margin.left),h===0?l-=v*s:h===90?(f-=d*.5,l=l-v+d/2-d*s):h===-90?(l=l+d/2-d*s,f-=d*.5):(l-=v*s,f+=v/2*p/90),f+=a,f+=o,c.labelWidth=d,c.labelHeight=v,c.x=Math.round(f),c.y=Math.round(l),this._rotate(e,c)},_setRotationCoords:function(e){var t=e.rot,n=e.absRot,r=0,i=0,s=e.labelWidth,o=e.labelHeight;t===0?i=o*.5:t===90?(r=s*.5,i=o):t===-90?r=s*.5:(i=o*.5,r=o/2*n/90),e.x-=r,e.y-=i},_getTransformOrigin:function(e){var t;return e===0?t=[0,0]:e===90?t=[.5,1]:e===-90?t=[.5,0]:t=[0,.5],t},offsetNodeForTick:function(e){var t=this,n=t.get("leftTickOffset"),r=0-n;e.setStyle("left",r)},setCalculatedSize:function(){var e=this,t=e.get("styles"),n=t.label,r=this._totalTitleSize,i=Math.round(e.get("rightTickOffset")+e._maxLabelSize+r+n.margin.left);this._explicitWidth&&(i=this._explicitWidth),e.set("calculatedWidth",i),e.get("contentBox").setStyle("width",i)}},e.RightAxisLayout=a,f=function(){},f.prototype={_getDefaultMargins:function(){return{top:4,left:0,right:0,bottom:0}},setTickOffsets:function(){var e=this,t=e.get("styles").majorTicks,n=t.length,r=n*.5,i=t.display;e.set("leftTickOffset",0),e.set("rightTickOffset",0);switch(i){case"inside":e.set("topTickOffset",n),e.set("bottomTickOffset",0);break;case"outside":e.set("topTickOffset",0),e.set("bottomTickOffset",n);break;case"cross":e.set("topTickOffset",r),e.set("bottomTickOffset"
+,r);break;default:e.set("topTickOffset",0),e.set("bottomTickOffset",0)}},getLineStart:function(){var e=this.get("styles"),t=e.padding,n=e.majorTicks,r=n.length,i=n.display,s={x:0,y:t.top};return i==="inside"?s.y+=r:i==="cross"&&(s.y+=r/2),s},drawTick:function(e,t,n){var r=this,i=r.get("styles"),s=i.padding,o=n.length,u={x:t.x,y:s.top},a={x:t.x,y:o+s.top};r.drawLine(e,u,a)},getLabelPoint:function(e){return{x:e.x,y:e.y+this.get("bottomTickOffset")}},updateMaxLabelSize:function(e,t){var n=this,r=this._labelRotationProps,i=r.rot,s=r.absRot,o=r.sinRadians,u=r.cosRadians,a;i===0?a=t:s===90?a=e:a=o*e+u*t,n._maxLabelSize=Math.max(n._maxLabelSize,a)},getExplicitlySized:function(e){if(this._explicitHeight){var t=this,n=t._explicitHeight,r=t._totalTitleSize,i=t.get("bottomTickOffset"),s=e.label.margin.right;return t._maxLabelSize=n-(i+s+r),!0}return!1},positionTitle:function(e){var t=this,n=t._titleBounds,r=t.get("styles").title.margin,i=t._titleRotationProps,s=n.bottom-n.top,o=e.offsetWidth,u=e.offsetHeight,a=t.get("width")*.5-o*.5,f=t.get("height")-u/2-s/2;i.labelWidth=o,i.labelHeight=u,r&&r.bottom&&(f-=r.bottom),i.x=a,i.y=f,i.transformOrigin=[.5,.5],t._rotate(e,i)},positionLabel:function(e,t,n,r){var i=this,s=parseFloat(n.label.offset),o=i.get("bottomTickOffset"),u=n.label,a=0,f=i._labelRotationProps,l=f.rot,c=f.absRot,h=Math.round(t.x),p=Math.round(t.y),d=i._labelWidths[r],v=i._labelHeights[r];u.margin&&u.margin.top&&(a=u.margin.top),l===90?(p-=v/2*l/90,h=h+v/2-v*s):l===-90?(p-=v/2*c/90,h=h-d+v/2-v*s):l>0?(h=h+v/2-v*s,p-=v/2*l/90):l<0?(h=h-d+v/2-v*s,p-=v/2*c/90):h-=d*s,p+=a,p+=o,f.labelWidth=d,f.labelHeight=v,f.x=h,f.y=p,i._rotate(e,f)},_setRotationCoords:function(e){var t=e.rot,n=e.absRot,r=e.labelWidth,i=e.labelHeight,s,o;t>0?(s=0,o=i/2*t/90):t<0?(s=r,o=i/2*n/90):(s=r*.5,o=0),e.x-=s,e.y-=o},_getTransformOrigin:function(e){var t;return e>0?t=[0,.5]:e<0?t=[1,.5]:t=[0,0],t},offsetNodeForTick:function(e){var t=this;e.setStyle("top",0-t.get("topTickOffset"))},setCalculatedSize:function(){var e=this,t=e.get("styles"),n=t.label,r=e._totalTitleSize,i=Math.round(e.get("bottomTickOffset")+e._maxLabelSize+n.margin.top+r);e._explicitHeight&&(i=e._explicitHeight),e.set("calculatedHeight",i)}},e.BottomAxisLayout=f,l=function(){},l.prototype={_getDefaultMargins:function(){return{top:0,left:0,right:0,bottom:4}},setTickOffsets:function(){var e=this,t=e.get("styles").majorTicks,n=t.length,r=n*.5,i=t.display;e.set("leftTickOffset",0),e.set("rightTickOffset",0);switch(i){case"inside":e.set("bottomTickOffset",n),e.set("topTickOffset",0);break;case"outside":e.set("bottomTickOffset",0),e.set("topTickOffset",n);break;case"cross":e.set("topTickOffset",r),e.set("bottomTickOffset",r);break;default:e.set("topTickOffset",0),e.set("bottomTickOffset",0)}},getLineStart:function(){var e=this,t=e.get("styles"),n=t.padding,r=t.majorTicks,i=r.length,s=r.display,o={x:0,y:n.top};return s==="outside"?o.y+=i:s==="cross"&&(o.y+=i/2),o},drawTick:function(e,t,n){var r=this,i=r.get("styles"),s=i.padding,o=n.length,u={x:t.x,y:s.top},a={x:t.x,y:o+s.top};r.drawLine(e,u,a)},getLabelPoint:function(e){return{x:e.x,y:e.y-this.get("topTickOffset")}},updateMaxLabelSize:function(e,t){var n=this,r=this._labelRotationProps,i=r.rot,s=r.absRot,o=r.sinRadians,u=r.cosRadians,a;i===0?a=t:s===90?a=e:a=o*e+u*t,n._maxLabelSize=Math.max(n._maxLabelSize,a)},getExplicitlySized:function(e){if(this._explicitHeight){var t=this,n=t._explicitHeight,r=t._totalTitleSize,i=t.get("topTickOffset"),s=e.label.margin.right;return t._maxLabelSize=n-(i+s+r),!0}return!1},positionTitle:function(e){var t=this,n=t._titleBounds,r=t.get("styles").title.margin,i=t._titleRotationProps,s=e.offsetWidth,o=e.offsetHeight,u=n.bottom-n.top,a=t.get("width")*.5-s*.5,f=u/2-o/2;i.labelWidth=s,i.labelHeight=o,r&&r.top&&(f+=r.top),i.x=a,i.y=f,i.transformOrigin=[.5,.5],t._rotate(e,i)},positionLabel:function(e,t,n,r){var i=this,s=parseFloat(n.label.offset),o=this._totalTitleSize,u=i._maxLabelSize,a=t.x,f=t.y+o+u,l=this._labelRotationProps,c=l.rot,h=l.absRot,p=this._labelWidths[r],d=this._labelHeights[r];c===0?(a-=p*s,f-=d):c===90?(a=a-p+d/2-d*s,f-=d*.5):c===-90?(a=a+d/2-d*s,f-=d*.5):c>0?(a=a-p+d/2-d*s,f-=d-d*c/180):(a=a+d/2-d*s,f-=d-d*h/180),l.x=Math.round(a),l.y=Math.round(f),l.labelWidth=p,l.labelHeight=d,this._rotate(e,l)},_setRotationCoords:function(e){var t=e.rot,n=e.absRot,r=e.labelWidth,i=e.labelHeight,s,o;t===0?(s=r*.5,o=i):t===90?(s=r,o=i*.5):t===-90?o=i*.5:t>0?(s=r,o=i-i*t/180):o=i-i*n/180,e.x-=s,e.y-=o},_getTransformOrigin:function(e){var t;return e===0?t=[0,0]:e===90?t=[1,.5]:e===-90?t=[0,.5]:e>0?t=[1,.5]:t=[0,.5],t},offsetNodeForTick:function(){},setCalculatedSize:function(){var e=this,t=e.get("graphic"),n=e.get("styles"),r=n.label.margin,i=r.bottom+e._maxLabelSize,s=e._totalTitleSize,o=this.get("topTickOffset"),u=Math.round(o+i+s);this._explicitHeight&&(u=this._explicitHeight),e.set("calculatedHeight",u),t.set("y",u-o)}},e.TopAxisLayout=l,e.Axis=e.Base.create("axis",e.Widget,[e.AxisBase],{getLabelByIndex:function(e,t){var n=this.get("position"),r=n==="left"||n==="right"?"vertical":"horizontal";return this._getLabelByIndex(e,t,r)},bindUI:function(){this.after("dataReady",e.bind(this._dataChangeHandler,this)),this.after("dataUpdate",e.bind(this._dataChangeHandler,this)),this.after("stylesChange",this._updateHandler),this.after("overlapGraphChange",this._updateHandler),this.after("positionChange",this._positionChangeHandler),this.after("widthChange",this._handleSizeChange),this.after("heightChange",this._handleSizeChange),this.after("calculatedWidthChange",this._handleSizeChange),this.after("calculatedHeightChange",this._handleSizeChange)},_calculatedWidth:0,_calculatedHeight:0,_dataChangeHandler:function(){this.get("rendered")&&this._drawAxis()},_positionChangeHandler:function(e){this._updateGraphic(e.newVal),this._updateHandler()},_updateGraphic:function(e){var t=this.get("graphic");e==="none"?t&&t.destroy():t||this._setCanvas()},_updateHandler:function(){this.get("rendered")&&this.
+_drawAxis()},renderUI:function(){this._updateGraphic(this.get("position"))},syncUI:function(){var e=this._layout,t,n,r,i,s;if(e){t=e._getDefaultMargins(),n=this.get("styles"),r=n.label.margin,i=n.title.margin;for(s in t)t.hasOwnProperty(s)&&(r[s]=r[s]===undefined?t[s]:r[s],i[s]=i[s]===undefined?t[s]:i[s])}this._drawAxis()},_setCanvas:function(){var t=this.get("contentBox"),n=this.get("boundingBox"),r=this.get("position"),i=this._parentNode,s=this.get("width"),o=this.get("height");n.setStyle("position","absolute"),n.setStyle("zIndex",2),s=s?s+"px":i.getStyle("width"),o=o?o+"px":i.getStyle("height"),r==="top"||r==="bottom"?t.setStyle("width",s):t.setStyle("height",o),t.setStyle("position","relative"),t.setStyle("left","0px"),t.setStyle("top","0px"),this.set("graphic",new e.Graphic),this.get("graphic").render(t)},_getDefaultStyles:function(){var t={majorTicks:{display:"inside",length:4,color:"#dad8c9",weight:1,alpha:1},minorTicks:{display:"none",length:2,color:"#dad8c9",weight:1},line:{weight:1,color:"#dad8c9",alpha:1},majorUnit:{determinant:"count",count:11,distance:75},top:"0px",left:"0px",width:"100px",height:"100px",label:{color:"#808080",alpha:1,fontSize:"85%",rotation:0,offset:.5,margin:{top:undefined,right:undefined,bottom:undefined,left:undefined}},title:{color:"#808080",alpha:1,fontSize:"85%",rotation:undefined,margin:{top:undefined,right:undefined,bottom:undefined,left:undefined}},hideOverlappingLabelTicks:!1};return e.merge(e.Renderer.prototype._getDefaultStyles(),t)},_handleSizeChange:function(e){var t=e.attrName,n=this.get("position"),r=n==="left"||n==="right",i=this.get("contentBox"),s=n==="bottom"||n==="top";i.setStyle("width",this.get("width")),i.setStyle("height",this.get("height")),(s&&t==="width"||r&&t==="height")&&this._drawAxis()},_layoutClasses:{top:l,bottom:f,left:u,right:a},drawLine:function(e,t,n){e.moveTo(t.x,t.y),e.lineTo(n.x,n.y)},_getTextRotationProps:function(e){if(e.rotation===undefined)switch(this.get("position")){case"left":e.rotation=-90;break;case"right":e.rotation=90;break;default:e.rotation=0}var t=Math.min(90,Math.max(-90,e.rotation)),n=Math.abs(t),r=Math.PI/180,i=parseFloat(parseFloat(Math.sin(n*r)).toFixed(8)),s=parseFloat(parseFloat(Math.cos(n*r)).toFixed(8));return{rot:t,absRot:n,radCon:r,sinRadians:i,cosRadians:s,textAlpha:e.alpha}},_drawAxis:function(){if(this._drawing){this._callLater=!0;return}this._drawing=!0,this._callLater=!1;if(this._layout){var e=this.get("styles"),t=e.line,n=e.label,r=e.majorTicks,i=r.display!=="none",s,o=0,u=this._layout,a,f,l,c,h,p=this.get("labelFunction"),d=this.get("labelFunctionScope"),v=this.get("labelFormat"),m=this.get("graphic"),g=this.get("path"),y,b,w=this.get("position"),E,S,x,T,N,C,k,L,A,O,M,_=this._labelValuesExplicitlySet?this.get("labelValues"):null,D=w==="left"||w==="right"?"vertical":"horizontal";this._labelWidths=[],this._labelHeights=[],m.set("autoDraw",!1),g.clear(),g.set("stroke",{weight:t.weight,color:t.color,opacity:t.alpha}),this._labelRotationProps=this._getTextRotationProps(n),this._labelRotationProps.transformOrigin=u._getTransformOrigin(this._labelRotationProps.rot),u.setTickOffsets.apply(this),a=this.getLength(),s=this.getTotalMajorUnits(),M=this.getEdgeOffset(s,a),this.set("edgeOffset",M),f=u.getLineStart.apply(this),D==="vertical"?(A="x",O="y"):(A="y",O="x"),E=this._getLabelData(f[A],A,O,this.get("minimum"),this.get("maximum"),M,a-M-M,s,_),T=E.points,S=E.values,s=T.length,this._labelValuesExplicitlySet||this.set("labelValues",S,{src:"internal"}),this.get("hideFirstMajorUnit")&&(N=T.shift(),k=S.shift(),s-=1),this.get("hideLastMajorUnit")&&(C=T.pop(),L=S.pop(),s-=1);if(s<1)this._clearLabelCache();else{this.drawLine(g,f,this.getLineEnd(f));if(i){y=this.get("tickPath"),y.clear(),y.set("stroke",{weight:r.weight,color:r.color,opacity:r.alpha});for(o=0;o<s;o+=1)x=T[o],x&&u.drawTick.apply(this,[y,T[o],r])}this._createLabelCache(),this._maxLabelSize=0,this._totalTitleSize=0,this._titleSize=0,this._setTitle(),b=u.getExplicitlySized.apply(this,[e]);for(o=0;o<s;o+=1)x=T[o],x&&(l=this.getLabel(n),this._labels.push(l),this.get("appendLabelFunction")(l,p.apply(d,[S[o],v])),c=Math.round(l.offsetWidth),h=Math.round(l.offsetHeight),b||this._layout.updateMaxLabelSize.apply(this,[c,h]),this._labelWidths.push(c),this._labelHeights.push(h));this._clearLabelCache(),this.get("overlapGraph")&&u.offsetNodeForTick.apply(this,[this.get("contentBox")]),u.setCalculatedSize.apply(this),this._titleTextField&&this._layout.positionTitle.apply(this,[this._titleTextField]),s=this._labels.length;for(o=0;o<s;++o)u.positionLabel.apply(this,[this.get("labels")[o],T[o],e,o]);N&&T.unshift(N),C&&T.push(C),k&&S.unshift(k),L&&S.push(L),this._tickPoints=T}}this._drawing=!1,this._callLater?this._drawAxis():(this._updatePathElement(),this.fire("axisRendered"))},_setTotalTitleSize:function(t){var n=this._titleTextField,r=n.offsetWidth,i=n.offsetHeight,s=this._titleRotationProps.rot,o,u,a=t.margin,f=this.get("position"),l=new e.Matrix;l.rotate(s),o=l.getContentRect(r,i),f==="left"||f==="right"?(u=o.right-o.left,a&&(u+=a.left+a.right)):(u=o.bottom-o.top,a&&(u+=a.top+a.bottom)),this._titleBounds=o,this._totalTitleSize=u},_updatePathElement:function(){var e=this._path,t=this._tickPath,n=!1,r=this.get("graphic");e&&(n=!0,e.end()),t&&(n=!0,t.end()),n&&r._redraw()},_setTitle:function(){var e,t,n,i=this.get("title"),s=this._titleTextField,o;if(i!==null&&i!==undefined){n={rotation:"rotation",margin:"margin",alpha:"alpha"},t=this.get("styles").title,s?r.createElementNS||s.style.filter&&(s.style.filter=null):(s=r.createElement("span"),s.style.display="block",s.style.whiteSpace="nowrap",s.setAttribute("class","axisTitle"),this.get("contentBox").append(s)),s.style.position="absolute";for(e in t)t.hasOwnProperty(e)&&!n.hasOwnProperty(e)&&(s.style[e]=t[e]);this.get("appendTitleFunction")(s,i),this._titleTextField=s,this._titleRotationProps=this._getTextRotationProps(t),this._setTotalTitleSize(t)}else s&&(o=s.parentNode,o&&o.removeChild(s),this._titleTextField=
+null,this._totalTitleSize=0)},getLabel:function(t){var n,i,s=this._labelCache,o={rotation:"rotation",margin:"margin",alpha:"alpha"};s&&s.length>0?i=s.shift():(i=r.createElement("span"),i.className=e.Lang.trim([i.className,"axisLabel"].join(" ")),this.get("contentBox").append(i)),r.createElementNS||i.style.filter&&(i.style.filter=null),i.style.display="block",i.style.whiteSpace="nowrap",i.style.position="absolute";for(n in t)t.hasOwnProperty(n)&&!o.hasOwnProperty(n)&&(i.style[n]=t[n]);return i},_createLabelCache:function(){if(this._labels)while(this._labels.length>0)this._labelCache.push(this._labels.shift());else this._clearLabelCache();this._labels=[]},_clearLabelCache:function(){if(this._labelCache){var t=this._labelCache.length,n=0,r;for(;n<t;++n)r=this._labelCache[n],this._removeChildren(r),e.Event.purgeElement(r,!0),r.parentNode.removeChild(r)}this._labelCache=[]},getLineEnd:function(e){var t=this.get("width"),n=this.get("height"),r=this.get("position");return r==="top"||r==="bottom"?{x:t,y:e.y}:{x:e.x,y:n}},getLength:function(){var e,t=this.get("styles"),n=t.padding,r=this.get("width"),i=this.get("height"),s=this.get("position");return s==="top"||s==="bottom"?e=r-(n.left+n.right):e=i-(n.top+n.bottom),e},getFirstPoint:function(e){var t=this.get("styles"),n=this.get("position"),r=t.padding,i={x:e.x,y:e.y};return n==="top"||n==="bottom"?i.x+=r.left+this.get("edgeOffset"):i.y+=this.get("height")-(r.top+this.get("edgeOffset")),i},_rotate:function(t,n){var s=n.rot,u=n.x,a=n.y,f,l,c=new e.Matrix,h=n.transformOrigin||[0,0],p;r.createElementNS?(c.translate(u,a),c.rotate(s),o.setStyle(t,"transformOrigin",h[0]*100+"% "+h[1]*100+"%"),o.setStyle(t,"transform",c.toCSSText())):(l=n.textAlpha,i.isNumber(l)&&l<1&&l>-1&&!isNaN(l)&&(f="progid:DXImageTransform.Microsoft.Alpha(Opacity="+Math.round(l*100)+")"),s!==0?(c.rotate(s),p=c.getContentRect(n.labelWidth,n.labelHeight),c.init(),c.translate(p.left,p.top),c.translate(u,a),this._simulateRotateWithTransformOrigin(c,s,h,n.labelWidth,n.labelHeight),f?f+=" ":f="",f+=c.toFilterText(),t.style.left=c.dx+"px",t.style.top=c.dy+"px"):(t.style.left=u+"px",t.style.top=a+"px"),f&&(t.style.filter=f))},_simulateRotateWithTransformOrigin:function(e,t,n,r,i){var s=n[0]*r,o=n[1]*i;s=isNaN(s)?0:s,o=isNaN(o)?0:o,e.translate(s,o),e.rotate(t),e.translate(-s,-o)},getMaxLabelBounds:function(){return this._getLabelBounds(this.getMaximumValue())},getMinLabelBounds:function(){return this._getLabelBounds(this.getMinimumValue())},_getLabelBounds:function(t){var n=this._layout,r=this.get("styles").label,i=new e.Matrix,s,o=this._getTextRotationProps(r);return o.transformOrigin=n._getTransformOrigin(o.rot),s=this.getLabel(r),this.get("appendLabelFunction")(s,this.get("labelFunction").apply(this,[t,this.get("labelFormat")])),o.labelWidth=s.offsetWidth,o.labelHeight=s.offsetHeight,this._removeChildren(s),e.Event.purgeElement(s,!0),s.parentNode.removeChild(s),o.x=0,o.y=0,n._setRotationCoords(o),i.translate(o.x,o.y),this._simulateRotateWithTransformOrigin(i,o.rot,o.transformOrigin,o.labelWidth,o.labelHeight),i.getContentRect(o.labelWidth,o.labelHeight)},_removeChildren:function(e){if(e.hasChildNodes()){var t;while(e.firstChild)t=e.firstChild,this._removeChildren(t),e.removeChild(t)}},destructor:function(){var e=this.get("contentBox").getDOMNode(),t=this.get("labels"),n=this.get("graphic"),r,i=t?t.length:0;if(i>0)while(t.length>0)r=t.shift(),this._removeChildren(r),e.removeChild(r),r=null;n&&n.destroy()},_maxLabelSize:0,_setText:function(e,t){e.innerHTML="",i.isNumber(t)?t+="":t||(t=""),s(t)&&(t=r.createTextNode(t)),e.appendChild(t)},getTotalMajorUnits:function(){var e,t=this.get("styles").majorUnit,n;return t.determinant==="count"?e=t.count:t.determinant==="distance"&&(n=this.getLength(),e=n/t.distance+1),e},getMajorUnitDistance:function(e,t,n){var r;return n.determinant==="count"?(this.get("calculateEdgeOffset")||(e-=1),r=t/e):n.determinant==="distance"&&(r=n.distance),r},_hasDataOverflow:function(){return this.get("setMin")||this.get("setMax")?!0:!1},getMinimumValue:function(){return this.get("minimum")},getMaximumValue:function(){return this.get("maximum")}},{ATTRS:{width:{lazyAdd:!1,getter:function(){return this._explicitWidth?this._explicitWidth:this._calculatedWidth},setter:function(e){return this._explicitWidth=e,e}},height:{lazyAdd:!1,getter:function(){return this._explicitHeight?this._explicitHeight:this._calculatedHeight},setter:function(e){return this._explicitHeight=e,e}},calculatedWidth:{getter:function(){return this._calculatedWidth},setter:function(e){return this._calculatedWidth=e,e}},calculatedHeight:{getter:function(){return this._calculatedHeight},setter:function(e){return this._calculatedHeight=e,e}},edgeOffset:{value:0},graphic:{},path:{readOnly:!0,getter:function(){if(!this._path){var e=this.get("graphic");e&&(this._path=e.addShape({type:"path"}))}return this._path}},tickPath:{readOnly:!0,getter:function(){if(!this._tickPath){var e=this.get("graphic");e&&(this._tickPath=e.addShape({type:"path"}))}return this._tickPath}},node:{},position:{lazyAdd:!1,setter:function(e){var t=this._layoutClasses[e];return e&&e!=="none"&&(this._layout=new t),e}},topTickOffset:{value:0},bottomTickOffset:{value:0},leftTickOffset:{value:0},rightTickOffset:{value:0},labels:{readOnly:!0,getter:function(){return this._labels}},tickPoints:{readOnly:!0,getter:function(){return this.get("position")==="none"?this.get("styles").majorUnit.count:this._tickPoints}},overlapGraph:{value:!0,validator:function(e){return i.isBoolean(e)}},maxLabelSize:{getter:function(){return this._maxLabelSize},setter:function(e){return this._maxLabelSize=e,e}},title:{value:null},appendLabelFunction:{valueFn:function(){return this._setText}},appendTitleFunction:{valueFn:function(){return this._setText}},labelValues:{lazyAdd:!1,setter:function(e){var t=arguments[2];return!e||t&&t.src&&t.src==="internal"?this._labelValuesExplicitlySet=!1:this._labelValuesExplicitlySet=!0,e}},hideFirstMajorUnit:{value:!1},hideLastMajorUnit
+:{value:!1}}}),e.AxisType=e.Base.create("baseAxis",e.Axis,[],{})},"3.17.2",{requires:["dom","widget","widget-position","widget-stack","graphics","axis-base"]});
diff --git a/js/yui3/base-base/base-base-min.js b/js/yui3/base-base/base-base-min.js
new file mode 100644
index 000000000..f96271e24
--- /dev/null
+++ b/js/yui3/base-base/base-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("base-base",function(e,t){function o(){i.apply(this,arguments),s.apply(this,arguments),r.apply(this,arguments)}var n=e.AttributeCore,r=e.AttributeExtras,i=e.BaseCore,s=e.BaseObservable;o._ATTR_CFG=i._ATTR_CFG.concat(s._ATTR_CFG),o._NON_ATTRS_CFG=i._NON_ATTRS_CFG.concat(s._NON_ATTRS_CFG),o.NAME="base",o.ATTRS=n.protectAttrs(i.ATTRS),o.modifyAttrs=i.modifyAttrs,e.mix(o,i,!1,null,1),e.mix(o,r,!1,null,1),e.mix(o,s,!0,null,1),o.prototype.constructor=o,e.Base=o},"3.17.2",{requires:["attribute-base","base-core","base-observable"]});
diff --git a/js/yui3/base-build/base-build-min.js b/js/yui3/base-build/base-build-min.js
new file mode 100644
index 000000000..08e26b7bb
--- /dev/null
+++ b/js/yui3/base-build/base-build-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("base-build",function(e,t){function f(e,t,n){n[e]&&(t[e]=(t[e]||[]).concat(n[e]))}function l(e,t,n){n._ATTR_CFG&&(t._ATTR_CFG_HASH=null,f.apply(null,arguments))}function c(e,t,r){n.modifyAttrs(t,r.ATTRS)}var n=e.BaseCore,r=e.Base,i=e.Lang,s="initializer",o="destructor",u=["_PLUG","_UNPLUG"],a;r._build=function(t,n,i,u,a,f){var l=r._build,c=l._ctor(n,f),h=l._cfg(n,f,i),p=l._mixCust,d=c._yuibuild.dynamic,v,m,g,y,b,w;for(v=0,m=i.length;v<m;v++)g=i[v],y=g.prototype,b=y[s],w=y[o],delete y[s],delete y[o],e.mix(c,g,!0,null,1),p(c,g,h),b&&(y[s]=b),w&&(y[o]=w),c._yuibuild.exts.push(g);return u&&e.mix(c.prototype,u,!0),a&&(e.mix(c,l._clean(a,h),!0),p(c,a,h)),c.prototype.hasImpl=l._impl,d&&(c.NAME=t,c.prototype.constructor=c,c.modifyAttrs=n.modifyAttrs),c},a=r._build,e.mix(a,{_mixCust:function(t,n,r){var s,o,u,a,f,l;r&&(s=r.aggregates,o=r.custom,u=r.statics),u&&e.mix(t,n,!0,u);if(s)for(l=0,f=s.length;l<f;l++)a=s[l],!t.hasOwnProperty(a)&&n.hasOwnProperty(a)&&(t[a]=i.isArray(n[a])?[]:{}),e.aggregate(t,n,!0,[a]);if(o)for(l in o)o.hasOwnProperty(l)&&o[l](l,t,n)},_tmpl:function(t){function n(){n.superclass.constructor.apply(this,arguments)}return e.extend(n,t),n},_impl:function(e){var t=this._getClasses(),n,r,i,s,o,u;for(n=0,r=t.length;n<r;n++){i=t[n];if(i._yuibuild){s=i._yuibuild.exts,o=s.length;for(u=0;u<o;u++)if(s[u]===e)return!0}}return!1},_ctor:function(e,t){var n=t&&!1===t.dynamic?!1:!0,r=n?a._tmpl(e):e,i=r._yuibuild;return i||(i=r._yuibuild={}),i.id=i.id||null,i.exts=i.exts||[],i.dynamic=n,r},_cfg:function(t,n,r){var i=[],s={},o=[],u,a=n&&n.aggregates,f=n&&n.custom,l=n&&n.statics,c=t,h,p;while(c&&c.prototype)u=c._buildCfg,u&&(u.aggregates&&(i=i.concat(u.aggregates)),u.custom&&e.mix(s,u.custom,!0),u.statics&&(o=o.concat(u.statics))),c=c.superclass?c.superclass.constructor:null;if(r)for(h=0,p=r.length;h<p;h++)c=r[h],u=c._buildCfg,u&&(u.aggregates&&(i=i.concat(u.aggregates)),u.custom&&e.mix(s,u.custom,!0),u.statics&&(o=o.concat(u.statics)));return a&&(i=i.concat(a)),f&&e.mix(s,n.cfgBuild,!0),l&&(o=o.concat(l)),{aggregates:i,custom:s,statics:o}},_clean:function(t,n){var r,i,s,o=e.merge(t),u=n.aggregates,a=n.custom;for(r in a)o.hasOwnProperty(r)&&delete o[r];for(i=0,s=u.length;i<s;i++)r=u[i],o.hasOwnProperty(r)&&delete o[r];return o}}),r.build=function(e,t,n,r){return a(e,t,n,null,null,r)},r.create=function(e,t,n,r,i){return a(e,t,n,r,i)},r.mix=function(e,t){return e._CACHED_CLASS_DATA&&(e._CACHED_CLASS_DATA=null),a(null,e,t,null,null,{dynamic:!1})},n._buildCfg={aggregates:u.concat(),custom:{ATTRS:c,_ATTR_CFG:l,_NON_ATTRS_CFG:f}},r._buildCfg={aggregates:u.concat(),custom:{ATTRS:c,_ATTR_CFG:l,_NON_ATTRS_CFG:f}}},"3.17.2",{requires:["base-base"]});
diff --git a/js/yui3/base-core/base-core-min.js b/js/yui3/base-core/base-core-min.js
new file mode 100644
index 000000000..4ca1b4344
--- /dev/null
+++ b/js/yui3/base-core/base-core-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("base-core",function(e,t){function v(e){this._BaseInvoked||(this._BaseInvoked=!0,this._initBase(e))}var n=e.Object,r=e.Lang,i=".",s="initialized",o="destroyed",u="initializer",a="value",f=Object.prototype.constructor,l="deep",c="shallow",h="destructor",p=e.AttributeCore,d=function(e,t,n){var r;for(r in t)n[r]&&(e[r]=t[r]);return e};v._ATTR_CFG=p._ATTR_CFG.concat("cloneDefaultValue"),v._NON_ATTRS_CFG=["plugins"],v.NAME="baseCore",v.ATTRS={initialized:{readOnly:!0,value:!1},destroyed:{readOnly:!0,value:!1}},v.modifyAttrs=function(t,n){typeof t!="function"&&(n=t,t=this);var r,i,s;r=t.ATTRS||(t.ATTRS={});if(n){t._CACHED_CLASS_DATA=null;for(s in n)n.hasOwnProperty(s)&&(i=r[s]||(r[s]={}),e.mix(i,n[s],!0))}},v.prototype={_initBase:function(t){e.stamp(this),this._initAttribute(t);var n=e.Plugin&&e.Plugin.Host;this._initPlugins&&n&&n.call(this),this._lazyAddAttrs!==!1&&(this._lazyAddAttrs=!0),this.name=this.constructor.NAME,this.init.apply(this,arguments)},_initAttribute:function(){p.call(this)},init:function(e){return this._baseInit(e),this},_baseInit:function(e){this._initHierarchy(e),this._initPlugins&&this._initPlugins(e),this._set(s,!0)},destroy:function(){return this._baseDestroy(),this},_baseDestroy:function(){this._destroyPlugins&&this._destroyPlugins(),this._destroyHierarchy(),this._set(o,!0)},_getClasses:function(){return this._classes||this._initHierarchyData(),this._classes},_getAttrCfgs:function(){return this._attrs||this._initHierarchyData(),this._attrs},_getInstanceAttrCfgs:function(e){var t={},r,i,s,o,u,a,f,l=e._subAttrs,c=this._attrCfgHash();for(a in e)if(e.hasOwnProperty(a)&&a!=="_subAttrs"){f=e[a],r=t[a]=d({},f,c),i=r.value,i&&typeof i=="object"&&this._cloneDefaultValue(a,r);if(l&&l.hasOwnProperty(a)){o=e._subAttrs[a];for(u in o)s=o[u],s.path&&n.setValue(r.value,s.path,s.value)}}return t},_filterAdHocAttrs:function(e,t){var n,r=this._nonAttrs,i;if(t){n={};for(i in t)!e[i]&&!r[i]&&t.hasOwnProperty(i)&&(n[i]={value:t[i]})}return n},_initHierarchyData:function(){var e=this.constructor,t=e._CACHED_CLASS_DATA,n,r,i,s,o,u=!e._ATTR_CFG_HASH,a,f={},l=[],c=[];n=e;if(!t){while(n){l[l.length]=n,n.ATTRS&&(c[c.length]=n.ATTRS);if(u){s=n._ATTR_CFG,o=o||{};if(s)for(r=0,i=s.length;r<i;r+=1)o[s[r]]=!0}a=n._NON_ATTRS_CFG;if(a)for(r=0,i=a.length;r<i;r++)f[a[r]]=!0;n=n.superclass?n.superclass.constructor:null}u&&(e._ATTR_CFG_HASH=o),t=e._CACHED_CLASS_DATA={classes:l,nonAttrs:f,attrs:this._aggregateAttrs(c)}}this._classes=t.classes,this._attrs=t.attrs,this._nonAttrs=t.nonAttrs},_attrCfgHash:function(){return this.constructor._ATTR_CFG_HASH},_cloneDefaultValue:function(t,n){var i=n.value,s=n.cloneDefaultValue;s===l||s===!0?n.value=e.clone(i):s===c?n.value=e.merge(i):s===undefined&&(f===i.constructor||r.isArray(i))&&(n.value=e.clone(i))},_aggregateAttrs:function(e){var t,n,r,s,o,u,f=this._attrCfgHash(),l,c={};if(e)for(u=e.length-1;u>=0;--u){n=e[u];for(t in n)n.hasOwnProperty(t)&&(s=d({},n[t],f),o=null,t.indexOf(i)!==-1&&(o=t.split(i),t=o.shift()),l=c[t],o&&l&&l.value?(r=c._subAttrs,r||(r=c._subAttrs={}),r[t]||(r[t]={}),r[t][o.join(i)]={value:s.value,path:o}):o||(l?(l.valueFn&&a in s&&(l.valueFn=null),d(l,s,f)):c[t]=s))}return c},_initHierarchy:function(e){var t=this._lazyAddAttrs,n,r,i,s,o,a,f,l,c,h,p,d=[],v=this._getClasses(),m=this._getAttrCfgs(),g=v.length-1;for(o=g;o>=0;o--){n=v[o],r=n.prototype,h=n._yuibuild&&n._yuibuild.exts,r.hasOwnProperty(u)&&(d[d.length]=r.initializer);if(h)for(a=0,f=h.length;a<f;a++)l=h[a],l.apply(this,arguments),c=l.prototype,c.hasOwnProperty(u)&&(d[d.length]=c.initializer)}p=this._getInstanceAttrCfgs(m),this._preAddAttrs&&this._preAddAttrs(p,e,t),this._allowAdHocAttrs&&this.addAttrs(this._filterAdHocAttrs(m,e),e,t),this.addAttrs(p,e,t);for(i=0,s=d.length;i<s;i++)d[i].apply(this,arguments)},_destroyHierarchy:function(){var e,t,n,r,i,s,o,u,a=this._getClasses();for(n=0,r=a.length;n<r;n++){e=a[n],t=e.prototype,o=e._yuibuild&&e._yuibuild.exts;if(o)for(i=0,s=o.length;i<s;i++)u=o[i].prototype,u.hasOwnProperty(h)&&u.destructor.apply(this,arguments);t.hasOwnProperty(h)&&t.destructor.apply(this,arguments)}},toString:function(){return this.name+"["+e.stamp(this,!0)+"]"}},e.mix(v,p,!1,null,1),v.prototype.constructor=v,e.BaseCore=v},"3.17.2",{requires:["attribute-core"]});
diff --git a/js/yui3/base-observable/base-observable-min.js b/js/yui3/base-observable/base-observable-min.js
new file mode 100644
index 000000000..03502a1ff
--- /dev/null
+++ b/js/yui3/base-observable/base-observable-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("base-observable",function(e,t){function f(){}var n=e.Lang,r="destroy",i="init",s="bubbleTargets",o="_bubbleTargets",u=e.AttributeObservable,a=e.BaseCore;f._ATTR_CFG=u._ATTR_CFG.concat(),f._NON_ATTRS_CFG=["on","after","bubbleTargets"],f.prototype={_initAttribute:function(){a.prototype._initAttribute.apply(this,arguments),u.call(this),this._eventPrefix=this.constructor.EVENT_PREFIX||this.constructor.NAME,this._yuievt.config.prefix=this._eventPrefix},init:function(e){var t=this._getFullType(i),n=this._publish(t);return n.emitFacade=!0,n.fireOnce=!0,n.defaultTargetOnly=!0,n.defaultFn=this._defInitFn,this._preInitEventCfg(e),n._hasPotentialSubscribers()?this.fire(t,{cfg:e}):(this._baseInit(e),n.fired=!0,n.firedWith=[{cfg:e}]),this},_preInitEventCfg:function(e){e&&(e.on&&this.on(e.on),e.after&&this.after(e.after));var t,r,i,u=e&&s in e;if(u||o in this){i=u?e&&e.bubbleTargets:this._bubbleTargets;if(n.isArray(i))for(t=0,r=i.length;t<r;t++)this.addTarget(i[t]);else i&&this.addTarget(i)}},destroy:function(){return this.publish(r,{fireOnce:!0,defaultTargetOnly:!0,defaultFn:this._defDestroyFn}),this.fire(r),this.detachAll(),this},_defInitFn:function(e){this._baseInit(e.cfg)},_defDestroyFn:function(e){this._baseDestroy(e.cfg)}},e.mix(f,u,!1,null,1),e.BaseObservable=f},"3.17.2",{requires:["attribute-observable","base-core"]});
diff --git a/js/yui3/base-pluginhost/base-pluginhost-min.js b/js/yui3/base-pluginhost/base-pluginhost-min.js
new file mode 100644
index 000000000..d4b01e717
--- /dev/null
+++ b/js/yui3/base-pluginhost/base-pluginhost-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("base-pluginhost",function(e,t){var n=e.Base,r=e.Plugin.Host;e.mix(n,r,!1,null,1),n.plug=r.plug,n.unplug=r.unplug},"3.17.2",{requires:["base-base","pluginhost"]});
diff --git a/js/yui3/button-core/button-core-min.js b/js/yui3/button-core/button-core-min.js
new file mode 100644
index 000000000..f91f0b3e5
--- /dev/null
+++ b/js/yui3/button-core/button-core-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("button-core",function(e,t){function i(e){this.initializer(e)}var n=e.ClassNameManager.getClassName,r=e.AttributeCore;i.prototype={TEMPLATE:"<button/>",constructor:i,initializer:function(e){this._initNode(e),this._initAttributes(e),this._renderUI(e)},_initNode:function(t){t.host?this._host=e.one(t.host):this._host=e.Node.create(this.TEMPLATE)},_initAttributes:function(e){r.call(this,i.ATTRS,e)},_renderUI:function(){var e=this.getNode(),t=e.get("nodeName").toLowerCase();e.addClass(i.CLASS_NAMES.BUTTON),t!=="button"&&t!=="input"&&e.set("role","button")},enable:function(){this.set("disabled",!1)},disable:function(){this.set("disabled",!0)},getNode:function(){return this._host||(this._host=this.get("boundingBox")),this._host},_getLabel:function(){var e=this.getNode(),t=i._getTextLabelFromNode(e);return t},_getLabelHTML:function(){var e=this.getNode(),t=i._getHTMLFromNode(e);return t},_setLabel:function(t,n,r){var i=e.Escape.html(t);return(!r||r.src!=="internal")&&this.set("labelHTML",i,{src:"internal"}),i},_setLabelHTML:function(e,t,n){var r=this.getNode(),s=i._getLabelNodeFromParent(r),o=r.get("nodeName").toLowerCase();return o==="input"?s.set("value",e):s.setHTML(e),(!n||n.src!=="internal")&&this.set("label",e,{src:"internal"}),e},_setDisabled:function(e){var t=this.getNode();return t.getDOMNode().disabled=e,t.toggleClass(i.CLASS_NAMES.DISABLED,e),e}},e.mix(i.prototype,r.prototype),i.ATTRS={label:{setter:"_setLabel",getter:"_getLabel",lazyAdd:!1},labelHTML:{setter:"_setLabelHTML",getter:"_getLabelHTML",lazyAdd:!1},disabled:{value:!1,setter:"_setDisabled",lazyAdd:!1}},i.NAME="button",i.CLASS_NAMES={BUTTON:n("button"),DISABLED:n("button","disabled"),SELECTED:n("button","selected"),LABEL:n("button","label")},i.ARIA_STATES={PRESSED:"aria-pressed",CHECKED:"aria-checked"},i.ARIA_ROLES={BUTTON:"button",CHECKBOX:"checkbox",TOGGLE:"toggle"},i._getLabelNodeFromParent=function(e){var t=e.one("."+i.CLASS_NAMES.LABEL)||e;return t},i._getTextLabelFromNode=function(e){var t=i._getLabelNodeFromParent(e),n=t.get("nodeName").toLowerCase(),r=t.get(n==="input"?"value":"text");return r},i._getHTMLFromNode=function(e){var t=i._getLabelNodeFromParent(e),n=t.getHTML();return n},i._getDisabledFromNode=function(e){return e.get("disabled")},e.ButtonCore=i},"3.17.2",{requires:["attribute-core","classnamemanager","node-base","escape"]});
diff --git a/js/yui3/button-group/button-group-min.js b/js/yui3/button-group/button-group-min.js
new file mode 100644
index 000000000..5cffc8caf
--- /dev/null
+++ b/js/yui3/button-group/button-group-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("button-group",function(e,t){function s(){s.superclass.constructor.apply(this,arguments)}var n="contentBox",r="click",i=e.ButtonCore.CLASS_NAMES;e.ButtonGroup=e.extend(s,e.Widget,{renderUI:function(){this.getButtons().plug(e.Plugin.Button)},bindUI:function(){var t=this,i=t.get(n);i.delegate(r,t._handleClick,e.ButtonGroup.BUTTON_SELECTOR,t),t.after("disabledChange",t._afterDisabledChange)},_afterDisabledChange:function(t){this.getButtons().each(t.newVal?e.ButtonCore.prototype.disable:e.ButtonCore.prototype.enable)},getButtons:function(){var t=this.get(n);return t.all(e.ButtonGroup.BUTTON_SELECTOR)},getSelectedButtons:function(){var e=this,t=[],n=e.getButtons(),r=s.CLASS_NAMES.SELECTED;return n.each(function(e){e.hasClass(r)&&t.push(e)}),t},getSelectedValues:function(){var t=this.getSelectedButtons(),n=[],r;return e.Array.each(t,function(e){r=e.getContent(),n.push(r)}),n},_handleClick:function(e){var t=this,n=e.target.ancestor("."+s.CLASS_NAMES.BUTTON,!0),r=t.get("type"),i=s.CLASS_NAMES.SELECTED,o=n.hasClass(i),u;r==="checkbox"?(n.toggleClass(i,!o),t.fire("selectionChange",{originEvent:e})):r==="radio"&&!o&&(u=t.getButtons(),u.removeClass(i),n.addClass(i),t.fire("selectionChange",{originEvent:e}))}},{NAME:"buttongroup",ATTRS:{type:{writeOnce:"initOnly",value:"radio"}},CLASS_NAMES:i,BUTTON_SELECTOR:"button, input[type=button], input[type=reset], input[type=submit], input[type=radio], input[type=checkbox]"})},"3.17.2",{requires:["button-plugin","cssbutton","widget"]});
diff --git a/js/yui3/button-plugin/button-plugin-min.js b/js/yui3/button-plugin/button-plugin-min.js
new file mode 100644
index 000000000..1a21056c6
--- /dev/null
+++ b/js/yui3/button-plugin/button-plugin-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("button-plugin",function(e,t){function n(){n.superclass.constructor.apply(this,arguments)}e.extend(n,e.ButtonCore,{_afterNodeGet:function(t){var n=this.constructor.ATTRS,r=n[t]&&n[t].getter&&this[n[t].getter];if(r)return new e.Do.AlterReturn("get "+t,r.call(this))},_afterNodeSet:function(e,t){var n=this.constructor.ATTRS,r=n[e]&&n[e].setter&&this[n[e].setter];r&&r.call(this,t)},_initNode:function(t){var n=t.host;this._host=n,e.Do.after(this._afterNodeGet,n,"get",this),e.Do.after(this._afterNodeSet,n,"set",this)},destroy:function(){}},{ATTRS:e.merge(e.ButtonCore.ATTRS),NAME:"buttonPlugin",NS:"button"}),n.createNode=function(t,n){var r;return t&&!n&&!t.nodeType&&!t.getDOMNode&&typeof t!="string"&&(n=t,t=n.srcNode),n=n||{},r=n.template||e.Plugin.Button.prototype.TEMPLATE,t=t||n.srcNode||e.DOM.create(r),e.one(t).plug(e.Plugin.Button,n)},e.namespace("Plugin").Button=n},"3.17.2",{requires:["button-core","cssbutton","node-pluginhost"]});
diff --git a/js/yui3/button/button-min.js b/js/yui3/button/button-min.js
new file mode 100644
index 000000000..24afbb322
--- /dev/null
+++ b/js/yui3/button/button-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("button",function(e,t){function o(){o.superclass.constructor.apply(this,arguments)}function u(){o.superclass.constructor.apply(this,arguments)}var n=e.ButtonCore,r=n.CLASS_NAMES,i=n.ARIA_STATES,s=n.ARIA_ROLES;e.extend(o,e.Widget,{BOUNDING_TEMPLATE:n.prototype.TEMPLATE,CONTENT_TEMPLATE:null},{NAME:n.NAME,ATTRS:n.ATTRS,HTML_PARSER:{labelHTML:n._getHTMLFromNode,disabled:n._getDisabledFromNode},CLASS_NAMES:r}),e.mix(o.prototype,n.prototype),e.extend(u,o,{trigger:"click",selectedAttrName:"",initializer:function(e){var t=this,n=t.get("type"),r=n==="checkbox"?"checked":"pressed",i=e[r]||!1;t.addAttr(r,{value:i}),t.selectedAttrName=r},destructor:function(){delete this.selectedAttrName},bindUI:function(){var e=this,t=e.get("contentBox");u.superclass.bindUI.call(e),t.on(e.trigger,e.toggle,e),e.after(e.selectedAttrName+"Change",e._afterSelectedChange)},syncUI:function(){var e=this,t=e.get("contentBox"),n=e.get("type"),r=u.ARIA_ROLES,i=n==="checkbox"?r.CHECKBOX:r.TOGGLE,s=e.selectedAttrName;u.superclass.syncUI.call(e),t.set("role",i),e._uiSetSelected(e.get(s))},_afterSelectedChange:function(e){this._uiSetSelected(e.newVal)},_uiSetSelected:function(e){var t=this,n=t.get("contentBox"),r=u.ARIA_STATES,i=t.get("type"),s=i==="checkbox"?r.CHECKED:r.PRESSED;n.toggleClass(o.CLASS_NAMES.SELECTED,e),n.set(s,e)},toggle:function(){var e=this;e._set(e.selectedAttrName,!e.get(e.selectedAttrName))}},{NAME:"toggleButton",ATTRS:{type:{value:"toggle",writeOnce:"initOnly"}},HTML_PARSER:{checked:function(e){return e.hasClass(r.SELECTED)},pressed:function(e){return e.hasClass(r.SELECTED)}},ARIA_STATES:i,ARIA_ROLES:s,CLASS_NAMES:r}),e.Button=o,e.ToggleButton=u},"3.17.2",{requires:["button-core","cssbutton","widget"]});
diff --git a/js/yui3/cache-base/cache-base-min.js b/js/yui3/cache-base/cache-base-min.js
new file mode 100644
index 000000000..242ce1be4
--- /dev/null
+++ b/js/yui3/cache-base/cache-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("cache-base",function(e,t){var n=e.Lang,r=e.Lang.isDate,i=function(){i.superclass.constructor.apply(this,arguments)};e.mix(i,{NAME:"cache",ATTRS:{max:{value:0,setter:"_setMax"},size:{readOnly:!0,getter:"_getSize"},uniqueKeys:{value:!1},expires:{value:0,validator:function(t){return e.Lang.isDate(t)||e.Lang.isNumber(t)&&t>=0}},entries:{readOnly:!0,getter:"_getEntries"}}}),e.extend(i,e.Base,{_entries:null,initializer:function(e){this.publish("add",{defaultFn:this._defAddFn}),this.publish("flush",{defaultFn:this._defFlushFn}),this._entries=[]},destructor:function(){this._entries=[]},_setMax:function(e){var t=this._entries;if(e>0){if(t)while(t.length>e)t.shift()}else e=0,this._entries=[];return e},_getSize:function(){return this._entries.length},_getEntries:function(){return this._entries},_defAddFn:function(e){var t=this._entries,r=e.entry,i=this.get("max"),s;this.get("uniqueKeys")&&(s=this._position(e.entry.request),n.isValue(s)&&t.splice(s,1));while(i&&t.length>=i)t.shift();t[t.length]=r},_defFlushFn:function(e){var t=this._entries,r=e.details[0],i;r&&n.isValue(r.request)?(i=this._position(r.request),n.isValue(i)&&t.splice(i,1)):this._entries=[]},_isMatch:function(e,t){return!t.expires||new Date<t.expires?e===t.request:!1},_position:function(e){var t=this._entries,n=t.length,r=n-1;if(this.get("max")===null||this.get("max")>0)for(;r>=0;r--)if(this._isMatch(e,t[r]))return r;return null},add:function(e,t){var i=this.get("expires");this.get("initialized")&&(this.get("max")===null||this.get("max")>0)&&(n.isValue(e)||n.isNull(e)||n.isUndefined(e))&&this.fire("add",{entry:{request:e,response:t,cached:new Date,expires:r(i)?i:i?new Date((new Date).getTime()+this.get("expires")):null}})},flush:function(e){this.fire("flush",{request:n.isValue(e)?e:null})},retrieve:function(e){var t=this._entries,r=t.length,i=null,s;if(r>0&&(this.get("max")===null||this.get("max")>0)){this.fire("request",{request:e}),s=this._position(e);if(n.isValue(s))return i=t[s],this.fire("retrieve",{entry:i}),s<r-1&&(t.splice(s,1),t[t.length]=i),i}return null}}),e.Cache=i},"3.17.2",{requires:["base"]});
diff --git a/js/yui3/cache-offline/cache-offline-min.js b/js/yui3/cache-offline/cache-offline-min.js
new file mode 100644
index 000000000..69a4053da
--- /dev/null
+++ b/js/yui3/cache-offline/cache-offline-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("cache-offline",function(e,t){function n(){n.superclass.constructor.apply(this,arguments)}var r=null,i=e.JSON;try{r=e.config.win.localStorage}catch(s){}e.mix(n,{NAME:"cacheOffline",ATTRS:{sandbox:{value:"default",writeOnce:"initOnly"},expires:{value:864e5},max:{value:null,readOnly:!0},uniqueKeys:{value:!0,readOnly:!0,setter:function(){return!0}}},flushAll:function(){var e=r,t;if(e)if(e.clear)e.clear();else for(t in e)e.hasOwnProperty(t)&&(e.removeItem(t),delete e[t])}}),e.extend(n,e.Cache,r?{_setMax:function(e){return null},_getSize:function(){var e=0,t=0,n=r.length;for(;t<n;++t)r.key(t).indexOf(this.get("sandbox"))===0&&e++;return e},_getEntries:function(){var e=[],t=0,n=r.length,s=this.get("sandbox");for(;t<n;++t)r.key(t).indexOf(s)===0&&(e[t]=i.parse(r.key(t).substring(s.length)));return e},_defAddFn:function(e){var t=e.entry,n=t.request,s=t.cached,o=t.expires;t.cached=s.getTime(),t.expires=o?o.getTime():o;try{r.setItem(this.get("sandbox")+i.stringify({request:n}),i.stringify(t))}catch(u){this.fire("error",{error:u})}},_defFlushFn:function(e){var t,n=r.length-1;for(;n>-1;--n)t=r.key(n),t.indexOf(this.get("sandbox"))===0&&r.removeItem(t)},retrieve:function(e){this.fire("request",{request:e});var t,n,s;try{s=this.get("sandbox")+i.stringify({request:e});try{t=i.parse(r.getItem(s))}catch(o){}}catch(u){}if(t){t.cached=new Date(t.cached),n=t.expires,n=n?new Date(n):null,t.expires=n;if(this._isMatch(e,t))return this.fire("retrieve",{entry:t}),t}return null}}:{_setMax:function(e){return null}}),e.CacheOffline=n},"3.17.2",{requires:["cache-base","json"]});
diff --git a/js/yui3/cache-plugin/cache-plugin-min.js b/js/yui3/cache-plugin/cache-plugin-min.js
new file mode 100644
index 000000000..70ef45b4f
--- /dev/null
+++ b/js/yui3/cache-plugin/cache-plugin-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("cache-plugin",function(e,t){function n(t){var n=t&&t.cache?t.cache:e.Cache,r=e.Base.create("dataSourceCache",n,[e.Plugin.Base]),i=new r(t);return r.NS="tmpClass",i}e.mix(n,{NS:"cache",NAME:"cachePlugin"}),e.namespace("Plugin").Cache=n},"3.17.2",{requires:["plugin","cache-base"]});
diff --git a/js/yui3/calendar-base/assets/calendar-base-core.css b/js/yui3/calendar-base/assets/calendar-base-core.css
new file mode 100644
index 000000000..594b4fa77
--- /dev/null
+++ b/js/yui3/calendar-base/assets/calendar-base-core.css
@@ -0,0 +1,28 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-calendar {
+}
+
+.yui3-calendar-content {
+}
+
+.yui3-calendar-pane {
+ width: 100%;
+}
+
+.yui3-calendar-grid {
+ width: 100%;
+}
+
+.yui3-calendar-column-hidden, .yui3-calendar-hidden {
+ display:none;
+}
+
+.yui3-calendar-day {
+
+} \ No newline at end of file
diff --git a/js/yui3/calendar-base/assets/skins/night/calendar-base.css b/js/yui3/calendar-base/assets/skins/night/calendar-base.css
new file mode 100644
index 000000000..27c7d0635
--- /dev/null
+++ b/js/yui3/calendar-base/assets/skins/night/calendar-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-calendar-pane{width:100%}.yui3-calendar-grid{width:100%}.yui3-calendar-column-hidden,.yui3-calendar-hidden{display:none}.yui3-skin-night .yui3-calendar-content{padding:10px;color:#cbcbcb;border:1px solid #303030;background:#151515;background:-moz-linear-gradient(top,#222 0,#151515 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#222),color-stop(100%,#151515));background:-webkit-linear-gradient(top,#222 0,#151515 100%);background:-o-linear-gradient(top,#222 0,#151515 100%);background:-ms-linear-gradient(top,#222 0,#151515 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#222222',endColorstr='#151515',GradientType=0);background:linear-gradient(top,#222 0,#151515 100%);-moz-border-radius:5px;border-radius:5px}.yui3-skin-night .yui3-calendar-grid{padding:5px;border-collapse:collapse}.yui3-skin-night .yui3-calendar-header{padding-bottom:10px}.yui3-skin-night .yui3-calendar-header-label{margin:0;font-size:1em;font-weight:bold;text-align:center;width:100%}.yui3-skin-night .yui3-calendar-day,.yui3-skin-night .yui3-calendar-prevmonth-day,.yui3-skin-night .yui3-calendar-nextmonth-day{padding:5px;border:1px solid #151515;background:#262727;text-align:center}.yui3-skin-night .yui3-calendar-day:hover{background:#383939;color:#fff}.yui3-skin-night .yui3-calendar-selection-disabled,.yui3-skin-night .yui3-calendar-selection-disabled:hover{background:#151515;color:#596060}.yui3-skin-night .yui3-calendar-weekday{color:#4f4f4f;font-weight:bold;text-align:center}.yui3-skin-night .yui3-calendar-prevmonth-day,.yui3-skin-night .yui3-calendar-nextmonth-day{color:#4f4f4f}.yui3-skin-night .yui3-calendar-day{font-weight:bold}.yui3-skin-night .yui3-calendar-day-selected{background-color:#505151;color:#fff}.yui3-skin-night .yui3-calendar-left-grid{margin-right:1em}[dir="rtl"] .yui3-skin-night .yui3-calendar-left-grid,.yui3-skin-night [dir="rtl"] .yui3-calendar-left-grid{margin-right:auto;margin-left:1em}.yui3-skin-sam .yui3-calendar-right-grid{margin-left:1em}[dir="rtl"] .yui3-skin-night .yui3-calendar-right-grid,.yui3-skin-night [dir="rtl"] .yui3-calendar-right-grid{margin-left:auto;margin-right:1em}#yui3-css-stamp.skin-night-calendar-base{display:none}
diff --git a/js/yui3/calendar-base/assets/skins/sam/calendar-base.css b/js/yui3/calendar-base/assets/skins/sam/calendar-base.css
new file mode 100644
index 000000000..009b7d8c6
--- /dev/null
+++ b/js/yui3/calendar-base/assets/skins/sam/calendar-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-calendar-pane{width:100%}.yui3-calendar-grid{width:100%}.yui3-calendar-column-hidden,.yui3-calendar-hidden{display:none}.yui3-skin-sam .yui3-calendar-content{padding:10px;color:#000;border:1px solid gray;background:#f2f2f2;background:-moz-linear-gradient(top,#f9f9f9 0,#f2f2f2 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#f9f9f9),color-stop(100%,#f2f2f2));background:-webkit-linear-gradient(top,#f9f9f9 0,#f2f2f2 100%);background:-o-linear-gradient(top,#f9f9f9 0,#f2f2f2 100%);background:-ms-linear-gradient(top,#f9f9f9 0,#f2f2f2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f9f9f9',endColorstr='#f2f2f2',GradientType=0);background:linear-gradient(top,#f9f9f9 0,#f2f2f2 100%);-moz-border-radius:5px;border-radius:5px}.yui3-skin-sam .yui3-calendar-grid{padding:5px;border-collapse:collapse}.yui3-skin-sam .yui3-calendar-header{padding-bottom:10px}.yui3-skin-sam .yui3-calendar-header-label{margin:0;font-size:1em;font-weight:bold;text-align:center;width:100%}.yui3-skin-sam .yui3-calendar-day,.yui3-skin-sam .yui3-calendar-prevmonth-day,.yui3-skin-sam .yui3-calendar-nextmonth-day{padding:5px;border:1px solid #ccc;background:#fff;text-align:center}.yui3-skin-sam .yui3-calendar-day:hover{background:#06c;color:#fff}.yui3-skin-sam .yui3-calendar-selection-disabled,.yui3-skin-sam .yui3-calendar-selection-disabled:hover{color:#a6a6a6;background:#ccc}.yui3-skin-sam .yui3-calendar-weekday{font-weight:bold}.yui3-skin-sam .yui3-calendar-prevmonth-day,.yui3-skin-sam .yui3-calendar-nextmonth-day{color:#a6a6a6}.yui3-skin-sam .yui3-calendar-day{font-weight:bold}.yui3-skin-sam .yui3-calendar-day-selected{background-color:#b3d4ff;color:#000}.yui3-skin-sam .yui3-calendar-left-grid{margin-right:1em}[dir="rtl"] .yui3-skin-sam .yui3-calendar-left-grid,.yui3-skin-sam [dir="rtl"] .yui3-calendar-left-grid{margin-right:auto;margin-left:1em}.yui3-skin-sam .yui3-calendar-right-grid{margin-left:1em}[dir="rtl"] .yui3-skin-sam .yui3-calendar-right-grid,.yui3-skin-sam [dir="rtl"] .yui3-calendar-right-grid{margin-left:auto;margin-right:1em}.yui3-skin-sam .yui3-calendar-day-highlighted{background-color:#dcdef5}.yui3-skin-sam .yui3-calendar-day-selected.yui3-calendar-day-highlighted{background-color:#758fbb}#yui3-css-stamp.skin-sam-calendar-base{display:none}
diff --git a/js/yui3/calendar-base/calendar-base-min.js b/js/yui3/calendar-base/calendar-base-min.js
new file mode 100644
index 000000000..d73e02ef2
--- /dev/null
+++ b/js/yui3/calendar-base/calendar-base-min.js
@@ -0,0 +1,10 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("calendar-base",function(e,t){function M(){M.superclass.constructor.apply(this,arguments)}var n=e.ClassNameManager.getClassName,r="calendar",i=n(r,"grid"),s=n(r,"left-grid"),o=n(r,"right-grid"),u=n(r,"body"),a=n(r,"header"),f=n(r,"header-label"),l=n(r,"weekdayrow"),c=n(r,"weekday"),h=n(r,"column-hidden"),p=n(r,"day-selected"),d=n(r,"selection-disabled"),v=n(r,"row"),m=n(r,"day"),g=n(r,"prevmonth-day"),y=n(r,"nextmonth-day"),b=n(r,"anchor"),w=n(r,"pane"),E=n(r,"status"),S=e.Lang,x=S.sub,T=e.Array.each,N=e.Object.each,C=e.Array.indexOf,k=e.Object.hasKey,L=e.Object.setValue,A=e.Object.isEmpty,O=e.DataType.Date;e.CalendarBase=e.extend(M,e.Widget,{_paneProperties:{},_paneNumber:1,_calendarId:null,_selectedDates:{},_rules:{},_filterFunction:null,_storedDateCells:{},initializer:function(){this._paneProperties={},this._calendarId=e.guid("calendar"),this._selectedDates={},A(this._rules)&&(this._rules={}),this._storedDateCells={}},renderUI:function(){var e=this.get("contentBox");e.appendChild(this._initCalendarHTML(this.get("date"))),this.get("showPrevMonth")&&this._afterShowPrevMonthChange(),this.get("showNextMonth")&&this._afterShowNextMonthChange(),this._renderCustomRules(),this._renderSelectedDates(),this.get("boundingBox").setAttribute("aria-labelledby",this._calendarId+"_header")},bindUI:function(){this.after("dateChange",this._afterDateChange),this.after("showPrevMonthChange",this._afterShowPrevMonthChange),this.after("showNextMonthChange",this._afterShowNextMonthChange),this.after("headerRendererChange",this._afterHeaderRendererChange),this.after("customRendererChange",this._afterCustomRendererChange),this.after("enabledDatesRuleChange",this._afterCustomRendererChange),this.after("disabledDatesRuleChange",this._afterCustomRendererChange),this.after("focusedChange",this._afterFocusedChange),this.after("selectionChange",this._renderSelectedDates),this._bindCalendarEvents()},_getSelectedDatesList:function(){var e=[];return N(this._selectedDates,function(t){N(t,function(t){N(t,function(t){e.push(t)},this)},this)},this),e},_getSelectedDatesInMonth:function(t){var n=t.getFullYear(),r=t.getMonth();return k(this._selectedDates,n)&&k(this._selectedDates[n],r)?e.Object.values(this._selectedDates[n][r]):[]},_isNumInList:function(e,t){if(t==="all")return!0;var n=t.split(","),r=n.length,i;while(r--){i=n[r].split("-");if(i.length===2&&e>=parseInt(i[0],10)&&e<=parseInt(i[1],10))return!0;if(i.length===1&&parseInt(n[r],10)===e)return!0}return!1},_getRulesForDate:function(e){var t=e.getFullYear(),n=e.getMonth(),r=e.getDate(),i=e.getDay(),s=this._rules,o=[],u,a,f,l;for(u in s)if(this._isNumInList(t,u))if(S.isString(s[u]))o.push(s[u]);else for(a in s[u])if(this._isNumInList(n,a))if(S.isString(s[u][a]))o.push(s[u][a]);else for(f in s[u][a])if(this._isNumInList(r,f))if(S.isString(s[u][a][f]))o.push(s[u][a][f]);else for(l in s[u][a][f])this._isNumInList(i,l)&&S.isString(s[u][a][f][l])&&o.push(s[u][a][f][l]);return o},_matchesRule:function(e,t){return C(this._getRulesForDate(e),t)>=0},_canBeSelected:function(e){var t=this.get("enabledDatesRule"),n=this.get("disabledDatesRule");return t?this._matchesRule(e,t):n?!this._matchesRule(e,n):!0},selectDates:function(e){return O.isValidDate(e)?this._addDateToSelection(e):S.isArray(e)&&this._addDatesToSelection(e),this},deselectDates:function(e){return e?O.isValidDate(e)?this._removeDateFromSelection(e):S.isArray(e)&&this._removeDatesFromSelection(e):this._clearSelection(),this},_addDateToSelection:function(e,t){e=this._normalizeTime(e);if(this._canBeSelected(e)){var n=e.getFullYear(),r=e.getMonth(),i=e.getDate();k(this._selectedDates,n)?k(this._selectedDates[n],r)?this._selectedDates[n][r][i]=e:(this._selectedDates[n][r]={},this._selectedDates[n][r][i]=e):(this._selectedDates[n]={},this._selectedDates[n][r]={},this._selectedDates[n][r][i]=e),this._selectedDates=L(this._selectedDates,[n,r,i],e),t||this._fireSelectionChange()}},_addDatesToSelection:function(e){T(e,this._addDateToSelection,this),this._fireSelectionChange()},_addDateRangeToSelection:function(e,t){var n=(t.getTimezoneOffset()-e.getTimezoneOffset())*6e4,r=e.getTime(),i=t.getTime(),s,o,u;r>i?(s=r,r=i,i=s+n):i-=n;for(o=r;o<=i;o+=864e5)u=new Date(o),u.setHours(12),this._addDateToSelection(u,o);this._fireSelectionChange()},_removeDateFromSelection:function(e,t){var n=e.getFullYear(),r=e.getMonth(),i=e.getDate();k(this._selectedDates,n)&&k(this._selectedDates[n],r)&&k(this._selectedDates[n][r],i)&&(delete this._selectedDates[n][r][i],t||this._fireSelectionChange())},_removeDatesFromSelection:function(e){T(e,this._removeDateFromSelection,this),this._fireSelectionChange()},_removeDateRangeFromSelection:function(e,t){var n=e.getTime(),r=t.getTime(),i;for(i=n;i<=r;i+=864e5)this._removeDateFromSelection(new Date(i),i);this._fireSelectionChange()},_clearSelection:function(e){this._selectedDates={},this.get("contentBox").all("."+p).removeClass(p).setAttribute("aria-selected",!1),e||this._fireSelectionChange()},_fireSelectionChange:function(){this.fire("selectionChange",{newSelection:this._getSelectedDatesList()})},_restoreModifiedCells:function(){var e=this.get("contentBox"),t;for(t in this._storedDateCells)e.one("#"+t).replace(this._storedDateCells[t]),delete this._storedDateCells[t]},_renderCustomRules:function(){this.get("contentBox").all("."+m+",."+y).removeClass(d).setAttribute("aria-disabled",!1);if(!A(this._rules)){var t,n,r;for(t=0;t<this._paneNumber;t++)n=O.addMonths(this.get("date"),t),r=O.listOfDatesInMonth(n),T(r,e.bind(this._renderCustomRulesHelper,this))}},_renderCustomRulesHelper:function(e){var t=this.get("enabledDatesRule"),n=this.get("disabledDatesRule"),r,i;r=this._getRulesForDate(e),r.length>0?((t&&C(r,t)<0||!t&&n&&C(r,n)>=0)&&this._disableDate(e),S.isFunction(this._filterFunction)&&(i=this._dateToNode(e),this._storedDateCells[i.get("id")]=i.cloneNode(!0),this._filterFunction(e,i,r))):t&&this._disableDate(e)},_renderSelectedDates:function(){this.get("contentBox").all("."+p).removeClass(p).setAttribute
+("aria-selected",!1);var t,n,r;for(t=0;t<this._paneNumber;t++)n=O.addMonths(this.get("date"),t),r=this._getSelectedDatesInMonth(n),T(r,e.bind(this._renderSelectedDatesHelper,this))},_renderSelectedDatesHelper:function(e){this._dateToNode(e).addClass(p).setAttribute("aria-selected",!0)},_disableDate:function(e){this._dateToNode(e).addClass(d).setAttribute("aria-disabled",!0)},_dateToNode:function(e){var t=e.getDate(),n=0,r=t%7,i=(12+e.getMonth()-this.get("date").getMonth())%12,s=this._calendarId+"_pane_"+i,o=this._paneProperties[s].cutoffCol;switch(r){case 0:o>=6?n=12:n=5;break;case 1:n=6;break;case 2:o>0?n=7:n=0;break;case 3:o>1?n=8:n=1;break;case 4:o>2?n=9:n=2;break;case 5:o>3?n=10:n=3;break;case 6:o>4?n=11:n=4}return this.get("contentBox").one("#"+this._calendarId+"_pane_"+i+"_"+n+"_"+t)},_nodeToDate:function(e){var t=e.get("id").split("_").reverse(),n=parseInt(t[2],10),r=parseInt(t[0],10),i=O.addMonths(this.get("date"),n),s=i.getFullYear(),o=i.getMonth();return new Date(s,o,r,12,0,0,0)},_bindCalendarEvents:function(){},_normalizeDate:function(e){return e?new Date(e.getFullYear(),e.getMonth(),1,12,0,0,0):null},_normalizeTime:function(e){return e?new Date(e.getFullYear(),e.getMonth(),e.getDate(),12,0,0,0):null},_getCutoffColumn:function(e,t){var n=this._normalizeDate(e).getDay()-t,r=6-(n+7)%7;return r},_turnPrevMonthOn:function(e){var t=e.get("id"),n=this._paneProperties[t].paneDate,r=O.daysInMonth(O.addMonths(n,-1)),i;this._paneProperties[t].hasOwnProperty("daysInPrevMonth")||(this._paneProperties[t].daysInPrevMonth=0);if(r!==this._paneProperties[t].daysInPrevMonth){this._paneProperties[t].daysInPrevMonth=r;for(i=5;i>=0;i--)e.one("#"+t+"_"+i+"_"+(i-5)).set("text",r--)}},_turnPrevMonthOff:function(e){var t=e.get("id"),n;this._paneProperties[t].daysInPrevMonth=0;for(n=5;n>=0;n--)e.one("#"+t+"_"+n+"_"+(n-5)).setContent("&nbsp;")},_cleanUpNextMonthCells:function(e){var t=e.get("id");e.one("#"+t+"_6_29").removeClass(y),e.one("#"+t+"_7_30").removeClass(y),e.one("#"+t+"_8_31").removeClass(y),e.one("#"+t+"_0_30").removeClass(y),e.one("#"+t+"_1_31").removeClass(y)},_turnNextMonthOn:function(e){var t=1,n=e.get("id"),r=this._paneProperties[n].daysInMonth,i=this._paneProperties[n].cutoffCol,s,o;for(s=r-22;s<i+7;s++)e.one("#"+n+"_"+s+"_"+(s+23)).set("text",t++).addClass(y);o=i,r===31&&i<=1?o=2:r===30&&i===0&&(o=1);for(s=o;s<i+7;s++)e.one("#"+n+"_"+s+"_"+(s+30)).set("text",t++).addClass(y)},_turnNextMonthOff:function(e){var t=e.get("id"),n=this._paneProperties[t].daysInMonth,r=this._paneProperties[t].cutoffCol,i,s;for(i=n-22;i<=12;i++)e.one("#"+t+"_"+i+"_"+(i+23)).setContent("&nbsp;").addClass(y);s=0,n===31&&r<=1?s=2:n===30&&r===0&&(s=1);for(i=s;i<=12;i++)e.one("#"+t+"_"+i+"_"+(i+30)).setContent("&nbsp;").addClass(y)},_afterShowNextMonthChange:function(){var e=this.get("contentBox"),t=e.one("#"+this._calendarId+"_pane_"+(this._paneNumber-1));this._cleanUpNextMonthCells(t),this.get("showNextMonth")?this._turnNextMonthOn(t):this._turnNextMonthOff(t)},_afterShowPrevMonthChange:function(){var e=this.get("contentBox"),t=e.one("#"+this._calendarId+"_pane_"+0);this.get("showPrevMonth")?this._turnPrevMonthOn(t):this._turnPrevMonthOff(t)},_afterHeaderRendererChange:function(){var e=this.get("contentBox").one("."+f);e.setContent(this._updateCalendarHeader(this.get("date")))},_afterCustomRendererChange:function(){this._restoreModifiedCells(),this._renderCustomRules()},_afterDateChange:function(){var e=this.get("contentBox"),t=e.one("."+a).one("."+f),n=e.all("."+i),r=this.get("date"),s=0;e.setStyle("visibility","hidden"),t.setContent(this._updateCalendarHeader(r)),this._restoreModifiedCells(),n.each(function(e){this._rerenderCalendarPane(O.addMonths(r,s++),e)},this),this._afterShowPrevMonthChange(),this._afterShowNextMonthChange(),this._renderCustomRules(),this._renderSelectedDates(),e.setStyle("visibility","inherit")},_initCalendarPane:function(t,n){var r=this.get("strings.very_short_weekdays")||["Su","Mo","Tu","We","Th","Fr","Sa"],i=e.Intl.get("datatype-date-format").A,s=this.get("strings.first_weekday")||0,o=this._getCutoffColumn(t,s),u=O.daysInMonth(t),a=["","","","","",""],f={},l,c,p,d,v,b,w,E;f.weekday_row="";for(l=s;l<=s+6;l++)f.weekday_row+=x(M.WEEKDAY_TEMPLATE,{short_weekdayname:r[l%7],weekdayname:i[l%7]});f.weekday_row_template=x(M.WEEKDAY_ROW_TEMPLATE,f);for(c=0;c<=5;c++)for(p=0;p<=12;p++){d=7*c-5+p,v=n+"_"+p+"_"+d,b=m,d<1?b=g:d>u&&(b=y);if(d<1||d>u)d="&nbsp;";w=p>=o&&p<o+7?"":h,a[c]+=x(M.CALDAY_TEMPLATE,{day_content:d,calendar_col_class:"calendar_col"+p,calendar_col_visibility_class:w,calendar_day_class:b,calendar_day_id:v})}return f.body_template="",T(a,function(e){f.body_template+=x(M.CALDAY_ROW_TEMPLATE,{calday_row:e})}),f.calendar_pane_id=n,f.calendar_pane_tabindex=this.get("tabIndex"),f.pane_arialabel=O.format(t,{format:"%B %Y"}),E=x(x(M.CALENDAR_GRID_TEMPLATE,f),M.CALENDAR_STRINGS),this._paneProperties[n]={cutoffCol:o,daysInMonth:u,paneDate:t},E},_rerenderCalendarPane:function(e,t){var n=this.get("strings.first_weekday")||0,r=this._getCutoffColumn(e,n),i=O.daysInMonth(e),s=t.get("id"),o,u,a;t.setStyle("visibility","hidden"),t.setAttribute("aria-label",O.format(e,{format:"%B %Y"}));for(o=0;o<=12;o++){u=t.all(".calendar_col"+o),u.removeClass(h);if(o<r||o>=r+7)u.addClass(h);else switch(o){case 0:a=t.one("#"+s+"_0_30"),i>=30?(a.set("text","30"),a.removeClass(y).addClass(m)):(a.setContent("&nbsp;"),a.removeClass(m).addClass(y));break;case 1:a=t.one("#"+s+"_1_31"),i>=31?(a.set("text","31"),a.removeClass(y).addClass(m)):(a.setContent("&nbsp;"),a.removeClass(m).addClass(y));break;case 6:a=t.one("#"+s+"_6_29"),i>=29?(a.set("text","29"),a.removeClass(y).addClass(m)):(a.setContent("&nbsp;"),a.removeClass(m).addClass(y));break;case 7:a=t.one("#"+s+"_7_30"),i>=30?(a.set("text","30"),a.removeClass(y).addClass(m)):(a.setContent("&nbsp;"),a.removeClass(m).addClass(y));break;case 8:a=t.one("#"+s+"_8_31"),i>=31?(a.set("text","31"),a.removeClass(y).addClass(m)):(a.setContent("&nbsp;"),a.removeClass(m).addClass(y)
+)}}this._paneProperties[s].cutoffCol=r,this._paneProperties[s].daysInMonth=i,this._paneProperties[s].paneDate=e,t.setStyle("visibility","inherit")},_updateCalendarHeader:function(t){var n="",r=this.get("headerRenderer");return e.Lang.isString(r)?n=O.format(t,{format:r}):r instanceof Function&&(n=r.call(this,t)),n},_initCalendarHeader:function(e){return x(x(M.HEADER_TEMPLATE,{calheader:this._updateCalendarHeader(e),calendar_id:this._calendarId}),M.CALENDAR_STRINGS)},_initCalendarHTML:function(t){function o(){return i=this._initCalendarPane(O.addMonths(t,r),n.calendar_id+"_pane_"+r),r++,i}var n={},r=0,i,s;return n.header_template=this._initCalendarHeader(t),n.calendar_id=this._calendarId,n.body_template=x(x(M.CONTENT_TEMPLATE,n),M.CALENDAR_STRINGS),s=n.body_template.replace(/\{calendar_grid_template\}/g,e.bind(o,this)),this._paneNumber=r,s}},{CALENDAR_STRINGS:{calendar_grid_class:i,calendar_body_class:u,calendar_hd_class:a,calendar_hd_label_class:f,calendar_weekdayrow_class:l,calendar_weekday_class:c,calendar_row_class:v,calendar_day_class:m,calendar_dayanchor_class:b,calendar_pane_class:w,calendar_right_grid_class:o,calendar_left_grid_class:s,calendar_status_class:E},CONTENT_TEMPLATE:'<div class="yui3-g {calendar_pane_class}" id="{calendar_id}">{header_template}<div class="yui3-u-1">{calendar_grid_template}</div></div>',ONE_PANE_TEMPLATE:'<div class="yui3-g {calendar_pane_class}" id="{calendar_id}">{header_template}<div class="yui3-u-1">{calendar_grid_template}</div></div>',TWO_PANE_TEMPLATE:'<div class="yui3-g {calendar_pane_class}" id="{calendar_id}">{header_template}<div class="yui3-u-1-2"><div class = "{calendar_left_grid_class}">{calendar_grid_template}</div></div><div class="yui3-u-1-2"><div class = "{calendar_right_grid_class}">{calendar_grid_template}</div></div></div>',THREE_PANE_TEMPLATE:'<div class="yui3-g {calendar_pane_class}" id="{calendar_id}">{header_template}<div class="yui3-u-1-3"><div class="{calendar_left_grid_class}">{calendar_grid_template}</div></div><div class="yui3-u-1-3">{calendar_grid_template}</div><div class="yui3-u-1-3"><div class="{calendar_right_grid_class}">{calendar_grid_template}</div></div></div>',CALENDAR_GRID_TEMPLATE:'<table class="{calendar_grid_class}" id="{calendar_pane_id}" role="grid" aria-readonly="true" aria-label="{pane_arialabel}" tabindex="{calendar_pane_tabindex}"><thead>{weekday_row_template}</thead><tbody>{body_template}</tbody></table>',HEADER_TEMPLATE:'<div class="yui3-g {calendar_hd_class}"><div class="yui3-u {calendar_hd_label_class}" id="{calendar_id}_header" aria-role="heading">{calheader}</div></div>',WEEKDAY_ROW_TEMPLATE:'<tr class="{calendar_weekdayrow_class}" role="row">{weekday_row}</tr>',CALDAY_ROW_TEMPLATE:'<tr class="{calendar_row_class}" role="row">{calday_row}</tr>',WEEKDAY_TEMPLATE:'<th class="{calendar_weekday_class}" role="columnheader" aria-label="{weekdayname}">{short_weekdayname}</th>',CALDAY_TEMPLATE:'<td class="{calendar_col_class} {calendar_day_class} {calendar_col_visibility_class}" id="{calendar_day_id}" role="gridcell" tabindex="-1">{day_content}</td>',NAME:"calendarBase",ATTRS:{tabIndex:{value:1},date:{value:new Date,setter:function(e){var t=this._normalizeDate(e);return O.areEqual(t,this.get("date"))?this.get("date"):t}},showPrevMonth:{value:!1},showNextMonth:{value:!1},strings:{valueFn:function(){return e.Intl.get("calendar-base")}},headerRenderer:{value:"%B %Y"},enabledDatesRule:{value:null},disabledDatesRule:{value:null},selectedDates:{readOnly:!0,getter:function(){return this._getSelectedDatesList()}},customRenderer:{lazyAdd:!1,value:{},setter:function(e){this._rules=e.rules,this._filterFunction=e.filterFunction}}}})},"3.17.2",{requires:["widget","datatype-date","datatype-date-math","cssgrids"],lang:["de","en","es","es-AR","fr","hu","it","ja","nb-NO","nl","pt-BR","ru","zh-Hans","zh-Hans-CN","zh-Hant","zh-Hant-HK","zh-HANT-TW"],skinnable:!0});
diff --git a/js/yui3/calendar-base/lang/calendar-base.js b/js/yui3/calendar-base/lang/calendar-base.js
new file mode 100644
index 000000000..141aecad4
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base",function(e){e.Intl.add("calendar-base","",{very_short_weekdays:["Su","Mo","Tu","We","Th","Fr","Sa"],first_weekday:0,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar-base/lang/calendar-base_de.js b/js/yui3/calendar-base/lang/calendar-base_de.js
new file mode 100644
index 000000000..1a3e7efbe
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base_de.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base_de",function(e){e.Intl.add("calendar-base","de",{very_short_weekdays:["So","Mo","Di","Mi","Do","Fr","Sa"],first_weekday:1,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar-base/lang/calendar-base_en.js b/js/yui3/calendar-base/lang/calendar-base_en.js
new file mode 100644
index 000000000..26f417962
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base_en.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base_en",function(e){e.Intl.add("calendar-base","en",{very_short_weekdays:["Su","Mo","Tu","We","Th","Fr","Sa"],first_weekday:0,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar-base/lang/calendar-base_es-AR.js b/js/yui3/calendar-base/lang/calendar-base_es-AR.js
new file mode 100644
index 000000000..d57e53734
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base_es-AR.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base_es-AR",function(e){e.Intl.add("calendar-base","es-AR",{very_short_weekdays:["Do","Lu","Ma","Mi","Ju","Vi","Sa"],first_weekday:0,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar-base/lang/calendar-base_es.js b/js/yui3/calendar-base/lang/calendar-base_es.js
new file mode 100644
index 000000000..465fcc7a2
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base_es.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base_es",function(e){e.Intl.add("calendar-base","es",{very_short_weekdays:["Do","Lu","Ma","Mi","Ju","Vi","Sa"],first_weekday:1,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar-base/lang/calendar-base_fr.js b/js/yui3/calendar-base/lang/calendar-base_fr.js
new file mode 100644
index 000000000..96860aef8
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base_fr.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base_fr",function(e){e.Intl.add("calendar-base","fr",{very_short_weekdays:["Di","Lu","Ma","Me","Je","Ve","Sa"],first_weekday:1,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar-base/lang/calendar-base_hu.js b/js/yui3/calendar-base/lang/calendar-base_hu.js
new file mode 100644
index 000000000..a24c53e0b
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base_hu.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base_hu",function(e){e.Intl.add("calendar-base","hu",{very_short_weekdays:["V","H","K","Sze","Cs","P","Szo"],first_weekday:1,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar-base/lang/calendar-base_it.js b/js/yui3/calendar-base/lang/calendar-base_it.js
new file mode 100644
index 000000000..62b774a42
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base_it.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base_it",function(e){e.Intl.add("calendar-base","it",{very_short_weekdays:["Do","Lu","Ma","Me","Gi","Ve","Sa"],first_weekday:1,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar-base/lang/calendar-base_ja.js b/js/yui3/calendar-base/lang/calendar-base_ja.js
new file mode 100644
index 000000000..eb38c0220
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base_ja.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base_ja",function(e){e.Intl.add("calendar-base","ja",{very_short_weekdays:["\u65e5","\u6708","\u706b","\u6c34","\u6728","\u91d1","\u571f"],first_weekday:0,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar-base/lang/calendar-base_nb-NO.js b/js/yui3/calendar-base/lang/calendar-base_nb-NO.js
new file mode 100644
index 000000000..2f6c9ac19
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base_nb-NO.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base_nb-NO",function(e){e.Intl.add("calendar-base","nb-NO",{very_short_weekdays:["S\u00f8","Ma","Ti","On","To","Fr","L\u00f8"],first_weekday:1,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar-base/lang/calendar-base_nl.js b/js/yui3/calendar-base/lang/calendar-base_nl.js
new file mode 100644
index 000000000..525b50713
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base_nl.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base_nl",function(e){e.Intl.add("calendar-base","nl",{very_short_weekdays:["zo","ma","di","woe","do","vr","za"],first_weekday:1,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar-base/lang/calendar-base_pt-BR.js b/js/yui3/calendar-base/lang/calendar-base_pt-BR.js
new file mode 100644
index 000000000..342e50bd8
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base_pt-BR.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base_pt-BR",function(e){e.Intl.add("calendar-base","pt-BR",{very_short_weekdays:["Dom","Seg","Ter","Qua","Qui","Sex","Sab"],first_weekday:0,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar-base/lang/calendar-base_ru.js b/js/yui3/calendar-base/lang/calendar-base_ru.js
new file mode 100644
index 000000000..54cdc390a
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base_ru.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base_ru",function(e){e.Intl.add("calendar-base","ru",{very_short_weekdays:["\u0412\u0441","\u041f\u043d","\u0412\u0442","\u0421\u0440","\u0427\u0442","\u041f\u0442","\u0421\u0431"],first_weekday:1,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar-base/lang/calendar-base_zh-HANT-TW.js b/js/yui3/calendar-base/lang/calendar-base_zh-HANT-TW.js
new file mode 100644
index 000000000..cb34e50a3
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base_zh-HANT-TW.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base_zh-HANT-TW",function(e){e.Intl.add("calendar-base","zh-HANT-TW",{very_short_weekdays:["\u65e5","\u4e00","\u4e8c","\u4e09","\u56db","\u4e94","\u516d"],first_weekday:0,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar-base/lang/calendar-base_zh-Hans-CN.js b/js/yui3/calendar-base/lang/calendar-base_zh-Hans-CN.js
new file mode 100644
index 000000000..3d941cbfd
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base_zh-Hans-CN.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base_zh-Hans-CN",function(e){e.Intl.add("calendar-base","zh-Hans-CN",{very_short_weekdays:["\u65e5","\u4e00","\u4e8c","\u4e09","\u56db","\u4e94","\u516d"],first_weekday:0,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar-base/lang/calendar-base_zh-Hans.js b/js/yui3/calendar-base/lang/calendar-base_zh-Hans.js
new file mode 100644
index 000000000..9c7beb76f
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base_zh-Hans.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base_zh-Hans",function(e){e.Intl.add("calendar-base","zh-Hans",{very_short_weekdays:["\u65e5","\u4e00","\u4e8c","\u4e09","\u56db","\u4e94","\u516d"],first_weekday:0,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar-base/lang/calendar-base_zh-Hant-HK.js b/js/yui3/calendar-base/lang/calendar-base_zh-Hant-HK.js
new file mode 100644
index 000000000..e1b89ec9e
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base_zh-Hant-HK.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base_zh-Hant-HK",function(e){e.Intl.add("calendar-base","zh-Hant-HK",{very_short_weekdays:["\u65e5","\u4e00","\u4e8c","\u4e09","\u56db","\u4e94","\u516d"],first_weekday:0,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar-base/lang/calendar-base_zh-Hant.js b/js/yui3/calendar-base/lang/calendar-base_zh-Hant.js
new file mode 100644
index 000000000..a5d5006ee
--- /dev/null
+++ b/js/yui3/calendar-base/lang/calendar-base_zh-Hant.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/calendar-base_zh-Hant",function(e){e.Intl.add("calendar-base","zh-Hant",{very_short_weekdays:["\u65e5","\u4e00","\u4e8c","\u4e09","\u56db","\u4e94","\u516d"],first_weekday:0,weekends:[0,6]})},"3.17.2");
diff --git a/js/yui3/calendar/assets/calendar-core.css b/js/yui3/calendar/assets/calendar-core.css
new file mode 100644
index 000000000..549f56c82
--- /dev/null
+++ b/js/yui3/calendar/assets/calendar-core.css
@@ -0,0 +1,38 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-calendar {
+}
+
+.yui3-calendar-content {
+}
+
+.yui3-calendar-column-hidden, .yui3-calendar-hidden {
+ display:none;
+}
+
+.yui3-calendar-day {
+ cursor: pointer;
+}
+
+.yui3-calendar-selection-disabled {
+ cursor: default;
+}
+
+.yui3-calendar-prevmonth-day {
+ cursor: default;
+}
+
+.yui3-calendar-nextmonth-day {
+ cursor: default;
+}
+
+.yui3-calendar-content:hover .yui3-calendar-day,
+.yui3-calendar-content:hover .yui3-calendar-prevmonth-day,
+.yui3-calendar-content:hover .yui3-calendar-nextmonth-day {
+ -moz-user-select: none;
+} \ No newline at end of file
diff --git a/js/yui3/calendar/assets/skins/night/calendar.css b/js/yui3/calendar/assets/skins/night/calendar.css
new file mode 100644
index 000000000..e4c98d013
--- /dev/null
+++ b/js/yui3/calendar/assets/skins/night/calendar.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-calendar-column-hidden,.yui3-calendar-hidden{display:none}.yui3-calendar-day{cursor:pointer}.yui3-calendar-selection-disabled{cursor:default}.yui3-calendar-prevmonth-day{cursor:default}.yui3-calendar-nextmonth-day{cursor:default}.yui3-calendar-content:hover .yui3-calendar-day,.yui3-calendar-content:hover .yui3-calendar-prevmonth-day,.yui3-calendar-content:hover .yui3-calendar-nextmonth-day{-moz-user-select:none}.yui3-skin-night .yui3-calendar-day-highlighted{background-color:#555}.yui3-skin-night .yui3-calendar-day-selected.yui3-calendar-day-highlighted{background-color:#777}#yui3-css-stamp.skin-night-calendar{display:none}
diff --git a/js/yui3/calendar/assets/skins/sam/calendar.css b/js/yui3/calendar/assets/skins/sam/calendar.css
new file mode 100644
index 000000000..14930c898
--- /dev/null
+++ b/js/yui3/calendar/assets/skins/sam/calendar.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-calendar-column-hidden,.yui3-calendar-hidden{display:none}.yui3-calendar-day{cursor:pointer}.yui3-calendar-selection-disabled{cursor:default}.yui3-calendar-prevmonth-day{cursor:default}.yui3-calendar-nextmonth-day{cursor:default}.yui3-calendar-content:hover .yui3-calendar-day,.yui3-calendar-content:hover .yui3-calendar-prevmonth-day,.yui3-calendar-content:hover .yui3-calendar-nextmonth-day{-moz-user-select:none}.yui3-skin-sam .yui3-calendar-day-highlighted{background-color:#dcdef5}.yui3-skin-sam .yui3-calendar-day-selected.yui3-calendar-day-highlighted{background-color:#758fbb}#yui3-css-stamp.skin-sam-calendar{display:none}
diff --git a/js/yui3/calendar/calendar-min.js b/js/yui3/calendar/calendar-min.js
new file mode 100644
index 000000000..8b217b4e1
--- /dev/null
+++ b/js/yui3/calendar/calendar-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("calendar",function(e,t){function b(){b.superclass.constructor.apply(this,arguments)}var n=e.ClassNameManager.getClassName,r="calendar",i=40,s=38,o=37,u=39,a=13,f=32,l=n(r,"day-selected"),c=n(r,"day-highlighted"),h=n(r,"day"),p=n(r,"prevmonth-day"),d=n(r,"nextmonth-day"),v=n(r,"grid"),m=e.DataType.Date,g=n(r,"pane"),y=e.UA.os;e.Calendar=e.extend(b,e.CalendarBase,{_keyEvents:[],_highlightedDateNode:null,_lastSelectedDate:null,initializer:function(){this.plug(e.Plugin.CalendarNavigator),this._keyEvents=[],this._highlightedDateNode=null,this._lastSelectedDate=null},_bindCalendarEvents:function(){var e=this.get("contentBox"),t=e.one("."+g);t.on("selectstart",this._preventSelectionStart),t.delegate("click",this._clickCalendar,"."+h+", ."+p+", ."+d,this),t.delegate("keydown",this._keydownCalendar,"."+v,this),t.delegate("focus",this._focusCalendarGrid,"."+v,this),t.delegate("focus",this._focusCalendarCell,"."+h,this),t.delegate("blur",this._blurCalendarGrid,"."+v+",."+h,this),this.after(["minimumDateChange","maximumDateChange"],this._afterCustomRendererChange)},_preventSelectionStart:function(e){e.preventDefault()},_highlightDateNode:function(e){this._unhighlightCurrentDateNode();var t=this._dateToNode(e);t.focus(),t.addClass(c)},_unhighlightCurrentDateNode:function(){var e=this.get("contentBox").all("."+c);e&&e.removeClass(c)},_getGridNumber:function(e){var t=e.get("id").split("_").reverse();return parseInt(t[0],10)},_blurCalendarGrid:function(){this._unhighlightCurrentDateNode()},_focusCalendarCell:function(e){this._highlightedDateNode=e.target,e.stopPropagation()},_focusCalendarGrid:function(){this._unhighlightCurrentDateNode(),this._highlightedDateNode=null},_keydownCalendar:function(e){var t=this._getGridNumber(e.target),n=this._highlightedDateNode?this._nodeToDate(this._highlightedDateNode):null,r=e.keyCode,c=0,h="",p,d,v,g,y;switch(r){case i:c=7,h="s";break;case s:c=-7,h="n";break;case o:c=-1,h="w";break;case u:c=1,h="e";break;case f:case a:e.preventDefault();if(this._highlightedDateNode){p=this.get("selectionMode");if(p==="single"&&!this._highlightedDateNode.hasClass(l))this._clearSelection(!0),this._addDateToSelection(n);else if(p==="multiple"||p==="multiple-sticky")this._highlightedDateNode.hasClass(l)?this._removeDateFromSelection(n):this._addDateToSelection(n)}}if(r===i||r===s||r===o||r===u)n||(n=m.addMonths(this.get("date"),t),c=0),e.preventDefault(),d=m.addDays(n,c),v=this.get("date"),g=m.addMonths(this.get("date"),this._paneNumber-1),y=new Date(g),g.setDate(m.daysInMonth(g)),m.isInRange(d,v,g)?this._highlightDateNode(d):m.isGreater(v,d)?m.isGreaterOrEqual(this.get("minimumDate"),v)||(this.set("date",m.addMonths(v,-1)),this._highlightDateNode(d)):m.isGreater(d,g)&&(m.isGreaterOrEqual(y,this.get("maximumDate"))||(this.set("date",m.addMonths(v,1)),this._highlightDateNode(d)))},_clickCalendar:function(e){var t=e.currentTarget,n=t.hasClass(h)&&!t.hasClass(p)&&!t.hasClass(d),r=t.hasClass(l),i;switch(this.get("selectionMode")){case"single":n&&(r||(this._clearSelection(!0),this._addDateToSelection(this._nodeToDate(t))));break;case"multiple-sticky":n&&(r?this._removeDateFromSelection(this._nodeToDate(t)):this._addDateToSelection(this._nodeToDate(t)));break;case"multiple":n&&(!e.metaKey&&!e.ctrlKey&&!e.shiftKey?(this._clearSelection(!0),this._lastSelectedDate=this._nodeToDate(t),this._addDateToSelection(this._lastSelectedDate)):(y==="macintosh"&&e.metaKey||y!=="macintosh"&&e.ctrlKey)&&!e.shiftKey?r?(this._removeDateFromSelection(this._nodeToDate(t)),this._lastSelectedDate=null):(this._lastSelectedDate=this._nodeToDate(t),this._addDateToSelection(this._lastSelectedDate)):(y==="macintosh"&&e.metaKey||y!=="macintosh"&&e.ctrlKey)&&e.shiftKey?this._lastSelectedDate?(i=this._nodeToDate(t),this._addDateRangeToSelection(i,this._lastSelectedDate),this._lastSelectedDate=i):(this._lastSelectedDate=this._nodeToDate(t),this._addDateToSelection(this._lastSelectedDate)):e.shiftKey&&(this._lastSelectedDate?(i=this._nodeToDate(t),this._clearSelection(!0),this._addDateRangeToSelection(i,this._lastSelectedDate),this._lastSelectedDate=i):(this._clearSelection(!0),this._lastSelectedDate=this._nodeToDate(t),this._addDateToSelection(this._lastSelectedDate))))}n?this.fire("dateClick",{cell:t,date:this._nodeToDate(t)}):t.hasClass(p)?this.fire("prevMonthClick"):t.hasClass(d)&&this.fire("nextMonthClick")},_canBeSelected:function(e){var t=this.get("minimumDate"),n=this.get("maximumDate");return t&&!m.isGreaterOrEqual(e,t)||n&&m.isGreater(e,n)?!1:b.superclass._canBeSelected.call(this,e)},_renderCustomRules:function(){b.superclass._renderCustomRules.call(this);var e=this.get("minimumDate"),t=this.get("maximumDate"),n=[],r,i,s,o;if(!e&&!t)return;for(o=0;o<this._paneNumber;o++)s=m.addMonths(this.get("date"),o),n=n.concat(m.listOfDatesInMonth(s));if(e)for(r=0,i=n.length;r<i;r++){if(!!m.isGreaterOrEqual(n[r],e))break;this._disableDate(n[r])}if(t)for(r=n.length-1;r>=0;r--){if(!m.isGreater(n[r],t))break;this._disableDate(n[r])}},subtractMonth:function(e){return this.set("date",m.addMonths(this.get("date"),-1)),e&&e.halt(),this},subtractYear:function(e){return this.set("date",m.addYears(this.get("date"),-1)),e&&e.halt(),this},addMonth:function(e){return this.set("date",m.addMonths(this.get("date"),1)),e&&e.halt(),this},addYear:function(e){return this.set("date",m.addYears(this.get("date"),1)),e&&e.halt(),this}},{NAME:"calendar",ATTRS:{selectionMode:{value:"single"},date:{value:new Date,lazyAdd:!1,setter:function(e){var t=this._normalizeDate(e),n=m.addMonths(t,this._paneNumber-1),r=this.get("minimumDate"),i=this.get("maximumDate");if((!r||m.isGreaterOrEqual(t,r))&&(!i||m.isGreaterOrEqual(i,n)))return t;if(r&&m.isGreater(r,t))return this._normalizeDate(r);if(i&&m.isGreater(n,i))return m.addMonths(this._normalizeDate(i),1-this._paneNumber)}},minimumDate:{value:null,setter:function(t){if(e.Lang.isDate(t)){var n=this.get("date"),r=this._normalizeTime(t);return n&&!m.isGreaterOrEqual(n,r)&&this.set("date",t),r}return null}},
+maximumDate:{value:null,setter:function(t){if(e.Lang.isDate(t)){var n=this.get("date");return n&&!m.isGreaterOrEqual(t,m.addMonths(n,this._paneNumber-1))&&this.set("date",m.addMonths(this._normalizeDate(t),1-this._paneNumber)),this._normalizeTime(t)}return null}}}})},"3.17.2",{requires:["calendar-base","calendarnavigator"],skinnable:!0});
diff --git a/js/yui3/calendarnavigator/assets/calendarnavigator-core.css b/js/yui3/calendarnavigator/assets/calendarnavigator-core.css
new file mode 100644
index 000000000..1f1c0754a
--- /dev/null
+++ b/js/yui3/calendarnavigator/assets/calendarnavigator-core.css
@@ -0,0 +1,25 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-calendar-header {
+ text-align: center;
+ position: relative;
+ /* ie6 width fix */
+ width: 100%;
+}
+
+.yui3-calendar-header-label {
+ display: inline;
+}
+
+.yui3-calendarnav-prevmonth {
+ cursor: pointer;
+}
+
+.yui3-calendarnav-nextmonth {
+ cursor: pointer;
+} \ No newline at end of file
diff --git a/js/yui3/calendarnavigator/assets/skins/night/calendarnavigator.css b/js/yui3/calendarnavigator/assets/skins/night/calendarnavigator.css
new file mode 100644
index 000000000..395aa419a
--- /dev/null
+++ b/js/yui3/calendarnavigator/assets/skins/night/calendarnavigator.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-calendar-header{text-align:center;position:relative;width:100%}.yui3-calendar-header-label{display:inline}.yui3-calendarnav-prevmonth{cursor:pointer}.yui3-calendarnav-nextmonth{cursor:pointer}.yui3-skin-night .yui3-calendarnav-prevmonth,.yui3-skin-night .yui3-calendarnav-nextmonth{width:0;height:0;padding:0;margin:0;border:10px solid transparent;position:absolute;font-size:0;line-height:0;_border-left-color:black;_border-top-color:black;_border-right-color:black;_border-bottom-color:black;_filter:chroma(color=black)}.yui3-skin-night .yui3-calendarnav-prevmonth:hover,[dir="rtl"] .yui3-skin-night .yui3-calendarnav-nextmonth:hover,.yui3-skin-night [dir="rtl"] .yui3-calendarnav-nextmonth:hover{border-right-color:#06c}.yui3-skin-night .yui3-calendarnav-nextmonth:hover,[dir="rtl"] .yui3-skin-night .yui3-calendarnav-prevmonth:hover,.yui3-skin-night [dir="rtl"] .yui3-calendarnav-prevmonth:hover{border-left-color:#06c}.yui3-skin-night .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,.yui3-skin-night .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover,[dir="rtl"] .yui3-skin-night .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,.yui3-skin-night [dir="rtl"] .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,[dir="rtl"] .yui3-skin-night .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover,.yui3-skin-night [dir="rtl"] .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover{cursor:default;border-right-color:#ccc;border-left-color:transparent}.yui3-skin-night .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,.yui3-skin-night .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover,[dir="rtl"] .yui3-skin-night .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,.yui3-skin-night [dir="rtl"] .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,[dir="rtl"] .yui3-skin-night .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover,.yui3-skin-night [dir="rtl"] .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover{cursor:default;border-left-color:#ccc;border-right-color:transparent}.yui3-skin-night .yui3-calendarnav-prevmonth{border-right-color:#fff;left:0;margin-left:-10px}.yui3-skin-night .yui3-calendarnav-nextmonth{border-left-color:#fff;right:0;margin-right:-10px}[dir="rtl"] .yui3-skin-night .yui3-calendarnav-prevmonth,.yui3-skin-night [dir="rtl"] .yui3-calendarnav-prevmonth{left:auto;right:0;border-left-color:#fff;border-right-color:transparent}[dir="rtl"] .yui3-skin-night .yui3-calendarnav-nextmonth,.yui3-skin-night [dir="rtl"] .yui3-calendarnav-nextmonth{left:0;right:auto;border-right-color:#fff;border-left-color:transparent}#yui3-css-stamp.skin-night-calendarnavigator{display:none}
diff --git a/js/yui3/calendarnavigator/assets/skins/sam/calendarnavigator.css b/js/yui3/calendarnavigator/assets/skins/sam/calendarnavigator.css
new file mode 100644
index 000000000..f514f76ca
--- /dev/null
+++ b/js/yui3/calendarnavigator/assets/skins/sam/calendarnavigator.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-calendar-header{text-align:center;position:relative;width:100%}.yui3-calendar-header-label{display:inline}.yui3-calendarnav-prevmonth{cursor:pointer}.yui3-calendarnav-nextmonth{cursor:pointer}.yui3-skin-sam .yui3-calendarnav-prevmonth,.yui3-skin-sam .yui3-calendarnav-nextmonth{width:0;height:0;padding:0;margin:0;border:10px solid transparent;position:absolute;font-size:0;line-height:0;_border-left-color:white;_border-top-color:white;_border-right-color:white;_border-bottom-color:white;_filter:chroma(color=white)}.yui3-skin-sam .yui3-calendarnav-prevmonth:hover,[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-nextmonth:hover,.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-nextmonth:hover{border-right-color:#06c}.yui3-skin-sam .yui3-calendarnav-nextmonth:hover,[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-prevmonth:hover,.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-prevmonth:hover{border-left-color:#06c}.yui3-skin-sam .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,.yui3-skin-sam .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover,[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover,.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover{cursor:default;border-right-color:#ccc;border-left-color:transparent}.yui3-skin-sam .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled,.yui3-skin-sam .yui3-calendarnav-nextmonth.yui3-calendarnav-month-disabled:hover,[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled,[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover,.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-prevmonth.yui3-calendarnav-month-disabled:hover{cursor:default;border-left-color:#ccc;border-right-color:transparent}.yui3-skin-sam .yui3-calendarnav-prevmonth{border-right-color:#000;left:0;margin-left:-10px}.yui3-skin-sam .yui3-calendarnav-nextmonth{border-left-color:#000;right:0;margin-right:-10px}[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-prevmonth,.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-prevmonth{left:auto;right:0;border-left-color:#000;border-right-color:transparent}[dir="rtl"] .yui3-skin-sam .yui3-calendarnav-nextmonth,.yui3-skin-sam [dir="rtl"] .yui3-calendarnav-nextmonth{left:0;right:auto;border-right-color:#000;border-left-color:transparent}#yui3-css-stamp.skin-sam-calendarnavigator{display:none}
diff --git a/js/yui3/calendarnavigator/calendarnavigator-min.js b/js/yui3/calendarnavigator/calendarnavigator-min.js
new file mode 100644
index 000000000..8a1ff692c
--- /dev/null
+++ b/js/yui3/calendarnavigator/calendarnavigator-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("calendarnavigator",function(e,t){function v(){v.superclass.constructor.apply(this,arguments)}var n="contentBox",r="host",i=e.ClassNameManager.getClassName,s=e.Lang.sub,o=e.Node,u=o.create,a="calendar",f="calendarnav",l=i(a,"header"),c=i(f,"prevmonth"),h=i(f,"nextmonth"),p=i(f,"month-disabled"),d=e.DataType.Date;v.NS="navigator",v.NAME="pluginCalendarNavigator",v.ATTRS={shiftByMonths:{value:1}},v.CALENDARNAV_STRINGS={prev_month_class:c,next_month_class:h},v.PREV_MONTH_CONTROL_TEMPLATE='<a class="yui3-u {prev_month_class}" role="button" aria-label="{prev_month_arialabel}" tabindex="{control_tabindex}" />',v.NEXT_MONTH_CONTROL_TEMPLATE='<a class="yui3-u {next_month_class}" role="button" aria-label="{next_month_arialabel}" tabindex="{control_tabindex}" />',e.extend(v,e.Plugin.Base,{_eventAttachments:{},_controls:{},initializer:function(){this._controls={},this._eventAttachments={},this.afterHostMethod("renderUI",this._initNavigationControls)},destructor:function(){},_focusNavigation:function(e){e.currentTarget.focus()},_subtractMonths:function(e){if(e.type==="click"||e.type==="keydown"&&(e.keyCode===13||e.keyCode===32)){var t=this.get(r),n=t.get("date");t.set("date",d.addMonths(n,-1*this.get("shiftByMonths"))),e.preventDefault()}},_addMonths:function(e){if(e.type==="click"||e.type==="keydown"&&(e.keyCode===13||e.keyCode===32)){var t=this.get(r),n=t.get("date");t.set("date",d.addMonths(n,this.get("shiftByMonths"))),e.preventDefault()}},_updateControlState:function(){var e=this.get(r),t=e.get("date"),n=d.addMonths(t,e._paneNumber-1),i=e._normalizeDate(e.get("minimumDate")),s=e._normalizeDate(e.get("maximumDate"));d.areEqual(i,t)?(this._eventAttachments.prevMonth&&(this._eventAttachments.prevMonth.detach(),this._eventAttachments.prevMonth=!1),this._controls.prevMonth.hasClass(p)||this._controls.prevMonth.addClass(p).setAttribute("aria-disabled","true")):(this._eventAttachments.prevMonth||(this._eventAttachments.prevMonth=this._controls.prevMonth.on(["click","keydown"],this._subtractMonths,this)),this._controls.prevMonth.hasClass(p)&&this._controls.prevMonth.removeClass(p).setAttribute("aria-disabled","false")),d.areEqual(s,n)?(this._eventAttachments.nextMonth&&(this._eventAttachments.nextMonth.detach(),this._eventAttachments.nextMonth=!1),this._controls.nextMonth.hasClass(p)||this._controls.nextMonth.addClass(p).setAttribute("aria-disabled","true")):(this._eventAttachments.nextMonth||(this._eventAttachments.nextMonth=this._controls.nextMonth.on(["click","keydown"],this._addMonths,this)),this._controls.nextMonth.hasClass(p)&&this._controls.nextMonth.removeClass(p).setAttribute("aria-disabled","false")),this._controls.prevMonth.on(["click","keydown"],this._focusNavigation,this),this._controls.nextMonth.on(["click","keydown"],this._focusNavigation,this)},_renderPrevControls:function(){var e=u(s(v.PREV_MONTH_CONTROL_TEMPLATE,v.CALENDARNAV_STRINGS));return e.on("selectstart",this.get(r)._preventSelectionStart),e},_renderNextControls:function(){var e=u(s(v.NEXT_MONTH_CONTROL_TEMPLATE,v.CALENDARNAV_STRINGS));return e.on("selectstart",this.get(r)._preventSelectionStart),e},_initNavigationControls:function(){var e=this.get(r),t=e.get(n).one("."+l);v.CALENDARNAV_STRINGS.control_tabindex=e.get("tabIndex"),v.CALENDARNAV_STRINGS.prev_month_arialabel="Go to previous month",v.CALENDARNAV_STRINGS.next_month_arialabel="Go to next month",this._controls.prevMonth=this._renderPrevControls(),this._controls.nextMonth=this._renderNextControls(),this._updateControlState(),e.after(["dateChange","minimumDateChange","maximumDateChange"],this._updateControlState,this),t.prepend(this._controls.prevMonth),t.append(this._controls.nextMonth)}}),e.namespace("Plugin").CalendarNavigator=v},"3.17.2",{requires:["plugin","classnamemanager","datatype-date","node"],skinnable:!0});
diff --git a/js/yui3/charts-base/charts-base-min.js b/js/yui3/charts-base/charts-base-min.js
new file mode 100644
index 000000000..291991554
--- /dev/null
+++ b/js/yui3/charts-base/charts-base-min.js
@@ -0,0 +1,15 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("charts-base",function(e,t){function f(){}function l(t){return t.type!=="pie"?new e.CartesianChart(t):new e.PieChart(t)}var n=e.config,r=n.win,i=n.doc,s=e.Lang,o=s.isString,u=e.ClassNameManager.getClassName,a=u("seriesmarker");e.Gridlines=e.Base.create("gridlines",e.Base,[e.Renderer],{_path:null,remove:function(){var e=this._path;e&&e.destroy()},draw:function(){this.get("axis")&&this.get("graph")&&this._drawGridlines()},_drawGridlines:function(){var t,n=this.get("axis"),r=n.get("position"),i,s=0,o,u=this.get("direction"),a=this.get("graph"),f=a.get("width"),l=a.get("height"),c=this.get("styles").line,h=c.color,p=c.weight,d=c.alpha,v=this.get("count"),m,g;if(isFinite(f)&&isFinite(l)&&f>0&&l>0){v&&e.Lang.isNumber(v)?i=this._getPoints(v,f,l):r!=="none"&&n&&n.get("tickPoints")?i=n.get("tickPoints"):i=this._getPoints(n.get("styles").majorUnit.count,f,l),o=i.length,t=a.get("gridlines"),t.set("width",f),t.set("height",l),t.set("stroke",{weight:p,color:h,opacity:d}),u==="vertical"?(g=this._verticalLine,m=l):(g=this._horizontalLine,m=f);for(s=0;s<o;s+=1)g(t,i[s],m);t.end()}},_getPoints:function(e,t,n){var r,i=[],s,o=e-1;for(r=0;r<e;r+=1)s=r/o,i[r]={x:t*s,y:n*s};return i},_horizontalLine:function(e,t,n){e.moveTo(0,t.y),e.lineTo(n,t.y)},_verticalLine:function(e,t,n){e.moveTo(t.x,0),e.lineTo(t.x,n)},_getDefaultStyles:function(){var e={line:{color:"#f0efe9",weight:1,alpha:1}};return e}},{ATTRS:{direction:{},axis:{},graph:{},count:{}}}),e.Graph=e.Base.create("graph",e.Widget,[e.Renderer],{bindUI:function(){var e=this.get("boundingBox");e.setStyle("position","absolute"),this.after("widthChange",this._sizeChangeHandler),this.after("heightChange",this._sizeChangeHandler),this.after("stylesChange",this._updateStyles),this.after("groupMarkersChange",this._drawSeries)},syncUI:function(){var t,n,r,i=this.get("seriesCollection"),s,o=0,u=i?i.length:0,a=this.get("horizontalGridlines"),f=this.get("verticalGridlines");this.get("showBackground")&&(t=this.get("background"),n=this.get("contentBox"),r=this.get("styles").background,r.stroke=r.border,r.stroke.opacity=r.stroke.alpha,r.fill.opacity=r.fill.alpha,r.width=this.get("width"),r.height=this.get("height"),r.type=r.shape,t.set(r));for(;o<u;++o)s=i[o],s instanceof e.SeriesBase&&s.render();a&&a instanceof e.Gridlines&&a.draw(),f&&f instanceof e.Gridlines&&f.draw()},seriesTypes:null,getSeriesByIndex:function(e){var t=this.get("seriesCollection"),n;return t&&t.length>e&&(n=t[e]),n},getSeriesByKey:function(e){var t=this._seriesDictionary,n;return t&&t.hasOwnProperty(e)&&(n=t[e]),n},addDispatcher:function(e){this._dispatchers||(this._dispatchers=[]),this._dispatchers.push(e)},_seriesCollection:null,_seriesDictionary:null,_parseSeriesCollection:function(t){if(!t)return;var n=t.length,r=0,i,s;this._seriesCollection=[],this._seriesDictionary={},this.seriesTypes=[];for(;r<n;++r){i=t[r];if(!(i instanceof e.CartesianSeries||i instanceof e.PieSeries)){this._createSeries(i);continue}this._addSeries(i)}n=this._seriesCollection.length;for(r=0;r<n;++r)i=this.get("seriesCollection")[r],s=i.get("direction")==="horizontal"?"yKey":"xKey",this._seriesDictionary[i.get(s)]=i},_addSeries:function(t){var n=t.get("type"),r=this.get("seriesCollection"),i=r.length,s=this.seriesTypes,o;t.get("graph")||t.set("graph",this),r.push(t),s.hasOwnProperty(n)||(this.seriesTypes[n]=[]),o=this.seriesTypes[n],t.set("graphOrder",i),t.set("order",o.length),o.push(t),t.set("seriesTypeCollection",o),this.addDispatcher(t),t.after("drawingComplete",e.bind(this._drawingCompleteHandler,this)),this.fire("seriesAdded",t)},_createSeries:function(t){var n=t.type,r=this.get("seriesCollection"),i=this.seriesTypes,s,o,u;t.graph=this,i.hasOwnProperty(n)||(i[n]=[]),s=i[n],t.graph=this,t.order=s.length,t.graphOrder=r.length,o=this._getSeries(t.type),u=new o(t),this.addDispatcher(u),u.after("drawingComplete",e.bind(this._drawingCompleteHandler,this)),s.push(u),r.push(u),u.set("seriesTypeCollection",s),this.get("rendered")&&u.render()},_seriesMap:{line:e.LineSeries,column:e.ColumnSeries,bar:e.BarSeries,area:e.AreaSeries,candlestick:e.CandlestickSeries,ohlc:e.OHLCSeries,stackedarea:e.StackedAreaSeries,stackedline:e.StackedLineSeries,stackedcolumn:e.StackedColumnSeries,stackedbar:e.StackedBarSeries,markerseries:e.MarkerSeries,spline:e.SplineSeries,areaspline:e.AreaSplineSeries,stackedspline:e.StackedSplineSeries,stackedareaspline:e.StackedAreaSplineSeries,stackedmarkerseries:e.StackedMarkerSeries,pie:e.PieSeries,combo:e.ComboSeries,stackedcombo:e.StackedComboSeries,combospline:e.ComboSplineSeries,stackedcombospline:e.StackedComboSplineSeries},_getSeries:function(e){var t;return s.isString(e)?t=this._seriesMap[e]:t=e,t},_markerEventHandler:function(e){var t=e.type,n=e.currentTarget,r=n.getAttribute("id").split("_"),i=this.getSeriesByIndex(r[1]),s=r[2];i.updateMarkerState(t,s)},_dispatchers:null,_updateStyles:function(){var e=this.get("styles").background,t=e.border;t.opacity=t.alpha,e.stroke=t,e.fill.opacity=e.fill.alpha,this.get("background").set(e),this._sizeChangeHandler()},_sizeChangeHandler:function(){var t=this.get("horizontalGridlines"),n=this.get("verticalGridlines"),r=this.get("width"),i=this.get("height"),s=this.get("styles").background,o,u;s&&s.border&&(o=s.border.weight||0),this.get("showBackground")&&(u=this.get("background"),r&&i&&(u.set("width",r),u.set("height",i))),this._gridlines&&this._gridlines.clear(),t&&t instanceof e.Gridlines&&t.draw(),n&&n instanceof e.Gridlines&&n.draw(),this._drawSeries()},_drawSeries:function(){if(this._drawing){this._callLater=!0;return}var t,n,r,i=this.get("graphic");i.set("autoDraw",!1),i.set("width",this.get("width")),i.set("height",this.get("height")),this._callLater=!1,this._drawing=!0,t=this.get("seriesCollection"),n=0,r=t?t.length:0;for(;n<r;++n){t[n].draw();if((!t[n].get("xcoords")||!t[n].get("ycoords"))&&!t[n]instanceof e.PieSeries){this._callLater=!0;break}}this._drawing=!1,this._callLater&&this._drawSeries()},_drawingCompleteHandler:function(t){var n=t.currentTarget
+,r,i=e.Array.indexOf(this._dispatchers,n);i>-1&&this._dispatchers.splice(i,1),this._dispatchers.length<1&&(r=this.get("graphic"),r.get("autoDraw")||r._redraw(),this.fire("chartRendered"))},_getDefaultStyles:function(){var e={background:{shape:"rect",fill:{color:"#faf9f2"},border:{color:"#dad8c9",weight:1}}};return e},destructor:function(){this._graphic&&(this._graphic.destroy(),this._graphic=null),this._background&&(this._background.get("graphic").destroy(),this._background=null),this._gridlines&&(this._gridlines.get("graphic").destroy(),this._gridlines=null)}},{ATTRS:{x:{setter:function(e){return this.get("boundingBox").setStyle("left",e+"px"),e}},y:{setter:function(e){return this.get("boundingBox").setStyle("top",e+"px"),e}},chart:{getter:function(){var e=this._state.chart||this;return e}},seriesCollection:{getter:function(){return this._seriesCollection},setter:function(e){return this._parseSeriesCollection(e),this._seriesCollection}},showBackground:{value:!0},seriesDictionary:{readOnly:!0,getter:function(){return this._seriesDictionary}},horizontalGridlines:{value:null,setter:function(t){var n,r,i=this.get("horizontalGridlines");i&&i instanceof e.Gridlines&&i.remove();if(t instanceof e.Gridlines)return i=t,t.set("graph",this),t;if(t){n={direction:"horizonal",graph:this};for(r in t)t.hasOwnProperty(r)&&(n[r]=t[r]);return i=new e.Gridlines(n),i}}},verticalGridlines:{value:null,setter:function(t){var n,r,i=this.get("verticalGridlines");i&&i instanceof e.Gridlines&&i.remove();if(t instanceof e.Gridlines)return i=t,t.set("graph",this),t;if(t){n={direction:"vertical",graph:this};for(r in t)t.hasOwnProperty(r)&&(n[r]=t[r]);return i=new e.Gridlines(n),i}}},background:{getter:function(){return this._background||(this._backgroundGraphic=new e.Graphic({render:this.get("contentBox")}),this._backgroundGraphic.get("node").style.zIndex=0,this._background=this._backgroundGraphic.addShape({type:"rect"})),this._background}},gridlines:{readOnly:!0,getter:function(){return this._gridlines||(this._gridlinesGraphic=new e.Graphic({render:this.get("contentBox")}),this._gridlinesGraphic.get("node").style.zIndex=1,this._gridlines=this._gridlinesGraphic.addShape({type:"path"})),this._gridlines}},graphic:{readOnly:!0,getter:function(){return this._graphic||(this._graphic=new e.Graphic({render:this.get("contentBox")}),this._graphic.get("node").style.zIndex=2,this._graphic.set("autoDraw",!1)),this._graphic}},groupMarkers:{value:!1}}}),f.ATTRS={dataProvider:{lazyAdd:!1,valueFn:function(){var e=[];return this._wereSeriesKeysExplicitlySet()||this.set("seriesKeys",this._buildSeriesKeys(e),{src:"internal"}),e},setter:function(e){var t=this._setDataValues(e);return this._wereSeriesKeysExplicitlySet()||this.set("seriesKeys",this._buildSeriesKeys(t),{src:"internal"}),t}},seriesKeys:{lazyAdd:!1,setter:function(e){var t=arguments[2];return!e||t&&t.src&&t.src==="internal"?this._seriesKeysExplicitlySet=!1:this._seriesKeysExplicitlySet=!0,e}},ariaLabel:{value:"Chart Application",setter:function(e){var t=this.get("contentBox");return t&&t.setAttribute("aria-label",e),e}},ariaDescription:{value:"Use the up and down keys to navigate between series. Use the left and right keys to navigate through items in a series.",setter:function(e){return this._description&&this._description.set("text",e),e}},tooltip:{valueFn:"_getTooltip",setter:function(e){return this._updateTooltip(e)}},categoryKey:{value:"category"},categoryType:{value:"category"},interactionType:{value:"marker"},axesCollection:{},graph:{valueFn:"_getGraph"},groupMarkers:{value:!1}},f.prototype={_wereSeriesKeysExplicitlySet:function(){var e=this.get("seriesKeys");return e&&this._seriesKeysExplicitlySet},_groupMarkersChangeHandler:function(e){var t=this.get("graph"),n=e.newVal;t&&t.set("groupMarkers",n)},_itemRendered:function(t){this._itemRenderQueue=this._itemRenderQueue.splice(1+e.Array.indexOf(this._itemRenderQueue,t.currentTarget),1),this._itemRenderQueue.length<1&&this._redraw()},_getGraph:function(){var t=new e.Graph({chart:this,groupMarkers:this.get("groupMarkers")});return t.after("chartRendered",e.bind(function(){this.fire("chartRendered")},this)),t},getSeries:function(e){var t=null,n=this.get("graph");return n&&(s.isNumber(e)?t=n.getSeriesByIndex(e):t=n.getSeriesByKey(e)),t},getAxisByKey:function(e){var t,n=this.get("axes");return n&&n.hasOwnProperty(e)&&(t=n[e]),t},getCategoryAxis:function(){var e,t=this.get("categoryKey"),n=this.get("axes");return n.hasOwnProperty(t)&&(e=n[t]),e},_direction:"horizontal",_dataProvider:null,_setDataValues:function(e){if(s.isArray(e[0])){var t,n=[],r=e[0],i=0,o=r.length,u,a=e.length;for(;i<o;++i){t={category:r[i]};for(u=1;u<a;++u)t["series"+u]=e[u][i];n[i]=t}return n}return e},_seriesCollection:null,_setSeriesCollection:function(e){this._seriesCollection=e},_getAxisClass:function(e){return this._axisClass[e]},_axisClass:{stacked:e.StackedAxis,numeric:e.NumericAxis,category:e.CategoryAxis,time:e.TimeAxis},_axes:null,initializer:function(){this._itemRenderQueue=[],this._seriesIndex=-1,this._itemIndex=-1,this.after("dataProviderChange",this._dataProviderChangeHandler)},renderUI:function(){var e=this.get("tooltip"),t=this.get("boundingBox"),n=this.get("contentBox");t.setStyle("position","absolute"),n.setStyle("position","absolute"),this._addAxes(),this._addSeries(),e&&e.show&&this._addTooltip(),this._setAriaElements(t,n)},_setAriaElements:function(e,t){var n=this._getAriaOffscreenNode(),r=this.get("id")+"_description",i=this._getAriaOffscreenNode();t.set("tabIndex",0),t.set("role","img"),t.setAttribute("aria-label",this.get("ariaLabel")),t.setAttribute("aria-describedby",r),n.set("id",r),n.set("tabIndex",-1),n.set("text",this.get("ariaDescription")),i.set("id","live-region"),i.set("aria-live","polite"),i.set("aria-atomic","true"),i.set("role","status"),e.setAttribute("role","application"),e.appendChild(n),e.appendChild(i),this._description=n,this._liveRegion=i},_getAriaOffscreenNode:function(){var t=e.Node.create("<div></div>"),n=e.UA.
+ie,r=n&&n<8?"rect(1px 1px 1px 1px)":"rect(1px, 1px, 1px, 1px)";return t.setStyle("position","absolute"),t.setStyle("height","1px"),t.setStyle("width","1px"),t.setStyle("overflow","hidden"),t.setStyle("clip",r),t},syncUI:function(){this._redraw()},bindUI:function(){this.after("tooltipChange",e.bind(this._tooltipChangeHandler,this)),this.after("widthChange",this._sizeChanged),this.after("heightChange",this._sizeChanged),this.after("groupMarkersChange",this._groupMarkersChangeHandler);var t=this.get("tooltip"),n="mouseout",i="mouseover",o=this.get("contentBox"),u=this.get("interactionType"),f=0,l,c="."+a,h=r&&"ontouchstart"in r&&!(e.UA.chrome&&e.UA.chrome<6);e.on("keydown",e.bind(function(e){var t=e.keyCode,n=parseFloat(t),r;n>36&&n<41&&(e.halt(),r=this._getAriaMessage(n),this._liveRegion.set("text",r))},this),this.get("contentBox")),u==="marker"?(n=t.hideEvent,i=t.showEvent,h?(e.delegate("touchend",e.bind(this._markerEventDispatcher,this),o,c),e.on("touchend",e.bind(function(e){o.contains(e.target)&&e.halt(!0),this._activeMarker&&(this._activeMarker=null,this.hideTooltip(e))},this))):(e.delegate("mouseenter",e.bind(this._markerEventDispatcher,this),o,c),e.delegate("mousedown",e.bind(this._markerEventDispatcher,this),o,c),e.delegate("mouseup",e.bind(this._markerEventDispatcher,this),o,c),e.delegate("mouseleave",e.bind(this._markerEventDispatcher,this),o,c),e.delegate("click",e.bind(this._markerEventDispatcher,this),o,c),e.delegate("mousemove",e.bind(this._positionTooltip,this),o,c))):u==="planar"&&(h?this._overlay.on("touchend",e.bind(this._planarEventDispatcher,this)):(this._overlay.on("mousemove",e.bind(this._planarEventDispatcher,this)),this.on("mouseout",this.hideTooltip)));if(t){this.on("markerEvent:touchend",e.bind(function(e){var n=e.series.get("markers")[e.index];this._activeMarker&&n===this._activeMarker?(this._activeMarker=null,this.hideTooltip(e)):(this._activeMarker=n,t.markerEventHandler.apply(this,[e]))},this));if(n&&i&&n===i)this.on(u+"Event:"+n,this.toggleTooltip);else{i&&this.on(u+"Event:"+i,t[u+"EventHandler"]);if(n){if(s.isArray(n)){l=n.length;for(;f<l;++f)this.on(u+"Event:"+n[f],this.hideTooltip)}this.on(u+"Event:"+n,this.hideTooltip)}}}},_markerEventDispatcher:function(e){var t=e.type,n=this.get("contentBox"),r=e.currentTarget,i=r.getAttribute("id").split("_"),s=i.pop(),o=i.pop(),u=this.getSeries(parseInt(o,10)),a=this.getSeriesItems(u,s),f=e&&e.hasOwnProperty("changedTouches"),l=f?e.changedTouches[0].pageX:e.pageX,c=f?e.changedTouches[0].pageY:e.pageY,h=l-n.getX(),p=c-n.getY();t==="mouseenter"?t="mouseover":t==="mouseleave"&&(t="mouseout"),u.updateMarkerState(t,s),e.halt(),this.fire("markerEvent:"+t,{originEvent:e,pageX:l,pageY:c,categoryItem:a.category,valueItem:a.value,node:r,x:h,y:p,series:u,index:s,seriesIndex:o})},_dataProviderChangeHandler:function(t){var n=t.newVal,r,i,s;this._seriesIndex=-1,this._itemIndex=-1,this instanceof e.CartesianChart&&(this.set("axes",this.get("axes")),this.set("seriesCollection",this.get("seriesCollection"))),r=this.get("axes");if(r)for(i in r)r.hasOwnProperty(i)&&(s=r[i],s instanceof e.Axis&&(s.get("position")!=="none"&&this._addToAxesRenderQueue(s),s.set("dataProvider",n)))},toggleTooltip:function(e){var t=this.get("tooltip");t.visible?this.hideTooltip():t.markerEventHandler.apply(this,[e])},_showTooltip:function(e,t,n){var r=this.get("tooltip"),i=r.node;e&&(r.visible=!0,r.setTextFunction(i,e),i.setStyle("top",n+"px"),i.setStyle("left",t+"px"),i.setStyle("visibility","visible"))},_positionTooltip:function(e){var t=this.get("tooltip"),n=t.node,r=this.get("contentBox"),i=e.pageX+10-r.getX(),s=e.pageY+10-r.getY();n&&(n.setStyle("left",i+"px"),n.setStyle("top",s+"px"))},hideTooltip:function(){var e=this.get("tooltip"),t=e.node;e.visible=!1,t.set("innerHTML",""),t.setStyle("left",-1e4),t.setStyle("top",-1e4),t.setStyle("visibility","hidden")},_addTooltip:function(){var e=this.get("tooltip"),t=this.get("id")+"_tooltip",n=this.get("contentBox"),r=i.getElementById(t);r&&n.removeChild(r),e.node.set("id",t),e.node.setStyle("visibility","hidden"),n.appendChild(e.node)},_updateTooltip:function(t){var n=this.get("tooltip")||this._getTooltip(),r,i,o,u={markerLabelFunction:"markerLabelFunction",planarLabelFunction:"planarLabelFunction",setTextFunction:"setTextFunction",showEvent:"showEvent",hideEvent:"hideEvent",markerEventHandler:"markerEventHandler",planarEventHandler:"planarEventHandler",show:"show"};if(s.isObject(t)){i=t.styles,t.node&&n.node?(n.node.destroy(!0),o=e.one(t.node)):o=n.node;if(i)for(r in i)i.hasOwnProperty(r)&&o.setStyle(r,i[r]);for(r in u)t.hasOwnProperty(r)&&(n[r]=t[r]);n.node=o}return n},_getTooltip:function(){var t=i.createElement("div"),n=u("chart-tooltip"),r={setTextFunction:this._setText,markerLabelFunction:this._tooltipLabelFunction,planarLabelFunction:this._planarLabelFunction,show:!0,hideEvent:"mouseout",showEvent:"mouseover",markerEventHandler:function(e){var t=this.get("tooltip"),n=t.markerLabelFunction.apply(this,[e.categoryItem,e.valueItem,e.index,e.series,e.seriesIndex]);this._showTooltip(n,e.x+10,e.y+10)},planarEventHandler:function(e){var t=this.get("tooltip"),n,r=this.get("categoryAxis");n=t.planarLabelFunction.apply(this,[r,e.valueItem,e.index,e.items,e.seriesIndex]),this._showTooltip(n,e.x+10,e.y+10)}};return t=e.one(t),t.set("id",this.get("id")+"_tooltip"),t.setStyle("fontSize","85%"),t.setStyle("opacity","0.83"),t.setStyle("position","absolute"),t.setStyle("paddingTop","2px"),t.setStyle("paddingRight","5px"),t.setStyle("paddingBottom","4px"),t.setStyle("paddingLeft","2px"),t.setStyle("backgroundColor","#fff"),t.setStyle("border","1px solid #dbdccc"),t.setStyle("pointerEvents","none"),t.setStyle("zIndex",3),t.setStyle("whiteSpace","noWrap"),t.setStyle("visibility","hidden"),t.addClass(n),r.node=e.one(t),r},_planarLabelFunction:function(e,t,n,r){var o=i.createElement("div"),u,a=0,f=r.length,l,c,h,p;e&&(c=e.get("labelFunction").apply(this,[e.getKeyValueAt(this.get("categoryKey"),n),e.get("labelFormat")]),s.isObject
+(c)||(c=i.createTextNode(c)),o.appendChild(c));for(;a<f;++a)p=r[a],p.get("visible")&&(u=t[a],l=u.axis,h=l.get("labelFunction").apply(this,[l.getKeyValueAt(u.key,n),l.get("labelFormat")]),o.appendChild(i.createElement("br")),o.appendChild(i.createTextNode(u.displayName)),o.appendChild(i.createTextNode(": ")),s.isObject(h)||(h=i.createTextNode(h)),o.appendChild(h));return o},_tooltipLabelFunction:function(e,t){var n=i.createElement("div"),r=e.axis.get("labelFunction").apply(this,[e.value,e.axis.get("labelFormat")]),o=t.axis.get("labelFunction").apply(this,[t.value,t.axis.get("labelFormat")]);return n.appendChild(i.createTextNode(e.displayName)),n.appendChild(i.createTextNode(": ")),s.isObject(r)||(r=i.createTextNode(r)),n.appendChild(r),n.appendChild(i.createElement("br")),n.appendChild(i.createTextNode(t.displayName)),n.appendChild(i.createTextNode(": ")),s.isObject(o)||(o=i.createTextNode(o)),n.appendChild(o),n},_tooltipChangeHandler:function(){if(this.get("tooltip")){var e=this.get("tooltip"),t=e.node,n=e.show,r=this.get("contentBox");t&&n&&(r.contains(t)||this._addTooltip())}},_setText:function(e,t){e.empty(),s.isNumber(t)?t+="":t||(t=""),o(t)&&(t=i.createTextNode(t)),e.appendChild(t)},_getAllKeys:function(e){var t=0,n=e.length,r,i,s={};for(;t<n;++t){r=e[t];for(i in r)r.hasOwnProperty(i)&&(s[i]=!0)}return s},_buildSeriesKeys:function(e){var t,n=this.get("categoryKey"),r=[],i;if(this._seriesKeysExplicitlySet)return this._seriesKeys;t=this._getAllKeys(e);for(i in t)t.hasOwnProperty(i)&&i!==n&&r.push(i);return r}},e.ChartBase=f,e.CartesianChart=e.Base.create("cartesianChart",e.Widget,[e.ChartBase,e.Renderer],{renderUI:function(){var t=this.get("boundingBox"),n=this.get("contentBox"),r=this.get("tooltip"),i=u("overlay");t.setStyle("position","absolute"),n.setStyle("position","absolute"),this._addAxes(),this._addGridlines(),this._addSeries(),r&&r.show&&this._addTooltip(),this.get("interactionType")==="planar"&&(this._overlay=e.Node.create("<div></div>"),this._overlay.set("id",this.get("id")+"_overlay"),this._overlay.setStyle("position","absolute"),this._overlay.setStyle("background","#fff"),this._overlay.setStyle("opacity",0),this._overlay.addClass(i),this._overlay.setStyle("zIndex",4),n.append(this._overlay)),this._setAriaElements(t,n),this._redraw()},_planarEventDispatcher:function(e){var t=this.get("graph"),n=this.get("boundingBox"),r=t.get("contentBox"),i=e&&e.hasOwnProperty("changedTouches"),s=i?e.changedTouches[0].pageX:e.pageX,o=i?e.changedTouches[0].pageY:e.pageY,u=s-n.getX(),a=o-n.getY(),f={x:s-r.getX(),y:o-r.getY()},l=t.get("seriesCollection"),c,h=0,p,d=this._selectedIndex,v,m=[],g=[],y=[],b=this.get("direction"),w,E,S,x,T,N,C;e.halt(!0),b==="horizontal"?(E="x",S="y"):(S="x",E="y"),x=f[E];if(l){N=l.length;while(h<N&&!T)l[h]&&(T=l[h].get(E+"MarkerPlane")),h++}if(T){N=T.length;for(h=0;h<N;++h)if(x<=T[h].end&&x>=T[h].start){p=h;break}N=l.length;for(h=0;h<N;++h)c=l[h],C=c.get(S+"coords"),w=c.get("markers"),w&&!isNaN(d)&&d>-1&&c.updateMarkerState("mouseout",d),C&&C[p]>-1&&(w&&!isNaN(p)&&p>-1&&c.updateMarkerState("mouseover",p),v=this.getSeriesItems(c,p),g.push(v.category),y.push(v.value),m.push(c));this._selectedIndex=p,p>-1?this.fire("planarEvent:mouseover",{categoryItem:g,valueItem:y,x:u,y:a,pageX:s,pageY:o,items:m,index:p,originEvent:e}):this.fire("planarEvent:mouseout")}},_type:"combo",_itemRenderQueue:null,_addToAxesRenderQueue:function(t){this._itemRenderQueue||(this._itemRenderQueue=[]),e.Array.indexOf(this._itemRenderQueue,t)<0&&this._itemRenderQueue.push(t)},_addToAxesCollection:function(e,t){var n=this.get(e+"AxesCollection");n||(n=[],this.set(e+"AxesCollection",n)),n.push(t)},_getDefaultSeriesCollection:function(){var e,t=this.get("dataProvider");return t&&(e=this._parseSeriesCollection()),e},_parseSeriesCollection:function(t){var n=this.get("direction"),r=this.get("styles").series,i=r&&s.isArray(r),o,u,a,f=[],l,c,h=[],p,d=this.get("seriesKeys").concat(),v,m,g,y=this.get("type"),b,w,E,S,x=[],T=this.get("categoryKey"),N=this.get("showMarkers"),C=this.get("showAreaFill"),k=this.get("showLines");t=t?t.concat():[],n==="vertical"?(l="yAxis",w="yKey",c="xAxis",E="xKey"):(l="xAxis",w="xKey",c="yAxis",E="yKey"),g=t.length;while(t&&t.length>0)p=t.shift(),b=this._getBaseAttribute(p,E),b?(m=e.Array.indexOf(d,b),m>-1?(d.splice(m,1),h.push(b),f.push(p)):x.push(p)):x.push(p);while(x.length>0)p=x.shift(),d.length>0?(b=d.shift(),this._setBaseAttribute(p,E,b),h.push(b),f.push(p)):p instanceof e.CartesianSeries&&p.destroy(!0);d.length>0&&(h=h.concat(d)),g=h.length;for(v=0;v<g;++v){p=f[v]||{type:y};if(p instanceof e.CartesianSeries)this._parseSeriesAxes(p);else{p[w]=p[w]||T,p[E]=p[E]||d.shift(),p[l]=this._getCategoryAxis(),p[c]=this._getSeriesAxis(p[E]),p.type=p.type||y,p.direction=p.direction||n;if(p.type==="combo"||p.type==="stackedcombo"||p.type==="combospline"||p.type==="stackedcombospline")C!==null&&(p.showAreaFill=p.showAreaFill!==null&&p.showAreaFill!==undefined?p.showAreaFill:C),N!==null&&(p.showMarkers=p.showMarkers!==null&&p.showMarkers!==undefined?p.showMarkers:N),k!==null&&(p.showLines=p.showLines!==null&&p.showLines!==undefined?p.showLines:k);r&&(o=i?v:p[E],a=r[o],a&&(u=p.styles,u?p.styles=this._mergeStyles(u,a):p.styles=a)),f[v]=p}}return f&&(S=this.get("graph"),S.set("seriesCollection",f),f=S.get("seriesCollection")),f},_parseSeriesAxes:function(t){var n=this.get("axes"),r=t.get("xAxis"),i=t.get("yAxis"),o=e.Axis,u;r&&!(r instanceof o)&&s.isString(r)&&n.hasOwnProperty(r)&&(u=n[r],u instanceof o&&t.set("xAxis",u)),i&&!(i instanceof o)&&s.isString(i)&&n.hasOwnProperty(i)&&(u=n[i],u instanceof o&&t.set("yAxis",u))},_getCategoryAxis:function(){var e,t=this.get("axes"),n=this.get("categoryAxisName")||this.get("categoryKey");return e=t[n],e},_getSeriesAxis:function(e,t){var n=this.get("axes"),r,i,s;if(n)if(t&&n.hasOwnProperty(t))s=n[t];else for(r in n)if(n.hasOwnProperty(r)){i=n[r].get("keys");if(i&&i.hasOwnProperty(e)){s=n[r];break}}return s},_getBaseAttribute:function(t,n){return t instanceof
+e.Base?t.get(n):t.hasOwnProperty(n)?t[n]:null},_setBaseAttribute:function(t,n,r){t instanceof e.Base?t.set(n,r):t[n]=r},_setAxes:function(t){var n=this._parseAxes(t),r={},i={edgeOffset:"edgeOffset",calculateEdgeOffset:"calculateEdgeOffset",position:"position",overlapGraph:"overlapGraph",labelValues:"labelValues",hideFirstMajorUnit:"hideFirstMajorUnit",hideLastMajorUnit:"hideLastMajorUnit",labelFunction:"labelFunction",labelFunctionScope:"labelFunctionScope",labelFormat:"labelFormat",appendLabelFunction:"appendLabelFunction",appendTitleFunction:"appendTitleFunction",maximum:"maximum",minimum:"minimum",roundingMethod:"roundingMethod",alwaysShowZero:"alwaysShowZero",scaleType:"scaleType",title:"title",width:"width",height:"height"},s=this.get("dataProvider"),o,u,a,f,l,c,h,p,d;for(u in n)if(n.hasOwnProperty(u)){c=n[u];if(c instanceof e.Axis)f=c;else{f=null,p={},p.dataProvider=c.dataProvider||s,p.keys=c.keys,c.hasOwnProperty("roundingUnit")&&(p.roundingUnit=c.roundingUnit),a=c.position,c.styles&&(p.styles=c.styles),p.position=c.position;for(o in i)i.hasOwnProperty(o)&&c.hasOwnProperty(o)&&(p[o]=c[o]);t&&(f=this.getAxisByKey(u)),f&&f instanceof e.Axis?(l=f.get("position"),a!==l&&(l!=="none"&&(d=this.get(l+"AxesCollection"),d.splice(e.Array.indexOf(d,f),1)),a!=="none"&&this._addToAxesCollection(a,f)),f.setAttrs(p)):(h=this._getAxisClass(c.type),f=new h(p),f.after("axisRendered",e.bind(this._itemRendered,this)))}f&&(d=this.get(a+"AxesCollection"),d&&e.Array.indexOf(d,f)>0&&f.set("overlapGraph",!1),r[u]=f)}return r},_addAxes:function(){var t=this.get("axes"),n,r,i,s=this.get("width"),o=this.get("height"),u=e.Node.one(this._parentNode);this._axesCollection||(this._axesCollection=[]);for(n in t)t.hasOwnProperty(n)&&(r=t[n],r instanceof e.Axis&&(s||(this.set("width",u.get("offsetWidth")),s=this.get("width")),o||(this.set("height",u.get("offsetHeight")),o=this.get("height")),this._addToAxesRenderQueue(r),i=r.get("position"),this.get(i+"AxesCollection")?this.get(i+"AxesCollection").push(r):this.set(i+"AxesCollection",[r]),this._axesCollection.push(r),r.get("keys").hasOwnProperty(this.get("categoryKey"))&&this.set("categoryAxis",r),r.render(this.get("contentBox"))))},_addSeries:function(){var e=this.get("graph");e.render(this.get("contentBox"))},_addGridlines:function(){var t=this.get("graph"),n=this.get("horizontalGridlines"),r=this.get("verticalGridlines"),i=this.get("direction"),s=this.get("leftAxesCollection"),o=this.get("rightAxesCollection"),u=this.get("bottomAxesCollection"),a=this.get("topAxesCollection"),f,l=this.get("categoryAxis"),c,h;this._axesCollection&&(f=this._axesCollection.concat(),f.splice(e.Array.indexOf(f,l),1)),n&&(s&&s[0]?c=s[0]:o&&o[0]?c=o[0]:c=i==="horizontal"?l:f[0],!this._getBaseAttribute(n,"axis")&&c&&this._setBaseAttribute(n,"axis",c),this._getBaseAttribute(n,"axis")&&t.set("horizontalGridlines",n)),r&&(u&&u[0]?h=u[0]:a&&a[0]?h=a[0]:h=i==="vertical"?l:f[0],!this._getBaseAttribute(r,"axis")&&h&&this._setBaseAttribute(r,"axis",h),this._getBaseAttribute(r,"axis")&&t.set("verticalGridlines",r))},_getDefaultAxes:function(){var e;return this.get("dataProvider")&&(e=this._parseAxes()),e},_parseAxes:function(t){var n=this.get("categoryKey"),r,i,o,u={},a=[],f=[],l=this.get("categoryAxisName")||this.get("categoryKey"),c=this.get("valueAxisName"),h=this.get("seriesKeys").concat(),p,d,v,m,g,y=this.get("direction"),b,w,E=[],S=this.get("stacked")?"stacked":"numeric";y==="vertical"?(b="bottom",w="left"):(b="left",w="bottom");if(t)for(p in t)if(t.hasOwnProperty(p)){r=t[p],o=this._getBaseAttribute(r,"keys"),i=this._getBaseAttribute(r,"type");if(i==="time"||i==="category")l=p,this.set("categoryAxisName",p),s.isArray(o)&&o.length>0&&(n=o[0],this.set("categoryKey",n)),u[p]=r;else if(p===l)u[p]=r;else{u[p]=r;if(p!==c&&o&&s.isArray(o)){m=o.length;for(v=0;v<m;++v)a.push(o[v]);E.push(u[p])}this._getBaseAttribute(u[p],"type")||this._setBaseAttribute(u[p],"type",S),this._getBaseAttribute(u[p],"position")||this._setBaseAttribute(u[p],"position",this._getDefaultAxisPosition(u[p],E,b))}}g=e.Array.indexOf(h,n),g>-1&&h.splice(g,1),d=h.length;for(p=0;p<d;++p)g=e.Array.indexOf(a,h[p]),g>-1&&(f=f.concat(a.splice(g,1)));a=f.concat(a),d=a.length;for(p=0;p<d;p+=1)g=e.Array.indexOf(h,a[p]),g>-1&&h.splice(g,1);return u.hasOwnProperty(l)||(u[l]={}),this._getBaseAttribute(u[l],"keys")||this._setBaseAttribute(u[l],"keys",[n]),this._getBaseAttribute(u[l],"position")||this._setBaseAttribute(u[l],"position",w),this._getBaseAttribute(u[l],"type")||this._setBaseAttribute(u[l],"type",this.get("categoryType")),!u.hasOwnProperty(c)&&h&&h.length>0&&(u[c]={keys:h},E.push(u[c])),a.length>0&&(h.length>0?h=a.concat(h):h=a),u.hasOwnProperty(c)&&(this._getBaseAttribute(u[c],"position")||this._setBaseAttribute(u[c],"position",this._getDefaultAxisPosition(u[c],E,b)),this._setBaseAttribute(u[c],"type",S),this._setBaseAttribute(u[c],"keys",h)),this._wereSeriesKeysExplicitlySet()||this.set("seriesKeys",h,{src:"internal"}),u},_getDefaultAxisPosition:function(t,n,r){var i=this.get("direction"),s=e.Array.indexOf(n,t);return n[s-1]&&n[s-1].position&&(i==="horizontal"?n[s-1].position==="left"?r="right":n[s-1].position==="right"&&(r="left"):n[s-1].position==="bottom"?r="top":r="bottom"),r},getSeriesItems:function(e,t){var n=e.get("xAxis"),r=e.get("yAxis"),i=e.get("xKey"),s=e.get("yKey"),o,u;return this.get("direction")==="vertical"?(o={axis:r,key:s,value:r.getKeyValueAt(s,t)},u={axis:n,key:i,value:n.getKeyValueAt(i,t)}):(u={axis:r,key:s,value:r.getKeyValueAt(s,t)},o={axis:n,key:i,value:n.getKeyValueAt(i,t)}),o.displayName=e.get("categoryDisplayName"),u.displayName=e.get("valueDisplayName"),o.value=o.axis.getKeyValueAt(o.key,t),u.value=u.axis.getKeyValueAt(u.key,t),{category:o,value:u}},_sizeChanged:function(){if(this._axesCollection){var e=this._axesCollection,t=0,n=e.length;for(;t<n;++t)this._addToAxesRenderQueue(e[t]);this._redraw()}},_getTopOverflow:function(e,t,n){var r=0,i,s=0,o;if(e){i=e.length;for(;r<i;++r)o=e[r],s=Math.max(s,Math.abs
+(o.getMaxLabelBounds().top)-o.getEdgeOffset(o.get("styles").majorTicks.count,n))}if(t){r=0,i=t.length;for(;r<i;++r)o=t[r],s=Math.max(s,Math.abs(o.getMaxLabelBounds().top)-o.getEdgeOffset(o.get("styles").majorTicks.count,n))}return s},_getRightOverflow:function(e,t,n){var r=0,i,s=0,o;if(e){i=e.length;for(;r<i;++r)o=e[r],s=Math.max(s,o.getMaxLabelBounds().right-o.getEdgeOffset(o.get("styles").majorTicks.count,n))}if(t){r=0,i=t.length;for(;r<i;++r)o=t[r],s=Math.max(s,o.getMaxLabelBounds().right-o.getEdgeOffset(o.get("styles").majorTicks.count,n))}return s},_getLeftOverflow:function(e,t,n){var r=0,i,s=0,o;if(e){i=e.length;for(;r<i;++r)o=e[r],s=Math.max(s,Math.abs(o.getMinLabelBounds().left)-o.getEdgeOffset(o.get("styles").majorTicks.count,n))}if(t){r=0,i=t.length;for(;r<i;++r)o=t[r],s=Math.max(s,Math.abs(o.getMinLabelBounds().left)-o.getEdgeOffset(o.get("styles").majorTicks.count,n))}return s},_getBottomOverflow:function(e,t,n){var r=0,i,s=0,o;if(e){i=e.length;for(;r<i;++r)o=e[r],s=Math.max(s,o.getMinLabelBounds().bottom-o.getEdgeOffset(o.get("styles").majorTicks.count,n))}if(t){r=0,i=t.length;for(;r<i;++r)o=t[r],s=Math.max(s,o.getMinLabelBounds().bottom-o.getEdgeOffset(o.get("styles").majorTicks.count,n))}return s},_redraw:function(){if(this._drawing){this._callLater=!0;return}this._drawing=!0,this._callLater=!1;var e=this.get("width"),t=this.get("height"),n=0,r=0,i=0,s=0,o=this.get("leftAxesCollection"),u=this.get("rightAxesCollection"),a=this.get("topAxesCollection"),f=this.get("bottomAxesCollection"),l=0,c,h,p="visible",d=this.get("graph"),v,m,g,y,b,w,E,S,x=this.get("allowContentOverflow"),T,N,C,k,L,A={};if(o){C=[],c=o.length;for(l=c-1;l>-1;--l)C.unshift(n),n+=o[l].get("width")}if(u){N=[],c=u.length,l=0;for(l=c-1;l>-1;--l)r+=u[l].get("width"),N.unshift(e-r)}if(a){k=[],c=a.length;for(l=c-1;l>-1;--l)k.unshift(i),i+=a[l].get("height")}if(f){L=[],c=f.length;for(l=c-1;l>-1;--l)s+=f[l].get("height"),L.unshift(t-s)}b=e-(n+r),w=t-(s+i),A.left=n,A.top=i,A.bottom=t-s,A.right=e-r;if(!x){v=this._getTopOverflow(o,u),m=this._getBottomOverflow(o,u),g=this._getLeftOverflow(f,a),y=this._getRightOverflow(f,a),T=v-i;if(T>0){A.top=v;if(k){l=0,c=k.length;for(;l<c;++l)k[l]+=T}}T=m-s;if(T>0){A.bottom=t-m;if(L){l=0,c=L.length;for(;l<c;++l)L[l]-=T}}T=g-n;if(T>0){A.left=g;if(C){l=0,c=C.length;for(;l<c;++l)C[l]+=T}}T=y-r;if(T>0){A.right=e-y;if(N){l=0,c=N.length;for(;l<c;++l)N[l]-=T}}}b=A.right-A.left,w=A.bottom-A.top,E=A.left,S=A.top;if(a){c=a.length,l=0;for(;l<c;l++)h=a[l],h.get("width")!==b&&h.set("width",b),h.get("boundingBox").setStyle("left",E+"px"),h.get("boundingBox").setStyle("top",k[l]+"px");h._hasDataOverflow()&&(p="hidden")}if(f){c=f.length,l=0;for(;l<c;l++)h=f[l],h.get("width")!==b&&h.set("width",b),h.get("boundingBox").setStyle("left",E+"px"),h.get("boundingBox").setStyle("top",L[l]+"px");h._hasDataOverflow()&&(p="hidden")}if(o){c=o.length,l=0;for(;l<c;++l)h=o[l],h.get("boundingBox").setStyle("top",S+"px"),h.get("boundingBox").setStyle("left",C[l]+"px"),h.get("height")!==w&&h.set("height",w);h._hasDataOverflow()&&(p="hidden")}if(u){c=u.length,l=0;for(;l<c;++l)h=u[l],h.get("boundingBox").setStyle("top",S+"px"),h.get("boundingBox").setStyle("left",N[l]+"px"),h.get("height")!==w&&h.set("height",w);h._hasDataOverflow()&&(p="hidden")}this._drawing=!1;if(this._callLater){this._redraw();return}d&&(d.get("boundingBox").setStyle("left",E+"px"),d.get("boundingBox").setStyle("top",S+"px"),d.set("width",b),d.set("height",w),d.get("boundingBox").setStyle("overflow",p)),this._overlay&&(this._overlay.setStyle("left",E+"px"),this._overlay.setStyle("top",S+"px"),this._overlay.setStyle("width",b+"px"),this._overlay.setStyle("height",w+"px"))},destructor:function(){var t=this.get("graph"),n=0,r,i=this.get("seriesCollection"),s=this._axesCollection,o=this.get("tooltip").node;this._description&&(this._description.empty(),this._description.remove(!0)),this._liveRegion&&(this._liveRegion.empty(),this._liveRegion.remove(!0)),r=i?i.length:0;for(;n<r;++n)i[n]instanceof e.CartesianSeries&&i[n].destroy(!0);r=s?s.length:0;for(n=0;n<r;++n)s[n]instanceof e.Axis&&s[n].destroy(!0);t&&t.destroy(!0),o&&(o.empty(),o.remove(!0)),this._overlay&&(this._overlay.empty(),this._overlay.remove(!0))},_getAriaMessage:function(e){var t="",n,r,i,s,o=this._seriesIndex,u=this._itemIndex,a=this.get("seriesCollection"),f=a.length,l;return e%2===0?(f>1?(e===38?o=o<1?f-1:o-1:e===40&&(o=o>=f-1?0:o+1),this._itemIndex=-1):o=0,this._seriesIndex=o,n=this.getSeries(parseInt(o,10)),t=n.get("valueDisplayName")+" series."):(o>-1?(t="",n=this.getSeries(parseInt(o,10))):(o=0,this._seriesIndex=o,n=this.getSeries(parseInt(o,10)),t=n.get("valueDisplayName")+" series."),l=n._dataLength?n._dataLength:0,e===37?u=u>0?u-1:l-1:e===39&&(u=u>=l-1?0:u+1),this._itemIndex=u,r=this.getSeriesItems(n,u),i=r.category,s=r.value,i&&s&&i.value&&s.value?(t+=i.displayName+": "+i.axis.formatLabel.apply(this,[i.value,i.axis.get("labelFormat")])+", ",t+=s.displayName+": "+s.axis.formatLabel.apply(this,[s.value,s.axis.get("labelFormat")])+", "):t+="No data available.",t+=u+1+" of "+l+". "),t}},{ATTRS:{allowContentOverflow:{value:!1},axesStyles:{lazyAdd:!1,getter:function(){var t=this.get("axes"),n,r=this._axesStyles;if(t)for(n in t)t.hasOwnProperty(n)&&t[n]instanceof e.Axis&&(r||(r={}),r[n]=t[n].get("styles"));return r},setter:function(e){var t=this.get("axes"),n;for(n in e)e.hasOwnProperty(n)&&t.hasOwnProperty(n)&&this._setBaseAttribute(t[n],"styles",e[n]);return e}},seriesStyles:{lazyAdd:!1,getter:function(){var e=this._seriesStyles,t=this.get("graph"),n,r;if(t){n=t.get("seriesDictionary");if(n){e={};for(r in n)n.hasOwnProperty(r)&&(e[r]=n[r].get("styles"))}}return e},setter:function(e){var t,n,r;if(s.isArray(e)){r=this.get("seriesCollection"),t=0,n=e.length;for(;t<n;++t)this._setBaseAttribute(r[t],"styles",e[t])}else for(t in e)e.hasOwnProperty(t)&&(r=this.getSeries(t),this._setBaseAttribute(r,"styles",e[t]));return e}},graphStyles:{lazyAdd:!1,getter:function(){var e=this.get("graph");return e?
+e.get("styles"):this._graphStyles},setter:function(e){var t=this.get("graph");return this._setBaseAttribute(t,"styles",e),e}},styles:{lazyAdd:!1,getter:function(){var e={axes:this.get("axesStyles"),series:this.get("seriesStyles"),graph:this.get("graphStyles")};return e},setter:function(e){e.hasOwnProperty("axes")&&(this.get("axesStyles")?this.set("axesStyles",e.axes):this._axesStyles=e.axes),e.hasOwnProperty("series")&&(this.get("seriesStyles")?this.set("seriesStyles",e.series):this._seriesStyles=e.series),e.hasOwnProperty("graph")&&this.set("graphStyles",e.graph)}},axes:{lazyAdd:!1,valueFn:"_getDefaultAxes",setter:function(e){return this.get("dataProvider")&&(e=this._setAxes(e)),e}},seriesCollection:{lazyAdd:!1,valueFn:"_getDefaultSeriesCollection",setter:function(e){return this.get("dataProvider")?this._parseSeriesCollection(e):e}},leftAxesCollection:{},bottomAxesCollection:{},rightAxesCollection:{},topAxesCollection:{},stacked:{value:!1},direction:{getter:function(){var e=this.get("type");return e==="bar"?"vertical":e==="column"?"horizontal":this._direction},setter:function(e){return this._direction=e,this._direction}},showAreaFill:{},showMarkers:{},showLines:{},categoryAxisName:{},valueAxisName:{value:"values"},horizontalGridlines:{getter:function(){var e=this.get("graph");return e?e.get("horizontalGridlines"):this._horizontalGridlines},setter:function(e){var t=this.get("graph");e&&!s.isObject(e)&&(e={}),t?t.set("horizontalGridlines",e):this._horizontalGridlines=e}},verticalGridlines:{getter:function(){var e=this.get("graph");return e?e.get("verticalGridlines"):this._verticalGridlines},setter:function(e){var t=this.get("graph");e&&!s.isObject(e)&&(e={}),t?t.set("verticalGridlines",e):this._verticalGridlines=e}},type:{getter:function(){return this.get("stacked")?"stacked"+this._type:this._type},setter:function(e){return this._type==="bar"?e!=="bar"&&this.set("direction","horizontal"):e==="bar"&&this.set("direction","vertical"),this._type=e,this._type}},categoryAxis:{}}}),e.PieChart=e.Base.create("pieChart",e.Widget,[e.ChartBase],{_getSeriesCollection:function(){if(this._seriesCollection)return this._seriesCollection;var e=this.get("axes"),t=[],n,r=0,i,s=this.get("type"),o,u="categoryAxis",a="categoryKey",f="valueAxis",l="valueKey";if(e){n=e.values.get("keyCollection"),o=e.category.get("keyCollection")[0],i=n.length;for(;r<i;++r)t[r]={type:s},t[r][u]="category",t[r][f]="values",t[r][a]=o,t[r][l]=n[r]}return this._seriesCollection=t,t},_parseAxes:function(t){this._axes||(this._axes={});var n,r,i,s,o,u,a=this.get("type"),f=this.get("width"),l=this.get("height"),c=e.Node.one(this._parentNode);f||(this.set("width",c.get("offsetWidth")),f=this.get("width")),l||(this.set("height",c.get("offsetHeight")),l=this.get("height"));for(n in t)t.hasOwnProperty(n)&&(s=t[n],r=a==="pie"?"none":s.position,u=this._getAxisClass(s.type),o={dataProvider:this.get("dataProvider")},s.hasOwnProperty("roundingUnit")&&(o.roundingUnit=s.roundingUnit),o.keys=s.keys,o.width=f,o.height=l,o.position=r,o.styles=s.styles,i=new u(o),i.on("axisRendered",e.bind(this._itemRendered,this)),this._axes[n]=i)},_addAxes:function(){var e=this.get("axes"),t,n,r;e||(this.set("axes",this._getDefaultAxes()),e=this.get("axes")),this._axesCollection||(this._axesCollection=[]);for(t in e)e.hasOwnProperty(t)&&(n=e[t],r=n.get("position"),this.get(r+"AxesCollection")?this.get(r+"AxesCollection").push(n):this.set(r+"AxesCollection",[n]),this._axesCollection.push(n))},_addSeries:function(){var e=this.get("graph"),t=this.get("seriesCollection");this._parseSeriesAxes(t),e.set("showBackground",!1),e.set("width",this.get("width")),e.set("height",this.get("height")),e.set("seriesCollection",t),this._seriesCollection=e.get("seriesCollection"),e.render(this.get("contentBox"))},_parseSeriesAxes:function(t){var n=0,r=t.length,i,s=this.get("axes"),o;for(;n<r;++n){i=t[n];if(i){if(i instanceof e.PieSeries){o=i.get("categoryAxis"),o&&!(o instanceof e.Axis)&&i.set("categoryAxis",s[o]),o=i.get("valueAxis"),o&&!(o instanceof e.Axis)&&i.set("valueAxis",s[o]);continue}i.categoryAxis=s.category,i.valueAxis=s.values,i.type||(i.type=this.get("type"))}}},_getDefaultAxes:function(){var e=this.get("categoryKey"),t=this.get("seriesKeys").concat(),n="numeric";return{values:{keys:t,type:n},category:{keys:[e],type:this.get("categoryType")}}},getSeriesItems:function(e,t){var n={axis:e.get("categoryAxis"),key:e.get("categoryKey"),displayName:e.get("categoryDisplayName")},r={axis:e.get("valueAxis"),key:e.get("valueKey"),displayName:e.get("valueDisplayName")};return n.value=n.axis.getKeyValueAt(n.key,t),r.value=r.axis.getKeyValueAt(r.key,t),{category:n,value:r}},_sizeChanged:function(){this._redraw()},_redraw:function(){var e=this.get("graph"),t=this.get("width"),n=this.get("height"),r;e&&(r=Math.min(t,n),e.set("width",r),e.set("height",r))},_tooltipLabelFunction:function(e,t,n,r){var s=i.createElement("div"),o=r.getTotalValues(),u=Math.round(t.value/o*1e4)/100;return s.appendChild(i.createTextNode(e.displayName+": "+e.axis.get("labelFunction").apply(this,[e.value,e.axis.get("labelFormat")]))),s.appendChild(i.createElement("br")),s.appendChild(i.createTextNode(t.displayName+": "+t.axis.get("labelFunction").apply(this,[t.value,t.axis.get("labelFormat")]))),s.appendChild(i.createElement("br")),s.appendChild(i.createTextNode(u+"%")),s},_getAriaMessage:function(e){var t="",n,r,i,s,o=0,u=this._itemIndex,a,f,l,c;return i=this.getSeries(parseInt(o,10)),c=i.get("markers"),a=c&&c.length?c.length:0,e===37?u=u>0?u-1:a-1:e===39&&(u=u>=a-1?0:u+1),this._itemIndex=u,r=this.getSeriesItems(i,u),n=r.category,s=r.value,f=i.getTotalValues(),l=Math.round(s.value/f*1e4)/100,n&&s?(t+=n.displayName+": "+n.axis.formatLabel.apply(this,[n.value,n.axis.get("labelFormat")])+", ",t+=s.displayName+": "+s.axis.formatLabel.apply(this,[s.value,s.axis.get("labelFormat")])+", ",t+="Percent of total "+s.displayName+": "+l+"%,"):t+="No data available,",t+=u+1+" of "+a+". ",t},destructor:function(){var t,n,r=this.
+get("tooltip"),i=r.node,s=this.get("graph"),o=this._axesCollection,u=this.get("seriesCollection");while(u.length>0)t=u.shift(),t.destroy(!0);while(o.length>0)n=o.shift(),n instanceof e.Axis&&n.destroy(!0);this._description&&(this._description.empty(),this._description.remove(!0)),this._liveRegion&&(this._liveRegion.empty(),this._liveRegion.remove(!0)),s&&s.destroy(!0),i&&(i.empty(),i.remove(!0))}},{ATTRS:{ariaDescription:{value:"Use the left and right keys to navigate through items.",setter:function(e){return this._description&&this._description.set("text",e),e}},axes:{getter:function(){return this._axes},setter:function(e){this._parseAxes(e)}},seriesCollection:{lazyAdd:!1,getter:function(){return this._getSeriesCollection()},setter:function(e){return this._setSeriesCollection(e)}},type:{value:"pie"}}}),e.Chart=l},"3.17.2",{requires:["dom","event-mouseenter","event-touch","graphics-group","axes","series-pie","series-line","series-marker","series-area","series-spline","series-column","series-bar","series-areaspline","series-combo","series-combospline","series-line-stacked","series-marker-stacked","series-area-stacked","series-spline-stacked","series-column-stacked","series-bar-stacked","series-areaspline-stacked","series-combo-stacked","series-combospline-stacked"]});
diff --git a/js/yui3/charts-legend/charts-legend-min.js b/js/yui3/charts-legend/charts-legend-min.js
new file mode 100644
index 000000000..8dabaf123
--- /dev/null
+++ b/js/yui3/charts-legend/charts-legend-min.js
@@ -0,0 +1,10 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("charts-legend",function(e,t){var n="top",r="right",i="bottom",s="left",o="external",u="horizontal",a="vertical",f="width",l="height",c="position",h="x",p="y",d="px",v,m={setter:function(t){var n=this.get("legend");return n&&n.destroy(!0),t instanceof e.ChartLegend?(n=t,n.set("chart",this)):(t.chart=this,t.hasOwnProperty("render")||(t.render=this.get("contentBox"),t.includeInChartLayout=!0),n=new e.ChartLegend(t)),n}},g={_positionLegendItems:function(e,t,n,r,i,s,o,u,a){var f=0,l=0,c,h,p,v,m,y=this.get("width"),b,w,E,S,x,T=s.top-u,N=y-(s.left+s.right),C,k,L,A;g._setRowArrays(e,N,o),b=g.rowArray,S=g.totalWidthArray,w=b.length;for(;l<w;++l){T+=u,E=b[l],m=E.length,x=g.getStartPoint(y,S[l],a,s);for(f=0;f<m;++f)c=E[f],h=c.node,p=c.width,v=c.height,c.x=x,c.y=0,C=isNaN(C)?x:Math.min(C,x),k=isNaN(k)?T:Math.min(k,T),L=isNaN(L)?x+p:Math.max(x+p,L),A=isNaN(A)?T+v:Math.max(T+v,A),h.setStyle("left",x+d),h.setStyle("top",T+d),x+=p+o;T+=c.height}this._contentRect={left:C,top:k,right:L,bottom:A},this.get("includeInChartLayout")&&this.set("height",T+s.bottom)},_setRowArrays:function(e,t,n){var r=e[0],i=[[r]],s=1,o=0,u=e.length,a=r.width,f,l=[[a]];for(;s<u;++s)r=e[s],f=r.width,a+n+f<=t?(a+=n+f,i[o].push(r)):(a=n+f,i[o]&&(o+=1),i[o]=[r]),l[o]=a;g.rowArray=i,g.totalWidthArray=l},getStartPoint:function(e,t,n,i){var o;switch(n){case s:o=i.left;break;case"center":o=(e-t)*.5;break;case r:o=e-t-i.right}return o}},y={_positionLegendItems:function(e,t,n,r,i,s,o,u,a){var f=0,l=0,c,h,p,v,m,g=this.get("height"),b,w,E,S,x,T=s.left-o,N,C=g-(s.top+s.bottom),k,L,A,O;y._setColumnArrays(e,C,u),b=y.columnArray,S=y.totalHeightArray,w=b.length;for(;l<w;++l){T+=o,E=b[l],m=E.length,x=y.getStartPoint(g,S[l],a,s),N=0;for(f=0;f<m;++f)c=E[f],h=c.node,p=c.height,v=c.width,c.y=x,c.x=T,k=isNaN(k)?T:Math.min(k,T),L=isNaN(L)?x:Math.min(L,x),A=isNaN(A)?T+v:Math.max(T+v,A),O=isNaN(O)?x+p:Math.max(x+p,O),h.setStyle("left",T+d),h.setStyle("top",x+d),x+=p+u,N=Math.max(N,c.width);T+=N}this._contentRect={left:k,top:L,right:A,bottom:O},this.get("includeInChartLayout")&&this.set("width",T+s.right)},_setColumnArrays:function(e,t,n){var r=e[0],i=[[r]],s=1,o=0,u=e.length,a=r.height,f,l=[[a]];for(;s<u;++s)r=e[s],f=r.height,a+n+f<=t?(a+=n+f,i[o].push(r)):(a=n+f,i[o]&&(o+=1),i[o]=[r]),l[o]=a;y.columnArray=i,y.totalHeightArray=l},getStartPoint:function(e,t,r,s){var o;switch(r){case n:o=s.top;break;case"middle":o=(e-t)*.5;break;case i:o=e-t-s.bottom}return o}},b=e.Base.create("cartesianChartLegend",e.CartesianChart,[],{_redraw:function(){if(this._drawing){this._callLater=!0;return}this._drawing=!0,this._callLater=!1;var e=this.get("width"),t=this.get("height"),n=this._getLayoutBoxDimensions(),r=n.left,i=n.right,s=n.top,o=n.bottom,u=this.get("leftAxesCollection"),a=this.get("rightAxesCollection"),f=this.get("topAxesCollection"),l=this.get("bottomAxesCollection"),c=0,h,p,v="visible",m=this.get("graph"),g,y,b,w,E,S,x,T,N=this.get("allowContentOverflow"),C,k,L,A,O,M=this.get("legend"),_={};if(u){L=[],h=u.length;for(c=h-1;c>-1;--c)L.unshift(r),r+=u[c].get("width")}if(a){k=[],h=a.length,c=0;for(c=h-1;c>-1;--c)i+=a[c].get("width"),k.unshift(e-i)}if(f){A=[],h=f.length;for(c=h-1;c>-1;--c)A.unshift(s),s+=f[c].get("height")}if(l){O=[],h=l.length;for(c=h-1;c>-1;--c)o+=l[c].get("height"),O.unshift(t-o)}E=e-(r+i),S=t-(o+s),_.left=r,_.top=s,_.bottom=t-o,_.right=e-i;if(!N){g=this._getTopOverflow(u,a),y=this._getBottomOverflow(u,a),b=this._getLeftOverflow(l,f),w=this._getRightOverflow(l,f),C=g-s;if(C>0){_.top=g;if(A){c=0,h=A.length;for(;c<h;++c)A[c]+=C}}C=y-o;if(C>0){_.bottom=t-y;if(O){c=0,h=O.length;for(;c<h;++c)O[c]-=C}}C=b-r;if(C>0){_.left=b;if(L){c=0,h=L.length;for(;c<h;++c)L[c]+=C}}C=w-i;if(C>0){_.right=e-w;if(k){c=0,h=k.length;for(;c<h;++c)k[c]-=C}}}E=_.right-_.left,S=_.bottom-_.top,x=_.left,T=_.top;if(M&&M.get("includeInChartLayout"))switch(M.get("position")){case"left":M.set("y",T),M.set("height",S);break;case"top":M.set("x",x),M.set("width",E);break;case"bottom":M.set("x",x),M.set("width",E);break;case"right":M.set("y",T),M.set("height",S)}if(f){h=f.length,c=0;for(;c<h;c++)p=f[c],p.get("width")!==E&&p.set("width",E),p.get("boundingBox").setStyle("left",x+d),p.get("boundingBox").setStyle("top",A[c]+d);p._hasDataOverflow()&&(v="hidden")}if(l){h=l.length,c=0;for(;c<h;c++)p=l[c],p.get("width")!==E&&p.set("width",E),p.get("boundingBox").setStyle("left",x+d),p.get("boundingBox").setStyle("top",O[c]+d);p._hasDataOverflow()&&(v="hidden")}if(u){h=u.length,c=0;for(;c<h;++c)p=u[c],p.get("boundingBox").setStyle("top",T+d),p.get("boundingBox").setStyle("left",L[c]+d),p.get("height")!==S&&p.set("height",S);p._hasDataOverflow()&&(v="hidden")}if(a){h=a.length,c=0;for(;c<h;++c)p=a[c],p.get("boundingBox").setStyle("top",T+d),p.get("boundingBox").setStyle("left",k[c]+d),p.get("height")!==S&&p.set("height",S);p._hasDataOverflow()&&(v="hidden")}this._drawing=!1;if(this._callLater){this._redraw();return}m&&(m.get("boundingBox").setStyle("left",x+d),m.get("boundingBox").setStyle("top",T+d),m.set("width",E),m.set("height",S),m.get("boundingBox").setStyle("overflow",v)),this._overlay&&(this._overlay.setStyle("left",x+d),this._overlay.setStyle("top",T+d),this._overlay.setStyle("width",E+d),this._overlay.setStyle("height",S+d))},_getLayoutBoxDimensions:function(){var e={top:0,right:0,bottom:0,left:0},t=this.get("legend"),a,d,v,m,g=this.get(f),y=this.get(l),b;if(t&&t.get("includeInChartLayout")){b=t.get("styles").gap,a=t.get(c);if(a!==o){d=t.get("direction"),v=d===u?l:f,m=t.get(v),e[a]=m+b;switch(a){case n:t.set(p,0);break;case i:t.set(p,y-m);break;case r:t.set(h,g-m);break;case s:t.set(h,0)}}}return e},destructor:function(){var e=this.get("legend");e&&e.destroy(!0)}},{ATTRS:{legend:m}});e.CartesianChart=b,v=e.Base.create("pieChartLegend",e.PieChart,[],{_redraw:function(){if(this._drawing){this._callLater=!0;return}this._drawing=!0,this._callLater=!1;var e=this.get("graph"),t=this.get("width"),o=this.get("height"),u,a,c=this.get("legend"),d=0,v=0,m=0,g=0,y,b,w,E,S,x;if(e)if(c){S=c.get
+("position"),x=c.get("direction"),u=e.get("width"),a=e.get("height"),y=c.get("width"),b=c.get("height"),E=c.get("styles").gap;if(x==="vertical"&&u+y+E!==t||x==="horizontal"&&a+b+E!==o){switch(c.get("position")){case s:w=Math.min(t-(y+E),o),b=o,d=y+E,c.set(l,b);break;case n:w=Math.min(o-(b+E),t),y=t,v=b+E,c.set(f,y);break;case r:w=Math.min(t-(y+E),o),b=o,m=w+E,c.set(l,b);break;case i:w=Math.min(o-(b+E),t),y=t,g=w+E,c.set(f,y)}e.set(f,w),e.set(l,w)}else switch(c.get("position")){case s:d=y+E;break;case n:v=b+E;break;case r:m=u+E;break;case i:g=a+E}}else e.set(h,0),e.set(p,0),e.set(f,t),e.set(l,o);this._drawing=!1;if(this._callLater){this._redraw();return}e&&(e.set(h,d),e.set(p,v)),c&&(c.set(h,m),c.set(p,g))}},{ATTRS:{legend:m}}),e.PieChart=v,e.ChartLegend=e.Base.create("chartlegend",e.Widget,[e.Renderer],{initializer:function(){this._items=[]},renderUI:function(){var t=this.get("boundingBox"),n=this.get("contentBox"),r=this.get("styles").background,i=new e.Rect({graphic:n,fill:r.fill,stroke:r.border});t.setStyle("display","block"),t.setStyle("position","absolute"),this.set("background",i)},bindUI:function(){this.get("chart").after("seriesCollectionChange",e.bind(this._updateHandler,this)),this.get("chart").after("stylesChange",e.bind(this._updateHandler,this)),this.after("stylesChange",this._updateHandler),this.after("positionChange",this._positionChangeHandler),this.after("widthChange",this._handleSizeChange),this.after("heightChange",this._handleSizeChange)},syncUI:function(){var e=this.get("width"),t=this.get("height");isFinite(e)&&isFinite(t)&&e>0&&t>0&&this._drawLegend()},_updateHandler:function(){this.get("rendered")&&this._drawLegend()},_positionChangeHandler:function(){var e=this.get("chart"),t=this._parentNode;t&&e&&this.get("includeInChartLayout")?this.fire("legendRendered"):this.get("rendered")&&this._drawLegend()},_handleSizeChange:function(e){var t=e.attrName,o=this.get(c),u=o===s||o===r,a=o===i||o===n;(a&&t===f||u&&t===l)&&this._drawLegend()},_drawLegend:function(){if(this._drawing){this._callLater=!0;return}this._drawing=!0,this._callLater=!1,this.get("includeInChartLayout")&&this.get("chart")._itemRenderQueue.unshift(this);var t=this.get("chart"),n=this.get("contentBox"),r=t.get("seriesCollection"),i,s=this.get("styles"),o=s.padding,u=s.item,a,f=u.hSpacing,l=u.vSpacing,c=this.get("direction"),h=c==="vertical"?s.vAlign:s.hAlign,p=s.marker,d=u.label,v,m=this._layout[c],g,y,b,w,E,S,x,T,N,C,k,L,A=[],O=p.width,M=p.height,_=0-f,D=0-l,P=0,H=0,B,j;p&&p.shape&&(w=p.shape),this._destroyLegendItems();if(t instanceof e.PieChart){i=r[0],v=i.get("categoryAxis").getDataByKey(i.get("categoryKey")),a=i.get("styles").marker,C=a.fill.colors,k=a.border.colors,L=a.border.weight,g=0,y=v.length,E=w||e.Circle,b=e.Lang.isArray(E);for(;g<y;++g)E=b?E[g]:E,T={color:C[g]},N={colors:k[g],weight:L},v=t.getSeriesItems(i,g).category.value,x=this._getLegendItem(n,this._getShapeClass(E),T,N,d,O,M,v),B=x.width,j=x.height,P=Math.max(P,B),H=Math.max(H,j),_+=B+f,D+=j+l,A.push(x)}else{g=0,y=r.length;for(;g<y;++g)i=r[g],a=this._getStylesBySeriesType(i,E),w||(E=a.shape,E||(E=e.Circle)),S=e.Lang.isArray(E)?E[g]:E,x=this._getLegendItem(n,this._getShapeClass(E),a.fill,a.border,d,O,M,i.get("valueDisplayName")),B=x.width,j=x.height,P=Math.max(P,B),H=Math.max(H,j),_+=B+f,D+=j+l,A.push(x)}this._drawing=!1,this._callLater?this._drawLegend():(m._positionLegendItems.apply(this,[A,P,H,_,D,o,f,l,h]),this._updateBackground(s),this.fire("legendRendered"))},_updateBackground:function(e){var t=e.background,n=this._contentRect,r=e.padding,i=n.left-r.left,s=n.top-r.top,o=n.right-i+r.right,u=n.bottom-s+r.bottom;this.get("background").set({fill:t.fill,stroke:t.border,width:o,height:u,x:i,y:s})},_getStylesBySeriesType:function(t){var n=t.get("styles"),r;return t instanceof e.LineSeries||t instanceof e.StackedLineSeries?(n=t.get("styles").line,r=n.color||t._getDefaultColor(t.get("graphOrder"),"line"),{border:{weight:1,color:r},fill:{color:r}}):t instanceof e.AreaSeries||t instanceof e.StackedAreaSeries?(n=t.get("styles").area,r=n.color||t._getDefaultColor(t.get("graphOrder"),"slice"),{border:{weight:1,color:r},fill:{color:r}}):(n=t.get("styles").marker,{fill:n.fill,border:{weight:n.border.weight,color:n.border.color,shape:n.shape},shape:n.shape})},_getLegendItem:function(t,n,r,i,s,o,u,a){var f=e.Node.create("<div>"),l=e.Node.create("<span>"),h,p,v,m,g,y=n;return f.setStyle(c,"absolute"),l.setStyle(c,"absolute"),l.setStyles(s),l.set("text",a),f.appendChild(l),t.append(f),p=l.get("offsetHeight"),v=p-u,m=o+v+2,l.setStyle("left",m+d),f.setStyle("height",p+d),f.setStyle("width",m+l.get("offsetWidth")+d),h=new y({fill:r,stroke:i,width:o,height:u,x:v*.5,y:v*.5,w:o,h:u,graphic:f}),l.setStyle("left",p+d),g={node:f,width:f.get("offsetWidth"),height:f.get("offsetHeight"),shape:h,textNode:l,text:a},this._items.push(g),g},_getShapeClass:function(){var e=this.get("background").get("graphic");return e._getShapeClass.apply(e,arguments)},_getDefaultStyles:function(){var e={padding:{top:8,right:8,bottom:8,left:9},gap:10,hAlign:"center",vAlign:"top",marker:this._getPlotDefaults(),item:{hSpacing:10,vSpacing:5,label:{color:"#808080",fontSize:"85%",whiteSpace:"nowrap"}},background:{shape:"rect",fill:{color:"#faf9f2"},border:{color:"#dad8c9",weight:1}}};return e},_getPlotDefaults:function(){var e={width:10,height:10};return e},_destroyLegendItems:function(){var e;if(this._items)while(this._items.length>0)e=this._items.shift(),e.shape.get("graphic").destroy(),e.node.empty(),e.node.destroy(!0),e.node=null,e=null;this._items=[]},_layout:{vertical:y,horizontal:g},destructor:function(){var e=this.get("background"),t;this._destroyLegendItems(),e&&(t=e.get("graphic"),t?t.destroy():e.destroy())}},{ATTRS:{includeInChartLayout:{value:!1},chart:{setter:function(t){return this.after("legendRendered",e.bind(t._itemRendered,t)),t}},direction:{value:"vertical"},position:{lazyAdd:!1,value:"right",setter:function(e){return e===n||e===i?this.set("direction",u):(e===s||e===r)&&this
+.set("direction",a),e}},width:{getter:function(){var e=this.get("chart"),t=this._parentNode;return t?e&&this.get("includeInChartLayout")||this._width?(this._width||(this._width=0),this._width):t.get("offsetWidth"):""},setter:function(e){return this._width=e,e}},height:{valueFn:"_heightGetter",getter:function(){var e=this.get("chart"),t=this._parentNode;return t?e&&this.get("includeInChartLayout")||this._height?(this._height||(this._height=0),this._height):t.get("offsetHeight"):""},setter:function(e){return this._height=e,e}},x:{lazyAdd:!1,value:0,setter:function(e){var t=this.get("boundingBox");return t&&t.setStyle(s,e+d),e}},y:{lazyAdd:!1,value:0,setter:function(e){var t=this.get("boundingBox");return t&&t.setStyle(n,e+d),e}},items:{getter:function(){return this._items}},background:{}}})},"3.17.2",{requires:["charts-base"]});
diff --git a/js/yui3/classnamemanager/classnamemanager-min.js b/js/yui3/classnamemanager/classnamemanager-min.js
new file mode 100644
index 000000000..d0a3b44b2
--- /dev/null
+++ b/js/yui3/classnamemanager/classnamemanager-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("classnamemanager",function(e,t){var n="classNamePrefix",r="classNameDelimiter",i=e.config;i[n]=i[n]||"yui3",i[r]=i[r]||"-",e.ClassNameManager=function(){var t=i[n],s=i[r];return{getClassName:e.cached(function(){var n=e.Array(arguments);return n[n.length-1]!==!0?n.unshift(t):n.pop(),n.join(s)})}}()},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/clickable-rail/assets/thumb-x-oblong-dark.png b/js/yui3/clickable-rail/assets/thumb-x-oblong-dark.png
new file mode 100644
index 000000000..bc0aa14ce
--- /dev/null
+++ b/js/yui3/clickable-rail/assets/thumb-x-oblong-dark.png
Binary files differ
diff --git a/js/yui3/clickable-rail/assets/thumb-x-oblong.png b/js/yui3/clickable-rail/assets/thumb-x-oblong.png
new file mode 100644
index 000000000..670ba1ea1
--- /dev/null
+++ b/js/yui3/clickable-rail/assets/thumb-x-oblong.png
Binary files differ
diff --git a/js/yui3/clickable-rail/assets/thumb-x-oblong2-dark.png b/js/yui3/clickable-rail/assets/thumb-x-oblong2-dark.png
new file mode 100644
index 000000000..20f126029
--- /dev/null
+++ b/js/yui3/clickable-rail/assets/thumb-x-oblong2-dark.png
Binary files differ
diff --git a/js/yui3/clickable-rail/assets/thumb-x-oblong2.png b/js/yui3/clickable-rail/assets/thumb-x-oblong2.png
new file mode 100644
index 000000000..76e34e60a
--- /dev/null
+++ b/js/yui3/clickable-rail/assets/thumb-x-oblong2.png
Binary files differ
diff --git a/js/yui3/clickable-rail/assets/thumb-y-oblong-dark.png b/js/yui3/clickable-rail/assets/thumb-y-oblong-dark.png
new file mode 100644
index 000000000..a0eed7087
--- /dev/null
+++ b/js/yui3/clickable-rail/assets/thumb-y-oblong-dark.png
Binary files differ
diff --git a/js/yui3/clickable-rail/assets/thumb-y-oblong.png b/js/yui3/clickable-rail/assets/thumb-y-oblong.png
new file mode 100644
index 000000000..e63c8d7d8
--- /dev/null
+++ b/js/yui3/clickable-rail/assets/thumb-y-oblong.png
Binary files differ
diff --git a/js/yui3/clickable-rail/assets/thumb-y-oblong2-dark.png b/js/yui3/clickable-rail/assets/thumb-y-oblong2-dark.png
new file mode 100644
index 000000000..e91ffb7b3
--- /dev/null
+++ b/js/yui3/clickable-rail/assets/thumb-y-oblong2-dark.png
Binary files differ
diff --git a/js/yui3/clickable-rail/assets/thumb-y-oblong2.png b/js/yui3/clickable-rail/assets/thumb-y-oblong2.png
new file mode 100644
index 000000000..89a466727
--- /dev/null
+++ b/js/yui3/clickable-rail/assets/thumb-y-oblong2.png
Binary files differ
diff --git a/js/yui3/clickable-rail/clickable-rail-min.js b/js/yui3/clickable-rail/clickable-rail-min.js
new file mode 100644
index 000000000..f16d116bc
--- /dev/null
+++ b/js/yui3/clickable-rail/clickable-rail-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("clickable-rail",function(e,t){function n(){this._initClickableRail()}e.ClickableRail=e.mix(n,{prototype:{_initClickableRail:function(){this._evtGuid=this._evtGuid||e.guid()+"|",this.publish("railMouseDown",{defaultFn:this._defRailMouseDownFn}),this.after("render",this._bindClickableRail),this.on("destroy",this._unbindClickableRail)},_bindClickableRail:function(){this._dd.addHandle(this.rail),this.rail.on(this._evtGuid+e.DD.Drag.START_EVENT,e.bind(this._onRailMouseDown,this))},_unbindClickableRail:function(){if(this.get("rendered")){var e=this.get("contentBox"),t=e.one("."+this.getClassName("rail"));t.detach(this.evtGuid+"*")}},_onRailMouseDown:function(e){this.get("clickableRail")&&!this.get("disabled")&&(this.fire("railMouseDown",{ev:e}),this.thumb.focus())},_defRailMouseDownFn:function(e){e=e.ev;var t=this._resolveThumb(e),n=this._key.xyIndex,r=parseFloat(this.get("length"),10),i,s,o;t&&(i=t.get("dragNode"),s=parseFloat(i.getStyle(this._key.dim),10),o=this._getThumbDestination(e,i),o=o[n]-this.rail.getXY()[n],o=Math.min(Math.max(o,0),r-s),this._uiMoveThumb(o,{source:"rail"}),e.target=this.thumb.one("img")||this.thumb,t._handleMouseDownEvent(e))},_resolveThumb:function(e){return this._dd},_getThumbDestination:function(e,t){var n=t.get("offsetWidth"),r=t.get("offsetHeight");return[e.pageX-Math.round(n/2),e.pageY-Math.round(r/2)]}},ATTRS:{clickableRail:{value:!0,validator:e.Lang.isBoolean}}},!0)},"3.17.2",{requires:["slider-base"]});
diff --git a/js/yui3/color-base/color-base-min.js b/js/yui3/color-base/color-base-min.js
new file mode 100644
index 000000000..428061e6f
--- /dev/null
+++ b/js/yui3/color-base/color-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("color-base",function(e,t){var n=/^#?([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})(\ufffe)?/,r=/^#?([\da-fA-F]{1})([\da-fA-F]{1})([\da-fA-F]{1})(\ufffe)?/,i=/rgba?\(([\d]{1,3}), ?([\d]{1,3}), ?([\d]{1,3}),? ?([.\d]*)?\)/,s={HEX:"hex",RGB:"rgb",RGBA:"rgba"},o={hex:"toHex",rgb:"toRGB",rgba:"toRGBA"};e.Color={KEYWORDS:{black:"000",silver:"c0c0c0",gray:"808080",white:"fff",maroon:"800000",red:"f00",purple:"800080",fuchsia:"f0f",green:"008000",lime:"0f0",olive:"808000",yellow:"ff0",navy:"000080",blue:"00f",teal:"008080",aqua:"0ff"},REGEX_HEX:n,REGEX_HEX3:r,REGEX_RGB:i,re_RGB:i,re_hex:n,re_hex3:r,STR_HEX:"#{*}{*}{*}",STR_RGB:"rgb({*}, {*}, {*})",STR_RGBA:"rgba({*}, {*}, {*}, {*})",TYPES:s,CONVERTS:o,convert:function(t,n){var r=e.Color.CONVERTS[n.toLowerCase()],i=t;return r&&e.Color[r]&&(i=e.Color[r](t)),i},toHex:function(t){var n=e.Color._convertTo(t,"hex"),r=n.toLowerCase()==="transparent";return n.charAt(0)!=="#"&&!r&&(n="#"+n),r?n.toLowerCase():n.toUpperCase()},toRGB:function(t){var n=e.Color._convertTo(t,"rgb");return n.toLowerCase()},toRGBA:function(t){var n=e.Color._convertTo(t,"rgba");return n.toLowerCase()},toArray:function(t){var n=e.Color.findType(t).toUpperCase(),r,i,s,o;return n==="HEX"&&t.length<5&&(n="HEX3"),n.charAt(n.length-1)==="A"&&(n=n.slice(0,-1)),r=e.Color["REGEX_"+n],r&&(i=r.exec(t)||[],s=i.length,s&&(i.shift(),s--,n==="HEX3"&&(i[0]+=i[0],i[1]+=i[1],i[2]+=i[2]),o=i[s-1],o||(i[s-1]=1))),i},fromArray:function(t,n){t=t.concat();if(typeof n=="undefined")return t.join(", ");var r="{*}";n=e.Color["STR_"+n.toUpperCase()],t.length===3&&n.match(/\{\*\}/g).length===4&&t.push(1);while(n.indexOf(r)>=0&&t.length>0)n=n.replace(r,t.shift());return n},findType:function(t){if(e.Color.KEYWORDS[t])return"keyword";var n=t.indexOf("("),r;return n>0&&(r=t.substr(0,n)),r&&e.Color.TYPES[r.toUpperCase()]?e.Color.TYPES[r.toUpperCase()]:"hex"},_getAlpha:function(t){var n,r=e.Color.toArray(t);return r.length>3&&(n=r.pop()),+n||1},_keywordToHex:function(t){var n=e.Color.KEYWORDS[t];if(n)return n},_convertTo:function(t,n){if(t==="transparent")return t;var r=e.Color.findType(t),i=n,s,o,u,a;return r==="keyword"&&(t=e.Color._keywordToHex(t),r="hex"),r==="hex"&&t.length<5&&(t.charAt(0)==="#"&&(t=t.substr(1)),t="#"+t.charAt(0)+t.charAt(0)+t.charAt(1)+t.charAt(1)+t.charAt(2)+t.charAt(2)),r===n?t:(r.charAt(r.length-1)==="a"&&(r=r.slice(0,-1)),s=n.charAt(n.length-1)==="a",s&&(n=n.slice(0,-1),o=e.Color._getAlpha(t)),a=n.charAt(0).toUpperCase()+n.substr(1).toLowerCase(),u=e.Color["_"+r+"To"+a],u||r!=="rgb"&&n!=="rgb"&&(t=e.Color["_"+r+"ToRgb"](t),r="rgb",u=e.Color["_"+r+"To"+a]),u&&(t=u(t,s)),s&&(e.Lang.isArray(t)||(t=e.Color.toArray(t)),t.push(o),t=e.Color.fromArray(t,i.toUpperCase())),t)},_hexToRgb:function(e,t){var n,r,i;return e.charAt(0)==="#"&&(e=e.substr(1)),e=parseInt(e,16),n=e>>16,r=e>>8&255,i=e&255,t?[n,r,i]:"rgb("+n+", "+r+", "+i+")"},_rgbToHex:function(t){var n=e.Color.toArray(t),r=n[2]|n[1]<<8|n[0]<<16;r=(+r).toString(16);while(r.length<6)r="0"+r;return"#"+r}}},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/color-harmony/color-harmony-min.js b/js/yui3/color-harmony/color-harmony-min.js
new file mode 100644
index 000000000..930e31a28
--- /dev/null
+++ b/js/yui3/color-harmony/color-harmony-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("color-harmony",function(e,t){var n="hsl",r="rgb",s=30,o=10,u=120,a=60,f=90,l=5,c=10,h=e.Color,p={getComplementary:function(e,t){var n=p._start(e),r=[];return t=t||h.findType(e),r.push({}),r.push({h:180}),p._adjustOffsetAndFinish(n,r,t)},getSplit:function(e,t,n){var r=p._start(e),i=[];return t=t||s,n=n||h.findType(e),i.push({}),i.push({h:180+t}),i.push({h:180-t}),p._adjustOffsetAndFinish(r,i,n)},getAnalogous:function(e,t,n){var r=p._start(e),i=[];return t=t||o,n=n||h.findType(e),i.push({}),i.push({h:t}),i.push({h:t*2}),i.push({h:-t}),i.push({h:-t*2}),p._adjustOffsetAndFinish(r,i,n)},getTriad:function(e,t){var n=p._start(e),r=[];return t=t||h.findType(e),r.push({}),r.push({h:u}),r.push({h:-u}),p._adjustOffsetAndFinish(n,r,t)},getTetrad:function(e,t,n){var r=p._start(e),i=[];return t=t||a,n=n||h.findType(e),i.push({}),i.push({h:t}),i.push({h:180}),i.push({h:180+t}),p._adjustOffsetAndFinish(r,i,n)},getSquare:function(e,t){var n=p._start(e),r=[];return t=t||h.findType(e),r.push({}),r.push({h:f}),r.push({h:f*2}),r.push({h:f*3}),p._adjustOffsetAndFinish(n,r,t)},getMonochrome:function(e,t,n){var r=p._start(e),i=[],s=0,o,u,a=r.concat();t=t||l,n=n||h.findType(e);if(t<2)return e;u=100/(t-1);for(;s<=100;s+=u)a[2]=Math.max(Math.min(s,100),0),i.push(a.concat());o=i.length;for(s=0;s<o;s++)i[s]=p._finish(i[s],n);return i},getSimilar:function(e,t,n,r){var s=p._start(e),o=[],u,a=+s[1],f,d,v,m=+s[2],g,y,b;r=r||h.findType(e),n=n||l,t=t||c,u=t>100?100:t,f=Math.max(0,a-u),d=Math.min(100,a+u),g=Math.max(0,m-u),y=Math.min(100,m+u),o.push({});for(i=0;i<n;i++)v=Math.round(Math.random()*(d-f)+f),b=Math.round(Math.random()*(y-g)+g),o.push({h:Math.random()*t*2-t,s:-(a-v),l:-(m-b)});return p._adjustOffsetAndFinish(s,o,r)},getOffset:function(t,n,r){var i=e.Lang.isArray(t),s,o;return i?(s=t,o="hsl"):(s=p._start(t),o=h.findType(t)),r=r||o,n.h&&(s[0]=(+s[0]+n.h)%360),n.s&&(s[1]=Math.max(Math.min(+s[1]+n.s,100),0)),n.l&&(s[2]=Math.max(Math.min(+s[2]+n.l,100),0)),i?s:p._finish(s,r)},getBrightness:function(t){var n=h.toArray(h._convertTo(t,r)),i=n[0],s=n[1],o=n[2],u=e.Color._brightnessWeights;return Math.round(Math.sqrt(i*i*u.r+s*s*u.g+o*o*u.b)/255*100)},getSimilarBrightness:function(t,r,i){var s=h.toArray(h._convertTo(t,n)),o=p.getBrightness(r);return i=i||h.findType(t),i==="keyword"&&(i="hex"),s[2]=p._searchLuminanceForBrightness(s,o,0,100),t=h.fromArray(s,e.Color.TYPES.HSLA),h._convertTo(t,i)},_start:function(e){var t=h.toArray(h._convertTo(e,n));return t[0]=p._toSubtractive(t[0]),t},_finish:function(e,t){return e[0]=p._toAdditive(e[0]),e="hsla("+e[0]+", "+e[1]+"%, "+e[2]+"%, "+e[3]+")",t==="keyword"&&(t="hex"),h._convertTo(e,t)},_toAdditive:function(t){return t=e.Color._constrainHue(t),t<=180?t/=1.5:t<240&&(t=120+(t-180)*2),e.Color._constrainHue(t,10)},_toSubtractive:function(t){return t=e.Color._constrainHue(t),t<=120?t*=1.5:t<240&&(t=180+(t-120)/2),e.Color._constrainHue(t,10)},_constrainHue:function(e,t){while(e<0)e+=360;return e%=360,t&&(e=Math.round(e*t)/t),e},_brightnessWeights:{r:.221,g:.711,b:.068},_searchLuminanceForBrightness:function(t,n,r,i){var s=(i+r)/2,o;return t[2]=s,o=p.getBrightness(h.fromArray(t,e.Color.TYPES.HSL)),o+2>n&&o-2<n?s:o>n?p._searchLuminanceForBrightness(t,n,r,s):p._searchLuminanceForBrightness(t,n,s,i)},_adjustOffsetAndFinish:function(e,t,n){var r=[],i,s=t.length,o;for(i=0;i<s;i++)o=e.concat(),t[i]&&(o=p.getOffset(o,t[i])),r.push(p._finish(o,n));return r}};e.Color=e.mix(e.Color,p)},"3.17.2",{requires:["color-hsl"]});
diff --git a/js/yui3/color-hsl/color-hsl-min.js b/js/yui3/color-hsl/color-hsl-min.js
new file mode 100644
index 000000000..bc13548f4
--- /dev/null
+++ b/js/yui3/color-hsl/color-hsl-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("color-hsl",function(e,t){Color={REGEX_HSL:/hsla?\(([.\d]*), ?([.\d]*)%, ?([.\d]*)%,? ?([.\d]*)?\)/,STR_HSL:"hsl({*}, {*}%, {*}%)",STR_HSLA:"hsla({*}, {*}%, {*}%, {*})",toHSL:function(t){var n=e.Color._convertTo(t,"hsl");return n.toLowerCase()},toHSLA:function(t){var n=e.Color._convertTo(t,"hsla");return n.toLowerCase()},_rgbToHsl:function(t,n){var r,i,s,o=e.Color.REGEX_RGB.exec(t),u=o[1]/255,a=o[2]/255,f=o[3]/255,l=Math.max(u,a,f),c=Math.min(u,a,f),h=!1,p=l-c,d=l+c;return u===a&&a===f&&(h=!0),p===0?r=0:u===l?r=(60*(a-f)/p+360)%360:a===l?r=60*(f-u)/p+120:r=60*(u-a)/p+240,s=d/2,s===0||s===1?i=s:s<=.5?i=p/d:i=p/(2-d),h&&(i=0),r=Math.round(r),i=Math.round(i*100),s=Math.round(s*100),n?[r,i,s]:"hsl("+r+", "+i+"%, "+s+"%)"},_hslToRgb:function(t,n){var r=e.Color.REGEX_HSL.exec(t),i=parseInt(r[1],10)/360,s=parseInt(r[2],10)/100,o=parseInt(r[3],10)/100,u,a,f,l,c;return o<=.5?c=o*(s+1):c=o+s-o*s,l=2*o-c,u=Math.round(Color._hueToRGB(l,c,i+1/3)*255),a=Math.round(Color._hueToRGB(l,c,i)*255),f=Math.round(Color._hueToRGB(l,c,i-1/3)*255),n?[u,a,f]:"rgb("+u+", "+a+", "+f+")"},_hueToRGB:function(e,t,n){return n<0?n+=1:n>1&&(n-=1),n*6<1?e+(t-e)*6*n:n*2<1?t:n*3<2?e+(t-e)*(2/3-n)*6:e}},e.Color=e.mix(Color,e.Color),e.Color.TYPES=e.mix(e.Color.TYPES,{HSL:"hsl",HSLA:"hsla"}),e.Color.CONVERTS=e.mix(e.Color.CONVERTS,{hsl:"toHSL",hsla:"toHSLA"})},"3.17.2",{requires:["color-base"]});
diff --git a/js/yui3/color-hsv/color-hsv-min.js b/js/yui3/color-hsv/color-hsv-min.js
new file mode 100644
index 000000000..1e791a239
--- /dev/null
+++ b/js/yui3/color-hsv/color-hsv-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("color-hsv",function(e,t){Color={REGEX_HSV:/hsva?\(([.\d]*), ?([.\d]*)%, ?([.\d]*)%,? ?([.\d]*)?\)/,STR_HSV:"hsv({*}, {*}%, {*}%)",STR_HSVA:"hsva({*}, {*}%, {*}%, {*})",toHSV:function(t){var n=e.Color._convertTo(t,"hsv");return n.toLowerCase()},toHSVA:function(t){var n=e.Color._convertTo(t,"hsva");return n.toLowerCase()},_rgbToHsv:function(t,n){var r,i,s,o=e.Color.REGEX_RGB.exec(t),u=o[1]/255,a=o[2]/255,f=o[3]/255,l=Math.max(u,a,f),c=Math.min(u,a,f),h=l-c;l===c?r=0:l===u?r=60*(a-f)/h:l===a?r=60*(f-u)/h+120:r=60*(u-a)/h+240,i=l===0?0:1-c/l;while(r<0)r+=360;return r%=360,r=Math.round(r),i=Math.round(i*100),s=Math.round(l*100),n?[r,i,s]:e.Color.fromArray([r,i,s],e.Color.TYPES.HSV)},_hsvToRgb:function(t,n){var r=e.Color.REGEX_HSV.exec(t),i=parseInt(r[1],10),s=parseInt(r[2],10)/100,o=parseInt(r[3],10)/100,u,a,f,l=Math.floor(i/60)%6,c=i/60-l,h=o*(1-s),p=o*(1-s*c),d=o*(1-s*(1-c));if(s===0)u=o,a=o,f=o;else switch(l){case 0:u=o,a=d,f=h;break;case 1:u=p,a=o,f=h;break;case 2:u=h,a=o,f=d;break;case 3:u=h,a=p,f=o;break;case 4:u=d,a=h,f=o;break;case 5:u=o,a=h,f=p}return u=Math.min(255,Math.round(u*256)),a=Math.min(255,Math.round(a*256)),f=Math.min(255,Math.round(f*256)),n?[u,a,f]:e.Color.fromArray([u,a,f],e.Color.TYPES.RGB)}},e.Color=e.mix(Color,e.Color),e.Color.TYPES=e.mix(e.Color.TYPES,{HSV:"hsv",HSVA:"hsva"}),e.Color.CONVERTS=e.mix(e.Color.CONVERTS,{hsv:"toHSV",hsva:"toHSVA"})},"3.17.2",{requires:["color-base"]});
diff --git a/js/yui3/console-filters/assets/console-filters-core.css b/js/yui3/console-filters/assets/console-filters-core.css
new file mode 100644
index 000000000..b870e2c78
--- /dev/null
+++ b/js/yui3/console-filters/assets/console-filters-core.css
@@ -0,0 +1,7 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
diff --git a/js/yui3/console-filters/assets/skins/sam/console-filters.css b/js/yui3/console-filters/assets/skins/sam/console-filters.css
new file mode 100644
index 000000000..1230e6343
--- /dev/null
+++ b/js/yui3/console-filters/assets/skins/sam/console-filters.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-console-ft .yui3-console-filters-categories,.yui3-skin-sam .yui3-console-ft .yui3-console-filters-sources{text-align:left;padding:5px 0;border:1px inset;margin:0 2px}.yui3-skin-sam .yui3-console-ft .yui3-console-filters-categories{background:#fff;border-bottom:2px ridge}.yui3-skin-sam .yui3-console-ft .yui3-console-filters-sources{background:#fff;margin-bottom:2px;border-top:0 none;border-bottom-right-radius:10px;border-bottom-left-radius:10px;-moz-border-radius-bottomright:10px;-moz-border-radius-bottomleft:10px;-webkit-border-bottom-right-radius:10px;-webkit-border-bottom-left-radius:10px}.yui3-skin-sam .yui3-console-filter-label{white-space:nowrap;margin-left:1ex}#yui3-css-stamp.skin-sam-console-filters{display:none}
diff --git a/js/yui3/console-filters/console-filters-min.js b/js/yui3/console-filters/console-filters-min.js
new file mode 100644
index 000000000..b5bc495cf
--- /dev/null
+++ b/js/yui3/console-filters/console-filters-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("console-filters",function(e,t){function b(){b.superclass.constructor.apply(this,arguments)}var n=e.ClassNameManager.getClassName,r="console",i="filters",s="filter",o="category",u="source",a="category.",f="source.",l="host",c="checked",h="defaultVisibility",p=".",d="",v=p+e.Console.CHROME_CLASSES.console_bd_class,m=p+e.Console.CHROME_CLASSES.console_ft_class,g="input[type=checkbox].",y=e.Lang.isString;e.namespace("Plugin").ConsoleFilters=e.extend(b,e.Plugin.Base,{_entries:null,_cacheLimit:Number.POSITIVE_INFINITY,_categories:null,_sources:null,initializer:function(){this._entries=[],this.get(l).on("entry",this._onEntry,this),this.doAfter("renderUI",this.renderUI),this.doAfter("syncUI",this.syncUI),this.doAfter("bindUI",this.bindUI),this.doAfter("clearConsole",this._afterClearConsole),this.get(l).get("rendered")&&(this.renderUI(),this.syncUI(),this.bindUI()),this.after("cacheLimitChange",this._afterCacheLimitChange)},destructor:function(){this._entries=[],this._categories&&this._categories.remove(),this._sources&&this._sources.remove()},renderUI:function(){var t=this.get(l).get("contentBox").one(m),n;t&&(n=e.Lang.sub(b.CATEGORIES_TEMPLATE,b.CHROME_CLASSES),this._categories=t.appendChild(e.Node.create(n)),n=e.Lang.sub(b.SOURCES_TEMPLATE,b.CHROME_CLASSES),this._sources=t.appendChild(e.Node.create(n)))},bindUI:function(){this._categories.on("click",e.bind(this._onCategoryCheckboxClick,this)),this._sources.on("click",e.bind(this._onSourceCheckboxClick,this)),this.after("categoryChange",this._afterCategoryChange),this.after("sourceChange",this._afterSourceChange)},syncUI:function(){e.each(this.get(o),function(e,t){this._uiSetCheckbox(o,t,e)},this),e.each(this.get(u),function(e,t){this._uiSetCheckbox(u,t,e)},this),this.refreshConsole()},_onEntry:function(e){this._entries.push(e.message);var t=a+e.message.category,n=f+e.message.source,r=this.get(t),i=this.get(n),s=this._entries.length-this._cacheLimit,o;s>0&&this._entries.splice(0,s),r===undefined&&(o=this.get(h),this.set(t,o),r=o),i===undefined&&(o=this.get(h),this.set(n,o),i=o),(!r||!i)&&e.preventDefault()},_afterClearConsole:function(){this._entries=[]},_afterCategoryChange:function(e){var t=e.subAttrName.replace(/category\./,d),n=e.prevVal,r=e.newVal;if(!t||n[t]!==undefined)this.refreshConsole(),this._filterBuffer();t&&!e.fromUI&&this._uiSetCheckbox(o,t,r[t])},_afterSourceChange:function(e){var t=e.subAttrName.replace(/source\./,d),n=e.prevVal,r=e.newVal;if(!t||n[t]!==undefined)this.refreshConsole(),this._filterBuffer();t&&!e.fromUI&&this._uiSetCheckbox(u,t,r[t])},_filterBuffer:function(){var e=this.get(o),t=this.get(u),n=this.get(l).buffer,r=null,i;for(i=n.length-1;i>=0;--i)!e[n[i].category]||!t[n[i].source]?r=r||i:r&&(n.splice(i,r-i),r=null);r&&n.splice(0,r+1)},_afterCacheLimitChange:function(e){if(isFinite(e.newVal)){var t=this._entries.length-e.newVal;t>0&&this._entries.splice(0,t)}},refreshConsole:function(){var e=this._entries,t=this.get(l),n=t.get("contentBox").one(v),r=t.get("consoleLimit"),i=this.get(o),s=this.get(u),a=[],f,c;if(n){t._cancelPrintLoop();for(f=e.length-1;f>=0&&r>=0;--f)c=e[f],i[c.category]&&s[c.source]&&(a.unshift(c),--r);n.setHTML(d),t.buffer=a,t.printBuffer()}},_uiSetCheckbox:function(e,t,i){if(e&&t){var u=e===o?this._categories:this._sources,a=g+n(r,s,t),f=u.one(a),h;f||(h=this.get(l),this._createCheckbox(u,t),f=u.one(a),h._uiSetHeight(h.get("height"))),f.set(c,i)}},_onCategoryCheckboxClick:function(e){var t=e.target,n;t.hasClass(b.CHROME_CLASSES.filter)&&(n=t.get("value"),n&&n in this.get(o)&&this.set(a+n,t.get(c),{fromUI:!0}))},_onSourceCheckboxClick:function(e){var t=e.target,n;t.hasClass(b.CHROME_CLASSES.filter)&&(n=t.get("value"),n&&n in this.get(u)&&this.set(f+n,t.get(c),{fromUI:!0}))},hideCategory:function(t,n){y(n)?e.Array.each(arguments,this.hideCategory,this):this.set(a+t,!1)},showCategory:function(t,n){y(n)?e.Array.each(arguments,this.showCategory,this):this.set(a+t,!0)},hideSource:function(t,n){y(n)?e.Array.each(arguments,this.hideSource,this):this.set(f+t,!1)},showSource:function(t,n){y(n)?e.Array.each(arguments,this.showSource,this):this.set(f+t,!0)},_createCheckbox:function(t,i){var o=e.merge(b.CHROME_CLASSES,{filter_name:i,filter_class:n(r,s,i)}),u=e.Node.create(e.Lang.sub(b.FILTER_TEMPLATE,o));t.appendChild(u)},_validateCategory:function(t,n){return e.Lang.isObject(n,!0)&&t.split(/\./).length<3},_validateSource:function(t,n){return e.Lang.isObject(n,!0)&&t.split(/\./).length<3},_setCacheLimit:function(t){return e.Lang.isNumber(t)?(this._cacheLimit=t,t):e.Attribute.INVALID_VALUE}},{NAME:"consoleFilters",NS:s,CATEGORIES_TEMPLATE:'<div class="{categories}"></div>',SOURCES_TEMPLATE:'<div class="{sources}"></div>',FILTER_TEMPLATE:'<label class="{filter_label}"><input type="checkbox" value="{filter_name}" class="{filter} {filter_class}"> {filter_name}</label>&#8201;',CHROME_CLASSES:{categories:n(r,i,"categories"),sources:n(r,i,"sources"),category:n(r,s,o),source:n(r,s,u),filter:n(r,s),filter_label:n(r,s,"label")},ATTRS:{defaultVisibility:{value:!0,validator:e.Lang.isBoolean},category:{value:{},validator:function(e,t){return this._validateCategory(t,e)}},source:{value:{},validator:function(e,t){return this._validateSource(t,e)}},cacheLimit:{value:Number.POSITIVE_INFINITY,setter:function(e){return this._setCacheLimit(e)}}}})},"3.17.2",{requires:["plugin","console"],skinnable:!0});
diff --git a/js/yui3/console/assets/console-core.css b/js/yui3/console/assets/console-core.css
new file mode 100644
index 000000000..b870e2c78
--- /dev/null
+++ b/js/yui3/console/assets/console-core.css
@@ -0,0 +1,7 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
diff --git a/js/yui3/console/assets/skins/sam/bg.png b/js/yui3/console/assets/skins/sam/bg.png
new file mode 100644
index 000000000..fd11e03de
--- /dev/null
+++ b/js/yui3/console/assets/skins/sam/bg.png
Binary files differ
diff --git a/js/yui3/console/assets/skins/sam/console.css b/js/yui3/console/assets/skins/sam/console.css
new file mode 100644
index 000000000..a471b45ec
--- /dev/null
+++ b/js/yui3/console/assets/skins/sam/console.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-console-separate{position:absolute;right:1em;top:1em;z-index:999}.yui3-skin-sam .yui3-console-inline{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:top}.yui3-skin-sam .yui3-console-inline .yui3-console-content{position:relative}.yui3-skin-sam .yui3-console-content{background:#777;_background:#d8d8da url(bg.png) repeat-x 0 0;font:normal 13px/1.3 Arial,sans-serif;text-align:left;border:1px solid #777;border-radius:10px;-moz-border-radius:10px;-webkit-border-radius:10px}.yui3-skin-sam .yui3-console-hd,.yui3-skin-sam .yui3-console-bd,.yui3-skin-sam .yui3-console-ft{position:relative}.yui3-skin-sam .yui3-console-hd,.yui3-skin-sam .yui3-console-ft .yui3-console-controls{text-align:right}.yui3-skin-sam .yui3-console-hd{background:#d8d8da url(bg.png) repeat-x 0 0;padding:1ex;border:1px solid transparent;_border:0 none;border-top-right-radius:10px;border-top-left-radius:10px;-moz-border-radius-topright:10px;-moz-border-radius-topleft:10px;-webkit-border-top-right-radius:10px;-webkit-border-top-left-radius:10px}.yui3-skin-sam .yui3-console-bd{background:#fff;border-top:1px solid #777;border-bottom:1px solid #777;color:#000;font-size:11px;overflow:auto;overflow-x:auto;overflow-y:scroll;_width:100%}.yui3-skin-sam .yui3-console-ft{background:#d8d8da url(bg.png) repeat-x 0 0;border:1px solid transparent;_border:0 none;border-bottom-right-radius:10px;border-bottom-left-radius:10px;-moz-border-radius-bottomright:10px;-moz-border-radius-bottomleft:10px;-webkit-border-bottom-right-radius:10px;-webkit-border-bottom-left-radius:10px}.yui3-skin-sam .yui3-console-controls{padding:4px 1ex;zoom:1}.yui3-skin-sam .yui3-console-title{color:#000;display:inline;float:left;font-weight:bold;font-size:13px;height:24px;line-height:24px;margin:0;padding-left:1ex}.yui3-skin-sam .yui3-console-pause-label{float:left}.yui3-skin-sam .yui3-console-button{line-height:1.3}.yui3-skin-sam .yui3-console-collapsed .yui3-console-bd,.yui3-skin-sam .yui3-console-collapsed .yui3-console-ft{display:none}.yui3-skin-sam .yui3-console-content.yui3-console-collapsed{-webkit-border-radius:0}.yui3-skin-sam .yui3-console-collapsed .yui3-console-hd{border-radius:10px;-moz-border-radius:10px;-webkit-border-radius:0}.yui3-skin-sam .yui3-console-entry{border-bottom:1px solid #aaa;min-height:32px;_height:32px}.yui3-skin-sam .yui3-console-entry-meta{margin:0;overflow:hidden}.yui3-skin-sam .yui3-console-entry-content{margin:0;padding:0 1ex;white-space:pre-wrap;word-wrap:break-word}.yui3-skin-sam .yui3-console-entry-meta .yui3-console-entry-src{color:#000;font-style:italic;font-weight:bold;float:right;margin:2px 5px 0 0}.yui3-skin-sam .yui3-console-entry-meta .yui3-console-entry-time{color:#777;padding-left:1ex}.yui3-skin-sam .yui3-console-entry-warn .yui3-console-entry-meta .yui3-console-entry-time{color:#555}.yui3-skin-sam .yui3-console-entry-info .yui3-console-entry-meta .yui3-console-entry-cat,.yui3-skin-sam .yui3-console-entry-warn .yui3-console-entry-meta .yui3-console-entry-cat,.yui3-skin-sam .yui3-console-entry-error .yui3-console-entry-meta .yui3-console-entry-cat{display:none}.yui3-skin-sam .yui3-console-entry-warn{background:#aee url(warn_error.png) no-repeat -15px 15px}.yui3-skin-sam .yui3-console-entry-error{background:#ffa url(warn_error.png) no-repeat 5px -24px;color:#900}.yui3-skin-sam .yui3-console-entry-warn .yui3-console-entry-content,.yui3-skin-sam .yui3-console-entry-error .yui3-console-entry-content{padding-left:24px}.yui3-skin-sam .yui3-console-entry-cat{text-transform:uppercase;padding:1px 4px;background-color:#ccc}.yui3-skin-sam .yui3-console-entry-info .yui3-console-entry-cat{background-color:#ac2}.yui3-skin-sam .yui3-console-entry-warn .yui3-console-entry-cat{background-color:#e81}.yui3-skin-sam .yui3-console-entry-error .yui3-console-entry-cat{background-color:#b00;color:#fff}.yui3-skin-sam .yui3-console-hidden{display:none}#yui3-css-stamp.skin-sam-console{display:none}
diff --git a/js/yui3/console/assets/skins/sam/warn_error.png b/js/yui3/console/assets/skins/sam/warn_error.png
new file mode 100644
index 000000000..8b0db799e
--- /dev/null
+++ b/js/yui3/console/assets/skins/sam/warn_error.png
Binary files differ
diff --git a/js/yui3/console/assets/warn_error.png b/js/yui3/console/assets/warn_error.png
new file mode 100644
index 000000000..8b0db799e
--- /dev/null
+++ b/js/yui3/console/assets/warn_error.png
Binary files differ
diff --git a/js/yui3/console/console-min.js b/js/yui3/console/console-min.js
new file mode 100644
index 000000000..828dab22e
--- /dev/null
+++ b/js/yui3/console/console-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("console",function(e,t){function et(){et.superclass.constructor.apply(this,arguments)}var n=e.ClassNameManager.getClassName,r="checked",i="clear",s="click",o="collapsed",u="console",a="contentBox",f="disabled",l="entry",c="error",h="height",p="info",d="lastTime",v="pause",m="paused",g="reset",y="startTime",b="title",w="warn",E=".",S=n(u,"button"),x=n(u,"checkbox"),T=n(u,i),N=n(u,"collapse"),C=n(u,o),k=n(u,"controls"),L=n(u,"hd"),A=n(u,"bd"),O=n(u,"ft"),M=n(u,b),_=n(u,l),D=n(u,l,"cat"),P=n(u,l,"content"),H=n(u,l,"meta"),B=n(u,l,"src"),j=n(u,l,"time"),F=n(u,v),I=n(u,v,"label"),q=/^(\S+)\s/,R=/&(?!#?[a-z0-9]+;)/g,U=/>/g,z=/</g,W="&#38;",X="&#62;",V="&#60;",$='<div class="{entry_class} {cat_class} {src_class}"><p class="{entry_meta_class}"><span class="{entry_src_class}">{sourceAndDetail}</span><span class="{entry_cat_class}">{category}</span><span class="{entry_time_class}"> {totalTime}ms (+{elapsedTime}) {localTime}</span></p><pre class="{entry_content_class}">{message}</pre></div>',J=e.Lang,K=e.Node.create,Q=J.isNumber,G=J.isString,Y=e.merge,Z=e.Lang.sub;e.Console=e.extend(et,e.Widget,{_evtCat:null,_head:null,_body:null,_foot:null,_printLoop:null,buffer:null,log:function(){return e.log.apply(e,arguments),this},clearConsole:function(){return this._body.empty(),this._cancelPrintLoop(),this.buffer=[],this},reset:function(){return this.fire(g),this},collapse:function(){return this.set(o,!0),this},expand:function(){return this.set(o,!1),this},printBuffer:function(t){var n=this.buffer,r=e.config.debug,i=[],s=this.get("consoleLimit"),o=this.get("newestOnTop"),u=o?this._body.get("firstChild"):null,a;n.length>s&&n.splice(0,n.length-s),t=Math.min(n.length,t||n.length),e.config.debug=!1;if(!this.get(m)&&this.get("rendered")){for(a=0;a<t&&n.length;++a)i[a]=this._createEntryHTML(n.shift());n.length||this._cancelPrintLoop(),i.length&&(o&&i.reverse(),this._body.insertBefore(K(i.join("")),u),this.get("scrollIntoView")&&this.scrollToLatest(),this._trimOldEntries())}return e.config.debug=r,this},initializer:function(){this._evtCat=e.stamp(this)+"|",this.buffer=[],this.get("logSource").on(this._evtCat+this.get("logEvent"),e.bind("_onLogEvent",this)),this.publish(l,{defaultFn:this._defEntryFn}),this.publish(g,{defaultFn:this._defResetFn}),this.after("rendered",this._schedulePrint)},destructor:function(){var e=this.get("boundingBox");this._cancelPrintLoop(),this.get("logSource").detach(this._evtCat+"*"),e.purge(!0)},renderUI:function(){this._initHead(),this._initBody(),this._initFoot();var e=this.get("style");e!=="block"&&this.get("boundingBox").addClass(this.getClassName(e))},syncUI:function(){this._uiUpdatePaused(this.get(m)),this._uiUpdateCollapsed(this.get(o)),this._uiSetHeight(this.get(h))},bindUI:function(){this.get(a).one("button."+N).on(s,this._onCollapseClick,this),this.get(a).one("input[type=checkbox]."+F).on(s,this._onPauseClick,this),this.get(a).one("button."+T).on(s,this._onClearClick,this),this.after(this._evtCat+"stringsChange",this._afterStringsChange),this.after(this._evtCat+"pausedChange",this._afterPausedChange),this.after(this._evtCat+"consoleLimitChange",this._afterConsoleLimitChange),this.after(this._evtCat+"collapsedChange",this._afterCollapsedChange)},_initHead:function(){var e=this.get(a),t=Y(et.CHROME_CLASSES,{str_collapse:this.get("strings.collapse"),str_title:this.get("strings.title")});this._head=K(Z(et.HEADER_TEMPLATE,t)),e.insertBefore(this._head,e.get("firstChild"))},_initBody:function(){this._body=K(Z(et.BODY_TEMPLATE,et.CHROME_CLASSES)),this.get(a).appendChild(this._body)},_initFoot:function(){var t=Y(et.CHROME_CLASSES,{id_guid:e.guid(),str_pause:this.get("strings.pause"),str_clear:this.get("strings.clear")});this._foot=K(Z(et.FOOTER_TEMPLATE,t)),this.get(a).appendChild(this._foot)},_isInLogLevel:function(e){var t=e.cat,n=this.get("logLevel");if(n!==p){t=t||p,G(t)&&(t=t.toLowerCase());if(t===w&&n===c||t===p&&n!==p)return!1}return!0},_normalizeMessage:function(e){var t=e.msg,n=e.cat,r=e.src,i={time:new Date,message:t,category:n||this.get("defaultCategory"),sourceAndDetail:r||this.get("defaultSource"),source:null,localTime:null,elapsedTime:null,totalTime:null};return i.source=q.test(i.sourceAndDetail)?RegExp.$1:i.sourceAndDetail,i.localTime=i.time.toLocaleTimeString?i.time.toLocaleTimeString():i.time+"",i.elapsedTime=i.time-this.get(d),i.totalTime=i.time-this.get(y),this._set(d,i.time),i},_schedulePrint:function(){!this._printLoop&&!this.get(m)&&this.get("rendered")&&(this._printLoop=e.later(this.get("printTimeout"),this,this.printBuffer,this.get("printLimit"),!0))},_createEntryHTML:function(e){return e=Y(this._htmlEscapeMessage(e),et.ENTRY_CLASSES,{cat_class:this.getClassName(l,e.category),src_class:this.getClassName(l,e.source)}),this.get("entryTemplate").replace(/\{(\w+)\}/g,function(t,n){return n in e?e[n]:""})},scrollToLatest:function(){var e=this.get("newestOnTop")?0:this._body.get("scrollHeight");this._body.set("scrollTop",e)},_htmlEscapeMessage:function(e){return e.message=this._encodeHTML(e.message),e.source=this._encodeHTML(e.source),e.sourceAndDetail=this._encodeHTML(e.sourceAndDetail),e.category=this._encodeHTML(e.category),e},_trimOldEntries:function(){e.config.debug=!1;var t=this._body,n=this.get("consoleLimit"),r=e.config.debug,i,s,o,u;if(t){i=t.all(E+_),u=i.size()-n;if(u>0){this.get("newestOnTop")?(o=n,u=i.size()):o=0,this._body.setStyle("display","none");for(;o<u;++o)s=i.item(o),s&&s.remove();this._body.setStyle("display","")}}e.config.debug=r},_encodeHTML:function(e){return G(e)?e.replace(R,W).replace(z,V).replace(U,X):e},_cancelPrintLoop:function(){this._printLoop&&(this._printLoop.cancel(),this._printLoop=null)},_validateStyle:function(e){return e==="inline"||e==="block"||e==="separate"},_onPauseClick:function(e){this.set(m,e.target.get(r))},_onClearClick:function(e){this.clearConsole()},_onCollapseClick:function(e){this.set(o,!this.get(o))},_validateLogSource:function(t){return t&&e.Lang.isFunction(t.on)},_setLogLevel:function(e){return G(e)&&(e=e.toLowerCase
+()),e===w||e===c?e:p},_getUseBrowserConsole:function(){var e=this.get("logSource");return e instanceof YUI?e.config.useBrowserConsole:null},_setUseBrowserConsole:function(t){var n=this.get("logSource");return n instanceof YUI?(t=!!t,n.config.useBrowserConsole=t,t):e.Attribute.INVALID_VALUE},_uiSetHeight:function(e){et.superclass._uiSetHeight.apply(this,arguments);if(this._head&&this._foot){var t=this.get("boundingBox").get("offsetHeight")-this._head.get("offsetHeight")-this._foot.get("offsetHeight");this._body.setStyle(h,t+"px")}},_uiSizeCB:function(){},_afterStringsChange:function(e){var t=e.subAttrName?e.subAttrName.split(E)[1]:null,n=this.get(a),r=e.prevVal,s=e.newVal;(!t||t===b)&&r.title!==s.title&&n.all(E+M).setHTML(s.title),(!t||t===v)&&r.pause!==s.pause&&n.all(E+I).setHTML(s.pause),(!t||t===i)&&r.clear!==s.clear&&n.all(E+T).set("value",s.clear)},_afterPausedChange:function(t){var n=t.newVal;t.src!==e.Widget.SRC_UI&&this._uiUpdatePaused(n),n?this._printLoop&&this._cancelPrintLoop():this._schedulePrint()},_uiUpdatePaused:function(e){var t=this._foot.all("input[type=checkbox]."+F);t&&t.set(r,e)},_afterConsoleLimitChange:function(){this._trimOldEntries()},_afterCollapsedChange:function(e){this._uiUpdateCollapsed(e.newVal)},_uiUpdateCollapsed:function(e){var t=this.get("boundingBox"),n=t.all("button."+N),r=e?"addClass":"removeClass",i=this.get("strings."+(e?"expand":"collapse"));t[r](C),n&&n.setHTML(i),this._uiSetHeight(e?this._head.get("offsetHeight"):this.get(h))},_afterVisibleChange:function(e){et.superclass._afterVisibleChange.apply(this,arguments),this._uiUpdateFromHideShow(e.newVal)},_uiUpdateFromHideShow:function(e){e&&this._uiSetHeight(this.get(h))},_onLogEvent:function(t){if(!this.get(f)&&this._isInLogLevel(t)){var n=e.config.debug;e.config.debug=!1,this.fire(l,{message:this._normalizeMessage(t)}),e.config.debug=n}},_defResetFn:function(){this.clearConsole(),this.set(y,new Date),this.set(f,!1),this.set(m,!1)},_defEntryFn:function(e){e.message&&(this.buffer.push(e.message),this._schedulePrint())}},{NAME:u,LOG_LEVEL_INFO:p,LOG_LEVEL_WARN:w,LOG_LEVEL_ERROR:c,ENTRY_CLASSES:{entry_class:_,entry_meta_class:H,entry_cat_class:D,entry_src_class:B,entry_time_class:j,entry_content_class:P},CHROME_CLASSES:{console_hd_class:L,console_bd_class:A,console_ft_class:O,console_controls_class:k,console_checkbox_class:x,console_pause_class:F,console_pause_label_class:I,console_button_class:S,console_clear_class:T,console_collapse_class:N,console_title_class:M},HEADER_TEMPLATE:'<div class="{console_hd_class}"><h4 class="{console_title_class}">{str_title}</h4><button type="button" class="{console_button_class} {console_collapse_class}">{str_collapse}</button></div>',BODY_TEMPLATE:'<div class="{console_bd_class}"></div>',FOOTER_TEMPLATE:'<div class="{console_ft_class}"><div class="{console_controls_class}"><label class="{console_pause_label_class}"><input type="checkbox" class="{console_checkbox_class} {console_pause_class}" value="1" id="{id_guid}"> {str_pause}</label><button type="button" class="{console_button_class} {console_clear_class}">{str_clear}</button></div></div>',ENTRY_TEMPLATE:$,ATTRS:{logEvent:{value:"yui:log",writeOnce:!0,validator:G},logSource:{value:e,writeOnce:!0,validator:function(e){return this._validateLogSource(e)}},strings:{valueFn:function(){return e.Intl.get("console")}},paused:{value:!1,validator:J.isBoolean},defaultCategory:{value:p,validator:G},defaultSource:{value:"global",validator:G},entryTemplate:{value:$,validator:G},logLevel:{value:e.config.logLevel||p,setter:function(e){return this._setLogLevel(e)}},printTimeout:{value:100,validator:Q},printLimit:{value:50,validator:Q},consoleLimit:{value:300,validator:Q},newestOnTop:{value:!0},scrollIntoView:{value:!0},startTime:{value:new Date},lastTime:{value:new Date,readOnly:!0},collapsed:{value:!1},height:{value:"300px"},width:{value:"300px"},useBrowserConsole:{lazyAdd:!1,value:!1,getter:function(){return this._getUseBrowserConsole()},setter:function(e){return this._setUseBrowserConsole(e)}},style:{value:"separate",writeOnce:!0,validator:function(e){return this._validateStyle(e)}}}})},"3.17.2",{requires:["yui-log","widget"],skinnable:!0,lang:["en","es","hu","it","ja"]});
diff --git a/js/yui3/console/lang/console.js b/js/yui3/console/lang/console.js
new file mode 100644
index 000000000..5ff3c9a13
--- /dev/null
+++ b/js/yui3/console/lang/console.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/console",function(e){e.Intl.add("console","",{title:"Log Console",pause:"Pause",clear:"Clear",collapse:"Collapse",expand:"Expand"})},"3.17.2");
diff --git a/js/yui3/console/lang/console_en.js b/js/yui3/console/lang/console_en.js
new file mode 100644
index 000000000..b0d474b1f
--- /dev/null
+++ b/js/yui3/console/lang/console_en.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/console_en",function(e){e.Intl.add("console","en",{title:"Log Console",pause:"Pause",clear:"Clear",collapse:"Collapse",expand:"Expand"})},"3.17.2");
diff --git a/js/yui3/console/lang/console_es.js b/js/yui3/console/lang/console_es.js
new file mode 100644
index 000000000..42ba657cc
--- /dev/null
+++ b/js/yui3/console/lang/console_es.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/console_es",function(e){e.Intl.add("console","es",{title:"Consola de informaci\u00f3n",pause:"Pausa",clear:"Borrar",collapse:"Colapsar",expand:"Expandir"})},"3.17.2");
diff --git a/js/yui3/console/lang/console_hu.js b/js/yui3/console/lang/console_hu.js
new file mode 100644
index 000000000..cfec3d64d
--- /dev/null
+++ b/js/yui3/console/lang/console_hu.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/console_hu",function(e){e.Intl.add("console","hu",{title:"Log Konzol",pause:"Sz\u00fcnet",clear:"T\u00f6r\u00f6l",collapse:"\u00d6sszecsuk",expand:"Kinyit"})},"3.17.2");
diff --git a/js/yui3/console/lang/console_it.js b/js/yui3/console/lang/console_it.js
new file mode 100644
index 000000000..b771d608d
--- /dev/null
+++ b/js/yui3/console/lang/console_it.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/console_it",function(e){e.Intl.add("console","it",{title:"Console dei messaggi",pause:"Pausa",clear:"Cancella",collapse:"Collassa",expand:"Espandi"})},"3.17.2");
diff --git a/js/yui3/console/lang/console_ja.js b/js/yui3/console/lang/console_ja.js
new file mode 100644
index 000000000..20fbf74ee
--- /dev/null
+++ b/js/yui3/console/lang/console_ja.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/console_ja",function(e){e.Intl.add("console","ja",{title:"\u30ed\u30b0\u30b3\u30f3\u30bd\u30fc\u30eb",pause:"\u4e00\u6642\u505c\u6b62",clear:"\u30af\u30ea\u30a2",collapse:"\u9589\u3058\u308b",expand:"\u958b\u304f"})},"3.17.2");
diff --git a/js/yui3/content-editable/content-editable-min.js b/js/yui3/content-editable/content-editable-min.js
new file mode 100644
index 000000000..e805b0a77
--- /dev/null
+++ b/js/yui3/content-editable/content-editable-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("content-editable",function(e,t){var n=e.Lang,r=e.Node,i="contentready",s="ready",o="p",u="blur",a="container",f="contentEditable",l="",c="focus",h="host",p="innerHTML",d="key",v="parentNode",m="paste",g="Text",y="use",b=function(){b.superclass.constructor.apply(this,arguments)};e.extend(b,e.Plugin.Base,{_rendered:null,_instance:null,initializer:function(){var e=this.get(h);e&&(e.frame=this),this._eventHandles=[],this.publish(s,{emitFacade:!0,defaultFn:this._defReadyFn})},destructor:function(){(new e.EventHandle(this._eventHandles)).detach(),this._container.removeAttribute(f)},_onDomEvent:function(e){var t;e.frameX=e.frameY=0,(e.pageX>0||e.pageY>0)&&e.type.substring(0,3)!==d&&(t=this._container.getXY(),e.frameX=t[0],e.frameY=t[1]),e.frameTarget=e.target,e.frameCurrentTarget=e.currentTarget,e.frameEvent=e,this.fire("dom:"+e.type,e)},_DOMPaste:function(e){var t=this.getInstance(),n=l,r=t.config.win;e._event.originalTarget&&(n=e._event.originalTarget),e._event.clipboardData&&(n=e._event.clipboardData.getData(g)),r.clipboardData&&(n=r.clipboardData.getData(g),n===l&&(r.clipboardData.setData(g,n)||(n=null))),e.frameTarget=e.target,e.frameCurrentTarget=e.currentTarget,e.frameEvent=e,n?e.clipboardData={data:n,getData:function(){return n}}:e.clipboardData=null,this.fire("dom:paste",e)},_defReadyFn:function(){var t=this.getInstance(),n=this.get(a);e.each(b.DOM_EVENTS,function(r,i){var s=e.bind(this._onDomEvent,this),o=e.UA.ie&&b.THROTTLE_TIME>0?e.throttle(s,b.THROTTLE_TIME):s;t.Node.DOM_EVENTS[i]||(t.Node.DOM_EVENTS[i]=1),r===1&&i!==c&&i!==u&&i!==m&&(i.substring(0,3)===d?this._eventHandles.push(n.on(i,o,n)):this._eventHandles.push(n.on(i,s,n)))},this),t.Node.DOM_EVENTS.paste=1,this._eventHandles.push(n.on(m,e.bind(this._DOMPaste,this),n),n.on(c,e.bind(this._onDomEvent,this),n),n.on(u,e.bind(this._onDomEvent,this),n)),t.__use=t.use,t.use=e.bind(this.use,this)},_onContentReady:function(t){if(!this._ready){this._ready=!0;var n=this.getInstance(),o=e.clone(this.get(y));this.fire(i),t&&(n.config.doc=r.getDOMNode(t.target)),o.push(e.bind(function(){n.EditorSelection&&(n.EditorSelection.DEFAULT_BLOCK_TAG=this.get("defaultblock"),n.EditorSelection.ROOT=this.get(a)),this.fire(s)},this)),n.use.apply(n,o)}},_getDefaultBlock:function(){return this._getHostValue("defaultblock")},_getDir:function(){return this._getHostValue("dir")},_getExtraCSS:function(){return this._getHostValue("extracss")},_getHTML:function(){var e,t;return this._ready&&(t=this.get(a),e=t.get(p)),e},_getHostValue:function(e){var t=this.get(h);if(t)return t.get(e)},_setHTML:function(t){if(this._ready){var n=this.get(a);n.set(p,t)}else this.once(i,e.bind(this._setHTML,this,t));return t},_setLinkedCSS:function(t){if(this._ready){var n=this.getInstance();n.Get.css(t)}else this.once(i,e.bind(this._setLinkedCSS,this,t));return t},_setDir:function(t){var n;return this._ready?(n=this.get(a),n.setAttribute("dir",t)):this.once(i,e.bind(this._setDir,this,t)),t},_setExtraCSS:function(t){if(this._ready){if(t){var n=this.getInstance(),s=n.one("head");this._extraCSSNode&&this._extraCSSNode.remove(),this._extraCSSNode=r.create("<style>"+t+"</style>"),s.append(this._extraCSSNode)}}else this.once(i,e.bind(this._setExtraCSS,this,t));return t},_setLang:function(t){var n;return this._ready?(n=this.get(a),n.setAttribute("lang",t)):this.once(i,e.bind(this._setLang,this,t)),t},_instanceLoaded:function(t){this._instance=t,this._onContentReady();var n=this._instance.config.doc;if(!e.UA.ie)try{n.execCommand("styleWithCSS",!1,!1),n.execCommand("insertbronreturn",!1,!1)}catch(r){}},_validateLinkedCSS:function(e){return n.isString(e)||n.isArray(e)},use:function(){var t=this.getInstance(),r=e.Array(arguments),i=!1;return n.isFunction(r[r.length-1])&&(i=r.pop()),i&&r.push(function(){i.apply(t,arguments)}),t.__use.apply(t,r)},delegate:function(e,t,n,r){var i=this.getInstance();return i?(r||(r=n,n=this.get(a)),i.delegate(e,t,n,r)):!1},getInstance:function(){return this._instance},render:function(t){var n,i,s;return this._rendered?this:(t&&this.set(a,t),container=this.get(a),container||(container=r.create(b.HTML),e.one("body").prepend(container),this.set(a,container)),this._rendered=!0,this._container.setAttribute(f,!0),n=e.clone(this.get(y)),s=e.bind(function(){i=YUI(),i.host=this.get(h),i.use("node-base",e.bind(this._instanceLoaded,this))},this),n.push(s),e.use.apply(e,n),this)},focus:function(){return this._container.focus(),this},show:function(){return this._container.show(),this.focus(),this},hide:function(){return this._container.hide(),this}},{THROTTLE_TIME:100,DOM_EVENTS:{click:1,dblclick:1,focusin:1,focusout:1,keydown:1,keypress:1,keyup:1,mousedown:1,mouseup:1,paste:1},HTML:"<div></div>",NAME:"contentEditable",NS:f,ATTRS:{dir:{lazyAdd:!1,validator:n.isString,setter:"_setDir",valueFn:"_getDir"},container:{setter:function(t){return this._container=e.one(t),this._container}},content:{getter:"_getHTML",lazyAdd:!1,setter:"_setHTML",validator:n.isString,value:"<br>"},defaultblock:{validator:n.isString,value:o,valueFn:"_getDefaultBlock"},extracss:{lazyAdd:!1,setter:"_setExtraCSS",validator:n.isString,valueFn:"_getExtraCSS"},id:{writeOnce:!0,getter:function(t){return t||(t="inlineedit-"+e.guid()),t}},lang:{validator:n.isString,setter:"_setLang",lazyAdd:!1,value:"en-US"},linkedcss:{setter:"_setLinkedCSS",validator:"_validateLinkedCSS"},node:{readOnly:!0,value:null,getter:function(){return this._container}},use:{validator:n.isArray,writeOnce:!0,value:["node-base","editor-selection","stylesheet"]}}}),e.namespace("Plugin"),e.Plugin.ContentEditable=b},"3.17.2",{requires:["node-base","editor-selection","stylesheet","plugin"]});
diff --git a/js/yui3/cookie/cookie-min.js b/js/yui3/cookie/cookie-min.js
new file mode 100644
index 000000000..2d7c2fbaf
--- /dev/null
+++ b/js/yui3/cookie/cookie-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("cookie",function(e,t){function h(e){throw new TypeError(e)}function p(e){(!s(e)||e==="")&&h("Cookie name must be a non-empty string.")}function d(e){(!s(e)||e==="")&&h("Subcookie name must be a non-empty string.")}var n=e.Lang,r=e.Object,i=null,s=n.isString,o=n.isObject,u=n.isUndefined,a=n.isFunction,f=encodeURIComponent,l=decodeURIComponent,c=e.config.doc;e.Cookie={_createCookieString:function(e,t,n,r){r=r||{};var i=f(e)+"="+(n?f(t):t),u=r.expires,a=r.path,l=r.domain;return o(r)&&(u instanceof Date&&(i+="; expires="+u.toUTCString()),s(a)&&a!==""&&(i+="; path="+a),s(l)&&l!==""&&(i+="; domain="+l),r.secure===!0&&(i+="; secure")),i},_createCookieHashString:function(e){o(e)||h("Cookie._createCookieHashString(): Argument must be an object.");var t=[];return r.each(e,function(e,n){!a(e)&&!u(e)&&t.push(f(n)+"="+f(String(e)))}),t.join("&")},_parseCookieHash:function(e){var t=e.split("&"),n=i,r={};if(e.length)for(var s=0,o=t.length;s<o;s++)n=t[s].split("="),r[l(n[0])]=l(n[1]);return r},_parseCookieString:function(e,t,n){var r={};if(s(e)&&e.length>0){var o=t===!1?function(e){return e}:l,a=e.split(/;\s/g),f=i,c=i,h=i;for(var p=0,d=a.length;p<d;p++){h=a[p].match(/([^=]+)=/i);if(h instanceof Array)try{f=l(h[1]),c=o(a[p].substring(h[1].length+1))}catch(v){}else f=l(a[p]),c="";!u(n)&&n.reverseCookieLoading?u(r[f])&&(r[f]=c):r[f]=c}}return r},_setDoc:function(e){c=e},exists:function(e){p(e);var t=this._parseCookieString(c.cookie,!0);return t.hasOwnProperty(e)},get:function(e,t){p(e);var n,r,s;return a(t)?(s=t,t={}):o(t)?s=t.converter:t={},n=this._parseCookieString(c.cookie,!t.raw,t),r=n[e],u(r)?i:a(s)?s(r):r},getSub:function(e,t,n,r){var s=this.getSubs(e,r);return s!==i?(d(t),u(s[t])?i:a(n)?n(s[t]):s[t]):i},getSubs:function(e,t){p(e);var n=this._parseCookieString(c.cookie,!1,t);return s(n[e])?this._parseCookieHash(n[e]):i},remove:function(t,n){return p(t),n=e.merge(n||{},{expires:new Date(0)}),this.set(t,"",n)},removeSub:function(e,t,n){p(e),d(t),n=n||{};var r=this.getSubs(e);if(o(r)&&r.hasOwnProperty(t)){delete r[t];if(!n.removeIfEmpty)return this.setSubs(e,r,n);for(var i in r)if(r.hasOwnProperty(i)&&!a(r[i])&&!u(r[i]))return this.setSubs(e,r,n);return this.remove(e,n)}return""},set:function(e,t,n){p(e),u(t)&&h("Cookie.set(): Value cannot be undefined."),n=n||{};var r=this._createCookieString(e,t,!n.raw,n);return c.cookie=r,r},setSub:function(e,t,n,r){p(e),d(t),u(n)&&h("Cookie.setSub(): Subcookie value cannot be undefined.");var i=this.getSubs(e);return o(i)||(i={}),i[t]=n,this.setSubs(e,i,r)},setSubs:function(e,t,n){p(e),o(t)||h("Cookie.setSubs(): Cookie value must be an object.");var r=this._createCookieString(e,this._createCookieHashString(t),!1,n);return c.cookie=r,r}}},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/createlink-base/createlink-base-min.js b/js/yui3/createlink-base/createlink-base-min.js
new file mode 100644
index 000000000..e28c91635
--- /dev/null
+++ b/js/yui3/createlink-base/createlink-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("createlink-base",function(e,t){var n={};n.STRINGS={PROMPT:"Please enter the URL for the link to point to:",DEFAULT:"http://"},e.namespace("Plugin"),e.Plugin.CreateLinkBase=n,e.mix(e.Plugin.ExecCommand.COMMANDS,{createlink:function(t){var r=this.get("host").getInstance(),i,s,o,u,a=prompt(n.STRINGS.PROMPT,n.STRINGS.DEFAULT);return a&&(u=r.config.doc.createElement("div"),a=a.replace(/"/g,"").replace(/'/g,""),a=r.config.doc.createTextNode(a),u.appendChild(a),a=u.innerHTML,this.get("host")._execCommand(t,a),o=new r.EditorSelection,i=o.getSelected(),!o.isCollapsed&&i.size()?(s=i.item(0).one("a"),s&&i.item(0).replace(s),e.UA.gecko&&s.get("parentNode").test("span")&&s.get("parentNode").one("br.yui-cursor")&&s.get("parentNode").insert(s,"before")):this.get("host").execCommand("inserthtml",'<a href="'+a+'">'+a+"</a>")),s}})},"3.17.2",{requires:["editor-base"]});
diff --git a/js/yui3/cssbase-context/cssbase-context-min.css b/js/yui3/cssbase-context/cssbase-context-min.css
new file mode 100644
index 000000000..828d780ef
--- /dev/null
+++ b/js/yui3/cssbase-context/cssbase-context-min.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-cssbase h1{font-size:138.5%}.yui3-cssbase h2{font-size:123.1%}.yui3-cssbase h3{font-size:108%}.yui3-cssbase h1,.yui3-cssbase h2,.yui3-cssbase h3{margin:1em 0}.yui3-cssbase h1,.yui3-cssbase h2,.yui3-cssbase h3,.yui3-cssbase h4,.yui3-cssbase h5,.yui3-cssbase h6,.yui3-cssbase strong{font-weight:bold}.yui3-cssbase abbr,.yui3-cssbase acronym{border-bottom:1px dotted #000;cursor:help}.yui3-cssbase em{font-style:italic}.yui3-cssbase blockquote,.yui3-cssbase ul,.yui3-cssbase ol,.yui3-cssbase dl{margin:1em}.yui3-cssbase ol,.yui3-cssbase ul,.yui3-cssbase dl{margin-left:2em}.yui3-cssbase ol{list-style:decimal outside}.yui3-cssbase ul{list-style:disc outside}.yui3-cssbase dl dd{margin-left:1em}.yui3-cssbase th,.yui3-cssbase td{border:1px solid #000;padding:.5em}.yui3-cssbase th{font-weight:bold;text-align:center}.yui3-cssbase caption{margin-bottom:.5em;text-align:center}.yui3-cssbase p,.yui3-cssbase fieldset,.yui3-cssbase table,.yui3-cssbase pre{margin-bottom:1em}.yui3-cssbase input[type=text],.yui3-cssbase input[type=password],.yui3-cssbase textarea{width:12.25em;*width:11.9em}#yui3-css-stamp.cssbase-context{display:none}
diff --git a/js/yui3/cssbase-context/cssbase-context.css b/js/yui3/cssbase-context/cssbase-context.css
new file mode 100644
index 000000000..c4e6b272f
--- /dev/null
+++ b/js/yui3/cssbase-context/cssbase-context.css
@@ -0,0 +1,83 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* base.css, part of YUI's CSS Foundation */
+.yui3-cssbase h1 {
+ /*18px via YUI Fonts CSS foundation*/
+ font-size:138.5%;
+}
+.yui3-cssbase h2 {
+ /*16px via YUI Fonts CSS foundation*/
+ font-size:123.1%;
+}
+.yui3-cssbase h3 {
+ /*14px via YUI Fonts CSS foundation*/
+ font-size:108%;
+}
+.yui3-cssbase h1,.yui3-cssbase h2,.yui3-cssbase h3 {
+ /* top & bottom margin based on font size */
+ margin:1em 0;
+}
+.yui3-cssbase h1,.yui3-cssbase h2,.yui3-cssbase h3,.yui3-cssbase h4,.yui3-cssbase h5,.yui3-cssbase h6,.yui3-cssbase strong {
+ /*bringing boldness back to headers and the strong element*/
+ font-weight:bold;
+}
+.yui3-cssbase abbr,.yui3-cssbase acronym {
+ /*indicating to users that more info is available */
+ border-bottom:1px dotted #000;
+ cursor:help;
+}
+.yui3-cssbase em {
+ /*bringing italics back to the em element*/
+ font-style:italic;
+}
+.yui3-cssbase blockquote,.yui3-cssbase ul,.yui3-cssbase ol,.yui3-cssbase dl {
+ /*giving blockquotes and lists room to breath*/
+ margin:1em;
+}
+.yui3-cssbase ol,.yui3-cssbase ul,.yui3-cssbase dl {
+ /*bringing lists on to the page with breathing room */
+ margin-left:2em;
+}
+.yui3-cssbase ol {
+ /*giving OL's LIs generated numbers*/
+ list-style: decimal outside;
+}
+.yui3-cssbase ul {
+ /*giving UL's LIs generated disc markers*/
+ list-style: disc outside;
+}
+.yui3-cssbase dl dd {
+ /*providing spacing for definition terms*/
+ margin-left:1em;
+}
+.yui3-cssbase th,.yui3-cssbase td {
+ /*borders and padding to make the table readable*/
+ border:1px solid #000;
+ padding:.5em;
+}
+.yui3-cssbase th {
+ /*distinguishing table headers from data cells*/
+ font-weight:bold;
+ text-align:center;
+}
+.yui3-cssbase caption {
+ /*coordinated margin to match cell's padding*/
+ margin-bottom:.5em;
+ /*centered so it doesn't blend in to other content*/
+ text-align:center;
+}
+.yui3-cssbase p,.yui3-cssbase fieldset,.yui3-cssbase table,.yui3-cssbase pre {
+ /*so things don't run into each other*/
+ margin-bottom:1em;
+}
+/* setting a consistent width, 160px;
+ control of type=file still not possible */
+.yui3-cssbase input[type=text],.yui3-cssbase input[type=password],.yui3-cssbase textarea{width:12.25em;*width:11.9em;}
+
+/* YUI CSS Detection Stamp */
+#yui3-css-stamp.cssbase-context { display: none; }
diff --git a/js/yui3/cssbase/cssbase-min.css b/js/yui3/cssbase/cssbase-min.css
new file mode 100644
index 000000000..3527844a0
--- /dev/null
+++ b/js/yui3/cssbase/cssbase-min.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+h1{font-size:138.5%}h2{font-size:123.1%}h3{font-size:108%}h1,h2,h3{margin:1em 0}h1,h2,h3,h4,h5,h6,strong{font-weight:bold}abbr,acronym{border-bottom:1px dotted #000;cursor:help}em{font-style:italic}blockquote,ul,ol,dl{margin:1em}ol,ul,dl{margin-left:2em}ol{list-style:decimal outside}ul{list-style:disc outside}dd{margin-left:1em}th,td{border:1px solid #000;padding:.5em}th{font-weight:bold;text-align:center}caption{margin-bottom:.5em;text-align:center}p,fieldset,table,pre{margin-bottom:1em}input[type=text],input[type=password],textarea{width:12.25em;*width:11.9em}#yui3-css-stamp.cssbase{display:none}
diff --git a/js/yui3/cssbase/cssbase.css b/js/yui3/cssbase/cssbase.css
new file mode 100644
index 000000000..1fe566acb
--- /dev/null
+++ b/js/yui3/cssbase/cssbase.css
@@ -0,0 +1,84 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* base.css, part of YUI's CSS Foundation */
+h1 {
+ /*18px via YUI Fonts CSS foundation*/
+ font-size:138.5%;
+}
+h2 {
+ /*16px via YUI Fonts CSS foundation*/
+ font-size:123.1%;
+}
+h3 {
+ /*14px via YUI Fonts CSS foundation*/
+ font-size:108%;
+}
+h1,h2,h3 {
+ /* top & bottom margin based on font size */
+ margin:1em 0;
+}
+h1,h2,h3,h4,h5,h6,strong {
+ /*bringing boldness back to headers and the strong element*/
+ font-weight:bold;
+}
+abbr,acronym {
+ /*indicating to users that more info is available */
+ border-bottom:1px dotted #000;
+ cursor:help;
+}
+em {
+ /*bringing italics back to the em element*/
+ font-style:italic;
+}
+blockquote,ul,ol,dl {
+ /*giving blockquotes and lists room to breath*/
+ margin:1em;
+}
+ol,ul,dl {
+ /*bringing lists on to the page with breathing room */
+ margin-left:2em;
+}
+ol {
+ /*giving OL's LIs generated numbers*/
+ list-style: decimal outside;
+}
+ul {
+ /*giving UL's LIs generated disc markers*/
+ list-style: disc outside;
+}
+dd {
+ /*providing spacing for definition terms*/
+ margin-left:1em;
+}
+th,td {
+ /*borders and padding to make the table readable*/
+ border:1px solid #000;
+ padding:.5em;
+}
+th {
+ /*distinguishing table headers from data cells*/
+ font-weight:bold;
+ text-align:center;
+}
+caption {
+ /*coordinated margin to match cell's padding*/
+ margin-bottom:.5em;
+ /*centered so it doesn't blend in to other content*/
+ text-align:center;
+}
+p,fieldset,table,pre {
+ /*so things don't run into each other*/
+ margin-bottom:1em;
+}
+/* setting a consistent width, 160px;
+ control of type=file still not possible
+ *width is for ie7 (no ie6 fallback) */
+input[type=text],input[type=password],textarea{width:12.25em;*width:11.9em;}
+
+/* YUI CSS Detection Stamp */
+#yui3-css-stamp.cssbase { display: none; }
diff --git a/js/yui3/cssbutton/cssbutton-min.css b/js/yui3/cssbutton/cssbutton-min.css
new file mode 100644
index 000000000..865ef25a2
--- /dev/null
+++ b/js/yui3/cssbutton/cssbutton-min.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-button{display:inline-block;*display:inline;zoom:1;font-size:100%;*font-size:90%;*overflow:visible;padding:.4em 1em .45em;line-height:normal;white-space:nowrap;vertical-align:baseline;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;color:#444;color:rgba(0,0,0,0.80);*color:#444;border:1px solid #999;border:none rgba(0,0,0,0);background-color:#e6e6e6;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80ffffff',endColorstr='#00ffffff',GradientType=0);background-image:-webkit-gradient(linear,0 0,0 100%,from(rgba(255,255,255,0.30)),color-stop(40%,rgba(255,255,255,0.15)),to(transparent));background-image:-webkit-linear-gradient(rgba(255,255,255,0.30),rgba(255,255,255,0.15) 40%,transparent);background-image:-moz-linear-gradient(top,rgba(255,255,255,0.30),rgba(255,255,255,0.15) 40%,transparent);background-image:-ms-linear-gradient(rgba(255,255,255,0.30),rgba(255,255,255,0.15) 40%,transparent);background-image:-o-linear-gradient(rgba(255,255,255,0.30),rgba(255,255,255,0.15) 40%,transparent);background-image:linear-gradient(rgba(255,255,255,0.30),rgba(255,255,255,0.15) 40%,transparent);text-decoration:none;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,0.25) inset,0 2px 0 rgba(255,255,255,0.30) inset,0 1px 2px rgba(0,0,0,0.15);-moz-box-shadow:0 0 0 1px rgba(0,0,0,0.25) inset,0 2px 0 rgba(255,255,255,0.30) inset,0 1px 2px rgba(0,0,0,0.15);box-shadow:0 0 0 1px rgba(0,0,0,0.25) inset,0 2px 0 rgba(255,255,255,0.30) inset,0 1px 2px rgba(0,0,0,0.15);-webkit-transition:.1s linear -webkit-box-shadow;-moz-transition:.1s linear -moz-box-shadow;-ms-transition:.1s linear box-shadow;-o-transition:.1s linear box-shadow;transition:.1s linear box-shadow}a.yui3-button{color:rgba(0,0,0,0.80);color:#444;text-decoration:none}.yui3-button-hover,.yui3-button:hover{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000',endColorstr='#26000000',GradientType=0);background-image:-webkit-gradient(linear,0 0,0 100%,from(transparent),color-stop(40%,rgba(0,0,0,0.05)),to(rgba(0,0,0,0.15)));background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,0.05) 40%,rgba(0,0,0,0.15));background-image:-moz-linear-gradient(top,transparent,rgba(0,0,0,0.05) 40%,rgba(0,0,0,0.15));background-image:-ms-linear-gradient(transparent,rgba(0,0,0,0.05) 40%,rgba(0,0,0,0.15));background-image:-o-linear-gradient(transparent,rgba(0,0,0,0.05) 40%,rgba(0,0,0,0.15));background-image:linear-gradient(transparent,rgba(0,0,0,0.05) 40%,rgba(0,0,0,0.15))}.yui3-button-active,.yui3-button:active{border:inset 1px solid #999;border:none rgba(0,0,0,0);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#1A000000',endColorstr='#26000000',GradientType=0);background-image:-webkit-gradient(linear,0 0,0 100%,from(rgba(0,0,0,0.10)),to(rgba(0,0,0,0.15)));background-image:-webkit-linear-gradient(rgba(0,0,0,0.10),rgba(0,0,0,0.15));background-image:-moz-linear-gradient(top,rgba(0,0,0,0.10),rgba(0,0,0,0.15));background-image:-ms-linear-gradient(rgba(0,0,0,0.10),rgba(0,0,0,0.15));background-image:-o-linear-gradient(rgba(0,0,0,0.10),rgba(0,0,0,0.15));background-image:linear-gradient(rgba(0,0,0,0.10),rgba(0,0,0,0.15));-webkit-box-shadow:0 0 0 1px rgba(0,0,0,0.25) inset,0 2px 4px rgba(0,0,0,0.30) inset;-moz-box-shadow:0 0 0 1px rgba(0,0,0,0.25) inset,0 2px 4px rgba(0,0,0,0.30) inset;box-shadow:0 0 0 1px rgba(0,0,0,0.25) inset,0 2px 4px rgba(0,0,0,0.30) inset}.yui3-button[disabled],.yui3-button-disabled,.yui3-button-disabled:hover,.yui3-button-disabled:active{cursor:default;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=55);-khtml-opacity:.55;-moz-opacity:.55;opacity:.55;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,0.25) inset;-moz-box-shadow:0 0 0 1px rgba(0,0,0,0.25) inset;box-shadow:0 0 0 1px rgba(0,0,0,0.25) inset}.yui3-button-hidden{display:none}.yui3-button::-moz-focus-inner{padding:0;border:0}.yui3-button:-moz-focusring{outline:thin dotted}.yui3-skin-sam .yui3-button-primary,.yui3-skin-sam .yui3-button-selected{background-color:#345fcb;color:#fff;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,0.25) inset,0 2px 0 rgba(255,255,255,0.17) inset,0 1px 2px rgba(0,0,0,0.15);-moz-box-shadow:0 0 0 1px rgba(0,0,0,0.25) inset,0 2px 0 rgba(255,255,255,0.17) inset,0 1px 2px rgba(0,0,0,0.15);box-shadow:0 0 0 1px rgba(0,0,0,0.25) inset,0 2px 0 rgba(255,255,255,0.17) inset,0 1px 2px rgba(0,0,0,0.15)}.yui3-skin-sam .yui3-button:-moz-focusring{outline-color:rgba(0,0,0,0.85)}.yui3-skin-night .yui3-button{border:0;background-color:#343536;color:#dcdcdc;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,0.25) inset,0 2px 0 rgba(255,255,255,0.15) inset,0 1px 2px rgba(0,0,0,0.15);-moz-box-shadow:0 0 0 1px rgba(0,0,0,0.25) inset,0 2px 0 rgba(255,255,255,0.15) inset,0 1px 2px rgba(0,0,0,0.15);box-shadow:0 0 0 1px rgba(0,0,0,0.25) inset,0 2px 0 rgba(255,255,255,0.15) inset,0 1px 2px rgba(0,0,0,0.15)}.yui3-skin-night .yui3-button-primary,.yui3-skin-night .yui3-button-selected{background-color:#747576;text-shadow:0 1px 2px rgba(0,0,0,0.7)}.yui3-skin-night .yui3-button:-moz-focusring{outline-color:rgba(255,255,255,0.85)}#yui3-css-stamp.cssbutton{display:none}
diff --git a/js/yui3/cssbutton/cssbutton.css b/js/yui3/cssbutton/cssbutton.css
new file mode 100644
index 000000000..d1f194788
--- /dev/null
+++ b/js/yui3/cssbutton/cssbutton.css
@@ -0,0 +1,154 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-button {
+ /* Structure */
+ display: inline-block;
+ *display: inline; /*IE 6/7*/
+ zoom: 1;
+ font-size: 100%;
+ *font-size: 90%; /*IE 6/7 - To reduce IE's oversized button text*/
+ *overflow: visible; /*IE 6/7 - Because of IE's overly large left/right padding on buttons */
+ padding: 0.4em 1em 0.45em;
+ line-height: normal;
+ white-space: nowrap;
+ vertical-align: baseline;
+ text-align: center;
+ cursor: pointer;
+ -webkit-user-drag: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+
+ /* Presentation */
+ color: #444; /* rgba not supported (IE 8) */
+ color: rgba(0, 0, 0, 0.80); /* rgba supported */
+ *color: #444; /* IE 6 & 7 */
+ border: 1px solid #999; /*IE 6/7/8*/
+ border: none rgba(0, 0, 0, 0); /*IE9 + everything else*/
+ background-color: #E6E6E6;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80ffffff', endColorstr='#00ffffff', GradientType=0);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(rgba(255,255,255, 0.30)), color-stop(40%, rgba(255,255,255, 0.15)), to(transparent));
+ background-image: -webkit-linear-gradient(rgba(255,255,255, 0.30), rgba(255,255,255, 0.15) 40%, transparent);
+ background-image: -moz-linear-gradient(top, rgba(255,255,255, 0.30), rgba(255,255,255, 0.15) 40%, transparent);
+ background-image: -ms-linear-gradient(rgba(255,255,255, 0.30), rgba(255,255,255, 0.15) 40%, transparent);
+ background-image: -o-linear-gradient(rgba(255,255,255, 0.30), rgba(255,255,255, 0.15) 40%, transparent);
+ background-image: linear-gradient(rgba(255,255,255, 0.30), rgba(255,255,255, 0.15) 40%, transparent);
+ text-decoration: none;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ -webkit-box-shadow: 0 0 0 1px rgba(0,0,0, 0.25) inset, 0 2px 0 rgba(255,255,255, 0.30) inset, 0 1px 2px rgba(0,0,0, 0.15);
+ -moz-box-shadow: 0 0 0 1px rgba(0,0,0, 0.25) inset, 0 2px 0 rgba(255,255,255, 0.30) inset, 0 1px 2px rgba(0,0,0, 0.15);
+ box-shadow: 0 0 0 1px rgba(0,0,0, 0.25) inset, 0 2px 0 rgba(255,255,255, 0.30) inset, 0 1px 2px rgba(0,0,0, 0.15);
+
+ /* Transitions */
+ -webkit-transition: 0.1s linear -webkit-box-shadow;
+ -moz-transition: 0.1s linear -moz-box-shadow;
+ -ms-transition: 0.1s linear box-shadow;
+ -o-transition: 0.1s linear box-shadow;
+ transition: 0.1s linear box-shadow;
+}
+
+a.yui3-button {
+ color: rgba(0,0,0, 0.80);
+ color: #444;
+ text-decoration:none;
+}
+
+.yui3-button-hover,
+.yui3-button:hover {
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#26000000', GradientType=0);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(transparent), color-stop(40%, rgba(0,0,0, 0.05)), to(rgba(0,0,0, 0.15)));
+ background-image: -webkit-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.15));
+ background-image: -moz-linear-gradient(top, transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.15));
+ background-image: -ms-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.15));
+ background-image: -o-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.15));
+ background-image: linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.15));
+}
+
+.yui3-button-active,
+.yui3-button:active {
+ border: inset 1px solid #999; /*IE 6/7/8*/
+ border: none rgba(0, 0, 0, 0); /*IE9 + everything else*/
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#1A000000', endColorstr='#26000000', GradientType=0);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(rgba(0,0,0, 0.10)), to(rgba(0,0,0, 0.15)));
+ background-image: -webkit-linear-gradient(rgba(0,0,0, 0.10), rgba(0,0,0, 0.15));
+ background-image: -moz-linear-gradient(top, rgba(0,0,0, 0.10), rgba(0,0,0, 0.15));
+ background-image: -ms-linear-gradient(rgba(0,0,0, 0.10), rgba(0,0,0, 0.15));
+ background-image: -o-linear-gradient(rgba(0,0,0, 0.10), rgba(0,0,0, 0.15));
+ background-image: linear-gradient(rgba(0,0,0, 0.10), rgba(0,0,0, 0.15));
+ -webkit-box-shadow: 0 0 0 1px rgba(0,0,0, 0.25) inset, 0 2px 4px rgba(0,0,0, 0.30) inset;
+ -moz-box-shadow: 0 0 0 1px rgba(0,0,0, 0.25) inset, 0 2px 4px rgba(0,0,0, 0.30) inset;
+ box-shadow: 0 0 0 1px rgba(0,0,0, 0.25) inset, 0 2px 4px rgba(0,0,0, 0.30) inset;
+}
+
+.yui3-button[disabled],
+.yui3-button-disabled,
+.yui3-button-disabled:hover,
+.yui3-button-disabled:active {
+ cursor: default;
+ background-image: none;
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+ filter: alpha(opacity=55);
+ -khtml-opacity: 0.55;
+ -moz-opacity: 0.55;
+ opacity: 0.55;
+ -webkit-box-shadow: 0 0 0 1px rgba(0,0,0, 0.25) inset;
+ -moz-box-shadow: 0 0 0 1px rgba(0,0,0, 0.25) inset;
+ box-shadow: 0 0 0 1px rgba(0,0,0, 0.25) inset;
+}
+
+.yui3-button-hidden {
+ display:none;
+}
+
+/* Firefox: Get rid of the inner focus border */
+.yui3-button::-moz-focus-inner{
+ padding: 0;
+ border: 0;
+}
+
+/* Firefox: Add a border around a focused button */
+.yui3-button:-moz-focusring {
+ outline: thin dotted;
+}
+
+/* Sam */
+.yui3-skin-sam .yui3-button-primary,
+.yui3-skin-sam .yui3-button-selected {
+ background-color: #345FCB;
+ color: #fff;
+ -webkit-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25) inset, 0 2px 0 rgba(255, 255, 255, 0.17) inset, 0 1px 2px rgba(0, 0, 0, 0.15);
+ -moz-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25) inset, 0 2px 0 rgba(255, 255, 255, 0.17) inset, 0 1px 2px rgba(0, 0, 0, 0.15);
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25) inset, 0 2px 0 rgba(255, 255, 255, 0.17) inset, 0 1px 2px rgba(0, 0, 0, 0.15);
+}
+.yui3-skin-sam .yui3-button:-moz-focusring {
+ outline-color: rgba(0, 0, 0, 0.85);
+}
+
+/* Night */
+.yui3-skin-night .yui3-button {
+ border: 0px;
+ background-color: #343536;
+ color: #DCDCDC;
+ -webkit-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25) inset, 0 2px 0 rgba(255, 255, 255, 0.15) inset, 0 1px 2px rgba(0, 0, 0, 0.15);
+ -moz-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25) inset, 0 2px 0 rgba(255, 255, 255, 0.15) inset, 0 1px 2px rgba(0, 0, 0, 0.15);
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25) inset, 0 2px 0 rgba(255, 255, 255, 0.15) inset, 0 1px 2px rgba(0, 0, 0, 0.15);
+}
+.yui3-skin-night .yui3-button-primary,
+.yui3-skin-night .yui3-button-selected {
+ background-color: #747576;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
+}
+
+.yui3-skin-night .yui3-button:-moz-focusring {
+ outline-color: rgba(255, 255, 255, 0.85);
+}
+
+/* YUI CSS Detection Stamp */
+#yui3-css-stamp.cssbutton { display: none; }
diff --git a/js/yui3/cssfonts-context/cssfonts-context-min.css b/js/yui3/cssfonts-context/cssfonts-context-min.css
new file mode 100644
index 000000000..37b5f74d7
--- /dev/null
+++ b/js/yui3/cssfonts-context/cssfonts-context-min.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-cssfonts body,.yui3-cssfonts{font:13px/1.231 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small}.yui3-cssfonts select,.yui3-cssfonts input,.yui3-cssfonts button,.yui3-cssfonts textarea{font:99% arial,helvetica,clean,sans-serif}.yui3-cssfonts table{font-size:inherit;font:100%}.yui3-cssfonts pre,.yui3-cssfonts code,.yui3-cssfonts kbd,.yui3-cssfonts samp,.yui3-cssfonts tt{font-family:monospace;*font-size:108%;line-height:100%}#yui3-css-stamp.cssfonts-context{display:none}
diff --git a/js/yui3/cssfonts-context/cssfonts-context.css b/js/yui3/cssfonts-context/cssfonts-context.css
new file mode 100644
index 000000000..30475574b
--- /dev/null
+++ b/js/yui3/cssfonts-context/cssfonts-context.css
@@ -0,0 +1,49 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/**
+ * Percents could work for IE, but for backCompat purposes, we are using keywords.
+ * x-small is for IE6/7 quirks mode.
+ */
+.yui3-cssfonts body, .yui3-cssfonts {
+ font:13px/1.231 arial,helvetica,clean,sans-serif;
+ *font-size:small; /* for IE */
+ *font:x-small; /* for IE in quirks mode */
+}
+
+/**
+ * Nudge down to get to 13px equivalent for these form elements
+ */
+.yui3-cssfonts select,
+.yui3-cssfonts input,
+.yui3-cssfonts button,
+.yui3-cssfonts textarea {
+ font:99% arial,helvetica,clean,sans-serif;
+}
+
+/**
+ * To help tables remember to inherit
+ */
+.yui3-cssfonts table {
+ font-size:inherit;
+ font:100%;
+}
+
+/**
+ * Bump up IE to get to 13px equivalent for these fixed-width elements
+ */
+.yui3-cssfonts pre,
+.yui3-cssfonts code,
+.yui3-cssfonts kbd,
+.yui3-cssfonts samp,
+.yui3-cssfonts tt {
+ font-family:monospace;
+ *font-size:108%;
+ line-height:100%;
+}
+/* YUI CSS Detection Stamp */
+#yui3-css-stamp.cssfonts-context { display: none; }
diff --git a/js/yui3/cssfonts/cssfonts-min.css b/js/yui3/cssfonts/cssfonts-min.css
new file mode 100644
index 000000000..6ffebc7b8
--- /dev/null
+++ b/js/yui3/cssfonts/cssfonts-min.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+body{font:13px/1.231 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small}select,input,button,textarea{font:99% arial,helvetica,clean,sans-serif}table{font-size:inherit;font:100%}pre,code,kbd,samp,tt{font-family:monospace;*font-size:108%;line-height:100%}#yui3-css-stamp.cssfonts{display:none}
diff --git a/js/yui3/cssfonts/cssfonts.css b/js/yui3/cssfonts/cssfonts.css
new file mode 100644
index 000000000..aa92b42da
--- /dev/null
+++ b/js/yui3/cssfonts/cssfonts.css
@@ -0,0 +1,49 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/**
+ * Percents could work for IE, but for backCompat purposes, we are using keywords.
+ * x-small is for IE6/7 quirks mode.
+ */
+body {
+ font:13px/1.231 arial,helvetica,clean,sans-serif;
+ *font-size:small; /* for IE */
+ *font:x-small; /* for IE in quirks mode */
+}
+
+/**
+ * Nudge down to get to 13px equivalent for these form elements
+ */
+select,
+input,
+button,
+textarea {
+ font:99% arial,helvetica,clean,sans-serif;
+}
+
+/**
+ * To help tables remember to inherit
+ */
+table {
+ font-size:inherit;
+ font:100%;
+}
+
+/**
+ * Bump up IE to get to 13px equivalent for these fixed-width elements
+ */
+pre,
+code,
+kbd,
+samp,
+tt {
+ font-family:monospace;
+ *font-size:108%;
+ line-height:100%;
+}
+/* YUI CSS Detection Stamp */
+#yui3-css-stamp.cssfonts { display: none; }
diff --git a/js/yui3/cssgrids-base/cssgrids-base-min.css b/js/yui3/cssgrids-base/cssgrids-base-min.css
new file mode 100644
index 000000000..a62a72467
--- /dev/null
+++ b/js/yui3/cssgrids-base/cssgrids-base-min.css
@@ -0,0 +1,13 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/*!
+Pure v0.4.2
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+https://github.com/yui/pure/blob/master/LICENSE.md
+*/.yui3-g{letter-spacing:-0.31em;*letter-spacing:normal;*word-spacing:-0.43em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap}.opera-only :-o-prefocus,.yui3-g{word-spacing:-0.43em}.yui3-u{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.yui3-g [class *= "yui3-u"]{font-family:sans-serif}#yui3-css-stamp.cssgrids-base{display:none}
diff --git a/js/yui3/cssgrids-base/cssgrids-base.css b/js/yui3/cssgrids-base/cssgrids-base.css
new file mode 100644
index 000000000..4634cdb49
--- /dev/null
+++ b/js/yui3/cssgrids-base/cssgrids-base.css
@@ -0,0 +1,86 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/*!
+Pure v0.4.2
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+https://github.com/yui/pure/blob/master/LICENSE.md
+*/
+
+/*csslint regex-selectors:false, known-properties:false, duplicate-properties:false*/
+
+.yui3-g {
+ letter-spacing: -0.31em;
+ /* Webkit: collapse white-space between units */
+ *letter-spacing: normal;
+ /* reset IE < 8 */
+ *word-spacing: -0.43em;
+ /* IE < 8: collapse white-space between units */
+ text-rendering: optimizespeed;
+ /* Webkit: fixes text-rendering: optimizeLegibility */
+ /*
+ Sets the font stack to fonts known to work properly with the above letter
+ and word spacings. See: https://github.com/yui/pure/issues/41/
+
+ The following font stack makes Pure Grids work on all known environments.
+
+ * FreeSans: Ships with many Linux distros, including Ubuntu
+
+ * Arimo: Ships with Chrome OS. Arimo has to be defined before Helvetica and
+ Arial to get picked up by the browser, even though neither is available
+ in Chrome OS.
+
+ * Droid Sans: Ships with all versions of Android.
+
+ * Helvetica, Arial, sans-serif: Common font stack on OS X and Windows.
+ */
+ font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif;
+ /*
+ Use flexbox when possible to avoid `letter-spacing` side-effects.
+
+ NOTE: Firefox (as of 25) does not currently support flex-wrap, so the
+ `-moz-` prefix version is omitted.
+ */
+ display: -webkit-flex;
+ -webkit-flex-flow: row wrap;
+ /* IE10 uses display: flexbox */
+ display: -ms-flexbox;
+ -ms-flex-flow: row wrap;
+}
+
+/* Opera as of 12 on Windows needs word-spacing.
+ The ".opera-only" selector is used to prevent actual prefocus styling
+ and is not required in markup.
+*/
+
+.opera-only :-o-prefocus,
+.yui3-g {
+ word-spacing: -0.43em;
+}
+
+.yui3-u {
+ display: inline-block;
+ *display: inline;
+ /* IE < 8: fake inline-block */
+ zoom: 1;
+ letter-spacing: normal;
+ word-spacing: normal;
+ vertical-align: top;
+ text-rendering: auto;
+}
+
+/*
+Resets the font family back to the OS/browser's default sans-serif font,
+this the same font stack that Normalize.css sets for the `body`.
+*/
+
+.yui3-g [class *= "yui3-u"] {
+ font-family: sans-serif;
+}
+/* YUI CSS Detection Stamp */
+#yui3-css-stamp.cssgrids-base { display: none; }
diff --git a/js/yui3/cssgrids-responsive/cssgrids-responsive-min.css b/js/yui3/cssgrids-responsive/cssgrids-responsive-min.css
new file mode 100644
index 000000000..be3cb98d4
--- /dev/null
+++ b/js/yui3/cssgrids-responsive/cssgrids-responsive-min.css
@@ -0,0 +1,13 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/*!
+Pure v0.4.2
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+https://github.com/yui/pure/blob/master/LICENSE.md
+*/.yui3-g{letter-spacing:-0.31em;*letter-spacing:normal;*word-spacing:-0.43em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap}.opera-only :-o-prefocus,.yui3-g{word-spacing:-0.43em}.yui3-u{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.yui3-g [class *= "yui3-u"]{font-family:sans-serif}.yui3-u-1,.yui3-u-1-1,.yui3-u-1-2,.yui3-u-1-3,.yui3-u-2-3,.yui3-u-1-4,.yui3-u-3-4,.yui3-u-1-5,.yui3-u-2-5,.yui3-u-3-5,.yui3-u-4-5,.yui3-u-5-5,.yui3-u-1-6,.yui3-u-5-6,.yui3-u-1-8,.yui3-u-3-8,.yui3-u-5-8,.yui3-u-7-8,.yui3-u-1-12,.yui3-u-5-12,.yui3-u-7-12,.yui3-u-11-12,.yui3-u-1-24,.yui3-u-2-24,.yui3-u-3-24,.yui3-u-4-24,.yui3-u-5-24,.yui3-u-6-24,.yui3-u-7-24,.yui3-u-8-24,.yui3-u-9-24,.yui3-u-10-24,.yui3-u-11-24,.yui3-u-12-24,.yui3-u-13-24,.yui3-u-14-24,.yui3-u-15-24,.yui3-u-16-24,.yui3-u-17-24,.yui3-u-18-24,.yui3-u-19-24,.yui3-u-20-24,.yui3-u-21-24,.yui3-u-22-24,.yui3-u-23-24,.yui3-u-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.yui3-u-1-24{width:4.1667%;*width:4.1357%}.yui3-u-1-12,.yui3-u-2-24{width:8.3333%;*width:8.3023%}.yui3-u-1-8,.yui3-u-3-24{width:12.5000%;*width:12.4690%}.yui3-u-1-6,.yui3-u-4-24{width:16.6667%;*width:16.6357%}.yui3-u-1-5{width:20%;*width:19.9690%}.yui3-u-5-24{width:20.8333%;*width:20.8023%}.yui3-u-1-4,.yui3-u-6-24{width:25%;*width:24.9690%}.yui3-u-7-24{width:29.1667%;*width:29.1357%}.yui3-u-1-3,.yui3-u-8-24{width:33.3333%;*width:33.3023%}.yui3-u-3-8,.yui3-u-9-24{width:37.5000%;*width:37.4690%}.yui3-u-2-5{width:40%;*width:39.9690%}.yui3-u-5-12,.yui3-u-10-24{width:41.6667%;*width:41.6357%}.yui3-u-11-24{width:45.8333%;*width:45.8023%}.yui3-u-1-2,.yui3-u-12-24{width:50%;*width:49.9690%}.yui3-u-13-24{width:54.1667%;*width:54.1357%}.yui3-u-7-12,.yui3-u-14-24{width:58.3333%;*width:58.3023%}.yui3-u-3-5{width:60%;*width:59.9690%}.yui3-u-5-8,.yui3-u-15-24{width:62.5000%;*width:62.4690%}.yui3-u-2-3,.yui3-u-16-24{width:66.6667%;*width:66.6357%}.yui3-u-17-24{width:70.8333%;*width:70.8023%}.yui3-u-3-4,.yui3-u-18-24{width:75%;*width:74.9690%}.yui3-u-19-24{width:79.1667%;*width:79.1357%}.yui3-u-4-5{width:80%;*width:79.9690%}.yui3-u-5-6,.yui3-u-20-24{width:83.3333%;*width:83.3023%}.yui3-u-7-8,.yui3-u-21-24{width:87.5000%;*width:87.4690%}.yui3-u-11-12,.yui3-u-22-24{width:91.6667%;*width:91.6357%}.yui3-u-23-24{width:95.8333%;*width:95.8023%}.yui3-u-1,.yui3-u-1-1,.yui3-u-5-5,.yui3-u-24-24{width:100%}.yui3-g-r{letter-spacing:-0.31em;*letter-spacing:normal;*word-spacing:-0.43em;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap}.opera-only :-o-prefocus,.yui3-g-r{word-spacing:-0.43em}.yui3-g-r [class *= "yui3-u"]{font-family:sans-serif}.yui3-g-r img{max-width:100%;height:auto}@media(min-width:980px){.yui3-visible-phone{display:none}.yui3-visible-tablet{display:none}.yui3-hidden-desktop{display:none}}@media(max-width:480px){.yui3-g-r>.yui3-u,.yui3-g-r>[class *= "yui3-u-"]{width:100%}}@media(max-width:767px){.yui3-g-r>.yui3-u,.yui3-g-r>[class *= "yui3-u-"]{width:100%}.yui3-hidden-phone{display:none}.yui3-visible-desktop{display:none}}@media(min-width:768px) and (max-width:979px){.yui3-hidden-tablet{display:none}.yui3-visible-desktop{display:none}}#yui3-css-stamp.cssgrids-responsive{display:none}
diff --git a/js/yui3/cssgrids-responsive/cssgrids-responsive.css b/js/yui3/cssgrids-responsive/cssgrids-responsive.css
new file mode 100644
index 000000000..e3b7501fb
--- /dev/null
+++ b/js/yui3/cssgrids-responsive/cssgrids-responsive.css
@@ -0,0 +1,404 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/*!
+Pure v0.4.2
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+https://github.com/yui/pure/blob/master/LICENSE.md
+*/
+
+/*csslint regex-selectors:false, known-properties:false, duplicate-properties:false*/
+
+.yui3-g {
+ letter-spacing: -0.31em;
+ /* Webkit: collapse white-space between units */
+ *letter-spacing: normal;
+ /* reset IE < 8 */
+ *word-spacing: -0.43em;
+ /* IE < 8: collapse white-space between units */
+ text-rendering: optimizespeed;
+ /* Webkit: fixes text-rendering: optimizeLegibility */
+ /*
+ Sets the font stack to fonts known to work properly with the above letter
+ and word spacings. See: https://github.com/yui/pure/issues/41/
+
+ The following font stack makes Pure Grids work on all known environments.
+
+ * FreeSans: Ships with many Linux distros, including Ubuntu
+
+ * Arimo: Ships with Chrome OS. Arimo has to be defined before Helvetica and
+ Arial to get picked up by the browser, even though neither is available
+ in Chrome OS.
+
+ * Droid Sans: Ships with all versions of Android.
+
+ * Helvetica, Arial, sans-serif: Common font stack on OS X and Windows.
+ */
+ font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif;
+ /*
+ Use flexbox when possible to avoid `letter-spacing` side-effects.
+
+ NOTE: Firefox (as of 25) does not currently support flex-wrap, so the
+ `-moz-` prefix version is omitted.
+ */
+ display: -webkit-flex;
+ -webkit-flex-flow: row wrap;
+ /* IE10 uses display: flexbox */
+ display: -ms-flexbox;
+ -ms-flex-flow: row wrap;
+}
+
+/* Opera as of 12 on Windows needs word-spacing.
+ The ".opera-only" selector is used to prevent actual prefocus styling
+ and is not required in markup.
+*/
+
+.opera-only :-o-prefocus,
+.yui3-g {
+ word-spacing: -0.43em;
+}
+
+.yui3-u {
+ display: inline-block;
+ *display: inline;
+ /* IE < 8: fake inline-block */
+ zoom: 1;
+ letter-spacing: normal;
+ word-spacing: normal;
+ vertical-align: top;
+ text-rendering: auto;
+}
+
+/*
+Resets the font family back to the OS/browser's default sans-serif font,
+this the same font stack that Normalize.css sets for the `body`.
+*/
+
+.yui3-g [class *= "yui3-u"] {
+ font-family: sans-serif;
+}
+
+.yui3-u-1,
+.yui3-u-1-1,
+.yui3-u-1-2,
+.yui3-u-1-3,
+.yui3-u-2-3,
+.yui3-u-1-4,
+.yui3-u-3-4,
+.yui3-u-1-5,
+.yui3-u-2-5,
+.yui3-u-3-5,
+.yui3-u-4-5,
+.yui3-u-5-5,
+.yui3-u-1-6,
+.yui3-u-5-6,
+.yui3-u-1-8,
+.yui3-u-3-8,
+.yui3-u-5-8,
+.yui3-u-7-8,
+.yui3-u-1-12,
+.yui3-u-5-12,
+.yui3-u-7-12,
+.yui3-u-11-12,
+.yui3-u-1-24,
+.yui3-u-2-24,
+.yui3-u-3-24,
+.yui3-u-4-24,
+.yui3-u-5-24,
+.yui3-u-6-24,
+.yui3-u-7-24,
+.yui3-u-8-24,
+.yui3-u-9-24,
+.yui3-u-10-24,
+.yui3-u-11-24,
+.yui3-u-12-24,
+.yui3-u-13-24,
+.yui3-u-14-24,
+.yui3-u-15-24,
+.yui3-u-16-24,
+.yui3-u-17-24,
+.yui3-u-18-24,
+.yui3-u-19-24,
+.yui3-u-20-24,
+.yui3-u-21-24,
+.yui3-u-22-24,
+.yui3-u-23-24,
+.yui3-u-24-24 {
+ display: inline-block;
+ *display: inline;
+ zoom: 1;
+ letter-spacing: normal;
+ word-spacing: normal;
+ vertical-align: top;
+ text-rendering: auto;
+}
+
+.yui3-u-1-24 {
+ width: 4.1667%;
+ *width: 4.1357%;
+}
+
+.yui3-u-1-12,
+.yui3-u-2-24 {
+ width: 8.3333%;
+ *width: 8.3023%;
+}
+
+.yui3-u-1-8,
+.yui3-u-3-24 {
+ width: 12.5000%;
+ *width: 12.4690%;
+}
+
+.yui3-u-1-6,
+.yui3-u-4-24 {
+ width: 16.6667%;
+ *width: 16.6357%;
+}
+
+.yui3-u-1-5 {
+ width: 20%;
+ *width: 19.9690%;
+}
+
+.yui3-u-5-24 {
+ width: 20.8333%;
+ *width: 20.8023%;
+}
+
+.yui3-u-1-4,
+.yui3-u-6-24 {
+ width: 25%;
+ *width: 24.9690%;
+}
+
+.yui3-u-7-24 {
+ width: 29.1667%;
+ *width: 29.1357%;
+}
+
+.yui3-u-1-3,
+.yui3-u-8-24 {
+ width: 33.3333%;
+ *width: 33.3023%;
+}
+
+.yui3-u-3-8,
+.yui3-u-9-24 {
+ width: 37.5000%;
+ *width: 37.4690%;
+}
+
+.yui3-u-2-5 {
+ width: 40%;
+ *width: 39.9690%;
+}
+
+.yui3-u-5-12,
+.yui3-u-10-24 {
+ width: 41.6667%;
+ *width: 41.6357%;
+}
+
+.yui3-u-11-24 {
+ width: 45.8333%;
+ *width: 45.8023%;
+}
+
+.yui3-u-1-2,
+.yui3-u-12-24 {
+ width: 50%;
+ *width: 49.9690%;
+}
+
+.yui3-u-13-24 {
+ width: 54.1667%;
+ *width: 54.1357%;
+}
+
+.yui3-u-7-12,
+.yui3-u-14-24 {
+ width: 58.3333%;
+ *width: 58.3023%;
+}
+
+.yui3-u-3-5 {
+ width: 60%;
+ *width: 59.9690%;
+}
+
+.yui3-u-5-8,
+.yui3-u-15-24 {
+ width: 62.5000%;
+ *width: 62.4690%;
+}
+
+.yui3-u-2-3,
+.yui3-u-16-24 {
+ width: 66.6667%;
+ *width: 66.6357%;
+}
+
+.yui3-u-17-24 {
+ width: 70.8333%;
+ *width: 70.8023%;
+}
+
+.yui3-u-3-4,
+.yui3-u-18-24 {
+ width: 75%;
+ *width: 74.9690%;
+}
+
+.yui3-u-19-24 {
+ width: 79.1667%;
+ *width: 79.1357%;
+}
+
+.yui3-u-4-5 {
+ width: 80%;
+ *width: 79.9690%;
+}
+
+.yui3-u-5-6,
+.yui3-u-20-24 {
+ width: 83.3333%;
+ *width: 83.3023%;
+}
+
+.yui3-u-7-8,
+.yui3-u-21-24 {
+ width: 87.5000%;
+ *width: 87.4690%;
+}
+
+.yui3-u-11-12,
+.yui3-u-22-24 {
+ width: 91.6667%;
+ *width: 91.6357%;
+}
+
+.yui3-u-23-24 {
+ width: 95.8333%;
+ *width: 95.8023%;
+}
+
+.yui3-u-1,
+.yui3-u-1-1,
+.yui3-u-5-5,
+.yui3-u-24-24 {
+ width: 100%;
+}
+
+/*csslint regex-selectors:false, known-properties:false, duplicate-properties:false*/
+
+.yui3-g-r {
+ letter-spacing: -0.31em;
+ *letter-spacing: normal;
+ *word-spacing: -0.43em;
+ /*
+ Sets the font stack to fonts known to work properly with the above letter
+ and word spacings. See: https://github.com/yui/pure/issues/41/
+
+ The following font stack makes Pure Grids work on all known environments.
+
+ * FreeSans: Ships with many Linux distros, including Ubuntu
+
+ * Arimo: Ships with Chrome OS. Arimo has to be defined before Helvetica and
+ Arial to get picked up by the browser, even though neither is available
+ in Chrome OS.
+
+ * Droid Sans: Ships with all versions of Android.
+
+ * Helvetica, Arial, sans-serif: Common font stack on OS X and Windows.
+ */
+ font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif;
+ /*
+ Use flexbox when possible to avoid `letter-spacing` side-effects.
+
+ NOTE: Firefox (as of 25) does not currently support flex-wrap, so the
+ `-moz-` prefix version is omitted.
+ */
+ display: -webkit-flex;
+ -webkit-flex-flow: row wrap;
+ /* IE10 uses display: flexbox */
+ display: -ms-flexbox;
+ -ms-flex-flow: row wrap;
+}
+
+/* Opera as of 12 on Windows needs word-spacing.
+ The ".opera-only" selector is used to prevent actual prefocus styling
+ and is not required in markup.
+*/
+
+.opera-only :-o-prefocus,
+.yui3-g-r {
+ word-spacing: -0.43em;
+}
+
+/*
+Resets the font family back to the OS/browser's default sans-serif font,
+this the same font stack that Normalize.css sets for the `body`.
+*/
+
+.yui3-g-r [class *= "yui3-u"] {
+ font-family: sans-serif;
+}
+
+.yui3-g-r img {
+ max-width: 100%;
+ height: auto;
+}
+
+@media (min-width: 980px) {
+ .yui3-visible-phone {
+ display: none;
+ }
+
+ .yui3-visible-tablet {
+ display: none;
+ }
+
+ .yui3-hidden-desktop {
+ display: none;
+ }
+}
+
+@media (max-width: 480px) {
+ .yui3-g-r > .yui3-u,
+ .yui3-g-r > [class *= "yui3-u-"] {
+ width: 100%;
+ }
+}
+
+@media (max-width: 767px) {
+ .yui3-g-r > .yui3-u,
+ .yui3-g-r > [class *= "yui3-u-"] {
+ width: 100%;
+ }
+
+ .yui3-hidden-phone {
+ display: none;
+ }
+
+ .yui3-visible-desktop {
+ display: none;
+ }
+}
+
+@media (min-width: 768px) and (max-width: 979px) {
+ .yui3-hidden-tablet {
+ display: none;
+ }
+
+ .yui3-visible-desktop {
+ display: none;
+ }
+}
+/* YUI CSS Detection Stamp */
+#yui3-css-stamp.cssgrids-responsive { display: none; }
diff --git a/js/yui3/cssgrids-units/cssgrids-units-min.css b/js/yui3/cssgrids-units/cssgrids-units-min.css
new file mode 100644
index 000000000..7adfd3366
--- /dev/null
+++ b/js/yui3/cssgrids-units/cssgrids-units-min.css
@@ -0,0 +1,13 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/*!
+Pure v0.4.2
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+https://github.com/yui/pure/blob/master/LICENSE.md
+*/.yui3-u-1,.yui3-u-1-1,.yui3-u-1-2,.yui3-u-1-3,.yui3-u-2-3,.yui3-u-1-4,.yui3-u-3-4,.yui3-u-1-5,.yui3-u-2-5,.yui3-u-3-5,.yui3-u-4-5,.yui3-u-5-5,.yui3-u-1-6,.yui3-u-5-6,.yui3-u-1-8,.yui3-u-3-8,.yui3-u-5-8,.yui3-u-7-8,.yui3-u-1-12,.yui3-u-5-12,.yui3-u-7-12,.yui3-u-11-12,.yui3-u-1-24,.yui3-u-2-24,.yui3-u-3-24,.yui3-u-4-24,.yui3-u-5-24,.yui3-u-6-24,.yui3-u-7-24,.yui3-u-8-24,.yui3-u-9-24,.yui3-u-10-24,.yui3-u-11-24,.yui3-u-12-24,.yui3-u-13-24,.yui3-u-14-24,.yui3-u-15-24,.yui3-u-16-24,.yui3-u-17-24,.yui3-u-18-24,.yui3-u-19-24,.yui3-u-20-24,.yui3-u-21-24,.yui3-u-22-24,.yui3-u-23-24,.yui3-u-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.yui3-u-1-24{width:4.1667%;*width:4.1357%}.yui3-u-1-12,.yui3-u-2-24{width:8.3333%;*width:8.3023%}.yui3-u-1-8,.yui3-u-3-24{width:12.5000%;*width:12.4690%}.yui3-u-1-6,.yui3-u-4-24{width:16.6667%;*width:16.6357%}.yui3-u-1-5{width:20%;*width:19.9690%}.yui3-u-5-24{width:20.8333%;*width:20.8023%}.yui3-u-1-4,.yui3-u-6-24{width:25%;*width:24.9690%}.yui3-u-7-24{width:29.1667%;*width:29.1357%}.yui3-u-1-3,.yui3-u-8-24{width:33.3333%;*width:33.3023%}.yui3-u-3-8,.yui3-u-9-24{width:37.5000%;*width:37.4690%}.yui3-u-2-5{width:40%;*width:39.9690%}.yui3-u-5-12,.yui3-u-10-24{width:41.6667%;*width:41.6357%}.yui3-u-11-24{width:45.8333%;*width:45.8023%}.yui3-u-1-2,.yui3-u-12-24{width:50%;*width:49.9690%}.yui3-u-13-24{width:54.1667%;*width:54.1357%}.yui3-u-7-12,.yui3-u-14-24{width:58.3333%;*width:58.3023%}.yui3-u-3-5{width:60%;*width:59.9690%}.yui3-u-5-8,.yui3-u-15-24{width:62.5000%;*width:62.4690%}.yui3-u-2-3,.yui3-u-16-24{width:66.6667%;*width:66.6357%}.yui3-u-17-24{width:70.8333%;*width:70.8023%}.yui3-u-3-4,.yui3-u-18-24{width:75%;*width:74.9690%}.yui3-u-19-24{width:79.1667%;*width:79.1357%}.yui3-u-4-5{width:80%;*width:79.9690%}.yui3-u-5-6,.yui3-u-20-24{width:83.3333%;*width:83.3023%}.yui3-u-7-8,.yui3-u-21-24{width:87.5000%;*width:87.4690%}.yui3-u-11-12,.yui3-u-22-24{width:91.6667%;*width:91.6357%}.yui3-u-23-24{width:95.8333%;*width:95.8023%}.yui3-u-1,.yui3-u-1-1,.yui3-u-5-5,.yui3-u-24-24{width:100%}#yui3-css-stamp.cssgrids-units{display:none}
diff --git a/js/yui3/cssgrids-units/cssgrids-units.css b/js/yui3/cssgrids-units/cssgrids-units.css
new file mode 100644
index 000000000..0a620fe05
--- /dev/null
+++ b/js/yui3/cssgrids-units/cssgrids-units.css
@@ -0,0 +1,227 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/*!
+Pure v0.4.2
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+https://github.com/yui/pure/blob/master/LICENSE.md
+*/
+
+.yui3-u-1,
+.yui3-u-1-1,
+.yui3-u-1-2,
+.yui3-u-1-3,
+.yui3-u-2-3,
+.yui3-u-1-4,
+.yui3-u-3-4,
+.yui3-u-1-5,
+.yui3-u-2-5,
+.yui3-u-3-5,
+.yui3-u-4-5,
+.yui3-u-5-5,
+.yui3-u-1-6,
+.yui3-u-5-6,
+.yui3-u-1-8,
+.yui3-u-3-8,
+.yui3-u-5-8,
+.yui3-u-7-8,
+.yui3-u-1-12,
+.yui3-u-5-12,
+.yui3-u-7-12,
+.yui3-u-11-12,
+.yui3-u-1-24,
+.yui3-u-2-24,
+.yui3-u-3-24,
+.yui3-u-4-24,
+.yui3-u-5-24,
+.yui3-u-6-24,
+.yui3-u-7-24,
+.yui3-u-8-24,
+.yui3-u-9-24,
+.yui3-u-10-24,
+.yui3-u-11-24,
+.yui3-u-12-24,
+.yui3-u-13-24,
+.yui3-u-14-24,
+.yui3-u-15-24,
+.yui3-u-16-24,
+.yui3-u-17-24,
+.yui3-u-18-24,
+.yui3-u-19-24,
+.yui3-u-20-24,
+.yui3-u-21-24,
+.yui3-u-22-24,
+.yui3-u-23-24,
+.yui3-u-24-24 {
+ display: inline-block;
+ *display: inline;
+ zoom: 1;
+ letter-spacing: normal;
+ word-spacing: normal;
+ vertical-align: top;
+ text-rendering: auto;
+}
+
+.yui3-u-1-24 {
+ width: 4.1667%;
+ *width: 4.1357%;
+}
+
+.yui3-u-1-12,
+.yui3-u-2-24 {
+ width: 8.3333%;
+ *width: 8.3023%;
+}
+
+.yui3-u-1-8,
+.yui3-u-3-24 {
+ width: 12.5000%;
+ *width: 12.4690%;
+}
+
+.yui3-u-1-6,
+.yui3-u-4-24 {
+ width: 16.6667%;
+ *width: 16.6357%;
+}
+
+.yui3-u-1-5 {
+ width: 20%;
+ *width: 19.9690%;
+}
+
+.yui3-u-5-24 {
+ width: 20.8333%;
+ *width: 20.8023%;
+}
+
+.yui3-u-1-4,
+.yui3-u-6-24 {
+ width: 25%;
+ *width: 24.9690%;
+}
+
+.yui3-u-7-24 {
+ width: 29.1667%;
+ *width: 29.1357%;
+}
+
+.yui3-u-1-3,
+.yui3-u-8-24 {
+ width: 33.3333%;
+ *width: 33.3023%;
+}
+
+.yui3-u-3-8,
+.yui3-u-9-24 {
+ width: 37.5000%;
+ *width: 37.4690%;
+}
+
+.yui3-u-2-5 {
+ width: 40%;
+ *width: 39.9690%;
+}
+
+.yui3-u-5-12,
+.yui3-u-10-24 {
+ width: 41.6667%;
+ *width: 41.6357%;
+}
+
+.yui3-u-11-24 {
+ width: 45.8333%;
+ *width: 45.8023%;
+}
+
+.yui3-u-1-2,
+.yui3-u-12-24 {
+ width: 50%;
+ *width: 49.9690%;
+}
+
+.yui3-u-13-24 {
+ width: 54.1667%;
+ *width: 54.1357%;
+}
+
+.yui3-u-7-12,
+.yui3-u-14-24 {
+ width: 58.3333%;
+ *width: 58.3023%;
+}
+
+.yui3-u-3-5 {
+ width: 60%;
+ *width: 59.9690%;
+}
+
+.yui3-u-5-8,
+.yui3-u-15-24 {
+ width: 62.5000%;
+ *width: 62.4690%;
+}
+
+.yui3-u-2-3,
+.yui3-u-16-24 {
+ width: 66.6667%;
+ *width: 66.6357%;
+}
+
+.yui3-u-17-24 {
+ width: 70.8333%;
+ *width: 70.8023%;
+}
+
+.yui3-u-3-4,
+.yui3-u-18-24 {
+ width: 75%;
+ *width: 74.9690%;
+}
+
+.yui3-u-19-24 {
+ width: 79.1667%;
+ *width: 79.1357%;
+}
+
+.yui3-u-4-5 {
+ width: 80%;
+ *width: 79.9690%;
+}
+
+.yui3-u-5-6,
+.yui3-u-20-24 {
+ width: 83.3333%;
+ *width: 83.3023%;
+}
+
+.yui3-u-7-8,
+.yui3-u-21-24 {
+ width: 87.5000%;
+ *width: 87.4690%;
+}
+
+.yui3-u-11-12,
+.yui3-u-22-24 {
+ width: 91.6667%;
+ *width: 91.6357%;
+}
+
+.yui3-u-23-24 {
+ width: 95.8333%;
+ *width: 95.8023%;
+}
+
+.yui3-u-1,
+.yui3-u-1-1,
+.yui3-u-5-5,
+.yui3-u-24-24 {
+ width: 100%;
+}
+/* YUI CSS Detection Stamp */
+#yui3-css-stamp.cssgrids-units { display: none; }
diff --git a/js/yui3/cssgrids/cssgrids-min.css b/js/yui3/cssgrids/cssgrids-min.css
new file mode 100644
index 000000000..44bcabb81
--- /dev/null
+++ b/js/yui3/cssgrids/cssgrids-min.css
@@ -0,0 +1,18 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/*!
+Pure v0.4.2
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+https://github.com/yui/pure/blob/master/LICENSE.md
+*/.yui3-g{letter-spacing:-0.31em;*letter-spacing:normal;*word-spacing:-0.43em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap}.opera-only :-o-prefocus,.yui3-g{word-spacing:-0.43em}.yui3-u{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.yui3-g [class *= "yui3-u"]{font-family:sans-serif}/*!
+Pure v0.4.2
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+https://github.com/yui/pure/blob/master/LICENSE.md
+*/.yui3-u-1,.yui3-u-1-1,.yui3-u-1-2,.yui3-u-1-3,.yui3-u-2-3,.yui3-u-1-4,.yui3-u-3-4,.yui3-u-1-5,.yui3-u-2-5,.yui3-u-3-5,.yui3-u-4-5,.yui3-u-5-5,.yui3-u-1-6,.yui3-u-5-6,.yui3-u-1-8,.yui3-u-3-8,.yui3-u-5-8,.yui3-u-7-8,.yui3-u-1-12,.yui3-u-5-12,.yui3-u-7-12,.yui3-u-11-12,.yui3-u-1-24,.yui3-u-2-24,.yui3-u-3-24,.yui3-u-4-24,.yui3-u-5-24,.yui3-u-6-24,.yui3-u-7-24,.yui3-u-8-24,.yui3-u-9-24,.yui3-u-10-24,.yui3-u-11-24,.yui3-u-12-24,.yui3-u-13-24,.yui3-u-14-24,.yui3-u-15-24,.yui3-u-16-24,.yui3-u-17-24,.yui3-u-18-24,.yui3-u-19-24,.yui3-u-20-24,.yui3-u-21-24,.yui3-u-22-24,.yui3-u-23-24,.yui3-u-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.yui3-u-1-24{width:4.1667%;*width:4.1357%}.yui3-u-1-12,.yui3-u-2-24{width:8.3333%;*width:8.3023%}.yui3-u-1-8,.yui3-u-3-24{width:12.5000%;*width:12.4690%}.yui3-u-1-6,.yui3-u-4-24{width:16.6667%;*width:16.6357%}.yui3-u-1-5{width:20%;*width:19.9690%}.yui3-u-5-24{width:20.8333%;*width:20.8023%}.yui3-u-1-4,.yui3-u-6-24{width:25%;*width:24.9690%}.yui3-u-7-24{width:29.1667%;*width:29.1357%}.yui3-u-1-3,.yui3-u-8-24{width:33.3333%;*width:33.3023%}.yui3-u-3-8,.yui3-u-9-24{width:37.5000%;*width:37.4690%}.yui3-u-2-5{width:40%;*width:39.9690%}.yui3-u-5-12,.yui3-u-10-24{width:41.6667%;*width:41.6357%}.yui3-u-11-24{width:45.8333%;*width:45.8023%}.yui3-u-1-2,.yui3-u-12-24{width:50%;*width:49.9690%}.yui3-u-13-24{width:54.1667%;*width:54.1357%}.yui3-u-7-12,.yui3-u-14-24{width:58.3333%;*width:58.3023%}.yui3-u-3-5{width:60%;*width:59.9690%}.yui3-u-5-8,.yui3-u-15-24{width:62.5000%;*width:62.4690%}.yui3-u-2-3,.yui3-u-16-24{width:66.6667%;*width:66.6357%}.yui3-u-17-24{width:70.8333%;*width:70.8023%}.yui3-u-3-4,.yui3-u-18-24{width:75%;*width:74.9690%}.yui3-u-19-24{width:79.1667%;*width:79.1357%}.yui3-u-4-5{width:80%;*width:79.9690%}.yui3-u-5-6,.yui3-u-20-24{width:83.3333%;*width:83.3023%}.yui3-u-7-8,.yui3-u-21-24{width:87.5000%;*width:87.4690%}.yui3-u-11-12,.yui3-u-22-24{width:91.6667%;*width:91.6357%}.yui3-u-23-24{width:95.8333%;*width:95.8023%}.yui3-u-1,.yui3-u-1-1,.yui3-u-5-5,.yui3-u-24-24{width:100%}#yui3-css-stamp.cssgrids{display:none}
diff --git a/js/yui3/cssgrids/cssgrids.css b/js/yui3/cssgrids/cssgrids.css
new file mode 100644
index 000000000..88e1d4053
--- /dev/null
+++ b/js/yui3/cssgrids/cssgrids.css
@@ -0,0 +1,303 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/*!
+Pure v0.4.2
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+https://github.com/yui/pure/blob/master/LICENSE.md
+*/
+
+/*csslint regex-selectors:false, known-properties:false, duplicate-properties:false*/
+
+.yui3-g {
+ letter-spacing: -0.31em;
+ /* Webkit: collapse white-space between units */
+ *letter-spacing: normal;
+ /* reset IE < 8 */
+ *word-spacing: -0.43em;
+ /* IE < 8: collapse white-space between units */
+ text-rendering: optimizespeed;
+ /* Webkit: fixes text-rendering: optimizeLegibility */
+ /*
+ Sets the font stack to fonts known to work properly with the above letter
+ and word spacings. See: https://github.com/yui/pure/issues/41/
+
+ The following font stack makes Pure Grids work on all known environments.
+
+ * FreeSans: Ships with many Linux distros, including Ubuntu
+
+ * Arimo: Ships with Chrome OS. Arimo has to be defined before Helvetica and
+ Arial to get picked up by the browser, even though neither is available
+ in Chrome OS.
+
+ * Droid Sans: Ships with all versions of Android.
+
+ * Helvetica, Arial, sans-serif: Common font stack on OS X and Windows.
+ */
+ font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif;
+ /*
+ Use flexbox when possible to avoid `letter-spacing` side-effects.
+
+ NOTE: Firefox (as of 25) does not currently support flex-wrap, so the
+ `-moz-` prefix version is omitted.
+ */
+ display: -webkit-flex;
+ -webkit-flex-flow: row wrap;
+ /* IE10 uses display: flexbox */
+ display: -ms-flexbox;
+ -ms-flex-flow: row wrap;
+}
+
+/* Opera as of 12 on Windows needs word-spacing.
+ The ".opera-only" selector is used to prevent actual prefocus styling
+ and is not required in markup.
+*/
+
+.opera-only :-o-prefocus,
+.yui3-g {
+ word-spacing: -0.43em;
+}
+
+.yui3-u {
+ display: inline-block;
+ *display: inline;
+ /* IE < 8: fake inline-block */
+ zoom: 1;
+ letter-spacing: normal;
+ word-spacing: normal;
+ vertical-align: top;
+ text-rendering: auto;
+}
+
+/*
+Resets the font family back to the OS/browser's default sans-serif font,
+this the same font stack that Normalize.css sets for the `body`.
+*/
+
+.yui3-g [class *= "yui3-u"] {
+ font-family: sans-serif;
+}/*!
+Pure v0.4.2
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+https://github.com/yui/pure/blob/master/LICENSE.md
+*/
+
+.yui3-u-1,
+.yui3-u-1-1,
+.yui3-u-1-2,
+.yui3-u-1-3,
+.yui3-u-2-3,
+.yui3-u-1-4,
+.yui3-u-3-4,
+.yui3-u-1-5,
+.yui3-u-2-5,
+.yui3-u-3-5,
+.yui3-u-4-5,
+.yui3-u-5-5,
+.yui3-u-1-6,
+.yui3-u-5-6,
+.yui3-u-1-8,
+.yui3-u-3-8,
+.yui3-u-5-8,
+.yui3-u-7-8,
+.yui3-u-1-12,
+.yui3-u-5-12,
+.yui3-u-7-12,
+.yui3-u-11-12,
+.yui3-u-1-24,
+.yui3-u-2-24,
+.yui3-u-3-24,
+.yui3-u-4-24,
+.yui3-u-5-24,
+.yui3-u-6-24,
+.yui3-u-7-24,
+.yui3-u-8-24,
+.yui3-u-9-24,
+.yui3-u-10-24,
+.yui3-u-11-24,
+.yui3-u-12-24,
+.yui3-u-13-24,
+.yui3-u-14-24,
+.yui3-u-15-24,
+.yui3-u-16-24,
+.yui3-u-17-24,
+.yui3-u-18-24,
+.yui3-u-19-24,
+.yui3-u-20-24,
+.yui3-u-21-24,
+.yui3-u-22-24,
+.yui3-u-23-24,
+.yui3-u-24-24 {
+ display: inline-block;
+ *display: inline;
+ zoom: 1;
+ letter-spacing: normal;
+ word-spacing: normal;
+ vertical-align: top;
+ text-rendering: auto;
+}
+
+.yui3-u-1-24 {
+ width: 4.1667%;
+ *width: 4.1357%;
+}
+
+.yui3-u-1-12,
+.yui3-u-2-24 {
+ width: 8.3333%;
+ *width: 8.3023%;
+}
+
+.yui3-u-1-8,
+.yui3-u-3-24 {
+ width: 12.5000%;
+ *width: 12.4690%;
+}
+
+.yui3-u-1-6,
+.yui3-u-4-24 {
+ width: 16.6667%;
+ *width: 16.6357%;
+}
+
+.yui3-u-1-5 {
+ width: 20%;
+ *width: 19.9690%;
+}
+
+.yui3-u-5-24 {
+ width: 20.8333%;
+ *width: 20.8023%;
+}
+
+.yui3-u-1-4,
+.yui3-u-6-24 {
+ width: 25%;
+ *width: 24.9690%;
+}
+
+.yui3-u-7-24 {
+ width: 29.1667%;
+ *width: 29.1357%;
+}
+
+.yui3-u-1-3,
+.yui3-u-8-24 {
+ width: 33.3333%;
+ *width: 33.3023%;
+}
+
+.yui3-u-3-8,
+.yui3-u-9-24 {
+ width: 37.5000%;
+ *width: 37.4690%;
+}
+
+.yui3-u-2-5 {
+ width: 40%;
+ *width: 39.9690%;
+}
+
+.yui3-u-5-12,
+.yui3-u-10-24 {
+ width: 41.6667%;
+ *width: 41.6357%;
+}
+
+.yui3-u-11-24 {
+ width: 45.8333%;
+ *width: 45.8023%;
+}
+
+.yui3-u-1-2,
+.yui3-u-12-24 {
+ width: 50%;
+ *width: 49.9690%;
+}
+
+.yui3-u-13-24 {
+ width: 54.1667%;
+ *width: 54.1357%;
+}
+
+.yui3-u-7-12,
+.yui3-u-14-24 {
+ width: 58.3333%;
+ *width: 58.3023%;
+}
+
+.yui3-u-3-5 {
+ width: 60%;
+ *width: 59.9690%;
+}
+
+.yui3-u-5-8,
+.yui3-u-15-24 {
+ width: 62.5000%;
+ *width: 62.4690%;
+}
+
+.yui3-u-2-3,
+.yui3-u-16-24 {
+ width: 66.6667%;
+ *width: 66.6357%;
+}
+
+.yui3-u-17-24 {
+ width: 70.8333%;
+ *width: 70.8023%;
+}
+
+.yui3-u-3-4,
+.yui3-u-18-24 {
+ width: 75%;
+ *width: 74.9690%;
+}
+
+.yui3-u-19-24 {
+ width: 79.1667%;
+ *width: 79.1357%;
+}
+
+.yui3-u-4-5 {
+ width: 80%;
+ *width: 79.9690%;
+}
+
+.yui3-u-5-6,
+.yui3-u-20-24 {
+ width: 83.3333%;
+ *width: 83.3023%;
+}
+
+.yui3-u-7-8,
+.yui3-u-21-24 {
+ width: 87.5000%;
+ *width: 87.4690%;
+}
+
+.yui3-u-11-12,
+.yui3-u-22-24 {
+ width: 91.6667%;
+ *width: 91.6357%;
+}
+
+.yui3-u-23-24 {
+ width: 95.8333%;
+ *width: 95.8023%;
+}
+
+.yui3-u-1,
+.yui3-u-1-1,
+.yui3-u-5-5,
+.yui3-u-24-24 {
+ width: 100%;
+}
+/* YUI CSS Detection Stamp */
+#yui3-css-stamp.cssgrids { display: none; }
diff --git a/js/yui3/cssnormalize-context/cssnormalize-context-min.css b/js/yui3/cssnormalize-context/cssnormalize-context-min.css
new file mode 100644
index 000000000..bf38c81e1
--- /dev/null
+++ b/js/yui3/cssnormalize-context/cssnormalize-context-min.css
@@ -0,0 +1,16 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/*!
+Pure v0.4.2
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+https://github.com/yui/pure/blob/master/LICENSE.md
+*//*!
+normalize.css v1.1.3 | MIT License | git.io/normalize
+Copyright (c) Nicolas Gallagher and Jonathan Neal
+*//*! normalize.css v1.1.3 | MIT License | git.io/normalize */.yui3-normalized article,.yui3-normalized aside,.yui3-normalized details,.yui3-normalized figcaption,.yui3-normalized figure,.yui3-normalized footer,.yui3-normalized header,.yui3-normalized hgroup,.yui3-normalized main,.yui3-normalized nav,.yui3-normalized section,.yui3-normalized summary{display:block}.yui3-normalized audio,.yui3-normalized canvas,.yui3-normalized video{display:inline-block;*display:inline;*zoom:1}.yui3-normalized audio:not([controls]){display:none;height:0}.yui3-normalized [hidden]{display:none}.yui3-normalized{font-size:100%;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}.yui3-normalized,.yui3-normalized button,.yui3-normalized input,.yui3-normalized select,.yui3-normalized textarea{font-family:sans-serif}.yui3-normalized{margin:0}.yui3-normalized a:focus{outline:thin dotted}.yui3-normalized a:active,.yui3-normalized a:hover{outline:0}.yui3-normalized h1{font-size:2em;margin:.67em 0}.yui3-normalized h2{font-size:1.5em;margin:.83em 0}.yui3-normalized h3{font-size:1.17em;margin:1em 0}.yui3-normalized h4{font-size:1em;margin:1.33em 0}.yui3-normalized h5{font-size:.83em;margin:1.67em 0}.yui3-normalized h6{font-size:.67em;margin:2.33em 0}.yui3-normalized abbr[title]{border-bottom:1px dotted}.yui3-normalized b,.yui3-normalized strong{font-weight:bold}.yui3-normalized blockquote{margin:1em 40px}.yui3-normalized dfn{font-style:italic}.yui3-normalized hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}.yui3-normalized mark{background:#ff0;color:#000}.yui3-normalized p,.yui3-normalized pre{margin:1em 0}.yui3-normalized code,.yui3-normalized kbd,.yui3-normalized pre,.yui3-normalized samp{font-family:monospace,serif;_font-family:'courier new',monospace;font-size:1em}.yui3-normalized pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word}.yui3-normalized q{quotes:none}.yui3-normalized q:before,.yui3-normalized q:after{content:'';content:none}.yui3-normalized small{font-size:80%}.yui3-normalized sub,.yui3-normalized sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}.yui3-normalized sup{top:-0.5em}.yui3-normalized sub{bottom:-0.25em}.yui3-normalized dl,.yui3-normalized menu,.yui3-normalized ol,.yui3-normalized ul{margin:1em 0}.yui3-normalized dd{margin:0 0 0 40px}.yui3-normalized menu,.yui3-normalized ol,.yui3-normalized ul{padding:0 0 0 40px}.yui3-normalized nav ul,.yui3-normalized nav ol{list-style:none;list-style-image:none}.yui3-normalized img{border:0;-ms-interpolation-mode:bicubic}.yui3-normalized svg:not(:root){overflow:hidden}.yui3-normalized figure{margin:0}.yui3-normalized form{margin:0}.yui3-normalized fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}.yui3-normalized legend{border:0;padding:0;white-space:normal;*margin-left:-7px}.yui3-normalized button,.yui3-normalized input,.yui3-normalized select,.yui3-normalized textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.yui3-normalized button,.yui3-normalized input{line-height:normal}.yui3-normalized button,.yui3-normalized select{text-transform:none}.yui3-normalized button,.yui3-normalized input[type="button"],.yui3-normalized input[type="reset"],.yui3-normalized input[type="submit"]{-webkit-appearance:button;cursor:pointer;*overflow:visible}.yui3-normalized button[disabled],.yui3-normalized input[disabled]{cursor:default}.yui3-normalized input[type="checkbox"],.yui3-normalized input[type="radio"]{box-sizing:border-box;padding:0;*height:13px;*width:13px}.yui3-normalized input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}.yui3-normalized input[type="search"]::-webkit-search-cancel-button,.yui3-normalized input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}.yui3-normalized button::-moz-focus-inner,.yui3-normalized input::-moz-focus-inner{border:0;padding:0}.yui3-normalized textarea{overflow:auto;vertical-align:top}.yui3-normalized table{border-collapse:collapse;border-spacing:0}.yui3-normalized [hidden]{display:none!important}#yui3-css-stamp.cssnormalize-context{display:none}
diff --git a/js/yui3/cssnormalize-context/cssnormalize-context.css b/js/yui3/cssnormalize-context/cssnormalize-context.css
new file mode 100644
index 000000000..63a7ad438
--- /dev/null
+++ b/js/yui3/cssnormalize-context/cssnormalize-context.css
@@ -0,0 +1,590 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/*!
+Pure v0.4.2
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+https://github.com/yui/pure/blob/master/LICENSE.md
+*/
+
+/*!
+normalize.css v1.1.3 | MIT License | git.io/normalize
+Copyright (c) Nicolas Gallagher and Jonathan Neal
+*/
+
+/*! normalize.css v1.1.3 | MIT License | git.io/normalize */
+
+/* ==========================================================================
+ HTML5 display definitions
+ ========================================================================== */
+
+/**
+ * Correct `block` display not defined in IE 6/7/8/9 and Firefox 3.
+ */
+
+.yui3-normalized article,
+.yui3-normalized aside,
+.yui3-normalized details,
+.yui3-normalized figcaption,
+.yui3-normalized figure,
+.yui3-normalized footer,
+.yui3-normalized header,
+.yui3-normalized hgroup,
+.yui3-normalized main,
+.yui3-normalized nav,
+.yui3-normalized section,
+.yui3-normalized summary {
+ display: block;
+}
+
+/**
+ * Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
+ */
+
+.yui3-normalized audio,
+.yui3-normalized canvas,
+.yui3-normalized video {
+ display: inline-block;
+ *display: inline;
+ *zoom: 1;
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+.yui3-normalized audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Address styling not present in IE 7/8/9, Firefox 3, and Safari 4.
+ * Known issue: no IE 6 support.
+ */
+
+.yui3-normalized [hidden] {
+ display: none;
+}
+
+/* ==========================================================================
+ Base
+ ========================================================================== */
+
+/**
+ * 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using
+ * `em` units.
+ * 2. Prevent iOS text size adjust after orientation change, without disabling
+ * user zoom.
+ */
+
+.yui3-normalized {
+ font-size: 100%;
+ /* 1 */
+ -ms-text-size-adjust: 100%;
+ /* 2 */
+ -webkit-text-size-adjust: 100%;
+ /* 2 */
+}
+
+/**
+ * Address `font-family` inconsistency between `textarea` and other form
+ * elements.
+ */
+
+.yui3-normalized,
+.yui3-normalized button,
+.yui3-normalized input,
+.yui3-normalized select,
+.yui3-normalized textarea {
+ font-family: sans-serif;
+}
+
+/**
+ * Address margins handled incorrectly in IE 6/7.
+ */
+
+.yui3-normalized {
+ margin: 0;
+}
+
+/* ==========================================================================
+ Links
+ ========================================================================== */
+
+/**
+ * Address `outline` inconsistency between Chrome and other browsers.
+ */
+
+.yui3-normalized a:focus {
+ outline: thin dotted;
+}
+
+/**
+ * Improve readability when focused and also mouse hovered in all browsers.
+ */
+
+.yui3-normalized a:active,
+.yui3-normalized a:hover {
+ outline: 0;
+}
+
+/* ==========================================================================
+ Typography
+ ========================================================================== */
+
+/**
+ * Address font sizes and margins set differently in IE 6/7.
+ * Address font sizes within `section` and `article` in Firefox 4+, Safari 5,
+ * and Chrome.
+ */
+
+.yui3-normalized h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+.yui3-normalized h2 {
+ font-size: 1.5em;
+ margin: 0.83em 0;
+}
+
+.yui3-normalized h3 {
+ font-size: 1.17em;
+ margin: 1em 0;
+}
+
+.yui3-normalized h4 {
+ font-size: 1em;
+ margin: 1.33em 0;
+}
+
+.yui3-normalized h5 {
+ font-size: 0.83em;
+ margin: 1.67em 0;
+}
+
+.yui3-normalized h6 {
+ font-size: 0.67em;
+ margin: 2.33em 0;
+}
+
+/**
+ * Address styling not present in IE 7/8/9, Safari 5, and Chrome.
+ */
+
+.yui3-normalized abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 3+, Safari 4/5, and Chrome.
+ */
+
+.yui3-normalized b,
+.yui3-normalized strong {
+ font-weight: bold;
+}
+
+.yui3-normalized blockquote {
+ margin: 1em 40px;
+}
+
+/**
+ * Address styling not present in Safari 5 and Chrome.
+ */
+
+.yui3-normalized dfn {
+ font-style: italic;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ * Known issue: no IE 6/7 normalization.
+ */
+
+.yui3-normalized hr {
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+ height: 0;
+}
+
+/**
+ * Address styling not present in IE 6/7/8/9.
+ */
+
+.yui3-normalized mark {
+ background: #ff0;
+ color: #000;
+}
+
+/**
+ * Address margins set differently in IE 6/7.
+ */
+
+.yui3-normalized p,
+.yui3-normalized pre {
+ margin: 1em 0;
+}
+
+/**
+ * Correct font family set oddly in IE 6, Safari 4/5, and Chrome.
+ */
+
+.yui3-normalized code,
+.yui3-normalized kbd,
+.yui3-normalized pre,
+.yui3-normalized samp {
+ font-family: monospace, serif;
+ _font-family: 'courier new', monospace;
+ font-size: 1em;
+}
+
+/**
+ * Improve readability of pre-formatted text in all browsers.
+ */
+
+.yui3-normalized pre {
+ white-space: pre;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+/**
+ * Address CSS quotes not supported in IE 6/7.
+ */
+
+.yui3-normalized q {
+ quotes: none;
+}
+
+/**
+ * Address `quotes` property not supported in Safari 4.
+ */
+
+.yui3-normalized q:before,
+.yui3-normalized q:after {
+ content: '';
+ content: none;
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+.yui3-normalized small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+.yui3-normalized sub,
+.yui3-normalized sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+.yui3-normalized sup {
+ top: -0.5em;
+}
+
+.yui3-normalized sub {
+ bottom: -0.25em;
+}
+
+/* ==========================================================================
+ Lists
+ ========================================================================== */
+
+/**
+ * Address margins set differently in IE 6/7.
+ */
+
+.yui3-normalized dl,
+.yui3-normalized menu,
+.yui3-normalized ol,
+.yui3-normalized ul {
+ margin: 1em 0;
+}
+
+.yui3-normalized dd {
+ margin: 0 0 0 40px;
+}
+
+/**
+ * Address paddings set differently in IE 6/7.
+ */
+
+.yui3-normalized menu,
+.yui3-normalized ol,
+.yui3-normalized ul {
+ padding: 0 0 0 40px;
+}
+
+/**
+ * Correct list images handled incorrectly in IE 7.
+ */
+
+.yui3-normalized nav ul,
+.yui3-normalized nav ol {
+ list-style: none;
+ list-style-image: none;
+}
+
+/* ==========================================================================
+ Embedded content
+ ========================================================================== */
+
+/**
+ * 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3.
+ * 2. Improve image quality when scaled in IE 7.
+ */
+
+.yui3-normalized img {
+ border: 0;
+ /* 1 */
+ -ms-interpolation-mode: bicubic;
+ /* 2 */
+}
+
+/**
+ * Correct overflow displayed oddly in IE 9.
+ */
+
+.yui3-normalized svg:not(:root) {
+ overflow: hidden;
+}
+
+/* ==========================================================================
+ Figures
+ ========================================================================== */
+
+/**
+ * Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11.
+ */
+
+.yui3-normalized figure {
+ margin: 0;
+}
+
+/* ==========================================================================
+ Forms
+ ========================================================================== */
+
+/**
+ * Correct margin displayed oddly in IE 6/7.
+ */
+
+.yui3-normalized form {
+ margin: 0;
+}
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+.yui3-normalized fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct color not being inherited in IE 6/7/8/9.
+ * 2. Correct text not wrapping in Firefox 3.
+ * 3. Correct alignment displayed oddly in IE 6/7.
+ */
+
+.yui3-normalized legend {
+ border: 0;
+ /* 1 */
+ padding: 0;
+ white-space: normal;
+ /* 2 */
+ *margin-left: -7px;
+ /* 3 */
+}
+
+/**
+ * 1. Correct font size not being inherited in all browsers.
+ * 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5,
+ * and Chrome.
+ * 3. Improve appearance and consistency in all browsers.
+ */
+
+.yui3-normalized button,
+.yui3-normalized input,
+.yui3-normalized select,
+.yui3-normalized textarea {
+ font-size: 100%;
+ /* 1 */
+ margin: 0;
+ /* 2 */
+ vertical-align: baseline;
+ /* 3 */
+ *vertical-align: middle;
+ /* 3 */
+}
+
+/**
+ * Address Firefox 3+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+.yui3-normalized button,
+.yui3-normalized input {
+ line-height: normal;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+.
+ * Correct `select` style inheritance in Firefox 4+ and Opera.
+ */
+
+.yui3-normalized button,
+.yui3-normalized select {
+ text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ * 4. Remove inner spacing in IE 7 without affecting normal text inputs.
+ * Known issue: inner spacing remains in IE 6.
+ */
+
+.yui3-normalized button,
+.yui3-normalized input[type="button"],
+.yui3-normalized /* 1 */
+input[type="reset"],
+.yui3-normalized input[type="submit"] {
+ -webkit-appearance: button;
+ /* 2 */
+ cursor: pointer;
+ /* 3 */
+ *overflow: visible;
+ /* 4 */
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+.yui3-normalized button[disabled],
+.yui3-normalized input[disabled] {
+ cursor: default;
+}
+
+/**
+ * 1. Address box sizing set to content-box in IE 8/9.
+ * 2. Remove excess padding in IE 8/9.
+ * 3. Remove excess padding in IE 7.
+ * Known issue: excess padding remains in IE 6.
+ */
+
+.yui3-normalized input[type="checkbox"],
+.yui3-normalized input[type="radio"] {
+ box-sizing: border-box;
+ /* 1 */
+ padding: 0;
+ /* 2 */
+ *height: 13px;
+ /* 3 */
+ *width: 13px;
+ /* 3 */
+}
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
+ * (include `-moz` to future-proof).
+ */
+
+.yui3-normalized input[type="search"] {
+ -webkit-appearance: textfield;
+ /* 1 */
+ -moz-box-sizing: content-box;
+ -webkit-box-sizing: content-box;
+ /* 2 */
+ box-sizing: content-box;
+}
+
+/**
+ * Remove inner padding and search cancel button in Safari 5 and Chrome
+ * on OS X.
+ */
+
+.yui3-normalized input[type="search"]::-webkit-search-cancel-button,
+.yui3-normalized input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * Remove inner padding and border in Firefox 3+.
+ */
+
+.yui3-normalized button::-moz-focus-inner,
+.yui3-normalized input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+/**
+ * 1. Remove default vertical scrollbar in IE 6/7/8/9.
+ * 2. Improve readability and alignment in all browsers.
+ */
+
+.yui3-normalized textarea {
+ overflow: auto;
+ /* 1 */
+ vertical-align: top;
+ /* 2 */
+}
+
+/* ==========================================================================
+ Tables
+ ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+.yui3-normalized table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+/*csslint important:false*/
+
+/* ==========================================================================
+ Pure Base Extras
+ ========================================================================== */
+
+/**
+ * Extra rules that Pure adds on top of Normalize.css
+ */
+
+/**
+ * Always hide an element when it has the `hidden` HTML attribute.
+ */
+
+.yui3-normalized [hidden] {
+ display: none !important;
+}
+/* YUI CSS Detection Stamp */
+#yui3-css-stamp.cssnormalize-context { display: none; }
diff --git a/js/yui3/cssnormalize/cssnormalize-min.css b/js/yui3/cssnormalize/cssnormalize-min.css
new file mode 100644
index 000000000..a741dd4ad
--- /dev/null
+++ b/js/yui3/cssnormalize/cssnormalize-min.css
@@ -0,0 +1,16 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/*!
+Pure v0.4.2
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+https://github.com/yui/pure/blob/master/LICENSE.md
+*//*!
+normalize.css v1.1.3 | MIT License | git.io/normalize
+Copyright (c) Nicolas Gallagher and Jonathan Neal
+*//*! normalize.css v1.1.3 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-size:100%;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}html,button,input,select,textarea{font-family:sans-serif}body{margin:0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{font-size:2em;margin:.67em 0}h2{font-size:1.5em;margin:.83em 0}h3{font-size:1.17em;margin:1em 0}h4{font-size:1em;margin:1.33em 0}h5{font-size:.83em;margin:1.67em 0}h6{font-size:.67em;margin:2.33em 0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}blockquote{margin:1em 40px}dfn{font-style:italic}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}mark{background:#ff0;color:#000}p,pre{margin:1em 0}code,kbd,pre,samp{font-family:monospace,serif;_font-family:'courier new',monospace;font-size:1em}pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word}q{quotes:none}q:before,q:after{content:'';content:none}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}dl,menu,ol,ul{margin:1em 0}dd{margin:0 0 0 40px}menu,ol,ul{padding:0 0 0 40px}nav ul,nav ol{list-style:none;list-style-image:none}img{border:0;-ms-interpolation-mode:bicubic}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0;white-space:normal;*margin-left:-7px}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;*overflow:visible}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*height:13px;*width:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}[hidden]{display:none!important}#yui3-css-stamp.cssnormalize{display:none}
diff --git a/js/yui3/cssnormalize/cssnormalize.css b/js/yui3/cssnormalize/cssnormalize.css
new file mode 100644
index 000000000..eaee8494c
--- /dev/null
+++ b/js/yui3/cssnormalize/cssnormalize.css
@@ -0,0 +1,590 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/*!
+Pure v0.4.2
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+https://github.com/yui/pure/blob/master/LICENSE.md
+*/
+
+/*!
+normalize.css v1.1.3 | MIT License | git.io/normalize
+Copyright (c) Nicolas Gallagher and Jonathan Neal
+*/
+
+/*! normalize.css v1.1.3 | MIT License | git.io/normalize */
+
+/* ==========================================================================
+ HTML5 display definitions
+ ========================================================================== */
+
+/**
+ * Correct `block` display not defined in IE 6/7/8/9 and Firefox 3.
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+nav,
+section,
+summary {
+ display: block;
+}
+
+/**
+ * Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
+ */
+
+audio,
+canvas,
+video {
+ display: inline-block;
+ *display: inline;
+ *zoom: 1;
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Address styling not present in IE 7/8/9, Firefox 3, and Safari 4.
+ * Known issue: no IE 6 support.
+ */
+
+[hidden] {
+ display: none;
+}
+
+/* ==========================================================================
+ Base
+ ========================================================================== */
+
+/**
+ * 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using
+ * `em` units.
+ * 2. Prevent iOS text size adjust after orientation change, without disabling
+ * user zoom.
+ */
+
+html {
+ font-size: 100%;
+ /* 1 */
+ -ms-text-size-adjust: 100%;
+ /* 2 */
+ -webkit-text-size-adjust: 100%;
+ /* 2 */
+}
+
+/**
+ * Address `font-family` inconsistency between `textarea` and other form
+ * elements.
+ */
+
+html,
+button,
+input,
+select,
+textarea {
+ font-family: sans-serif;
+}
+
+/**
+ * Address margins handled incorrectly in IE 6/7.
+ */
+
+body {
+ margin: 0;
+}
+
+/* ==========================================================================
+ Links
+ ========================================================================== */
+
+/**
+ * Address `outline` inconsistency between Chrome and other browsers.
+ */
+
+a:focus {
+ outline: thin dotted;
+}
+
+/**
+ * Improve readability when focused and also mouse hovered in all browsers.
+ */
+
+a:active,
+a:hover {
+ outline: 0;
+}
+
+/* ==========================================================================
+ Typography
+ ========================================================================== */
+
+/**
+ * Address font sizes and margins set differently in IE 6/7.
+ * Address font sizes within `section` and `article` in Firefox 4+, Safari 5,
+ * and Chrome.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+h2 {
+ font-size: 1.5em;
+ margin: 0.83em 0;
+}
+
+h3 {
+ font-size: 1.17em;
+ margin: 1em 0;
+}
+
+h4 {
+ font-size: 1em;
+ margin: 1.33em 0;
+}
+
+h5 {
+ font-size: 0.83em;
+ margin: 1.67em 0;
+}
+
+h6 {
+ font-size: 0.67em;
+ margin: 2.33em 0;
+}
+
+/**
+ * Address styling not present in IE 7/8/9, Safari 5, and Chrome.
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 3+, Safari 4/5, and Chrome.
+ */
+
+b,
+strong {
+ font-weight: bold;
+}
+
+blockquote {
+ margin: 1em 40px;
+}
+
+/**
+ * Address styling not present in Safari 5 and Chrome.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ * Known issue: no IE 6/7 normalization.
+ */
+
+hr {
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+ height: 0;
+}
+
+/**
+ * Address styling not present in IE 6/7/8/9.
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+/**
+ * Address margins set differently in IE 6/7.
+ */
+
+p,
+pre {
+ margin: 1em 0;
+}
+
+/**
+ * Correct font family set oddly in IE 6, Safari 4/5, and Chrome.
+ */
+
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, serif;
+ _font-family: 'courier new', monospace;
+ font-size: 1em;
+}
+
+/**
+ * Improve readability of pre-formatted text in all browsers.
+ */
+
+pre {
+ white-space: pre;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+/**
+ * Address CSS quotes not supported in IE 6/7.
+ */
+
+q {
+ quotes: none;
+}
+
+/**
+ * Address `quotes` property not supported in Safari 4.
+ */
+
+q:before,
+q:after {
+ content: '';
+ content: none;
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+/* ==========================================================================
+ Lists
+ ========================================================================== */
+
+/**
+ * Address margins set differently in IE 6/7.
+ */
+
+dl,
+menu,
+ol,
+ul {
+ margin: 1em 0;
+}
+
+dd {
+ margin: 0 0 0 40px;
+}
+
+/**
+ * Address paddings set differently in IE 6/7.
+ */
+
+menu,
+ol,
+ul {
+ padding: 0 0 0 40px;
+}
+
+/**
+ * Correct list images handled incorrectly in IE 7.
+ */
+
+nav ul,
+nav ol {
+ list-style: none;
+ list-style-image: none;
+}
+
+/* ==========================================================================
+ Embedded content
+ ========================================================================== */
+
+/**
+ * 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3.
+ * 2. Improve image quality when scaled in IE 7.
+ */
+
+img {
+ border: 0;
+ /* 1 */
+ -ms-interpolation-mode: bicubic;
+ /* 2 */
+}
+
+/**
+ * Correct overflow displayed oddly in IE 9.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* ==========================================================================
+ Figures
+ ========================================================================== */
+
+/**
+ * Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11.
+ */
+
+figure {
+ margin: 0;
+}
+
+/* ==========================================================================
+ Forms
+ ========================================================================== */
+
+/**
+ * Correct margin displayed oddly in IE 6/7.
+ */
+
+form {
+ margin: 0;
+}
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct color not being inherited in IE 6/7/8/9.
+ * 2. Correct text not wrapping in Firefox 3.
+ * 3. Correct alignment displayed oddly in IE 6/7.
+ */
+
+legend {
+ border: 0;
+ /* 1 */
+ padding: 0;
+ white-space: normal;
+ /* 2 */
+ *margin-left: -7px;
+ /* 3 */
+}
+
+/**
+ * 1. Correct font size not being inherited in all browsers.
+ * 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5,
+ * and Chrome.
+ * 3. Improve appearance and consistency in all browsers.
+ */
+
+button,
+input,
+select,
+textarea {
+ font-size: 100%;
+ /* 1 */
+ margin: 0;
+ /* 2 */
+ vertical-align: baseline;
+ /* 3 */
+ *vertical-align: middle;
+ /* 3 */
+}
+
+/**
+ * Address Firefox 3+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+button,
+input {
+ line-height: normal;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+.
+ * Correct `select` style inheritance in Firefox 4+ and Opera.
+ */
+
+button,
+select {
+ text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ * 4. Remove inner spacing in IE 7 without affecting normal text inputs.
+ * Known issue: inner spacing remains in IE 6.
+ */
+
+button,
+html input[type="button"],
+/* 1 */
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button;
+ /* 2 */
+ cursor: pointer;
+ /* 3 */
+ *overflow: visible;
+ /* 4 */
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled],
+html input[disabled] {
+ cursor: default;
+}
+
+/**
+ * 1. Address box sizing set to content-box in IE 8/9.
+ * 2. Remove excess padding in IE 8/9.
+ * 3. Remove excess padding in IE 7.
+ * Known issue: excess padding remains in IE 6.
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box;
+ /* 1 */
+ padding: 0;
+ /* 2 */
+ *height: 13px;
+ /* 3 */
+ *width: 13px;
+ /* 3 */
+}
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
+ * (include `-moz` to future-proof).
+ */
+
+input[type="search"] {
+ -webkit-appearance: textfield;
+ /* 1 */
+ -moz-box-sizing: content-box;
+ -webkit-box-sizing: content-box;
+ /* 2 */
+ box-sizing: content-box;
+}
+
+/**
+ * Remove inner padding and search cancel button in Safari 5 and Chrome
+ * on OS X.
+ */
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * Remove inner padding and border in Firefox 3+.
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+/**
+ * 1. Remove default vertical scrollbar in IE 6/7/8/9.
+ * 2. Improve readability and alignment in all browsers.
+ */
+
+textarea {
+ overflow: auto;
+ /* 1 */
+ vertical-align: top;
+ /* 2 */
+}
+
+/* ==========================================================================
+ Tables
+ ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+/*csslint important:false*/
+
+/* ==========================================================================
+ Pure Base Extras
+ ========================================================================== */
+
+/**
+ * Extra rules that Pure adds on top of Normalize.css
+ */
+
+/**
+ * Always hide an element when it has the `hidden` HTML attribute.
+ */
+
+[hidden] {
+ display: none !important;
+}
+/* YUI CSS Detection Stamp */
+#yui3-css-stamp.cssnormalize { display: none; }
diff --git a/js/yui3/cssreset-context/cssreset-context-min.css b/js/yui3/cssreset-context/cssreset-context-min.css
new file mode 100644
index 000000000..0737e46f4
--- /dev/null
+++ b/js/yui3/cssreset-context/cssreset-context-min.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-cssreset html{color:#000;background:#FFF}.yui3-cssreset body,.yui3-cssreset div,.yui3-cssreset dl,.yui3-cssreset dt,.yui3-cssreset dd,.yui3-cssreset ul,.yui3-cssreset ol,.yui3-cssreset li,.yui3-cssreset h1,.yui3-cssreset h2,.yui3-cssreset h3,.yui3-cssreset h4,.yui3-cssreset h5,.yui3-cssreset h6,.yui3-cssreset pre,.yui3-cssreset code,.yui3-cssreset form,.yui3-cssreset fieldset,.yui3-cssreset legend,.yui3-cssreset input,.yui3-cssreset textarea,.yui3-cssreset p,.yui3-cssreset blockquote,.yui3-cssreset th,.yui3-cssreset td{margin:0;padding:0}.yui3-cssreset table{border-collapse:collapse;border-spacing:0}.yui3-cssreset fieldset,.yui3-cssreset img{border:0}.yui3-cssreset address,.yui3-cssreset caption,.yui3-cssreset cite,.yui3-cssreset code,.yui3-cssreset dfn,.yui3-cssreset em,.yui3-cssreset strong,.yui3-cssreset th,.yui3-cssreset var{font-style:normal;font-weight:normal}.yui3-cssreset ol,.yui3-cssreset ul{list-style:none}.yui3-cssreset caption,.yui3-cssreset th{text-align:left}.yui3-cssreset h1,.yui3-cssreset h2,.yui3-cssreset h3,.yui3-cssreset h4,.yui3-cssreset h5,.yui3-cssreset h6{font-size:100%;font-weight:normal}.yui3-cssreset q:before,.yui3-cssreset q:after{content:''}.yui3-cssreset abbr,.yui3-cssreset acronym{border:0;font-variant:normal}.yui3-cssreset sup{vertical-align:text-top}.yui3-cssreset sub{vertical-align:text-bottom}.yui3-cssreset input,.yui3-cssreset textarea,.yui3-cssreset select{font-family:inherit;font-size:inherit;font-weight:inherit}.yui3-cssreset input,.yui3-cssreset textarea,.yui3-cssreset select{*font-size:100%}.yui3-cssreset legend{color:#000}#yui3-css-stamp.cssreset-context{display:none}
diff --git a/js/yui3/cssreset-context/cssreset-context.css b/js/yui3/cssreset-context/cssreset-context.css
new file mode 100644
index 000000000..940f3dd51
--- /dev/null
+++ b/js/yui3/cssreset-context/cssreset-context.css
@@ -0,0 +1,128 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/*e
+ TODO will need to remove settings on HTML since we can't namespace it.
+ TODO with the prefix, should I group by selector or property for weight savings?
+*/
+.yui3-cssreset html{
+ color:#000;
+ background:#FFF;
+}
+/*
+ TODO remove settings on BODY since we can't namespace it.
+*/
+/*
+ TODO test putting a class on HEAD.
+ - Fails on FF.
+*/
+.yui3-cssreset body,
+.yui3-cssreset div,
+.yui3-cssreset dl,
+.yui3-cssreset dt,
+.yui3-cssreset dd,
+.yui3-cssreset ul,
+.yui3-cssreset ol,
+.yui3-cssreset li,
+.yui3-cssreset h1,
+.yui3-cssreset h2,
+.yui3-cssreset h3,
+.yui3-cssreset h4,
+.yui3-cssreset h5,
+.yui3-cssreset h6,
+.yui3-cssreset pre,
+.yui3-cssreset code,
+.yui3-cssreset form,
+.yui3-cssreset fieldset,
+.yui3-cssreset legend,
+.yui3-cssreset input,
+.yui3-cssreset textarea,
+.yui3-cssreset p,
+.yui3-cssreset blockquote,
+.yui3-cssreset th,
+.yui3-cssreset td {
+ margin:0;
+ padding:0;
+}
+.yui3-cssreset table {
+ border-collapse:collapse;
+ border-spacing:0;
+}
+.yui3-cssreset fieldset,
+.yui3-cssreset img {
+ border:0;
+}
+/*
+ TODO think about hanlding inheritence differently, maybe letting IE6 fail a bit...
+*/
+.yui3-cssreset address,
+.yui3-cssreset caption,
+.yui3-cssreset cite,
+.yui3-cssreset code,
+.yui3-cssreset dfn,
+.yui3-cssreset em,
+.yui3-cssreset strong,
+.yui3-cssreset th,
+.yui3-cssreset var {
+ font-style:normal;
+ font-weight:normal;
+}
+
+.yui3-cssreset ol,
+.yui3-cssreset ul {
+ list-style:none;
+}
+
+.yui3-cssreset caption,
+.yui3-cssreset th {
+ text-align:left;
+}
+.yui3-cssreset h1,
+.yui3-cssreset h2,
+.yui3-cssreset h3,
+.yui3-cssreset h4,
+.yui3-cssreset h5,
+.yui3-cssreset h6 {
+ font-size:100%;
+ font-weight:normal;
+}
+.yui3-cssreset q:before,
+.yui3-cssreset q:after {
+ content:'';
+}
+.yui3-cssreset abbr,
+.yui3-cssreset acronym {
+ border:0;
+ font-variant:normal;
+}
+/* to preserve line-height and selector appearance */
+.yui3-cssreset sup {
+ vertical-align:text-top;
+}
+.yui3-cssreset sub {
+ vertical-align:text-bottom;
+}
+.yui3-cssreset input,
+.yui3-cssreset textarea,
+.yui3-cssreset select {
+ font-family:inherit;
+ font-size:inherit;
+ font-weight:inherit;
+}
+/*to enable resizing for IE*/
+.yui3-cssreset input,
+.yui3-cssreset textarea,
+.yui3-cssreset select {
+ *font-size:100%;
+}
+/*because legend doesn't inherit in IE */
+.yui3-cssreset legend {
+ color:#000;
+}
+
+/* YUI CSS Detection Stamp */
+#yui3-css-stamp.cssreset-context { display: none; }
diff --git a/js/yui3/cssreset/cssreset-min.css b/js/yui3/cssreset/cssreset-min.css
new file mode 100644
index 000000000..8e0fafdf3
--- /dev/null
+++ b/js/yui3/cssreset/cssreset-min.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+html{color:#000;background:#FFF}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0}table{border-collapse:collapse;border-spacing:0}fieldset,img{border:0}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal}ol,ul{list-style:none}caption,th{text-align:left}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal}q:before,q:after{content:''}abbr,acronym{border:0;font-variant:normal}sup{vertical-align:text-top}sub{vertical-align:text-bottom}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;*font-size:100%}legend{color:#000}#yui3-css-stamp.cssreset{display:none}
diff --git a/js/yui3/cssreset/cssreset.css b/js/yui3/cssreset/cssreset.css
new file mode 100644
index 000000000..24f9cb5a4
--- /dev/null
+++ b/js/yui3/cssreset/cssreset.css
@@ -0,0 +1,123 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/*
+ TODO will need to remove settings on HTML since we can't namespace it.
+ TODO with the prefix, should I group by selector or property for weight savings?
+*/
+html{
+ color:#000;
+ background:#FFF;
+}
+/*
+ TODO remove settings on BODY since we can't namespace it.
+*/
+/*
+ TODO test putting a class on HEAD.
+ - Fails on FF.
+*/
+body,
+div,
+dl,
+dt,
+dd,
+ul,
+ol,
+li,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+pre,
+code,
+form,
+fieldset,
+legend,
+input,
+textarea,
+p,
+blockquote,
+th,
+td {
+ margin:0;
+ padding:0;
+}
+table {
+ border-collapse:collapse;
+ border-spacing:0;
+}
+fieldset,
+img {
+ border:0;
+}
+/*
+ TODO think about hanlding inheritence differently, maybe letting IE6 fail a bit...
+*/
+address,
+caption,
+cite,
+code,
+dfn,
+em,
+strong,
+th,
+var {
+ font-style:normal;
+ font-weight:normal;
+}
+
+ol,
+ul {
+ list-style:none;
+}
+
+caption,
+th {
+ text-align:left;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-size:100%;
+ font-weight:normal;
+}
+q:before,
+q:after {
+ content:'';
+}
+abbr,
+acronym {
+ border:0;
+ font-variant:normal;
+}
+/* to preserve line-height and selector appearance */
+sup {
+ vertical-align:text-top;
+}
+sub {
+ vertical-align:text-bottom;
+}
+input,
+textarea,
+select {
+ font-family:inherit;
+ font-size:inherit;
+ font-weight:inherit;
+ *font-size:100%; /*to enable resizing for IE*/
+}
+/*because legend doesn't inherit in IE */
+legend {
+ color:#000;
+}
+
+/* YUI CSS Detection Stamp */
+#yui3-css-stamp.cssreset { display: none; }
diff --git a/js/yui3/dataschema-array/dataschema-array-min.js b/js/yui3/dataschema-array/dataschema-array-min.js
new file mode 100644
index 000000000..bace4c85e
--- /dev/null
+++ b/js/yui3/dataschema-array/dataschema-array-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dataschema-array",function(e,t){var n=e.Lang,r={apply:function(e,t){var i=t,s={results:[],meta:{}};return n.isArray(i)?e&&n.isArray(e.resultFields)?s=r._parseResults.call(this,e.resultFields,i,s):s.results=i:s.error=new Error("Array schema parse failure"),s},_parseResults:function(t,r,i){var s=[],o,u,a,f,l,c,h,p;for(h=r.length-1;h>-1;h--){o={},u=r[h],a=n.isObject(u)&&!n.isFunction(u)?2:n.isArray(u)?1:n.isString(u)?0:-1;if(a>0)for(p=t.length-1;p>-1;p--)f=t[p],l=n.isUndefined(f.key)?f:f.key,c=n.isUndefined(u[l])?u[p]:u[l],o[l]=e.DataSchema.Base.parse.call(this,c,f);else a===0?o=u:o=null;s[h]=o}return i.results=s,i}};e.DataSchema.Array=e.mix(r,e.DataSchema.Base)},"3.17.2",{requires:["dataschema-base"]});
diff --git a/js/yui3/dataschema-base/dataschema-base-min.js b/js/yui3/dataschema-base/dataschema-base-min.js
new file mode 100644
index 000000000..3233052f4
--- /dev/null
+++ b/js/yui3/dataschema-base/dataschema-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dataschema-base",function(e,t){var n=e.Lang,r={apply:function(e,t){return t},parse:function(t,r){if(r.parser){var i=n.isFunction(r.parser)?r.parser:e.Parsers[r.parser+""];i&&(t=i.call(this,t))}return t}};e.namespace("DataSchema").Base=r,e.namespace("Parsers")},"3.17.2",{requires:["base"]});
diff --git a/js/yui3/dataschema-json/dataschema-json-min.js b/js/yui3/dataschema-json/dataschema-json-min.js
new file mode 100644
index 000000000..57d2bebf3
--- /dev/null
+++ b/js/yui3/dataschema-json/dataschema-json-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dataschema-json",function(e,t){var n=e.Lang,r=n.isFunction,i=n.isObject,s=n.isArray,o=e.DataSchema.Base,u;u={getPath:function(e){var t=null,n=[],r=0;if(e){e=e.replace(/\[\s*(['"])(.*?)\1\s*\]/g,function(e,t,i){return n[r]=i,".@"+r++}).replace(/\[(\d+)\]/g,function(e,t){return n[r]=parseInt(t,10)|0,".@"+r++}).replace(/^\./,""),t=e.split(".");for(r=t.length-1;r>=0;--r)t[r].charAt(0)==="@"&&(t[r]=n[parseInt(t[r].substr(1),10)])}return t},getLocationValue:function(e,t){var n=0,r=e.length;for(;n<r;n++){if(!(i(t)&&e[n]in t)){t=undefined;break}t=t[e[n]]}return t},apply:function(t,n){var r=n,s={results:[],meta:{}};if(!i(n))try{r=e.JSON.parse(n)}catch(o){return s.error=o,s}return i(r)&&t?(s=u._parseResults.call(this,t,r,s),t.metaFields!==undefined&&(s=u._parseMeta(t.metaFields,r,s))):s.error=new Error("JSON schema parse failure"),s},_parseResults:function(e,t,n){var r=u.getPath,i=u.getLocationValue,o=r(e.resultListLocator),a=o?i(o,t)||t[e.resultListLocator]:t;return s(a)?s(e.resultFields)?n=u._getFieldValues.call(this,e.resultFields,a,n):n.results=a:e.resultListLocator&&(n.results=[],n.error=new Error("JSON results retrieval failure")),n},_getFieldValues:function(t,n,i){var s=[],a=t.length,f,l,c,h,p,d,v,m,g=[],y=[],b=[],w,E;for(f=0;f<a;f++)c=t[f],h=c.key||c,p=c.locator||h,d=u.getPath(p),d&&(d.length===1?g.push({key:h,path:d[0]}):y.push({key:h,path:d,locator:p})),v=r(c.parser)?c.parser:e.Parsers[c.parser+""],v&&b.push({key:h,parser:v});for(f=n.length-1;f>=0;--f){E={},w=n[f];if(w){for(l=y.length-1;l>=0;--l){d=y[l],m=u.getLocationValue(d.path,w);if(m===undefined){m=u.getLocationValue([d.locator],w);if(m!==undefined){g.push({key:d.key,path:d.locator}),y.splice(f,1);continue}}E[d.key]=o.parse.call(this,u.getLocationValue(d.path,w),d)}for(l=g.length-1;l>=0;--l)d=g[l],E[d.key]=o.parse.call(this,w[d.path]===undefined?w[l]:w[d.path],d);for(l=b.length-1;l>=0;--l)h=b[l].key,E[h]=b[l].parser.call(this,E[h]),E[h]===undefined&&(E[h]=null);s[f]=E}}return i.results=s,i},_parseMeta:function(e,t,n){if(i(e)){var r,s;for(r in e)e.hasOwnProperty(r)&&(s=u.getPath(e[r]),s&&t&&(n.meta[r]=u.getLocationValue(s,t)))}else n.error=new Error("JSON meta data retrieval failure");return n}},e.DataSchema.JSON=e.mix(u,o)},"3.17.2",{requires:["dataschema-base","json"]});
diff --git a/js/yui3/dataschema-text/dataschema-text-min.js b/js/yui3/dataschema-text/dataschema-text-min.js
new file mode 100644
index 000000000..3430f817a
--- /dev/null
+++ b/js/yui3/dataschema-text/dataschema-text-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dataschema-text",function(e,t){var n=e.Lang,r=n.isString,i=n.isUndefined,s={apply:function(e,t){var n=t,i={results:[],meta:{}};return r(t)&&e&&r(e.resultDelimiter)?i=s._parseResults.call(this,e,n,i):i.error=new Error("Text schema parse failure"),i},_parseResults:function(t,n,s){var o=t.resultDelimiter,u=r(t.fieldDelimiter)&&t.fieldDelimiter,a=t.resultFields||[],f=[],l=e.DataSchema.Base.parse,c,h,p,d,v,m,g,y,b;n.slice(-o.length)===o&&(n=n.slice(0,-o.length)),c=n.split(t.resultDelimiter);if(u)for(y=c.length-1;y>=0;--y){p={},d=c[y],h=d.split(t.fieldDelimiter);for(b=a.length-1;b>=0;--b)v=a[b],m=i(v.key)?v:v.key,g=i(h[m])?h[b]:h[m],p[m]=l.call(this,g,v);f[y]=p}else f=c;return s.results=f,s}};e.DataSchema.Text=e.mix(s,e.DataSchema.Base)},"3.17.2",{requires:["dataschema-base"]});
diff --git a/js/yui3/dataschema-xml/dataschema-xml-min.js b/js/yui3/dataschema-xml/dataschema-xml-min.js
new file mode 100644
index 000000000..2f781150c
--- /dev/null
+++ b/js/yui3/dataschema-xml/dataschema-xml-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dataschema-xml",function(e,t){var n=e.Lang,r={1:!0,9:!0,11:!0},i;i={apply:function(e,t){var n=t,s={results:[],meta:{}};return n&&r[n.nodeType]&&e?(s=i._parseResults(e,n,s),s=i._parseMeta(e.metaFields,n,s)):s.error=new Error("XML schema parse failure"),s},_getLocationValue:function(t,n){var r=t.locator||t.key||t,s=n.ownerDocument||n,o,u,a=null;try{o=i._getXPathResult(r,n,s);while(u=o.iterateNext())a=u.textContent||u.value||u.text||u.innerHTML||u.innerText||null;return e.DataSchema.Base.parse.call(this,a,t)}catch(f){}return null},_getXPathResult:function(t,r,i){if(!n.isUndefined(i.evaluate))return i.evaluate(t,r,i.createNSResolver(r.ownerDocument?r.ownerDocument.documentElement:r.documentElement),0,null);var s=[],o=t.split(/\b\/\b/),u=0,a=o.length,f,l,c,h;try{try{i.setProperty("SelectionLanguage","XPath")}catch(p){}s=r.selectNodes(t)}catch(p){for(;u<a&&r;u++){f=o[u];if(f.indexOf("[")>-1&&f.indexOf("]")>-1)l=f.slice(f.indexOf("[")+1,f.indexOf("]")),l--,r=r.children[l],h=!0;else if(f.indexOf("@")>-1)l=f.substr(f.indexOf("@")),r=l?r.getAttribute(l.replace("@","")):r;else if(-1<f.indexOf("//"))l=r.getElementsByTagName(f.substr(2)),r=l.length?l[l.length-1]:null;else if(a!=u+1)for(c=r.childNodes.length-1;0<=c;c-=1)f===r.childNodes[c].tagName&&(r=r.childNodes[c],c=-1)}r&&(n.isString(r)?s[0]={value:r}:h?s[0]={value:r.innerHTML}:s=e.Array(r.childNodes,0,!0))}return{index:0,iterateNext:function(){if(this.index>=this.values.length)return undefined;var e=this.values[this.index];return this.index+=1,e},values:s}},_parseField:function(e,t,n){var r=e.key||e,s;e.schema?(s={results:[],meta:{}},s=i._parseResults(e.schema,n,s),t[r]=s.results):t[r]=i._getLocationValue(e,n)},_parseMeta:function(e,t,r){if(n.isObject(e)){var s,o=t.ownerDocument||t;for(s in e)e.hasOwnProperty(s)&&(r.meta[s]=i._getLocationValue(e[s],o))}return r},_parseResult:function(e,t){var n={},r;for(r=e.length-1;0<=r;r--)i._parseField(e[r],n,t);return n},_parseResults:function(e,t,r){if(e.resultListLocator&&n.isArray(e.resultFields)){var s=t.ownerDocument||t,o=e.resultFields,u=[],a,f,l=0;if(e.resultListLocator.match(/^[:\-\w]+$/)){f=t.getElementsByTagName(e.resultListLocator);for(l=f.length-1;l>=0;--l)u[l]=i._parseResult(o,f[l])}else{f=i._getXPathResult(e.resultListLocator,t,s);while(a=f.iterateNext())u[l]=i._parseResult(o,a),l+=1}u.length?r.results=u:r.error=new Error("XML schema result nodes retrieval failure")}return r}},e.DataSchema.XML=e.mix(i,e.DataSchema.Base)},"3.17.2",{requires:["dataschema-base"]});
diff --git a/js/yui3/datasource-arrayschema/datasource-arrayschema-min.js b/js/yui3/datasource-arrayschema/datasource-arrayschema-min.js
new file mode 100644
index 000000000..66ea54061
--- /dev/null
+++ b/js/yui3/datasource-arrayschema/datasource-arrayschema-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datasource-arrayschema",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)};e.mix(n,{NS:"schema",NAME:"dataSourceArraySchema",ATTRS:{schema:{}}}),e.extend(n,e.Plugin.Base,{initializer:function(e){this.doBefore("_defDataFn",this._beforeDefDataFn)},_beforeDefDataFn:function(t){var n=e.DataSource.IO&&this.get("host")instanceof e.DataSource.IO&&e.Lang.isString(t.data.responseText)?t.data.responseText:t.data,r=e.DataSchema.Array.apply.call(this,this.get("schema"),n),i=t.details[0];return r||(r={meta:{},results:n}),i.response=r,this.get("host").fire("response",i),new e.Do.Halt("DataSourceArraySchema plugin halted _defDataFn")}}),e.namespace("Plugin").DataSourceArraySchema=n},"3.17.2",{requires:["datasource-local","plugin","dataschema-array"]});
diff --git a/js/yui3/datasource-cache/datasource-cache-min.js b/js/yui3/datasource-cache/datasource-cache-min.js
new file mode 100644
index 000000000..96a9095b4
--- /dev/null
+++ b/js/yui3/datasource-cache/datasource-cache-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datasource-cache",function(e,t){function r(t){var n=t&&t.cache?t.cache:e.Cache,r=e.Base.create("dataSourceCache",n,[e.Plugin.Base,e.Plugin.DataSourceCacheExtension]),i=new r(t);return r.NS="tmpClass",i}var n=function(){};e.mix(n,{NS:"cache",NAME:"dataSourceCacheExtension"}),n.prototype={initializer:function(e){this.doBefore("_defRequestFn",this._beforeDefRequestFn),this.doBefore("_defResponseFn",this._beforeDefResponseFn)},_beforeDefRequestFn:function(t){var n=this.retrieve(t.request)||null,r=t.details[0];if(n&&n.response)return r.cached=n.cached,r.response=n.response,r.data=n.data,this.get("host").fire("response",r),new e.Do.Halt("DataSourceCache extension halted _defRequestFn")},_beforeDefResponseFn:function(e){e.response&&!e.cached&&this.add(e.request,e.response)}},e.namespace("Plugin").DataSourceCacheExtension=n,e.mix(r,{NS:"cache",NAME:"dataSourceCache"}),e.namespace("Plugin").DataSourceCache=r},"3.17.2",{requires:["datasource-local","plugin","cache-base"]});
diff --git a/js/yui3/datasource-function/datasource-function-min.js b/js/yui3/datasource-function/datasource-function-min.js
new file mode 100644
index 000000000..80851cc5d
--- /dev/null
+++ b/js/yui3/datasource-function/datasource-function-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datasource-function",function(e,t){var n=e.Lang,r=function(){r.superclass.constructor.apply(this,arguments)};e.mix(r,{NAME:"dataSourceFunction",ATTRS:{source:{validator:n.isFunction}}}),e.extend(r,e.DataSource.Local,{_defRequestFn:function(e){var t=this.get("source"),n=e.details[0];if(t)try{n.data=t(e.request,this,e)}catch(r){n.error=r}else n.error=new Error("Function data failure");return this.fire("data",n),e.tId}}),e.DataSource.Function=r},"3.17.2",{requires:["datasource-local"]});
diff --git a/js/yui3/datasource-get/datasource-get-min.js b/js/yui3/datasource-get/datasource-get-min.js
new file mode 100644
index 000000000..45460e987
--- /dev/null
+++ b/js/yui3/datasource-get/datasource-get-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datasource-get",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)};e.DataSource.Get=e.extend(n,e.DataSource.Local,{_defRequestFn:function(t){var n=this.get("source"),r=this.get("get"),i=e.guid().replace(/\-/g,"_"),s=this.get("generateRequestCallback"),o=t.details[0],u=this;return this._last=i,YUI.Env.DataSource.callbacks[i]=function(n){delete YUI.Env.DataSource.callbacks[i],delete e.DataSource.Local.transactions[t.tId];var r=u.get("asyncMode")!=="ignoreStaleResponses"||u._last===i;r&&(o.data=n,u.fire("data",o))},n+=t.request+s.call(this,i),e.DataSource.Local.transactions[t.tId]=r.script(n,{autopurge:!0,onFailure:function(n){delete YUI.Env.DataSource.callbacks[i],delete e.DataSource.Local.transactions[t.tId],o.error=new Error(n.msg||"Script node data failure"),u.fire("data",o)},onTimeout:function(n){delete YUI.Env.DataSource.callbacks[i],delete e.DataSource.Local.transactions[t.tId],o.error=new Error(n.msg||"Script node data timeout"),u.fire("data",o)}}),t.tId},_generateRequest:function(e){return"&"+this.get("scriptCallbackParam")+"=YUI.Env.DataSource.callbacks."+e}},{NAME:"dataSourceGet",ATTRS:{get:{value:e.Get,cloneDefaultValue:!1},asyncMode:{value:"allowAll"},scriptCallbackParam:{value:"callback"},generateRequestCallback:{value:function(){return this._generateRequest.apply(this,arguments)}}}}),YUI.namespace("Env.DataSource.callbacks")},"3.17.2",{requires:["datasource-local","get"]});
diff --git a/js/yui3/datasource-io/datasource-io-min.js b/js/yui3/datasource-io/datasource-io-min.js
new file mode 100644
index 000000000..76afd2ae5
--- /dev/null
+++ b/js/yui3/datasource-io/datasource-io-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datasource-io",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)};e.mix(n,{NAME:"dataSourceIO",ATTRS:{io:{value:e.io,cloneDefaultValue:!1},ioConfig:{value:null}}}),e.extend(n,e.DataSource.Local,{initializer:function(e){this._queue={interval:null,conn:null,requests:[]}},successHandler:function(t,n,r){var i=this.get("ioConfig"),s=r.details[0];delete e.DataSource.Local.transactions[r.tId],s.data=n,this.fire("data",s),i&&i.on&&i.on.success&&i.on.success.apply(i.context||e,arguments)},failureHandler:function(t,n,r){var i=this.get("ioConfig"),s=r.details[0];delete e.DataSource.Local.transactions[r.tId],s.error=new Error("IO data failure"),s.data=n,this.fire("data",s),i&&i.on&&i.on.failure&&i.on.failure.apply(i.context||e,arguments)},_queue:null,_defRequestFn:function(t){var n=this.get("source"),r=this.get("io"),i=this.get("ioConfig"),s=t.request,o=e.merge(i,t.cfg,{on:e.merge(i,{success:this.successHandler,failure:this.failureHandler}),context:this,arguments:t});return e.Lang.isString(s)&&(o.method&&o.method.toUpperCase()==="POST"?o.data=o.data?o.data+s:s:n+=s),e.DataSource.Local.transactions[t.tId]=r(n,o),t.tId}}),e.DataSource.IO=n},"3.17.2",{requires:["datasource-local","io-base"]});
diff --git a/js/yui3/datasource-jsonschema/datasource-jsonschema-min.js b/js/yui3/datasource-jsonschema/datasource-jsonschema-min.js
new file mode 100644
index 000000000..cd0967a43
--- /dev/null
+++ b/js/yui3/datasource-jsonschema/datasource-jsonschema-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datasource-jsonschema",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)};e.mix(n,{NS:"schema",NAME:"dataSourceJSONSchema",ATTRS:{schema:{}}}),e.extend(n,e.Plugin.Base,{initializer:function(e){this.doBefore("_defDataFn",this._beforeDefDataFn)},_beforeDefDataFn:function(t){var n=t.data&&(t.data.responseText||t.data),r=this.get("schema"),i=t.details[0];return i.response=e.DataSchema.JSON.apply.call(this,r,n)||{meta:{},results:n},this.get("host").fire("response",i),new e.Do.Halt("DataSourceJSONSchema plugin halted _defDataFn")}}),e.namespace("Plugin").DataSourceJSONSchema=n},"3.17.2",{requires:["datasource-local","plugin","dataschema-json"]});
diff --git a/js/yui3/datasource-local/datasource-local-min.js b/js/yui3/datasource-local/datasource-local-min.js
new file mode 100644
index 000000000..a2fd7d5ea
--- /dev/null
+++ b/js/yui3/datasource-local/datasource-local-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datasource-local",function(e,t){var n=e.Lang,r=function(){r.superclass.constructor.apply(this,arguments)};e.mix(r,{NAME:"dataSourceLocal",ATTRS:{source:{value:null}},_tId:0,transactions:{},issueCallback:function(e,t){var n=e.on||e.callback,r=n&&n.success,i=e.details[0];i.error=e.error||e.response.error,i.error&&(t.fire("error",i),r=n&&n.failure),r&&r(i)}}),e.extend(r,e.Base,{initializer:function(e){this._initEvents()},_initEvents:function(){this.publish("request",{defaultFn:e.bind("_defRequestFn",this),queuable:!0}),this.publish("data",{defaultFn:e.bind("_defDataFn",this),queuable:!0}),this.publish("response",{defaultFn:e.bind("_defResponseFn",this),queuable:!0})},_defRequestFn:function(e){var t=this.get("source"),r=e.details[0];n.isUndefined(t)&&(r.error=new Error("Local source undefined")),r.data=t,this.fire("data",r)},_defDataFn:function(e){var t=e.data,r=e.meta,i={results:n.isArray(t)?t:[t],meta:r?r:{}},s=e.details[0];s.response=i,this.fire("response",s)},_defResponseFn:function(e){r.issueCallback(e,this)},sendRequest:function(e){var t=r._tId++,n;return e=e||{},n=e.on||e.callback,this.fire("request",{tId:t,request:e.request,on:n,callback:n,cfg:e.cfg||{}}),t}}),e.namespace("DataSource").Local=r},"3.17.2",{requires:["base"]});
diff --git a/js/yui3/datasource-polling/datasource-polling-min.js b/js/yui3/datasource-polling/datasource-polling-min.js
new file mode 100644
index 000000000..155613632
--- /dev/null
+++ b/js/yui3/datasource-polling/datasource-polling-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datasource-polling",function(e,t){function n(){this._intervals={}}n.prototype={_intervals:null,setInterval:function(t,n){var r=e.later(t,this,this.sendRequest,[n],!0);return this._intervals[r.id]=r,e.later(0,this,this.sendRequest,[n]),r.id},clearInterval:function(e,t){e=t||e,this._intervals[e]&&(this._intervals[e].cancel(),delete this._intervals[e])},clearAllIntervals:function(){e.each(this._intervals,this.clearInterval,this)}},e.augment(e.DataSource.Local,n)},"3.17.2",{requires:["datasource-local"]});
diff --git a/js/yui3/datasource-textschema/datasource-textschema-min.js b/js/yui3/datasource-textschema/datasource-textschema-min.js
new file mode 100644
index 000000000..c8b30ae5c
--- /dev/null
+++ b/js/yui3/datasource-textschema/datasource-textschema-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datasource-textschema",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)};e.mix(n,{NS:"schema",NAME:"dataSourceTextSchema",ATTRS:{schema:{}}}),e.extend(n,e.Plugin.Base,{initializer:function(e){this.doBefore("_defDataFn",this._beforeDefDataFn)},_beforeDefDataFn:function(t){var n=this.get("schema"),r=t.details[0],i=t.data.responseText||t.data;return r.response=e.DataSchema.Text.apply.call(this,n,i)||{meta:{},results:i},this.get("host").fire("response",r),new e.Do.Halt("DataSourceTextSchema plugin halted _defDataFn")}}),e.namespace("Plugin").DataSourceTextSchema=n},"3.17.2",{requires:["datasource-local","plugin","dataschema-text"]});
diff --git a/js/yui3/datasource-xmlschema/datasource-xmlschema-min.js b/js/yui3/datasource-xmlschema/datasource-xmlschema-min.js
new file mode 100644
index 000000000..f843e96f2
--- /dev/null
+++ b/js/yui3/datasource-xmlschema/datasource-xmlschema-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datasource-xmlschema",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)};e.mix(n,{NS:"schema",NAME:"dataSourceXMLSchema",ATTRS:{schema:{}}}),e.extend(n,e.Plugin.Base,{initializer:function(e){this.doBefore("_defDataFn",this._beforeDefDataFn)},_beforeDefDataFn:function(t){var n=this.get("schema"),r=t.details[0],i=e.XML.parse(t.data.responseText)||t.data;return r.response=e.DataSchema.XML.apply.call(this,n,i)||{meta:{},results:i},this.get("host").fire("response",r),new e.Do.Halt("DataSourceXMLSchema plugin halted _defDataFn")}}),e.namespace("Plugin").DataSourceXMLSchema=n},"3.17.2",{requires:["datasource-local","plugin","datatype-xml","dataschema-xml"]});
diff --git a/js/yui3/datatable-base-deprecated/assets/skins/sam/dt-arrow-dn.png b/js/yui3/datatable-base-deprecated/assets/skins/sam/dt-arrow-dn.png
new file mode 100644
index 000000000..9c42b8331
--- /dev/null
+++ b/js/yui3/datatable-base-deprecated/assets/skins/sam/dt-arrow-dn.png
Binary files differ
diff --git a/js/yui3/datatable-base-deprecated/assets/skins/sam/dt-arrow-up.png b/js/yui3/datatable-base-deprecated/assets/skins/sam/dt-arrow-up.png
new file mode 100644
index 000000000..07e237512
--- /dev/null
+++ b/js/yui3/datatable-base-deprecated/assets/skins/sam/dt-arrow-up.png
Binary files differ
diff --git a/js/yui3/datatable-base/assets/datatable-base-core.css b/js/yui3/datatable-base/assets/datatable-base-core.css
new file mode 100644
index 000000000..a0a28b92d
--- /dev/null
+++ b/js/yui3/datatable-base/assets/datatable-base-core.css
@@ -0,0 +1,11 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* foundational CSS */
+.yui3-datatable-table {
+ empty-cells: show;
+}
diff --git a/js/yui3/datatable-base/assets/skins/night/datatable-base.css b/js/yui3/datatable-base/assets/skins/night/datatable-base.css
new file mode 100644
index 000000000..5808f35d2
--- /dev/null
+++ b/js/yui3/datatable-base/assets/skins/night/datatable-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-table{empty-cells:show}.yui3-skin-night .yui3-datatable{color:#8e8e8e;font-family:HelveticaNeue,arial,helvetica,clean,sans-serif}.yui3-skin-night .yui3-datatable-table{border:1px solid #323434;border-collapse:separate;border-spacing:0;color:#8e8e8e;margin:0;padding:0}.yui3-skin-night .yui3-datatable-caption{color:#474747;font:italic 85%/1 HelveticaNeue,arial,helvetica,clean,sans-serif;padding:1em 0;text-align:center}.yui3-skin-night .yui3-datatable-cell,.yui3-skin-night .yui3-datatable-header{border-left:1px solid #303030;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:4px 10px 4px 10px}.yui3-skin-night .yui3-datatable-cell:first-child,.yui3-skin-night .yui3-datatable-first-header{border-left-width:0}.yui3-skin-night .yui3-datatable-header{background-color:#3b3c3d;background:-moz-linear-gradient(0% 100% 90deg,#242526 0,#3b3c3d 96%,#2c2d2f 100%);background:-webkit-gradient(linear,left bottom,left top,from(#242526),color-stop(0.96,#3b3c3d),to(#2c2d2f));color:#eee;font-weight:normal;text-align:left;vertical-align:bottom;white-space:nowrap}.yui3-skin-night .yui3-datatable-cell{background-color:transparent}.yui3-skin-night .yui3-datatable-even .yui3-datatable-cell{background-color:#0e0e0e}.yui3-skin-night .yui3-datatable-odd .yui3-datatable-cell{background-color:#1d1e1e}#yui3-css-stamp.skin-night-datatable-base{display:none}
diff --git a/js/yui3/datatable-base/assets/skins/sam/datatable-base.css b/js/yui3/datatable-base/assets/skins/sam/datatable-base.css
new file mode 100644
index 000000000..8ce156884
--- /dev/null
+++ b/js/yui3/datatable-base/assets/skins/sam/datatable-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-table{empty-cells:show}.yui3-skin-sam .yui3-datatable-table{margin:0;padding:0;font-family:arial,sans-serif;border-collapse:separate;border-spacing:0;border:1px solid #cbcbcb}.yui3-skin-sam .yui3-datatable-caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.yui3-skin-sam .yui3-datatable-cell,.yui3-skin-sam .yui3-datatable-header{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:4px 10px 4px 10px}.yui3-skin-sam .yui3-datatable-cell:first-child,.yui3-skin-sam .yui3-datatable-first-header{border-left-width:0}.yui3-skin-sam .yui3-datatable-header{background:#fff url(../../../../assets/skins/sam/sprite.png) repeat-x 0 0;background-image:-webkit-linear-gradient(transparent 40%,rgba(0,0,0,0.21));background-image:-moz-linear-gradient(top,transparent 40%,rgba(0,0,0,0.21));background-image:-ms-linear-gradient(transparent 40%,rgba(0,0,0,0.21));background-image:-o-linear-gradient(transparent 40%,rgba(0,0,0,0.21));background-image:linear-gradient(transparent 40%,rgba(0,0,0,0.21));color:#000;font-weight:normal;text-align:left;text-shadow:0 1px 1px #fff;vertical-align:bottom;white-space:nowrap}.yui3-skin-sam .yui3-datatable-cell{background-color:transparent}.yui3-skin-sam .yui3-datatable-even .yui3-datatable-cell{background-color:#fff}.yui3-skin-sam .yui3-datatable-odd .yui3-datatable-cell{background-color:#edf5ff}#yui3-css-stamp.skin-sam-datatable-base{display:none}
diff --git a/js/yui3/datatable-base/datatable-base-min.js b/js/yui3/datatable-base/datatable-base-min.js
new file mode 100644
index 000000000..5ef317981
--- /dev/null
+++ b/js/yui3/datatable-base/datatable-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatable-base",function(e,t){e.DataTable.Base=e.Base.create("datatable",e.Widget,[e.DataTable.Core],{delegate:function(){var e=this.get("contentBox");return e.delegate.apply(e,arguments)},destructor:function(){this.view&&this.view.destroy()},getCell:function(){return this.view&&this.view.getCell&&this.view.getCell.apply(this.view,arguments)},getRow:function(){return this.view&&this.view.getRow&&this.view.getRow.apply(this.view,arguments)},_afterDisplayColumnsChange:function(e){this._extractDisplayColumns(e.newVal||[])},bindUI:function(){this._eventHandles.relayCoreChanges=this.after(["columnsChange","dataChange","summaryChange","captionChange","widthChange"],e.bind("_relayCoreAttrChange",this))},_defRenderViewFn:function(e){e.view.render()},_extractDisplayColumns:function(t){function r(t){var i,s,o;for(i=0,s=t.length;i<s;++i)o=t[i],e.Lang.isArray(o.children)?r(o.children):n.push(o)}var n=[];r(t),this._displayColumns=n},initializer:function(){this.publish("renderView",{defaultFn:e.bind("_defRenderViewFn",this)}),this._extractDisplayColumns(this.get("columns")||[]),this.after("columnsChange",e.bind("_afterDisplayColumnsChange",this))},_relayCoreAttrChange:function(e){var t=e.attrName==="data"?"modelList":e.attrName;this.view.set(t,e.newVal)},renderUI:function(){var t=this,n=this.get("view");n&&(this.view=new n(e.merge(this.getAttrs(),{host:this,container:this.get("contentBox"),modelList:this.data},this.get("viewConfig"))),this._eventHandles.legacyFeatureProps||(this._eventHandles.legacyFeatureProps=this.view.after({renderHeader:function(e){t.head=e.view,t._theadNode=e.view.theadNode,t._tableNode=e.view.get("container")},renderFooter:function(e){t.foot=e.view,t._tfootNode=e.view.tfootNode,t._tableNode=e.view.get("container")},renderBody:function(e){t.body=e.view,t._tbodyNode=e.view.tbodyNode,t._tableNode=e.view.get("container")},renderTable:function(){var e=this.get("container");t._tableNode=this.tableNode||e.one("."+this.getClassName("table")+", table"),t._captionNode=this.captionNode||e.one("caption"),t._theadNode||(t._theadNode=e.one("."+this.getClassName("columns")+", thead")),t._tbodyNode||(t._tbodyNode=e.one("."+this.getClassName("data")+", tbody")),t._tfootNode||(t._tfootNode=e.one("."+this.getClassName("footer")+", tfoot"))}})),this.view.addTarget(this))},syncUI:function(){this.view&&this.fire("renderView",{view:this.view})},_validateView:function(t){return t===null||e.Lang.isFunction(t)&&t.prototype.render}},{ATTRS:{view:{value:e.DataTable.TableView,validator:"_validateView"},viewConfig:{}}}),e.DataTable=e.mix(e.Base.create("datatable",e.DataTable.Base,[]),e.DataTable)},"3.17.2",{requires:["datatable-core","datatable-table","datatable-head","datatable-body","base-build","widget"],skinnable:!0});
diff --git a/js/yui3/datatable-body/datatable-body-min.js b/js/yui3/datatable-body/datatable-body-min.js
new file mode 100644
index 000000000..760a909b5
--- /dev/null
+++ b/js/yui3/datatable-body/datatable-body-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatable-body",function(e,t){var n=e.Lang,r=n.isArray,i=n.isNumber,s=n.isString,o=n.sub,u=e.Escape.html,a=e.Array,f=e.bind,l=e.Object,c=/\{value\}/g,h="contentUpdate",p={above:[-1,0],below:[1,0],next:[0,1],prev:[0,-1],previous:[0,-1]};e.namespace("DataTable").BodyView=e.Base.create("tableBody",e.View,[],{CELL_TEMPLATE:'<td {headers} class="{className}">{content}</td>',ROW_TEMPLATE:'<tr id="{rowId}" data-yui3-record="{clientId}" class="{rowClass}">{content}</tr>',TBODY_TEMPLATE:'<tbody class="{className}"></tbody>',getCell:function(t,n){var i=this.tbodyNode,o,u,a,f;return t&&i&&(r(t)?(o=i.get("children").item(t[0]),u=o&&o.get("children").item(t[1])):t._node&&(u=t.ancestor("."+this.getClassName("cell"),!0)),u&&n&&(f=i.get("firstChild.rowIndex"),s(n)&&(p[n]||e.error("Unrecognized shift: "+n,null,"datatable-body"),n=p[n]),r(n)&&(a=u.get("parentNode.rowIndex")+n[0]-f,o=i.get("children").item(a),a=u.get("cellIndex")+n[1],u=o&&o.get("children").item(a)))),u||null},getClassName:function(){var t=this.host,n;return t&&t.getClassName?t.getClassName.apply(t,arguments):(n=a(arguments),n.unshift(this.constructor.NAME),e.ClassNameManager.getClassName.apply(e.ClassNameManager,n))},getRecord:function(e){var t=this.get("modelList"),n=this.tbodyNode,r=null,i;return n&&(s(e)&&(e=n.one("#"+e)),e&&e._node&&(r=e.ancestor(function(e){return e.get("parentNode").compareTo(n)},!0),i=r&&t.getByClientId(r.getData("yui3-record")))),i||null},getRow:function(e){var t=this.tbodyNode,n=null;return t&&(e&&(e=this._idMap[e.get?e.get("clientId"):e]||e),n=i(e)?t.get("children").item(e):t.one("#"+e)),n},render:function(){var e=this.get("container"),t=this.get("modelList"),n=this.get("columns"),r=this.tbodyNode||(this.tbodyNode=this._createTBodyNode());return this._createRowTemplate(n),t&&(r.setHTML(this._createDataHTML(n)),this._applyNodeFormatters(r,n)),r.get("parentNode")!==e&&e.appendChild(r),this.bindUI(),this},refreshRow:function(e,t,n){var r,i,s=n.length,o;for(o=0;o<s;o++)r=this.getColumn(n[o]),r!==null&&(i=e.one("."+this.getClassName("col",r._id||r.key)),this.refreshCell(i,t));return this},refreshCell:function(t,n,r){var i,s,o,u=n.toJSON();t=this.getCell(t),n||(n=this.getRecord(t)),r||(r=this.getColumn(t));if(r.nodeFormatter)o={cell:t.one("."+this.getClassName("liner"))||t,column:r,data:u,record:n,rowIndex:this._getRowIndex(t.ancestor("tr")),td:t,value:u[r.key]},keep=r.nodeFormatter.call(host,o),keep===!1&&t.destroy(!0);else if(r.formatter){r._formatterFn||(r=this._setColumnsFormatterFn([r])[0]),s=r._formatterFn||null,s&&(o={value:u[r.key],data:u,column:r,record:n,className:"",rowClass:"",rowIndex:this._getRowIndex(t.ancestor("tr"))},i=s.call(this.get("host"),o),i===undefined&&(i=o.value));if(i===undefined||i===null||i==="")i=r.emptyCellValue||""}else i=u[r.key]||r.emptyCellValue||"";return t.setHTML(r.allowHTML?i:e.Escape.html(i)),this},getColumn:function(t){t&&t._node&&(t=t.get("className").match(new RegExp(this.getClassName("col")+"-([^ ]*)"))[1]);if(this.host)return this.host._columnMap[t]||null;var n=this.get("columns"),r=null;return e.Array.some(n,function(e){if((e._id||e.key)===t)return r=e,!0}),r},_afterColumnsChange:function(){this.render()},_afterDataChange:function(t){var n=(t.type.match(/:(add|change|remove)$/)||[])[1],r=t.index,i=this.get("columns"),s,o=t.changed&&e.Object.keys(t.changed),u,a,f,l;for(f=0,l=i.length;f<l;f++){s=i[f];if(s.hasOwnProperty("nodeFormatter")){this.render(),this.fire(h);return}}switch(n){case"change":for(f=0,l=i.length;f<l;f++)s=i[f],u=s.key,s.formatter&&!t.changed[u]&&o.push(u);this.refreshRow(this.getRow(t.target),t.target,o);break;case"add":r=Math.min(r,this.get("modelList").size()-1),this._setColumnsFormatterFn(i),a=e.Node.create(this._createRowHTML(t.model,r,i)),this.tbodyNode.insert(a,r),this._restripe(r);break;case"remove":this.getRow(r).remove(!0),this._restripe(r-1);break;default:this.render()}this.fire(h)},_restripe:function(e){var t=this._restripeTask,n;e=Math.max(e|0,0),t?t.index=Math.min(t.index,e):(n=this,this._restripeTask={timer:setTimeout(function(){if(!n||n.get("destroy")||!n.tbodyNode||!n.tbodyNode.inDoc()){n._restripeTask=null;return}var e=[n.CLASS_ODD,n.CLASS_EVEN],t=[n.CLASS_EVEN,n.CLASS_ODD],r=n._restripeTask.index;n.tbodyNode.get("childNodes").slice(r).each(function(n,i){n.replaceClass.apply(n,(r+i)%2?t:e)}),n._restripeTask=null},0),index:e})},_afterModelListChange:function(){var e=this._eventHandles;e.dataChange&&(e.dataChange.detach(),delete e.dataChange,this.bindUI()),this.tbodyNode&&this.render()},_applyNodeFormatters:function(e,t){var n=this.host||this,r=this.get("modelList"),i=[],s="."+this.getClassName("liner"),o,u,a;for(u=0,a=t.length;u<a;++u)t[u].nodeFormatter&&i.push(u);r&&i.length&&(o=e.get("childNodes"),r.each(function(e,r){var u={data:e.toJSON(),record:e,rowIndex:r},a=o.item(r),f,l,c,h,p,d,v;if(a){p=a.get("childNodes");for(f=0,l=i.length;f<l;++f)d=p.item(i[f]),d&&(c=u.column=t[i[f]],h=c.key||c.id,u.value=e.get(h),u.td=d,u.cell=d.one(s)||d,v=c.nodeFormatter.call(n,u),v===!1&&d.destroy(!0))}}))},bindUI:function(){var e=this._eventHandles,t=this.get("modelList"),n=t.model.NAME+":change";e.columnsChange||(e.columnsChange=this.after("columnsChange",f("_afterColumnsChange",this))),t&&!e.dataChange&&(e.dataChange=t.after(["add","remove","reset",n],f("_afterDataChange",this)))},_createDataHTML:function(e){var t=this.get("modelList"),n="";return t&&t.each(function(t,r){n+=this._createRowHTML(t,r,e)},this),n},_createRowHTML:function(e,t,n){var r=e.toJSON(),i=e.get("clientId"),s={rowId:this._getRowId(i),clientId:i,rowClass:t%2?this.CLASS_ODD:this.CLASS_EVEN},a=this.host||this,f,l,c,h,p,d;for(f=0,l=n.length;f<l;++f){c=n[f],p=r[c.key],h=c._id||c.key,s[h+"-className"]="",c._formatterFn&&(d={value:p,data:r,column:c,record:e,className:"",rowClass:"",rowIndex:t},p=c._formatterFn.call(a,d),p===undefined&&(p=d.value),s[h+"-className"]=d.className,s.rowClass+=" "+d.rowClass);if(!s.hasOwnProperty(h)||r.hasOwnProperty(c.key)){if(p===undefined||p===null||p===""
+)p=c.emptyCellValue||"";s[h]=c.allowHTML?p:u(p)}}return s.rowClass=s.rowClass.replace(/\s+/g," "),o(this._rowTemplate,s)},_getRowIndex:function(e){var t=this.tbodyNode,n=1;if(t&&e){if(e.ancestor("tbody")!==t)return null;while(e=e.previous())n++}return n},_createRowTemplate:function(e){var t="",n=this.CELL_TEMPLATE,r,i,s,u,a,f,l,h;this._setColumnsFormatterFn(e);for(r=0,i=e.length;r<i;++r)s=e[r],u=s.key,a=s._id||u,h=s._formatterFn,f=(s._headers||[]).length>1?'headers="'+s._headers.join(" ")+'"':"",l={content:"{"+a+"}",headers:f,className:this.getClassName("col",a)+" "+(s.className||"")+" "+this.getClassName("cell")+" {"+a+"-className}"},!h&&s.formatter&&(l.content=s.formatter.replace(c,l.content)),s.nodeFormatter&&(l.content=""),t+=o(s.cellTemplate||n,l);this._rowTemplate=o(this.ROW_TEMPLATE,{content:t})},_setColumnsFormatterFn:function(t){var r=e.DataTable.BodyView.Formatters,i,s,o,u;for(o=0,u=t.length;o<u;o++)s=t[o],i=s.formatter,!s._formatterFn&&i&&(n.isFunction(i)?s._formatterFn=i:i in r&&(s._formatterFn=r[i].call(this.host||this,s)));return t},_createTBodyNode:function(){return e.Node.create(o(this.TBODY_TEMPLATE,{className:this.getClassName("data")}))},destructor:function(){(new e.EventHandle(l.values(this._eventHandles))).detach()},_getRowId:function(t){return this._idMap[t]||(this._idMap[t]=e.guid())},initializer:function(e){this.host=e.host,this._eventHandles={modelListChange:this.after("modelListChange",f("_afterModelListChange",this))},this._idMap={},this.CLASS_ODD=this.getClassName("odd"),this.CLASS_EVEN=this.getClassName("even")}},{Formatters:{}})},"3.17.2",{requires:["datatable-core","view","classnamemanager"]});
diff --git a/js/yui3/datatable-column-widths/datatable-column-widths-min.js b/js/yui3/datatable-column-widths/datatable-column-widths-min.js
new file mode 100644
index 000000000..2e9f6c69e
--- /dev/null
+++ b/js/yui3/datatable-column-widths/datatable-column-widths-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatable-column-widths",function(e,t){function i(){}var n=e.Lang.isNumber,r=e.Array.indexOf;e.Features.add("table","badColWidth",{test:function(){var t=e.one("body"),n,r;return t&&(n=t.insertBefore('<table style="position:absolute;visibility:hidden;border:0 none"><colgroup><col style="width:9px"></colgroup><tbody><tr><td style="padding:0 4px;font:normal 2px/2px arial;border:0 none">.</td></tr></tbody></table>',t.get("firstChild")),r=n.one("td").getComputedStyle("width")!=="1px",n.remove(!0)),r}}),e.mix(i.prototype,{COL_TEMPLATE:"<col/>",COLGROUP_TEMPLATE:"<colgroup/>",setColumnWidth:function(e,t){var i=this.getColumn(e),s=i&&r(this._displayColumns,i);return s>-1&&(n(t)&&(t+="px"),i.width=t,this._setColumnWidth(s,t)),this},_createColumnGroup:function(){return e.Node.create(this.COLGROUP_TEMPLATE)},initializer:function(){this.after(["renderView","columnsChange"],this._uiSetColumnWidths)},_setColumnWidth:function(t,r){var i=this._colgroupNode,s=i&&i.all("col").item(t),o,u;s&&(r&&n(r)&&(r+="px"),s.setStyle("width",r),r&&e.Features.test("table","badColWidth")&&(o=this.getCell([0,t]),o&&(u=function(e){return parseInt(o.getComputedStyle(e),10)||0},s.setStyle("width",parseInt(r,10)-u("paddingLeft")-u("paddingRight")-u("borderLeftWidth")-u("borderRightWidth")+"px"))))},_uiSetColumnWidths:function(){if(!this.view)return;var e=this.COL_TEMPLATE,t=this._colgroupNode,n=this._displayColumns,r,i;t?t.empty():(t=this._colgroupNode=this._createColumnGroup(),this._tableNode.insertBefore(t,this._tableNode.one("> thead, > tfoot, > tbody")));for(r=0,i=n.length;r<i;++r)t.append(e),this._setColumnWidth(r,n[r].width)}},!0),e.DataTable.ColumnWidths=i,e.Base.mix(e.DataTable,[i])},"3.17.2",{requires:["datatable-base"]});
diff --git a/js/yui3/datatable-core/datatable-core-min.js b/js/yui3/datatable-core/datatable-core-min.js
new file mode 100644
index 000000000..52d3c2e4b
--- /dev/null
+++ b/js/yui3/datatable-core/datatable-core-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatable-core",function(e,t){var n=e.Attribute.INVALID_VALUE,r=e.Lang,i=r.isFunction,s=r.isObject,o=r.isArray,u=r.isString,a=r.isNumber,f=e.Array,l=e.Object.keys,c;c=e.namespace("DataTable").Core=function(){},c.ATTRS={columns:{validator:o,setter:"_setColumns",getter:"_getColumns"},recordType:{getter:"_getRecordType",setter:"_setRecordType"},data:{valueFn:"_initData",setter:"_setData",lazyAdd:!1},recordset:{setter:"_setRecordset",getter:"_getRecordset",lazyAdd:!1},columnset:{setter:"_setColumnset",getter:"_getColumnset",lazyAdd:!1}},e.mix(c.prototype,{getColumn:function(e){var t,n,r,i,u;s(e)&&!o(e)?e&&e._node?t=this.body.getColumn(e):t=e:t=this.get("columns."+e);if(t)return t;n=this.get("columns");if(a(e)||o(e)){e=f(e),u=n;for(r=0,i=e.length-1;u&&r<i;++r)u=u[e[r]]&&u[e[r]].children;return u&&u[e[r]]||null}return null},getRecord:function(e){var t=this.data.getById(e)||this.data.getByClientId(e);return t||(a(e)&&(t=this.data.item(e)),!t&&this.view&&this.view.getRecord&&(t=this.view.getRecord.apply(this.view,arguments))),t||null},_allowAdHocAttrs:!0,_afterColumnsChange:function(e){this._setColumnMap(e.newVal)},_afterDataChange:function(e){var t=e.newVal;this.data=e.newVal,!this.get("columns")&&t.size()&&this._initColumns()},_afterRecordTypeChange:function(e){var t=this.data.toJSON();this.data.model=e.newVal,this.data.reset(t),!this.get("columns")&&t&&(t.length?this._initColumns():this.set("columns",l(e.newVal.ATTRS)))},_createRecordClass:function(t){var n,r,i;if(o(t)){n={};for(r=0,i=t.length;r<i;++r)n[t[r]]={}}else s(t)&&(n=t);return e.Base.create("record",e.Model,[],null,{ATTRS:n})},destructor:function(){(new e.EventHandle(e.Object.values(this._eventHandles))).detach()},_getColumns:function(e,t){return t.length>8?this._columnMap:e},_getColumnset:function(e,t){return this.get(t.replace(/^columnset/,"columns"))},_getRecordType:function(e){return e||this.data&&this.data.model},_initColumns:function(){var e=this.get("columns")||[],t;!e.length&&this.data.size()&&(t=this.data.item(0),t.toJSON&&(t=t.toJSON()),this.set("columns",l(t))),this._setColumnMap(e)},_initCoreEvents:function(){this._eventHandles.coreAttrChanges=this.after({columnsChange:e.bind("_afterColumnsChange",this),recordTypeChange:e.bind("_afterRecordTypeChange",this),dataChange:e.bind("_afterDataChange",this)})},_initData:function(){var t=this.get("recordType"),n=new e.ModelList;return t&&(n.model=t),n},_initDataProperty:function(t){var n;this.data||(n=this.get("recordType"),t&&t.each&&t.toJSON?(this.data=t,n&&(this.data.model=n)):(this.data=new e.ModelList,n&&(this.data.model=n)),this.data.addTarget(this))},initializer:function(e){var t=e.data,n=e.columns,r;this._initDataProperty(t),n||(r=(e.recordType||e.data===this.data)&&this.get("recordType"),r?n=l(r.ATTRS):o(t)&&t.length&&(n=l(t[0])),n&&this.set("columns",n)),this._initColumns(),this._eventHandles={},this._initCoreEvents()},_setColumnMap:function(e){function n(e){var r,i,s,o;for(r=0,i=e.length;r<i;++r)s=e[r],o=s.key,o&&!t[o]&&(t[o]=s),t[s._id]=s,s.children&&n(s.children)}var t={};n(e),this._columnMap=t},_setColumns:function(t){function f(e){var t={},n,u,l;r.push(e),i.push(t);for(n in e)e.hasOwnProperty(n)&&(u=e[n],o(u)?t[n]=u.slice():s(u,!0)?(l=a(r,u),t[n]=l===-1?f(u):i[l]):t[n]=e[n]);return t}function l(e){return e=e.replace(/\s+/,"-"),n[e]?e+=n[e]++:n[e]=1,e}function c(t,n){var r=[],i,s,a,h;for(i=0,s=t.length;i<s;++i)r[i]=a=u(t[i])?{key:t[i]}:f(t[i]),h=e.stamp(a),a.id||(a.id=h),a.field&&(a.name=a.field),n?a._parent=n:delete a._parent,a._id=l(a.name||a.key||a.id),o(a.children)&&(a.children=c(a.children,a));return r}var n={},r=[],i=[],a=e.Array.indexOf;return t&&c(t)},_setColumnset:function(e){return this.set("columns",e),o(e)?e:n},_setData:function(e){e===null&&(e=[]);if(o(e))this._initDataProperty(),this.data.reset(e,{silent:!0}),e=this.data;else if(!e||!e.each||!e.toJSON)e=n;return e},_setRecordset:function(t){var n;return t&&e.Recordset&&t instanceof e.Recordset&&(n=[],t.each(function(e){n.push(e.get("data"))}),t=n),this.set("data",t),t},_setRecordType:function(e){var t;return i(e)&&e.prototype.toJSON&&e.prototype.setAttrs?t=e:s(e)&&(t=this._createRecordClass(e)),t||n}})},"3.17.2",{requires:["escape","model-list","node-event-delegate"]});
diff --git a/js/yui3/datatable-datasource/datatable-datasource-min.js b/js/yui3/datatable-datasource/datatable-datasource-min.js
new file mode 100644
index 000000000..e8fe7c7c0
--- /dev/null
+++ b/js/yui3/datatable-datasource/datatable-datasource-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatable-datasource",function(e,t){function n(){n.superclass.constructor.apply(this,arguments)}e.mix(n,{NS:"datasource",NAME:"dataTableDataSource",ATTRS:{datasource:{setter:"_setDataSource"},initialRequest:{setter:"_setInitialRequest"}}}),e.extend(n,e.Plugin.Base,{_setDataSource:function(t){return t||new e.DataSource.Local(t)},_setInitialRequest:function(){},initializer:function(t){e.Lang.isUndefined(t.initialRequest)||this.load({request:t.initialRequest})},load:function(t){t=t||{},t.request=t.request||this.get("initialRequest"),t.callback=t.callback||{success:e.bind(this.onDataReturnInitializeTable,this),failure:e.bind(this.onDataReturnInitializeTable,this),argument:this.get("host").get("state")};var n=t.datasource||this.get("datasource");n&&n.sendRequest(t)},onDataReturnInitializeTable:function(e){var t=e.response&&e.response.results||[];this.get("host").set("data",t)}}),e.namespace("Plugin").DataTableDataSource=n},"3.17.2",{requires:["datatable-base","plugin","datasource-local"]});
diff --git a/js/yui3/datatable-foot/datatable-foot-min.js b/js/yui3/datatable-foot/datatable-foot-min.js
new file mode 100644
index 000000000..98ef7fac8
--- /dev/null
+++ b/js/yui3/datatable-foot/datatable-foot-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatable-foot",function(e,t){e.namespace("DataTable").FooterView=e.Base.create("tableFooter",e.View,[],{TFOOT_TEMPLATE:'<tfoot class="{className}"/>',getClassName:function(){var t=this.host,n=t&&t.constructor.NAME||this.constructor.NAME;return t&&t.getClassName?t.getClassName.apply(t,arguments):e.ClassNameManager.getClassName.apply(e.ClassNameManager,[n].concat(e.Array(arguments,0,!0)))},render:function(){var e=this.tfootNode||(this.tfootNode=this._createTFootNode());return this.host&&this.host._theadNode&&this.host._theadNode.insert(e,"after"),this},_createTFootNode:function(){return e.Node.create(e.Lang.sub(this.TFOOT_TEMPLATE,{className:this.getClassName("foot")}))},initializer:function(e){this.host=e&&e.host}})},"3.17.2",{requires:["datatable-core","view"]});
diff --git a/js/yui3/datatable-formatters/datatable-formatters-min.js b/js/yui3/datatable-formatters/datatable-formatters-min.js
new file mode 100644
index 000000000..6ffc87fb4
--- /dev/null
+++ b/js/yui3/datatable-formatters/datatable-formatters-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatable-formatters",function(e,t){var n=e.Lang,r=n.isValue,i=e.Escape.html,s=e.ClassNameManager.getClassName,o=function(e){return s("datatable",e)},u=function(e,t){return r(e)?i(e.toString()):t||""},a={button:function(e){var t=o("button"),n="<button>"+(e.buttonLabel||"Click")+"</button>";return e.allowHTML=!0,function(e){return e.className=t,n}},"boolean":function(e){var t=e.booleanLabels||this.get("booleanLabels")||{"true":"true","false":"false"};return function(e){var n=e.value;return!n&&n!==!1?n:(n=n?"true":"false",e.className=o(n),t[n])}},currency:function(t){var n=o("currency"),r=t.currencyFormat||this.get("currencyFormat"),i=e.Number.format;return function(e){e.className=n;var t=parseFloat(e.value);return!t&&t!==0?e.value:i(t,r)}},_date:function(t){var n=o("date"),r=e.Date.format;return t={format:t},function(e){return e.className=n,r(e.value,t)}},date:function(e){return a._date(e.dateFormat||this.get("dateFormat"))},localDate:function(){return a._date("%x")},localTime:function(){return a._date("%X")},localDateTime:function(){return a._date("%c")},email:function(e){var t=o("email"),n=e.linkFrom,r=e.emptyCellValue,i=(this.getColumn(n)||{}).emptyCellValue;return e.allowHTML=!0,function(e){var s=u(e.value,r),o=n?u(e.data[n],i):s;return e.className=t,o?'<a href="mailto:'+o+'">'+s+"</a>":s}},link:function(e){var t=o("link"),n=e.linkFrom,r=e.emptyCellValue,i=(this.getColumn(n)||{}).emptyCellValue;return e.allowHTML=!0,function(e){var s=u(e.value,r),o=n?u(e.data[n],i):s;return e.className=t,o?'<a href="'+o+'">'+s+"</a>":s}},number:function(t){var n=o("number"),r=t.numberFormat||this.get("numberFormat"),i=e.Number.format;return function(e){e.className=n;var t=parseFloat(e.value);return!t&&t!==0?e.value:i(t,r)}},lookup:function(e){var t=o("lookup"),r=e.lookupTable||{},i,s,u;if(n.isArray(r)){i=r,r={};for(s=0,u=i.length;s<u;++s)r[i[s].value]=i[s].text}return function(e){return e.className=t,r[e.value]}}};e.mix(e.DataTable.BodyView.Formatters,a)},"3.17.2",{requires:["datatable-body","datatype-number-format","datatype-date-format","escape"]});
diff --git a/js/yui3/datatable-head/datatable-head-min.js b/js/yui3/datatable-head/datatable-head-min.js
new file mode 100644
index 000000000..b9bbfc6e5
--- /dev/null
+++ b/js/yui3/datatable-head/datatable-head-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatable-head",function(e,t){var n=e.Lang,r=n.sub,i=n.isArray,s=e.Array;e.namespace("DataTable").HeaderView=e.Base.create("tableHeader",e.View,[],{CELL_TEMPLATE:'<th id="{id}" colspan="{_colspan}" rowspan="{_rowspan}" class="{className}" scope="col" {_id}{abbr}{title}>{content}</th>',ROW_TEMPLATE:"<tr>{content}</tr>",THEAD_TEMPLATE:'<thead class="{className}"></thead>',getClassName:function(){var t=this.host,n=t&&t.constructor.NAME||this.constructor.NAME;return t&&t.getClassName?t.getClassName.apply(t,arguments):e.ClassNameManager.getClassName.apply(e.ClassNameManager,[n].concat(s(arguments,0,!0)))},render:function(){var t=this.get("container"),n=this.theadNode||(this.theadNode=this._createTHeadNode()),i=this.columns,s={_colspan:1,_rowspan:1,abbr:"",title:""},o,u,a,f,l,c,h,p;if(n&&i){c="";if(i.length)for(o=0,u=i.length;o<u;++o){h="";for(a=0,f=i[o].length;a<f;++a)l=i[o][a],p=e.merge(s,l,{className:this.getClassName("header"),content:l.label||l.key||"Column "+(a+1)}),p._id=l._id?' data-yui3-col-id="'+l._id+'"':"",l.abbr&&(p.abbr=' abbr="'+l.abbr+'"'),l.title&&(p.title=' title="'+l.title+'"'),l.className&&(p.className+=" "+l.className),l._first&&(p.className+=" "+this.getClassName("first","header")),l._id&&(p.className+=" "+this.getClassName("col",l._id)),h+=r(l.headerTemplate||this.CELL_TEMPLATE,p);c+=r(this.ROW_TEMPLATE,{content:h})}n.setHTML(c),n.get("parentNode")!==t&&t.insertBefore(n,t.one("tfoot, tbody"))}return this.bindUI(),this},_afterColumnsChange:function(e){this.columns=this._parseColumns(e.newVal),this.render()},bindUI:function(){this._eventHandles.columnsChange||(this._eventHandles.columnsChange=this.after("columnsChange",e.bind("_afterColumnsChange",this)))},_createTHeadNode:function(){return e.Node.create(r(this.THEAD_TEMPLATE,{className:this.getClassName("columns")}))},destructor:function(){(new e.EventHandle(e.Object.values(this._eventHandles))).detach()},initializer:function(e){this.host=e.host,this.columns=this._parseColumns(e.columns),this._eventHandles=[]},_parseColumns:function(t){var n=[],r=[],s=1,o,u,a,f,l,c,h,p;if(i(t)&&t.length){t=t.slice(),r.push([t,-1]);while(r.length){o=r[r.length-1],u=o[0],c=o[1]+1;for(h=u.length;c<h;++c){u[c]=a=e.merge(u[c]),f=a.children,e.stamp(a),a.id||(a.id=e.guid());if(i(f)&&f.length){r.push([f,-1]),o[1]=c,s=Math.max(s,r.length);break}a._colspan=1}if(c>=h){if(r.length>1){o=r[r.length-2],l=o[0][o[1]],l._colspan=0;for(c=0,h=u.length;c<h;++c)u[c]._parent=l,l._colspan+=u[c]._colspan}r.pop()}}for(c=0;c<s;++c)n.push([]);r.push([t,-1]);while(r.length){o=r[r.length-1],u=o[0],c=o[1]+1;for(h=u.length;c<h;++c){a=u[c],f=a.children,n[r.length-1].push(a),o[1]=c,a._headers=[a.id];for(p=r.length-2;p>=0;--p)l=r[p][0][r[p][1]],a._headers.unshift(l.id);if(f&&f.length){r.push([f,-1]);break}a._rowspan=s-r.length+1}c>=h&&r.pop()}}for(c=0,h=n.length;c<h;c+=a._rowspan)a=n[c][0],a._first=!0;return n}})},"3.17.2",{requires:["datatable-core","view","classnamemanager"]});
diff --git a/js/yui3/datatable-highlight/assets/datatable-highlight-core.css b/js/yui3/datatable-highlight/assets/datatable-highlight-core.css
new file mode 100644
index 000000000..97f5da18a
--- /dev/null
+++ b/js/yui3/datatable-highlight/assets/datatable-highlight-core.css
@@ -0,0 +1,33 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable tr td {
+ -webkit-transition: background-color 0.05s ease-in;
+ -moz-transition: background-color 0.05s ease-in;
+ -o-transition: background-color 0.05s ease-in;
+ transition: background-color 0.05s ease-in;
+}
+.yui3-datatable .yui3-datatable-highlight-row td {
+ -webkit-transition: background-color 0.1s ease-out;
+ -moz-transition: background-color 0.1s ease-out;
+ -o-transition: background-color 0.1s ease-out;
+ transition: background-color 0.1s ease-out;
+}
+
+.yui3-datatable tr .yui3-datatable-highlight-col {
+ -webkit-transition: background-color 0.1s ease-out;
+ -moz-transition: background-color 0.1s ease-out;
+ -o-transition: background-color 0.1s ease-out;
+ transition: background-color 0.1s ease-out;
+}
+
+.yui3-datatable tr .yui3-datatable-highlight-cell {
+ -webkit-transition: background-color 0.1s ease-out;
+ -moz-transition: background-color 0.1s ease-out;
+ -o-transition: background-color 0.1s ease-out;
+ transition: background-color 0.1s ease-out;
+} \ No newline at end of file
diff --git a/js/yui3/datatable-highlight/assets/skins/night/datatable-highlight.css b/js/yui3/datatable-highlight/assets/skins/night/datatable-highlight.css
new file mode 100644
index 000000000..c8a498da2
--- /dev/null
+++ b/js/yui3/datatable-highlight/assets/skins/night/datatable-highlight.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable tr td{-webkit-transition:background-color .05s ease-in;-moz-transition:background-color .05s ease-in;-o-transition:background-color .05s ease-in;transition:background-color .05s ease-in}.yui3-datatable .yui3-datatable-highlight-row td{-webkit-transition:background-color .1s ease-out;-moz-transition:background-color .1s ease-out;-o-transition:background-color .1s ease-out;transition:background-color .1s ease-out}.yui3-datatable tr .yui3-datatable-highlight-col{-webkit-transition:background-color .1s ease-out;-moz-transition:background-color .1s ease-out;-o-transition:background-color .1s ease-out;transition:background-color .1s ease-out}.yui3-datatable tr .yui3-datatable-highlight-cell{-webkit-transition:background-color .1s ease-out;-moz-transition:background-color .1s ease-out;-o-transition:background-color .1s ease-out;transition:background-color .1s ease-out}.yui3-skin-night .yui3-datatable .yui3-datatable-highlight-row td{background-color:#38383f}.yui3-skin-night .yui3-datatable tr .yui3-datatable-highlight-col{background-color:#38383f}.yui3-skin-night .yui3-datatable tr .yui3-datatable-highlight-cell{background-color:#38383f}#yui3-css-stamp.skin-night-datatable-highlight{display:none}
diff --git a/js/yui3/datatable-highlight/assets/skins/sam/datatable-highlight.css b/js/yui3/datatable-highlight/assets/skins/sam/datatable-highlight.css
new file mode 100644
index 000000000..5a220d4d1
--- /dev/null
+++ b/js/yui3/datatable-highlight/assets/skins/sam/datatable-highlight.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable tr td{-webkit-transition:background-color .05s ease-in;-moz-transition:background-color .05s ease-in;-o-transition:background-color .05s ease-in;transition:background-color .05s ease-in}.yui3-datatable .yui3-datatable-highlight-row td{-webkit-transition:background-color .1s ease-out;-moz-transition:background-color .1s ease-out;-o-transition:background-color .1s ease-out;transition:background-color .1s ease-out}.yui3-datatable tr .yui3-datatable-highlight-col{-webkit-transition:background-color .1s ease-out;-moz-transition:background-color .1s ease-out;-o-transition:background-color .1s ease-out;transition:background-color .1s ease-out}.yui3-datatable tr .yui3-datatable-highlight-cell{-webkit-transition:background-color .1s ease-out;-moz-transition:background-color .1s ease-out;-o-transition:background-color .1s ease-out;transition:background-color .1s ease-out}.yui3-skin-sam .yui3-datatable .yui3-datatable-highlight-row td{background-color:#fef2cd}.yui3-skin-sam .yui3-datatable tr .yui3-datatable-highlight-col{background-color:#fef2cd}.yui3-skin-sam .yui3-datatable tr .yui3-datatable-highlight-cell{background-color:#fef2cd}#yui3-css-stamp.skin-sam-datatable-highlight{display:none}
diff --git a/js/yui3/datatable-highlight/datatable-highlight-min.js b/js/yui3/datatable-highlight/datatable-highlight-min.js
new file mode 100644
index 000000000..55404e0de
--- /dev/null
+++ b/js/yui3/datatable-highlight/datatable-highlight-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatable-highlight",function(e,t){function r(){}var n=e.ClassNameManager.getClassName;r.ATTRS={highlightRows:{value:!1,setter:"_setHighlightRows",validator:e.Lang.isBoolean},highlightCols:{value:!1,setter:"_setHighlightCols",validator:e.Lang.isBoolean},highlightCells:{value:!1,setter:"_setHighlightCells",validator:e.Lang.isBoolean}},r.prototype={highlightClassNames:{row:n(t,"row"),col:n(t,"col"),cell:n(t,"cell")},_colSelector:".{prefix}-data .{prefix}-col-{col}",_colNameRegex:"{prefix}-col-(\\S*)",_highlightDelegates:{},_setHighlightRows:function(t){var n=this._highlightDelegates;return n.row&&n.row.detach(),t===!0&&(n.row=this.delegate("hover",e.bind(this._highlightRow,this),e.bind(this._highlightRow,this),"tbody tr")),t},_setHighlightCols:function(t){var n=this._highlightDelegates;n.col&&n.col.detach(),t===!0&&(this._buildColSelRegex(),n.col=this.delegate("hover",e.bind(this._highlightCol,this),e.bind(this._highlightCol,this),"tr td"))},_setHighlightCells:function(t){var n=this._highlightDelegates;return n.cell&&n.cell.detach(),t===!0&&(n.cell=this.delegate("hover",e.bind(this._highlightCell,this),e.bind(this._highlightCell,this),"tbody td")),t},_highlightRow:function(e){e.currentTarget.toggleClass(this.highlightClassNames.row,e.phase==="over")},_highlightCol:function(t){var n=this._colNameRegex.exec(t.currentTarget.getAttribute("class")),r=e.Lang.sub(this._colSelector,{prefix:this._cssPrefix,col:n[1]});this.view.tableNode.all(r).toggleClass(this.highlightClassNames.col,t.phase==="over")},_highlightCell:function(e){e.currentTarget.toggleClass(this.highlightClassNames.cell,e.phase==="over")},_buildColSelRegex:function(){var t=this._colNameRegex,n;typeof t=="string"&&(this._colNameRegex=new RegExp(e.Lang.sub(t,{prefix:this._cssPrefix})))}},e.DataTable.Highlight=r,e.Base.mix(e.DataTable,[e.DataTable.Highlight])},"3.17.2",{requires:["datatable-base","event-hover"],skinnable:!0});
diff --git a/js/yui3/datatable-keynav/datatable-keynav-min.js b/js/yui3/datatable-keynav/datatable-keynav-min.js
new file mode 100644
index 000000000..a7fb7b3af
--- /dev/null
+++ b/js/yui3/datatable-keynav/datatable-keynav-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatable-keynav",function(e,t){var n=e.Array.each,r=function(){};r.KEY_NAMES={8:"backspace",9:"tab",13:"enter",27:"esc",32:"space",33:"pgup",34:"pgdown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",112:"f1",113:"f2",114:"f3",115:"f4",116:"f5",117:"f6",118:"f7",119:"f8",120:"f9",121:"f10",122:"f11",123:"f12"},r.ARIA_ACTIONS={left:"_keyMoveLeft",right:"_keyMoveRight",up:"_keyMoveUp",down:"_keyMoveDown",home:"_keyMoveRowStart",end:"_keyMoveRowEnd",pgup:"_keyMoveColTop",pgdown:"_keyMoveColBottom"},r.ATTRS={focusedCell:{setter:"_focusedCellSetter"},keyIntoHeaders:{value:!0}},e.mix(r.prototype,{keyActions:null,_keyNavSubscr:null,_keyNavTHead:null,_keyNavNestedHeaders:!1,_keyNavColPrefix:null,_keyNavColRegExp:null,initializer:function(){this.onceAfter("render",this._afterKeyNavRender),this._keyNavSubscr=[this.after("focusedCellChange",this._afterKeyNavFocusedCellChange),this.after("focusedChange",this._afterKeyNavFocusedChange)],this._keyNavColPrefix=this.getClassName("col",""),this._keyNavColRegExp=new RegExp(this._keyNavColPrefix+"(.+?)(\\s|$)"),this.keyActions=e.clone(r.ARIA_ACTIONS)},destructor:function(){n(this._keyNavSubscr,function(e){e&&e.detach&&e.detach()})},_afterKeyNavFocusedCellChange:function(e){var t=e.newVal,n=e.prevVal;n&&n.set("tabIndex",-1),t?(t.set("tabIndex",0),this.get("focused")&&(t.scrollIntoView(),t.focus())):this.set("focused",null)},_afterKeyNavFocusedChange:function(e){var t=this.get("focusedCell");e.newVal?t?(t.scrollIntoView(),t.focus()):this._keyMoveFirst():t&&t.blur()},_afterKeyNavRender:function(){var e=this.get("contentBox");this._keyNavSubscr.push(e.on("keydown",this._onKeyNavKeyDown,this),e.on("click",this._onKeyNavClick,this)),this._keyNavTHead=(this._yScrollHeader||this._tableNode).one("thead"),this._keyMoveFirst(),this._keyNavNestedHeaders=this.get("columns").length!==this.head.theadNode.all("th").size()},_onKeyNavClick:function(e){var t=e.target.ancestor(this.get("keyIntoHeaders")?"td, th":"td",!0);t&&(this.focus(),this.set("focusedCell",t))},_onKeyNavKeyDown:function(e){var t=e.keyCode,i=r.KEY_NAMES[t]||t,s;n(["alt","ctrl","meta","shift"],function(n){e[n+"Key"]&&(t=n+"-"+t,i=n+"-"+i)}),s=this.keyActions[t]||this.keyActions[i],typeof s=="string"?this[s]?this[s].call(this,e):this._keyNavFireEvent(s,e):s.call(this,e)},_keyNavFireEvent:function(e,t){var n=t.target.ancestor("td, th",!0);n&&this.fire(e,{cell:n,row:n.ancestor("tr"),record:this.getRecord(n),column:this.getColumn(n.get("cellIndex"))},t)},_keyMoveFirst:function(){this.set("focusedCell",this.get("keyIntoHeaders")?this._keyNavTHead.one("th"):this._tbodyNode.one("td"),{src:"keyNav"})},_keyMoveLeft:function(e){var t=this.get("focusedCell"),n=t.get("cellIndex"),r=t.ancestor();e.preventDefault();if(n===0)return;t=r.get("cells").item(n-1),this.set("focusedCell",t,{src:"keyNav"})},_keyMoveRight:function(e){var t=this.get("focusedCell"),n=t.ancestor("tr"),r=n.ancestor(),i=r===this._keyNavTHead,s,o;e.preventDefault(),s=t.next();if(n.get("rowIndex")!==0&&i&&this._keyNavNestedHeaders)if(s)t=s;else{o=this._getTHParent(t);if(!o||!o.next())return;t=o.next()}else{if(!s)return;t=s}this.set("focusedCell",t,{src:"keyNav"})},_keyMoveUp:function(e){var t=this.get("focusedCell"),n=t.get("cellIndex"),r=t.ancestor("tr"),i=r.get("rowIndex"),s=r.ancestor(),o=s.get("rows"),u=s===this._keyNavTHead,a;e.preventDefault(),u||(i-=s.get("firstChild").get("rowIndex"));if(i===0){if(u||!this.get("keyIntoHeaders"))return;s=this._keyNavTHead,o=s.get("rows"),this._keyNavNestedHeaders?(key=this._getCellColumnName(t),t=s.one("."+this._keyNavColPrefix+key),n=t.get("cellIndex"),r=t.ancestor("tr")):(r=s.get("firstChild"),t=r.get("cells").item(n))}else u&&this._keyNavNestedHeaders?(key=this._getCellColumnName(t),a=this._columnMap[key]._parent,a&&(t=s.one("#"+a.id))):(r=o.item(i-1),t=r.get("cells").item(n));this.set("focusedCell",t)},_keyMoveDown:function(e){var t=this.get("focusedCell"),n=t.get("cellIndex"),r=t.ancestor("tr"),i=r.get("rowIndex")+1,s=r.ancestor(),o=s===this._keyNavTHead,u=this.body&&this.body.tbodyNode,a=s.get("rows"),f,l;e.preventDefault(),o&&(this._keyNavNestedHeaders?(f=this._getCellColumnName(t),l=this._columnMap[f].children,i+=(t.getAttribute("rowspan")||1)-1,l?t=s.one("#"+l[0].id):(t=u.one("."+this._keyNavColPrefix+f),s=u,a=s.get("rows")),n=t.get("cellIndex")):(r=u.one("tr"),t=r.get("cells").item(n))),i-=a.item(0).get("rowIndex");if(i>=a.size()){if(!o)return;s=u,r=s.one("tr")}else r=a.item(i);this.set("focusedCell",r.get("cells").item(n))},_keyMoveRowStart:function(e){var t=this.get("focusedCell").ancestor();this.set("focusedCell",t.get("firstChild"),{src:"keyNav"}),e.preventDefault()},_keyMoveRowEnd:function(e){var t=this.get("focusedCell").ancestor();this.set("focusedCell",t.get("lastChild"),{src:"keyNav"}),e.preventDefault()},_keyMoveColTop:function(e){var t=this.get("focusedCell"),n=t.get("cellIndex"),r,i;e.preventDefault();if(this._keyNavNestedHeaders&&this.get("keyIntoHeaders")){r=this._getCellColumnName(t),i=this._columnMap[r];while(i._parent)i=i._parent;t=this._keyNavTHead.one("#"+i.id)}else t=(this.get("keyIntoHeaders")?this._keyNavTHead:this._tbodyNode).get("firstChild").get("cells").item(n);this.set("focusedCell",t,{src:"keyNav"})},_keyMoveColBottom:function(e){var t=this.get("focusedCell"),n=t.get("cellIndex");this.set("focusedCell",this._tbodyNode.get("lastChild").get("cells").item(n),{src:"keyNav"}),e.preventDefault()},_focusedCellSetter:function(t){if(t instanceof e.Node){var n=t.get("tagName").toUpperCase();if((n==="TD"||n==="TH")&&this.get("contentBox").contains(t))return t}else if(t===null)return t;return e.Attribute.INVALID_VALUE},_getTHParent:function(e){var t=this._getCellColumnName(e),n=this._columnMap[t]&&this._columnMap[t]._parent;return n?e.ancestor().ancestor().one("."+this._keyNavColPrefix+n.key):null},_getCellColumnName:function(e){return e.getData("yui3-col-id")||this._keyNavColRegExp.exec(e.get("className"))[1]}}),e.DataTable.KeyNav=r,e.Base.mix(e.DataTable,[r])},"3.17.2"
+,{requires:["datatable-base"]});
diff --git a/js/yui3/datatable-message/assets/datatable-message-core.css b/js/yui3/datatable-message/assets/datatable-message-core.css
new file mode 100644
index 000000000..376e1b266
--- /dev/null
+++ b/js/yui3/datatable-message/assets/datatable-message-core.css
@@ -0,0 +1,15 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-message {
+ display: none;
+}
+
+.yui3-datatable-message-visible .yui3-datatable-message {
+ display: block;
+ display: table-row-group;
+}
diff --git a/js/yui3/datatable-message/assets/skins/night/datatable-message.css b/js/yui3/datatable-message/assets/skins/night/datatable-message.css
new file mode 100644
index 000000000..58d56bdea
--- /dev/null
+++ b/js/yui3/datatable-message/assets/skins/night/datatable-message.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-message{display:none}.yui3-datatable-message-visible .yui3-datatable-message{display:block;display:table-row-group}.yui3-skin-night .yui3-datatable-message-content{background-color:#0e0e0e;border:0 none;border-bottom:1px solid #303030;padding:4px 10px}#yui3-css-stamp.skin-night-datatable-message{display:none}
diff --git a/js/yui3/datatable-message/assets/skins/sam/datatable-message.css b/js/yui3/datatable-message/assets/skins/sam/datatable-message.css
new file mode 100644
index 000000000..6fdaf71fb
--- /dev/null
+++ b/js/yui3/datatable-message/assets/skins/sam/datatable-message.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-message{display:none}.yui3-datatable-message-visible .yui3-datatable-message{display:block;display:table-row-group}.yui3-skin-sam .yui3-datatable-message-content{border:0 none;border-bottom:1px solid #cbcbcb;padding:4px 10px}#yui3-css-stamp.skin-sam-datatable-message{display:none}
diff --git a/js/yui3/datatable-message/datatable-message-min.js b/js/yui3/datatable-message/datatable-message-min.js
new file mode 100644
index 000000000..903d397b8
--- /dev/null
+++ b/js/yui3/datatable-message/datatable-message-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatable-message",function(e,t){var n;e.namespace("DataTable").Message=n=function(){},n.ATTRS={showMessages:{value:!0,validator:e.Lang.isBoolean}},e.mix(n.prototype,{MESSAGE_TEMPLATE:'<tbody class="{className}"><tr><td class="{contentClass}" colspan="{colspan}"></td></tr></tbody>',hideMessage:function(){return this.get("boundingBox").removeClass(this.getClassName("message","visible")),this},showMessage:function(e){var t=this.getString(e)||e;return this._messageNode||this._initMessageNode(),this.get("showMessages")&&(t?(this._messageNode.one("."+this.getClassName("message","content")).setHTML(t),this.get("boundingBox").addClass(this.getClassName("message","visible"))):this.hideMessage()),this},_afterMessageColumnsChange:function(){var e;this._messageNode&&(e=this._messageNode.one("."+this.getClassName("message","content")),e&&e.set("colSpan",this._displayColumns.length))},_afterMessageDataChange:function(){this._uiSetMessage()},_afterShowMessagesChange:function(e){e.newVal?this._uiSetMessage(e):this._messageNode&&(this.get("boundingBox").removeClass(this.getClassName("message","visible")),this._messageNode.remove().destroy(!0),this._messageNode=null)},_bindMessageUI:function(){this.after(["dataChange","*:add","*:remove","*:reset"],e.bind("_afterMessageDataChange",this)),this.after("columnsChange",e.bind("_afterMessageColumnsChange",this)),this.after("showMessagesChange",e.bind("_afterShowMessagesChange",this))},initializer:function(){this._initMessageStrings(),this.get("showMessages")&&this.after("table:renderBody",e.bind("_initMessageNode",this)),this.after(e.bind("_bindMessageUI",this),this,"bindUI"),this.after(e.bind("_syncMessageUI",this),this,"syncUI")},_initMessageNode:function(){this._messageNode||(this._messageNode=e.Node.create(e.Lang.sub(this.MESSAGE_TEMPLATE,{className:this.getClassName("message"),contentClass:this.getClassName("message","content"),colspan:this._displayColumns.length||1})),this._tableNode.insertBefore(this._messageNode,this._tbodyNode))},_initMessageStrings:function(){this.set("strings",e.mix(this.get("strings")||{},e.Intl.get("datatable-message")))},_syncMessageUI:function(){this._uiSetMessage()},_uiSetMessage:function(e){this.data.size()?this.hideMessage():this.showMessage(e&&e.message||"emptyMessage")}}),e.Lang.isFunction(e.DataTable)&&e.Base.mix(e.DataTable,[n])},"3.17.2",{requires:["datatable-base"],lang:["en","fr","es","hu","it"],skinnable:!0});
diff --git a/js/yui3/datatable-message/lang/datatable-message.js b/js/yui3/datatable-message/lang/datatable-message.js
new file mode 100644
index 000000000..49ab8fb19
--- /dev/null
+++ b/js/yui3/datatable-message/lang/datatable-message.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatable-message",function(e){e.Intl.add("datatable-message","",{emptyMessage:"No data to display",loadingMessage:"Loading..."})},"3.17.2");
diff --git a/js/yui3/datatable-message/lang/datatable-message_en.js b/js/yui3/datatable-message/lang/datatable-message_en.js
new file mode 100644
index 000000000..f37be8ed9
--- /dev/null
+++ b/js/yui3/datatable-message/lang/datatable-message_en.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatable-message_en",function(e){e.Intl.add("datatable-message","en",{emptyMessage:"No data to display",loadingMessage:"Loading..."})},"3.17.2");
diff --git a/js/yui3/datatable-message/lang/datatable-message_es.js b/js/yui3/datatable-message/lang/datatable-message_es.js
new file mode 100644
index 000000000..90231a399
--- /dev/null
+++ b/js/yui3/datatable-message/lang/datatable-message_es.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatable-message_es",function(e){e.Intl.add("datatable-message","es",{emptyMessage:"No hay datos que mostrar",loadingMessage:"Cargando..."})},"3.17.2");
diff --git a/js/yui3/datatable-message/lang/datatable-message_fr.js b/js/yui3/datatable-message/lang/datatable-message_fr.js
new file mode 100644
index 000000000..1a0b9966b
--- /dev/null
+++ b/js/yui3/datatable-message/lang/datatable-message_fr.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatable-message_fr",function(e){e.Intl.add("datatable-message","fr",{emptyMessage:"Aucune donn\u00e9e \u00e0 afficher",loadingMessage:"Chargement..."})},"3.17.2");
diff --git a/js/yui3/datatable-message/lang/datatable-message_hu.js b/js/yui3/datatable-message/lang/datatable-message_hu.js
new file mode 100644
index 000000000..630799d0f
--- /dev/null
+++ b/js/yui3/datatable-message/lang/datatable-message_hu.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatable-message_hu",function(e){e.Intl.add("datatable-message","hu",{emptyMessage:"Nincs megjelen\u00edthet\u0151 adat",loadingMessage:"Bet\u00f6lt\u00e9s..."})},"3.17.2");
diff --git a/js/yui3/datatable-message/lang/datatable-message_it.js b/js/yui3/datatable-message/lang/datatable-message_it.js
new file mode 100644
index 000000000..669217ae7
--- /dev/null
+++ b/js/yui3/datatable-message/lang/datatable-message_it.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatable-message_it",function(e){e.Intl.add("datatable-message","it",{emptyMessage:"Non ci sono dati da mostrare",loadingMessage:"Caricando..."})},"3.17.2");
diff --git a/js/yui3/datatable-mutable/datatable-mutable-min.js b/js/yui3/datatable-mutable/datatable-mutable-min.js
new file mode 100644
index 000000000..51ac5f6a2
--- /dev/null
+++ b/js/yui3/datatable-mutable/datatable-mutable-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatable-mutable",function(e,t){var n=e.Array,r=e.Lang,i=r.isString,s=r.isArray,o=r.isObject,u=r.isNumber,a=e.Array.indexOf,f;e.namespace("DataTable").Mutable=f=function(){},f.ATTRS={autoSync:{value:!1,validator:r.isBoolean}},e.mix(f.prototype,{addColumn:function(e,t){i(e)&&(e={key:e});if(e){if(arguments.length<2||!u(t)&&!s(t))t=this.get("columns").length;this.fire("addColumn",{column:e,index:t})}return this},modifyColumn:function(e,t){return i(t)&&(t={key:t}),o(t)&&this.fire("modifyColumn",{column:e,newColumnDef:t}),this},moveColumn:function(e,t){return e!==undefined&&(u(t)||s(t))&&this.fire("moveColumn",{column:e,index:t}),this},removeColumn:function(e){return e!==undefined&&this.fire("removeColumn",{column:e}),this},addRow:function(e,t){var r=t&&"sync"in t?t.sync:this.get("autoSync"),i,s,o,u,a;if(e&&this.data){i=this.data.add.apply(this.data,arguments);if(r){i=n(i),a=n(arguments,1,!0);for(o=0,u=i.length;o<u;++o)s=i[o],s.isNew()&&i[o].save.apply(i[o],a)}}return this},removeRow:function(e,t){var r=this.data,i=t&&"sync"in t?t.sync:this.get("autoSync"),s,u,a,f,l;o(e)&&e instanceof this.get("recordType")?u=e:r&&e!==undefined&&(u=r.getById(e)||r.getByClientId(e)||r.item(e));if(u){l=n(arguments,1,!0),s=r.remove.apply(r,[u].concat(l));if(i){o(l[0])||l.unshift({}),l[0]["delete"]=!0,s=n(s);for(a=0,f=s.length;a<f;++a)u=s[a],u.destroy.apply(u,l)}}return this},modifyRow:function(e,t,r){var i=this.data,s=r&&"sync"in r?r.sync:this.get("autoSync"),u,a;return o(e)&&e instanceof this.get("recordType")?u=e:i&&e!==undefined&&(u=i.getById(e)||i.getByClientId(e)||i.item(e)),u&&o(t)&&(a=n(arguments,1,!0),u.setAttrs.apply(u,a),s&&!u.isNew()&&u.save.apply(u,a)),this},_defAddColumnFn:function(e){var t=n(e.index),r=this.get("columns"),i=r,s,o;for(s=0,o=t.length-1;i&&s<o;++s)i=i[t[s]]&&i[t[s]].children;i&&(i.splice(t[s],0,e.column),this.set("columns",r,{originEvent:e}))},_defModifyColumnFn:function(t){var n=this.get("columns"),r=this.getColumn(t.column);r&&(e.mix(r,t.newColumnDef,!0),this.set("columns",n,{originEvent:t}))},_defMoveColumnFn:function(e){var t=this.get("columns"),r=this.getColumn(e.column),i=n(e.index),s,o,u,f,l;if(r){s=r._parent?r._parent.children:t,o=a(s,r);if(o>-1){u=t;for(f=0,l=i.length-1;u&&f<l;++f)u=u[i[f]]&&u[i[f]].children;u&&(l=u.length,s.splice(o,1),i=i[f],l>u.lenth&&o<i&&i--,u.splice(i,0,r),this.set("columns",t,{originEvent:e}))}}},_defRemoveColumnFn:function(t){var n=this.get("columns"),r=this.getColumn(t.column),i,s;r&&(i=r._parent?r._parent.children:n,s=e.Array.indexOf(i,r),s>-1&&(i.splice(s,1),this.set("columns",n,{originEvent:t})))},initializer:function(){this.publish({addColumn:{defaultFn:e.bind("_defAddColumnFn",this)},removeColumn:{defaultFn:e.bind("_defRemoveColumnFn",this)},moveColumn:{defaultFn:e.bind("_defMoveColumnFn",this)},modifyColumn:{defaultFn:e.bind("_defModifyColumnFn",this)}})}}),f.prototype.addRows=f.prototype.addRow,r.isFunction(e.DataTable)&&e.Base.mix(e.DataTable,[f])},"3.17.2",{requires:["datatable-base"]});
diff --git a/js/yui3/datatable-paginator-templates/datatable-paginator-templates-min.js b/js/yui3/datatable-paginator-templates/datatable-paginator-templates-min.js
new file mode 100644
index 000000000..2a877afd4
--- /dev/null
+++ b/js/yui3/datatable-paginator-templates/datatable-paginator-templates-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatable-paginator-templates",function(e,t){var n=new e.Template,r='<tr><td class="<%= this.wrapperClass %>" colspan="<%= this.numOfCols %>"/></tr>',i="<%= buttons %><%= this.classNames.gotoPage %><%= this.classNames.perPage %>",s='<button class="<%= this.classNames.control %> <%= this.classNames.control %>-<%= this.type %>" data-type="<%= this.type %>"><%= this.label %></button>',o='<div class="<%= this.classNames.controls %> <%= this.classNames.group %>"><%== this.buttons %></div>',u='<form action="#" class="<%= this.classNames.group %>"><label><%= this.strings.goToLabel %><input type="text" value="<%= this.page %>"><button><%= this.strings.goToAction %></button></label></form>',a='<div class="<%= this.classNames.group %> <%= this.classNames.perPage %>"><label><%= this.strings.perPage %> <select><% Y.Array.each(this.options, function (option, i) { %><option value="<%= option.value %>" <%= option.selected %>><%= option.label %></option><% }); %></select></label></div>';e.namespace("DataTable.Templates").Paginator={rowWrapper:n.compile(r),button:n.compile(s),content:n.compile(i),buttons:n.compile(o),gotoPage:n.compile(u),perPage:n.compile(a)}},"3.17.2",{requires:["template"]});
diff --git a/js/yui3/datatable-paginator/assets/datatable-paginator-core.css b/js/yui3/datatable-paginator/assets/datatable-paginator-core.css
new file mode 100644
index 000000000..ec8293af1
--- /dev/null
+++ b/js/yui3/datatable-paginator/assets/datatable-paginator-core.css
@@ -0,0 +1,65 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-paginator-wrapper {
+ border: none;
+ padding: 0;
+}
+.yui3-datatable-paginator {
+ padding: 3px;
+ white-space: nowrap;
+}
+.yui3-datatable-paginator .yui3-paginator-content {
+ position: relative;
+}
+.yui3-datatable-paginator .yui3-paginator-page-select {
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+.yui3-datatable-paginator .yui3-datatable-paginator-group {
+ display: inline-block;
+ zoom: 1; *display: inline;
+}
+.yui3-datatable-paginator .yui3-datatable-paginator-control {
+ display: inline-block;
+ zoom: 1; *display: inline;
+ margin: 0 3px;
+ padding: 0 0.2em;
+ text-align: center;
+ text-decoration: none;
+ line-height: 1.5;
+ border: 1px solid transparent;
+ border-radius: 3px;
+ background: transparent;
+}
+.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled,
+.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled:hover {
+ cursor: default;
+}
+.yui3-datatable-paginator .yui3-datatable-paginator-group input {
+ width: 3em;
+}
+.yui3-datatable-paginator form {
+ text-align: center;
+ margin: 0 2em;
+}
+.yui3-datatable-paginator .yui3-datatable-paginator-per-page {
+ text-align: right;
+}
+/* FOR USE WHEN DISPLAYING ICONS
+.yui3-datatable-paginator .control-first,
+.yui3-datatable-paginator .control-last,
+.yui3-datatable-paginator .control-prev,
+.yui3-datatable-paginator .control-next {
+ text-indent: -999px;
+ direction: ltr;
+ overflow: hidden;
+ position: relative;
+ width: 1em;
+}
+*/ \ No newline at end of file
diff --git a/js/yui3/datatable-paginator/assets/skins/night/datatable-paginator.css b/js/yui3/datatable-paginator/assets/skins/night/datatable-paginator.css
new file mode 100644
index 000000000..00137b837
--- /dev/null
+++ b/js/yui3/datatable-paginator/assets/skins/night/datatable-paginator.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-paginator-wrapper{border:0;padding:0}.yui3-datatable-paginator{padding:3px;white-space:nowrap}.yui3-datatable-paginator .yui3-paginator-content{position:relative}.yui3-datatable-paginator .yui3-paginator-page-select{position:absolute;right:0;top:0}.yui3-datatable-paginator .yui3-datatable-paginator-group{display:inline-block;zoom:1;*display:inline}.yui3-datatable-paginator .yui3-datatable-paginator-control{display:inline-block;zoom:1;*display:inline;margin:0 3px;padding:0 .2em;text-align:center;text-decoration:none;line-height:1.5;border:1px solid transparent;border-radius:3px;background:transparent}.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled,.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled:hover{cursor:default}.yui3-datatable-paginator .yui3-datatable-paginator-group input{width:3em}.yui3-datatable-paginator form{text-align:center;margin:0 2em}.yui3-datatable-paginator .yui3-datatable-paginator-per-page{text-align:right}.yui3-datatable-paginator{background:white url(../../../../assets/skins/night/sprite.png) repeat-x 0 0;background-image:-webkit-linear-gradient(transparent 40%,hsla(0,0%,0%,0.21));background-image:-moz-linear-gradient(top,transparent 40%,hsla(0,0%,0%,0.21));background-image:-ms-linear-gradient(transparent 40%,hsla(0,0%,0%,0.21));background-image:-o-linear-gradient(transparent 40%,hsla(0,0%,0%,0.21));background-image:linear-gradient(transparent 40%,hsla(0,0%,0%,0.21))}.yui3-datatable-paginator .yui3-datatable-paginator-control{color:#242d42}.yui3-datatable-paginator .yui3-datatable-paginator-control-first:hover,.yui3-datatable-paginator .yui3-datatable-paginator-control-last:hover,.yui3-datatable-paginator .yui3-datatable-paginator-control-prev:hover,.yui3-datatable-paginator .yui3-datatable-paginator-control-next:hover{box-shadow:0 1px 2px #292442}.yui3-datatable-paginator .yui3-datatable-paginator-control-first:active,.yui3-datatable-paginator .yui3-datatable-paginator-control-last:active,.yui3-datatable-paginator .yui3-datatable-paginator-control-prev:active,.yui3-datatable-paginator .yui3-datatable-paginator-control-next:active{box-shadow:inset 0 1px 1px #292442;background:#e0deed;background:hsla(250,30%,90%,0.3)}.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled,.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled:hover{color:#bdc7db;border-color:transparent;box-shadow:none}#yui3-css-stamp.skin-night-datatable-paginator{display:none}
diff --git a/js/yui3/datatable-paginator/assets/skins/sam/datatable-paginator.css b/js/yui3/datatable-paginator/assets/skins/sam/datatable-paginator.css
new file mode 100644
index 000000000..daf4c8dab
--- /dev/null
+++ b/js/yui3/datatable-paginator/assets/skins/sam/datatable-paginator.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-paginator-wrapper{border:0;padding:0}.yui3-datatable-paginator{padding:3px;white-space:nowrap}.yui3-datatable-paginator .yui3-paginator-content{position:relative}.yui3-datatable-paginator .yui3-paginator-page-select{position:absolute;right:0;top:0}.yui3-datatable-paginator .yui3-datatable-paginator-group{display:inline-block;zoom:1;*display:inline}.yui3-datatable-paginator .yui3-datatable-paginator-control{display:inline-block;zoom:1;*display:inline;margin:0 3px;padding:0 .2em;text-align:center;text-decoration:none;line-height:1.5;border:1px solid transparent;border-radius:3px;background:transparent}.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled,.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled:hover{cursor:default}.yui3-datatable-paginator .yui3-datatable-paginator-group input{width:3em}.yui3-datatable-paginator form{text-align:center;margin:0 2em}.yui3-datatable-paginator .yui3-datatable-paginator-per-page{text-align:right}.yui3-datatable-paginator{background:white url(../../../../assets/skins/sam/sprite.png) repeat-x 0 0;background-image:-webkit-linear-gradient(transparent 40%,hsla(0,0%,0%,0.21));background-image:-moz-linear-gradient(top,transparent 40%,hsla(0,0%,0%,0.21));background-image:-ms-linear-gradient(transparent 40%,hsla(0,0%,0%,0.21));background-image:-o-linear-gradient(transparent 40%,hsla(0,0%,0%,0.21));background-image:linear-gradient(transparent 40%,hsla(0,0%,0%,0.21));border-color:#cbcbcb}.yui3-datatable-paginator .yui3-datatable-paginator-control{color:#242d42}.yui3-datatable-paginator .yui3-datatable-paginator-control-first:hover,.yui3-datatable-paginator .yui3-datatable-paginator-control-last:hover,.yui3-datatable-paginator .yui3-datatable-paginator-control-prev:hover,.yui3-datatable-paginator .yui3-datatable-paginator-control-next:hover{box-shadow:0 1px 2px #292442}.yui3-datatable-paginator .yui3-datatable-paginator-control-first:active,.yui3-datatable-paginator .yui3-datatable-paginator-control-last:active,.yui3-datatable-paginator .yui3-datatable-paginator-control-prev:active,.yui3-datatable-paginator .yui3-datatable-paginator-control-next:active{box-shadow:inset 0 1px 1px #292442;background:#e0deed;background:hsla(250,30%,90%,0.3)}.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled,.yui3-datatable-paginator .yui3-datatable-paginator-control-disabled:hover{color:#bdc7db;border-color:transparent;box-shadow:none}#yui3-css-stamp.skin-sam-datatable-paginator{display:none}
diff --git a/js/yui3/datatable-paginator/datatable-paginator-min.js b/js/yui3/datatable-paginator/datatable-paginator-min.js
new file mode 100644
index 000000000..e0866b6f0
--- /dev/null
+++ b/js/yui3/datatable-paginator/datatable-paginator-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatable-paginator",function(e,t){function f(){}var n,r,i=e.DataTable.Templates.Paginator,s=e.Lang.sub,o=e.ClassNameManager.getClassName,u=o(t,"control-disabled"),a="paginator:ui";n=e.Base.create("dt-pg-model",e.Model,[e.Paginator.Core]),r=e.Base.create("dt-pg-view",e.View,[],{_eventHandles:[],containerTemplate:'<div class="{paginator}"/>',contentTemplate:"{buttons}{goto}{perPage}",_allowAdHocAttrs:!1,initializer:function(){this.containerTemplate=s(this.containerTemplate,{paginator:o(t)}),this._initStrings(),this._initClassNames(),this.attachEvents()},render:function(){var e=this.get("model"),t=s(this.contentTemplate,{buttons:this._buildButtonsGroup(),"goto":this._buildGotoGroup(),perPage:this._buildPerPageGroup()});return this.get("container").append(t),this.attachEvents(),this._rendered=!0,this._updateControlsUI(e.get("page")),this._updateItemsPerPageUI(e.get("itemsPerPage")),this},attachEvents:function(){r.superclass.attachEvents.apply(this,arguments);var t=this.get("container");this.classNames||this._initClassNames(),this._attachedViewEvents.push(t.delegate("click",this._controlClick,"."+this.classNames.control,this),this.get("model").after("change",this._modelChange,this)),t.all("form").each(e.bind(function(e){this._attachedViewEvents.push(e.after("submit",this._controlSubmit,this))},this)),t.all("select").each(e.bind(function(e){this._attachedViewEvents.push(e.after("change",this._controlChange,this))},this))},_buildButtonsGroup:function(){var e=this.get("strings"),t=this.classNames,n;return n=i.button({type:"first",label:e.first,classNames:t})+i.button({type:"prev",label:e.prev,classNames:t})+i.button({type:"next",label:e.next,classNames:t})+i.button({type:"last",label:e.last,classNames:t}),i.buttons({classNames:t,buttons:n})},_buildGotoGroup:function(){return i.gotoPage({classNames:this.classNames,strings:this.get("strings"),page:this.get("model").get("page")})},_buildPerPageGroup:function(){var e=this.get("pageSizes"),t=this.get("model").get("rowsPerPage"),n,r,s;for(s=0,r=e.length;s<r;s++)n=e[s],typeof n!="object"&&(n={value:n,label:n}),n.selected=n.value===t?" selected":"";return i.perPage({classNames:this.classNames,strings:this.get("strings"),options:this.get("pageSizes")})},_modelChange:function(e){var t=e.changed,n=t&&t.page,r=t&&t.itemsPerPage;n&&this._updateControlsUI(n.newVal),r&&(this._updateItemsPerPageUI(r.newVal),n||this._updateControlsUI(e.target.get("page")))},_updateControlsUI:function(e){if(!this._rendered)return;var t=this.get("model"),n="."+this.classNames.control,r=this.get("container"),i=t.hasPrevPage(),s=t.hasNextPage();r.one(n+"-first").toggleClass(u,!i).set("disabled",!i),r.one(n+"-prev").toggleClass(u,!i).set("disabled",!i),r.one(n+"-next").toggleClass(u,!s).set("disabled",!s),r.one(n+"-last").toggleClass(u,!s).set("disabled",!s),r.one("form input").set("value",e)},_updateItemsPerPageUI:function(e){if(!this._rendered)return;this.get("container").one("select").set("value",e)},_controlClick:function(e){e.preventDefault();var t=e.currentTarget;if(t.hasClass(u))return;this.fire(a,{type:t.getData("type"),val:t.getData("page")||null})},_controlChange:function(e){if(e.target.hasClass(u))return;val=e.target.get("value"),this.fire(a,{type:"perPage",val:parseInt(val,10)})},_controlSubmit:function(e){if(e.target.hasClass(u))return;e.preventDefault(),input=e.target.one("input"),this.fire(a,{type:"page",val:+input.get("value")})},_initClassNames:function(){this.classNames={control:o(t,"control"),controls:o(t,"controls"),group:o(t,"group"),perPage:o(t,"per-page")}},_initStrings:function(){this.set("strings",e.mix(this.get("strings")||{},e.Intl.get("datatable-paginator")))},_defPageSizeVal:function(){this._initStrings();var e=this.get("strings");return[10,50,100,{label:e.showAll,value:-1}]}},{ATTRS:{pageSizes:{valueFn:"_defPageSizeVal"},model:{}}}),f.ATTRS={paginatorModel:{setter:"_setPaginatorModel",value:null,writeOnce:"initOnly"},paginatorModelType:{getter:"_getConstructor",value:"DataTable.Paginator.Model",writeOnce:"initOnly"},paginatorView:{getter:"_getConstructor",value:"DataTable.Paginator.View",writeOnce:"initOnly"},pageSizes:{setter:"_setPageSizesFn",valueFn:"_defPageSizeVal"},paginatorStrings:{},rowsPerPage:{value:null},paginatorLocation:{value:"footer"}},e.mix(f.prototype,{firstPage:function(){return this.get("paginatorModel").set("page",1),this},lastPage:function(){var e=this.get("paginatorModel");return e.set("page",e.get("totalPages")),this},previousPage:function(){return this.get("paginatorModel").prevPage(),this},nextPage:function(){return this.get("paginatorModel").nextPage(),this},initializer:function(){this._initPaginatorStrings(),this._augmentData(),this._eventHandles.paginatorRender||(this._eventHandles.paginatorRender=e.Do.after(this._paginatorRender,this,"render"))},_paginatorRender:function(){var e=this.get("paginatorModel");this._paginatorRenderUI(),e.after("change",this._afterPaginatorModelChange,this),this.after("dataChange",this._afterDataChangeWithPaginator,this),this.after("rowsPerPageChange",this._afterRowsPerPageChange,this),this.data.after(["add","remove","change"],this._afterDataUpdatesWithPaginator,this),e.set("itemsPerPage",this.get("rowsPerPage")),e.set("totalItems",this.get("data").size())},_afterDataChangeWithPaginator:function(){var e=this.get("data"),t=this.get("paginatorModel");t.set("totalItems",e.size()),t.get("page")!==1?this.firstPage():(this._augmentData(),e.fire.call(e,"reset",{src:"reset",models:e._items.concat()}))},_afterDataUpdatesWithPaginator:function(){var e=this.get("paginatorModel"),t=this.get("data");e.set("totalItems",t.size())},_afterRowsPerPageChange:function(e){var t=this.get("data"),n=this.get("paginatorModel"),r;if(e.newVal!==null)this._paginatorRenderUI(),t._paged||this._augmentData(),t._paged.index=(n.get("page")-1)*n.get("itemsPerPage"),t._paged.length=n.get("itemsPerPage");else{while(this._pgViews.length)r=this._pgViews.shift(),r.destroy({remove:!0}),r._rendered=null;t._paged.index=0,t._paged.
+length=null}this.get("paginatorModel").set("itemsPerPage",parseInt(e.newVal,10))},_paginatorRenderUI:function(){if(!this.get("rowsPerPage"))return;var n=this._pgViews,r=this.get("paginatorView"),s={pageSizes:this.get("pageSizes"),model:this.get("paginatorModel")},u=this.get("paginatorLocation");e.Lang.isArray(u)||(u=[u]),n||(n=this._pgViews=[]),e.Array.each(u,function(u){var a=new r(s),f=a.render().get("container"),l;a.after("*:ui",this._uiPgHandler,this),n.push(a),u._node?(u.append(f),this.after("destroy",function(){a.destroy({remove:!0})})):u==="footer"?(this.foot||(this.foot=new e.DataTable.FooterView({host:this}),this.foot.render(),this.fire("renderFooter",{view:this.foot})),l=e.Node.create(i.rowWrapper({wrapperClass:o(t,"wrapper"),numOfCols:this.get("columns").length})),l.one("td").append(f),this.foot.tfootNode.append(l),a.after("destroy",function(){l.remove(!0)})):u==="header"&&(this.view&&this.view.tableNode?this.view.tableNode.insert(f,"before"):this.get("contentBox").prepend(f))},this)},_uiPgHandler:function(e){var t=this.get("paginatorModel");switch(e.type){case"first":t.set("page",1);break;case"last":t.set("page",t.get("totalPages"));break;case"prev":case"next":t[e.type+"Page"]();break;case"page":t.set("page",e.val);break;case"perPage":t.set("itemsPerPage",e.val),t.set("page",1)}},_afterPaginatorModelChange:function(){var e=this.get("paginatorModel"),t=this.get("data");t._paged?(t._paged.index=(e.get("page")-1)*e.get("itemsPerPage"),t._paged.length=e.get("itemsPerPage")):this._augmentData(),t.fire.call(t,"reset",{src:"reset",models:t._items.concat()})},_augmentData:function(){var t=this.get("paginatorModel");if(this.get("rowsPerPage")===null)return;e.mix(this.get("data"),{_paged:{index:(t.get("page")-1)*t.get("itemsPerPage"),length:t.get("itemsPerPage")},getPage:function(){var e=this._paged,t=e.index;return e.length>=0?this._items.slice(t,t+e.length):this._items.slice(t)},size:function(e){return e&&this._paged.length>=0?this._paged.length:this._items.length},each:function(){var t=Array.prototype.slice.call(arguments);return t.unshift(this.getPage()),e.Array.each.apply(null,t),this}},!0)},_setPageSizesFn:function(t){var n,r=t.length,i,s;e.Lang.isArray(t)||(t=[t],r=t.length);for(n=0;n<r;n++)typeof t[n]!="object"&&(i=t[n],s=t[n],parseInt(s,10)!=s&&(s=-1),t[n]={label:i,value:s});return t},_setPaginatorModel:function(e){if(!e||!e._isYUIModel){var t=this.get("paginatorModelType");e=new t(e)}return e},_getConstructor:function(t){return typeof t=="string"?e.Object.getValue(e,t.split(".")):t},_initPaginatorStrings:function(){this.set("paginatorStrings",e.mix(this.get("paginatorStrings")||{},e.Intl.get("datatable-paginator")))},_defPageSizeVal:function(){this._initPaginatorStrings();var e=this.get("paginatorStrings");return[10,50,100,{label:e.showAll,value:-1}]}},!0),e.DataTable.Paginator=f,e.DataTable.Paginator.Model=n,e.DataTable.Paginator.View=r,e.Base.mix(e.DataTable,[e.DataTable.Paginator])},"3.17.2",{requires:["model","view","paginator-core","datatable-foot","datatable-paginator-templates"],lang:["en","fr"],skinnable:!0});
diff --git a/js/yui3/datatable-paginator/lang/datatable-paginator.js b/js/yui3/datatable-paginator/lang/datatable-paginator.js
new file mode 100644
index 000000000..cfc917642
--- /dev/null
+++ b/js/yui3/datatable-paginator/lang/datatable-paginator.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatable-paginator",function(e){e.Intl.add("datatable-paginator","",{first:"First",prev:"Previous",next:"Next",last:"Last",goToLabel:"Page:",goToAction:"Go",perPage:"Rows:",showAll:"Show All"})},"3.17.2");
diff --git a/js/yui3/datatable-paginator/lang/datatable-paginator_en.js b/js/yui3/datatable-paginator/lang/datatable-paginator_en.js
new file mode 100644
index 000000000..568e07186
--- /dev/null
+++ b/js/yui3/datatable-paginator/lang/datatable-paginator_en.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatable-paginator_en",function(e){e.Intl.add("datatable-paginator","en",{first:"First",prev:"Previous",next:"Next",last:"Last",goToLabel:"Page:",goToAction:"Go",perPage:"Rows:",showAll:"Show All"})},"3.17.2");
diff --git a/js/yui3/datatable-paginator/lang/datatable-paginator_fr.js b/js/yui3/datatable-paginator/lang/datatable-paginator_fr.js
new file mode 100644
index 000000000..cec4f2925
--- /dev/null
+++ b/js/yui3/datatable-paginator/lang/datatable-paginator_fr.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatable-paginator_fr",function(e){e.Intl.add("datatable-paginator","fr",{first:"Premi\u00e8re",prev:"Pr\u00e9c\u00e9dente",next:"Suivante",last:"Derni\u00e8re",goToLabel:"Page :",goToAction:"Aller",perPage:"Lignes :",showAll:"Afficher tout"})},"3.17.2");
diff --git a/js/yui3/datatable-scroll/assets/datatable-scroll-core.css b/js/yui3/datatable-scroll/assets/datatable-scroll-core.css
new file mode 100644
index 000000000..1ca3f5024
--- /dev/null
+++ b/js/yui3/datatable-scroll/assets/datatable-scroll-core.css
@@ -0,0 +1,78 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* foundational CSS */
+.yui3-datatable-scrollable-x {
+ _overflow-x: hidden;
+ _position: relative;
+}
+
+.yui3-datatable-scrollable-y,
+.yui3-datatable-scrollable-y .yui3-datatable-x-scroller {
+ _overflow-y: hidden;
+ _position: relative;
+}
+
+.yui3-datatable-y-scroller-container {
+ overflow-x: hidden;
+ position: relative;
+}
+
+.yui3-datatable-scrollable-y .yui3-datatable-content {
+ /* To allow absolute positioning of virtual scrollbar */
+ position: relative;
+}
+
+.yui3-datatable-scrollable-y .yui3-datatable-table .yui3-datatable-columns {
+ /* Prevent masked headers from showing during momentum scrolling */
+ visibility: hidden;
+}
+
+.yui3-datatable-scroll-columns {
+ position: absolute;
+ width: 100%;
+ z-index: 2;
+}
+
+.yui3-datatable-y-scroller,
+.yui3-datatable-scrollable-x .yui3-datatable-caption-table {
+ width: 100%;
+}
+
+.yui3-datatable-x-scroller {
+ position: relative;
+ overflow-x: scroll;
+ overflow-y: hidden;
+}
+
+.yui3-datatable-scrollable-y .yui3-datatable-y-scroller {
+ position: relative;
+ overflow-x: hidden;
+ overflow-y: scroll;
+ z-index: 1;
+ -webkit-overflow-scrolling: touch;
+}
+
+.yui3-datatable-scrollbar {
+ position: absolute;
+ overflow-x: hidden;
+ overflow-y: scroll;
+ z-index: 2;
+}
+
+.yui3-datatable-scrollbar div {
+ position: absolute;
+ width: 1px;
+ visibility: hidden;
+}
+
+/* Removed because it prevented cmd + zoom resizing in Chrome (at least) */
+/*
+.yui3-datatable-header {
+ -webkit-text-size-adjust: none;
+}
+*/
diff --git a/js/yui3/datatable-scroll/assets/skins/night/datatable-scroll.css b/js/yui3/datatable-scroll/assets/skins/night/datatable-scroll.css
new file mode 100644
index 000000000..50e5bbde7
--- /dev/null
+++ b/js/yui3/datatable-scroll/assets/skins/night/datatable-scroll.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-scrollable-x{_overflow-x:hidden;_position:relative}.yui3-datatable-scrollable-y,.yui3-datatable-scrollable-y .yui3-datatable-x-scroller{_overflow-y:hidden;_position:relative}.yui3-datatable-y-scroller-container{overflow-x:hidden;position:relative}.yui3-datatable-scrollable-y .yui3-datatable-content{position:relative}.yui3-datatable-scrollable-y .yui3-datatable-table .yui3-datatable-columns{visibility:hidden}.yui3-datatable-scroll-columns{position:absolute;width:100%;z-index:2}.yui3-datatable-y-scroller,.yui3-datatable-scrollable-x .yui3-datatable-caption-table{width:100%}.yui3-datatable-x-scroller{position:relative;overflow-x:scroll;overflow-y:hidden}.yui3-datatable-scrollable-y .yui3-datatable-y-scroller{position:relative;overflow-x:hidden;overflow-y:scroll;z-index:1;-webkit-overflow-scrolling:touch}.yui3-datatable-scrollbar{position:absolute;overflow-x:hidden;overflow-y:scroll;z-index:2}.yui3-datatable-scrollbar div{position:absolute;width:1px;visibility:hidden}.yui3-skin-sam .yui3-datatable-scroll-columns{border-collapse:separate;border-spacing:0;font-family:HelveticaNeue,arial,helvetica,clean,sans-serif;margin:0;padding:0;top:0;left:0}.yui3-skin-sam .yui3-datatable-scroll-columns .yui3-datatable-header{padding:0}.yui3-skin-sam .yui3-datatable-x-scroller,.yui3-skin-sam .yui3-datatable-y-scroller-container{border:1px solid #303030;border-left-color:#323434}.yui3-skin-sam .yui3-datatable-scrollable-x .yui3-datatable-y-scroller-container,.yui3-skin-sam .yui3-datatable-x-scroller .yui3-datatable-table,.yui3-skin-sam .yui3-datatable-y-scroller .yui3-datatable-table{border:0 none}#yui3-css-stamp.skin-night-datatable-scroll{display:none}
diff --git a/js/yui3/datatable-scroll/assets/skins/sam/datatable-scroll.css b/js/yui3/datatable-scroll/assets/skins/sam/datatable-scroll.css
new file mode 100644
index 000000000..9e41f42cc
--- /dev/null
+++ b/js/yui3/datatable-scroll/assets/skins/sam/datatable-scroll.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-scrollable-x{_overflow-x:hidden;_position:relative}.yui3-datatable-scrollable-y,.yui3-datatable-scrollable-y .yui3-datatable-x-scroller{_overflow-y:hidden;_position:relative}.yui3-datatable-y-scroller-container{overflow-x:hidden;position:relative}.yui3-datatable-scrollable-y .yui3-datatable-content{position:relative}.yui3-datatable-scrollable-y .yui3-datatable-table .yui3-datatable-columns{visibility:hidden}.yui3-datatable-scroll-columns{position:absolute;width:100%;z-index:2}.yui3-datatable-y-scroller,.yui3-datatable-scrollable-x .yui3-datatable-caption-table{width:100%}.yui3-datatable-x-scroller{position:relative;overflow-x:scroll;overflow-y:hidden}.yui3-datatable-scrollable-y .yui3-datatable-y-scroller{position:relative;overflow-x:hidden;overflow-y:scroll;z-index:1;-webkit-overflow-scrolling:touch}.yui3-datatable-scrollbar{position:absolute;overflow-x:hidden;overflow-y:scroll;z-index:2}.yui3-datatable-scrollbar div{position:absolute;width:1px;visibility:hidden}.yui3-skin-sam .yui3-datatable-scroll-columns{border-collapse:separate;border-spacing:0;font-family:arial,sans-serif;margin:0;padding:0;top:0;left:0}.yui3-skin-sam .yui3-datatable-scroll-columns .yui3-datatable-header{padding:0}.yui3-skin-sam .yui3-datatable-x-scroller,.yui3-skin-sam .yui3-datatable-y-scroller-container{border:1px solid #cbcbcb}.yui3-skin-sam .yui3-datatable-scrollable-x .yui3-datatable-y-scroller-container,.yui3-skin-sam .yui3-datatable-x-scroller .yui3-datatable-table,.yui3-skin-sam .yui3-datatable-y-scroller .yui3-datatable-table{border:0 none}#yui3-css-stamp.skin-sam-datatable-scroll{display:none}
diff --git a/js/yui3/datatable-scroll/datatable-scroll-min.js b/js/yui3/datatable-scroll/datatable-scroll-min.js
new file mode 100644
index 000000000..9476e9254
--- /dev/null
+++ b/js/yui3/datatable-scroll/datatable-scroll-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatable-scroll",function(e,t){function u(e,t){return parseInt(e.getComputedStyle(t),10)||0}var n=e.Lang,r=n.isString,i=n.isNumber,s=n.isArray,o;e.DataTable.Scrollable=o=function(){},o.ATTRS={scrollable:{value:!1,setter:"_setScrollable"}},e.mix(o.prototype,{scrollTo:function(e){var t;return e&&this._tbodyNode&&(this._yScrollNode||this._xScrollNode)&&(s(e)?t=this.getCell(e):i(e)?t=this.getRow(e):r(e)?t=this._tbodyNode.one("#"+e):e._node&&e.ancestor(".yui3-datatable")===this.get("boundingBox")&&(t=e),t&&t.scrollIntoView()),this},_CAPTION_TABLE_TEMPLATE:'<table class="{className}" role="presentation"></table>',_SCROLL_LINER_TEMPLATE:'<div class="{className}"></div>',_SCROLLBAR_TEMPLATE:'<div class="{className}"><div></div></div>',_X_SCROLLER_TEMPLATE:'<div class="{className}"></div>',_Y_SCROLL_HEADER_TEMPLATE:'<table cellspacing="0" aria-hidden="true" class="{className}"></table>',_Y_SCROLLER_TEMPLATE:'<div class="{className}"><div class="{scrollerClassName}"></div></div>',_addScrollbarPadding:function(){var t=this._yScrollHeader,n="."+this.getClassName("header"),r,i,s,o,u;if(t){r=e.DOM.getScrollbarWidth()+"px",i=t.all("tr");for(o=0,u=i.size();o<u;o+=+s.get("rowSpan"))s=i.item(o).all(n).pop(),s.setStyle("paddingRight",r)}},_afterScrollableChange:function(){var t=this._xScrollNode;this._xScroll&&t&&(this._yScroll&&!this._yScrollNode?t.setStyle("paddingRight",e.DOM.getScrollbarWidth()+"px"):!this._yScroll&&this._yScrollNode&&t.setStyle("paddingRight","")),this._syncScrollUI()},_afterScrollCaptionChange:function(){(this._xScroll||this._yScroll)&&this._syncScrollUI()},_afterScrollColumnsChange:function(){if(this._xScroll||this._yScroll)this._yScroll&&this._yScrollHeader&&this._syncScrollHeaders(),this._syncScrollUI()},_afterScrollDataChange:function(){(this._xScroll||this._yScroll)&&this._syncScrollUI()},_afterScrollHeightChange:function(){this._yScroll&&this._syncScrollUI()},_afterScrollSort:function(){var e,t;this._yScroll&&this._yScrollHeader&&(t="."+this.getClassName("header"),e=this._theadNode.all(t),this._yScrollHeader.all(t).each(function(t,n){t.set("className",e.item(n).get("className"))}))},_afterScrollWidthChange:function(){(this._xScroll||this._yScroll)&&this._syncScrollUI()},_bindScrollbar:function(){var t=this._scrollbarNode,n=this._yScrollNode;t&&n&&!this._scrollbarEventHandle&&(this._scrollbarEventHandle=new e.Event.Handle([t.on("scroll",this._syncScrollPosition,this),n.on("scroll",this._syncScrollPosition,this)]))},_bindScrollResize:function(){this._scrollResizeHandle||(this._scrollResizeHandle=e.on("resize",this._syncScrollUI,null,this))},_bindScrollUI:function(){this.after({columnsChange:e.bind("_afterScrollColumnsChange",this),heightChange:e.bind("_afterScrollHeightChange",this),widthChange:e.bind("_afterScrollWidthChange",this),captionChange:e.bind("_afterScrollCaptionChange",this),scrollableChange:e.bind("_afterScrollableChange",this),sort:e.bind("_afterScrollSort",this)}),this.after(["dataChange","*:add","*:remove","*:reset","*:change"],e.bind("_afterScrollDataChange",this))},_clearScrollLock:function(){this._scrollLock&&(this._scrollLock.cancel(),delete this._scrollLock)},_createScrollbar:function(){var t=this._scrollbarNode;return t||(t=this._scrollbarNode=e.Node.create(e.Lang.sub(this._SCROLLBAR_TEMPLATE,{className:this.getClassName("scrollbar")})),t.setStyle("width",e.DOM.getScrollbarWidth()+1+"px")),t},_createScrollCaptionTable:function(){return this._captionTable||(this._captionTable=e.Node.create(e.Lang.sub(this._CAPTION_TABLE_TEMPLATE,{className:this.getClassName("caption","table")})),this._captionTable.empty()),this._captionTable},_createXScrollNode:function(){return this._xScrollNode||(this._xScrollNode=e.Node.create(e.Lang.sub(this._X_SCROLLER_TEMPLATE,{className:this.getClassName("x","scroller")}))),this._xScrollNode},_createYScrollHeader:function(){var t=this._yScrollHeader;return t||(t=this._yScrollHeader=e.Node.create(e.Lang.sub(this._Y_SCROLL_HEADER_TEMPLATE,{className:this.getClassName("scroll","columns")}))),t},_createYScrollNode:function(){var t;return this._yScrollNode||(t=this.getClassName("y","scroller"),this._yScrollContainer=e.Node.create(e.Lang.sub(this._Y_SCROLLER_TEMPLATE,{className:this.getClassName("y","scroller","container"),scrollerClassName:t})),this._yScrollNode=this._yScrollContainer.one("."+t)),this._yScrollContainer},_disableScrolling:function(){this._removeScrollCaptionTable(),this._disableXScrolling(),this._disableYScrolling(),this._unbindScrollResize(),this._uiSetWidth(this.get("width"))},_disableXScrolling:function(){this._removeXScrollNode()},_disableYScrolling:function(){this._removeYScrollHeader(),this._removeYScrollNode(),this._removeYScrollContainer(),this._removeScrollbar()},destructor:function(){this._unbindScrollbar(),this._unbindScrollResize(),this._clearScrollLock()},initializer:function(){this._setScrollProperties(),this.after(["scrollableChange","heightChange","widthChange"],this._setScrollProperties),this.after("renderView",e.bind("_syncScrollUI",this)),e.Do.after(this._bindScrollUI,this,"bindUI")},_removeScrollCaptionTable:function(){this._captionTable&&(this._captionNode&&this._tableNode.prepend(this._captionNode),this._captionTable.remove().destroy(!0),delete this._captionTable)},_removeXScrollNode:function(){var e=this._xScrollNode;e&&(e.replace(e.get("childNodes").toFrag()),e.remove().destroy(!0),delete this._xScrollNode)},_removeYScrollContainer:function(){var e=this._yScrollContainer;e&&(e.replace(e.get("childNodes").toFrag()),e.remove().destroy(!0),delete this._yScrollContainer)},_removeYScrollHeader:function(){this._yScrollHeader&&(this._yScrollHeader.remove().destroy(!0),delete this._yScrollHeader)},_removeYScrollNode:function(){var e=this._yScrollNode;e&&(e.replace(e.get("childNodes").toFrag()),e.remove().destroy(!0),delete this._yScrollNode)},_removeScrollbar:function(){this._scrollbarNode&&(this._scrollbarNode.remove().destroy(!0),delete this._scrollbarNode),this._scrollbarEventHandle&&(this._scrollbarEventHandle
+.detach(),delete this._scrollbarEventHandle)},_setScrollable:function(t){return t===!0&&(t="xy"),r(t)&&(t=t.toLowerCase()),t===!1||t==="y"||t==="x"||t==="xy"?t:e.Attribute.INVALID_VALUE},_setScrollProperties:function(){var e=this.get("scrollable")||"",t=this.get("width"),n=this.get("height");this._xScroll=t&&e.indexOf("x")>-1,this._yScroll=n&&e.indexOf("y")>-1},_syncScrollPosition:function(t){var n=this._scrollbarNode,r=this._yScrollNode,i=t.currentTarget,s;if(n&&r){if(this._scrollLock&&this._scrollLock.source!==i)return;this._clearScrollLock(),this._scrollLock=e.later(300,this,this._clearScrollLock),this._scrollLock.source=i,s=i===n?r:n,s.set("scrollTop",i.get("scrollTop"))}},_syncScrollCaptionUI:function(){var t=this._captionNode,n=this._tableNode,r=this._captionTable,i;t?(i=t.getAttribute("id"),r||(r=this._createScrollCaptionTable(),this.get("contentBox").prepend(r)),t.get("parentNode").compareTo(r)||(r.empty().insert(t),i||(i=e.stamp(t),t.setAttribute("id",i)),n.setAttribute("aria-describedby",i))):r&&this._removeScrollCaptionTable()},_syncScrollColumnWidths:function(){var t=[];this._theadNode&&this._yScrollHeader&&(this._theadNode.all("."+this.getClassName("header")).each(function(n){t.push(e.UA.ie&&e.UA.ie<8?n.get("clientWidth")-u(n,"paddingLeft")-u(n,"paddingRight")+"px":n.getComputedStyle("width"))}),this._yScrollHeader.all("."+this.getClassName("scroll","liner")).each(function(e,n){e.setStyle("width",t[n])}))},_syncScrollHeaders:function(){var t=this._yScrollHeader,n=this._SCROLL_LINER_TEMPLATE,r=this.getClassName("scroll","liner"),i=this.getClassName("header"),s=this._theadNode.all("."+i);this._theadNode&&t&&(t.empty().appendChild(this._theadNode.cloneNode(!0)),t.all("[id]").removeAttribute("id"),t.all("."+i).each(function(t,i){var o=e.Node.create(e.Lang.sub(n,{className:r})),u=s.item(i);o.setStyle("padding",u.getComputedStyle("paddingTop")+" "+u.getComputedStyle("paddingRight")+" "+u.getComputedStyle("paddingBottom")+" "+u.getComputedStyle("paddingLeft")),o.appendChild(t.get("childNodes").toFrag()),t.appendChild(o)},this),this._syncScrollColumnWidths(),this._addScrollbarPadding())},_syncScrollUI:function(){var e=this._xScroll,t=this._yScroll,n=this._xScrollNode,r=this._yScrollNode,i=n&&n.get("scrollLeft"),s=r&&r.get("scrollTop");this._uiSetScrollable(),e||t?((this.get("width")||"").slice(-1)==="%"?this._bindScrollResize():this._unbindScrollResize(),this._syncScrollCaptionUI()):this._disableScrolling(),this._yScrollHeader&&this._yScrollHeader.setStyle("display","none"),e&&(t||this._disableYScrolling(),this._syncXScrollUI(t)),t&&(e||this._disableXScrolling(),this._syncYScrollUI(e)),i&&this._xScrollNode&&this._xScrollNode.set("scrollLeft",i),s&&this._yScrollNode&&this._yScrollNode.set("scrollTop",s)},_syncXScrollUI:function(t){var n=this._xScrollNode,r=this._yScrollContainer,i=this._tableNode,s=this.get("width"),o=this.get("boundingBox").get("offsetWidth"),a=e.DOM.getScrollbarWidth(),f,l;n||(n=this._createXScrollNode(),(r||i).replace(n).appendTo(n)),f=u(n,"borderLeftWidth")+u(n,"borderRightWidth"),n.setStyle("width",""),this._uiSetDim("width",""),t&&this._yScrollContainer&&this._yScrollContainer.setStyle("width",""),e.UA.ie&&e.UA.ie<8&&(i.setStyle("width",s),i.get("offsetWidth")),i.setStyle("width",""),l=i.get("offsetWidth"),i.setStyle("width",l+"px"),this._uiSetDim("width",s),n.setStyle("width",o-f+"px"),n.get("offsetWidth")-f>l&&(t?i.setStyle("width",n.get("offsetWidth")-f-a+"px"):i.setStyle("width","100%"))},_syncYScrollUI:function(t){var n=this._yScrollContainer,r=this._yScrollNode,i=this._xScrollNode,s=this._yScrollHeader,o=this._scrollbarNode,a=this._tableNode,f=this._theadNode,l=this._captionTable,c=this.get("boundingBox"),h=this.get("contentBox"),p=this.get("width"),d=c.get("offsetHeight"),v=e.DOM.getScrollbarWidth(),m;l&&!t&&l.setStyle("width",p||"100%"),n||(n=this._createYScrollNode(),r=this._yScrollNode,a.replace(n).appendTo(r)),m=t?i:n,t||a.setStyle("width",""),t&&(d-=v),r.setStyle("height",d-m.get("offsetTop")-u(m,"borderTopWidth")-u(m,"borderBottomWidth")+"px"),t?n.setStyle("width",a.get("offsetWidth")+v+"px"):this._uiSetYScrollWidth(p),l&&!t&&l.setStyle("width",n.get("offsetWidth")+"px"),f&&!s&&(s=this._createYScrollHeader(),n.prepend(s),this._syncScrollHeaders()),s&&(this._syncScrollColumnWidths(),s.setStyle("display",""),o||(o=this._createScrollbar(),this._bindScrollbar(),h.prepend(o)),this._uiSetScrollbarHeight(),this._uiSetScrollbarPosition(m))},_uiSetScrollable:function(){this.get("boundingBox").toggleClass(this.getClassName("scrollable","x"),this._xScroll).toggleClass(this.getClassName("scrollable","y"),this._yScroll)},_uiSetScrollbarHeight:function(){var e=this._scrollbarNode,t=this._yScrollNode,n=this._yScrollHeader;e&&t&&n&&(e.get("firstChild").setStyle("height",this._tbodyNode.get("scrollHeight")+"px"),e.setStyle("height",parseFloat(t.getComputedStyle("height"))-parseFloat(n.getComputedStyle("height"))+"px"))},_uiSetScrollbarPosition:function(t){var n=this._scrollbarNode,r=this._yScrollHeader;n&&t&&r&&n.setStyles({top:parseFloat(r.getComputedStyle("height"))+u(t,"borderTopWidth")+t.get("offsetTop")+"px",left:t.get("offsetWidth")-e.DOM.getScrollbarWidth()-1-u(t,"borderRightWidth")+"px"})},_uiSetYScrollWidth:function(t){var n=this._yScrollContainer,r=this._tableNode,i,s,o,u;n&&r&&(u=e.DOM.getScrollbarWidth(),t?(s=n.get("offsetWidth")-n.get("clientWidth")+u,n.setStyle("width",t),o=n.get("clientWidth")-s,r.setStyle("width",o+"px"),i=r.get("offsetWidth"),n.setStyle("width",i+u+"px")):(r.setStyle("width",""),n.setStyle("width",""),n.setStyle("width",r.get("offsetWidth")+u+"px")))},_unbindScrollbar:function(){this._scrollbarEventHandle&&this._scrollbarEventHandle.detach()},_unbindScrollResize:function(){this._scrollResizeHandle&&(this._scrollResizeHandle.detach(),delete this._scrollResizeHandle)}},!0),e.Base.mix(e.DataTable,[o])},"3.17.2",{requires:["datatable-base","datatable-column-widths","dom-screen"],skinnable:!0});
diff --git a/js/yui3/datatable-sort/assets/datatable-sort-core.css b/js/yui3/datatable-sort/assets/datatable-sort-core.css
new file mode 100644
index 000000000..6a5a3d720
--- /dev/null
+++ b/js/yui3/datatable-sort/assets/datatable-sort-core.css
@@ -0,0 +1,24 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* foundational CSS */
+.yui3-datatable-sortable-column {
+ z-index: 1;
+}
+.yui3-datatable-sortable-column:focus,
+.yui3-datatable-sortable-column:active {
+ /* So the focus ring isn't masked by the surrounding elements */
+ z-index: 2;
+}
+.yui3-datatable-sort-liner {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+} \ No newline at end of file
diff --git a/js/yui3/datatable-sort/assets/skins/night/datatable-sort.css b/js/yui3/datatable-sort/assets/skins/night/datatable-sort.css
new file mode 100644
index 000000000..e755d381d
--- /dev/null
+++ b/js/yui3/datatable-sort/assets/skins/night/datatable-sort.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-sortable-column{z-index:1}.yui3-datatable-sortable-column:focus,.yui3-datatable-sortable-column:active{z-index:2}.yui3-datatable-sort-liner{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.yui3-skin-night .yui3-datatable-sortable-column{cursor:pointer}.yui3-skin-night .yui3-datatable-columns .yui3-datatable-sorted,.yui3-skin-night .yui3-datatable-sortable-column:hover{background-color:#4d4e4f;*background:#505152 url(../../../../assets/skins/night/sprite.png) repeat-x 0 -100px;background-image:-webkit-gradient(linear,0 0,0 100%,from(rgba(255,255,255,0.2)),color-stop(40%,rgba(255,255,255,0.1)),color-stop(80%,rgba(255,255,255,0.01)),to(transparent));background-image:-webkit-linear-gradient(rgba(255,255,255,0.2),rgba(255,255,255,0.1) 40%,rgba(255,255,255,0.01) 80%,transparent);background-image:-moz-linear-gradient(top,rgba(255,255,255,0.2),rgba(255,255,255,0.1) 40%,rgba(255,255,255,0.01) 80%,transparent);background-image:-ms-linear-gradient(rgba(255,255,255,0.2),rgba(255,255,255,0.1) 40%,rgba(255,255,255,0.01) 80%,transparent);background-image:-o-linear-gradient(rgba(255,255,255,0.2),rgba(255,255,255,0.1) 40%,rgba(255,255,255,0.01) 80%,transparent);background-image:linear-gradient(rgba(255,255,255,0.2),rgba(255,255,255,0.1) 40%,rgba(255,255,255,0.01) 80%,transparent)}.yui3-skin-night .yui3-datatable-sort-liner{display:block;height:100%;position:relative;padding-right:15px;position:relative}.yui3-skin-night .yui3-datatable-sort-indicator{position:absolute;right:0;bottom:.5ex;width:7px;height:10px;background:url(sort-arrow-sprite.png) no-repeat 0 0;_background:url(sort-arrow-sprite-ie.png) no-repeat 0 0;overflow:hidden}.yui3-skin-night .yui3-datatable-sorted .yui3-datatable-sort-indicator{background-position:0 -10px}.yui3-skin-night .yui3-datatable-sorted-desc .yui3-datatable-sort-indicator{background-position:0 -20px}.yui3-skin-night .yui3-datatable-data .yui3-datatable-even .yui3-datatable-sorted{background-color:#262626;color:#b3b2b2}.yui3-skin-night .yui3-datatable-data .yui3-datatable-odd .yui3-datatable-sorted{background-color:#393a3a;color:#cbcbcb}#yui3-css-stamp.skin-night-datatable-sort{display:none}
diff --git a/js/yui3/datatable-sort/assets/skins/night/sort-arrow-sprite-ie.png b/js/yui3/datatable-sort/assets/skins/night/sort-arrow-sprite-ie.png
new file mode 100644
index 000000000..9ced28945
--- /dev/null
+++ b/js/yui3/datatable-sort/assets/skins/night/sort-arrow-sprite-ie.png
Binary files differ
diff --git a/js/yui3/datatable-sort/assets/skins/night/sort-arrow-sprite.png b/js/yui3/datatable-sort/assets/skins/night/sort-arrow-sprite.png
new file mode 100644
index 000000000..c4befbc9a
--- /dev/null
+++ b/js/yui3/datatable-sort/assets/skins/night/sort-arrow-sprite.png
Binary files differ
diff --git a/js/yui3/datatable-sort/assets/skins/sam/datatable-sort.css b/js/yui3/datatable-sort/assets/skins/sam/datatable-sort.css
new file mode 100644
index 000000000..9326f550d
--- /dev/null
+++ b/js/yui3/datatable-sort/assets/skins/sam/datatable-sort.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-datatable-sortable-column{z-index:1}.yui3-datatable-sortable-column:focus,.yui3-datatable-sortable-column:active{z-index:2}.yui3-datatable-sort-liner{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.yui3-skin-sam .yui3-datatable-sortable-column{cursor:pointer}.yui3-skin-sam .yui3-datatable-columns .yui3-datatable-sorted,.yui3-skin-sam .yui3-datatable-sortable-column:hover{*background:#c1c4c8 url(../../../../assets/skins/sam/sprite.png) repeat-x 0 -100px;background-color:#f1f2f3}.yui3-skin-sam .yui3-datatable-sort-liner{display:block;height:100%;position:relative;padding-right:15px;position:relative}.yui3-skin-sam .yui3-datatable-sort-indicator{position:absolute;right:0;bottom:.5ex;width:7px;height:10px;background:url(sort-arrow-sprite.png) no-repeat 0 0;_background:url(sort-arrow-sprite-ie.png) no-repeat 0 0;overflow:hidden}.yui3-skin-sam .yui3-datatable-sorted .yui3-datatable-sort-indicator{background-position:0 -10px}.yui3-skin-sam .yui3-datatable-sorted-desc .yui3-datatable-sort-indicator{background-position:0 -20px}.yui3-skin-sam .yui3-datatable-data .yui3-datatable-even .yui3-datatable-sorted{background-color:#edf5ff}.yui3-skin-sam .yui3-datatable-data .yui3-datatable-odd .yui3-datatable-sorted{background-color:#dbeaff}#yui3-css-stamp.skin-sam-datatable-sort{display:none}
diff --git a/js/yui3/datatable-sort/assets/skins/sam/sort-arrow-sprite-ie.png b/js/yui3/datatable-sort/assets/skins/sam/sort-arrow-sprite-ie.png
new file mode 100644
index 000000000..f1f6576ab
--- /dev/null
+++ b/js/yui3/datatable-sort/assets/skins/sam/sort-arrow-sprite-ie.png
Binary files differ
diff --git a/js/yui3/datatable-sort/assets/skins/sam/sort-arrow-sprite.png b/js/yui3/datatable-sort/assets/skins/sam/sort-arrow-sprite.png
new file mode 100644
index 000000000..47b8b1550
--- /dev/null
+++ b/js/yui3/datatable-sort/assets/skins/sam/sort-arrow-sprite.png
Binary files differ
diff --git a/js/yui3/datatable-sort/datatable-sort-min.js b/js/yui3/datatable-sort/datatable-sort-min.js
new file mode 100644
index 000000000..b6d8070a0
--- /dev/null
+++ b/js/yui3/datatable-sort/datatable-sort-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatable-sort",function(e,t){function l(){}var n=e.Lang,r=n.isBoolean,i=n.isString,s=n.isArray,o=n.isObject,u=e.Array,a=n.sub,f={asc:1,desc:-1,1:1,"-1":-1};l.ATTRS={sortable:{value:"auto",validator:"_validateSortable"},sortBy:{validator:"_validateSortBy",getter:"_getSortBy"},strings:{}},e.mix(l.prototype,{sort:function(t,n){return this.fire("sort",e.merge(n||{},{sortBy:t||this.get("sortBy")}))},SORTABLE_HEADER_TEMPLATE:'<div class="{className}" tabindex="0" unselectable="on"><span class="{indicatorClass}"></span></div>',toggleSort:function(t,n){var r=this._sortBy,i=[],s,o,a,f,l;for(s=0,o=r.length;s<o;++s)f={},f[r[s]._id]=r[s].sortDir,i.push(f);if(t){t=u(t);for(s=0,o=t.length;s<o;++s){f=t[s],l=-1;for(a=i.length-1;s>=0;--s)if(i[a][f]){i[a][f]*=-1;break}}}else for(s=0,o=i.length;s<o;++s)for(f in i[s])if(i[s].hasOwnProperty(f)){i[s][f]*=-1;break}return this.fire("sort",e.merge(n||{},{sortBy:i}))},_afterSortByChange:function(){this._setSortBy(),this._sortBy.length&&(this.data.comparator||(this.data.comparator=this._sortComparator),this.data.sort())},_afterSortDataChange:function(e){(e.prevVal!==e.newVal||e.newVal.hasOwnProperty("_compare"))&&this._initSortFn()},_afterSortRecordChange:function(e){var t,n;for(t=0,n=this._sortBy.length;t<n;++t)if(e.changed[this._sortBy[t].key]){this.data.sort();break}},_bindSortUI:function(){var t=this._eventHandles;t.sortAttrs||(t.sortAttrs=this.after(["sortableChange","sortByChange","columnsChange"],e.bind("_uiSetSortable",this))),!t.sortUITrigger&&this._theadNode&&(t.sortUITrigger=this.delegate(["click","keydown"],e.rbind("_onUITriggerSort",this),"."+this.getClassName("sortable","column")))},_defSortFn:function(e){this.set.apply(this,["sortBy",e.sortBy].concat(e.details))},_getSortBy:function(e,t){var n,r,i,s;t=t.slice(7);if(t==="state"){n=[];for(r=0,i=this._sortBy.length;r<i;++r)s=this._sortBy[r],n.push({column:s._id,dir:s.sortDir});return{state:n.length===1?n[0]:n}}return e},initializer:function(){var t=e.bind("_parseSortable",this);this._parseSortable(),this._setSortBy(),this._initSortFn(),this._initSortStrings(),this.after({"table:renderHeader":e.bind("_renderSortable",this),dataChange:e.bind("_afterSortDataChange",this),sortByChange:e.bind("_afterSortByChange",this),sortableChange:t,columnsChange:t}),this.data.after(this.data.model.NAME+":change",e.bind("_afterSortRecordChange",this)),this.publish("sort",{defaultFn:e.bind("_defSortFn",this)})},_initSortFn:function(){var e=this;this.data._compare=function(t,n){var r=0,i,s,o,u,a,f,l;for(i=0,s=e._sortBy.length;!r&&i<s;++i)o=e._sortBy[i],u=o.sortDir,a=o.caseSensitive,o.sortFn?r=o.sortFn(t,n,u===-1):(f=t.get(o.key)||"",l=n.get(o.key)||"",!a&&typeof f=="string"&&typeof l=="string"&&(f=f.toLowerCase(),l=l.toLowerCase()),r=f>l?u:f<l?-u:0);return r},this._sortBy.length?(this.data.comparator=this._sortComparator,this.data.sort()):delete this.data.comparator},_initSortStrings:function(){this.set("strings",e.mix(this.get("strings")||{},e.Intl.get("datatable-sort")))},_onUITriggerSort:function(e){var t=e.currentTarget.getAttribute("data-yui3-col-id"),n=t&&this.getColumn(t),r,i,s;if(e.type==="keydown"&&e.keyCode!==32)return;e.preventDefault();if(n){if(e.shiftKey){r=this.get("sortBy")||[];for(i=0,s=r.length;i<s;++i)if(t===r[i]||Math.abs(r[i][t])===1){o(r[i])||(r[i]={}),r[i][t]=-(n.sortDir||0)||1;break}i>=s&&r.push(n._id)}else r=[{}],r[0][t]=-(n.sortDir||0)||1;this.fire("sort",{originEvent:e,sortBy:r})}},_parseSortable:function(){var e=this.get("sortable"),t=[],n,r,i;if(s(e))for(n=0,r=e.length;n<r;++n){i=e[n];if(!o(i,!0)||s(i))i=this.getColumn(i);i&&t.push(i)}else if(e){t=this._displayColumns.slice();if(e==="auto")for(n=t.length-1;n>=0;--n)t[n].sortable||t.splice(n,1)}this._sortable=t},_renderSortable:function(){this._uiSetSortable(),this._bindSortUI()},_setSortBy:function(){var e=this._displayColumns,t=this.get("sortBy")||[],n=" "+this.getClassName("sorted"),r,i,s,a,l,c;this._sortBy=[];for(r=0,i=e.length;r<i;++r)c=e[r],delete c.sortDir,c.className&&(c.className=c.className.replace(n,""));t=u(t);for(r=0,i=t.length;r<i;++r){s=t[r],a=1;if(o(s)){l=s;for(s in l)if(l.hasOwnProperty(s)){a=f[l[s]];break}}s&&(c=this.getColumn(s)||{_id:s,key:s},c&&(c.sortDir=a,c.className||(c.className=""),c.className+=n,this._sortBy.push(c)))}},_sortComparator:function(e){return e},_uiSetSortable:function(){var t=this._sortable||[],n=this.getClassName("sortable","column"),r=this.getClassName("sorted"),i=this.getClassName("sorted","desc"),s=this.getClassName("sort","liner"),o=this.getClassName("sort","indicator"),u={},f,l,c,h,p,d,v;this.get("boundingBox").toggleClass(this.getClassName("sortable"),t.length);for(f=0,l=t.length;f<l;++f)u[t[f].id]=t[f];this._theadNode.all("."+n).each(function(e){var t=u[e.get("id")],a=e.one("."+s),f;t?t.sortDir||e.removeClass(r).removeClass(i):(e.removeClass(n).removeClass(r).removeClass(i),a&&a.replace(a.get("childNodes").toFrag()),f=e.one("."+o),f&&f.remove().destroy(!0))});for(f=0,l=t.length;f<l;++f)c=t[f],h=this._theadNode.one("#"+c.id),v=c.sortDir===-1,h&&(p=h.one("."+s),h.addClass(n),c.sortDir&&(h.addClass(r),h.toggleClass(i,v),h.setAttribute("aria-sort",v?"descending":"ascending")),p||(p=e.Node.create(e.Lang.sub(this.SORTABLE_HEADER_TEMPLATE,{className:s,indicatorClass:o})),p.prepend(h.get("childNodes").toFrag()),h.append(p)),d=a(this.getString(c.sortDir===1?"reverseSortBy":"sortBy"),{title:c.title||"",key:c.key||"",abbr:c.abbr||"",label:c.label||"",column:c.abbr||c.label||c.key||"column "+f}),h.setAttribute("title",d),h.setAttribute("aria-labelledby",c.id))},_validateSortable:function(e){return e==="auto"||r(e)||s(e)},_validateSortBy:function(e){return e===null||i(e)||o(e,!0)||s(e)&&(i(e[0])||o(e,!0))}},!0),e.DataTable.Sortable=l,e.Base.mix(e.DataTable,[l])},"3.17.2",{requires:["datatable-base"],lang:["en","fr","es","hu"],skinnable:!0});
diff --git a/js/yui3/datatable-sort/lang/datatable-sort.js b/js/yui3/datatable-sort/lang/datatable-sort.js
new file mode 100644
index 000000000..af050a98b
--- /dev/null
+++ b/js/yui3/datatable-sort/lang/datatable-sort.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatable-sort",function(e){e.Intl.add("datatable-sort","",{asc:"Ascending",desc:"Descending",sortBy:"Sort by {column}",reverseSortBy:"Reverse sort by {column}"})},"3.17.2");
diff --git a/js/yui3/datatable-sort/lang/datatable-sort_en.js b/js/yui3/datatable-sort/lang/datatable-sort_en.js
new file mode 100644
index 000000000..c9c3d388c
--- /dev/null
+++ b/js/yui3/datatable-sort/lang/datatable-sort_en.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatable-sort_en",function(e){e.Intl.add("datatable-sort","en",{asc:"Ascending",desc:"Descending",sortBy:"Sort by {column}",reverseSortBy:"Reverse sort by {column}"})},"3.17.2");
diff --git a/js/yui3/datatable-sort/lang/datatable-sort_es.js b/js/yui3/datatable-sort/lang/datatable-sort_es.js
new file mode 100644
index 000000000..e064910d2
--- /dev/null
+++ b/js/yui3/datatable-sort/lang/datatable-sort_es.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatable-sort_es",function(e){e.Intl.add("datatable-sort","es",{asc:"Ascendente",desc:"Descendente",sortBy:"Ordenar por {column}",reverseSortBy:"Ordenar descendente por {column}"})},"3.17.2");
diff --git a/js/yui3/datatable-sort/lang/datatable-sort_fr.js b/js/yui3/datatable-sort/lang/datatable-sort_fr.js
new file mode 100644
index 000000000..de4c6a556
--- /dev/null
+++ b/js/yui3/datatable-sort/lang/datatable-sort_fr.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatable-sort_fr",function(e){e.Intl.add("datatable-sort","fr",{asc:"Croissant",desc:"D\u00e9croissant",sortBy:"Trier par {column}",reverseSortBy:"Trier par {column} dans l'ordre d\u00e9croissant"})},"3.17.2");
diff --git a/js/yui3/datatable-sort/lang/datatable-sort_hu.js b/js/yui3/datatable-sort/lang/datatable-sort_hu.js
new file mode 100644
index 000000000..2810d9c26
--- /dev/null
+++ b/js/yui3/datatable-sort/lang/datatable-sort_hu.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatable-sort_hu",function(e){e.Intl.add("datatable-sort","hu",{asc:"N\u00f6vekv\u0151",desc:"Cs\u00f6kken\u0151",sortBy:"Sorrend: {column}",reverseSortBy:"Ford\u00edtott sorrend: {column}"})},"3.17.2");
diff --git a/js/yui3/datatable-table/datatable-table-min.js b/js/yui3/datatable-table/datatable-table-min.js
new file mode 100644
index 000000000..5d96571af
--- /dev/null
+++ b/js/yui3/datatable-table/datatable-table-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatable-table",function(e,t){var n=e.Array,r=e.Lang,i=r.sub,s=r.isArray,o=r.isFunction;e.namespace("DataTable").TableView=e.Base.create("table",e.View,[],{CAPTION_TEMPLATE:'<caption class="{className}"></caption>',TABLE_TEMPLATE:'<table cellspacing="0" class="{className}"></table>',getCell:function(){return this.body&&this.body.getCell&&this.body.getCell.apply(this.body,arguments)},getClassName:function(){var t=this.host,r=t&&t.constructor.NAME||this.constructor.NAME;return t&&t.getClassName?t.getClassName.apply(t,arguments):e.ClassNameManager.getClassName.apply(e.ClassNameManager,[r].concat(n(arguments,0,!0)))},getRecord:function(){return this.body&&this.body.getRecord&&this.body.getRecord.apply(this.body,arguments)},getRow:function(){return this.body&&this.body.getRow&&this.body.getRow.apply(this.body,arguments)},_afterSummaryChange:function(e){this._uiSetSummary(e.newVal)},_afterCaptionChange:function(e){this._uiSetCaption(e.newVal)},_afterWidthChange:function(e){this._uiSetWidth(e.newVal)},_bindUI:function(){var t;this._eventHandles||(t=e.bind("_relayAttrChange",this),this._eventHandles=this.after({columnsChange:t,modelListChange:t,summaryChange:e.bind("_afterSummaryChange",this),captionChange:e.bind("_afterCaptionChange",this),widthChange:e.bind("_afterWidthChange",this)}))},_createTable:function(){return e.Node.create(i(this.TABLE_TEMPLATE,{className:this.getClassName("table")})).empty()},_defRenderBodyFn:function(e){e.view.render()},_defRenderFooterFn:function(e){e.view.render()},_defRenderHeaderFn:function(e){e.view.render()},_defRenderTableFn:function(t){var n=this.get("container"),r=this.getAttrs();this.tableNode||(this.tableNode=this._createTable()),r.host=this.get("host")||this,r.table=this,r.container=this.tableNode,this._uiSetCaption(this.get("caption")),this._uiSetSummary(this.get("summary")),this._uiSetWidth(this.get("width"));if(this.head||t.headerView)this.head||(this.head=new t.headerView(e.merge(r,t.headerConfig))),this.fire("renderHeader",{view:this.head});if(this.foot||t.footerView)this.foot||(this.foot=new t.footerView(e.merge(r,t.footerConfig))),this.fire("renderFooter",{view:this.foot});r.columns=this.displayColumns;if(this.body||t.bodyView)this.body||(this.body=new t.bodyView(e.merge(r,t.bodyConfig))),this.fire("renderBody",{view:this.body});n.contains(this.tableNode)||n.append(this.tableNode),this._bindUI()},destructor:function(){this.head&&this.head.destroy&&this.head.destroy(),delete this.head,this.foot&&this.foot.destroy&&this.foot.destroy(),delete this.foot,this.body&&this.body.destroy&&this.body.destroy(),delete this.body,this._eventHandles&&(this._eventHandles.detach(),delete this._eventHandles),this.tableNode&&this.tableNode.remove().destroy(!0)},_extractDisplayColumns:function(){function n(e){var r,i,o;for(r=0,i=e.length;r<i;++r)o=e[r],s(o.children)?n(o.children):t.push(o)}var e=this.get("columns"),t=[];e&&n(e),this.displayColumns=t},_initEvents:function(){this.publish({renderTable:{defaultFn:e.bind("_defRenderTableFn",this)},renderHeader:{defaultFn:e.bind("_defRenderHeaderFn",this)},renderBody:{defaultFn:e.bind("_defRenderBodyFn",this)},renderFooter:{defaultFn:e.bind("_defRenderFooterFn",this)}})},initializer:function(e){this.host=e.host,this._initEvents(),this._extractDisplayColumns(),this.after("columnsChange",this._extractDisplayColumns,this)},_relayAttrChange:function(e){var t=e.attrName,n=e.newVal;this.head&&this.head.set(t,n),this.foot&&this.foot.set(t,n),this.body&&(t==="columns"&&(n=this.displayColumns),this.body.set(t,n))},render:function(){return this.get("container")&&this.fire("renderTable",{headerView:this.get("headerView"),headerConfig:this.get("headerConfig"),bodyView:this.get("bodyView"),bodyConfig:this.get("bodyConfig"),footerView:this.get("footerView"),footerConfig:this.get("footerConfig")}),this},_uiSetCaption:function(t){var n=this.tableNode,r=this.captionNode;t?(r||(this.captionNode=r=e.Node.create(i(this.CAPTION_TEMPLATE,{className:this.getClassName("caption")})),n.prepend(this.captionNode)),r.setHTML(t)):r&&(r.remove(!0),delete this.captionNode)},_uiSetSummary:function(e){e?this.tableNode.setAttribute("summary",e):this.tableNode.removeAttribute("summary")},_uiSetWidth:function(e){var t=this.tableNode;t.setStyle("width",e?this.get("container").get("offsetWidth")-(parseInt(t.getComputedStyle("borderLeftWidth"),10)||0)-(parseInt(t.getComputedStyle("borderLeftWidth"),10)||0)+"px":""),t.setStyle("width",e)},_validateView:function(e){return o(e)&&e.prototype.render}},{ATTRS:{columns:{validator:s},width:{value:"",validator:r.isString},headerView:{value:e.DataTable.HeaderView,validator:"_validateView"},footerView:{validator:"_validateView"},bodyView:{value:e.DataTable.BodyView,validator:"_validateView"}}})},"3.17.2",{requires:["datatable-core","datatable-head","datatable-body","view","classnamemanager"]});
diff --git a/js/yui3/datatype-date-format/datatype-date-format-min.js b/js/yui3/datatype-date-format/datatype-date-format-min.js
new file mode 100644
index 000000000..1f72e6cca
--- /dev/null
+++ b/js/yui3/datatype-date-format/datatype-date-format-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatype-date-format",function(e,t){var n=function(e,t,n){typeof n=="undefined"&&(n=10),t+="";for(;parseInt(e,10)<n&&n>1;n/=10)e=t+e;return e.toString()},r={formats:{a:function(e,t){return t.a[e.getDay()]},A:function(e,t){return t.A[e.getDay()]},b:function(e,t){return t.b[e.getMonth()]},B:function(e,t){return t.B[e.getMonth()]},C:function(e){return n(parseInt(e.getFullYear()/100,10),0)},d:["getDate","0"],e:["getDate"," "],g:function(e){return n(parseInt(r.formats.G(e)%100,10),0)},G:function(e){var t=e.getFullYear(),n=parseInt(r.formats.V(e),10),i=parseInt(r.formats.W(e),10);return i>n?t++:i===0&&n>=52&&t--,t},H:["getHours","0"],I:function(e){var t=e.getHours()%12;return n(t===0?12:t,0)},j:function(e){var t=new Date(""+e.getFullYear()+"/1/1 GMT"),r=new Date(""+e.getFullYear()+"/"+(e.getMonth()+1)+"/"+e.getDate()+" GMT"),i=r-t,s=parseInt(i/6e4/60/24,10)+1;return n(s,0,100)},k:["getHours"," "],l:function(e){var t=e.getHours()%12;return n(t===0?12:t," ")},m:function(e){return n(e.getMonth()+1,0)},M:["getMinutes","0"],p:function(e,t){return t.p[e.getHours()>=12?1:0]},P:function(e,t){return t.P[e.getHours()>=12?1:0]},s:function(e,t){return parseInt(e.getTime()/1e3,10)},S:["getSeconds","0"],u:function(e){var t=e.getDay();return t===0?7:t},U:function(e){var t=parseInt(r.formats.j(e),10),i=6-e.getDay(),s=parseInt((t+i)/7,10);return n(s,0)},V:function(e){var t=parseInt(r.formats.W(e),10),i=(new Date(""+e.getFullYear()+"/1/1")).getDay(),s=t+(i>4||i<=1?0:1);return s===53&&(new Date(""+e.getFullYear()+"/12/31")).getDay()<4?s=1:s===0&&(s=r.formats.V(new Date(""+(e.getFullYear()-1)+"/12/31"))),n(s,0)},w:"getDay",W:function(e){var t=parseInt(r.formats.j(e),10),i=7-r.formats.u(e),s=parseInt((t+i)/7,10);return n(s,0,10)},y:function(e){return n(e.getFullYear()%100,0)},Y:"getFullYear",z:function(e){var t=e.getTimezoneOffset(),r=n(parseInt(Math.abs(t/60),10),0),i=n(Math.abs(t%60),0);return(t>0?"-":"+")+r+i},Z:function(e){var t=e.toString().replace(/^.*:\d\d( GMT[+-]\d+)? \(?([A-Za-z ]+)\)?\d*$/,"$2").replace(/[a-z ]/g,"");return t.length>4&&(t=r.formats.z(e)),t},"%":function(e){return"%"}},aggregates:{c:"locale",D:"%m/%d/%y",F:"%Y-%m-%d",h:"%b",n:"\n",r:"%I:%M:%S %p",R:"%H:%M",t:" ",T:"%H:%M:%S",x:"locale",X:"locale"},format:function(t,i){i=i||{};if(!e.Lang.isDate(t))return e.Lang.isValue(t)?t:"";var s,o,u,a,f;s=i.format||"%Y-%m-%d",o=e.Intl.get("datatype-date-format");var l=function(e,t){if(u&&t==="r")return o[t];var n=r.aggregates[t];return n==="locale"?o[t]:n},c=function(i,s){var u=r.formats[s];switch(e.Lang.type(u)){case"string":return t[u]();case"function":return u.call(t,t,o);case"array":if(e.Lang.type(u[0])==="string")return n(t[u[0]](),u[1]);default:return s}};while(s.match(/%[cDFhnrRtTxX]/))s=s.replace(/%([cDFhnrRtTxX])/g,l);var h=s.replace(/%([aAbBCdegGHIjklmMpPsSuUVwWyYzZ%])/g,c);return l=c=undefined,h}};e.mix(e.namespace("Date"),r),e.namespace("DataType"),e.DataType.Date=e.Date},"3.17.2",{lang:["ar","ar-JO","ca","ca-ES","da","da-DK","de","de-AT","de-DE","el","el-GR","en","en-AU","en-CA","en-GB","en-IE","en-IN","en-JO","en-MY","en-NZ","en-PH","en-SG","en-US","es","es-AR","es-BO","es-CL","es-CO","es-EC","es-ES","es-MX","es-PE","es-PY","es-US","es-UY","es-VE","fi","fi-FI","fr","fr-BE","fr-CA","fr-FR","hi","hi-IN","hu","id","id-ID","it","it-IT","ja","ja-JP","ko","ko-KR","ms","ms-MY","nb","nb-NO","nl","nl-BE","nl-NL","pl","pl-PL","pt","pt-BR","ro","ro-RO","ru","ru-RU","sv","sv-SE","th","th-TH","tr","tr-TR","vi","vi-VN","zh-Hans","zh-Hans-CN","zh-Hant","zh-Hant-HK","zh-Hant-TW"]});
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format.js b/js/yui3/datatype-date-format/lang/datatype-date-format.js
new file mode 100644
index 000000000..7e3ec19d7
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format",function(e){e.Intl.add("datatype-date-format","",{a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%Y-%m-%dT%H:%M:%S%z",p:["AM","PM"],P:["am","pm"],x:"%Y-%m-%d",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_ar-JO.js b/js/yui3/datatype-date-format/lang/datatype-date-format_ar-JO.js
new file mode 100644
index 000000000..1149b9bfd
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_ar-JO.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_ar-JO",function(e){e.Intl.add("datatype-date-format","ar-JO",{a:["\u0627\u0644\u0623\u062d\u062f","\u0627\u0644\u0627\u062b\u0646\u064a\u0646","\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621","\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621","\u0627\u0644\u062e\u0645\u064a\u0633","\u0627\u0644\u062c\u0645\u0639\u0629","\u0627\u0644\u0633\u0628\u062a"],A:["\u0627\u0644\u0623\u062d\u062f","\u0627\u0644\u0625\u062b\u0646\u064a\u0646","\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621","\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621","\u0627\u0644\u062e\u0645\u064a\u0633","\u0627\u0644\u062c\u0645\u0639\u0629","\u0627\u0644\u0633\u0628\u062a"],b:["\u0643\u0627\u0646\u0648\u0646 \u0627\u0644\u062b\u0627\u0646\u064a","\u0634\u0628\u0627\u0637","\u0622\u0630\u0627\u0631","\u0646\u064a\u0633\u0627\u0646","\u0623\u064a\u0627\u0631","\u062d\u0632\u064a\u0631\u0627\u0646","\u062a\u0645\u0648\u0632","\u0622\u0628","\u0623\u064a\u0644\u0648\u0644","\u062a\u0634\u0631\u064a\u0646 \u0627\u0644\u0623\u0648\u0644","\u062a\u0634\u0631\u064a\u0646 \u0627\u0644\u062b\u0627\u0646\u064a","\u0643\u0627\u0646\u0648\u0646 \u0627\u0644\u0623\u0648\u0644"],B:["\u0643\u0627\u0646\u0648\u0646 \u0627\u0644\u062b\u0627\u0646\u064a","\u0634\u0628\u0627\u0637","\u0622\u0630\u0627\u0631","\u0646\u064a\u0633\u0627\u0646","\u0623\u064a\u0627\u0631","\u062d\u0632\u064a\u0631\u0627\u0646","\u062a\u0645\u0648\u0632","\u0622\u0628","\u0623\u064a\u0644\u0648\u0644","\u062a\u0634\u0631\u064a\u0646 \u0627\u0644\u0623\u0648\u0644","\u062a\u0634\u0631\u064a\u0646 \u0627\u0644\u062b\u0627\u0646\u064a","\u0643\u0627\u0646\u0648\u0646 \u0627\u0644\u0623\u0648\u0644"],c:"%a\u060c %d %B %Y %Z %l:%M:%S %p",p:["\u0635","\u0645"],P:["\u0635","\u0645"],x:"%d\u200f/%m\u200f/%Y",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_ar.js b/js/yui3/datatype-date-format/lang/datatype-date-format_ar.js
new file mode 100644
index 000000000..ab84cca1d
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_ar.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_ar",function(e){e.Intl.add("datatype-date-format","ar",{a:["\u0623\u062d\u062f","\u0625\u062b\u0646\u064a\u0646","\u062b\u0644\u0627\u062b\u0627\u0621","\u0623\u0631\u0628\u0639\u0627\u0621","\u062e\u0645\u064a\u0633","\u062c\u0645\u0639\u0629","\u0633\u0628\u062a"],A:["\u0627\u0644\u0623\u062d\u062f","\u0627\u0644\u0625\u062b\u0646\u064a\u0646","\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621","\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621","\u0627\u0644\u062e\u0645\u064a\u0633","\u0627\u0644\u062c\u0645\u0639\u0629","\u0627\u0644\u0633\u0628\u062a"],b:["\u064a\u0646\u0627\u064a\u0631","\u0641\u0628\u0631\u0627\u064a\u0631","\u0645\u0627\u0631\u0633","\u0623\u0628\u0631\u064a\u0644","\u0645\u0627\u064a\u0648","\u064a\u0648\u0646\u064a\u0648","\u064a\u0648\u0644\u064a\u0648","\u0623\u063a\u0633\u0637\u0633","\u0633\u0628\u062a\u0645\u0628\u0631","\u0623\u0643\u062a\u0648\u0628\u0631","\u0646\u0648\u0641\u0645\u0628\u0631","\u062f\u064a\u0633\u0645\u0628\u0631"],B:["\u064a\u0646\u0627\u064a\u0631","\u0641\u0628\u0631\u0627\u064a\u0631","\u0645\u0627\u0631\u0633","\u0623\u0628\u0631\u064a\u0644","\u0645\u0627\u064a\u0648","\u064a\u0648\u0646\u064a\u0648","\u064a\u0648\u0644\u064a\u0648","\u0623\u063a\u0633\u0637\u0633","\u0633\u0628\u062a\u0645\u0628\u0631","\u0623\u0643\u062a\u0648\u0628\u0631","\u0646\u0648\u0641\u0645\u0628\u0631","\u062f\u064a\u0633\u0645\u0628\u0631"],c:"%a\u060c %d %B %Y %Z %l:%M:%S %p",p:["\u0635","\u0645"],P:["\u0635","\u0645"],x:"%d\u200f/%m\u200f/%Y",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_ca-ES.js b/js/yui3/datatype-date-format/lang/datatype-date-format_ca-ES.js
new file mode 100644
index 000000000..3e21f6d70
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_ca-ES.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_ca-ES",function(e){e.Intl.add("datatype-date-format","ca-ES",{a:["dg.","dl.","dt.","dc.","dj.","dv.","ds."],A:["diumenge","dilluns","dimarts","dimecres","dijous","divendres","dissabte"],b:["gen.","febr.","mar\u00e7","abr.","maig","juny","jul.","ag.","set.","oct.","nov.","des."],B:["gener","febrer","mar\u00e7","abril","maig","juny","juliol","agost","setembre","octubre","novembre","desembre"],c:"%a %d %b %Y %k:%M:%S %Z",p:["A.M.","P.M."],P:["a.m.","p.m."],x:"%d/%m/%y",X:"%k:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_ca.js b/js/yui3/datatype-date-format/lang/datatype-date-format_ca.js
new file mode 100644
index 000000000..e48c1d295
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_ca.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_ca",function(e){e.Intl.add("datatype-date-format","ca",{a:["dg.","dl.","dt.","dc.","dj.","dv.","ds."],A:["diumenge","dilluns","dimarts","dimecres","dijous","divendres","dissabte"],b:["gen.","febr.","mar\u00e7","abr.","maig","juny","jul.","ag.","set.","oct.","nov.","des."],B:["gener","febrer","mar\u00e7","abril","maig","juny","juliol","agost","setembre","octubre","novembre","desembre"],c:"%a %d %b %Y %k:%M:%S %Z",p:["A.M.","P.M."],P:["a.m.","p.m."],x:"%d/%m/%y",X:"%k:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_da-DK.js b/js/yui3/datatype-date-format/lang/datatype-date-format_da-DK.js
new file mode 100644
index 000000000..396ea7806
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_da-DK.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_da-DK",function(e){e.Intl.add("datatype-date-format","da-DK",{a:["s\u00f8n","man","tir","ons","tor","fre","l\u00f8r"],A:["s\u00f8ndag","mandag","tirsdag","onsdag","torsdag","fredag","l\u00f8rdag"],b:["jan.","feb.","mar.","apr.","maj","jun.","jul.","aug.","sep.","okt.","nov.","dec."],B:["januar","februar","marts","april","maj","juni","juli","august","september","oktober","november","december"],c:"%a. %d. %b %Y %H.%M.%S %Z",p:["F.M.","E.M."],P:["f.m.","e.m."],x:"%d/%m/%y",X:"%H.%M.%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_da.js b/js/yui3/datatype-date-format/lang/datatype-date-format_da.js
new file mode 100644
index 000000000..174847359
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_da.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_da",function(e){e.Intl.add("datatype-date-format","da",{a:["s\u00f8n","man","tir","ons","tor","fre","l\u00f8r"],A:["s\u00f8ndag","mandag","tirsdag","onsdag","torsdag","fredag","l\u00f8rdag"],b:["jan.","feb.","mar.","apr.","maj","jun.","jul.","aug.","sep.","okt.","nov.","dec."],B:["januar","februar","marts","april","maj","juni","juli","august","september","oktober","november","december"],c:"%a. %d. %b %Y %H.%M.%S %Z",p:["F.M.","E.M."],P:["f.m.","e.m."],x:"%d/%m/%y",X:"%H.%M.%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_de-AT.js b/js/yui3/datatype-date-format/lang/datatype-date-format_de-AT.js
new file mode 100644
index 000000000..1f32c6238
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_de-AT.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_de-AT",function(e){e.Intl.add("datatype-date-format","de-AT",{a:["So.","Mo.","Di.","Mi.","Do.","Fr.","Sa."],A:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],b:["J\u00e4n","Feb","M\u00e4r","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],B:["J\u00e4nner","Februar","M\u00e4rz","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],c:"%a, %d. %b %Y %H:%M:%S %Z",p:["VORM.","NACHM."],P:["vorm.","nachm."],x:"%d.%m.%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_de-DE.js b/js/yui3/datatype-date-format/lang/datatype-date-format_de-DE.js
new file mode 100644
index 000000000..7c8bd8c60
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_de-DE.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_de-DE",function(e){e.Intl.add("datatype-date-format","de-DE",{a:["So.","Mo.","Di.","Mi.","Do.","Fr.","Sa."],A:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],b:["Jan","Feb","M\u00e4r","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],B:["Januar","Februar","M\u00e4rz","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],c:"%a, %d. %b %Y %H:%M:%S %Z",p:["VORM.","NACHM."],P:["vorm.","nachm."],x:"%d.%m.%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_de.js b/js/yui3/datatype-date-format/lang/datatype-date-format_de.js
new file mode 100644
index 000000000..c0df98421
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_de.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_de",function(e){e.Intl.add("datatype-date-format","de",{a:["So.","Mo.","Di.","Mi.","Do.","Fr.","Sa."],A:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],b:["Jan","Feb","M\u00e4r","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],B:["Januar","Februar","M\u00e4rz","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],c:"%a, %d. %b %Y %H:%M:%S %Z",p:["VORM.","NACHM."],P:["vorm.","nachm."],x:"%d.%m.%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_el-GR.js b/js/yui3/datatype-date-format/lang/datatype-date-format_el-GR.js
new file mode 100644
index 000000000..0aa2579d0
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_el-GR.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_el-GR",function(e){e.Intl.add("datatype-date-format","el-GR",{a:["\u039a\u03c5\u03c1","\u0394\u03b5\u03c5","\u03a4\u03c1\u03b9","\u03a4\u03b5\u03c4","\u03a0\u03b5\u03bc","\u03a0\u03b1\u03c1","\u03a3\u03b1\u03b2"],A:["\u039a\u03c5\u03c1\u03b9\u03b1\u03ba\u03ae","\u0394\u03b5\u03c5\u03c4\u03ad\u03c1\u03b1","\u03a4\u03c1\u03af\u03c4\u03b7","\u03a4\u03b5\u03c4\u03ac\u03c1\u03c4\u03b7","\u03a0\u03ad\u03bc\u03c0\u03c4\u03b7","\u03a0\u03b1\u03c1\u03b1\u03c3\u03ba\u03b5\u03c5\u03ae","\u03a3\u03ac\u03b2\u03b2\u03b1\u03c4\u03bf"],b:["\u0399\u03b1\u03bd","\u03a6\u03b5\u03b2","\u039c\u03b1\u03c1","\u0391\u03c0\u03c1","\u039c\u03b1\u03ca","\u0399\u03bf\u03c5\u03bd","\u0399\u03bf\u03c5\u03bb","\u0391\u03c5\u03b3","\u03a3\u03b5\u03c0","\u039f\u03ba\u03c4","\u039d\u03bf\u03b5","\u0394\u03b5\u03ba"],B:["\u0399\u03b1\u03bd\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5","\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5","\u039c\u03b1\u03c1\u03c4\u03af\u03bf\u03c5","\u0391\u03c0\u03c1\u03b9\u03bb\u03af\u03bf\u03c5","\u039c\u03b1\u0390\u03bf\u03c5","\u0399\u03bf\u03c5\u03bd\u03af\u03bf\u03c5","\u0399\u03bf\u03c5\u03bb\u03af\u03bf\u03c5","\u0391\u03c5\u03b3\u03bf\u03cd\u03c3\u03c4\u03bf\u03c5","\u03a3\u03b5\u03c0\u03c4\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5","\u039f\u03ba\u03c4\u03c9\u03b2\u03c1\u03af\u03bf\u03c5","\u039d\u03bf\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5","\u0394\u03b5\u03ba\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5"],c:"%a, %d %b %Y %l:%M:%S %p %Z",p:["\u03a0.\u039c.","\u039c.\u039c."],P:["\u03c0.\u03bc.","\u03bc.\u03bc."],x:"%d/%m/%Y",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_el.js b/js/yui3/datatype-date-format/lang/datatype-date-format_el.js
new file mode 100644
index 000000000..c8a57b85a
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_el.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_el",function(e){e.Intl.add("datatype-date-format","el",{a:["\u039a\u03c5\u03c1","\u0394\u03b5\u03c5","\u03a4\u03c1\u03b9","\u03a4\u03b5\u03c4","\u03a0\u03b5\u03bc","\u03a0\u03b1\u03c1","\u03a3\u03b1\u03b2"],A:["\u039a\u03c5\u03c1\u03b9\u03b1\u03ba\u03ae","\u0394\u03b5\u03c5\u03c4\u03ad\u03c1\u03b1","\u03a4\u03c1\u03af\u03c4\u03b7","\u03a4\u03b5\u03c4\u03ac\u03c1\u03c4\u03b7","\u03a0\u03ad\u03bc\u03c0\u03c4\u03b7","\u03a0\u03b1\u03c1\u03b1\u03c3\u03ba\u03b5\u03c5\u03ae","\u03a3\u03ac\u03b2\u03b2\u03b1\u03c4\u03bf"],b:["\u0399\u03b1\u03bd","\u03a6\u03b5\u03b2","\u039c\u03b1\u03c1","\u0391\u03c0\u03c1","\u039c\u03b1\u03ca","\u0399\u03bf\u03c5\u03bd","\u0399\u03bf\u03c5\u03bb","\u0391\u03c5\u03b3","\u03a3\u03b5\u03c0","\u039f\u03ba\u03c4","\u039d\u03bf\u03b5","\u0394\u03b5\u03ba"],B:["\u0399\u03b1\u03bd\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5","\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5","\u039c\u03b1\u03c1\u03c4\u03af\u03bf\u03c5","\u0391\u03c0\u03c1\u03b9\u03bb\u03af\u03bf\u03c5","\u039c\u03b1\u0390\u03bf\u03c5","\u0399\u03bf\u03c5\u03bd\u03af\u03bf\u03c5","\u0399\u03bf\u03c5\u03bb\u03af\u03bf\u03c5","\u0391\u03c5\u03b3\u03bf\u03cd\u03c3\u03c4\u03bf\u03c5","\u03a3\u03b5\u03c0\u03c4\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5","\u039f\u03ba\u03c4\u03c9\u03b2\u03c1\u03af\u03bf\u03c5","\u039d\u03bf\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5","\u0394\u03b5\u03ba\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5"],c:"%a, %d %b %Y %l:%M:%S %p %Z",p:["\u03a0.\u039c.","\u039c.\u039c."],P:["\u03c0.\u03bc.","\u03bc.\u03bc."],x:"%d/%m/%Y",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_en-AU.js b/js/yui3/datatype-date-format/lang/datatype-date-format_en-AU.js
new file mode 100644
index 000000000..0737ec9c3
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_en-AU.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_en-AU",function(e){e.Intl.add("datatype-date-format","en-AU",{a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a, %b %d, %Y %l:%M:%S %p %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_en-CA.js b/js/yui3/datatype-date-format/lang/datatype-date-format_en-CA.js
new file mode 100644
index 000000000..e33fea2c8
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_en-CA.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_en-CA",function(e){e.Intl.add("datatype-date-format","en-CA",{a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a, %b %d, %Y %l:%M:%S %p %Z",p:["AM","PM"],P:["am","pm"],x:"%y-%m-%d",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_en-GB.js b/js/yui3/datatype-date-format/lang/datatype-date-format_en-GB.js
new file mode 100644
index 000000000..5bb92fe17
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_en-GB.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_en-GB",function(e){e.Intl.add("datatype-date-format","en-GB",{a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a, %b %d, %Y %H:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%Y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_en-IE.js b/js/yui3/datatype-date-format/lang/datatype-date-format_en-IE.js
new file mode 100644
index 000000000..4b1258b7d
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_en-IE.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_en-IE",function(e){e.Intl.add("datatype-date-format","en-IE",{a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a, %b %d, %Y %H:%M:%S %Z",p:["A.M.","P.M."],P:["a.m.","p.m."],x:"%d/%m/%Y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_en-IN.js b/js/yui3/datatype-date-format/lang/datatype-date-format_en-IN.js
new file mode 100644
index 000000000..367fc45fd
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_en-IN.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_en-IN",function(e){e.Intl.add("datatype-date-format","en-IN",{a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a, %b %d, %Y %l:%M:%S %p %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_en-JO.js b/js/yui3/datatype-date-format/lang/datatype-date-format_en-JO.js
new file mode 100644
index 000000000..32f62c26f
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_en-JO.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_en-JO",function(e){e.Intl.add("datatype-date-format","en-JO",{a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a, %b %d, %Y %l:%M:%S %p %Z",p:["AM","PM"],P:["am","pm"],x:"%m/%d/%y",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_en-MY.js b/js/yui3/datatype-date-format/lang/datatype-date-format_en-MY.js
new file mode 100644
index 000000000..a42d7f39a
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_en-MY.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_en-MY",function(e){e.Intl.add("datatype-date-format","en-MY",{a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a, %b %d, %Y %l:%M:%S %p %Z",p:["AM","PM"],P:["am","pm"],x:"%m/%d/%y",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_en-NZ.js b/js/yui3/datatype-date-format/lang/datatype-date-format_en-NZ.js
new file mode 100644
index 000000000..2ec7ac6a9
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_en-NZ.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_en-NZ",function(e){e.Intl.add("datatype-date-format","en-NZ",{a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a, %b %d, %Y %l:%M:%S %p %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_en-PH.js b/js/yui3/datatype-date-format/lang/datatype-date-format_en-PH.js
new file mode 100644
index 000000000..0afc6e0a4
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_en-PH.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_en-PH",function(e){e.Intl.add("datatype-date-format","en-PH",{a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a, %b %d, %Y %l:%M:%S %p %Z",p:["AM","PM"],P:["am","pm"],x:"%m/%d/%y",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_en-SG.js b/js/yui3/datatype-date-format/lang/datatype-date-format_en-SG.js
new file mode 100644
index 000000000..2197d4ce9
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_en-SG.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_en-SG",function(e){e.Intl.add("datatype-date-format","en-SG",{a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a, %b %d, %Y %l:%M:%S %p %Z",p:["AM","PM"],P:["am","pm"],x:"%m/%d/%y",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_en-US.js b/js/yui3/datatype-date-format/lang/datatype-date-format_en-US.js
new file mode 100644
index 000000000..c94a738b6
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_en-US.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_en-US",function(e){e.Intl.add("datatype-date-format","en-US",{a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a, %b %d, %Y %l:%M:%S %p %Z",p:["AM","PM"],P:["am","pm"],x:"%m/%d/%y",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_en.js b/js/yui3/datatype-date-format/lang/datatype-date-format_en.js
new file mode 100644
index 000000000..e954729a3
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_en.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_en",function(e){e.Intl.add("datatype-date-format","en",{a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a, %b %d, %Y %l:%M:%S %p %Z",p:["AM","PM"],P:["am","pm"],x:"%m/%d/%y",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_es-AR.js b/js/yui3/datatype-date-format/lang/datatype-date-format_es-AR.js
new file mode 100644
index 000000000..9f031fdb0
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_es-AR.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_es-AR",function(e){e.Intl.add("datatype-date-format","es-AR",{a:["dom","lun","mar","mi\u00e9","jue","vie","s\u00e1b"],A:["domingo","lunes","martes","mi\u00e9rcoles","jueves","viernes","s\u00e1bado"],b:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],B:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],c:"%a, %d %b %Y %Hh'%M:%S %Z",p:["A.M.","P.M."],P:["a.m.","p.m."],x:"%d/%m/%y",X:"%Hh'%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_es-BO.js b/js/yui3/datatype-date-format/lang/datatype-date-format_es-BO.js
new file mode 100644
index 000000000..c5dd8510c
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_es-BO.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_es-BO",function(e){e.Intl.add("datatype-date-format","es-BO",{a:["dom","lun","mar","mi\u00e9","jue","vie","s\u00e1b"],A:["domingo","lunes","martes","mi\u00e9rcoles","jueves","viernes","s\u00e1bado"],b:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],B:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],c:"%a, %d %b %Y %H:%M:%S %Z",p:["A.M.","P.M."],P:["a.m.","p.m."],x:"%d/%m/%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_es-CL.js b/js/yui3/datatype-date-format/lang/datatype-date-format_es-CL.js
new file mode 100644
index 000000000..85e732715
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_es-CL.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_es-CL",function(e){e.Intl.add("datatype-date-format","es-CL",{a:["dom","lun","mar","mi\u00e9","jue","vie","s\u00e1b"],A:["domingo","lunes","martes","mi\u00e9rcoles","jueves","viernes","s\u00e1bado"],b:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],B:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],c:"%a, %d %b %Y %H:%M:%S %Z",p:["A.M.","P.M."],P:["a.m.","p.m."],x:"%d-%m-%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_es-CO.js b/js/yui3/datatype-date-format/lang/datatype-date-format_es-CO.js
new file mode 100644
index 000000000..5700f0c3e
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_es-CO.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_es-CO",function(e){e.Intl.add("datatype-date-format","es-CO",{a:["dom","lun","mar","mi\u00e9","jue","vie","s\u00e1b"],A:["domingo","lunes","martes","mi\u00e9rcoles","jueves","viernes","s\u00e1bado"],b:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],B:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],c:"%a, %d %b %Y %H:%M:%S %Z",p:["A.M.","P.M."],P:["a.m.","p.m."],x:"%d/%m/%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_es-EC.js b/js/yui3/datatype-date-format/lang/datatype-date-format_es-EC.js
new file mode 100644
index 000000000..6ffeaf681
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_es-EC.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_es-EC",function(e){e.Intl.add("datatype-date-format","es-EC",{a:["dom","lun","mar","mi\u00e9","jue","vie","s\u00e1b"],A:["domingo","lunes","martes","mi\u00e9rcoles","jueves","viernes","s\u00e1bado"],b:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],B:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],c:"%a, %d %b %Y %H:%M:%S %Z",p:["A.M.","P.M."],P:["a.m.","p.m."],x:"%d/%m/%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_es-ES.js b/js/yui3/datatype-date-format/lang/datatype-date-format_es-ES.js
new file mode 100644
index 000000000..b244b574b
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_es-ES.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_es-ES",function(e){e.Intl.add("datatype-date-format","es-ES",{a:["dom","lun","mar","mi\u00e9","jue","vie","s\u00e1b"],A:["domingo","lunes","martes","mi\u00e9rcoles","jueves","viernes","s\u00e1bado"],b:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],B:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],c:"%a, %d %b %Y %H:%M:%S %Z",p:["A.M.","P.M."],P:["a.m.","p.m."],x:"%d/%m/%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_es-MX.js b/js/yui3/datatype-date-format/lang/datatype-date-format_es-MX.js
new file mode 100644
index 000000000..2bf016be3
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_es-MX.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_es-MX",function(e){e.Intl.add("datatype-date-format","es-MX",{a:["dom","lun","mar","mi\u00e9","jue","vie","s\u00e1b"],A:["domingo","lunes","martes","mi\u00e9rcoles","jueves","viernes","s\u00e1bado"],b:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],B:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],c:"%a, %d %b %Y %H:%M:%S %Z",p:["A.M.","P.M."],P:["a.m.","p.m."],x:"%d/%m/%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_es-PE.js b/js/yui3/datatype-date-format/lang/datatype-date-format_es-PE.js
new file mode 100644
index 000000000..7b3b612fe
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_es-PE.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_es-PE",function(e){e.Intl.add("datatype-date-format","es-PE",{a:["dom","lun","mar","mi\u00e9","jue","vie","s\u00e1b"],A:["domingo","lunes","martes","mi\u00e9rcoles","jueves","viernes","s\u00e1bado"],b:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],B:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],c:"%a, %d %b %Y %HH%M'%S\" %Z",p:["A.M.","P.M."],P:["a.m.","p.m."],x:"%d/%m/%y",X:"%HH%M'%S\""})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_es-PY.js b/js/yui3/datatype-date-format/lang/datatype-date-format_es-PY.js
new file mode 100644
index 000000000..e2cbcd70f
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_es-PY.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_es-PY",function(e){e.Intl.add("datatype-date-format","es-PY",{a:["dom","lun","mar","mi\u00e9","jue","vie","s\u00e1b"],A:["domingo","lunes","martes","mi\u00e9rcoles","jueves","viernes","s\u00e1bado"],b:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],B:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],c:"%a, %d %b %Y %H:%M:%S %Z",p:["A.M.","P.M."],P:["a.m.","p.m."],x:"%d/%m/%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_es-US.js b/js/yui3/datatype-date-format/lang/datatype-date-format_es-US.js
new file mode 100644
index 000000000..b98f1c5f0
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_es-US.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_es-US",function(e){e.Intl.add("datatype-date-format","es-US",{a:["dom","lun","mar","mi\u00e9","jue","vie","s\u00e1b"],A:["domingo","lunes","martes","mi\u00e9rcoles","jueves","viernes","s\u00e1bado"],b:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],B:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],c:"%a, %d %b %Y %l:%M:%S %p %Z",p:["A.M.","P.M."],P:["a.m.","p.m."],x:"%m/%d/%y",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_es-UY.js b/js/yui3/datatype-date-format/lang/datatype-date-format_es-UY.js
new file mode 100644
index 000000000..7ae36cf7e
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_es-UY.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_es-UY",function(e){e.Intl.add("datatype-date-format","es-UY",{a:["dom","lun","mar","mi\u00e9","jue","vie","s\u00e1b"],A:["domingo","lunes","martes","mi\u00e9rcoles","jueves","viernes","s\u00e1bado"],b:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],B:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],c:"%a, %d %b %Y %H:%M:%S %Z",p:["A.M.","P.M."],P:["a.m.","p.m."],x:"%d/%m/%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_es-VE.js b/js/yui3/datatype-date-format/lang/datatype-date-format_es-VE.js
new file mode 100644
index 000000000..e57bc49eb
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_es-VE.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_es-VE",function(e){e.Intl.add("datatype-date-format","es-VE",{a:["dom","lun","mar","mi\u00e9","jue","vie","s\u00e1b"],A:["domingo","lunes","martes","mi\u00e9rcoles","jueves","viernes","s\u00e1bado"],b:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],B:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],c:"%a, %d %b %Y %H:%M:%S %Z",p:["A.M.","P.M."],P:["a.m.","p.m."],x:"%d/%m/%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_es.js b/js/yui3/datatype-date-format/lang/datatype-date-format_es.js
new file mode 100644
index 000000000..971f4562b
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_es.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_es",function(e){e.Intl.add("datatype-date-format","es",{a:["dom","lun","mar","mi\u00e9","jue","vie","s\u00e1b"],A:["domingo","lunes","martes","mi\u00e9rcoles","jueves","viernes","s\u00e1bado"],b:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],B:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],c:"%a, %d %b %Y %H:%M:%S %Z",p:["A.M.","P.M."],P:["a.m.","p.m."],x:"%d/%m/%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_fi-FI.js b/js/yui3/datatype-date-format/lang/datatype-date-format_fi-FI.js
new file mode 100644
index 000000000..a23fc5fbe
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_fi-FI.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_fi-FI",function(e){e.Intl.add("datatype-date-format","fi-FI",{a:["su","ma","ti","ke","to","pe","la"],A:["sunnuntaina","maanantaina","tiistaina","keskiviikkona","torstaina","perjantaina","lauantaina"],b:["tammikuuta","helmikuuta","maaliskuuta","huhtikuuta","toukokuuta","kes\u00e4kuuta","hein\u00e4kuuta","elokuuta","syyskuuta","lokakuuta","marraskuuta","joulukuuta"],B:["tammikuuta","helmikuuta","maaliskuuta","huhtikuuta","toukokuuta","kes\u00e4kuuta","hein\u00e4kuuta","elokuuta","syyskuuta","lokakuuta","marraskuuta","joulukuuta"],c:"%a %d. %b %Y %k.%M.%S %Z",p:["AP.","IP."],P:["ap.","ip."],x:"%d.%m.%Y",X:"%k.%M.%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_fi.js b/js/yui3/datatype-date-format/lang/datatype-date-format_fi.js
new file mode 100644
index 000000000..0b5dc29a6
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_fi.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_fi",function(e){e.Intl.add("datatype-date-format","fi",{a:["su","ma","ti","ke","to","pe","la"],A:["sunnuntaina","maanantaina","tiistaina","keskiviikkona","torstaina","perjantaina","lauantaina"],b:["tammikuuta","helmikuuta","maaliskuuta","huhtikuuta","toukokuuta","kes\u00e4kuuta","hein\u00e4kuuta","elokuuta","syyskuuta","lokakuuta","marraskuuta","joulukuuta"],B:["tammikuuta","helmikuuta","maaliskuuta","huhtikuuta","toukokuuta","kes\u00e4kuuta","hein\u00e4kuuta","elokuuta","syyskuuta","lokakuuta","marraskuuta","joulukuuta"],c:"%a %d. %b %Y %k.%M.%S %Z",p:["AP.","IP."],P:["ap.","ip."],x:"%d.%m.%Y",X:"%k.%M.%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_fr-BE.js b/js/yui3/datatype-date-format/lang/datatype-date-format_fr-BE.js
new file mode 100644
index 000000000..cefd319d8
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_fr-BE.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_fr-BE",function(e){e.Intl.add("datatype-date-format","fr-BE",{a:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],A:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],b:["janv.","f\u00e9vr.","mars","avr.","mai","juin","juil.","ao\u00fbt","sept.","oct.","nov.","d\u00e9c."],B:["janvier","f\u00e9vrier","mars","avril","mai","juin","juillet","ao\u00fbt","septembre","octobre","novembre","d\u00e9cembre"],c:"%a %d %b %Y %k h %M min %S s %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%k h %M min %S s"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_fr-CA.js b/js/yui3/datatype-date-format/lang/datatype-date-format_fr-CA.js
new file mode 100644
index 000000000..f17fe81d6
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_fr-CA.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_fr-CA",function(e){e.Intl.add("datatype-date-format","fr-CA",{a:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],A:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],b:["janv.","f\u00e9vr.","mars","avr.","mai","juin","juil.","ao\u00fbt","sept.","oct.","nov.","d\u00e9c."],B:["janvier","f\u00e9vrier","mars","avril","mai","juin","juillet","ao\u00fbt","septembre","octobre","novembre","d\u00e9cembre"],c:"%a %d %b %Y %H h %M min %S s %Z",p:["AM","PM"],P:["am","pm"],x:"%y-%m-%d",X:"%H h %M min %S s"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_fr-FR.js b/js/yui3/datatype-date-format/lang/datatype-date-format_fr-FR.js
new file mode 100644
index 000000000..a838c373c
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_fr-FR.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_fr-FR",function(e){e.Intl.add("datatype-date-format","fr-FR",{a:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],A:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],b:["janv.","f\u00e9vr.","mars","avr.","mai","juin","juil.","ao\u00fbt","sept.","oct.","nov.","d\u00e9c."],B:["janvier","f\u00e9vrier","mars","avril","mai","juin","juillet","ao\u00fbt","septembre","octobre","novembre","d\u00e9cembre"],c:"%a %d %b %Y %H:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_fr.js b/js/yui3/datatype-date-format/lang/datatype-date-format_fr.js
new file mode 100644
index 000000000..79a8cdc91
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_fr.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_fr",function(e){e.Intl.add("datatype-date-format","fr",{a:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],A:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],b:["janv.","f\u00e9vr.","mars","avr.","mai","juin","juil.","ao\u00fbt","sept.","oct.","nov.","d\u00e9c."],B:["janvier","f\u00e9vrier","mars","avril","mai","juin","juillet","ao\u00fbt","septembre","octobre","novembre","d\u00e9cembre"],c:"%a %d %b %Y %H:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_hi-IN.js b/js/yui3/datatype-date-format/lang/datatype-date-format_hi-IN.js
new file mode 100644
index 000000000..086bb426e
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_hi-IN.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_hi-IN",function(e){e.Intl.add("datatype-date-format","hi-IN",{a:["\u0930\u0935\u093f","\u0938\u094b\u092e","\u092e\u0902\u0917\u0932","\u092c\u0941\u0927","\u0917\u0941\u0930\u0941","\u0936\u0941\u0915\u094d\u0930","\u0936\u0928\u093f"],A:["\u0930\u0935\u093f\u0935\u093e\u0930","\u0938\u094b\u092e\u0935\u093e\u0930","\u092e\u0902\u0917\u0932\u0935\u093e\u0930","\u092c\u0941\u0927\u0935\u093e\u0930","\u0917\u0941\u0930\u0941\u0935\u093e\u0930","\u0936\u0941\u0915\u094d\u0930\u0935\u093e\u0930","\u0936\u0928\u093f\u0935\u093e\u0930"],b:["\u091c\u0928\u0935\u0930\u0940","\u092b\u0930\u0935\u0930\u0940","\u092e\u093e\u0930\u094d\u091a","\u0905\u092a\u094d\u0930\u0948\u0932","\u092e\u0908","\u091c\u0942\u0928","\u091c\u0941\u0932\u093e\u0908","\u0905\u0917\u0938\u094d\u0924","\u0938\u093f\u0924\u092e\u094d\u092c\u0930","\u0905\u0915\u094d\u0924\u0942\u092c\u0930","\u0928\u0935\u092e\u094d\u092c\u0930","\u0926\u093f\u0938\u092e\u094d\u092c\u0930"],B:["\u091c\u0928\u0935\u0930\u0940","\u092b\u0930\u0935\u0930\u0940","\u092e\u093e\u0930\u094d\u091a","\u0905\u092a\u094d\u0930\u0948\u0932","\u092e\u0908","\u091c\u0942\u0928","\u091c\u0941\u0932\u093e\u0908","\u0905\u0917\u0938\u094d\u0924","\u0938\u093f\u0924\u092e\u094d\u092c\u0930","\u0905\u0915\u094d\u0924\u0942\u092c\u0930","\u0928\u0935\u092e\u094d\u092c\u0930","\u0926\u093f\u0938\u092e\u094d\u092c\u0930"],c:"%a, %d %b %Y %l:%M:%S %p %Z",p:["AM","PM"],P:["am","pm"],x:"%d-%m-%y",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_hi.js b/js/yui3/datatype-date-format/lang/datatype-date-format_hi.js
new file mode 100644
index 000000000..cac92cf2c
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_hi.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_hi",function(e){e.Intl.add("datatype-date-format","hi",{a:["\u0930\u0935\u093f","\u0938\u094b\u092e","\u092e\u0902\u0917\u0932","\u092c\u0941\u0927","\u0917\u0941\u0930\u0941","\u0936\u0941\u0915\u094d\u0930","\u0936\u0928\u093f"],A:["\u0930\u0935\u093f\u0935\u093e\u0930","\u0938\u094b\u092e\u0935\u093e\u0930","\u092e\u0902\u0917\u0932\u0935\u093e\u0930","\u092c\u0941\u0927\u0935\u093e\u0930","\u0917\u0941\u0930\u0941\u0935\u093e\u0930","\u0936\u0941\u0915\u094d\u0930\u0935\u093e\u0930","\u0936\u0928\u093f\u0935\u093e\u0930"],b:["\u091c\u0928\u0935\u0930\u0940","\u092b\u0930\u0935\u0930\u0940","\u092e\u093e\u0930\u094d\u091a","\u0905\u092a\u094d\u0930\u0948\u0932","\u092e\u0908","\u091c\u0942\u0928","\u091c\u0941\u0932\u093e\u0908","\u0905\u0917\u0938\u094d\u0924","\u0938\u093f\u0924\u092e\u094d\u092c\u0930","\u0905\u0915\u094d\u0924\u0942\u092c\u0930","\u0928\u0935\u092e\u094d\u092c\u0930","\u0926\u093f\u0938\u092e\u094d\u092c\u0930"],B:["\u091c\u0928\u0935\u0930\u0940","\u092b\u0930\u0935\u0930\u0940","\u092e\u093e\u0930\u094d\u091a","\u0905\u092a\u094d\u0930\u0948\u0932","\u092e\u0908","\u091c\u0942\u0928","\u091c\u0941\u0932\u093e\u0908","\u0905\u0917\u0938\u094d\u0924","\u0938\u093f\u0924\u092e\u094d\u092c\u0930","\u0905\u0915\u094d\u0924\u0942\u092c\u0930","\u0928\u0935\u092e\u094d\u092c\u0930","\u0926\u093f\u0938\u092e\u094d\u092c\u0930"],c:"%a, %d %b %Y %l:%M:%S %p %Z",p:["AM","PM"],P:["am","pm"],x:"%d-%m-%y",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_hu.js b/js/yui3/datatype-date-format/lang/datatype-date-format_hu.js
new file mode 100644
index 000000000..7d418d65d
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_hu.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_hu",function(e){e.Intl.add("datatype-date-format","hu",{a:["Vas","H\u00e9","Ke","Sze","Cs\u00fc","P\u00e9","Szo"],A:["Vas\u00e1rnap","H\u00e9tf\u0151","Kedd","Szerda","Cs\u00fct\u00f6rt\u00f6k","P\u00e9ntek","Szombat"],b:["Jan","Feb","M\u00e1rc","\u00c1pr","M\u00e1j","J\u00fan","J\u00fal","Aug","Szep","Okt","Nov","Dec"],B:["Janu\u00e1r","Febru\u00e1r","M\u00e1rcius","\u00e1prilis","M\u00e1jus","J\u00fanius","J\u00falius","Augusztus","Szeptember","Okt\u00f3ber","November","December"],c:"%a, %b %d, %Y %l:%M:%S %p %Z",p:["DE","DU"],P:["de.","du."],x:"%m/%d/%y",X:"%l:%M:%S %p"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_id-ID.js b/js/yui3/datatype-date-format/lang/datatype-date-format_id-ID.js
new file mode 100644
index 000000000..ee0d39fba
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_id-ID.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_id-ID",function(e){e.Intl.add("datatype-date-format","id-ID",{a:["Min","Sen","Sel","Rab","Kam","Jum","Sab"],A:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],b:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Agu","Sep","Okt","Nov","Des"],B:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","November","Desember"],c:"%a, %Y %b %d %H:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_id.js b/js/yui3/datatype-date-format/lang/datatype-date-format_id.js
new file mode 100644
index 000000000..1abb4b632
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_id.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_id",function(e){e.Intl.add("datatype-date-format","id",{a:["Min","Sen","Sel","Rab","Kam","Jum","Sab"],A:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],b:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Agu","Sep","Okt","Nov","Des"],B:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","November","Desember"],c:"%a, %Y %b %d %H:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_it-IT.js b/js/yui3/datatype-date-format/lang/datatype-date-format_it-IT.js
new file mode 100644
index 000000000..dc8f226ce
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_it-IT.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_it-IT",function(e){e.Intl.add("datatype-date-format","it-IT",{a:["dom","lun","mar","mer","gio","ven","sab"],A:["domenica","luned\u00ec","marted\u00ec","mercoled\u00ec","gioved\u00ec","venerd\u00ec","sabato"],b:["gen","feb","mar","apr","mag","giu","lug","ago","set","ott","nov","dic"],B:["gennaio","febbraio","marzo","aprile","maggio","giugno","luglio","agosto","settembre","ottobre","novembre","dicembre"],c:"%a %d %b %Y %H.%M.%S %Z",p:["M.","P."],P:["m.","p."],x:"%d/%m/%y",X:"%H.%M.%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_it.js b/js/yui3/datatype-date-format/lang/datatype-date-format_it.js
new file mode 100644
index 000000000..3a62b6a20
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_it.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_it",function(e){e.Intl.add("datatype-date-format","it",{a:["dom","lun","mar","mer","gio","ven","sab"],A:["domenica","luned\u00ec","marted\u00ec","mercoled\u00ec","gioved\u00ec","venerd\u00ec","sabato"],b:["gen","feb","mar","apr","mag","giu","lug","ago","set","ott","nov","dic"],B:["gennaio","febbraio","marzo","aprile","maggio","giugno","luglio","agosto","settembre","ottobre","novembre","dicembre"],c:"%a %d %b %Y %H.%M.%S %Z",p:["M.","P."],P:["m.","p."],x:"%d/%m/%y",X:"%H.%M.%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_ja-JP.js b/js/yui3/datatype-date-format/lang/datatype-date-format_ja-JP.js
new file mode 100644
index 000000000..ea40500f3
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_ja-JP.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_ja-JP",function(e){e.Intl.add("datatype-date-format","ja-JP",{a:["\u65e5","\u6708","\u706b","\u6c34","\u6728","\u91d1","\u571f"],A:["\u65e5\u66dc\u65e5","\u6708\u66dc\u65e5","\u706b\u66dc\u65e5","\u6c34\u66dc\u65e5","\u6728\u66dc\u65e5","\u91d1\u66dc\u65e5","\u571f\u66dc\u65e5"],b:["1\u6708","2\u6708","3\u6708","4\u6708","5\u6708","6\u6708","7\u6708","8\u6708","9\u6708","10\u6708","11\u6708","12\u6708"],B:["1\u6708","2\u6708","3\u6708","4\u6708","5\u6708","6\u6708","7\u6708","8\u6708","9\u6708","10\u6708","11\u6708","12\u6708"],c:"%Y\u5e74%m\u6708%d\u65e5(%a)%k\u6642%M\u5206%S\u79d2 %Z",p:["\u5348\u524d","\u5348\u5f8c"],P:["\u5348\u524d","\u5348\u5f8c"],x:"%y/%m/%d",X:"%k\u6642%M\u5206%S\u79d2"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_ja.js b/js/yui3/datatype-date-format/lang/datatype-date-format_ja.js
new file mode 100644
index 000000000..754763932
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_ja.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_ja",function(e){e.Intl.add("datatype-date-format","ja",{a:["\u65e5","\u6708","\u706b","\u6c34","\u6728","\u91d1","\u571f"],A:["\u65e5\u66dc\u65e5","\u6708\u66dc\u65e5","\u706b\u66dc\u65e5","\u6c34\u66dc\u65e5","\u6728\u66dc\u65e5","\u91d1\u66dc\u65e5","\u571f\u66dc\u65e5"],b:["1\u6708","2\u6708","3\u6708","4\u6708","5\u6708","6\u6708","7\u6708","8\u6708","9\u6708","10\u6708","11\u6708","12\u6708"],B:["1\u6708","2\u6708","3\u6708","4\u6708","5\u6708","6\u6708","7\u6708","8\u6708","9\u6708","10\u6708","11\u6708","12\u6708"],c:"%Y\u5e74%m\u6708%d\u65e5(%a)%k\u6642%M\u5206%S\u79d2 %Z",p:["\u5348\u524d","\u5348\u5f8c"],P:["\u5348\u524d","\u5348\u5f8c"],x:"%y/%m/%d",X:"%k\u6642%M\u5206%S\u79d2"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_ko-KR.js b/js/yui3/datatype-date-format/lang/datatype-date-format_ko-KR.js
new file mode 100644
index 000000000..795d374f3
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_ko-KR.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_ko-KR",function(e){e.Intl.add("datatype-date-format","ko-KR",{a:["\uc77c","\uc6d4","\ud654","\uc218","\ubaa9","\uae08","\ud1a0"],A:["\uc77c\uc694\uc77c","\uc6d4\uc694\uc77c","\ud654\uc694\uc77c","\uc218\uc694\uc77c","\ubaa9\uc694\uc77c","\uae08\uc694\uc77c","\ud1a0\uc694\uc77c"],b:["1\uc6d4","2\uc6d4","3\uc6d4","4\uc6d4","5\uc6d4","6\uc6d4","7\uc6d4","8\uc6d4","9\uc6d4","10\uc6d4","11\uc6d4","12\uc6d4"],B:["1\uc6d4","2\uc6d4","3\uc6d4","4\uc6d4","5\uc6d4","6\uc6d4","7\uc6d4","8\uc6d4","9\uc6d4","10\uc6d4","11\uc6d4","12\uc6d4"],c:"%Y\ub144 %b %d\uc77c %a%p %I\uc2dc %M\ubd84 %S\ucd08 %Z",p:["\uc624\uc804","\uc624\ud6c4"],P:["\uc624\uc804","\uc624\ud6c4"],x:"%y. %m. %d.",X:"%p %I\uc2dc %M\ubd84 %S\ucd08"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_ko.js b/js/yui3/datatype-date-format/lang/datatype-date-format_ko.js
new file mode 100644
index 000000000..11fa8c090
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_ko.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_ko",function(e){e.Intl.add("datatype-date-format","ko",{a:["\uc77c","\uc6d4","\ud654","\uc218","\ubaa9","\uae08","\ud1a0"],A:["\uc77c\uc694\uc77c","\uc6d4\uc694\uc77c","\ud654\uc694\uc77c","\uc218\uc694\uc77c","\ubaa9\uc694\uc77c","\uae08\uc694\uc77c","\ud1a0\uc694\uc77c"],b:["1\uc6d4","2\uc6d4","3\uc6d4","4\uc6d4","5\uc6d4","6\uc6d4","7\uc6d4","8\uc6d4","9\uc6d4","10\uc6d4","11\uc6d4","12\uc6d4"],B:["1\uc6d4","2\uc6d4","3\uc6d4","4\uc6d4","5\uc6d4","6\uc6d4","7\uc6d4","8\uc6d4","9\uc6d4","10\uc6d4","11\uc6d4","12\uc6d4"],c:"%Y\ub144 %b %d\uc77c %a%p %I\uc2dc %M\ubd84 %S\ucd08 %Z",p:["\uc624\uc804","\uc624\ud6c4"],P:["\uc624\uc804","\uc624\ud6c4"],x:"%y. %m. %d.",X:"%p %I\uc2dc %M\ubd84 %S\ucd08"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_ms-MY.js b/js/yui3/datatype-date-format/lang/datatype-date-format_ms-MY.js
new file mode 100644
index 000000000..21e7a243f
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_ms-MY.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_ms-MY",function(e){e.Intl.add("datatype-date-format","ms-MY",{a:["Ahd","Isn","Sel","Rab","Kha","Jum","Sab"],A:["Ahad","Isnin","Selasa","Rabu","Khamis","Jumaat","Sabtu"],b:["Jan","Feb","Mac","Apr","Mei","Jun","Jul","Ogos","Sep","Okt","Nov","Dis"],B:["Januari","Februari","Mac","April","Mei","Jun","Julai","Ogos","September","Oktober","November","Disember"],c:"%a, %Y %b %d %H:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%Y-%m-%d",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_ms.js b/js/yui3/datatype-date-format/lang/datatype-date-format_ms.js
new file mode 100644
index 000000000..2e572483e
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_ms.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_ms",function(e){e.Intl.add("datatype-date-format","ms",{a:["Ahd","Isn","Sel","Rab","Kha","Jum","Sab"],A:["Ahad","Isnin","Selasa","Rabu","Khamis","Jumaat","Sabtu"],b:["Jan","Feb","Mac","Apr","Mei","Jun","Jul","Ogos","Sep","Okt","Nov","Dis"],B:["Januari","Februari","Mac","April","Mei","Jun","Julai","Ogos","September","Oktober","November","Disember"],c:"%a, %Y %b %d %H:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%Y-%m-%d",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_nb-NO.js b/js/yui3/datatype-date-format/lang/datatype-date-format_nb-NO.js
new file mode 100644
index 000000000..2bd2005a7
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_nb-NO.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_nb-NO",function(e){e.Intl.add("datatype-date-format","nb-NO",{a:["s\u00f8n.","man.","tir.","ons.","tor.","fre.","l\u00f8r."],A:["s\u00f8ndag","mandag","tirsdag","onsdag","torsdag","fredag","l\u00f8rdag"],b:["jan.","feb.","mars","apr.","mai","juni","juli","aug.","sep.","okt.","nov.","des."],B:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],c:"%a %d. %b %Y kl. %H.%M.%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d.%m.%y",X:"kl. %H.%M.%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_nb.js b/js/yui3/datatype-date-format/lang/datatype-date-format_nb.js
new file mode 100644
index 000000000..781623aa4
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_nb.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_nb",function(e){e.Intl.add("datatype-date-format","nb",{a:["s\u00f8n.","man.","tir.","ons.","tor.","fre.","l\u00f8r."],A:["s\u00f8ndag","mandag","tirsdag","onsdag","torsdag","fredag","l\u00f8rdag"],b:["jan.","feb.","mars","apr.","mai","juni","juli","aug.","sep.","okt.","nov.","des."],B:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],c:"%a %d. %b %Y kl. %H.%M.%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d.%m.%y",X:"kl. %H.%M.%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_nl-BE.js b/js/yui3/datatype-date-format/lang/datatype-date-format_nl-BE.js
new file mode 100644
index 000000000..27af18669
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_nl-BE.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_nl-BE",function(e){e.Intl.add("datatype-date-format","nl-BE",{a:["zo","ma","di","wo","do","vr","za"],A:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],b:["jan.","feb.","mrt.","apr.","mei","jun.","jul.","aug.","sep.","okt.","nov.","dec."],B:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],c:"%a %d %b %Y %H:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_nl-NL.js b/js/yui3/datatype-date-format/lang/datatype-date-format_nl-NL.js
new file mode 100644
index 000000000..d70467276
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_nl-NL.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_nl-NL",function(e){e.Intl.add("datatype-date-format","nl-NL",{a:["zo","ma","di","wo","do","vr","za"],A:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],b:["jan.","feb.","mrt.","apr.","mei","jun.","jul.","aug.","sep.","okt.","nov.","dec."],B:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],c:"%a %d %b %Y %H:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d-%m-%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_nl.js b/js/yui3/datatype-date-format/lang/datatype-date-format_nl.js
new file mode 100644
index 000000000..dce409367
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_nl.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_nl",function(e){e.Intl.add("datatype-date-format","nl",{a:["zo","ma","di","wo","do","vr","za"],A:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],b:["jan.","feb.","mrt.","apr.","mei","jun.","jul.","aug.","sep.","okt.","nov.","dec."],B:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],c:"%a %d %b %Y %H:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d-%m-%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_pl-PL.js b/js/yui3/datatype-date-format/lang/datatype-date-format_pl-PL.js
new file mode 100644
index 000000000..8a591a922
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_pl-PL.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_pl-PL",function(e){e.Intl.add("datatype-date-format","pl-PL",{a:["niedz.","pon.","wt.","\u015br.","czw.","pt.","sob."],A:["niedziela","poniedzia\u0142ek","wtorek","\u015broda","czwartek","pi\u0105tek","sobota"],b:["sty","lut","mar","kwi","maj","cze","lip","sie","wrz","pa\u017a","lis","gru"],B:["stycznia","lutego","marca","kwietnia","maja","czerwca","lipca","sierpnia","wrze\u015bnia","pa\u017adziernika","listopada","grudnia"],c:"%a, %d %b %Y %H:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d-%m-%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_pl.js b/js/yui3/datatype-date-format/lang/datatype-date-format_pl.js
new file mode 100644
index 000000000..dd005b2dc
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_pl.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_pl",function(e){e.Intl.add("datatype-date-format","pl",{a:["niedz.","pon.","wt.","\u015br.","czw.","pt.","sob."],A:["niedziela","poniedzia\u0142ek","wtorek","\u015broda","czwartek","pi\u0105tek","sobota"],b:["sty","lut","mar","kwi","maj","cze","lip","sie","wrz","pa\u017a","lis","gru"],B:["stycznia","lutego","marca","kwietnia","maja","czerwca","lipca","sierpnia","wrze\u015bnia","pa\u017adziernika","listopada","grudnia"],c:"%a, %d %b %Y %H:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d-%m-%y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_pt-BR.js b/js/yui3/datatype-date-format/lang/datatype-date-format_pt-BR.js
new file mode 100644
index 000000000..936ff181a
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_pt-BR.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_pt-BR",function(e){e.Intl.add("datatype-date-format","pt-BR",{a:["dom","seg","ter","qua","qui","sex","s\u00e1b"],A:["domingo","segunda-feira","ter\u00e7a-feira","quarta-feira","quinta-feira","sexta-feira","s\u00e1bado"],b:["jan","fev","mar","abr","mai","jun","jul","ago","set","out","nov","dez"],B:["janeiro","fevereiro","mar\u00e7o","abril","maio","junho","julho","agosto","setembro","outubro","novembro","dezembro"],c:"%a, %d de %b de %Y %Hh%Mmin%Ss %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%Hh%Mmin%Ss"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_pt.js b/js/yui3/datatype-date-format/lang/datatype-date-format_pt.js
new file mode 100644
index 000000000..df11692aa
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_pt.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_pt",function(e){e.Intl.add("datatype-date-format","pt",{a:["dom","seg","ter","qua","qui","sex","s\u00e1b"],A:["domingo","segunda-feira","ter\u00e7a-feira","quarta-feira","quinta-feira","sexta-feira","s\u00e1bado"],b:["jan","fev","mar","abr","mai","jun","jul","ago","set","out","nov","dez"],B:["janeiro","fevereiro","mar\u00e7o","abril","maio","junho","julho","agosto","setembro","outubro","novembro","dezembro"],c:"%a, %d de %b de %Y %Hh%Mmin%Ss %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%Hh%Mmin%Ss"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_ro-RO.js b/js/yui3/datatype-date-format/lang/datatype-date-format_ro-RO.js
new file mode 100644
index 000000000..e79917ce8
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_ro-RO.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_ro-RO",function(e){e.Intl.add("datatype-date-format","ro-RO",{a:["Du","Lu","Ma","Mi","Jo","Vi","S\u00e2"],A:["duminic\u0103","luni","mar\u021bi","miercuri","joi","vineri","s\u00e2mb\u0103t\u0103"],b:["ian.","feb.","mar.","apr.","mai","iun.","iul.","aug.","sept.","oct.","nov.","dec."],B:["ianuarie","februarie","martie","aprilie","mai","iunie","iulie","august","septembrie","octombrie","noiembrie","decembrie"],c:"%a, %d %b %Y, %H:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d.%m.%Y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_ro.js b/js/yui3/datatype-date-format/lang/datatype-date-format_ro.js
new file mode 100644
index 000000000..fc31c7d11
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_ro.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_ro",function(e){e.Intl.add("datatype-date-format","ro",{a:["Du","Lu","Ma","Mi","Jo","Vi","S\u00e2"],A:["duminic\u0103","luni","mar\u021bi","miercuri","joi","vineri","s\u00e2mb\u0103t\u0103"],b:["ian.","feb.","mar.","apr.","mai","iun.","iul.","aug.","sept.","oct.","nov.","dec."],B:["ianuarie","februarie","martie","aprilie","mai","iunie","iulie","august","septembrie","octombrie","noiembrie","decembrie"],c:"%a, %d %b %Y, %H:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d.%m.%Y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_ru-RU.js b/js/yui3/datatype-date-format/lang/datatype-date-format_ru-RU.js
new file mode 100644
index 000000000..f641510cd
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_ru-RU.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_ru-RU",function(e){e.Intl.add("datatype-date-format","ru-RU",{a:["\u0412\u0441","\u041f\u043d","\u0412\u0442","\u0421\u0440","\u0427\u0442","\u041f\u0442","\u0421\u0431"],A:["\u0432\u043e\u0441\u043a\u0440\u0435\u0441\u0435\u043d\u044c\u0435","\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a","\u0432\u0442\u043e\u0440\u043d\u0438\u043a","\u0441\u0440\u0435\u0434\u0430","\u0447\u0435\u0442\u0432\u0435\u0440\u0433","\u043f\u044f\u0442\u043d\u0438\u0446\u0430","\u0441\u0443\u0431\u0431\u043e\u0442\u0430"],b:["\u044f\u043d\u0432.","\u0444\u0435\u0432\u0440.","\u043c\u0430\u0440\u0442\u0430","\u0430\u043f\u0440.","\u043c\u0430\u044f","\u0438\u044e\u043d\u044f","\u0438\u044e\u043b\u044f","\u0430\u0432\u0433.","\u0441\u0435\u043d\u0442.","\u043e\u043a\u0442.","\u043d\u043e\u044f\u0431.","\u0434\u0435\u043a."],B:["\u044f\u043d\u0432\u0430\u0440\u044f","\u0444\u0435\u0432\u0440\u0430\u043b\u044f","\u043c\u0430\u0440\u0442\u0430","\u0430\u043f\u0440\u0435\u043b\u044f","\u043c\u0430\u044f","\u0438\u044e\u043d\u044f","\u0438\u044e\u043b\u044f","\u0430\u0432\u0433\u0443\u0441\u0442\u0430","\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044f","\u043e\u043a\u0442\u044f\u0431\u0440\u044f","\u043d\u043e\u044f\u0431\u0440\u044f","\u0434\u0435\u043a\u0430\u0431\u0440\u044f"],c:"%a, %d %b %Y %k:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d.%m.%y",X:"%k:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_ru.js b/js/yui3/datatype-date-format/lang/datatype-date-format_ru.js
new file mode 100644
index 000000000..c6a0cee17
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_ru.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_ru",function(e){e.Intl.add("datatype-date-format","ru",{a:["\u0412\u0441","\u041f\u043d","\u0412\u0442","\u0421\u0440","\u0427\u0442","\u041f\u0442","\u0421\u0431"],A:["\u0432\u043e\u0441\u043a\u0440\u0435\u0441\u0435\u043d\u044c\u0435","\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a","\u0432\u0442\u043e\u0440\u043d\u0438\u043a","\u0441\u0440\u0435\u0434\u0430","\u0447\u0435\u0442\u0432\u0435\u0440\u0433","\u043f\u044f\u0442\u043d\u0438\u0446\u0430","\u0441\u0443\u0431\u0431\u043e\u0442\u0430"],b:["\u044f\u043d\u0432.","\u0444\u0435\u0432\u0440.","\u043c\u0430\u0440\u0442\u0430","\u0430\u043f\u0440.","\u043c\u0430\u044f","\u0438\u044e\u043d\u044f","\u0438\u044e\u043b\u044f","\u0430\u0432\u0433.","\u0441\u0435\u043d\u0442.","\u043e\u043a\u0442.","\u043d\u043e\u044f\u0431.","\u0434\u0435\u043a."],B:["\u044f\u043d\u0432\u0430\u0440\u044f","\u0444\u0435\u0432\u0440\u0430\u043b\u044f","\u043c\u0430\u0440\u0442\u0430","\u0430\u043f\u0440\u0435\u043b\u044f","\u043c\u0430\u044f","\u0438\u044e\u043d\u044f","\u0438\u044e\u043b\u044f","\u0430\u0432\u0433\u0443\u0441\u0442\u0430","\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044f","\u043e\u043a\u0442\u044f\u0431\u0440\u044f","\u043d\u043e\u044f\u0431\u0440\u044f","\u0434\u0435\u043a\u0430\u0431\u0440\u044f"],c:"%a, %d %b %Y %k:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d.%m.%y",X:"%k:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_sv-SE.js b/js/yui3/datatype-date-format/lang/datatype-date-format_sv-SE.js
new file mode 100644
index 000000000..d9a4d7fc3
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_sv-SE.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_sv-SE",function(e){e.Intl.add("datatype-date-format","sv-SE",{a:["s\u00f6n","m\u00e5n","tis","ons","tors","fre","l\u00f6r"],A:["s\u00f6ndag","m\u00e5ndag","tisdag","onsdag","torsdag","fredag","l\u00f6rdag"],b:["jan","feb","mar","apr","maj","jun","jul","aug","sep","okt","nov","dec"],B:["januari","februari","mars","april","maj","juni","juli","augusti","september","oktober","november","december"],c:"%a %d %b %Y kl. %H.%M.%S %Z",p:["FM","EM"],P:["fm","em"],x:"%Y-%m-%d",X:"kl. %H.%M.%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_sv.js b/js/yui3/datatype-date-format/lang/datatype-date-format_sv.js
new file mode 100644
index 000000000..3fbb4c375
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_sv.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_sv",function(e){e.Intl.add("datatype-date-format","sv",{a:["s\u00f6n","m\u00e5n","tis","ons","tors","fre","l\u00f6r"],A:["s\u00f6ndag","m\u00e5ndag","tisdag","onsdag","torsdag","fredag","l\u00f6rdag"],b:["jan","feb","mar","apr","maj","jun","jul","aug","sep","okt","nov","dec"],B:["januari","februari","mars","april","maj","juni","juli","augusti","september","oktober","november","december"],c:"%a %d %b %Y kl. %H.%M.%S %Z",p:["FM","EM"],P:["fm","em"],x:"%Y-%m-%d",X:"kl. %H.%M.%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_th-TH.js b/js/yui3/datatype-date-format/lang/datatype-date-format_th-TH.js
new file mode 100644
index 000000000..a677ad99b
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_th-TH.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_th-TH",function(e){e.Intl.add("datatype-date-format","th-TH",{a:["\u0e2d\u0e32.","\u0e08.","\u0e2d.","\u0e1e.","\u0e1e\u0e24.","\u0e28.","\u0e2a."],A:["\u0e27\u0e31\u0e19\u0e2d\u0e32\u0e17\u0e34\u0e15\u0e22\u0e4c","\u0e27\u0e31\u0e19\u0e08\u0e31\u0e19\u0e17\u0e23\u0e4c","\u0e27\u0e31\u0e19\u0e2d\u0e31\u0e07\u0e04\u0e32\u0e23","\u0e27\u0e31\u0e19\u0e1e\u0e38\u0e18","\u0e27\u0e31\u0e19\u0e1e\u0e24\u0e2b\u0e31\u0e2a\u0e1a\u0e14\u0e35","\u0e27\u0e31\u0e19\u0e28\u0e38\u0e01\u0e23\u0e4c","\u0e27\u0e31\u0e19\u0e40\u0e2a\u0e32\u0e23\u0e4c"],b:["\u0e21.\u0e04.","\u0e01.\u0e1e.","\u0e21\u0e35.\u0e04.","\u0e40\u0e21.\u0e22.","\u0e1e.\u0e04.","\u0e21\u0e34.\u0e22.","\u0e01.\u0e04.","\u0e2a.\u0e04.","\u0e01.\u0e22.","\u0e15.\u0e04.","\u0e1e.\u0e22.","\u0e18.\u0e04."],B:["\u0e21\u0e01\u0e23\u0e32\u0e04\u0e21","\u0e01\u0e38\u0e21\u0e20\u0e32\u0e1e\u0e31\u0e19\u0e18\u0e4c","\u0e21\u0e35\u0e19\u0e32\u0e04\u0e21","\u0e40\u0e21\u0e29\u0e32\u0e22\u0e19","\u0e1e\u0e24\u0e29\u0e20\u0e32\u0e04\u0e21","\u0e21\u0e34\u0e16\u0e38\u0e19\u0e32\u0e22\u0e19","\u0e01\u0e23\u0e01\u0e0e\u0e32\u0e04\u0e21","\u0e2a\u0e34\u0e07\u0e2b\u0e32\u0e04\u0e21","\u0e01\u0e31\u0e19\u0e22\u0e32\u0e22\u0e19","\u0e15\u0e38\u0e25\u0e32\u0e04\u0e21","\u0e1e\u0e24\u0e28\u0e08\u0e34\u0e01\u0e32\u0e22\u0e19","\u0e18\u0e31\u0e19\u0e27\u0e32\u0e04\u0e21"],c:"%a %d %b %Y, %k \u0e19\u0e32\u0e2c\u0e34\u0e01\u0e32 %M \u0e19\u0e32\u0e17\u0e35 %S \u0e27\u0e34\u0e19\u0e32\u0e17\u0e35 %Z",p:["\u0e01\u0e48\u0e2d\u0e19\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07","\u0e2b\u0e25\u0e31\u0e07\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07"],P:["\u0e01\u0e48\u0e2d\u0e19\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07","\u0e2b\u0e25\u0e31\u0e07\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07"],x:"%d/%m/%Y",X:"%k \u0e19\u0e32\u0e2c\u0e34\u0e01\u0e32 %M \u0e19\u0e32\u0e17\u0e35 %S \u0e27\u0e34\u0e19\u0e32\u0e17\u0e35"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_th.js b/js/yui3/datatype-date-format/lang/datatype-date-format_th.js
new file mode 100644
index 000000000..aed065c34
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_th.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_th",function(e){e.Intl.add("datatype-date-format","th",{a:["\u0e2d\u0e32.","\u0e08.","\u0e2d.","\u0e1e.","\u0e1e\u0e24.","\u0e28.","\u0e2a."],A:["\u0e27\u0e31\u0e19\u0e2d\u0e32\u0e17\u0e34\u0e15\u0e22\u0e4c","\u0e27\u0e31\u0e19\u0e08\u0e31\u0e19\u0e17\u0e23\u0e4c","\u0e27\u0e31\u0e19\u0e2d\u0e31\u0e07\u0e04\u0e32\u0e23","\u0e27\u0e31\u0e19\u0e1e\u0e38\u0e18","\u0e27\u0e31\u0e19\u0e1e\u0e24\u0e2b\u0e31\u0e2a\u0e1a\u0e14\u0e35","\u0e27\u0e31\u0e19\u0e28\u0e38\u0e01\u0e23\u0e4c","\u0e27\u0e31\u0e19\u0e40\u0e2a\u0e32\u0e23\u0e4c"],b:["\u0e21.\u0e04.","\u0e01.\u0e1e.","\u0e21\u0e35.\u0e04.","\u0e40\u0e21.\u0e22.","\u0e1e.\u0e04.","\u0e21\u0e34.\u0e22.","\u0e01.\u0e04.","\u0e2a.\u0e04.","\u0e01.\u0e22.","\u0e15.\u0e04.","\u0e1e.\u0e22.","\u0e18.\u0e04."],B:["\u0e21\u0e01\u0e23\u0e32\u0e04\u0e21","\u0e01\u0e38\u0e21\u0e20\u0e32\u0e1e\u0e31\u0e19\u0e18\u0e4c","\u0e21\u0e35\u0e19\u0e32\u0e04\u0e21","\u0e40\u0e21\u0e29\u0e32\u0e22\u0e19","\u0e1e\u0e24\u0e29\u0e20\u0e32\u0e04\u0e21","\u0e21\u0e34\u0e16\u0e38\u0e19\u0e32\u0e22\u0e19","\u0e01\u0e23\u0e01\u0e0e\u0e32\u0e04\u0e21","\u0e2a\u0e34\u0e07\u0e2b\u0e32\u0e04\u0e21","\u0e01\u0e31\u0e19\u0e22\u0e32\u0e22\u0e19","\u0e15\u0e38\u0e25\u0e32\u0e04\u0e21","\u0e1e\u0e24\u0e28\u0e08\u0e34\u0e01\u0e32\u0e22\u0e19","\u0e18\u0e31\u0e19\u0e27\u0e32\u0e04\u0e21"],c:"%a %d %b %Y, %k \u0e19\u0e32\u0e2c\u0e34\u0e01\u0e32 %M \u0e19\u0e32\u0e17\u0e35 %S \u0e27\u0e34\u0e19\u0e32\u0e17\u0e35 %Z",p:["\u0e01\u0e48\u0e2d\u0e19\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07","\u0e2b\u0e25\u0e31\u0e07\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07"],P:["\u0e01\u0e48\u0e2d\u0e19\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07","\u0e2b\u0e25\u0e31\u0e07\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07"],x:"%d/%m/%Y",X:"%k \u0e19\u0e32\u0e2c\u0e34\u0e01\u0e32 %M \u0e19\u0e32\u0e17\u0e35 %S \u0e27\u0e34\u0e19\u0e32\u0e17\u0e35"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_tr-TR.js b/js/yui3/datatype-date-format/lang/datatype-date-format_tr-TR.js
new file mode 100644
index 000000000..8b9bcdfbc
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_tr-TR.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_tr-TR",function(e){e.Intl.add("datatype-date-format","tr-TR",{a:["Paz","Pzt","Sal","\u00c7ar","Per","Cum","Cmt"],A:["Pazar","Pazartesi","Sal\u0131","\u00c7ar\u015famba","Per\u015fembe","Cuma","Cumartesi"],b:["Oca","\u015eub","Mar","Nis","May","Haz","Tem","A\u011fu","Eyl","Eki","Kas","Ara"],B:["Ocak","\u015eubat","Mart","Nisan","May\u0131s","Haziran","Temmuz","A\u011fustos","Eyl\u00fcl","Ekim","Kas\u0131m","Aral\u0131k"],c:"%d %b %Y %a %H:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d.%m.%Y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_tr.js b/js/yui3/datatype-date-format/lang/datatype-date-format_tr.js
new file mode 100644
index 000000000..ca1e60a66
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_tr.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_tr",function(e){e.Intl.add("datatype-date-format","tr",{a:["Paz","Pzt","Sal","\u00c7ar","Per","Cum","Cmt"],A:["Pazar","Pazartesi","Sal\u0131","\u00c7ar\u015famba","Per\u015fembe","Cuma","Cumartesi"],b:["Oca","\u015eub","Mar","Nis","May","Haz","Tem","A\u011fu","Eyl","Eki","Kas","Ara"],B:["Ocak","\u015eubat","Mart","Nisan","May\u0131s","Haziran","Temmuz","A\u011fustos","Eyl\u00fcl","Ekim","Kas\u0131m","Aral\u0131k"],c:"%d %b %Y %a %H:%M:%S %Z",p:["AM","PM"],P:["am","pm"],x:"%d.%m.%Y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_vi-VN.js b/js/yui3/datatype-date-format/lang/datatype-date-format_vi-VN.js
new file mode 100644
index 000000000..b65bda623
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_vi-VN.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_vi-VN",function(e){e.Intl.add("datatype-date-format","vi-VN",{a:["CN","Th 2","Th 3","Th 4","Th 5","Th 6","Th 7"],A:["Ch\u1ee7 nh\u1eadt","Th\u1ee9 hai","Th\u1ee9 ba","Th\u1ee9 t\u01b0","Th\u1ee9 n\u0103m","Th\u1ee9 s\u00e1u","Th\u1ee9 b\u1ea3y"],b:["thg 1","thg 2","thg 3","thg 4","thg 5","thg 6","thg 7","thg 8","thg 9","thg 10","thg 11","thg 12"],B:["th\u00e1ng m\u1ed9t","th\u00e1ng hai","th\u00e1ng ba","th\u00e1ng t\u01b0","th\u00e1ng n\u0103m","th\u00e1ng s\u00e1u","th\u00e1ng b\u1ea3y","th\u00e1ng t\u00e1m","th\u00e1ng ch\u00edn","th\u00e1ng m\u01b0\u1eddi","th\u00e1ng m\u01b0\u1eddi m\u1ed9t","th\u00e1ng m\u01b0\u1eddi hai"],c:"%H:%M:%S %Z %a, %d %b %Y",p:["SA","CH"],P:["sa","ch"],x:"%d/%m/%Y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_vi.js b/js/yui3/datatype-date-format/lang/datatype-date-format_vi.js
new file mode 100644
index 000000000..50e8ae32c
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_vi.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_vi",function(e){e.Intl.add("datatype-date-format","vi",{a:["CN","Th 2","Th 3","Th 4","Th 5","Th 6","Th 7"],A:["Ch\u1ee7 nh\u1eadt","Th\u1ee9 hai","Th\u1ee9 ba","Th\u1ee9 t\u01b0","Th\u1ee9 n\u0103m","Th\u1ee9 s\u00e1u","Th\u1ee9 b\u1ea3y"],b:["thg 1","thg 2","thg 3","thg 4","thg 5","thg 6","thg 7","thg 8","thg 9","thg 10","thg 11","thg 12"],B:["th\u00e1ng m\u1ed9t","th\u00e1ng hai","th\u00e1ng ba","th\u00e1ng t\u01b0","th\u00e1ng n\u0103m","th\u00e1ng s\u00e1u","th\u00e1ng b\u1ea3y","th\u00e1ng t\u00e1m","th\u00e1ng ch\u00edn","th\u00e1ng m\u01b0\u1eddi","th\u00e1ng m\u01b0\u1eddi m\u1ed9t","th\u00e1ng m\u01b0\u1eddi hai"],c:"%H:%M:%S %Z %a, %d %b %Y",p:["SA","CH"],P:["sa","ch"],x:"%d/%m/%Y",X:"%H:%M:%S"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hans-CN.js b/js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hans-CN.js
new file mode 100644
index 000000000..a3243035c
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hans-CN.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_zh-Hans-CN",function(e){e.Intl.add("datatype-date-format","zh-Hans-CN",{a:["\u5468\u65e5","\u5468\u4e00","\u5468\u4e8c","\u5468\u4e09","\u5468\u56db","\u5468\u4e94","\u5468\u516d"],A:["\u661f\u671f\u65e5","\u661f\u671f\u4e00","\u661f\u671f\u4e8c","\u661f\u671f\u4e09","\u661f\u671f\u56db","\u661f\u671f\u4e94","\u661f\u671f\u516d"],b:["1\u6708","2\u6708","3\u6708","4\u6708","5\u6708","6\u6708","7\u6708","8\u6708","9\u6708","10\u6708","11\u6708","12\u6708"],B:["1\u6708","2\u6708","3\u6708","4\u6708","5\u6708","6\u6708","7\u6708","8\u6708","9\u6708","10\u6708","11\u6708","12\u6708"],c:"%Y\u5e74%b%d\u65e5%a%Z%p%l\u65f6%M\u5206%S\u79d2",p:["\u4e0a\u5348","\u4e0b\u5348"],P:["\u4e0a\u5348","\u4e0b\u5348"],x:"%y-%m-%d",X:"%p%l\u65f6%M\u5206%S\u79d2"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hans.js b/js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hans.js
new file mode 100644
index 000000000..0570e592d
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hans.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_zh-Hans",function(e){e.Intl.add("datatype-date-format","zh-Hans",{a:["\u5468\u65e5","\u5468\u4e00","\u5468\u4e8c","\u5468\u4e09","\u5468\u56db","\u5468\u4e94","\u5468\u516d"],A:["\u661f\u671f\u65e5","\u661f\u671f\u4e00","\u661f\u671f\u4e8c","\u661f\u671f\u4e09","\u661f\u671f\u56db","\u661f\u671f\u4e94","\u661f\u671f\u516d"],b:["1\u6708","2\u6708","3\u6708","4\u6708","5\u6708","6\u6708","7\u6708","8\u6708","9\u6708","10\u6708","11\u6708","12\u6708"],B:["1\u6708","2\u6708","3\u6708","4\u6708","5\u6708","6\u6708","7\u6708","8\u6708","9\u6708","10\u6708","11\u6708","12\u6708"],c:"%Y\u5e74%b%d\u65e5%a%Z%p%l\u65f6%M\u5206%S\u79d2",p:["\u4e0a\u5348","\u4e0b\u5348"],P:["\u4e0a\u5348","\u4e0b\u5348"],x:"%y-%m-%d",X:"%p%l\u65f6%M\u5206%S\u79d2"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hant-HK.js b/js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hant-HK.js
new file mode 100644
index 000000000..f19779ab7
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hant-HK.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_zh-Hant-HK",function(e){e.Intl.add("datatype-date-format","zh-Hant-HK",{a:["\u9031\u65e5","\u9031\u4e00","\u9031\u4e8c","\u9031\u4e09","\u9031\u56db","\u9031\u4e94","\u9031\u516d"],A:["\u661f\u671f\u65e5","\u661f\u671f\u4e00","\u661f\u671f\u4e8c","\u661f\u671f\u4e09","\u661f\u671f\u56db","\u661f\u671f\u4e94","\u661f\u671f\u516d"],b:["1\u6708","2\u6708","3\u6708","4\u6708","5\u6708","6\u6708","7\u6708","8\u6708","9\u6708","10\u6708","11\u6708","12\u6708"],B:["1\u6708","2\u6708","3\u6708","4\u6708","5\u6708","6\u6708","7\u6708","8\u6708","9\u6708","10\u6708","11\u6708","12\u6708"],c:"%Y\u5e74%b%d\u65e5%a%Z%p%l\u6642%M\u5206%S\u79d2",p:["\u4e0a\u5348","\u4e0b\u5348"],P:["\u4e0a\u5348","\u4e0b\u5348"],x:"%y\u5e74%m\u6708%d\u65e5",X:"%p%l\u6642%M\u5206%S\u79d2"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hant-TW.js b/js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hant-TW.js
new file mode 100644
index 000000000..b3c9eea63
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hant-TW.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_zh-Hant-TW",function(e){e.Intl.add("datatype-date-format","zh-Hant-TW",{a:["\u9031\u65e5","\u9031\u4e00","\u9031\u4e8c","\u9031\u4e09","\u9031\u56db","\u9031\u4e94","\u9031\u516d"],A:["\u661f\u671f\u65e5","\u661f\u671f\u4e00","\u661f\u671f\u4e8c","\u661f\u671f\u4e09","\u661f\u671f\u56db","\u661f\u671f\u4e94","\u661f\u671f\u516d"],b:["1\u6708","2\u6708","3\u6708","4\u6708","5\u6708","6\u6708","7\u6708","8\u6708","9\u6708","10\u6708","11\u6708","12\u6708"],B:["1\u6708","2\u6708","3\u6708","4\u6708","5\u6708","6\u6708","7\u6708","8\u6708","9\u6708","10\u6708","11\u6708","12\u6708"],c:"%Y\u5e74%b%d\u65e5%a%Z%p%l\u6642%M\u5206%S\u79d2",p:["\u4e0a\u5348","\u4e0b\u5348"],P:["\u4e0a\u5348","\u4e0b\u5348"],x:"%y/%m/%d",X:"%p%l\u6642%M\u5206%S\u79d2"})},"3.17.2");
diff --git a/js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hant.js b/js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hant.js
new file mode 100644
index 000000000..4df72ee28
--- /dev/null
+++ b/js/yui3/datatype-date-format/lang/datatype-date-format_zh-Hant.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/datatype-date-format_zh-Hant",function(e){e.Intl.add("datatype-date-format","zh-Hant",{a:["\u9031\u65e5","\u9031\u4e00","\u9031\u4e8c","\u9031\u4e09","\u9031\u56db","\u9031\u4e94","\u9031\u516d"],A:["\u661f\u671f\u65e5","\u661f\u671f\u4e00","\u661f\u671f\u4e8c","\u661f\u671f\u4e09","\u661f\u671f\u56db","\u661f\u671f\u4e94","\u661f\u671f\u516d"],b:["1\u6708","2\u6708","3\u6708","4\u6708","5\u6708","6\u6708","7\u6708","8\u6708","9\u6708","10\u6708","11\u6708","12\u6708"],B:["1\u6708","2\u6708","3\u6708","4\u6708","5\u6708","6\u6708","7\u6708","8\u6708","9\u6708","10\u6708","11\u6708","12\u6708"],c:"%Y\u5e74%b%d\u65e5%a%Z%p%l\u6642%M\u5206%S\u79d2",p:["\u4e0a\u5348","\u4e0b\u5348"],P:["\u4e0a\u5348","\u4e0b\u5348"],x:"%y/%m/%d",X:"%p%l\u6642%M\u5206%S\u79d2"})},"3.17.2");
diff --git a/js/yui3/datatype-date-math/datatype-date-math-min.js b/js/yui3/datatype-date-math/datatype-date-math-min.js
new file mode 100644
index 000000000..a885ed17c
--- /dev/null
+++ b/js/yui3/datatype-date-math/datatype-date-math-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatype-date-math",function(e,t){var n=e.Lang;e.mix(e.namespace("Date"),{isValidDate:function(e){return n.isDate(e)&&isFinite(e)&&e!="Invalid Date"&&!isNaN(e)&&e!=null?!0:!1},areEqual:function(e,t){return this.isValidDate(e)&&this.isValidDate(t)&&e.getTime()==t.getTime()},isGreater:function(e,t){return this.isValidDate(e)&&this.isValidDate(t)&&e.getTime()>t.getTime()},isGreaterOrEqual:function(e,t){return this.isValidDate(e)&&this.isValidDate(t)&&e.getTime()>=t.getTime()},isInRange:function(e,t,n){return this.isGreaterOrEqual(e,t)&&this.isGreaterOrEqual(n,e)},addDays:function(e,t){return new Date(e.getTime()+864e5*t)},addMonths:function(e,t){var n=e.getFullYear(),r=e.getMonth()+t;n=Math.floor(n+r/12),r=(r%12+12)%12;var i=new Date(e.getTime());return i.setFullYear(n),i.setMonth(r),i},addYears:function(e,t){var n=e.getFullYear()+t,r=new Date(e.getTime());return r.setFullYear(n),r},listOfDatesInMonth:function(e){if(!this.isValidDate(e))return[];var t=this.daysInMonth(e),n=e.getFullYear(),r=e.getMonth(),i=[];for(var s=1;s<=t;s++)i.push(new Date(n,r,s,12,0,0));return i},daysInMonth:function(e){if(!this.isValidDate(e))return 0;var t=e.getMonth(),n=[31,28,31,30,31,30,31,31,30,31,30,31];if(t!=1)return n[t];var r=e.getFullYear();return r%400===0?29:r%100===0?28:r%4===0?29:28}}),e.namespace("DataType"),e.DataType.Date=e.Date},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/datatype-date-parse/datatype-date-parse-min.js b/js/yui3/datatype-date-parse/datatype-date-parse-min.js
new file mode 100644
index 000000000..a5be5fe7e
--- /dev/null
+++ b/js/yui3/datatype-date-parse/datatype-date-parse-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatype-date-parse",function(e,t){e.mix(e.namespace("Date"),{parse:function(t){var n=new Date(+t||t);return e.Lang.isDate(n)?n:null}}),e.namespace("Parsers").date=e.Date.parse,e.namespace("DataType"),e.DataType.Date=e.Date},"3.17.2");
diff --git a/js/yui3/datatype-number-format/datatype-number-format-min.js b/js/yui3/datatype-number-format/datatype-number-format-min.js
new file mode 100644
index 000000000..61481a49c
--- /dev/null
+++ b/js/yui3/datatype-number-format/datatype-number-format-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatype-number-format",function(e,t){var n=e.Lang;e.mix(e.namespace("Number"),{format:function(e,t){if(n.isNumber(e)){t=t||{};var r=e<0,i=e+"",s=t.decimalPlaces,o=t.decimalSeparator||".",u=t.thousandsSeparator,a,f,l,c;n.isNumber(s)&&s>=0&&s<=20&&(i=e.toFixed(s)),o!=="."&&(i=i.replace(".",o));if(u){a=i.lastIndexOf(o),a=a>-1?a:i.length,f=i.substring(a);for(l=0,c=a;c>0;c--)l%3===0&&c!==a&&(!r||c>1)&&(f=u+f),f=i.charAt(c-1)+f,l++;i=f}return i=t.prefix?t.prefix+i:i,i=t.suffix?i+t.suffix:i,i}return n.isValue(e)&&e.toString?e.toString():""}}),e.namespace("DataType"),e.DataType.Number=e.Number},"3.17.2");
diff --git a/js/yui3/datatype-number-parse/datatype-number-parse-min.js b/js/yui3/datatype-number-parse/datatype-number-parse-min.js
new file mode 100644
index 000000000..94479ec76
--- /dev/null
+++ b/js/yui3/datatype-number-parse/datatype-number-parse-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatype-number-parse",function(e,t){var n=e.Escape.regex,r="\\s*";e.mix(e.namespace("Number"),{_buildParser:e.cached(function(e,t,i,s){var o=[],u;return e&&o.push("^"+r+n(e)+r),t&&o.push(r+n(t)+r+"$"),i&&o.push(n(i)+"(?=\\d)"),u=new RegExp("(?:"+o.join("|")+")","g"),s==="."&&(s=null),function(e){return e=e.replace(u,""),s?e.replace(s,"."):e}}),parse:function(t,n){var r;n&&typeof t=="string"&&(r=this._buildParser(n.prefix,n.suffix,n.thousandsSeparator,n.decimalSeparator),t=r(t)),typeof t=="string"&&e.Lang.trim(t)!==""&&(t=+t);if(typeof t!="number"||!isFinite(t))t=null;return t}}),e.namespace("Parsers").number=e.Number.parse,e.namespace("DataType"),e.DataType.Number=e.Number},"3.17.2",{requires:["escape"]});
diff --git a/js/yui3/datatype-xml-format/datatype-xml-format-min.js b/js/yui3/datatype-xml-format/datatype-xml-format-min.js
new file mode 100644
index 000000000..335672d0f
--- /dev/null
+++ b/js/yui3/datatype-xml-format/datatype-xml-format-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatype-xml-format",function(e,t){var n=e.Lang;e.mix(e.namespace("XML"),{format:function(e){try{if(!n.isUndefined(e.getXml))return e.getXml();if(!n.isUndefined(XMLSerializer))return(new XMLSerializer).serializeToString(e)}catch(t){return e&&e.xml?e.xml:n.isValue(e)&&e.toString?e.toString():""}}}),e.namespace("DataType"),e.DataType.XML=e.XML},"3.17.2");
diff --git a/js/yui3/datatype-xml-parse/datatype-xml-parse-min.js b/js/yui3/datatype-xml-parse/datatype-xml-parse-min.js
new file mode 100644
index 000000000..f210f2dfa
--- /dev/null
+++ b/js/yui3/datatype-xml-parse/datatype-xml-parse-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("datatype-xml-parse",function(e,t){e.mix(e.namespace("XML"),{parse:function(t){var n=null,r;return typeof t=="string"&&(r=e.config.win,r.ActiveXObject!==undefined?(n=new ActiveXObject("Microsoft.XMLDOM"),n.async=!1,n.loadXML(t)):r.DOMParser!==undefined?n=(new DOMParser).parseFromString(t,"text/xml"):r.Windows!==undefined&&(n=new Windows.Data.Xml.Dom.XmlDocument,n.loadXml(t))),n===null||n.documentElement===null||n.documentElement.nodeName==="parsererror",n}}),e.namespace("Parsers").xml=e.XML.parse,e.namespace("DataType"),e.DataType.XML=e.XML},"3.17.2");
diff --git a/js/yui3/dd-constrain/dd-constrain-min.js b/js/yui3/dd-constrain/dd-constrain-min.js
new file mode 100644
index 000000000..891d3c81a
--- /dev/null
+++ b/js/yui3/dd-constrain/dd-constrain-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dd-constrain",function(e,t){var n="dragNode",r="offsetHeight",i="offsetWidth",s="host",o="tickXArray",u="tickYArray",a=e.DD.DDM,f="top",l="right",c="bottom",h="left",p="view",d=null,v="drag:tickAlignX",m="drag:tickAlignY",g=function(){this._lazyAddAttrs=!1,g.superclass.constructor.apply(this,arguments)};g.NAME="ddConstrained",g.NS="con",g.ATTRS={host:{},stickX:{value:!1},stickY:{value:!1},tickX:{value:!1},tickY:{value:!1},tickXArray:{value:!1},tickYArray:{value:!1},gutter:{value:"0",setter:function(t){return e.DD.DDM.cssSizestoObject(t)}},constrain:{value:p,setter:function(t){var n=e.one(t);return n&&(t=n),t}},constrain2region:{setter:function(e){return this.set("constrain",e)}},constrain2node:{setter:function(t){return this.set("constrain",e.one(t))}},constrain2view:{setter:function(){return this.set("constrain",p)}},cacheRegion:{value:!0}},d={_lastTickXFired:null,_lastTickYFired:null,initializer:function(){this._createEvents(),this._eventHandles=[this.get(s).on("drag:end",e.bind(this._handleEnd,this)),this.get(s).on("drag:start",e.bind(this._handleStart,this)),this.get(s).after("drag:align",e.bind(this.align,this)),this.get(s).after("drag:drag",e.bind(this.drag,this))]},destructor:function(){e.Array.each(this._eventHandles,function(e){e.detach()}),this._eventHandles.length=0},_createEvents:function(){var t=[v,m];e.Array.each(t,function(e){this.publish(e,{type:e,emitFacade:!0,bubbles:!0,queuable:!1,prefix:"drag"})},this)},_handleEnd:function(){this._lastTickYFired=null,this._lastTickXFired=null},_handleStart:function(){this.resetCache()},_regionCache:null,_cacheRegion:function(){this._regionCache=this.get("constrain").get("region")},resetCache:function(){this._regionCache=null},_getConstraint:function(){var t=this.get("constrain"),r=this.get("gutter"),i;t&&(t instanceof e.Node?(this._regionCache||(this._eventHandles.push(e.on("resize",e.bind(this._cacheRegion,this),e.config.win)),this._cacheRegion()),i=e.clone(this._regionCache),this.get("cacheRegion")||this.resetCache()):e.Lang.isObject(t)&&(i=e.clone(t)));if(!t||!i)t=p;return t===p&&(i=this.get(s).get(n).get("viewportRegion")),e.Object.each(r,function(e,t){t===l||t===c?i[t]-=e:i[t]+=e}),i},getRegion:function(e){var t={},o=null,u=null,a=this.get(s);return t=this._getConstraint(),e&&(o=a.get(n).get(r),u=a.get(n).get(i),t[l]=t[l]-u,t[c]=t[c]-o),t},_checkRegion:function(e){var t=e,o=this.getRegion(),u=this.get(s),a=u.get(n).get(r),p=u.get(n).get(i);return t[1]>o[c]-a&&(e[1]=o[c]-a),o[f]>t[1]&&(e[1]=o[f]),t[0]>o[l]-p&&(e[0]=o[l]-p),o[h]>t[0]&&(e[0]=o[h]),e},inRegion:function(e){e=e||this.get(s).get(n).getXY();var t=this._checkRegion([e[0],e[1]]),r=!1;return e[0]===t[0]&&e[1]===t[1]&&(r=!0),r},align:function(){var e=this.get(s),t=[e.actXY[0],e.actXY[1]],n=this.getRegion(!0);this.get("stickX")&&(t[1]=e.startXY[1]-e.deltaXY[1]),this.get("stickY")&&(t[0]=e.startXY[0]-e.deltaXY[0]),n&&(t=this._checkRegion(t)),t=this._checkTicks(t,n),e.actXY=t},drag:function(){var t=this.get(s),n=this.get("tickX"),r=this.get("tickY"),i=[t.actXY[0],t.actXY[1]];(e.Lang.isNumber(n)||this.get(o))&&this._lastTickXFired!==i[0]&&(this._tickAlignX(),this._lastTickXFired=i[0]),(e.Lang.isNumber(r)||this.get(u))&&this._lastTickYFired!==i[1]&&(this._tickAlignY(),this._lastTickYFired=i[1])},_checkTicks:function(e,t){var n=this.get(s),r=n.startXY[0]-n.deltaXY[0],i=n.startXY[1]-n.deltaXY[1],p=this.get("tickX"),d=this.get("tickY");return p&&!this.get(o)&&(e[0]=a._calcTicks(e[0],r,p,t[h],t[l])),d&&!this.get(u)&&(e[1]=a._calcTicks(e[1],i,d,t[f],t[c])),this.get(o)&&(e[0]=a._calcTickArray(e[0],this.get(o),t[h],t[l])),this.get(u)&&(e[1]=a._calcTickArray(e[1],this.get(u),t[f],t[c])),e},_tickAlignX:function(){this.fire(v)},_tickAlignY:function(){this.fire(m)}},e.namespace("Plugin"),e.extend(g,e.Base,d),e.Plugin.DDConstrained=g,e.mix(a,{_calcTicks:function(e,t,n,r,i){var s=(e-t)/n,o=Math.floor(s),u=Math.ceil(s);return(o!==0||u!==0)&&s>=o&&s<=u&&(e=t+n*o,r&&i&&(e<r&&(e=t+n*(o+1)),e>i&&(e=t+n*(o-1)))),e},_calcTickArray:function(e,t,n,r){var i=0,s=t.length,o=0,u,a,f;if(!t||t.length===0)return e;if(t[0]>=e)return t[0];for(i=0;i<s;i++){o=i+1;if(t[o]&&t[o]>=e)return u=e-t[i],a=t[o]-e,f=a>u?t[i]:t[o],n&&r&&f>r&&(t[i]?f=t[i]:f=t[s-1]),f}return t[t.length-1]}})},"3.17.2",{requires:["dd-drag"]});
diff --git a/js/yui3/dd-ddm-base/dd-ddm-base-min.js b/js/yui3/dd-ddm-base/dd-ddm-base-min.js
new file mode 100644
index 000000000..efe555b93
--- /dev/null
+++ b/js/yui3/dd-ddm-base/dd-ddm-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dd-ddm-base",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)};n.NAME="ddm",n.ATTRS={dragCursor:{value:"move"},clickPixelThresh:{value:3},clickTimeThresh:{value:1e3},throttleTime:{value:-1},dragMode:{value:"point",setter:function(e){return this._setDragMode(e),e}}},e.extend(n,e.Base,{_createPG:function(){},_active:null,_setDragMode:function(t){t===null&&(t=e.DD.DDM.get("dragMode"));switch(t){case 1:case"intersect":return 1;case 2:case"strict":return 2;case 0:case"point":return 0}return 0},CSS_PREFIX:e.ClassNameManager.getClassName("dd"),_activateTargets:function(){},_drags:[],activeDrag:!1,_regDrag:function(e){return this.getDrag(e.get("node"))?!1:(this._active||this._setupListeners(),this._drags.push(e),!0)},_unregDrag:function(t){var n=[];e.Array.each(this._drags,function(e){e!==t&&(n[n.length]=e)}),this._drags=n},_setupListeners:function(){this._createPG(),this._active=!0;var t=e.one(e.config.doc);t.on("mousemove",e.throttle(e.bind(this._docMove,this),this.get("throttleTime"))),t.on("mouseup",e.bind(this._end,this))},_start:function(){this.fire("ddm:start"),this._startDrag()},_startDrag:function(){},_endDrag:function(){},_dropMove:function(){},_end:function(){this.activeDrag&&(this._shimming=!1,this._endDrag(),this.fire("ddm:end"),this.activeDrag.end.call(this.activeDrag),this.activeDrag=null)},stopDrag:function(){return this.activeDrag&&this._end(),this},_shimming:!1,_docMove:function(e){this._shimming||this._move(e)},_move:function(e){this.activeDrag&&(this.activeDrag._move.call(this.activeDrag,e),this._dropMove())},cssSizestoObject:function(e){var t=e.split(" ");switch(t.length){case 1:t[1]=t[2]=t[3]=t[0];break;case 2:t[2]=t[0],t[3]=t[1];break;case 3:t[3]=t[1]}return{top:parseInt(t[0],10),right:parseInt(t[1],10),bottom:parseInt(t[2],10),left:parseInt(t[3],10)}},getDrag:function(t){var n=!1,r=e.one(t);return r instanceof e.Node&&e.Array.each(this._drags,function(e){r.compareTo(e.get("node"))&&(n=e)}),n},swapPosition:function(t,n){t=e.DD.DDM.getNode(t),n=e.DD.DDM.getNode(n);var r=t.getXY(),i=n.getXY();return t.setXY(i),n.setXY(r),t},getNode:function(t){return t instanceof e.Node?t:(t&&t.get?e.Widget&&t instanceof e.Widget?t=t.get("boundingBox"):t=t.get("node"):t=e.one(t),t)},swapNode:function(t,n){t=e.DD.DDM.getNode(t),n=e.DD.DDM.getNode(n);var r=n.get("parentNode"),i=n.get("nextSibling");return i===t?r.insertBefore(t,n):n===t.get("nextSibling")?r.insertBefore(n,t):(t.get("parentNode").replaceChild(n,t),r.insertBefore(t,i)),t}}),e.namespace("DD"),e.DD.DDM=new n},"3.17.2",{requires:["node","base","yui-throttle","classnamemanager"]});
diff --git a/js/yui3/dd-ddm-drop/dd-ddm-drop-min.js b/js/yui3/dd-ddm-drop/dd-ddm-drop-min.js
new file mode 100644
index 000000000..a8206edc2
--- /dev/null
+++ b/js/yui3/dd-ddm-drop/dd-ddm-drop-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dd-ddm-drop",function(e,t){e.mix(e.DD.DDM,{_noShim:!1,_activeShims:[],_hasActiveShim:function(){return this._noShim?!0:this._activeShims.length},_addActiveShim:function(e){this._activeShims.push(e)},_removeActiveShim:function(t){var n=[];e.Array.each(this._activeShims,function(e){e._yuid!==t._yuid&&n.push(e)}),this._activeShims=n},syncActiveShims:function(t){e.later(0,this,function(t){var n=t?this.targets:this._lookup();e.Array.each(n,function(e){e.sizeShim.call(e)},this)},t)},mode:0,POINT:0,INTERSECT:1,STRICT:2,useHash:!0,activeDrop:null,validDrops:[],otherDrops:{},targets:[],_addValid:function(e){return this.validDrops.push(e),this},_removeValid:function(t){var n=[];return e.Array.each(this.validDrops,function(e){e!==t&&n.push(e)}),this.validDrops=n,this},isOverTarget:function(e){if(this.activeDrag&&e){var t=this.activeDrag.mouseXY,n,r=this.activeDrag.get("dragMode"),i,s=e.shim;if(t&&this.activeDrag){i=this.activeDrag.region;if(r===this.STRICT)return this.activeDrag.get("dragNode").inRegion(e.region,!0,i);if(e&&e.shim)return r===this.INTERSECT&&this._noShim?(n=i||this.activeDrag.get("node"),e.get("node").intersect(n,e.region).inRegion):(this._noShim&&(s=e.get("node")),s.intersect({top:t[1],bottom:t[1],left:t[0],right:t[0]},e.region).inRegion)}}return!1},clearCache:function(){this.validDrops=[],this.otherDrops={},this._activeShims=[]},_activateTargets:function(){this._noShim=!0,this.clearCache(),e.Array.each(this.targets,function(e){e._activateShim([]),e.get("noShim")===!0&&(this._noShim=!1)},this),this._handleTargetOver()},getBestMatch:function(t,n){var r=null,i=0,s;return e.Object.each(t,function(e){var t=this.activeDrag.get("dragNode").intersect(e.get("node"));e.region.area=t.area,t.inRegion&&t.area>i&&(i=t.area,r=e)},this),n?(s=[],e.Object.each(t,function(e){e!==r&&s.push(e)},this),[r,s]):r},_deactivateTargets:function(){var t=[],n,r=this.activeDrag,i=this.activeDrop;r&&i&&this.otherDrops[i]?(r.get("dragMode")?(n=this.getBestMatch(this.otherDrops,!0),i=n[0],t=n[1]):(t=this.otherDrops,delete t[i]),r.get("node").removeClass(this.CSS_PREFIX+"-drag-over"),i&&(i.fire("drop:hit",{drag:r,drop:i,others:t}),r.fire("drag:drophit",{drag:r,drop:i,others:t}))):r&&r.get("dragging")&&(r.get("node").removeClass(this.CSS_PREFIX+"-drag-over"),r.fire("drag:dropmiss",{pageX:r.lastXY[0],pageY:r.lastXY[1]})),this.activeDrop=null,e.Array.each(this.targets,function(e){e._deactivateShim([])},this)},_dropMove:function(){this._hasActiveShim()?this._handleTargetOver():e.Object.each(this.otherDrops,function(e){e._handleOut.apply(e,[])})},_lookup:function(){if(!this.useHash||this._noShim)return this.validDrops;var t=[];return e.Array.each(this.validDrops,function(e){e.shim&&e.shim.inViewportRegion(!1,e.region)&&t.push(e)}),t},_handleTargetOver:function(){var t=this._lookup();e.Array.each(t,function(e){e._handleTargetOver.call(e)},this)},_regTarget:function(e){this.targets.push(e)},_unregTarget:function(t){var n=[],r;e.Array.each(this.targets,function(e){e!==t&&n.push(e)},this),this.targets=n,r=[],e.Array.each(this.validDrops,function(e){e!==t&&r.push(e)}),this.validDrops=r},getDrop:function(t){var n=!1,r=e.one(t);return r instanceof e.Node&&e.Array.each(this.targets,function(e){r.compareTo(e.get("node"))&&(n=e)}),n}},!0)},"3.17.2",{requires:["dd-ddm"]});
diff --git a/js/yui3/dd-ddm/dd-ddm-min.js b/js/yui3/dd-ddm/dd-ddm-min.js
new file mode 100644
index 000000000..25ada65d0
--- /dev/null
+++ b/js/yui3/dd-ddm/dd-ddm-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dd-ddm",function(e,t){e.mix(e.DD.DDM,{_pg:null,_debugShim:!1,_activateTargets:function(){},_deactivateTargets:function(){},_startDrag:function(){this.activeDrag&&this.activeDrag.get("useShim")&&(this._shimming=!0,this._pg_activate(),this._activateTargets())},_endDrag:function(){this._pg_deactivate(),this._deactivateTargets()},_pg_deactivate:function(){this._pg.setStyle("display","none")},_pg_activate:function(){this._pg||this._createPG();var e=this.activeDrag.get("activeHandle"),t="auto";e&&(t=e.getStyle("cursor")),t==="auto"&&(t=this.get("dragCursor")),this._pg_size(),this._pg.setStyles({top:0,left:0,display:"block",opacity:this._debugShim?".5":"0",cursor:t})},_pg_size:function(){if(this.activeDrag){var t=e.one("body"),n=t.get("docHeight"),r=t.get("docWidth");this._pg.setStyles({height:n+"px",width:r+"px"})}},_createPG:function(){var t=e.Node.create("<div></div>"),n=e.one("body"),r;t.setStyles({top:"0",left:"0",position:"absolute",zIndex:"9999",overflow:"hidden",backgroundColor:"red",display:"none",height:"5px",width:"5px"}),t.set("id",e.stamp(t)),t.addClass(e.DD.DDM.CSS_PREFIX+"-shim"),n.prepend(t),this._pg=t,this._pg.on("mousemove",e.throttle(e.bind(this._move,this),this.get("throttleTime"))),this._pg.on("mouseup",e.bind(this._end,this)),r=e.one("win"),e.on("window:resize",e.bind(this._pg_size,this)),r.on("scroll",e.bind(this._pg_size,this))}},!0)},"3.17.2",{requires:["dd-ddm-base","event-resize"]});
diff --git a/js/yui3/dd-delegate/dd-delegate-min.js b/js/yui3/dd-delegate/dd-delegate-min.js
new file mode 100644
index 000000000..1fa96d029
--- /dev/null
+++ b/js/yui3/dd-delegate/dd-delegate-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dd-delegate",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)},r="container",i="nodes",s=e.Node.create("<div>Temp Node</div>");e.extend(n,e.Base,{_bubbleTargets:e.DD.DDM,dd:null,_shimState:null,_handles:null,_onNodeChange:function(e){this.set("dragNode",e.newVal)},_afterDragEnd:function(){e.DD.DDM._noShim=this._shimState,this.set("lastNode",this.dd.get("node")),this.get("lastNode").removeClass(e.DD.DDM.CSS_PREFIX+"-dragging"),this.dd._unprep(),this.dd.set("node",s)},_delMouseDown:function(t){var n=t.currentTarget,r=this.dd,s=n,o=this.get("dragConfig");n.test(this.get(i))&&!n.test(this.get("invalid"))&&(this._shimState=e.DD.DDM._noShim,e.DD.DDM._noShim=!0,this.set("currentNode",n),r.set("node",n),o&&o.dragNode?s=o.dragNode:r.proxy&&(s=e.DD.DDM._proxy),r.set("dragNode",s),r._prep(),r.fire("drag:mouseDown",{ev:t}))},_onMouseEnter:function(){this._shimState=e.DD.DDM._noShim,e.DD.DDM._noShim=!0},_onMouseLeave:function(){e.DD.DDM._noShim=this._shimState},initializer:function(){this._handles=[];var t=this.get("dragConfig")||{},n=this.get(r);t.node=s.cloneNode(!0),t.bubbleTargets=this,this.get("handles")&&(t.handles=this.get("handles")),this.dd=new e.DD.Drag(t),this.dd.after("drag:end",e.bind(this._afterDragEnd,this)),this.dd.on("dragNodeChange",e.bind(this._onNodeChange,this)),this.dd.after("drag:mouseup",function(){this._unprep()}),this._handles.push(e.delegate(e.DD.Drag.START_EVENT,e.bind(this._delMouseDown,this),n,this.get(i))),this._handles.push(e.on("mouseenter",e.bind(this._onMouseEnter,this),n)),this._handles.push(e.on("mouseleave",e.bind(this._onMouseLeave,this),n)),e.later(50,this,this.syncTargets),e.DD.DDM.regDelegate(this)},syncTargets:function(){if(!e.Plugin.Drop||this.get("destroyed"))return;var t,n,s;return this.get("target")&&(t=e.one(this.get(r)).all(this.get(i)),n=this.dd.get("groups"),s=this.get("dragConfig"),s&&s.groups&&(n=s.groups),t.each(function(e){this.createDrop(e,n)},this)),this},createDrop:function(t,n){var r={useShim:!1,bubbleTargets:this};return t.drop||t.plug(e.Plugin.Drop,r),t.drop.set("groups",n),t},destructor:function(){this.dd&&this.dd.destroy();if(e.Plugin.Drop){var t=e.one(this.get(r)).all(this.get(i));t.unplug(e.Plugin.Drop)}e.Array.each(this._handles,function(e){e.detach()})}},{NAME:"delegate",ATTRS:{container:{value:"body"},nodes:{value:".dd-draggable"},invalid:{value:"input, select, button, a, textarea"},lastNode:{value:s},currentNode:{value:s},dragNode:{value:s},over:{value:!1},target:{value:!1},dragConfig:{value:null},handles:{value:null}}}),e.mix(e.DD.DDM,{_delegates:[],regDelegate:function(e){this._delegates.push(e)},getDelegate:function(t){var n=null;return t=e.one(t),e.Array.each(this._delegates,function(e){t.test(e.get(r))&&(n=e)},this),n}}),e.namespace("DD"),e.DD.Delegate=n},"3.17.2",{requires:["dd-drag","dd-drop-plugin","event-mouseenter"]});
diff --git a/js/yui3/dd-drag/dd-drag-min.js b/js/yui3/dd-drag/dd-drag-min.js
new file mode 100644
index 000000000..19ff68213
--- /dev/null
+++ b/js/yui3/dd-drag/dd-drag-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dd-drag",function(e,t){var n=e.DD.DDM,r="node",i="dragging",s="dragNode",o="offsetHeight",u="offsetWidth",a="drag:mouseDown",f="drag:afterMouseDown",l="drag:removeHandle",c="drag:addHandle",h="drag:removeInvalid",p="drag:addInvalid",d="drag:start",v="drag:end",m="drag:drag",g="drag:align",y=function(t){this._lazyAddAttrs=!1,y.superclass.constructor.apply(this,arguments);var r=n._regDrag(this);r||e.error("Failed to register node, already in use: "+t.node)};y.NAME="drag",y.START_EVENT="mousedown",y.ATTRS={node:{setter:function(t){if(this._canDrag(t))return t;var n=e.one(t);return n||e.error("DD.Drag: Invalid Node Given: "+t),n}},dragNode:{setter:function(t){if(this._canDrag(t))return t;var n=e.one(t);return n||e.error("DD.Drag: Invalid dragNode Given: "+t),n}},offsetNode:{value:!0},startCentered:{value:!1},clickPixelThresh:{value:n.get("clickPixelThresh")},clickTimeThresh:{value:n.get("clickTimeThresh")},lock:{value:!1,setter:function(e){return e?this.get(r).addClass(n.CSS_PREFIX+"-locked"):this.get(r).removeClass(n.CSS_PREFIX+"-locked"),e}},data:{value:!1},move:{value:!0},useShim:{value:!0},activeHandle:{value:!1},primaryButtonOnly:{value:!0},dragging:{value:!1},parent:{value:!1},target:{value:!1,setter:function(e){return this._handleTarget(e),e}},dragMode:{value:null,setter:function(e){return n._setDragMode(e)}},groups:{value:["default"],getter:function(){return this._groups?e.Object.keys(this._groups):(this._groups={},[])},setter:function(t){return this._groups=e.Array.hash(t),t}},handles:{value:null,setter:function(t){return t?(this._handles={},e.Array.each(t,function(t){var n=t;if(t instanceof e.Node||t instanceof e.NodeList)n=t._yuid;this._handles[n]=t},this)):this._handles=null,t}},bubbles:{setter:function(e){return this.addTarget(e),e}},haltDown:{value:!0}},e.extend(y,e.Base,{_canDrag:function(e){return e&&e.setXY&&e.getXY&&e.test&&e.contains?!0:!1},_bubbleTargets:e.DD.DDM,addToGroup:function(e){return this._groups[e]=!0,n._activateTargets(),this},removeFromGroup:function(e){return delete this._groups[e],n._activateTargets(),this},target:null,_handleTarget:function(t){e.DD.Drop&&(t===!1?this.target&&(n._unregTarget(this.target),this.target=null):(e.Lang.isObject(t)||(t={}),t.bubbleTargets=t.bubbleTargets||this.getTargets(),t.node=this.get(r),t.groups=t.groups||this.get("groups"),this.target=new e.DD.Drop(t)))},_groups:null,_createEvents:function(){this.publish(a,{defaultFn:this._defMouseDownFn,queuable:!1,emitFacade:!0,bubbles:!0,prefix:"drag"}),this.publish(g,{defaultFn:this._defAlignFn,queuable:!1,emitFacade:!0,bubbles:!0,prefix:"drag"}),this.publish(m,{defaultFn:this._defDragFn,queuable:!1,emitFacade:!0,bubbles:!0,prefix:"drag"}),this.publish(v,{defaultFn:this._defEndFn,preventedFn:this._prevEndFn,queuable:!1,emitFacade:!0,bubbles:!0,prefix:"drag"});var t=[f,l,c,h,p,d,"drag:drophit","drag:dropmiss","drag:over","drag:enter","drag:exit"];e.Array.each(t,function(e){this.publish(e,{type:e,emitFacade:!0,bubbles:!0,preventable:!1,queuable:!1,prefix:"drag"})},this)},_ev_md:null,_startTime:null,_endTime:null,_handles:null,_invalids:null,_invalidsDefault:{textarea:!0,input:!0,a:!0,button:!0,select:!0},_dragThreshMet:null,_fromTimeout:null,_clickTimeout:null,deltaXY:null,startXY:null,nodeXY:null,lastXY:null,actXY:null,realXY:null,mouseXY:null,region:null,_handleMouseUp:function(){this.fire("drag:mouseup"),this._fixIEMouseUp(),n.activeDrag&&n._end()},_fixDragStart:function(e){this.validClick(e)&&e.preventDefault()},_ieSelectFix:function(){return!1},_ieSelectBack:null,_fixIEMouseDown:function(){e.UA.ie&&(this._ieSelectBack=e.config.doc.body.onselectstart,e.config.doc.body.onselectstart=this._ieSelectFix)},_fixIEMouseUp:function(){e.UA.ie&&(e.config.doc.body.onselectstart=this._ieSelectBack)},_handleMouseDownEvent:function(e){this.validClick(e)&&e.preventDefault(),this.fire(a,{ev:e})},_defMouseDownFn:function(t){var r=t.ev;this._dragThreshMet=!1,this._ev_md=r;if(this.get("primaryButtonOnly")&&r.button>1)return!1;this.validClick(r)&&(this._fixIEMouseDown(r),y.START_EVENT.indexOf("gesture")!==0&&(this.get("haltDown")?r.halt():r.preventDefault()),this._setStartPosition([r.pageX,r.pageY]),n.activeDrag=this,this._clickTimeout=e.later(this.get("clickTimeThresh"),this,this._timeoutCheck)),this.fire(f,{ev:r})},validClick:function(t){var n=!1,i=!1,s=t.target,o=null,u=null,a=null,f=!1;if(this._handles)e.Object.each(this._handles,function(t,r){t instanceof e.Node||t instanceof e.NodeList?n||(a=t,a instanceof e.Node&&(a=new e.NodeList(t._node)),a.each(function(e){e.contains(s)&&(n=!0)})):e.Lang.isString(r)&&s.test(r+", "+r+" *")&&!o&&(o=r,n=!0)});else{i=this.get(r);if(i.contains(s)||i.compareTo(s))n=!0}return n&&this._invalids&&e.Object.each(this._invalids,function(t,r){e.Lang.isString(r)&&s.test(r+", "+r+" *")&&(n=!1)}),n&&(o?(u=t.currentTarget.all(o),f=!1,u.each(function(e){(e.contains(s)||e.compareTo(s))&&!f&&(f=!0,this.set("activeHandle",e))},this)):this.set("activeHandle",this.get(r))),n},_setStartPosition:function(e){this.startXY=e,this.nodeXY=this.lastXY=this.realXY=this.get(r).getXY(),this.get("offsetNode")?this.deltaXY=[this.startXY[0]-this.nodeXY[0],this.startXY[1]-this.nodeXY[1]]:this.deltaXY=[0,0]},_timeoutCheck:function(){!this.get("lock")&&!this._dragThreshMet&&this._ev_md&&(this._fromTimeout=this._dragThreshMet=!0,this.start(),this._alignNode([this._ev_md.pageX,this._ev_md.pageY],!0))},removeHandle:function(t){var n=t;if(t instanceof e.Node||t instanceof e.NodeList)n=t._yuid;return this._handles[n]&&(delete this._handles[n],this.fire(l,{handle:t})),this},addHandle:function(t){this._handles||(this._handles={});var n=t;if(t instanceof e.Node||t instanceof e.NodeList)n=t._yuid;return this._handles[n]=t,this.fire(c,{handle:t}),this},removeInvalid:function(e){return this._invalids[e]&&(this._invalids[e]=null,delete this._invalids[e],this.fire(h,{handle:e})),this},addInvalid:function(t){return e.Lang.isString(t)&&(this._invalids[t]=!0,this.fire(p,{handle:t})),this},initializer:function(
+){this.get(r).dd=this;if(!this.get(r).get("id")){var t=e.stamp(this.get(r));this.get(r).set("id",t)}this.actXY=[],this._invalids=e.clone(this._invalidsDefault,!0),this._createEvents(),this.get(s)||this.set(s,this.get(r)),this.on("initializedChange",e.bind(this._prep,this)),this.set("groups",this.get("groups"))},_prep:function(){this._dragThreshMet=!1;var t=this.get(r);t.addClass(n.CSS_PREFIX+"-draggable"),t.on(y.START_EVENT,e.bind(this._handleMouseDownEvent,this)),t.on("mouseup",e.bind(this._handleMouseUp,this)),t.on("dragstart",e.bind(this._fixDragStart,this))},_unprep:function(){var e=this.get(r);e.removeClass(n.CSS_PREFIX+"-draggable"),e.detachAll("mouseup"),e.detachAll("dragstart"),e.detachAll(y.START_EVENT),this.mouseXY=[],this.deltaXY=[0,0],this.startXY=[],this.nodeXY=[],this.lastXY=[],this.actXY=[],this.realXY=[]},start:function(){if(!this.get("lock")&&!this.get(i)){var e=this.get(r),t,a,f;this._startTime=(new Date).getTime(),n._start(),e.addClass(n.CSS_PREFIX+"-dragging"),this.fire(d,{pageX:this.nodeXY[0],pageY:this.nodeXY[1],startTime:this._startTime}),e=this.get(s),f=this.nodeXY,t=e.get(u),a=e.get(o),this.get("startCentered")&&this._setStartPosition([f[0]+t/2,f[1]+a/2]),this.region={0:f[0],1:f[1],area:0,top:f[1],right:f[0]+t,bottom:f[1]+a,left:f[0]},this.set(i,!0)}return this},end:function(){return this._endTime=(new Date).getTime(),this._clickTimeout&&this._clickTimeout.cancel(),this._dragThreshMet=this._fromTimeout=!1,!this.get("lock")&&this.get(i)&&this.fire(v,{pageX:this.lastXY[0],pageY:this.lastXY[1],startTime:this._startTime,endTime:this._endTime}),this.get(r).removeClass(n.CSS_PREFIX+"-dragging"),this.set(i,!1),this.deltaXY=[0,0],this},_defEndFn:function(){this._fixIEMouseUp(),this._ev_md=null},_prevEndFn:function(){this._fixIEMouseUp(),this.get(s).setXY(this.nodeXY),this._ev_md=null,this.region=null},_align:function(e){this.fire(g,{pageX:e[0],pageY:e[1]})},_defAlignFn:function(e){this.actXY=[e.pageX-this.deltaXY[0],e.pageY-this.deltaXY[1]]},_alignNode:function(e,t){this._align(e),t||this._moveNode()},_moveNode:function(e){var t=[],n=[],r=this.nodeXY,i=this.actXY;t[0]=i[0]-this.lastXY[0],t[1]=i[1]-this.lastXY[1],n[0]=i[0]-this.nodeXY[0],n[1]=i[1]-this.nodeXY[1],this.region={0:i[0],1:i[1],area:0,top:i[1],right:i[0]+this.get(s).get(u),bottom:i[1]+this.get(s).get(o),left:i[0]},this.fire(m,{pageX:i[0],pageY:i[1],scroll:e,info:{start:r,xy:i,delta:t,offset:n}}),this.lastXY=i},_defDragFn:function(t){if(this.get("move")){if(t.scroll&&t.scroll.node){var n=t.scroll.node.getDOMNode();n===e.config.win?n.scrollTo(t.scroll.left,t.scroll.top):(t.scroll.node.set("scrollTop",t.scroll.top),t.scroll.node.set("scrollLeft",t.scroll.left))}this.get(s).setXY([t.pageX,t.pageY]),this.realXY=[t.pageX,t.pageY]}},_move:function(e){if(this.get("lock"))return!1;this.mouseXY=[e.pageX,e.pageY];if(!this._dragThreshMet){var t=Math.abs(this.startXY[0]-e.pageX),n=Math.abs(this.startXY[1]-e.pageY);if(t>this.get("clickPixelThresh")||n>this.get("clickPixelThresh"))this._dragThreshMet=!0,this.start(),e&&e.preventDefault&&e.preventDefault(),this._alignNode([e.pageX,e.pageY])}else this._clickTimeout&&this._clickTimeout.cancel(),this._alignNode([e.pageX,e.pageY])},stopDrag:function(){return this.get(i)&&n._end(),this},destructor:function(){this._unprep(),this.target&&this.target.destroy(),n._unregDrag(this)}}),e.namespace("DD"),e.DD.Drag=y},"3.17.2",{requires:["dd-ddm-base"]});
diff --git a/js/yui3/dd-drop-plugin/dd-drop-plugin-min.js b/js/yui3/dd-drop-plugin/dd-drop-plugin-min.js
new file mode 100644
index 000000000..be9d5fbd8
--- /dev/null
+++ b/js/yui3/dd-drop-plugin/dd-drop-plugin-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dd-drop-plugin",function(e,t){var n=function(e){e.node=e.host,n.superclass.constructor.apply(this,arguments)};n.NAME="dd-drop-plugin",n.NS="drop",e.extend(n,e.DD.Drop),e.namespace("Plugin"),e.Plugin.Drop=n},"3.17.2",{requires:["dd-drop"]});
diff --git a/js/yui3/dd-drop/dd-drop-min.js b/js/yui3/dd-drop/dd-drop-min.js
new file mode 100644
index 000000000..c21675307
--- /dev/null
+++ b/js/yui3/dd-drop/dd-drop-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dd-drop",function(e,t){var n="node",r=e.DD.DDM,i="offsetHeight",s="offsetWidth",o="drop:over",u="drop:enter",a="drop:exit",f=function(){this._lazyAddAttrs=!1,f.superclass.constructor.apply(this,arguments),e.on("domready",e.bind(function(){e.later(100,this,this._createShim)},this)),r._regTarget(this)};f.NAME="drop",f.ATTRS={node:{setter:function(t){var n=e.one(t);return n||e.error("DD.Drop: Invalid Node Given: "+t),n}},groups:{value:["default"],getter:function(){return this._groups?e.Object.keys(this._groups):(this._groups={},[])},setter:function(t){return this._groups=e.Array.hash(t),t}},padding:{value:"0",setter:function(e){return r.cssSizestoObject(e)}},lock:{value:!1,setter:function(e){return e?this.get(n).addClass(r.CSS_PREFIX+"-drop-locked"):this.get(n).removeClass(r.CSS_PREFIX+"-drop-locked"),e}},bubbles:{setter:function(e){return this.addTarget(e),e}},useShim:{value:!0,setter:function(t){return e.DD.DDM._noShim=!t,t}}},e.extend(f,e.Base,{_bubbleTargets:e.DD.DDM,addToGroup:function(e){return this._groups[e]=!0,this},removeFromGroup:function(e){return delete this._groups[e],this},_createEvents:function(){var t=[o,u,a,"drop:hit"];e.Array.each(t,function(e){this.publish(e,{type:e,emitFacade:!0,preventable:!1,bubbles:!0,queuable:!1,prefix:"drop"})},this)},_valid:null,_groups:null,shim:null,region:null,overTarget:null,inGroup:function(t){this._valid=!1;var n=!1;return e.Array.each(t,function(e){this._groups[e]&&(n=!0,this._valid=!0)},this),n},initializer:function(){e.later(100,this,this._createEvents);var t=this.get(n),i;t.get("id")||(i=e.stamp(t),t.set("id",i)),t.addClass(r.CSS_PREFIX+"-drop"),this.set("groups",this.get("groups"))},destructor:function(){r._unregTarget(this),this.shim&&this.shim!==this.get(n)&&(this.shim.detachAll(),this.shim.remove(),this.shim=null),this.get(n).removeClass(r.CSS_PREFIX+"-drop"),this.detachAll()},_deactivateShim:function(){if(!this.shim)return!1;this.get(n).removeClass(r.CSS_PREFIX+"-drop-active-valid"),this.get(n).removeClass(r.CSS_PREFIX+"-drop-active-invalid"),this.get(n).removeClass(r.CSS_PREFIX+"-drop-over"),this.get("useShim")&&this.shim.setStyles({top:"-999px",left:"-999px",zIndex:"1"}),this.overTarget=!1},_activateShim:function(){if(!r.activeDrag)return!1;if(this.get(n)===r.activeDrag.get(n))return!1;if(this.get("lock"))return!1;var e=this.get(n);this.inGroup(r.activeDrag.get("groups"))?(e.removeClass(r.CSS_PREFIX+"-drop-active-invalid"),e.addClass(r.CSS_PREFIX+"-drop-active-valid"),r._addValid(this),this.overTarget=!1,this.get("useShim")||(this.shim=this.get(n)),this.sizeShim()):(r._removeValid(this),e.removeClass(r.CSS_PREFIX+"-drop-active-valid"),e.addClass(r.CSS_PREFIX+"-drop-active-invalid"))},sizeShim:function(){if(!r.activeDrag)return!1;if(this.get(n)===r.activeDrag.get(n))return!1;if(this.get("lock"))return!1;if(!this.shim)return e.later(100,this,this.sizeShim),!1;var t=this.get(n),o=t.get(i),u=t.get(s),a=t.getXY(),f=this.get("padding"),l,c,h;u=u+f.left+f.right,o=o+f.top+f.bottom,a[0]=a[0]-f.left,a[1]=a[1]-f.top,r.activeDrag.get("dragMode")===r.INTERSECT&&(l=r.activeDrag,c=l.get(n).get(i),h=l.get(n).get(s),o+=c,u+=h,a[0]=a[0]-(h-l.deltaXY[0]),a[1]=a[1]-(c-l.deltaXY[1])),this.get("useShim")&&this.shim.setStyles({height:o+"px",width:u+"px",top:a[1]+"px",left:a[0]+"px"}),this.region={0:a[0],1:a[1],area:0,top:a[1],right:a[0]+u,bottom:a[1]+o,left:a[0]}},_createShim:function(){if(!r._pg){e.later(10,this,this._createShim);return}if(this.shim)return;var t=this.get("node");this.get("useShim")&&(t=e.Node.create('<div id="'+this.get(n).get("id")+'_shim"></div>'),t.setStyles({height:this.get(n).get(i)+"px",width:this.get(n).get(s)+"px",backgroundColor:"yellow",opacity:".5",zIndex:"1",overflow:"hidden",top:"-900px",left:"-900px",position:"absolute"}),r._pg.appendChild(t),t.on("mouseover",e.bind(this._handleOverEvent,this)),t.on("mouseout",e.bind(this._handleOutEvent,this))),this.shim=t},_handleTargetOver:function(){r.isOverTarget(this)?(this.get(n).addClass(r.CSS_PREFIX+"-drop-over"),r.activeDrop=this,r.otherDrops[this]=this,this.overTarget?(r.activeDrag.fire("drag:over",{drop:this,drag:r.activeDrag}),this.fire(o,{drop:this,drag:r.activeDrag})):r.activeDrag.get("dragging")&&(this.overTarget=!0,this.fire(u,{drop:this,drag:r.activeDrag}),r.activeDrag.fire("drag:enter",{drop:this,drag:r.activeDrag}),r.activeDrag.get(n).addClass(r.CSS_PREFIX+"-drag-over"))):this._handleOut()},_handleOverEvent:function(){this.shim.setStyle("zIndex","999"),r._addActiveShim(this)},_handleOutEvent:function(){this.shim.setStyle("zIndex","1"),r._removeActiveShim(this)},_handleOut:function(e){(!r.isOverTarget(this)||e)&&this.overTarget&&(this.overTarget=!1,e||r._removeActiveShim(this),r.activeDrag&&(this.get(n).removeClass(r.CSS_PREFIX+"-drop-over"),r.activeDrag.get(n).removeClass(r.CSS_PREFIX+"-drag-over"),this.fire(a,{drop:this,drag:r.activeDrag}),r.activeDrag.fire("drag:exit",{drop:this,drag:r.activeDrag}),delete r.otherDrops[this]))}}),e.DD.Drop=f},"3.17.2",{requires:["dd-drag","dd-ddm-drop"]});
diff --git a/js/yui3/dd-gestures/dd-gestures-min.js b/js/yui3/dd-gestures/dd-gestures-min.js
new file mode 100644
index 000000000..3dd13170d
--- /dev/null
+++ b/js/yui3/dd-gestures/dd-gestures-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dd-gestures",function(e,t){e.DD.Drag.START_EVENT="gesturemovestart",e.DD.Drag.prototype._prep=function(){this._dragThreshMet=!1;var t=this.get("node"),n=e.DD.DDM;t.addClass(n.CSS_PREFIX+"-draggable"),t.on(e.DD.Drag.START_EVENT,e.bind(this._handleMouseDownEvent,this),{minDistance:this.get("clickPixelThresh"),minTime:this.get("clickTimeThresh")}),t.on("gesturemoveend",e.bind(this._handleMouseUp,this),{standAlone:!0}),t.on("dragstart",e.bind(this._fixDragStart,this))};var n=e.DD.Drag.prototype._unprep;e.DD.Drag.prototype._unprep=function(){var e=this.get("node");n.call(this),e.detachAll("gesturemoveend")},e.DD.DDM._setupListeners=function(){var t=e.DD.DDM;this._createPG(),this._active=!0,e.one(e.config.doc).on("gesturemove",e.throttle(e.bind(t._move,t),t.get("throttleTime")),{standAlone:!0})}},"3.17.2",{requires:["dd-drag","event-synthetic","event-gestures"]});
diff --git a/js/yui3/dd-plugin/dd-plugin-min.js b/js/yui3/dd-plugin/dd-plugin-min.js
new file mode 100644
index 000000000..1c176abab
--- /dev/null
+++ b/js/yui3/dd-plugin/dd-plugin-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dd-plugin",function(e,t){var n=function(t){e.Widget&&t.host instanceof e.Widget?(t.node=t.host.get("boundingBox"),t.widget=t.host):(t.node=t.host,t.widget=!1),n.superclass.constructor.call(this,t)},r="drag:start",i="drag:drag",s="drag:end";n.NAME="dd-plugin",n.NS="dd",e.extend(n,e.DD.Drag,{_widgetHandles:null,_widget:undefined,_stoppedPosition:undefined,_usesWidgetPosition:function(t){var n=!1;return t&&(n=t.hasImpl&&t.hasImpl(e.WidgetPosition)?!0:!1),n},_checkEvents:function(){this._widget&&(this.proxy?this._widgetHandles.length>0&&this._removeWidgetListeners():this._widgetHandles.length===0&&this._attachWidgetListeners())},_removeWidgetListeners:function(){e.Array.each(this._widgetHandles,function(e){e.detach()}),this._widgetHandles=[]},_attachWidgetListeners:function(){this._usesWidgetPosition(this._widget)&&(this._widgetHandles.push(this.on(i,this._setWidgetCoords)),this._widgetHandles.push(this.on(s,this._updateStopPosition)))},initializer:function(e){this._widgetHandles=[],this._widget=e.widget,this.on(r,this._checkEvents),this._attachWidgetListeners()},_setWidgetCoords:function(e){var t=this._stoppedPosition||e.target.nodeXY,n=e.target.realXY,r=[n[0]-t[0],n[1]-t[1]];r[0]!==0&&r[1]!==0?this._widget.set("xy",n):r[0]===0?this._widget.set("y",n[1]):r[1]===0&&this._widget.set("x",n[0])},_updateStopPosition:function(e){this._stoppedPosition=e.target.realXY}}),e.namespace("Plugin"),e.Plugin.Drag=n},"3.17.2",{optional:["dd-constrain","dd-proxy"],requires:["dd-drag"]});
diff --git a/js/yui3/dd-proxy/dd-proxy-min.js b/js/yui3/dd-proxy/dd-proxy-min.js
new file mode 100644
index 000000000..10dae2ca6
--- /dev/null
+++ b/js/yui3/dd-proxy/dd-proxy-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dd-proxy",function(e,t){var n=e.DD.DDM,r="node",i="dragNode",s="host",o=!0,u,a=function(){a.superclass.constructor.apply(this,arguments)};a.NAME="DDProxy",a.NS="proxy",a.ATTRS={host:{},moveOnEnd:{value:o},hideOnEnd:{value:o},resizeFrame:{value:o},positionProxy:{value:o},borderStyle:{value:"1px solid #808080"},cloneNode:{value:!1}},u={_hands:null,_init:function(){if(!n._proxy){n._createFrame(),e.on("domready",e.bind(this._init,this));return}this._hands||(this._hands=[]);var t,o,u=this.get(s),a=u.get(i);a.compareTo(u.get(r))&&n._proxy&&u.set(i,n._proxy),e.Array.each(this._hands,function(e){e.detach()}),t=n.on("ddm:start",e.bind(function(){n.activeDrag===u&&n._setFrame(u)},this)),o=n.on("ddm:end",e.bind(function(){u.get("dragging")&&(this.get("moveOnEnd")&&u.get(r).setXY(u.lastXY),this.get("hideOnEnd")&&u.get(i).setStyle("display","none"),this.get("cloneNode")&&(u.get(i).remove(),u.set(i,n._proxy)))},this)),this._hands=[t,o]},initializer:function(){this._init()},destructor:function(){var t=this.get(s);e.Array.each(this._hands,function(e){e.detach()}),t.set(i,t.get(r))},clone:function(){var t=this.get(s),n=t.get(r),o=n.cloneNode(!0);return o.all('input[type="radio"]').removeAttribute("name"),delete o._yuid,o.setAttribute("id",e.guid()),o.setStyle("position","absolute"),n.get("parentNode").appendChild(o),t.set(i,o),o}},e.namespace("Plugin"),e.extend(a,e.Base,u),e.Plugin.DDProxy=a,e.mix(n,{_createFrame:function(){if(!n._proxy){n._proxy=o;var t=e.Node.create("<div></div>"),r=e.one("body");t.setStyles({position:"absolute",display:"none",zIndex:"999",top:"-999px",left:"-999px"}),r.prepend(t),t.set("id",e.guid()),t.addClass(n.CSS_PREFIX+"-proxy"),n._proxy=t}},_setFrame:function(e){var t=e.get(r),s=e.get(i),o,u="auto";o=n.activeDrag.get("activeHandle"),o&&(u=o.getStyle("cursor")),u==="auto"&&(u=n.get("dragCursor")),s.setStyles({visibility:"hidden",display:"block",cursor:u,border:e.proxy.get("borderStyle")}),e.proxy.get("cloneNode")&&(s=e.proxy.clone()),e.proxy.get("resizeFrame")&&s.setStyles({height:t.get("offsetHeight")+"px",width:t.get("offsetWidth")+"px"}),e.proxy.get("positionProxy")&&s.setXY(e.nodeXY),s.setStyle("visibility","visible")}})},"3.17.2",{requires:["dd-drag"]});
diff --git a/js/yui3/dd-scroll/dd-scroll-min.js b/js/yui3/dd-scroll/dd-scroll-min.js
new file mode 100644
index 000000000..c325319db
--- /dev/null
+++ b/js/yui3/dd-scroll/dd-scroll-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dd-scroll",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)},r,i,s="host",o="buffer",u="parentScroll",a="windowScroll",f="scrollTop",l="scrollLeft",c="offsetWidth",h="offsetHeight";n.ATTRS={parentScroll:{value:!1,setter:function(e){return e?e:!1}},buffer:{value:30,validator:e.Lang.isNumber},scrollDelay:{value:235,validator:e.Lang.isNumber},host:{value:null},windowScroll:{value:!1,validator:e.Lang.isBoolean},vertical:{value:!0,validator:e.Lang.isBoolean},horizontal:{value:!0,validator:e.Lang.isBoolean}},e.extend(n,e.Base,{_scrolling:null,_vpRegionCache:null,_dimCache:null,_scrollTimer:null,_getVPRegion:function(){var e={},t=this.get(u),n=this.get(o),r=this.get(a),i=r?[]:t.getXY(),s=r?"winWidth":c,p=r?"winHeight":h,d=r?t.get(f):i[1],v=r?t.get(l):i[0];return e={top:d+n,right:t.get(s)+v-n,bottom:t.get(p)+d-n,left:v+n},this._vpRegionCache=e,e},initializer:function(){var t=this.get(s);t.after("drag:start",e.bind(this.start,this)),t.after("drag:end",e.bind(this.end,this)),t.on("drag:align",e.bind(this.align,this)),e.one("win").on("scroll",e.bind(function(){this._vpRegionCache=null},this))},_checkWinScroll:function(e){var t=this._getVPRegion(),n=this.get(s),r=this.get(a),i=n.lastXY,c=!1,h=this.get(o),p=this.get(u),d=p.get(f),v=p.get(l),m=this._dimCache.w,g=this._dimCache.h,y=i[1]+g,b=i[1],w=i[0]+m,E=i[0],S=b,x=E,T=d,N=v;this.get("horizontal")&&(E<=t.left&&(c=!0,x=i[0]-(r?h:0),N=v-h),w>=t.right&&(c=!0,x=i[0]+(r?h:0),N=v+h)),this.get("vertical")&&(y>=t.bottom&&(c=!0,S=i[1]+(r?h:0),T=d+h),b<=t.top&&(c=!0,S=i[1]-(r?h:0),T=d-h)),T<0&&(T=0,S=i[1]),N<0&&(N=0,x=i[0]),S<0&&(S=i[1]),x<0&&(x=i[0]),e?(n.actXY=[x,S],n._alignNode([x,S],!0),i=n.actXY,n.actXY=[x,S],n._moveNode({node:p,top:T,left:N}),!T&&!N&&this._cancelScroll()):c?this._initScroll():this._cancelScroll()},_initScroll:function(){this._cancelScroll(),this._scrollTimer=e.Lang.later(this.get("scrollDelay"),this,this._checkWinScroll,[!0],!0)},_cancelScroll:function(){this._scrolling=!1,this._scrollTimer&&(this._scrollTimer.cancel(),delete this._scrollTimer)},align:function(e){this._scrolling&&(this._cancelScroll(),e.preventDefault()),this._scrolling||this._checkWinScroll()},_setDimCache:function(){var e=this.get(s).get("dragNode");this._dimCache={h:e.get(h),w:e.get(c)}},start:function(){this._setDimCache()},end:function(){this._dimCache=null,this._cancelScroll()}}),e.namespace("Plugin"),r=function(){r.superclass.constructor.apply(this,arguments)},r.ATTRS=e.merge(n.ATTRS,{windowScroll:{value:!0,setter:function(t){return t&&this.set(u,e.one("win")),t}}}),e.extend(r,n,{initializer:function(){this.set("windowScroll",this.get("windowScroll"))}}),r.NAME=r.NS="winscroll",e.Plugin.DDWinScroll=r,i=function(){i.superclass.constructor.apply(this,arguments)},i.ATTRS=e.merge(n.ATTRS,{node:{value:!1,setter:function(t){var n=e.one(t);return n?this.set(u,n):t!==!1&&e.error("DDNodeScroll: Invalid Node Given: "+t),n}}}),e.extend(i,n,{initializer:function(){this.set("node",this.get("node"))}}),i.NAME=i.NS="nodescroll",e.Plugin.DDNodeScroll=i,e.DD.Scroll=n},"3.17.2",{requires:["dd-drag"]});
diff --git a/js/yui3/dial/assets/dial-core.css b/js/yui3/dial/assets/dial-core.css
new file mode 100644
index 000000000..7a50a2755
--- /dev/null
+++ b/js/yui3/dial/assets/dial-core.css
@@ -0,0 +1,49 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+v\:oval,
+v\:shadow,
+v\:fill {
+ behavior: url(#default#VML);
+ display: inline-block;
+ zoom: 1; *display: inline; /* IE < 8: fake inline-block */
+}
+.yui3-dial{
+ position:relative;
+ display:-moz-inline-stack;
+ display:inline-block;
+ zoom:1;
+ *display:inline;
+ /*text-align:center; This causes problems with the angle calc with longer labels*/
+}
+.yui3-dial-content,
+.yui3-dial-ring{
+ position:relative;
+}
+.yui3-dial-handle,
+.yui3-dial-marker,
+.yui3-dial-center-button,
+.yui3-dial-reset-string,
+.yui3-dial-handle-vml,
+.yui3-dial-marker-vml,
+.yui3-dial-center-button-vml,
+.yui3-dial-ring-vml v\:oval,
+.yui3-dial-center-button-vml v\:oval
+{
+ position:absolute;
+}
+.yui3-dial-center-button-vml v\:oval {
+ font-size:1px;
+ top:0;
+ left:0;
+}
+.yui3-dial-content .yui3-dial-ring .yui3-dial-hidden v\:oval,
+.yui3-dial-content .yui3-dial-ring .yui3-dial-hidden {
+ /* [#2530206] using opacity instead of display:none;. display:none was mis-positioning the marker when we set the dial value on ring mousedown. */
+ opacity:0;
+ filter:alpha(opacity=0);
+}
diff --git a/js/yui3/dial/assets/skins/night/dial.css b/js/yui3/dial/assets/skins/night/dial.css
new file mode 100644
index 000000000..dfc5ce22f
--- /dev/null
+++ b/js/yui3/dial/assets/skins/night/dial.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+v\:oval,v\:shadow,v\:fill{behavior:url(#default#VML);display:inline-block;zoom:1;*display:inline}.yui3-dial{position:relative;display:-moz-inline-stack;display:inline-block;zoom:1;*display:inline}.yui3-dial-content,.yui3-dial-ring{position:relative}.yui3-dial-handle,.yui3-dial-marker,.yui3-dial-center-button,.yui3-dial-reset-string,.yui3-dial-handle-vml,.yui3-dial-marker-vml,.yui3-dial-center-button-vml,.yui3-dial-ring-vml v\:oval,.yui3-dial-center-button-vml v\:oval{position:absolute}.yui3-dial-center-button-vml v\:oval{font-size:1px;top:0;left:0}.yui3-dial-content .yui3-dial-ring .yui3-dial-hidden v\:oval,.yui3-dial-content .yui3-dial-ring .yui3-dial-hidden{opacity:0;filter:alpha(opacity=0)}.yui3-skin-night .yui3-dial{color:#fff}.yui3-skin-night .yui3-dial-handle{background:#439ede;opacity:.3;-moz-box-shadow:1px 1px 1px rgba(0,0,0,0.9) inset;-webkit-box-shadow:1px 1px 1px rgba(0,0,0,0.9) inset;box-shadow:1px 1px 1px rgba(0,0,0,0.9) inset;cursor:pointer;font-size:1px}.yui3-skin-night .yui3-dial-ring{background:#595b5b;background:-moz-linear-gradient(0% 100% 315deg,#5e6060,#2d2e2f);background:-webkit-gradient(linear,50% 0,100% 100%,from(#636666),to(#424344));-moz-box-shadow:1px 1px 2px rgba(0,0,0,0.7) inset;-webkit-box-shadow:1px 1px 3px rgba(0,0,0,0.7) inset;box-shadow:1px 1px 5px rgba(0,0,0,0.4) inset}.yui3-skin-night .yui3-dial-center-button{-moz-box-shadow:-1px -1px 2px rgba(0,0,0,0.3) inset,1px 1px 2px rgba(0,0,0,0.5);-webkit-box-shadow:-1px -1px 2px rgba(0,0,0,0.3) inset,1px 1px 2px rgba(0,0,0,0.5);box-shadow:-1px -1px 2px rgba(0,0,0,0.3) inset,1px 1px 2px rgba(0,0,0,0.5);background:#dddbd4;background:-moz-radial-gradient(30% 30% 0deg,circle farthest-side,#999c9c 24%,#898989 41%,#535555 87%) repeat scroll 0 0 transparent;background:-webkit-gradient(radial,15 15,15,30 30,40,from(#999c9c),to(#535555),color-stop(.2,#898989));cursor:pointer;opacity:.7}.yui3-skin-night .yui3-dial-reset-string{color:#fff;font-size:72%;text-decoration:none}.yui3-skin-night .yui3-dial-label{color:#cbcbcb;margin-bottom:.8em}.yui3-skin-night .yui3-dial-value-string{margin-left:.5em;color:#dcdcdc;font-size:130%}.yui3-skin-night .yui3-dial-value{visibility:hidden;position:absolute;top:0;left:102%;width:4em}.yui3-skin-night .yui3-dial-north-mark{position:absolute;border-left:2px solid #434343;height:5px;left:50%;top:-7px;font-size:1px}.yui3-skin-night .yui3-dial-marker{background-color:#a0d8ff;opacity:.2;font-size:1px}.yui3-skin-night .yui3-dial-marker-max-min{background-color:#ff0404;opacity:.6}.yui3-skin-night .yui3-dial-ring-vml,.yui3-skin-night .yui3-dial-center-button-vml,.yui3-skin-night .yui3-dial-marker v\:oval.yui3-dial-marker-max-min,.yui3-skin-night v\:oval.yui3-dial-marker-max-min,.yui3-skin-night .yui3-dial-marker-vml,.yui3-skin-night .yui3-dial-handle-vml{background:0;opacity:1}#yui3-css-stamp.skin-night-dial{display:none}
diff --git a/js/yui3/dial/assets/skins/sam/dial.css b/js/yui3/dial/assets/skins/sam/dial.css
new file mode 100644
index 000000000..86b3da09c
--- /dev/null
+++ b/js/yui3/dial/assets/skins/sam/dial.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+v\:oval,v\:shadow,v\:fill{behavior:url(#default#VML);display:inline-block;zoom:1;*display:inline}.yui3-dial{position:relative;display:-moz-inline-stack;display:inline-block;zoom:1;*display:inline}.yui3-dial-content,.yui3-dial-ring{position:relative}.yui3-dial-handle,.yui3-dial-marker,.yui3-dial-center-button,.yui3-dial-reset-string,.yui3-dial-handle-vml,.yui3-dial-marker-vml,.yui3-dial-center-button-vml,.yui3-dial-ring-vml v\:oval,.yui3-dial-center-button-vml v\:oval{position:absolute}.yui3-dial-center-button-vml v\:oval{font-size:1px;top:0;left:0}.yui3-dial-content .yui3-dial-ring .yui3-dial-hidden v\:oval,.yui3-dial-content .yui3-dial-ring .yui3-dial-hidden{opacity:0;filter:alpha(opacity=0)}.yui3-skin-sam .yui3-dial-handle{background:#6c3a3a;opacity:.3;-moz-box-shadow:1px 1px 1px rgba(0,0,0,0.9) inset;cursor:pointer;font-size:1px}.yui3-skin-sam .yui3-dial-ring{background:#bebdb7;background:-moz-linear-gradient(100% 100% 135deg,#7b7a6d,#fff);background:-webkit-gradient(linear,left top,right bottom,from(#fff),to(#7b7a6d));box-shadow:1px 1px 5px rgba(0,0,0,0.4) inset;-webkit-box-shadow:1px 1px 5px rgba(0,0,0,0.4) inset;-moz-box-shadow:1px 1px 5px rgba(0,0,0,0.4) inset}.yui3-skin-sam .yui3-dial-center-button{box-shadow:-1px -1px 2px rgba(0,0,0,0.3) inset,1px 1px 2px rgba(0,0,0,0.5);-moz-box-shadow:-1px -1px 2px rgba(0,0,0,0.3) inset,1px 1px 2px rgba(0,0,0,0.5);background:#dddbd4;background:-moz-radial-gradient(30% 30% 0deg,circle farthest-side,#fbfbf9 24%,#f2f0ea 41%,#d3d0c3 83%) repeat scroll 0 0 transparent;background:-webkit-gradient(radial,15 15,15,30 30,40,from(#fbfbf9),to(#d3d0c3),color-stop(.2,#f2f0ea));cursor:pointer;opacity:.7}.yui3-skin-sam .yui3-dial-reset-string{color:#676767;font-size:85%;text-decoration:underline}.yui3-skin-sam .yui3-dial-label{color:#808080;margin-bottom:.8em}.yui3-skin-sam .yui3-dial-value-string{margin-left:.5em;color:#000;font-size:130%}.yui3-skin-sam .yui3-dial-value{visibility:hidden;position:absolute;top:0;left:102%;width:4em}.yui3-skin-sam .yui3-dial-north-mark{position:absolute;border-left:2px solid #ccc;height:5px;width:10px;left:50%;top:-7px;font-size:1px}.yui3-skin-sam .yui3-dial-marker{background-color:#000;opacity:.2;font-size:1px}.yui3-skin-sam .yui3-dial-marker-max-min{background-color:#ab3232;opacity:.6}.yui3-skin-sam .yui3-dial-ring-vml,.yui3-skin-sam .yui3-dial-center-button-vml,.yui3-skin-sam .yui3-dial-marker v\:oval.yui3-dial-marker-max-min,.yui3-skin-sam v\:oval.yui3-dial-marker-max-min,.yui3-skin-sam .yui3-dial-marker-vml,.yui3-skin-sam .yui3-dial-handle-vml{background:0;opacity:1}#yui3-css-stamp.skin-sam-dial{display:none}
diff --git a/js/yui3/dial/dial-min.js b/js/yui3/dial/dial-min.js
new file mode 100644
index 000000000..dba99b061
--- /dev/null
+++ b/js/yui3/dial/dial-min.js
@@ -0,0 +1,10 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dial",function(e,t){function o(e){o.superclass.constructor.apply(this,arguments)}function u(t){return e.ClassNameManager.getClassName(o.NAME,t)}var n=!1;e.UA.ie&&e.UA.ie<9&&(n=!0);var r=e.Lang,i=e.Widget,s=e.Node;o.NAME="dial",o.ATTRS={min:{value:-220},max:{value:220},diameter:{value:100},handleDiameter:{value:.2},markerDiameter:{value:.1},centerButtonDiameter:{value:.5},value:{value:0,validator:function(e){return this._validateValue(e)}},minorStep:{value:1},majorStep:{value:10},stepsPerRevolution:{value:100},decimalPlaces:{value:0},strings:{valueFn:function(){return e.Intl.get("dial")}},handleDistance:{value:.75}},o.CSS_CLASSES={label:u("label"),labelString:u("label-string"),valueString:u("value-string"),northMark:u("north-mark"),ring:u("ring"),ringVml:u("ring-vml"),marker:u("marker"),markerVml:u("marker-vml"),markerMaxMin:u("marker-max-min"),centerButton:u("center-button"),centerButtonVml:u("center-button-vml"),resetString:u("reset-string"),handle:u("handle"),handleVml:u("handle-vml"),hidden:u("hidden"),dragging:e.ClassNameManager.getClassName("dd-dragging")},o.LABEL_TEMPLATE='<div class="'+o.CSS_CLASSES.label+'"><span id="" class="'+o.CSS_CLASSES.labelString+'">{label}</span><span class="'+o.CSS_CLASSES.valueString+'"></span></div>',n===!1?(o.RING_TEMPLATE='<div class="'+o.CSS_CLASSES.ring+'"><div class="'+o.CSS_CLASSES.northMark+'"></div></div>',o.MARKER_TEMPLATE='<div class="'+o.CSS_CLASSES.marker+" "+o.CSS_CLASSES.hidden+'"></div>',o.CENTER_BUTTON_TEMPLATE='<div class="'+o.CSS_CLASSES.centerButton+'"><div class="'+o.CSS_CLASSES.resetString+" "+o.CSS_CLASSES.hidden+'">{resetStr}</div></div>',o.HANDLE_TEMPLATE='<div class="'+o.CSS_CLASSES.handle+'" aria-labelledby="" aria-valuetext="" aria-valuemax="" aria-valuemin="" aria-valuenow="" role="slider" tabindex="0" title="{tooltipHandle}">'):(o.RING_TEMPLATE='<div class="'+o.CSS_CLASSES.ring+" "+o.CSS_CLASSES.ringVml+'">'+'<div class="'+o.CSS_CLASSES.northMark+'"></div>'+'<v:oval strokecolor="#ceccc0" strokeweight="1px"><v:fill type=gradient color="#8B8A7F" color2="#EDEDEB" angle="45"/></v:oval>'+"</div>"+"",o.MARKER_TEMPLATE='<div class="'+o.CSS_CLASSES.markerVml+" "+o.CSS_CLASSES.hidden+'">'+'<v:oval stroked="false">'+'<v:fill opacity="20%" color="#000"/>'+"</v:oval>"+"</div>"+"",o.CENTER_BUTTON_TEMPLATE='<div class="'+o.CSS_CLASSES.centerButton+" "+o.CSS_CLASSES.centerButtonVml+'">'+'<v:oval strokecolor="#ceccc0" strokeweight="1px">'+'<v:fill type=gradient color="#C7C5B9" color2="#fefcf6" colors="35% #d9d7cb, 65% #fefcf6" angle="45"/>'+'<v:shadow on="True" color="#000" opacity="10%" offset="2px, 2px"/>'+"</v:oval>"+'<div class="'+o.CSS_CLASSES.resetString+" "+o.CSS_CLASSES.hidden+'">{resetStr}</div>'+"</div>"+"",o.HANDLE_TEMPLATE='<div class="'+o.CSS_CLASSES.handleVml+'" aria-labelledby="" aria-valuetext="" aria-valuemax="" aria-valuemin="" aria-valuenow="" role="slider" tabindex="0" title="{tooltipHandle}">'+'<v:oval stroked="false">'+'<v:fill opacity="20%" color="#6C3A3A"/>'+"</v:oval>"+"</div>"+""),e.extend(o,i,{renderUI:function(){this._renderLabel(),this._renderRing(),this._renderMarker(),this._renderCenterButton(),this._renderHandle(),this.contentBox=this.get("contentBox"),this._originalValue=this.get("value"),this._minValue=this.get("min"),this._maxValue=this.get("max"),this._stepsPerRevolution=this.get("stepsPerRevolution"),this._minTimesWrapped=Math.floor(this._minValue/this._stepsPerRevolution-1),this._maxTimesWrapped=Math.floor(this._maxValue/this._stepsPerRevolution+1),this._timesWrapped=0,this._angle=this._getAngleFromValue(this.get("value")),this._prevAng=this._angle,this._setTimesWrappedFromValue(this._originalValue),this._handleNode.set("aria-valuemin",this._minValue),this._handleNode.set("aria-valuemax",this._maxValue)},_setBorderRadius:function(){this._ringNode.setStyles({WebkitBorderRadius:this._ringNodeRadius+"px",MozBorderRadius:this._ringNodeRadius+"px",borderRadius:this._ringNodeRadius+"px"}),this._handleNode.setStyles({WebkitBorderRadius:this._handleNodeRadius+"px",MozBorderRadius:this._handleNodeRadius+"px",borderRadius:this._handleNodeRadius+"px"}),this._markerNode.setStyles({WebkitBorderRadius:this._markerNodeRadius+"px",MozBorderRadius:this._markerNodeRadius+"px",borderRadius:this._markerNodeRadius+"px"}),this._centerButtonNode.setStyles({WebkitBorderRadius:this._centerButtonNodeRadius+"px",MozBorderRadius:this._centerButtonNodeRadius+"px",borderRadius:this._centerButtonNodeRadius+"px"})},_handleCenterButtonEnter:function(){this._resetString.removeClass(o.CSS_CLASSES.hidden)},_handleCenterButtonLeave:function(){this._resetString.addClass(o.CSS_CLASSES.hidden)},bindUI:function(){this.after("valueChange",this._afterValueChange);var t=this.get("boundingBox"),n=e.UA.opera?"press:":"down:",r=n+"38,40,33,34,35,36",i=n+"37,39",s=n+"37+meta,39+meta",o=e.DD.Drag;e.on("key",e.bind(this._onDirectionKey,this),t,r),e.on("key",e.bind(this._onLeftRightKey,this),t,i),t.on("key",this._onLeftRightKeyMeta,s,this),e.on("mouseenter",e.bind(this._handleCenterButtonEnter,this),this._centerButtonNode),e.on("mouseleave",e.bind(this._handleCenterButtonLeave,this),this._centerButtonNode),e.on("gesturemovestart",e.bind(this._resetDial,this),this._centerButtonNode),e.on("gesturemoveend",e.bind(this._handleCenterButtonMouseup,this),this._centerButtonNode),e.on(o.START_EVENT,e.bind(this._handleHandleMousedown,this),this._handleNode),e.on(o.START_EVENT,e.bind(this._handleMousedown,this),this._ringNode),e.on("gesturemoveend",e.bind(this._handleRingMouseup,this),this._ringNode),this._dd1=new o({node:this._handleNode,on:{"drag:drag":e.bind(this._handleDrag,this),"drag:start":e.bind(this._handleDragStart,this),"drag:end":e.bind(this._handleDragEnd,this)}}),e.bind(this._dd1.addHandle(this._ringNode),this)},_setTimesWrappedFromValue:function(e){e%this._stepsPerRevolution===0?this._timesWrapped=e/this._stepsPerRevolution:this._timesWrapped=Math.floor(e/this._stepsPerRevolution)},_getAngleFromHandleCenter:function(e,t){var n=Math.atan((
+this._dialCenterY-t)/(this._dialCenterX-e))*(180/Math.PI);return n=this._dialCenterX-e<0?n+90:n+90+180,n},_calculateDialCenter:function(){this._dialCenterX=this._ringNode.get("offsetWidth")/2,this._dialCenterY=this._ringNode.get("offsetHeight")/2},_handleRingMouseup:function(){this._handleNode.focus()},_handleCenterButtonMouseup:function(){this._handleNode.focus()},_handleHandleMousedown:function(){this._handleNode.focus()},_handleDrag:function(e){var t,n,r,i;t=parseInt(this._handleNode.getStyle("left"),10)+this._handleNodeRadius,n=parseInt(this._handleNode.getStyle("top"),10)+this._handleNodeRadius,r=this._getAngleFromHandleCenter(t,n),this._prevAng>270&&r<90?this._timesWrapped<this._maxTimesWrapped&&(this._timesWrapped=this._timesWrapped+1):this._prevAng<90&&r>270&&this._timesWrapped>this._minTimesWrapped&&(this._timesWrapped=this._timesWrapped-1),i=this._getValueFromAngle(r),i>this._maxValue+this._stepsPerRevolution?this._timesWrapped--:i<this._minValue-this._stepsPerRevolution&&this._timesWrapped++,this._prevAng=r,this._handleValuesBeyondMinMax(e,i)},_handleMousedown:function(t){if(this._ringNode.compareTo(t.target)){var n=this._getAngleFromValue(this._minValue),r=this._getAngleFromValue(this._maxValue),i,s,o,u,a;e.UA.ios?(o=t.clientX-this._ringNode.getX(),u=t.clientY-this._ringNode.getY()):(o=t.clientX+e.one("document").get("scrollLeft")-this._ringNode.getX(),u=t.clientY+e.one("document").get("scrollTop")-this._ringNode.getY()),a=this._getAngleFromHandleCenter(o,u);if(this._maxValue-this._minValue>this._stepsPerRevolution)Math.abs(this._prevAng-a)>180?this._timesWrapped>this._minTimesWrapped&&this._timesWrapped<this._maxTimesWrapped&&(this._timesWrapped=this._prevAng-a>0?this._timesWrapped+1:this._timesWrapped-1):this._timesWrapped===this._minTimesWrapped&&a-this._prevAng<180&&this._timesWrapped++;else if(this._maxValue-this._minValue===this._stepsPerRevolution)a<n?this._timesWrapped=1:this._timesWrapped=0;else if(n>r)this._prevAng>=n&&a<=(n+r)/2?this._timesWrapped++:this._prevAng<=r&&a>(n+r)/2&&this._timesWrapped--;else if(a<n||a>r){s=((n+r)/2+180)%360,s>180?i=r<a&&a<s?this.get("max"):this.get("min"):i=n>a&&a>s?this.get("min"):this.get("max"),this._prevAng=this._getAngleFromValue(i),this.set("value",i),this._setTimesWrappedFromValue(i);return}i=this._getValueFromAngle(a),i>this._maxValue?this._prevAng=this._getAngleFromValue(this._maxValue):i<this._minValue?this._prevAng=this._getAngleFromValue(this._minValue):this._prevAng=a,this._handleValuesBeyondMinMax(t,i)}},_handleValuesBeyondMinMax:function(e,t){t>=this._minValue&&t<=this._maxValue?(this.set("value",t),e.currentTarget===this._ringNode&&this._dd1._handleMouseDownEvent(e)):t>this._maxValue?this.set("value",this._maxValue):t<this._minValue&&this.set("value",this._minValue)},_handleDragStart:function(e){this._markerNode.removeClass(o.CSS_CLASSES.hidden)},_handleDragEnd:function(){var t=this._handleNode;t.transition({duration:.08,easing:"ease-in",left:this._setNodeToFixedRadius(this._handleNode,!0)[0]+"px",top:this._setNodeToFixedRadius(this._handleNode,!0)[1]+"px"},e.bind(function(){var e=this.get("value");e>this._minValue&&e<this._maxValue?this._markerNode.addClass(o.CSS_CLASSES.hidden):(this._setTimesWrappedFromValue(e),this._prevAng=this._getAngleFromValue(e))},this))},_setNodeToFixedRadius:function(e,t){var n=this._angle-90,r=Math.PI/180,i=Math.round(Math.sin(n*r)*this._handleDistance),s=Math.round(Math.cos(n*r)*this._handleDistance),o=e.get("offsetWidth");i-=o*.5,s-=o*.5;if(t)return[this._ringNodeRadius+s,this._ringNodeRadius+i];e.setStyle("left",this._ringNodeRadius+s+"px"),e.setStyle("top",this._ringNodeRadius+i+"px")},syncUI:function(){this._setSizes(),this._calculateDialCenter(),this._setBorderRadius(),this._uiSetValue(this.get("value")),this._markerNode.addClass(o.CSS_CLASSES.hidden),this._resetString.addClass(o.CSS_CLASSES.hidden)},_setSizes:function(){var e=this.get("diameter"),t,n,r,i=function(e,t,n){var r="px";e.getElementsByTagName("oval").setStyle("width",t*n+r),e.getElementsByTagName("oval").setStyle("height",t*n+r),e.setStyle("width",t*n+r),e.setStyle("height",t*n+r)};i(this._ringNode,e,1),i(this._handleNode,e,this.get("handleDiameter")),i(this._markerNode,e,this.get("markerDiameter")),i(this._centerButtonNode,e,this.get("centerButtonDiameter")),this._ringNodeRadius=this._ringNode.get("offsetWidth")*.5,this._handleNodeRadius=this._handleNode.get("offsetWidth")*.5,this._markerNodeRadius=this._markerNode.get("offsetWidth")*.5,this._centerButtonNodeRadius=this._centerButtonNode.get("offsetWidth")*.5,this._handleDistance=this._ringNodeRadius*this.get("handleDistance"),t=this._ringNodeRadius-this._centerButtonNodeRadius,this._centerButtonNode.setStyle("left",t+"px"),this._centerButtonNode.setStyle("top",t+"px"),n=this._centerButtonNodeRadius-this._resetString.get("offsetWidth")*.5,r=this._centerButtonNodeRadius-this._resetString.get("offsetHeight")*.5,this._resetString.setStyles({left:n+"px",top:r+"px"})},_renderLabel:function(){var t=this.get("contentBox"),n=t.one("."+o.CSS_CLASSES.label);n||(n=s.create(e.Lang.sub(o.LABEL_TEMPLATE,this.get("strings"))),t.append(n)),this._labelNode=n,this._valueStringNode=this._labelNode.one("."+o.CSS_CLASSES.valueString)},_renderRing:function(){var e=this.get("contentBox"),t=e.one("."+o.CSS_CLASSES.ring);t||(t=e.appendChild(o.RING_TEMPLATE),t.setStyles({width:this.get("diameter")+"px",height:this.get("diameter")+"px"})),this._ringNode=t},_renderMarker:function(){var e=this.get("contentBox"),t=e.one("."+o.CSS_CLASSES.marker);t||(t=e.one("."+o.CSS_CLASSES.ring).appendChild(o.MARKER_TEMPLATE)),this._markerNode=t},_renderCenterButton:function(){var t=this.get("contentBox"),n=t.one("."+o.CSS_CLASSES.centerButton);n||(n=s.create(e.Lang.sub(o.CENTER_BUTTON_TEMPLATE,this.get("strings"))),t.one("."+o.CSS_CLASSES.ring).append(n)),this._centerButtonNode=n,this._resetString=this._centerButtonNode.one("."+o.CSS_CLASSES.resetString)},_renderHandle:function(){var t=o.CSS_CLASSES.label+e.guid(),n=this
+.get("contentBox"),r=n.one("."+o.CSS_CLASSES.handle);r||(r=s.create(e.Lang.sub(o.HANDLE_TEMPLATE,this.get("strings"))),r.setAttribute("aria-labelledby",t),this._labelNode.one("."+o.CSS_CLASSES.labelString).setAttribute("id",t),n.one("."+o.CSS_CLASSES.ring).append(r)),this._handleNode=r},_setLabelString:function(e){this.get("contentBox").one("."+o.CSS_CLASSES.labelString).setHTML(e)},_setResetString:function(e){this.get("contentBox").one("."+o.CSS_CLASSES.resetString).setHTML(e)},_setTooltipString:function(e){this._handleNode.set("title",e)},_onDirectionKey:function(e){e.preventDefault();switch(e.charCode){case 38:this._incrMinor();break;case 40:this._decrMinor();break;case 36:this._setToMin();break;case 35:this._setToMax();break;case 33:this._incrMajor();break;case 34:this._decrMajor()}},_onLeftRightKey:function(e){e.preventDefault();switch(e.charCode){case 37:this._decrMinor();break;case 39:this._incrMinor()}},_onLeftRightKeyMeta:function(e){e.preventDefault();switch(e.charCode){case 37:this._setToMin();break;case 39:this._setToMax()}},_incrMinor:function(){var e=this.get("value")+this.get("minorStep");e=Math.min(e,this.get("max")),this.set("value",e.toFixed(this.get("decimalPlaces"))-0)},_decrMinor:function(){var e=this.get("value")-this.get("minorStep");e=Math.max(e,this.get("min")),this.set("value",e.toFixed(this.get("decimalPlaces"))-0)},_incrMajor:function(){var e=this.get("value")+this.get("majorStep");e=Math.min(e,this.get("max")),this.set("value",e.toFixed(this.get("decimalPlaces"))-0)},_decrMajor:function(){var e=this.get("value")-this.get("majorStep");e=Math.max(e,this.get("min")),this.set("value",e.toFixed(this.get("decimalPlaces"))-0)},_setToMax:function(){this.set("value",this.get("max"))},_setToMin:function(){this.set("value",this.get("min"))},_resetDial:function(e){e&&e.stopPropagation(),this.set("value",this._originalValue),this._resetString.addClass(o.CSS_CLASSES.hidden),this._handleNode.focus()},_getAngleFromValue:function(e){var t=e%this._stepsPerRevolution,n=t/this._stepsPerRevolution*360;return n<0?n+360:n},_getValueFromAngle:function(e){e<0?e=360+e:e===0&&(e=360);var t=e/360*this._stepsPerRevolution;return t+=this._timesWrapped*this._stepsPerRevolution,t.toFixed(this.get("decimalPlaces"))-0},_afterValueChange:function(e){this._uiSetValue(e.newVal)},_valueToDecimalPlaces:function(e){},_uiSetValue:function(e){this._angle=this._getAngleFromValue(e),this._handleNode.hasClass(o.CSS_CLASSES.dragging)===!1&&(this._setTimesWrappedFromValue(e),this._setNodeToFixedRadius(this._handleNode,!1),this._prevAng=this._getAngleFromValue(this.get("value"))),this._valueStringNode.setHTML(e.toFixed(this.get("decimalPlaces"))),this._handleNode.set("aria-valuenow",e),this._handleNode.set("aria-valuetext",e),this._setNodeToFixedRadius(this._markerNode,!1),e===this._maxValue||e===this._minValue?(this._markerNode.addClass(o.CSS_CLASSES.markerMaxMin),n===!0&&this._markerNode.getElementsByTagName("fill").set("color","#AB3232"),this._markerNode.removeClass(o.CSS_CLASSES.hidden)):(n===!0&&this._markerNode.getElementsByTagName("fill").set("color","#000"),this._markerNode.removeClass(o.CSS_CLASSES.markerMaxMin),this._handleNode.hasClass(o.CSS_CLASSES.dragging)===!1&&this._markerNode.addClass(o.CSS_CLASSES.hidden))},_validateValue:function(e){var t=this.get("min"),n=this.get("max");return r.isNumber(e)&&e>=t&&e<=n}}),e.Dial=o},"3.17.2",{requires:["widget","dd-drag","event-mouseenter","event-move","event-key","transition","intl"],lang:["en","es","hu"],skinnable:!0});
diff --git a/js/yui3/dial/lang/dial.js b/js/yui3/dial/lang/dial.js
new file mode 100644
index 000000000..7e3498b4a
--- /dev/null
+++ b/js/yui3/dial/lang/dial.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/dial",function(e){e.Intl.add("dial","",{label:"My label",resetStr:"Reset",tooltipHandle:"Drag to set value"})},"3.17.2");
diff --git a/js/yui3/dial/lang/dial_en.js b/js/yui3/dial/lang/dial_en.js
new file mode 100644
index 000000000..3a407dc99
--- /dev/null
+++ b/js/yui3/dial/lang/dial_en.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/dial_en",function(e){e.Intl.add("dial","en",{label:"My label",resetStr:"Reset",tooltipHandle:"Drag to set value"})},"3.17.2");
diff --git a/js/yui3/dial/lang/dial_es.js b/js/yui3/dial/lang/dial_es.js
new file mode 100644
index 000000000..e3f113c45
--- /dev/null
+++ b/js/yui3/dial/lang/dial_es.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/dial_es",function(e){e.Intl.add("dial","es",{label:"Mi etiqueta",resetStr:"Resetear",tooltipHandle:"Arrastre para ajustar el valor"})},"3.17.2");
diff --git a/js/yui3/dial/lang/dial_hu.js b/js/yui3/dial/lang/dial_hu.js
new file mode 100644
index 000000000..ad49d7e55
--- /dev/null
+++ b/js/yui3/dial/lang/dial_hu.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lang/dial_hu",function(e){e.Intl.add("dial","hu",{label:"Saj\u00e1t c\u00edmke",resetStr:"\u00dajrakezd",tooltipHandle:"H\u00e1zza az \u00e9rt\u00e9k be\u00e1ll\u00edt\u00e1s\u00e1hoz"})},"3.17.2");
diff --git a/js/yui3/dom-base/dom-base-min.js b/js/yui3/dom-base/dom-base-min.js
new file mode 100644
index 000000000..d06d4da48
--- /dev/null
+++ b/js/yui3/dom-base/dom-base-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dom-base",function(e,t){var n=e.config.doc.documentElement,r=e.DOM,i="tagName",s="ownerDocument",o="",u=e.Features.add,a=e.Features.test;e.mix(r,{getText:n.textContent!==undefined?function(e){var t="";return e&&(t=e.textContent),t||""}:function(e){var t="";return e&&(t=e.innerText||e.nodeValue),t||""},setText:n.textContent!==undefined?function(e,t){e&&(e.textContent=t)}:function(e,t){"innerText"in e?e.innerText=t:"nodeValue"in e&&(e.nodeValue=t)},CUSTOM_ATTRIBUTES:n.hasAttribute?{htmlFor:"for",className:"class"}:{"for":"htmlFor","class":"className"},setAttribute:function(e,t,n,i){e&&t&&e.setAttribute&&(t=r.CUSTOM_ATTRIBUTES[t]||t,e.setAttribute(t,n,i))},getAttribute:function(e,t,n){n=n!==undefined?n:2;var i="";return e&&t&&e.getAttribute&&(t=r.CUSTOM_ATTRIBUTES[t]||t,i=e.tagName==="BUTTON"&&t==="value"?r.getValue(e):e.getAttribute(t,n),i===null&&(i="")),i},VALUE_SETTERS:{},VALUE_GETTERS:{},getValue:function(e){var t="",n;return e&&e[i]&&(n=r.VALUE_GETTERS[e[i].toLowerCase()],n?t=n(e):t=e.value),t===o&&(t=o),typeof t=="string"?t:""},setValue:function(e,t){var n;e&&e[i]&&(n=r.VALUE_SETTERS[e[i].toLowerCase()],t=t===null?"":t,n?n(e,t):e.value=t)},creators:{}}),u("value-set","select",{test:function(){var t=e.config.doc.createElement("select");return t.innerHTML="<option>1</option><option>2</option>",t.value="2",t.value&&t.value==="2"}}),a("value-set","select")||(r.VALUE_SETTERS.select=function(e,t){for(var n=0,i=e.getElementsByTagName("option"),s;s=i[n++];)if(r.getValue(s)===t){s.selected=!0;break}}),e.mix(r.VALUE_GETTERS,{button:function(e){return e.attributes&&e.attributes.value?e.attributes.value.value:""}}),e.mix(r.VALUE_SETTERS,{button:function(e,t){var n=e.attributes.value;n||(n=e[s].createAttribute("value"),e.setAttributeNode(n)),n.value=t}}),e.mix(r.VALUE_GETTERS,{option:function(e){var t=e.attributes;return t.value&&t.value.specified?e.value:e.text},select:function(e){var t=e.value,n=e.options;return n&&n.length&&(e.multiple||e.selectedIndex>-1&&(t=r.getValue(n[e.selectedIndex]))),t}});var f,l,c;e.mix(e.DOM,{hasClass:function(t,n){var r=e.DOM._getRegExp("(?:^|\\s+)"+n+"(?:\\s+|$)");return r.test(t.className)},addClass:function(t,n){e.DOM.hasClass(t,n)||(t.className=e.Lang.trim([t.className,n].join(" ")))},removeClass:function(t,n){n&&l(t,n)&&(t.className=e.Lang.trim(t.className.replace(e.DOM._getRegExp("(?:^|\\s+)"+n+"(?:\\s+|$)")," ")),l(t,n)&&c(t,n))},replaceClass:function(e,t,n){c(e,t),f(e,n)},toggleClass:function(e,t,n){var r=n!==undefined?n:!l(e,t);r?f(e,t):c(e,t)}}),l=e.DOM.hasClass,c=e.DOM.removeClass,f=e.DOM.addClass;var h=/<([a-z]+)/i,r=e.DOM,u=e.Features.add,a=e.Features.test,p={},d=function(t,n){var r=e.config.doc.createElement("div"),i=!0;r.innerHTML=t;if(!r.firstChild||r.firstChild.tagName!==n.toUpperCase())i=!1;return i},v=/(?:\/(?:thead|tfoot|tbody|caption|col|colgroup)>)+\s*<tbody/,m="<table>",g="</table>",y;e.mix(e.DOM,{_fragClones:{},_create:function(e,t,n){n=n||"div";var i=r._fragClones[n];return i?i=i.cloneNode(!1):i=r._fragClones[n]=t.createElement(n),i.innerHTML=e,i},_children:function(e,t){var n=0,r=e.children,i,s,o;r&&r.tags&&(t?r=e.children.tags(t):s=r.tags("!").length);if(!r||!r.tags&&t||s){i=r||e.childNodes,r=[];while(o=i[n++])o.nodeType===1&&(!t||t===o.tagName)&&r.push(o)}return r||[]},create:function(t,n){typeof t=="string"&&(t=e.Lang.trim(t)),n=n||e.config.doc;var i=h.exec(t),s=r._create,o=p,u=null,a,f,l,c;return t!=undefined&&(i&&i[1]&&(a=o[i[1].toLowerCase()],typeof a=="function"?s=a:f=a),l=s(t,n,f),c=l.childNodes,c.length===1?u=l.removeChild(c[0]):c[0]&&c[0].className==="yui3-big-dummy"?(y=l.selectedIndex,c.length===2?u=c[0].nextSibling:(l.removeChild(c[0]),u=r._nl2frag(c,n))):u=r._nl2frag(c,n)),u},_nl2frag:function(t,n){var r=null,i,s;if(t&&(t.push||t.item)&&t[0]){n=n||t[0].ownerDocument,r=n.createDocumentFragment(),t.item&&(t=e.Array(t,0,!0));for(i=0,s=t.length;i<s;i++)r.appendChild(t[i])}return r},addHTML:function(t,n,i){var s=t.parentNode,o=0,u,a=n,f;if(n!=undefined)if(n.nodeType)f=n;else if(typeof n=="string"||typeof n=="number")a=f=r.create(n);else if(n[0]&&n[0].nodeType){f=e.config.doc.createDocumentFragment();while(u=n[o++])f.appendChild(u)}if(i)if(f&&i.parentNode)i.parentNode.insertBefore(f,i);else switch(i){case"replace":while(t.firstChild)t.removeChild(t.firstChild);f&&t.appendChild(f);break;case"before":f&&s.insertBefore(f,t);break;case"after":f&&(t.nextSibling?s.insertBefore(f,t.nextSibling):s.appendChild(f));break;default:f&&t.appendChild(f)}else f&&t.appendChild(f);return t.nodeName=="SELECT"&&y>0&&(t.selectedIndex=y-1),a},wrap:function(t,n){var r=n&&n.nodeType?n:e.DOM.create(n),i=r.getElementsByTagName("*");i.length&&(r=i[i.length-1]),t.parentNode&&t.parentNode.replaceChild(r,t),r.appendChild(t)},unwrap:function(e){var t=e.parentNode,n=t.lastChild,r=e,i;if(t){i=t.parentNode;if(i){e=t.firstChild;while(e!==n)r=e.nextSibling,i.insertBefore(e,t),e=r;i.replaceChild(n,t)}else t.removeChild(e)}}}),u("innerhtml","table",{test:function(){var t=e.config.doc.createElement("table");try{t.innerHTML="<tbody></tbody>"}catch(n){return!1}return t.firstChild&&t.firstChild.nodeName==="TBODY"}}),u("innerhtml-div","tr",{test:function(){return d("<tr></tr>","tr")}}),u("innerhtml-div","script",{test:function(){return d("<script></script>","script")}}),a("innerhtml","table")||(p.tbody=function(t,n){var i=r.create(m+t+g,n),s=e.DOM._children(i,"tbody")[0];return i.children.length>1&&s&&!v.test(t)&&s.parentNode.removeChild(s),i}),a("innerhtml-div","script")||(p.script=function(e,t){var n=t.createElement("div");return n.innerHTML="-"+e,n.removeChild(n.firstChild),n},p.link=p.style=p.script),a("innerhtml-div","tr")||(e.mix(p,{option:function(e,t){return r.create('<select><option class="yui3-big-dummy" selected></option>'+e+"</select>",t)},tr:function(e,t){return r.create("<tbody>"+e+"</tbody>",t)},td:function(e,t){return r.create("<tr>"+e+"</tr>",t)},col:function(e,t){return r.create("<colgroup>"+e+"</colgroup>",t)},tbody:"table"}),e.mix(p,{legend:"fieldset"
+,th:p.td,thead:p.tbody,tfoot:p.tbody,caption:p.tbody,colgroup:p.tbody,optgroup:p.option})),r.creators=p,e.mix(e.DOM,{setWidth:function(t,n){e.DOM._setSize(t,"width",n)},setHeight:function(t,n){e.DOM._setSize(t,"height",n)},_setSize:function(e,t,n){n=n>0?n:0;var r=0;e.style[t]=n+"px",r=t==="height"?e.offsetHeight:e.offsetWidth,r>n&&(n-=r-n,n<0&&(n=0),e.style[t]=n+"px")}})},"3.17.2",{requires:["dom-core"]});
diff --git a/js/yui3/dom-core/dom-core-min.js b/js/yui3/dom-core/dom-core-min.js
new file mode 100644
index 000000000..492c59231
--- /dev/null
+++ b/js/yui3/dom-core/dom-core-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dom-core",function(e,t){var n="nodeType",r="ownerDocument",i="documentElement",s="defaultView",o="parentWindow",u="tagName",a="parentNode",f="previousSibling",l="nextSibling",c="contains",h="compareDocumentPosition",p=[],d=function(){var t=e.config.doc.createElement("div"),n=t.appendChild(e.config.doc.createTextNode("")),r=!1;try{r=t.contains(n)}catch(i){}return r}(),v={byId:function(e,t){return v.allById(e,t)[0]||null},getId:function(e){var t;return e.id&&!e.id.tagName&&!e.id.item?t=e.id:e.attributes&&e.attributes.id&&(t=e.attributes.id.value),t},setId:function(e,t){e.setAttribute?e.setAttribute("id",t):e.id=t},ancestor:function(e,t,n,r){var i=null;return n&&(i=!t||t(e)?e:null),i||v.elementByAxis(e,a,t,null,r)},ancestors:function(e,t,n,r){var i=e,s=[];while(i=v.ancestor(i,t,n,r)){n=!1;if(i){s.unshift(i);if(r&&r(i))return s}}return s},elementByAxis:function(e,t,n,r,i){while(e&&(e=e[t])){if((r||e[u])&&(!n||n(e)))return e;if(i&&i(e))return null}return null},contains:function(e,t){var r=!1;if(!t||!e||!t[n]||!e[n])r=!1;else if(e[c]&&(t[n]===1||d))r=e[c](t);else if(e[h]){if(e===t||!!(e[h](t)&16))r=!0}else r=v._bruteContains(e,t);return r},inDoc:function(e,t){var n=!1,s;return e&&e.nodeType&&(t||(t=e[r]),s=t[i],s&&s.contains&&e.tagName?n=s.contains(e):n=v.contains(s,e)),n},allById:function(t,n){n=n||e.config.doc;var r=[],i=[],s,o;if(n.querySelectorAll)i=n.querySelectorAll('[id="'+t+'"]');else if(n.all){r=n.all(t);if(r){r.nodeName&&(r.id===t?(i.push(r),r=p):r=[r]);if(r.length)for(s=0;o=r[s++];)(o.id===t||o.attributes&&o.attributes.id&&o.attributes.id.value===t)&&i.push(o)}}else i=[v._getDoc(n).getElementById(t)];return i},isWindow:function(e){return!!(e&&e.scrollTo&&e.document)},_removeChildNodes:function(e){while(e.firstChild)e.removeChild(e.firstChild)},siblings:function(e,t){var n=[],r=e;while(r=r[f])r[u]&&(!t||t(r))&&n.unshift(r);r=e;while(r=r[l])r[u]&&(!t||t(r))&&n.push(r);return n},_bruteContains:function(e,t){while(t){if(e===t)return!0;t=t.parentNode}return!1},_getRegExp:function(e,t){return t=t||"",v._regexCache=v._regexCache||{},v._regexCache[e+t]||(v._regexCache[e+t]=new RegExp(e,t)),v._regexCache[e+t]},_getDoc:function(t){var i=e.config.doc;return t&&(i=t[n]===9?t:t[r]||t.document||e.config.doc),i},_getWin:function(t){var n=v._getDoc(t);return n[s]||n[o]||e.config.win},_batch:function(e,t,n,r,i,s){t=typeof t=="string"?v[t]:t;var o,u=0,a,f;if(t&&e)while(a=e[u++])o=o=t.call(v,a,n,r,i,s),typeof o!="undefined"&&(f||(f=[]),f.push(o));return typeof f!="undefined"?f:e},generateID:function(t){var n=t.id;return n||(n=e.stamp(t),t.id=n),n}};e.DOM=v},"3.17.2",{requires:["oop","features"]});
diff --git a/js/yui3/dom-screen/dom-screen-min.js b/js/yui3/dom-screen/dom-screen-min.js
new file mode 100644
index 000000000..518f64705
--- /dev/null
+++ b/js/yui3/dom-screen/dom-screen-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dom-screen",function(e,t){(function(e){var t="documentElement",n="compatMode",r="position",i="fixed",s="relative",o="left",u="top",a="BackCompat",f="medium",l="borderLeftWidth",c="borderTopWidth",h="getBoundingClientRect",p="getComputedStyle",d=e.DOM,v=/^t(?:able|d|h)$/i,m;e.UA.ie&&(e.config.doc[n]!=="BackCompat"?m=t:m="body"),e.mix(d,{winHeight:function(e){var t=d._getWinSize(e).height;return t},winWidth:function(e){var t=d._getWinSize(e).width;return t},docHeight:function(e){var t=d._getDocSize(e).height;return Math.max(t,d._getWinSize(e).height)},docWidth:function(e){var t=d._getDocSize(e).width;return Math.max(t,d._getWinSize(e).width)},docScrollX:function(n,r){r=r||n?d._getDoc(n):e.config.doc;var i=r.defaultView,s=i?i.pageXOffset:0;return Math.max(r[t].scrollLeft,r.body.scrollLeft,s)},docScrollY:function(n,r){r=r||n?d._getDoc(n):e.config.doc;var i=r.defaultView,s=i?i.pageYOffset:0;return Math.max(r[t].scrollTop,r.body.scrollTop,s)},getXY:function(){return e.config.doc[t][h]?function(r){var i=null,s,o,u,f,l,c,p,v,g,y;if(r&&r.tagName){p=r.ownerDocument,u=p[n],u!==a?y=p[t]:y=p.body,y.contains?g=y.contains(r):g=e.DOM.contains(y,r);if(g){v=p.defaultView,v&&"pageXOffset"in v?(s=v.pageXOffset,o=v.pageYOffset):(s=m?p[m].scrollLeft:d.docScrollX(r,p),o=m?p[m].scrollTop:d.docScrollY(r,p)),e.UA.ie&&(!p.documentMode||p.documentMode<8||u===a)&&(l=y.clientLeft,c=y.clientTop),f=r[h](),i=[f.left,f.top];if(l||c)i[0]-=l,i[1]-=c;if(o||s)if(!e.UA.ios||e.UA.ios>=4.2)i[0]+=s,i[1]+=o}else i=d._getOffset(r)}return i}:function(t){var n=null,s,o,u,a,f;if(t)if(d.inDoc(t)){n=[t.offsetLeft,t.offsetTop],s=t.ownerDocument,o=t,u=e.UA.gecko||e.UA.webkit>519?!0:!1;while(o=o.offsetParent)n[0]+=o.offsetLeft,n[1]+=o.offsetTop,u&&(n=d._calcBorders(o,n));if(d.getStyle(t,r)!=i){o=t;while(o=o.parentNode){a=o.scrollTop,f=o.scrollLeft,e.UA.gecko&&d.getStyle(o,"overflow")!=="visible"&&(n=d._calcBorders(o,n));if(a||f)n[0]-=f,n[1]-=a}n[0]+=d.docScrollX(t,s),n[1]+=d.docScrollY(t,s)}else n[0]+=d.docScrollX(t,s),n[1]+=d.docScrollY(t,s)}else n=d._getOffset(t);return n}}(),getScrollbarWidth:e.cached(function(){var t=e.config.doc,n=t.createElement("div"),r=t.getElementsByTagName("body")[0],i=.1;return r&&(n.style.cssText="position:absolute;visibility:hidden;overflow:scroll;width:20px;",n.appendChild(t.createElement("p")).style.height="1px",r.insertBefore(n,r.firstChild),i=n.offsetWidth-n.clientWidth,r.removeChild(n)),i},null,.1),getX:function(e){return d.getXY(e)[0]},getY:function(e){return d.getXY(e)[1]},setXY:function(e,t,n){var i=d.setStyle,a,f,l,c;e&&t&&(a=d.getStyle(e,r),f=d._getOffset(e),a=="static"&&(a=s,i(e,r,a)),c=d.getXY(e),t[0]!==null&&i(e,o,t[0]-c[0]+f[0]+"px"),t[1]!==null&&i(e,u,t[1]-c[1]+f[1]+"px"),n||(l=d.getXY(e),(l[0]!==t[0]||l[1]!==t[1])&&d.setXY(e,t,!0)))},setX:function(e,t){return d.setXY(e,[t,null])},setY:function(e,t){return d.setXY(e,[null,t])},swapXY:function(e,t){var n=d.getXY(e);d.setXY(e,d.getXY(t)),d.setXY(t,n)},_calcBorders:function(t,n){var r=parseInt(d[p](t,c),10)||0,i=parseInt(d[p](t,l),10)||0;return e.UA.gecko&&v.test(t.tagName)&&(r=0,i=0),n[0]+=i,n[1]+=r,n},_getWinSize:function(r,i){i=i||r?d._getDoc(r):e.config.doc;var s=i.defaultView||i.parentWindow,o=i[n],u=s.innerHeight,a=s.innerWidth,f=i[t];return o&&!e.UA.opera&&(o!="CSS1Compat"&&(f=i.body),u=f.clientHeight,a=f.clientWidth),{height:u,width:a}},_getDocSize:function(r){var i=r?d._getDoc(r):e.config.doc,s=i[t];return i[n]!="CSS1Compat"&&(s=i.body),{height:s.scrollHeight,width:s.scrollWidth}}})})(e),function(e){var t="top",n="right",r="bottom",i="left",s=function(e,s){var o=Math.max(e[t],s[t]),u=Math.min(e[n],s[n]),a=Math.min(e[r],s[r]),f=Math.max(e[i],s[i]),l={};return l[t]=o,l[n]=u,l[r]=a,l[i]=f,l},o=e.DOM;e.mix(o,{region:function(e){var t=o.getXY(e),n=!1;return e&&t&&(n=o._getRegion(t[1],t[0]+e.offsetWidth,t[1]+e.offsetHeight,t[0])),n},intersect:function(u,a,f){var l=f||o.region(u),c={},h=a,p;if(h.tagName)c=o.region(h);else{if(!e.Lang.isObject(a))return!1;c=a}return p=s(c,l),{top:p[t],right:p[n],bottom:p[r],left:p[i],area:(p[r]-p[t])*(p[n]-p[i]),yoff:p[r]-p[t],xoff:p[n]-p[i],inRegion:o.inRegion(u,a,!1,f)}},inRegion:function(u,a,f,l){var c={},h=l||o.region(u),p=a,d;if(p.tagName)c=o.region(p);else{if(!e.Lang.isObject(a))return!1;c=a}return f?h[i]>=c[i]&&h[n]<=c[n]&&h[t]>=c[t]&&h[r]<=c[r]:(d=s(c,h),d[r]>=d[t]&&d[n]>=d[i]?!0:!1)},inViewportRegion:function(e,t,n){return o.inRegion(e,o.viewportRegion(e),t,n)},_getRegion:function(e,s,o,u){var a={};return a[t]=a[1]=e,a[i]=a[0]=u,a[r]=o,a[n]=s,a.width=a[n]-a[i],a.height=a[r]-a[t],a},viewportRegion:function(t){t=t||e.config.doc.documentElement;var n=!1,r,i;return t&&(r=o.docScrollX(t),i=o.docScrollY(t),n=o._getRegion(i,o.winWidth(t)+r,i+o.winHeight(t),r)),n}})}(e)},"3.17.2",{requires:["dom-base","dom-style"]});
diff --git a/js/yui3/dom-style-ie/dom-style-ie-min.js b/js/yui3/dom-style-ie/dom-style-ie-min.js
new file mode 100644
index 000000000..4c2b8b16a
--- /dev/null
+++ b/js/yui3/dom-style-ie/dom-style-ie-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dom-style-ie",function(e,t){var n="hasLayout",r="px",i="filter",s="filters",o="opacity",u="auto",a="borderWidth",f="borderTopWidth",l="borderRightWidth",c="borderBottomWidth",h="borderLeftWidth",p="width",d="height",v="transparent",m="visible",g="getComputedStyle",y=e.config.doc.documentElement,b=e.Features.test,w=e.Features.add,E=/^(\d[.\d]*)+(em|ex|px|gd|rem|vw|vh|vm|ch|mm|cm|in|pt|pc|deg|rad|ms|s|hz|khz|%){1}?/i,S=e.UA.ie>=8,x=function(e){return e.currentStyle||e.style},T={CUSTOM_STYLES:{},get:function(t,n){var i="",s;return t&&(s=x(t)[n],n===o&&e.DOM.CUSTOM_STYLES[o]?i=e.DOM.CUSTOM_STYLES[o].get(t):!s||s.indexOf&&s.indexOf(r)>-1?i=s:e.DOM.IE.COMPUTED[n]?i=e.DOM.IE.COMPUTED[n](t,n):E.test(s)?i=T.getPixel(t,n)+r:i=s),i},sizeOffsets:{width:["Left","Right"],height:["Top","Bottom"],top:["Top"],bottom:["Bottom"]},getOffset:function(e,t){var n=x(e)[t],i=t.charAt(0).toUpperCase()+t.substr(1),s="pixel"+i,o=T.sizeOffsets[t],a=e.ownerDocument.compatMode,f="";return n===u||n.indexOf("%")>-1?(f=e["offset"+i],a!=="BackCompat"&&(o[0]&&(f-=T.getPixel(e,"padding"+o[0]),f-=T.getBorderWidth(e,"border"+o[0]+"Width",1)),o[1]&&(f-=T.getPixel(e,"padding"+o[1]),f-=T.getBorderWidth(e,"border"+o[1]+"Width",1)))):(!e.style[s]&&!e.style[t]&&(e.style[t]=n),f=e.style[s]),f+r},borderMap:{thin:S?"1px":"2px",medium:S?"3px":"4px",thick:S?"5px":"6px"},getBorderWidth:function(e,t,n){var i=e.currentStyle[t];return i.indexOf(r)<0&&(T.borderMap[i]&&e.currentStyle.borderStyle!=="none"?i=T.borderMap[i]:i=0),n?parseFloat(i):i},getPixel:function(e,t){var n=null,r=x(e),i=r.right,s=r[t];return e.style.right=s,n=e.style.pixelRight,e.style.right=i,n},getMargin:function(e,t){var n,i=x(e);return i[t]===u?n=0:n=T.getPixel(e,t),n+r},getVisibility:function(e,t){var n;while((n=e.currentStyle)&&n[t]==="inherit")e=e.parentNode;return n?n[t]:m},getColor:function(t,n){var r=x(t)[n];return(!r||r===v)&&e.DOM.elementByAxis(t,"parentNode",null,function(e){r=x(e)[n];if(r&&r!==v)return t=e,!0}),e.Color.toRGB(r)},getBorderColor:function(t,n){var r=x(t),i=r[n]||r.color;return e.Color.toRGB(e.Color.toHex(i))}},N={};w("style","computedStyle",{test:function(){return"getComputedStyle"in e.config.win}}),w("style","opacity",{test:function(){return"opacity"in y.style}}),w("style","filter",{test:function(){return"filters"in y}}),!b("style","opacity")&&b("style","filter")&&(e.DOM.CUSTOM_STYLES[o]={get:function(e){var t=100;try{t=e[s]["DXImageTransform.Microsoft.Alpha"][o]}catch(n){try{t=e[s]("alpha")[o]}catch(r){}}return t/100},set:function(e,t,r){var s,u=x(e),a=u[i];r=r||e.style,t===""&&(s=o in u?u[o]:1,t=s),typeof a=="string"&&(r[i]=a.replace(/alpha([^)]*\))/gi,"")+(t<=1?"alpha("+o+"="+t*100+")":""),r[i]||r.removeAttribute(i),u[n]||(r.zoom=1))}});try{e.config.doc.createElement("div").style.height="-1px"}catch(C){e.DOM.CUSTOM_STYLES.height={set:function(e,t,n){var r=parseFloat(t);if(r>=0||t==="auto"||t==="")n.height=t}},e.DOM.CUSTOM_STYLES.width={set:function(e,t,n){var r=parseFloat(t);if(r>=0||t==="auto"||t==="")n.width=t}}}b("style","computedStyle")||(N[p]=N[d]=T.getOffset,N.color=N.backgroundColor=T.getColor,N[a]=N[f]=N[l]=N[c]=N[h]=T.getBorderWidth,N.marginTop=N.marginRight=N.marginBottom=N.marginLeft=T.getMargin,N.visibility=T.getVisibility,N.borderColor=N.borderTopColor=N.borderRightColor=N.borderBottomColor=N.borderLeftColor=T.getBorderColor,e.DOM[g]=T.get,e.namespace("DOM.IE"),e.DOM.IE.COMPUTED=N,e.DOM.IE.ComputedStyle=T)},"3.17.2",{requires:["dom-style","color-base"]});
diff --git a/js/yui3/dom-style/dom-style-min.js b/js/yui3/dom-style/dom-style-min.js
new file mode 100644
index 000000000..027eac741
--- /dev/null
+++ b/js/yui3/dom-style/dom-style-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dom-style",function(e,t){var n="documentElement",r="defaultView",i="ownerDocument",s="style",o="float",u="cssFloat",a="styleFloat",f="transparent",l="getComputedStyle",c="getBoundingClientRect",h=e.config.doc,p=e.DOM,d,v,m=["WebkitTransform","MozTransform","OTransform","msTransform","transform"],g=/width|height|top|left|right|bottom|margin|padding/i;e.Array.each(m,function(e){e in h[n].style&&(d=e,v=e+"Origin")}),e.mix(p,{DEFAULT_UNIT:"px",CUSTOM_STYLES:{},setStyle:function(e,t,n,r){r=r||e.style;var i=p.CUSTOM_STYLES;if(r){n===null||n===""?n="":!isNaN(Number(n))&&g.test(t)&&(n+=p.DEFAULT_UNIT);if(t in i){if(i[t].set){i[t].set(e,n,r);return}typeof i[t]=="string"&&(t=i[t])}else t===""&&(t="cssText",n="");r[t]=n}},getStyle:function(e,t,n){n=n||e.style;var r=p.CUSTOM_STYLES,i="";if(n){if(t in r){if(r[t].get)return r[t].get(e,t,n);typeof r[t]=="string"&&(t=r[t])}i=n[t],i===""&&(i=p[l](e,t))}return i},setStyles:function(t,n){var r=t.style;e.each(n,function(e,n){p.setStyle(t,n,e,r)},p)},getComputedStyle:function(e,t){var n="",o=e[i],u;return e[s]&&o[r]&&o[r][l]&&(u=o[r][l](e,null),u&&(n=u[t])),n}}),h[n][s][u]!==undefined?p.CUSTOM_STYLES[o]=u:h[n][s][a]!==undefined&&(p.CUSTOM_STYLES[o]=a),e.UA.webkit&&(p[l]=function(e,t){var n=e[i][r],s=n[l](e,"")[t];return s==="rgba(0, 0, 0, 0)"&&(s=f),s}),e.DOM._getAttrOffset=function(t,n){var r=e.DOM[l](t,n),i=t.offsetParent,s,o,u;return r==="auto"&&(s=e.DOM.getStyle(t,"position"),s==="static"||s==="relative"?r=0:i&&i[c]&&(o=i[c]()[n],u=t[c]()[n],n==="left"||n==="top"?r=u-o:r=o-t[c]()[n])),r},e.DOM._getOffset=function(e){var t,n=null;return e&&(t=p.getStyle(e,"position"),n=[parseInt(p[l](e,"left"),10),parseInt(p[l](e,"top"),10)],isNaN(n[0])&&(n[0]=parseInt(p.getStyle(e,"left"),10),isNaN(n[0])&&(n[0]=t==="relative"?0:e.offsetLeft||0)),isNaN(n[1])&&(n[1]=parseInt(p.getStyle(e,"top"),10),isNaN(n[1])&&(n[1]=t==="relative"?0:e.offsetTop||0))),n},d&&(p.CUSTOM_STYLES.transform={set:function(e,t,n){n[d]=t},get:function(e){return p[l](e,d)}},p.CUSTOM_STYLES.transformOrigin={set:function(e,t,n){n[v]=t},get:function(e){return p[l](e,v)}})},"3.17.2",{requires:["dom-base"]});
diff --git a/js/yui3/dump/dump-min.js b/js/yui3/dump/dump-min.js
new file mode 100644
index 000000000..cd38d4075
--- /dev/null
+++ b/js/yui3/dump/dump-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("dump",function(e,t){var n=e.Lang,r="{...}",i="f(){...}",s=", ",o=" => ",u=function(e,t){var u,a,f=[],l=n.type(e);if(!n.isObject(e))return e+"";if(l=="date")return e;if(e.nodeType&&e.tagName)return e.tagName+"#"+e.id;if(e.document&&e.navigator)return"window";if(e.location&&e.body)return"document";if(l=="function")return i;t=n.isNumber(t)?t:3;if(l=="array"){f.push("[");for(u=0,a=e.length;u<a;u+=1)n.isObject(e[u])?f.push(t>0?n.dump(e[u],t-1):r):f.push(e[u]),f.push(s);f.length>1&&f.pop(),f.push("]")}else if(l=="regexp")f.push(e.toString());else{f.push("{");for(u in e)if(e.hasOwnProperty(u))try{f.push(u+o),n.isObject(e[u])?f.push(t>0?n.dump(e[u],t-1):r):f.push(e[u]),f.push(s)}catch(c){f.push("Error: "+c.message)}f.length>1&&f.pop(),f.push("}")}return f.join("")};e.dump=u,n.dump=u},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/editor-base/editor-base-min.js b/js/yui3/editor-base/editor-base-min.js
new file mode 100644
index 000000000..797003e07
--- /dev/null
+++ b/js/yui3/editor-base/editor-base-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("editor-base",function(e,t){var n=e.Lang,r=function(){r.superclass.constructor.apply(this,arguments)},i=":last-child";e.extend(r,e.Base,{frame:null,initializer:function(){this.publish("nodeChange",{emitFacade:!0,bubbles:!0,defaultFn:this._defNodeChangeFn})},destructor:function(){this.detachAll()},copyStyles:function(t,n){if(t.test("a"))return;var r=["color","fontSize","fontFamily","backgroundColor","fontStyle"],i={};e.each(r,function(e){i[e]=t.getStyle(e)}),t.ancestor("b,strong")&&(i.fontWeight="bold"),t.ancestor("u")&&(i.textDecoration||(i.textDecoration="underline")),n.setStyles(i)},_lastBookmark:null,_resolveChangedNode:function(e){var t=this.getInstance(),n,r,s,o=this._getRoot(),u;e&&e.compareTo(o)&&(u=new t.EditorSelection,u&&u.anchorNode&&(e=u.anchorNode));if(t&&e&&e.test("html")){n=o.one(i);while(!s)n?(r=n.one(i),r?n=r:s=!0):s=!0;n&&(n.test("br")&&(n.previous()?n=n.previous():n=n.get("parentNode")),n&&(e=n))}return e||(e=o),e},_getRoot:function(){return this.getInstance().EditorSelection.ROOT},_defNodeChangeFn:function(t){var n=(new Date).getTime(),i=this.getInstance(),s,o,u,a={},f,l,c=[],h="",p="",d,v=!1,m=this._getRoot();if(e.UA.ie&&e.UA.ie<11)try{s=i.config.doc.selection.createRange(),s.getBookmark&&(this._lastBookmark=s.getBookmark())}catch(g){}t.changedNode=this._resolveChangedNode(t.changedNode);switch(t.changedType){case"tab":!t.changedNode.test("li, li *")&&!t.changedEvent.shiftKey&&(t.changedEvent.frameEvent.preventDefault(),e.UA.webkit?this.execCommand("inserttext"," "):e.UA.gecko?this.frame.exec._command("inserthtml",r.TABKEY):e.UA.ie&&this.execCommand("inserthtml",r.TABKEY));break;case"backspace-up":e.UA.webkit&&t.changedNode&&t.changedNode.set("innerHTML",t.changedNode.get("innerHTML"))}e.UA.webkit&&t.commands&&(t.commands.indent||t.commands.outdent)&&(d=m.all(".webkit-indent-blockquote, blockquote"),d.size()&&d.setStyle("margin","")),o=this.getDomPath(t.changedNode,!1),t.commands&&(a=t.commands),e.each(o,function(t){var n=t.tagName.toLowerCase(),s=r.TAG2CMD[n],o,u,d,m,g;s&&(a[s]=1),o=t.currentStyle||t.style,""+o.fontWeight=="normal"&&(v=!0),""+o.fontWeight=="bold"&&(a.bold=1),e.UA.ie&&o.fontWeight>400&&(a.bold=1),o.fontStyle==="italic"&&(a.italic=1),o.textDecoration.indexOf("underline")>-1&&(a.underline=1),o.textDecoration.indexOf("line-through")>-1&&(a.strikethrough=1),u=i.one(t),u.getStyle("fontFamily")&&(d=u.getStyle("fontFamily").split(",")[0].toLowerCase(),d&&(f=d),f&&(f=f.replace(/'/g,"").replace(/"/g,""))),l=r.NORMALIZE_FONTSIZE(u),m=t.className.split(" "),e.each(m,function(e){e!==""&&e.substr(0,4)!=="yui_"&&c.push(e)}),h=r.FILTER_RGB(u.getStyle("color")),g=r.FILTER_RGB(o.backgroundColor),g!=="transparent"&&g!==""&&(p=g)}),v&&(delete a.bold,delete a.italic),t.dompath=i.all(o),t.classNames=c,t.commands=a,t.fontFamily||(t.fontFamily=f),t.fontSize||(t.fontSize=l),t.fontColor||(t.fontColor=h),t.backgroundColor||(t.backgroundColor=p),u=(new Date).getTime()},getDomPath:function(e,t){var n=[],r,i,s=this._getRoot(),o=this.frame.getInstance();r=o.Node.getDOMNode(e),i=o.Node.getDOMNode(s);while(r!==null){if(r===o.config.doc.documentElement||r===o.config.doc||!r.tagName){r=null;break}if(!o.DOM.inDoc(r)){r=null;break}r.nodeName&&r.nodeType&&r.nodeType===1&&n.push(r);if(r===i){r=null;break}r=r.parentNode}return n.length===0&&(n[0]=o.config.doc.body),t?o.all(n.reverse()):n.reverse()},_afterFrameReady:function(){var t=this.frame.getInstance();this.frame.on("dom:mouseup",e.bind(this._onFrameMouseUp,this)),this.frame.on("dom:mousedown",e.bind(this._onFrameMouseDown,this)),this.frame.on("dom:keydown",e.bind(this._onFrameKeyDown,this)),e.UA.ie&&e.UA.ie<11&&(this.frame.on("dom:activate",e.bind(this._onFrameActivate,this)),this.frame.on("dom:beforedeactivate",e.bind(this._beforeFrameDeactivate,this))),this.frame.on("dom:keyup",e.bind(this._onFrameKeyUp,this)),this.frame.on("dom:keypress",e.bind(this._onFrameKeyPress,this)),this.frame.on("dom:paste",e.bind(this._onPaste,this)),t.EditorSelection.filter(),this.fire("ready")},_beforeFrameDeactivate:function(e){if(e.frameTarget.test("html"))return;var t=this.getInstance(),n=t.config.doc.selection.createRange();n.compareEndPoints&&!n.compareEndPoints("StartToEnd",n)&&n.pasteHTML('<var id="yui-ie-cursor">')},_onFrameActivate:function(e){if(e.frameTarget.test("html"))return;var t=this.getInstance(),n=new t.EditorSelection,r=n.createRange(),i=this._getRoot(),s=i.all("#yui-ie-cursor");s.size()&&s.each(function(e){e.set("id","");if(r.moveToElementText)try{r.moveToElementText(e._node);var t=r.move("character",-1);t===-1&&r.move("character",1),r.select(),r.text=""}catch(n){}e.remove()})},_onPaste:function(e){this.fire("nodeChange",{changedNode:e.frameTarget,changedType:"paste",changedEvent:e.frameEvent})},_onFrameMouseUp:function(e){this.fire("nodeChange",{changedNode:e.frameTarget,changedType:"mouseup",changedEvent:e.frameEvent})},_onFrameMouseDown:function(e){this.fire("nodeChange",{changedNode:e.frameTarget,changedType:"mousedown",changedEvent:e.frameEvent})},_currentSelection:null,_currentSelectionTimer:null,_currentSelectionClear:null,_onFrameKeyDown:function(t){var n,i;this._currentSelection?i=this._currentSelection:(this._currentSelectionTimer&&this._currentSelectionTimer.cancel(),this._currentSelectionTimer=e.later(850,this,function(){this._currentSelectionClear=!0}),n=this.frame.getInstance(),i=new n.EditorSelection(t),this._currentSelection=i),n=this.frame.getInstance(),i=new n.EditorSelection,this._currentSelection=i,i&&i.anchorNode&&(this.fire("nodeChange",{changedNode:i.anchorNode,changedType:"keydown",changedEvent:t.frameEvent}),r.NC_KEYS[t.keyCode]&&(this.fire("nodeChange",{changedNode:i.anchorNode,changedType:r.NC_KEYS[t.keyCode],changedEvent:t.frameEvent}),this.fire("nodeChange",{changedNode:i.anchorNode,changedType:r.NC_KEYS[t.keyCode]+"-down",changedEvent:t.frameEvent})))},_onFrameKeyPress:function(e){var t=this._currentSelection;t&&t.anchorNode&&(this.fire("nodeChange",{changedNode:t.anchorNode,changedType:"keypress"
+,changedEvent:e.frameEvent}),r.NC_KEYS[e.keyCode]&&this.fire("nodeChange",{changedNode:t.anchorNode,changedType:r.NC_KEYS[e.keyCode]+"-press",changedEvent:e.frameEvent}))},_onFrameKeyUp:function(e){var t=this.frame.getInstance(),n=new t.EditorSelection(e);n&&n.anchorNode&&(this.fire("nodeChange",{changedNode:n.anchorNode,changedType:"keyup",selection:n,changedEvent:e.frameEvent}),r.NC_KEYS[e.keyCode]&&this.fire("nodeChange",{changedNode:n.anchorNode,changedType:r.NC_KEYS[e.keyCode]+"-up",selection:n,changedEvent:e.frameEvent})),this._currentSelectionClear&&(this._currentSelectionClear=this._currentSelection=null)},_validateLinkedCSS:function(e){return n.isString(e)||n.isArray(e)},execCommand:function(e,t){var n=this.frame.execCommand(e,t),r=this.frame.getInstance(),i=new r.EditorSelection,s={},o={changedNode:i.anchorNode,changedType:"execcommand",nodes:n};switch(e){case"forecolor":o.fontColor=t;break;case"backcolor":o.backgroundColor=t;break;case"fontsize":o.fontSize=t;break;case"fontname":o.fontFamily=t}return s[e]=1,o.commands=s,this.fire("nodeChange",o),n},getInstance:function(){return this.frame.getInstance()},render:function(t){var n=this.frame;return n||(this.plug(e.Plugin.Frame,{designMode:!0,title:r.STRINGS.title,use:r.USE,dir:this.get("dir"),extracss:this.get("extracss"),linkedcss:this.get("linkedcss"),defaultblock:this.get("defaultblock")}),n=this.frame),n.hasPlugin("exec")||n.plug(e.Plugin.ExecCommand),n.after("ready",e.bind(this._afterFrameReady,this)),n.addTarget(this),n.set("content",this.get("content")),n.render(t),this},focus:function(e){return this.frame.focus(e),this},show:function(){return this.frame.show(),this},hide:function(){return this.frame.hide(),this},getContent:function(){var e="",t=this.getInstance();return t&&t.EditorSelection&&(e=t.EditorSelection.unfilter()),e=e.replace(/ _yuid="([^>]*)"/g,""),e}},{NORMALIZE_FONTSIZE:function(e){var t=e.getStyle("fontSize"),n=t;switch(t){case"-webkit-xxx-large":t="48px";break;case"xx-large":t="32px";break;case"x-large":t="24px";break;case"large":t="18px";break;case"medium":t="16px";break;case"small":t="13px";break;case"x-small":t="10px"}return n!==t&&e.setStyle("fontSize",t),t},TABKEY:'<span class="tab">&nbsp;&nbsp;&nbsp;&nbsp;</span>',FILTER_RGB:function(e){if(e.toLowerCase().indexOf("rgb")!==-1){var t=new RegExp("(.*?)rgb\\s*?\\(\\s*?([0-9]+).*?,\\s*?([0-9]+).*?,\\s*?([0-9]+).*?\\)(.*?)","gi"),n=e.replace(t,"$1,$2,$3,$4,$5").split(","),r,i,s;n.length===5&&(r=parseInt(n[1],10).toString(16),i=parseInt(n[2],10).toString(16),s=parseInt(n[3],10).toString(16),r=r.length===1?"0"+r:r,i=i.length===1?"0"+i:i,s=s.length===1?"0"+s:s,e="#"+r+i+s)}return e},TAG2CMD:{b:"bold",strong:"bold",i:"italic",em:"italic",u:"underline",sup:"superscript",sub:"subscript",img:"insertimage",a:"createlink",ul:"insertunorderedlist",ol:"insertorderedlist"},NC_KEYS:{8:"backspace",9:"tab",13:"enter",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",46:"delete"},USE:["node","selector-css3","editor-selection","stylesheet"],NAME:"editorBase",STRINGS:{title:"Rich Text Editor"},ATTRS:{content:{validator:n.isString,value:'<br class="yui-cursor">',setter:function(t){return t.substr(0,1)==="\n"&&(t=t.substr(1)),t===""&&(t='<br class="yui-cursor">'),t===" "&&e.UA.gecko&&(t='<br class="yui-cursor">'),this.frame.set("content",t)},getter:function(){return this.frame.get("content")}},dir:{validator:n.isString,writeOnce:!0,value:"ltr"},linkedcss:{validator:"_validateLinkedCSS",value:"",setter:function(e){return this.frame&&this.frame.set("linkedcss",e),e}},extracss:{validator:n.isString,value:"",setter:function(e){return this.frame&&this.frame.set("extracss",e),e}},defaultblock:{validator:n.isString,value:"p"}}}),e.EditorBase=r},"3.17.2",{requires:["base","frame","node","exec-command","editor-selection"]});
diff --git a/js/yui3/editor-bidi/editor-bidi-min.js b/js/yui3/editor-bidi/editor-bidi-min.js
new file mode 100644
index 000000000..df1d8568e
--- /dev/null
+++ b/js/yui3/editor-bidi/editor-bidi-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("editor-bidi",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)},r="host",i="dir",s="nodeChange",o="bidiContextChange",u="style";e.extend(n,e.Base,{lastDirection:null,firstEvent:null,_checkForChange:function(){var e=this.get(r),t=e.getInstance(),i=new t.EditorSelection,s,u;i.isCollapsed?(s=n.blockParent(i.focusNode,!1,t.EditorSelection.ROOT),s&&(u=s.getStyle("direction"),u!==this.lastDirection&&(e.fire(o,{changedTo:u}),this.lastDirection=u))):(e.fire(o,{changedTo:"select"}),this.lastDirection=null)},_afterNodeChange:function(e){if(this.firstEvent||n.EVENTS[e.changedType])this._checkForChange(),this.firstEvent=!1},_afterMouseUp:function(){this._checkForChange(),this.firstEvent=!1},initializer:function(){var t=this.get(r);this.firstEvent=!0,t.after(s,e.bind(this._afterNodeChange,this)),t.after("dom:mouseup",e.bind(this._afterMouseUp,this))}},{EVENTS:{"backspace-up":!0,"pageup-up":!0,"pagedown-down":!0,"end-up":!0,"home-up":!0,"left-up":!0,"up-up":!0,"right-up":!0,"down-up":!0,"delete-up":!0},BLOCKS:e.EditorSelection.BLOCKS,DIV_WRAPPER:"<DIV></DIV>",blockParent:function(t,r,i){var s=t,o,u;return i=i||e.EditorSelection.ROOT,s||(s=i),s.test(n.BLOCKS)||(s=s.ancestor(n.BLOCKS)),r&&s.compareTo(i)&&(o=e.Node.create(n.DIV_WRAPPER),s.get("children").each(function(e,t){t===0?u=e:o.append(e)}),u.replace(o),o.prepend(u),s=o),s},_NODE_SELECTED:"bidiSelected",addParents:function(t,r){var i,s,o;tester=function(e){if(!e.getData(n._NODE_SELECTED))return o=!1,!0},r=r||e.EditorSelection.ROOT;for(i=0;i<t.length;i+=1)t[i].setData(n._NODE_SELECTED,!0);for(i=0;i<t.length;i+=1)s=t[i].get("parentNode"),!r.compareTo(s)&&!s.getData(n._NODE_SELECTED)&&(o=!0,s.get("children").some(tester),o&&(t.push(s),s.setData(n._NODE_SELECTED,!0)));for(i=0;i<t.length;i+=1)t[i].clearData(n._NODE_SELECTED);return t},NAME:"editorBidi",NS:"editorBidi",ATTRS:{host:{value:!1}},RE_TEXT_ALIGN:/text-align:\s*\w*\s*;/,removeTextAlign:function(e){return e&&(e.getAttribute(u).match(n.RE_TEXT_ALIGN)&&e.setAttribute(u,e.getAttribute(u).replace(n.RE_TEXT_ALIGN,"")),e.hasAttribute("align")&&e.removeAttribute("align")),e}}),e.namespace("Plugin"),e.Plugin.EditorBidi=n,e.Plugin.ExecCommand.COMMANDS.bidi=function(t,s){var o=this.getInstance(),u=new o.EditorSelection,a=this.get(r).get(r).editorBidi,f,l,c,h=o.EditorSelection.ROOT,p,d,v;if(!a){e.error("bidi execCommand is not available without the EditorBiDi plugin.");return}return o.EditorSelection.filterBlocks(),u.isCollapsed?(l=n.blockParent(u.anchorNode,!1,h),l||(l=h.one(o.EditorSelection.BLOCKS)),l=n.removeTextAlign(l),s||(v=l.getAttribute(i),!v||v==="ltr"?s="rtl":s="ltr"),l.setAttribute(i,s),e.UA.ie&&(c=l.all("br.yui-cursor"),c.size()===1&&l.get("childNodes").size()===1&&c.remove()),f=l):(p=u.getSelected(),d=[],p.each(function(e){d.push(n.blockParent(e,!1,h))}),d=o.all(n.addParents(d,h)),d.each(function(e){var t=s;e=n.removeTextAlign(e),t||(v=e.getAttribute(i),!v||v==="ltr"?t="rtl":t="ltr"),e.setAttribute(i,t)}),f=d),a._checkForChange(),f}},"3.17.2",{requires:["editor-base"]});
diff --git a/js/yui3/editor-br/editor-br-min.js b/js/yui3/editor-br/editor-br-min.js
new file mode 100644
index 000000000..1baf96542
--- /dev/null
+++ b/js/yui3/editor-br/editor-br-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("editor-br",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)},r="host",i="li";e.extend(n,e.Base,{_onKeyDown:function(t){if(t.stopped){t.halt();return}if(t.keyCode===13){var n=this.get(r),s=n.getInstance(),o=new s.EditorSelection;o&&(e.UA.ie&&(!o.anchorNode||!o.anchorNode.test(i)&&!o.anchorNode.ancestor(i))&&(n.execCommand("inserthtml",s.EditorSelection.CURSOR),t.halt()),e.UA.webkit&&(!o.anchorNode||!o.anchorNode.test(i)&&!o.anchorNode.ancestor(i))&&(n.frame._execCommand("insertlinebreak",null),t.halt()))}},_afterEditorReady:function(){var t=this.get(r).getInstance(),n;try{t.config.doc.execCommand("insertbronreturn",null,!0)}catch(i){}if(e.UA.ie||e.UA.webkit)n=t.EditorSelection.ROOT,n.test("body")&&(n=t.config.doc),t.on("keydown",e.bind(this._onKeyDown,this),n)},_onNodeChange:function(e){switch(e.changedType){case"backspace-up":case"backspace-down":case"delete-up":var t=this.get(r).getInstance(),n=e.changedNode,i=t.config.doc.createTextNode(" ");n.appendChild(i),n.removeChild(i)}},initializer:function(){var t=this.get(r);if(t.editorPara){e.error("Can not plug EditorBR and EditorPara at the same time.");return}t.after("ready",e.bind(this._afterEditorReady,this)),e.UA.gecko&&t.on("nodeChange",e.bind(this._onNodeChange,this))}},{NAME:"editorBR",NS:"editorBR",ATTRS:{host:{value:!1}}}),e.namespace("Plugin"),e.Plugin.EditorBR=n},"3.17.2",{requires:["editor-base"]});
diff --git a/js/yui3/editor-inline/editor-inline-min.js b/js/yui3/editor-inline/editor-inline-min.js
new file mode 100644
index 000000000..552eb126a
--- /dev/null
+++ b/js/yui3/editor-inline/editor-inline-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("editor-inline",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)};e.extend(n,e.EditorBase,{initializer:function(){this.plug(e.Plugin.ContentEditable)}}),e.InlineEditor=n},"3.17.2",{requires:["editor-base","content-editable"]});
diff --git a/js/yui3/editor-lists/editor-lists-min.js b/js/yui3/editor-lists/editor-lists-min.js
new file mode 100644
index 000000000..3f5c2c558
--- /dev/null
+++ b/js/yui3/editor-lists/editor-lists-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("editor-lists",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)},r="li",i="ol",s="ul",o="host";e.extend(n,e.Base,{_onNodeChange:function(t){var u=this.get(o).getInstance(),a,f,l,c,h=!1,p,d=!1;t.changedType==="tab"&&(t.changedNode.test(r+", "+r+" *")&&(t.changedEvent.halt(),t.preventDefault(),a=t.changedNode,l=t.changedEvent.shiftKey,c=a.ancestor(i+","+s),p=s,c.get("tagName").toLowerCase()===i&&(p=i),a.test(r)||(a=a.ancestor(r)),l?a.ancestor(r)&&(a.ancestor(r).insert(a,"after"),h=!0,d=!0):a.previous(r)&&(f=u.Node.create("<"+p+"></"+p+">"),a.previous(r).append(f),f.append(a),h=!0)),h&&(a.test(r)||(a=a.ancestor(r)),a.all(n.REMOVE).remove(),e.UA.ie&&(a=a.append(n.NON).one(n.NON_SEL)),(new u.EditorSelection).selectNode(a,!0,d)))},initializer:function(){this.get(o).on("nodeChange",e.bind(this._onNodeChange,this))}},{NON:'<span class="yui-non">&nbsp;</span>',NON_SEL:"span.yui-non",REMOVE:"br",NAME:"editorLists",NS:"lists",ATTRS:{host:{value:!1}}}),e.namespace("Plugin"),e.Plugin.EditorLists=n},"3.17.2",{requires:["editor-base"]});
diff --git a/js/yui3/editor-para-base/editor-para-base-min.js b/js/yui3/editor-para-base/editor-para-base-min.js
new file mode 100644
index 000000000..ae27c904f
--- /dev/null
+++ b/js/yui3/editor-para-base/editor-para-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("editor-para-base",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)},r="host",i="> p",s="p",o="<br>";e.extend(n,e.Base,{_getRoot:function(){return this.get(r).getInstance().EditorSelection.ROOT},_fixFirstPara:function(){var e=this.get(r),t=e.getInstance(),n,u,a=this._getRoot(),f=a.getHTML(),l=f.length?!0:!1;f===o&&(f="",l=!1),a.setHTML("<"+s+">"+f+t.EditorSelection.CURSOR+"</"+s+">"),u=a.one(i),n=new t.EditorSelection,n.selectNode(u,!0,l)},_afterEditorReady:function(){var e=this.get(r),t=e.getInstance(),n;t&&(t.EditorSelection.filterBlocks(),n=t.EditorSelection.DEFAULT_BLOCK_TAG,i="> "+n,s=n)},_afterContentChange:function(){var e=this.get(r),t=e.getInstance();t&&t.EditorSelection&&t.EditorSelection.filterBlocks()},_afterPaste:function(){var t=this.get(r),n=t.getInstance();e.later(50,t,function(){n.EditorSelection.filterBlocks()})},initializer:function(){var t=this.get(r);if(t.editorBR){e.error("Can not plug EditorPara and EditorBR at the same time.");return}t.after("ready",e.bind(this._afterEditorReady,this)),t.after("contentChange",e.bind(this._afterContentChange,this)),e.Env.webkit&&t.after("dom:paste",e.bind(this._afterPaste,this))}},{NAME:"editorParaBase",NS:"editorParaBase",ATTRS:{host:{value:!1}}}),e.namespace("Plugin"),e.Plugin.EditorParaBase=n},"3.17.2",{requires:["editor-base"]});
diff --git a/js/yui3/editor-para-ie/editor-para-ie-min.js b/js/yui3/editor-para-ie/editor-para-ie-min.js
new file mode 100644
index 000000000..b06a97982
--- /dev/null
+++ b/js/yui3/editor-para-ie/editor-para-ie-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("editor-para-ie",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)},r="host",i="nodeChange",s="p";e.extend(n,e.Plugin.EditorParaBase,{_getRoot:function(){return this.get(r).getInstance().EditorSelection.ROOT},_onNodeChange:function(e){var t=this.get(r),n=t.getInstance(),i=n.EditorSelection.DEFAULT_BLOCK_TAG,o,u=":last-child",a,f,l,c,h,p=!1;switch(e.changedType){case"enter-up":a=this._lastPara?this._lastPara:e.changedNode,f=a.one("br.yui-cursor"),this._lastPara&&delete this._lastPara,f&&(f.previous()||f.next())&&f.ancestor(s)&&f.remove(),a.test(i)||(l=a.ancestor(i),l&&(a=l,l=null));if(a.test(i)){o=a.previous();if(o){c=o.one(u);while(!p)c?(h=c.one(u),h?c=h:p=!0):p=!0;c&&t.copyStyles(c,a)}}break;case"enter":e.changedNode.test("br")?e.changedNode.remove():e.changedNode.test("p, span")&&(f=e.changedNode.one("br.yui-cursor"),f&&f.remove())}},initializer:function(){var t=this.get(r);if(t.editorBR){e.error("Can not plug EditorPara and EditorBR at the same time.");return}t.on(i,e.bind(this._onNodeChange,this))}},{NAME:"editorPara",NS:"editorPara",ATTRS:{host:{value:!1}}}),e.namespace("Plugin"),e.Plugin.EditorPara=n},"3.17.2",{requires:["editor-para-base"]});
diff --git a/js/yui3/editor-para/editor-para-min.js b/js/yui3/editor-para/editor-para-min.js
new file mode 100644
index 000000000..ffb4d975e
--- /dev/null
+++ b/js/yui3/editor-para/editor-para-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("editor-para",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)},r="host",i="nodeChange",s="parentNode",o="> p",u="p",a="<br>",f="firstChild",l="li";e.extend(n,e.Plugin.EditorParaBase,{_getRoot:function(){return this.get(r).getInstance().EditorSelection.ROOT},_onNodeChange:function(t){var n=this.get(r),i=n.getInstance(),c,h,p,d,v,m=i.EditorSelection.DEFAULT_BLOCK_TAG,g,y,b,w,E,S,x,T,N,C,k,L,A,O,M,_,D=":last-child",H,B,j,F,I,q=!1,R=this._getRoot(),U;switch(t.changedType){case"enter-up":H=this._lastPara?this._lastPara:t.changedNode,B=H.one("br.yui-cursor"),this._lastPara&&delete this._lastPara,B&&(B.previous()||B.next())&&B.ancestor(u)&&B.remove(),H.test(m)||(N=H.ancestor(m),N&&(H=N,N=null));if(H.test(m)){C=H.previous();if(C){F=C.one(D);while(!q)F?(I=F.one(D),I?F=I:q=!0):q=!0;F&&n.copyStyles(F,H)}}break;case"enter":e.UA.webkit&&t.changedEvent.shiftKey&&(n.execCommand("insertbr"),t.changedEvent.preventDefault()),t.changedNode.test("li")&&!e.UA.ie&&(c=i.EditorSelection.getText(t.changedNode),c===""&&(p=t.changedNode.ancestor("ol,ul"),j=p.getAttribute("dir"),j!==""&&(j=' dir = "'+j+'"'),p=t.changedNode.ancestor(i.EditorSelection.BLOCKS),d=i.Node.create("<p"+j+">"+i.EditorSelection.CURSOR+"</p>"),p.insert(d,"after"),t.changedNode.remove(),t.changedEvent.halt(),v=new i.EditorSelection,v.selectNode(d,!0,!1)));if(e.UA.gecko&&n.get("defaultblock")!=="p"){p=t.changedNode;if(!p.test(l)&&!p.ancestor(l)){p.test(m)||(p=p.ancestor(m)),d=i.Node.create("<"+m+"></"+m+">"),p.insert(d,"after"),v=new i.EditorSelection;if(v.anchorOffset){g=v.anchorNode.get("textContent"),h=i.one(i.config.doc.createTextNode(g.substr(0,v.anchorOffset))),y=i.one(i.config.doc.createTextNode(g.substr(v.anchorOffset))),w=v.anchorNode,w.setContent(""),E=w.cloneNode(),E.append(y),S=!1,T=w;while(!S)T=T.get(s),T&&!T.test(m)?(x=T.cloneNode(),x.set("innerHTML",""),x.append(E),b=T.get("childNodes"),U=!1,b.each(function(e){U&&x.append(e),e===w&&(U=!0)}),w=T,E=x):S=!0;y=E,v.anchorNode.append(h),y&&d.append(y)}d.get(f)&&(d=d.get(f)),d.prepend(i.EditorSelection.CURSOR),v.focusCursor(!0,!0),c=i.EditorSelection.getText(d),c!==""&&i.EditorSelection.cleanCursor(),t.changedEvent.preventDefault()}}break;case"keyup":e.UA.gecko&&R&&R.getHTML().length<20&&(R.one(o)||this._fixFirstPara());break;case"backspace-up":case"backspace-down":case"delete-up":e.UA.ie||(k=R.all(o),A=R,k.item(0)&&(A=k.item(0)),L=A.one("br"),L&&(L.removeAttribute("id"),L.removeAttribute("class")),h=i.EditorSelection.getText(A),h=h.replace(/ /g,"").replace(/\n/g,""),M=A.all("img"),h.length===0&&!M.size()&&(A.test(u)||this._fixFirstPara(),O=null,t.changedNode&&t.changedNode.test(u)&&(O=t.changedNode),!O&&n._lastPara&&n._lastPara.inDoc()&&(O=n._lastPara),O&&!O.test(u)&&(O=O.ancestor(u)),O&&!O.previous()&&O.get(s)&&O.get(s).compareTo(R)&&(t.changedEvent.frameEvent.halt(),t.preventDefault())),e.UA.webkit&&t.changedNode&&(t.preventDefault(),A=t.changedNode,A.test("li")&&!A.previous()&&!A.next()&&(c=A.get("innerHTML").replace(a,""),c===""&&A.get(s)&&(A.get(s).replace(i.Node.create(a)),t.changedEvent.frameEvent.halt(),i.EditorSelection.filterBlocks())))),e.UA.gecko&&this._fixGeckoOnBackspace(i)}e.UA.gecko&&t.changedNode&&!t.changedNode.test(m)&&(O=t.changedNode.ancestor(m),O&&(this._lastPara=O))},_fixGeckoOnBackspace:function(e){var t=new e.EditorSelection,n,r;if(!t.isCollapsed||t.anchorNode.get("nodeName")!=="P"||t.anchorOffset===0)return;r=t.anchorNode.get("childNodes"),n=t.anchorNode.get("lastChild");if(t.anchorOffset!==r.size()||n.get("nodeName")!=="BR")return;if(t.anchorOffset===1){t.selectNode(t.anchorNode,!0);return}n=n.get("previousSibling");if(n.get("nodeType")!==Node.TEXT_NODE)return;offset=n.get("length"),t.getEditorOffset()===offset&&t.selectNode(n,!0,offset)},initializer:function(){var t=this.get(r);if(t.editorBR){e.error("Can not plug EditorPara and EditorBR at the same time.");return}t.on(i,e.bind(this._onNodeChange,this))}},{NAME:"editorPara",NS:"editorPara",ATTRS:{host:{value:!1}}}),e.namespace("Plugin"),e.Plugin.EditorPara=n},"3.17.2",{requires:["editor-para-base"]});
diff --git a/js/yui3/editor-selection/editor-selection-min.js b/js/yui3/editor-selection/editor-selection-min.js
new file mode 100644
index 000000000..5f3900d4f
--- /dev/null
+++ b/js/yui3/editor-selection/editor-selection-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("editor-selection",function(e,t){var n="textContent",r="innerHTML",i="fontFamily";e.UA.ie&&e.UA.ie<11&&(n="nodeValue"),e.EditorSelection=function(t){var n,r,i,s,o,u,a,f=0,l,c,h=e.EditorSelection.ROOT;e.config.win.getSelection&&(!e.UA.ie||e.UA.ie<9||e.UA.ie>10)?n=e.config.win.getSelection():e.config.doc.selection&&(n=e.config.doc.selection.createRange()),this._selection=n;if(!n)return!1;if(n.pasteHTML){this.isCollapsed=n.compareEndPoints("StartToEnd",n)?!1:!0;if(this.isCollapsed){this.anchorNode=this.focusNode=e.one(n.parentElement()),t&&(i=e.config.doc.elementFromPoint(t.clientX,t.clientY)),o=n.duplicate();if(!i){r=n.parentElement(),s=r.childNodes;for(u=0;u<s.length;u++)o.inRange(n)&&(i||(i=s[u]))}this.ieNode=i,i&&(i.nodeType!==3&&(i.firstChild&&(i=i.firstChild),h.compareTo(i)&&i.firstChild&&(i=i.firstChild)),this.anchorNode=this.focusNode=e.EditorSelection.resolve(i),o.moveToElementText(n.parentElement()),a=n.compareEndPoints("StartToStart",o),a&&(f=this.getEditorOffset(h),n.move("character",-f)),this.anchorOffset=this.focusOffset=f,this.anchorTextNode=this.focusTextNode=e.one(i))}else n.htmlText&&n.htmlText!==""&&(l=e.Node.create(n.htmlText),l&&l.get("id")?(c=l.get("id"),this.anchorNode=this.focusNode=e.one("#"+c)):l&&(l=l.get("childNodes"),this.anchorNode=this.focusNode=l.item(0)))}else this.isCollapsed=n.isCollapsed,this.anchorNode=e.EditorSelection.resolve(n.anchorNode),this.focusNode=e.EditorSelection.resolve(n.focusNode),this.anchorOffset=n.anchorOffset,this.focusOffset=n.focusOffset,this.anchorTextNode=e.one(n.anchorNode||this.anchorNode),this.focusTextNode=e.one(n.focusNode||this.focusNode);e.Lang.isString(n.text)?this.text=n.text:n.toString?this.text=n.toString():this.text=""},e.EditorSelection.removeFontFamily=function(t){t.removeAttribute("face");var n=t.getAttribute("style").toLowerCase();(n===""||n==="font-family: ")&&t.removeAttribute("style"),n.match(e.EditorSelection.REG_FONTFAMILY)&&(n=n.replace(e.EditorSelection.REG_FONTFAMILY,""),t.setAttribute("style",n))},e.EditorSelection.filter=function(t){var n=(new Date).getTime(),r=e.EditorSelection,s=r.ROOT,o,u=s.all(r.ALL),a=s.all("strong,em"),f=e.config.doc,l,c={},h="",p,d=(new Date).getTime(),v;u.each(function(t){var n=e.Node.getDOMNode(t);n.style[i]&&(c["."+t._yuid]=n.style[i],t.addClass(t._yuid),r.removeFontFamily(n))}),v=(new Date).getTime(),s.all(".hr").addClass("yui-skip").addClass("yui-non"),e.UA.ie&&(l=e.Node.getDOMNode(s).getElementsByTagName("hr"),e.each(l,function(e){var t=f.createElement("div"),n=t.style;t.className="hr yui-non yui-skip",t.setAttribute("readonly",!0),t.setAttribute("contenteditable",!1),e.parentNode&&e.parentNode.replaceChild(t,e),n.border="1px solid #ccc",n.lineHeight="0",n.height="0",n.fontSize="0",n.marginTop="5px",n.marginBottom="5px",n.marginLeft="0px",n.marginRight="0px",n.padding="0"})),e.each(c,function(e,t){h+=t+" { font-family: "+e.replace(/"/gi,"")+"; }"}),e.StyleSheet(h,"editor"),a.each(function(e,t){var n=e.get("tagName").toLowerCase(),i="i";n==="strong"&&(i="b"),r.prototype._swap(a.item(t),i)}),p=s.all("ol,ul"),p.each(function(e){var t=e.all("li");t.size()||e.remove()}),t&&r.filterBlocks(),o=(new Date).getTime()},e.EditorSelection.filterBlocks=function(){var t=(new Date).getTime(),r,i=e.Node.getDOMNode(e.EditorSelection.ROOT).childNodes,s,o,u=!1,a=!0,f,l,c,h,p,d;if(i){for(s=0;s<i.length;s++)o=e.one(i[s]),o.test(e.EditorSelection.BLOCKS)?u=e.EditorSelection._wrapBlock(u):(a=!0,i[s].nodeType===3&&(h=i[s][n].match(e.EditorSelection.REG_CHAR),p=i[s][n].match(e.EditorSelection.REG_NON),h===null&&p&&(a=!1)),a&&(u||(u=[]),u.push(i[s])));u=e.EditorSelection._wrapBlock(u)}l=e.all(e.EditorSelection.DEFAULT_BLOCK_TAG);if(l.size()===1){c=l.item(0).all("br");if(c.size()===1){c.item(0).test(".yui-cursor")||c.item(0).remove(),d=l.item(0).get("innerHTML");if(d===""||d===" ")l.set("innerHTML",e.EditorSelection.CURSOR),f=new e.EditorSelection,f.focusCursor(!0,!0);c.item(0).test(".yui-cursor")&&e.UA.ie&&c.item(0).remove()}}else l.each(function(e){var t=e.get("innerHTML");t===""&&e.remove()});r=(new Date).getTime()},e.EditorSelection.REG_FONTFAMILY=/font-family:\s*;/,e.EditorSelection.REG_CHAR=/[a-zA-Z-0-9_!@#\$%\^&*\(\)-=_+\[\]\\{}|;':",.\/<>\?]/gi,e.EditorSelection.REG_NON=/[\s|\n|\t]/gi,e.EditorSelection.REG_NOHTML=/<\S[^><]*>/g,e.EditorSelection._wrapBlock=function(t){if(t){var n=e.Node.create("<"+e.EditorSelection.DEFAULT_BLOCK_TAG+"></"+e.EditorSelection.DEFAULT_BLOCK_TAG+">"),r=e.one(t[0]),i;for(i=1;i<t.length;i++)n.append(t[i]);r.replace(n),n.prepend(r)}return!1},e.EditorSelection.unfilter=function(){var t=e.EditorSelection.ROOT,n=t.all("[class]"),r="",s,o,u=t;return n.each(function(e){e.hasClass(e._yuid)&&(e.setStyle(i,e.getStyle(i)),e.removeClass(e._yuid),e.getAttribute("class")===""&&e.removeAttribute("class"))}),s=t.all(".yui-non"),s.each(function(e){!e.hasClass("yui-skip")&&e.get("innerHTML")===""?e.remove():e.removeClass("yui-non").removeClass("yui-skip")}),o=t.all("[id]"),o.each(function(e){e.get("id").indexOf("yui_3_")===0&&(e.removeAttribute("id"),e.removeAttribute("_yuid"))}),u&&(r=u.get("innerHTML")),t.all(".hr").addClass("yui-skip").addClass("yui-non"),r},e.EditorSelection.resolve=function(t){if(!t)return e.EditorSelection.ROOT;if(t&&t.nodeType===3)try{t=t.parentNode}catch(n){t=e.EditorSelection.ROOT}return e.one(t)},e.EditorSelection.getText=function(t){var n=t.get("innerHTML").replace(e.EditorSelection.REG_NOHTML,"");return n=n.replace("<span><br></span>","").replace("<br>",""),n},e.EditorSelection.DEFAULT_BLOCK_TAG="p",e.EditorSelection.ALL="[style],font[face]",e.EditorSelection.BLOCKS="p,div,ul,ol,table,style",e.EditorSelection.TMP="yui-tmp",e.EditorSelection.DEFAULT_TAG="span",e.EditorSelection.CURID="yui-cursor",e.EditorSelection.CUR_WRAPID="yui-cursor-wrapper",e.EditorSelection.CURSOR='<span><br class="yui-cursor"></span>',e.EditorSelection.ROOT=e.one("body"),e.EditorSelection.hasCursor=function(){var t=e.all("#"+e.EditorSelection.CUR_WRAPID);return t.size()},e.EditorSelection
+.cleanCursor=function(){var t,n="br.yui-cursor";t=e.all(n),t.size()&&t.each(function(t){var n=t.get("parentNode.parentNode.childNodes"),r;n.size()?t.remove():(r=e.EditorSelection.getText(n.item(0)),r!==""&&t.remove())})},e.EditorSelection.prototype={text:null,isCollapsed:null,anchorNode:null,anchorOffset:null,anchorTextNode:null,focusNode:null,focusOffset:null,focusTextNode:null,_selection:null,_wrap:function(t,n){var i=e.Node.create("<"+n+"></"+n+">");return i.set(r,t.get(r)),t.set(r,""),t.append(i),e.Node.getDOMNode(i)},_swap:function(t,n){var i=e.Node.create("<"+n+"></"+n+">");return i.set(r,t.get(r)),t.replace(i,t),e.Node.getDOMNode(i)},getSelected:function(){var t=e.EditorSelection,n=t.ROOT,r,s=[];return t.filter(),e.config.doc.execCommand("fontname",null,t.TMP),r=n.all(t.ALL),r.each(function(o,u){o.getStyle(i)===t.TMP&&(o.setStyle(i,""),t.removeFontFamily(o),o.compareTo(n)||s.push(e.Node.getDOMNode(r.item(u))))}),e.all(s)},insertContent:function(e){return this.insertAtCursor(e,this.anchorTextNode,this.anchorOffset,!0)},insertAtCursor:function(t,r,i,s){var o=e.Node.create("<"+e.EditorSelection.DEFAULT_TAG+' class="yui-non"></'+e.EditorSelection.DEFAULT_TAG+">"),u,a,f,l,c=this.createRange(),h,p=e.EditorSelection.ROOT;p.compareTo(r)&&(h=e.Node.create("<span></span>"),r.append(h),r=h);if(c.pasteHTML){if(i===0&&r&&!r.previous()&&r.get("nodeType")===3)return r.insert(t,"before"),c.moveToElementText&&c.moveToElementText(e.Node.getDOMNode(r.previous())),c.collapse(!1),c.select(),r.previous();l=e.Node.create(t);try{c.pasteHTML('<span id="rte-insert"></span>')}catch(d){}u=p.one("#rte-insert");if(u)return u.set("id",""),u.replace(l),c.moveToElementText&&c.moveToElementText(e.Node.getDOMNode(l)),c.collapse(!1),c.select(),l;e.on("available",function(){u.set("id",""),u.replace(l),c.moveToElementText&&c.moveToElementText(e.Node.getDOMNode(l)),c.collapse(!1),c.select()},"#rte-insert")}else i>0?(u=r.get(n),a=e.one(e.config.doc.createTextNode(u.substr(0,i))),f=e.one(e.config.doc.createTextNode(u.substr(i))),r.replace(a,r),l=e.Node.create(t),l.get("nodeType")===11&&(h=e.Node.create("<span></span>"),h.append(l),l=h),a.insert(l,"after"),f&&(l.insert(o,"after"),o.insert(f,"after"),this.selectNode(o,s))):(r.get("nodeType")===3&&(r=r.get("parentNode")||p),l=e.Node.create(t),t=r.get("innerHTML").replace(/\n/gi,""),t===""||t==="<br>"?r.append(l):l.get("parentNode")?r.insert(l,"before"):p.prepend(l),r.get("firstChild").test("br")&&r.get("firstChild").remove());return l},wrapContent:function(t){t=t?t:e.EditorSelection.DEFAULT_TAG;if(!this.isCollapsed){var n=this.getSelected(),r=[],i,s,o,u;return n.each(function(e,i){var s=e.get("tagName").toLowerCase();s==="font"?r.push(this._swap(n.item(i),t)):r.push(this._wrap(n.item(i),t))},this),i=this.createRange(),o=r[0],s=r[r.length-1],this._selection.removeAllRanges?(i.setStart(r[0],0),i.setEnd(s,s.childNodes.length),this._selection.removeAllRanges(),this._selection.addRange(i)):(i.moveToElementText&&(i.moveToElementText(e.Node.getDOMNode(o)),u=this.createRange(),u.moveToElementText(e.Node.getDOMNode(s)),i.setEndPoint("EndToEnd",u)),i.select()),r=e.all(r),r}return e.all([])},replace:function(t,r){var i=this.createRange(),s,o,u,a;return i.getBookmark?(u=i.getBookmark(),o=this.anchorNode.get("innerHTML").replace(t,r),this.anchorNode.set("innerHTML",o),i.moveToBookmark(u),a=e.one(i.parentElement())):(s=this.anchorTextNode,o=s.get(n),u=o.indexOf(t),o=o.replace(t,""),s.set(n,o),a=this.insertAtCursor(r,s,u,!0)),a},remove:function(){return this._selection&&this._selection.removeAllRanges&&this._selection.removeAllRanges(),this},createRange:function(){return e.config.doc.selection?e.config.doc.selection.createRange():e.config.doc.createRange()},selectNode:function(t,n,r){if(!t)return;r=r||0,t=e.Node.getDOMNode(t);var i=this.createRange();if(i.selectNode){try{i.selectNode(t)}catch(s){}this._selection.removeAllRanges(),this._selection.addRange(i);if(n)try{this._selection.collapse(t,r)}catch(s){this._selection.collapse(t,0)}}else{t.nodeType===3&&(t=t.parentNode);try{i.moveToElementText(t)}catch(o){}n&&i.collapse(r?!1:!0),i.select()}return this},setCursor:function(){return this.removeCursor(!1),this.insertContent(e.EditorSelection.CURSOR)},getCursor:function(){return e.EditorSelection.ROOT.all("."+e.EditorSelection.CURID)},removeCursor:function(e){var t=this.getCursor();return t&&(e?(t.removeAttribute("id"),t.set("innerHTML",'<br class="yui-cursor">')):t.remove()),t},focusCursor:function(e,t){e!==!1&&(e=!0),t!==!1&&(t=!0);var n=this.removeCursor(!0);n&&n.each(function(n){this.selectNode(n,e,t)},this)},toString:function(){return"EditorSelection Object"},getEditorOffset:function(t){var n=(t||e.EditorSelection.ROOT).getDOMNode(),r=0,i=e.config.doc,s=e.config.win,o,u,a;return typeof s.getSelection!="undefined"?(u=s.getSelection().getRangeAt(0),a=u.cloneRange(),a.selectNodeContents(n),a.setEnd(u.endContainer,u.endOffset),r=a.toString().length):(o=i.selection,o&&o.type!=="Control"&&(u=o.createRange(),a=i.body.createTextRange(),a.moveToElementText(n),a.setEndPoint("EndToEnd",u),r=a.text.length)),r}},e.Selection=e.EditorSelection},"3.17.2",{requires:["node"]});
diff --git a/js/yui3/editor-tab/editor-tab-min.js b/js/yui3/editor-tab/editor-tab-min.js
new file mode 100644
index 000000000..d60866c0c
--- /dev/null
+++ b/js/yui3/editor-tab/editor-tab-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("editor-tab",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)},r="host";e.extend(n,e.Base,{_onNodeChange:function(e){var t="indent";e.changedType==="tab"&&(e.changedNode.test("li, li *")||(e.changedEvent.halt(),e.preventDefault(),e.changedEvent.shiftKey&&(t="outdent"),this.get(r).execCommand(t,"")))},initializer:function(){this.get(r).on("nodeChange",e.bind(this._onNodeChange,this))}},{NAME:"editorTab",NS:"tab",ATTRS:{host:{value:!1}}}),e.namespace("Plugin"),e.Plugin.EditorTab=n},"3.17.2",{requires:["editor-base"]});
diff --git a/js/yui3/escape/escape-min.js b/js/yui3/escape/escape-min.js
new file mode 100644
index 000000000..c68d9f81c
--- /dev/null
+++ b/js/yui3/escape/escape-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("escape",function(e,t){var n={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","/":"&#x2F;","`":"&#x60;"},r={html:function(e){return(e+"").replace(/[&<>"'\/`]/g,r._htmlReplacer)},regex:function(e){return(e+"").replace(/[\-$\^*()+\[\]{}|\\,.?\s]/g,"\\$&")},_htmlReplacer:function(e){return n[e]}};r.regexp=r.regex,e.Escape=r},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/event-base-ie/event-base-ie-min.js b/js/yui3/event-base-ie/event-base-ie-min.js
new file mode 100644
index 000000000..4b9f5fe29
--- /dev/null
+++ b/js/yui3/event-base-ie/event-base-ie-min.js
@@ -0,0 +1,10 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+(function(){var e,t=YUI.Env,n=YUI.config,r=n.doc,i=r&&r.documentElement,s="onreadystatechange",o=n.pollInterval||40;i.doScroll&&!t._ieready&&(t._ieready=function(){t._ready()},
+/*! DOMReady: based on work by: Dean Edwards/John Resig/Matthias Miller/Diego Perini */
+self!==self.top?(e=function(){r.readyState=="complete"&&(t.remove(r,s,e),t.ieready())},t.add(r,s,e)):t._dri=setInterval(function(){try{i.doScroll("left"),clearInterval(t._dri),t._dri=null,t._ieready()}catch(e){}},o))})(),YUI.add("event-base-ie",function(e,t){function n(){e.DOM2EventFacade.apply(this,arguments)}function r(t){var n=e.config.doc.createEventObject(t),i=r.prototype;return n.hasOwnProperty=function(){return!0},n.init=i.init,n.halt=i.halt,n.preventDefault=i.preventDefault,n.stopPropagation=i.stopPropagation,n.stopImmediatePropagation=i.stopImmediatePropagation,e.DOM2EventFacade.apply(n,arguments),n}var i=e.config.doc&&e.config.doc.implementation,s=e.config.lazyEventFacade,o={0:1,4:2,2:3},u={mouseout:"toElement",mouseover:"fromElement"},a=e.DOM2EventFacade.resolve,f={init:function(){n.superclass.init.apply(this,arguments);var t=this._event,r,i,s,u,f,l;this.target=a(t.srcElement),"clientX"in t&&!r&&0!==r&&(r=t.clientX,i=t.clientY,s=e.config.doc,u=s.body,f=s.documentElement,r+=f.scrollLeft||u&&u.scrollLeft||0,i+=f.scrollTop||u&&u.scrollTop||0,this.pageX=r,this.pageY=i),t.type=="mouseout"?l=t.toElement:t.type=="mouseover"&&(l=t.fromElement),this.relatedTarget=a(l||t.relatedTarget),this.which=this.button=t.keyCode||o[t.button]||t.button},stopPropagation:function(){this._event.cancelBubble=!0,this._wrapper.stopped=1,this.stopped=1},stopImmediatePropagation:function(){this.stopPropagation(),this._wrapper.stopped=2,this.stopped=2},preventDefault:function(e){this._event.returnValue=e||!1,this._wrapper.prevented=1,this.prevented=1}};e.extend(n,e.DOM2EventFacade,f),e.extend(r,e.DOM2EventFacade,f),r.prototype.init=function(){var e=this._event,t=this._wrapper.overrides,n=r._define,i=r._lazyProperties,s;this.altKey=e.altKey,this.ctrlKey=e.ctrlKey,this.metaKey=e.metaKey,this.shiftKey=e.shiftKey,this.type=t&&t.type||e.type,this.clientX=e.clientX,this.clientY=e.clientY,this.keyCode=this.charCode=e.keyCode,this.which=this.button=e.keyCode||o[e.button]||e.button;for(s in i)i.hasOwnProperty(s)&&n(this,s,i[s]);this._touch&&this._touch(e,this._currentTarget,this._wrapper)},r._lazyProperties={target:function(){return a(this._event.srcElement)},relatedTarget:function(){var e=this._event,t=u[e.type]||"relatedTarget";return a(e[t]||e.relatedTarget)},currentTarget:function(){return a(this._currentTarget)},wheelDelta:function(){var e=this._event;if(e.type==="mousewheel"||e.type==="DOMMouseScroll")return e.detail?e.detail*-1:Math.round(e.wheelDelta/80)||(e.wheelDelta<0?-1:1)},pageX:function(){var t=this._event,n=t.pageX,r,i,s;return n===undefined&&(r=e.config.doc,i=r.body&&r.body.scrollLeft,s=r.documentElement.scrollLeft,n=t.clientX+(s||i||0)),n},pageY:function(){var t=this._event,n=t.pageY,r,i,s;return n===undefined&&(r=e.config.doc,i=r.body&&r.body.scrollTop,s=r.documentElement.scrollTop,n=t.clientY+(s||i||0)),n}},r._define=function(e,t,n){function r(r){var i=arguments.length?r:n.call(this);return delete e[t],Object.defineProperty(e,t,{value:i,configurable:!0,writable:!0}),i}Object.defineProperty(e,t,{get:r,set:r,configurable:!0})};if(i&&!i.hasFeature("Events","2.0")){if(s)try{Object.defineProperty(e.config.doc.createEventObject(),"z",{})}catch(l){s=!1}e.DOMEventFacade=s?r:n}},"3.17.2",{requires:["node-base"]});
diff --git a/js/yui3/event-base/event-base-min.js b/js/yui3/event-base/event-base-min.js
new file mode 100644
index 000000000..9693dc473
--- /dev/null
+++ b/js/yui3/event-base/event-base-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-base",function(e,t){e.publish("domready",{fireOnce:!0,async:!0}),YUI.Env.DOMReady?e.fire("domready"):e.Do.before(function(){e.fire("domready")},YUI.Env,"_ready");var n=e.UA,r={},i={63232:38,63233:40,63234:37,63235:39,63276:33,63277:34,25:9,63272:46,63273:36,63275:35},s=function(t){if(!t)return t;try{t&&3==t.nodeType&&(t=t.parentNode)}catch(n){return null}return e.one(t)},o=function(e,t,n){this._event=e,this._currentTarget=t,this._wrapper=n||r,this.init()};e.extend(o,Object,{init:function(){var e=this._event,t=this._wrapper.overrides,r=e.pageX,o=e.pageY,u,a=this._currentTarget;this.altKey=e.altKey,this.ctrlKey=e.ctrlKey,this.metaKey=e.metaKey,this.shiftKey=e.shiftKey,this.type=t&&t.type||e.type,this.clientX=e.clientX,this.clientY=e.clientY,this.pageX=r,this.pageY=o,u=e.keyCode||e.charCode,n.webkit&&u in i&&(u=i[u]),this.keyCode=u,this.charCode=u,this.which=e.which||e.charCode||u,this.button=this.which,this.target=s(e.target),this.currentTarget=s(a),this.relatedTarget=s(e.relatedTarget);if(e.type=="mousewheel"||e.type=="DOMMouseScroll")this.wheelDelta=e.detail?e.detail*-1:Math.round(e.wheelDelta/80)||(e.wheelDelta<0?-1:1);this._touch&&this._touch(e,a,this._wrapper)},stopPropagation:function(){this._event.stopPropagation(),this._wrapper.stopped=1,this.stopped=1},stopImmediatePropagation:function(){var e=this._event;e.stopImmediatePropagation?e.stopImmediatePropagation():this.stopPropagation(),this._wrapper.stopped=2,this.stopped=2},preventDefault:function(e){var t=this._event;t.preventDefault(),e&&(t.returnValue=e),this._wrapper.prevented=1,this.prevented=1},halt:function(e){e?this.stopImmediatePropagation():this.stopPropagation(),this.preventDefault()}}),o.resolve=s,e.DOM2EventFacade=o,e.DOMEventFacade=o,function(){e.Env.evt.dom_wrappers={},e.Env.evt.dom_map={};var t=e.Env.evt,n=e.config,r=n.win,i=YUI.Env.add,s=YUI.Env.remove,o=function(){YUI.Env.windowLoaded=!0,e.Event._load(),s(r,"load",o)},u=function(){e.Event._unload()},a="domready",f="~yui|2|compat~",l=function(t){try{return t&&typeof t!="string"&&e.Lang.isNumber(t.length)&&!t.tagName&&!e.DOM.isWindow(t)}catch(n){return!1}},c=e.CustomEvent.prototype._delete,h=function(t){var n=c.apply(this,arguments);return this.hasSubs()||e.Event._clean(this),n},p=function(){var n=!1,o=0,c=[],d=t.dom_wrappers,v=null,m=t.dom_map;return{POLL_RETRYS:1e3,POLL_INTERVAL:40,lastError:null,_interval:null,_dri:null,DOMReady:!1,startInterval:function(){p._interval||(p._interval=setInterval(p._poll,p.POLL_INTERVAL))},onAvailable:function(t,n,r,i,s,u){var a=e.Array(t),f,l;for(f=0;f<a.length;f+=1)c.push({id:a[f],fn:n,obj:r,override:i,checkReady:s,compat:u});return o=this.POLL_RETRYS,setTimeout(p._poll,0),l=new e.EventHandle({_delete:function(){if(l.handle){l.handle.detach();return}var e,t;for(e=0;e<a.length;e++)for(t=0;t<c.length;t++)a[e]===c[t].id&&c.splice(t,1)}}),l},onContentReady:function(e,t,n,r,i){return p.onAvailable(e,t,n,r,!0,i)},attach:function(t,n,r,i){return p._attach(e.Array(arguments,0,!0))},_createWrapper:function(t,n,s,o,u){var a,f=e.stamp(t),l="event:"+f+n;return!1===u&&(l+="native"),s&&(l+="capture"),a=d[l],a||(a=e.publish(l,{silent:!0,bubbles:!1,emitFacade:!1,contextFn:function(){return o?a.el:(a.nodeRef=a.nodeRef||e.one(a.el),a.nodeRef)}}),a.overrides={},a.el=t,a.key=l,a.domkey=f,a.type=n,a.fn=function(e){a.fire(p.getEvent(e,t,o||!1===u))},a.capture=s,t==r&&n=="load"&&(a.fireOnce=!0,v=l),a._delete=h,d[l]=a,m[f]=m[f]||{},m[f][l]=a,i(t,n,a.fn,s)),a},_attach:function(t,n){var i,s,o,u,a,c=!1,h,d=t[0],v=t[1],m=t[2]||r,g=n&&n.facade,y=n&&n.capture,b=n&&n.overrides;t[t.length-1]===f&&(i=!0);if(!v||!v.call)return!1;if(l(m))return s=[],e.each(m,function(e,r){t[2]=e,s.push(p._attach(t.slice(),n))}),new e.EventHandle(s);if(e.Lang.isString(m)){if(i)o=e.DOM.byId(m);else{o=e.Selector.query(m);switch(o.length){case 0:o=null;break;case 1:o=o[0];break;default:return t[2]=o,p._attach(t,n)}}if(!o)return h=p.onAvailable(m,function(){h.handle=p._attach(t,n)},p,!0,!1,i),h;m=o}return m?(e.Node&&e.instanceOf(m,e.Node)&&(m=e.Node.getDOMNode(m)),u=p._createWrapper(m,d,y,i,g),b&&e.mix(u.overrides,b),m==r&&d=="load"&&YUI.Env.windowLoaded&&(c=!0),i&&t.pop(),a=t[3],h=u._on(v,a,t.length>4?t.slice(4):null),c&&u.fire(),h):!1},detach:function(t,n,r,i){var s=e.Array(arguments,0,!0),o,u,a,c,h,v;s[s.length-1]===f&&(o=!0);if(t&&t.detach)return t.detach();typeof r=="string"&&(o?r=e.DOM.byId(r):(r=e.Selector.query(r),u=r.length,u<1?r=null:u==1&&(r=r[0])));if(!r)return!1;if(r.detach)return s.splice(2,1),r.detach.apply(r,s);if(l(r)){a=!0;for(c=0,u=r.length;c<u;++c)s[2]=r[c],a=e.Event.detach.apply(e.Event,s)&&a;return a}return!t||!n||!n.call?p.purgeElement(r,!1,t):(h="event:"+e.stamp(r)+t,v=d[h],v?v.detach(n):!1)},getEvent:function(t,n,i){var s=t||r.event;return i?s:new e.DOMEventFacade(s,n,d["event:"+e.stamp(n)+t.type])},generateId:function(t){return e.DOM.generateID(t)},_isValidCollection:l,_load:function(t){n||(n=!0,e.fire&&e.fire(a),p._poll())},_poll:function(){if(p.locked)return;if(e.UA.ie&&!YUI.Env.DOMReady){p.startInterval();return}p.locked=!0;var t,r,i,s,u,a,f=!n;f||(f=o>0),u=[],a=function(t,n){var r,i=n.override;try{n.compat?(n.override?i===!0?r=n.obj:r=i:r=t,n.fn.call(r,n.obj)):(r=n.obj||e.one(t),n.fn.apply(r,e.Lang.isArray(i)?i:[]))}catch(s){}};for(t=0,r=c.length;t<r;++t)i=c[t],i&&!i.checkReady&&(s=i.compat?e.DOM.byId(i.id):e.Selector.query(i.id,null,!0),s?(a(s,i),c[t]=null):u.push(i));for(t=0,r=c.length;t<r;++t){i=c[t];if(i&&i.checkReady){s=i.compat?e.DOM.byId(i.id):e.Selector.query(i.id,null,!0);if(s){if(n||s.get&&s.get("nextSibling")||s.nextSibling)a(s,i),c[t]=null}else u.push(i)}}o=u.length===0?0:o-1,f?p.startInterval():(clearInterval(p._interval),p._interval=null),p.locked=!1;return},purgeElement:function(t,n,r){var i=e.Lang.isString(t)?e.Selector.query(t,null,!0):t,s=p.getListeners(i,r),o,u,a,f;if(n&&i){s=s||[],a=e.Selector.query("*",i),u=a.length;for(o=0;o<u;++o)f=p.getListeners(a[o],r),f&&(s=s.concat(f))}if(s)for(o=0,u=s.length;o<u;++o)s[o].detachAll()}
+,_clean:function(t){var n=t.key,r=t.domkey;s(t.el,t.type,t.fn,t.capture),delete d[n],delete e._yuievt.events[n],m[r]&&(delete m[r][n],e.Object.size(m[r])||delete m[r])},getListeners:function(n,r){var i=e.stamp(n,!0),s=m[i],o=[],u=r?"event:"+i+r:null,a=t.plugins;return s?(u?(a[r]&&a[r].eventDef&&(u+="_synth"),s[u]&&o.push(s[u]),u+="native",s[u]&&o.push(s[u])):e.each(s,function(e,t){o.push(e)}),o.length?o:null):null},_unload:function(t){e.each(d,function(e,n){e.type=="unload"&&e.fire(t),e.detachAll()}),s(r,"unload",u)},nativeAdd:i,nativeRemove:s}}();e.Event=p,n.injected||YUI.Env.windowLoaded?o():i(r,"load",o);if(e.UA.ie){e.on(a,p._poll);if(e.UA.ie<7)try{i(r,"unload",u)}catch(d){}}p.Custom=e.CustomEvent,p.Subscriber=e.Subscriber,p.Target=e.EventTarget,p.Handle=e.EventHandle,p.Facade=e.EventFacade,p._poll()}(),e.Env.evt.plugins.available={on:function(t,n,r,i){var s=arguments.length>4?e.Array(arguments,4,!0):null;return e.Event.onAvailable.call(e.Event,r,n,i,s)}},e.Env.evt.plugins.contentready={on:function(t,n,r,i){var s=arguments.length>4?e.Array(arguments,4,!0):null;return e.Event.onContentReady.call(e.Event,r,n,i,s)}}},"3.17.2",{requires:["event-custom-base"]});
diff --git a/js/yui3/event-contextmenu/event-contextmenu-min.js b/js/yui3/event-contextmenu/event-contextmenu-min.js
new file mode 100644
index 000000000..2201e28ec
--- /dev/null
+++ b/js/yui3/event-contextmenu/event-contextmenu-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-contextmenu",function(e,t){var n=e.Event,r=e.DOM,i=e.UA,s=e.UA.os,o=i.ie,u=i.gecko,a=i.webkit,f=i.opera,l=s==="windows",c=s==="macintosh",h={},p={on:function(t,i,s,p){var d=[];d.push(n._attach(["contextmenu",function(n){n.preventDefault();var r=e.stamp(t),i=h[r];i&&(n.clientX=i.clientX,n.clientY=i.clientY,n.pageX=i.pageX,n.pageY=i.pageY,delete h[r]),s.fire(n)},t])),d.push(t[p?"delegate":"on"]("keydown",function(n){var i=this.getDOMNode(),p=n.shiftKey,d=n.keyCode,v=p&&d==121,m=l&&d==93,g=n.ctrlKey,y=d===77,b=c&&(a||u)&&g&&p&&n.altKey&&y,w=c&&f&&g&&p&&y,E=0,S=0,x,T,N,C,k,L,A;if(l&&(v||m)||b||w){((o||l&&(u||f))&&v||w)&&n.preventDefault(),k=r.getXY(i),L=k[0],A=k[1],x=r.docScrollX(),T=r.docScrollY(),e.Lang.isUndefined(L)||(E=L+i.offsetWidth/2-x,S=A+i.offsetHeight/2-T),N=E+x,C=S+T;if(m||l&&a&&v)h[e.stamp(t)]={clientX:E,clientY:S,pageX:N,pageY:C};if((o||l&&(u||f))&&v||c)n.clientX=E,n.clientY=S,n.pageX=N,n.pageY=C,s.fire(n)}},p)),i._handles=d},detach:function(t,n,r){e.each(n._handles,function(e){e.detach()})}};p.delegate=p.on,p.detachDelegate=p.detach,n.define("contextmenu",p,!0)},"3.17.2",{requires:["event-synthetic","dom-screen"]});
diff --git a/js/yui3/event-custom-base/event-custom-base-min.js b/js/yui3/event-custom-base/event-custom-base-min.js
new file mode 100644
index 000000000..858bb0fe7
--- /dev/null
+++ b/js/yui3/event-custom-base/event-custom-base-min.js
@@ -0,0 +1,10 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-custom-base",function(e,t){e.Env.evt={handles:{},plugins:{}};var n=0,r=1,i={objs:null,before:function(t,r,i,s){var o=t,u;return s&&(u=[t,s].concat(e.Array(arguments,4,!0)),o=e.rbind.apply(e,u)),this._inject(n,o,r,i)},after:function(t,n,i,s){var o=t,u;return s&&(u=[t,s].concat(e.Array(arguments,4,!0)),o=e.rbind.apply(e,u)),this._inject(r,o,n,i)},_inject:function(t,n,r,i){var s=e.stamp(r),o,u;return r._yuiaop||(r._yuiaop={}),o=r._yuiaop,o[i]||(o[i]=new e.Do.Method(r,i),r[i]=function(){return o[i].exec.apply(o[i],arguments)}),u=s+e.stamp(n)+i,o[i].register(u,n,t),new e.EventHandle(o[i],u)},detach:function(e){e.detach&&e.detach()}};e.Do=i,i.Method=function(e,t){this.obj=e,this.methodName=t,this.method=e[t],this.before={},this.after={}},i.Method.prototype.register=function(e,t,n){n?this.after[e]=t:this.before[e]=t},i.Method.prototype._delete=function(e){delete this.before[e],delete this.after[e]},i.Method.prototype.exec=function(){var t=e.Array(arguments,0,!0),n,r,s,o=this.before,u=this.after,a=!1;for(n in o)if(o.hasOwnProperty(n)){r=o[n].apply(this.obj,t);if(r)switch(r.constructor){case i.Halt:return r.retVal;case i.AlterArgs:t=r.newArgs;break;case i.Prevent:a=!0;break;default:}}a||(r=this.method.apply(this.obj,t)),i.originalRetVal=r,i.currentRetVal=r;for(n in u)if(u.hasOwnProperty(n)){s=u[n].apply(this.obj,t);if(s&&s.constructor===i.Halt)return s.retVal;s&&s.constructor===i.AlterReturn&&(r=s.newRetVal,i.currentRetVal=r)}return r},i.AlterArgs=function(e,t){this.msg=e,this.newArgs=t},i.AlterReturn=function(e,t){this.msg=e,this.newRetVal=t},i.Halt=function(e,t){this.msg=e,this.retVal=t},i.Prevent=function(e){this.msg=e},i.Error=i.Halt;var s=e.Array,o="after",u=["broadcast","monitored","bubbles","context","contextFn","currentTarget","defaultFn","defaultTargetOnly","details","emitFacade","fireOnce","async","host","preventable","preventedFn","queuable","silent","stoppedFn","target","type"],a=s.hash(u),f=Array.prototype.slice,l=9,c="yui:log",h=function(e,t,n){var r;for(r in t)a[r]&&(n||!(r in e))&&(e[r]=t[r]);return e};e.CustomEvent=function(t,n){this._kds=e.CustomEvent.keepDeprecatedSubs,this.id=e.guid(),this.type=t,this.silent=this.logSystem=t===c,this._kds&&(this.subscribers={},this.afters={}),n&&h(this,n,!0)},e.CustomEvent.keepDeprecatedSubs=!1,e.CustomEvent.mixConfigs=h,e.CustomEvent.prototype={constructor:e.CustomEvent,signature:l,context:e,preventable:!0,bubbles:!0,hasSubs:function(e){var t=0,n=0,r=this._subscribers,i=this._afters,s=this.sibling;return r&&(t=r.length),i&&(n=i.length),s&&(r=s._subscribers,i=s._afters,r&&(t+=r.length),i&&(n+=i.length)),e?e==="after"?n:t:t+n},monitor:function(e){this.monitored=!0;var t=this.id+"|"+this.type+"_"+e,n=f.call(arguments,0);return n[0]=t,this.host.on.apply(this.host,n)},getSubs:function(){var e=this.sibling,t=this._subscribers,n=this._afters,r,i;return e&&(r=e._subscribers,i=e._afters),r?t?t=t.concat(r):t=r.concat():t?t=t.concat():t=[],i?n?n=n.concat(i):n=i.concat():n?n=n.concat():n=[],[t,n]},applyConfig:function(e,t){h(this,e,t)},_on:function(t,n,r,i){var s=new e.Subscriber(t,n,r,i),u;return this.fireOnce&&this.fired&&(u=this.firedWith,this.emitFacade&&this._addFacadeToArgs&&this._addFacadeToArgs(u),this.async?setTimeout(e.bind(this._notify,this,s,u),0):this._notify(s,u)),i===o?(this._afters||(this._afters=[]),this._afters.push(s)):(this._subscribers||(this._subscribers=[]),this._subscribers.push(s)),this._kds&&(i===o?this.afters[s.id]=s:this.subscribers[s.id]=s),new e.EventHandle(this,s)},subscribe:function(e,t){var n=arguments.length>2?f.call(arguments,2):null;return this._on(e,t,n,!0)},on:function(e,t){var n=arguments.length>2?f.call(arguments,2):null;return this.monitored&&this.host&&this.host._monitor("attach",this,{args:arguments}),this._on(e,t,n,!0)},after:function(e,t){var n=arguments.length>2?f.call(arguments,2):null;return this._on(e,t,n,o)},detach:function(e,t){if(e&&e.detach)return e.detach();var n,r,i=0,s=this._subscribers,o=this._afters;if(s)for(n=s.length;n>=0;n--)r=s[n],r&&(!e||e===r.fn)&&(this._delete(r,s,n),i++);if(o)for(n=o.length;n>=0;n--)r=o[n],r&&(!e||e===r.fn)&&(this._delete(r,o,n),i++);return i},unsubscribe:function(){return this.detach.apply(this,arguments)},_notify:function(e,t,n){var r;return r=e.notify(t,this),!1===r||this.stopped>1?!1:!0},log:function(e,t){},fire:function(){var e=[];return e.push.apply(e,arguments),this._fire(e)},_fire:function(e){return this.fireOnce&&this.fired?!0:(this.fired=!0,this.fireOnce&&(this.firedWith=e),this.emitFacade?this.fireComplex(e):this.fireSimple(e))},fireSimple:function(e){this.stopped=0,this.prevented=0;if(this.hasSubs()){var t=this.getSubs();this._procSubs(t[0],e),this._procSubs(t[1],e)}return this.broadcast&&this._broadcast(e),this.stopped?!1:!0},fireComplex:function(e){return e[0]=e[0]||{},this.fireSimple(e)},_procSubs:function(e,t,n){var r,i,s;for(i=0,s=e.length;i<s;i++){r=e[i];if(r&&r.fn){!1===this._notify(r,t,n)&&(this.stopped=2);if(this.stopped===2)return!1}}return!0},_broadcast:function(t){if(!this.stopped&&this.broadcast){var n=t.concat();n.unshift(this.type),this.host!==e&&e.fire.apply(e,n),this.broadcast===2&&e.Global.fire.apply(e.Global,n)}},unsubscribeAll:function(){return this.detachAll.apply(this,arguments)},detachAll:function(){return this.detach()},_delete:function(e,t,n){var r=e._when;t||(t=r===o?this._afters:this._subscribers),t&&(n=s.indexOf(t,e,0),e&&t[n]===e&&t.splice(n,1)),this._kds&&(r===o?delete this.afters[e.id]:delete this.subscribers[e.id]),this.monitored&&this.host&&this.host._monitor("detach",this,{ce:this,sub:e}),e&&(e.deleted=!0)}},e.Subscriber=function(t,n,r,i){this.fn=t,this.context=n,this.id=e.guid(),this.args=r,this._when=i},e.Subscriber.prototype={constructor:e.Subscriber,_notify:function(e,t,n){if(this.deleted&&!this.postponed){if(!this.postponed)return delete this.postponed,null;delete this.fn,delete this.context}var r=this.args,i;switch(n.signature){case 0:i=this.fn.call(e,n.type,t,e);break;case 1:i=this.fn.call(e,t[0]||null,e);break;
+default:r||t?(t=t||[],r=r?t.concat(r):t,i=this.fn.apply(e,r)):i=this.fn.call(e)}return this.once&&n._delete(this),i},notify:function(t,n){var r=this.context,i=!0;r||(r=n.contextFn?n.contextFn():n.context);if(e.config&&e.config.throwFail)i=this._notify(r,t,n);else try{i=this._notify(r,t,n)}catch(s){e.error(this+" failed: "+s.message,s)}return i},contains:function(e,t){return t?this.fn===e&&this.context===t:this.fn===e},valueOf:function(){return this.id}},e.EventHandle=function(e,t){this.evt=e,this.sub=t},e.EventHandle.prototype={batch:function(t,n){t.call(n||this,this),e.Lang.isArray(this.evt)&&e.Array.each(this.evt,function(e){e.batch.call(n||e,t)})},detach:function(){var t=this.evt,n=0,r;if(t)if(e.Lang.isArray(t))for(r=0;r<t.length;r++)n+=t[r].detach();else t._delete(this.sub),n=1;return n},monitor:function(e){return this.evt.monitor.apply(this.evt,arguments)}};var p=e.Lang,d=":",v="|",m="~AFTER~",g=/(.*?)(:)(.*?)/,y=e.cached(function(e){return e.replace(g,"*$2$3")}),b=function(e,t){return!t||!e||e.indexOf(d)>-1?e:t+d+e},w=e.cached(function(e,t){var n=e,r,i,s;return p.isString(n)?(s=n.indexOf(m),s>-1&&(i=!0,n=n.substr(m.length)),s=n.indexOf(v),s>-1&&(r=n.substr(0,s),n=n.substr(s+1),n==="*"&&(n=null)),[r,t?b(n,t):n,i,n]):n}),E=function(t){var n=this._yuievt,r;n||(n=this._yuievt={events:{},targets:null,config:{host:this,context:this},chain:e.config.chain}),r=n.config,t&&(h(r,t,!0),t.chain!==undefined&&(n.chain=t.chain),t.prefix&&(r.prefix=t.prefix))};E.prototype={constructor:E,once:function(){var e=this.on.apply(this,arguments);return e.batch(function(e){e.sub&&(e.sub.once=!0)}),e},onceAfter:function(){var e=this.after.apply(this,arguments);return e.batch(function(e){e.sub&&(e.sub.once=!0)}),e},parseType:function(e,t){return w(e,t||this._yuievt.config.prefix)},on:function(t,n,r){var i=this._yuievt,s=w(t,i.config.prefix),o,u,a,l,c,h,d,v=e.Env.evt.handles,g,y,b,E=e.Node,S,x,T;this._monitor("attach",s[1],{args:arguments,category:s[0],after:s[2]});if(p.isObject(t))return p.isFunction(t)?e.Do.before.apply(e.Do,arguments):(o=n,u=r,a=f.call(arguments,0),l=[],p.isArray(t)&&(T=!0),g=t._after,delete t._after,e.each(t,function(e,t){p.isObject(e)&&(o=e.fn||(p.isFunction(e)?e:o),u=e.context||u);var n=g?m:"";a[0]=n+(T?e:t),a[1]=o,a[2]=u,l.push(this.on.apply(this,a))},this),i.chain?this:new e.EventHandle(l));h=s[0],g=s[2],b=s[3];if(E&&e.instanceOf(this,E)&&b in E.DOM_EVENTS)return a=f.call(arguments,0),a.splice(2,0,E.getDOMNode(this)),e.on.apply(e,a);t=s[1];if(e.instanceOf(this,YUI)){y=e.Env.evt.plugins[t],a=f.call(arguments,0),a[0]=b,E&&(S=a[2],e.instanceOf(S,e.NodeList)?S=e.NodeList.getDOMNodes(S):e.instanceOf(S,E)&&(S=E.getDOMNode(S)),x=b in E.DOM_EVENTS,x&&(a[2]=S));if(y)d=y.on.apply(e,a);else if(!t||x)d=e.Event._attach(a)}return d||(c=i.events[t]||this.publish(t),d=c._on(n,r,arguments.length>3?f.call(arguments,3):null,g?"after":!0),t.indexOf("*:")!==-1&&(this._hasSiblings=!0)),h&&(v[h]=v[h]||{},v[h][t]=v[h][t]||[],v[h][t].push(d)),i.chain?this:d},subscribe:function(){return this.on.apply(this,arguments)},detach:function(t,n,r){var i=this._yuievt.events,s,o=e.Node,u=o&&e.instanceOf(this,o);if(!t&&this!==e){for(s in i)i.hasOwnProperty(s)&&i[s].detach(n,r);return u&&e.Event.purgeElement(o.getDOMNode(this)),this}var a=w(t,this._yuievt.config.prefix),l=p.isArray(a)?a[0]:null,c=a?a[3]:null,h,d=e.Env.evt.handles,v,m,g,y,b=function(e,t,n){var r=e[t],i,s;if(r)for(s=r.length-1;s>=0;--s)i=r[s].evt,(i.host===n||i.el===n)&&r[s].detach()};if(l){m=d[l],t=a[1],v=u?e.Node.getDOMNode(this):this;if(m){if(t)b(m,t,v);else for(s in m)m.hasOwnProperty(s)&&b(m,s,v);return this}}else{if(p.isObject(t)&&t.detach)return t.detach(),this;if(u&&(!c||c in o.DOM_EVENTS))return g=f.call(arguments,0),g[2]=o.getDOMNode(this),e.detach.apply(e,g),this}h=e.Env.evt.plugins[c];if(e.instanceOf(this,YUI)){g=f.call(arguments,0);if(h&&h.detach)return h.detach.apply(e,g),this;if(!t||!h&&o&&t in o.DOM_EVENTS)return g[0]=t,e.Event.detach.apply(e.Event,g),this}return y=i[a[1]],y&&y.detach(n,r),this},unsubscribe:function(){return this.detach.apply(this,arguments)},detachAll:function(e){return this.detach(e)},unsubscribeAll:function(){return this.detachAll.apply(this,arguments)},publish:function(t,n){var r,i=this._yuievt,s=i.config,o=s.prefix;return typeof t=="string"?(o&&(t=b(t,o)),r=this._publish(t,s,n)):(r={},e.each(t,function(e,t){o&&(t=b(t,o)),r[t]=this._publish(t,s,e||n)},this)),r},_getFullType:function(e){var t=this._yuievt.config.prefix;return t?t+d+e:e},_publish:function(t,n,r){var i,s=this._yuievt,o=s.config,u=o.host,a=o.context,f=s.events;return i=f[t],(o.monitored&&!i||i&&i.monitored)&&this._monitor("publish",t,{args:arguments}),i||(i=f[t]=new e.CustomEvent(t,n),n||(i.host=u,i.context=a)),r&&h(i,r,!0),i},_monitor:function(e,t,n){var r,i,s;if(t){typeof t=="string"?(s=t,i=this.getEvent(t,!0)):(i=t,s=t.type);if(this._yuievt.config.monitored&&(!i||i.monitored)||i&&i.monitored)r=s+"_"+e,n.monitored=e,this.fire.call(this,r,n)}},fire:function(e){var t=typeof e=="string",n=arguments.length,r=e,i=this._yuievt,s=i.config,o=s.prefix,u,a,l,c;t&&n<=3?n===2?c=[arguments[1]]:n===3?c=[arguments[1],arguments[2]]:c=[]:c=f.call(arguments,t?1:0),t||(r=e&&e.type),o&&(r=b(r,o)),a=i.events[r],this._hasSiblings&&(l=this.getSibling(r,a),l&&!a&&(a=this.publish(r))),(s.monitored&&(!a||a.monitored)||a&&a.monitored)&&this._monitor("fire",a||r,{args:c});if(!a){if(i.hasTargets)return this.bubble({type:r},c,this);u=!0}else l&&(a.sibling=l),u=a._fire(c);return i.chain?this:u},getSibling:function(e,t){var n;return e.indexOf(d)>-1&&(e=y(e),n=this.getEvent(e,!0),n&&(n.applyConfig(t),n.bubbles=!1,n.broadcast=0)),n},getEvent:function(e,t){var n,r;return t||(n=this._yuievt.config.prefix,e=n?b(e,n):e),r=this._yuievt.events,r[e]||null},after:function(t,n){var r=f.call(arguments,0);switch(p.type(t)){case"function":return e.Do.after.apply(e.Do,arguments);case"array":case"object":r[0]._after=!0;break;default:r[0]=m+t}return this.on.apply(this,r)},before:function(){return this.on.apply
+(this,arguments)}},e.EventTarget=E,e.mix(e,E.prototype),E.call(e,{bubbles:!1}),YUI.Env.globalEvents=YUI.Env.globalEvents||new E,e.Global=YUI.Env.globalEvents},"3.17.2",{requires:["oop"]});
diff --git a/js/yui3/event-custom-complex/event-custom-complex-min.js b/js/yui3/event-custom-complex/event-custom-complex-min.js
new file mode 100644
index 000000000..aeacfd072
--- /dev/null
+++ b/js/yui3/event-custom-complex/event-custom-complex-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-custom-complex",function(e,t){var n,r,i=e.Object,s,o={},u=e.CustomEvent.prototype,a=e.EventTarget.prototype,f=function(e,t){var n;for(n in t)r.hasOwnProperty(n)||(e[n]=t[n])};e.EventFacade=function(e,t){e||(e=o),this._event=e,this.details=e.details,this.type=e.type,this._type=e.type,this.target=e.target,this.currentTarget=t,this.relatedTarget=e.relatedTarget},e.mix(e.EventFacade.prototype,{stopPropagation:function(){this._event.stopPropagation(),this.stopped=1},stopImmediatePropagation:function(){this._event.stopImmediatePropagation(),this.stopped=2},preventDefault:function(){this._event.preventDefault(),this.prevented=1},halt:function(e){this._event.halt(e),this.prevented=1,this.stopped=e?2:1}}),u.fireComplex=function(t){var n,r,i,s,o,u=!0,a,f,l,c,h,p,d,v,m,g=this,y=g.host||g,b,w,E=g.stack,S=y._yuievt,x;if(E&&g.queuable&&g.type!==E.next.type)return E.queue||(E.queue=[]),E.queue.push([g,t]),!0;x=g.hasSubs()||S.hasTargets||g.broadcast,g.target=g.target||y,g.currentTarget=y,g.details=t.concat();if(x){n=E||{id:g.id,next:g,silent:g.silent,stopped:0,prevented:0,bubbling:null,type:g.type,defaultTargetOnly:g.defaultTargetOnly},f=g.getSubs(),l=f[0],c=f[1],g.stopped=g.type!==n.type?0:n.stopped,g.prevented=g.type!==n.type?0:n.prevented,g.stoppedFn&&(a=new e.EventTarget({fireOnce:!0,context:y}),g.events=a,a.on("stopped",g.stoppedFn)),g._facade=null,r=g._createFacade(t),l&&g._procSubs(l,t,r),g.bubbles&&y.bubble&&!g.stopped&&(w=n.bubbling,n.bubbling=g.type,n.type!==g.type&&(n.stopped=0,n.prevented=0),u=y.bubble(g,t,null,n),g.stopped=Math.max(g.stopped,n.stopped),g.prevented=Math.max(g.prevented,n.prevented),n.bubbling=w),d=g.prevented,d?(v=g.preventedFn,v&&v.apply(y,t)):(m=g.defaultFn,m&&(!g.defaultTargetOnly&&!n.defaultTargetOnly||y===r.target)&&m.apply(y,t)),g.broadcast&&g._broadcast(t);if(c&&!g.prevented&&g.stopped<2){h=n.afterQueue;if(n.id===g.id||g.type!==S.bubbling){g._procSubs(c,t,r);if(h)while(b=h.last())b()}else p=c,n.execDefaultCnt&&(p=e.merge(p),e.each(p,function(e){e.postponed=!0})),h||(n.afterQueue=new e.Queue),n.afterQueue.add(function(){g._procSubs(p,t,r)})}g.target=null;if(n.id===g.id){s=n.queue;if(s)while(s.length)i=s.pop(),o=i[0],n.next=o,o._fire(i[1]);g.stack=null}u=!g.stopped,g.type!==S.bubbling&&(n.stopped=0,n.prevented=0,g.stopped=0,g.prevented=0)}else m=g.defaultFn,m&&(r=g._createFacade(t),(!g.defaultTargetOnly||y===r.target)&&m.apply(y,t));return g._facade=null,u},u._hasPotentialSubscribers=function(){return this.hasSubs()||this.host._yuievt.hasTargets||this.broadcast},u._createFacade=u._getFacade=function(t){var n=this.details,r=n&&n[0],i=r&&typeof r=="object",s=this._facade;return s||(s=new e.EventFacade(this,this.currentTarget)),i?(f(s,r),r.type&&(s.type=r.type),t&&(t[0]=s)):t&&t.unshift(s),s.details=this.details,s.target=this.originalTarget||this.target,s.currentTarget=this.currentTarget,s.stopped=0,s.prevented=0,this._facade=s,this._facade},u._addFacadeToArgs=function(e){var t=e[0];t&&t.halt&&t.stopImmediatePropagation&&t.stopPropagation&&t._event||this._createFacade(e)},u.stopPropagation=function(){this.stopped=1,this.stack&&(this.stack.stopped=1),this.events&&this.events.fire("stopped",this)},u.stopImmediatePropagation=function(){this.stopped=2,this.stack&&(this.stack.stopped=2),this.events&&this.events.fire("stopped",this)},u.preventDefault=function(){this.preventable&&(this.prevented=1,this.stack&&(this.stack.prevented=1))},u.halt=function(e){e?this.stopImmediatePropagation():this.stopPropagation(),this.preventDefault()},a.addTarget=function(t){var n=this._yuievt;return n.targets||(n.targets={}),n.targets[e.stamp(t)]=t,n.hasTargets=!0,this},a.getTargets=function(){var e=this._yuievt.targets;return e?i.values(e):[]},a.removeTarget=function(t){var n=this._yuievt.targets;return n&&(delete n[e.stamp(t,!0)],i.size(n)===0&&(this._yuievt.hasTargets=!1)),this},a.bubble=function(e,t,n,r){var i=this._yuievt.targets,s=!0,o,u,a,f,l,c=e&&e.type,h=n||e&&e.target||this,p;if(!e||!e.stopped&&i)for(a in i)if(i.hasOwnProperty(a)){o=i[a],u=o._yuievt.events[c],o._hasSiblings&&(l=o.getSibling(c,u)),l&&!u&&(u=o.publish(c)),p=o._yuievt.bubbling,o._yuievt.bubbling=c;if(!u)o._yuievt.hasTargets&&o.bubble(e,t,h,r);else{l&&(u.sibling=l),u.target=h,u.originalTarget=h,u.currentTarget=o,f=u.broadcast,u.broadcast=!1,u.emitFacade=!0,u.stack=r,s=s&&u.fire.apply(u,t||e.details||[]),u.broadcast=f,u.originalTarget=null;if(u.stopped)break}o._yuievt.bubbling=p}return s},a._hasPotentialSubscribers=function(e){var t=this._yuievt,n=t.events[e];return n?n.hasSubs()||t.hasTargets||n.broadcast:!1},n=new e.EventFacade,r={};for(s in n)r[s]=!0},"3.17.2",{requires:["event-custom-base"]});
diff --git a/js/yui3/event-delegate/event-delegate-min.js b/js/yui3/event-delegate/event-delegate-min.js
new file mode 100644
index 000000000..a21382a5e
--- /dev/null
+++ b/js/yui3/event-delegate/event-delegate-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-delegate",function(e,t){function f(t,r,u,l){var c=n(arguments,0,!0),h=i(u)?u:null,p,d,v,m,g,y,b,w,E;if(s(t)){w=[];if(o(t))for(y=0,b=t.length;y<b;++y)c[0]=t[y],w.push(e.delegate.apply(e,c));else{c.unshift(null);for(y in t)t.hasOwnProperty(y)&&(c[0]=y,c[1]=t[y],w.push(e.delegate.apply(e,c)))}return new e.EventHandle(w)}p=t.split(/\|/),p.length>1&&(g=p.shift(),c[0]=t=p.shift()),d=e.Node.DOM_EVENTS[t],s(d)&&d.delegate&&(E=d.delegate.apply(d,arguments));if(!E){if(!t||!r||!u||!l)return;v=h?e.Selector.query(h,null,!0):u,!v&&i(u)&&(E=e.on("available",function(){e.mix(E,e.delegate.apply(e,c),!0)},u)),!E&&v&&(c.splice(2,2,v),E=e.Event._attach(c,{facade:!1}),E.sub.filter=l,E.sub._notify=f.notifySub)}return E&&g&&(m=a[g]||(a[g]={}),m=m[t]||(m[t]=[]),m.push(E)),E}var n=e.Array,r=e.Lang,i=r.isString,s=r.isObject,o=r.isArray,u=e.Selector.test,a=e.Env.evt.handles;f.notifySub=function(t,r,i){r=r.slice(),this.args&&r.push.apply(r,this.args);var s=f._applyFilter(this.filter,r,i),o,u,a,l;if(s){s=n(s),o=r[0]=new e.DOMEventFacade(r[0],i.el,i),o.container=e.one(i.el);for(u=0,a=s.length;u<a&&!o.stopped;++u){o.currentTarget=e.one(s[u]),l=this.fn.apply(this.context||o.currentTarget,r);if(l===!1)break}return l}},f.compileFilter=e.cached(function(e){return function(t,n){return u(t._node,e,n.currentTarget===n.target?null:n.currentTarget._node)}}),f._disabledRE=/^(?:button|input|select|textarea)$/i,f._applyFilter=function(t,n,r){var s=n[0],o=r.el,a=s.target||s.srcElement,l=[],c=!1;a.nodeType===3&&(a=a.parentNode);if(a.disabled&&f._disabledRE.test(a.nodeName))return l;n.unshift(a);if(i(t))while(a){c=a===o,u(a,t,c?null:o)&&l.push(a);if(c)break;a=a.parentNode}else{n[0]=e.one(a),n[1]=new e.DOMEventFacade(s,o,r);while(a){t.apply(n[0],n)&&l.push(a);if(a===o)break;a=a.parentNode,n[0]=e.one(a)}n[1]=s}return l.length<=1&&(l=l[0]),n.shift(),l},e.delegate=e.Event.delegate=f},"3.17.2",{requires:["node-base"]});
diff --git a/js/yui3/event-flick/event-flick-min.js b/js/yui3/event-flick/event-flick-min.js
new file mode 100644
index 000000000..f79bd30be
--- /dev/null
+++ b/js/yui3/event-flick/event-flick-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-flick",function(e,t){var n=e.Event._GESTURE_MAP,r={start:n.start,end:n.end,move:n.move},i="start",s="end",o="move",u="ownerDocument",a="minVelocity",f="minDistance",l="preventDefault",c="_fs",h="_fsh",p="_feh",d="_fmh",v="nodeType";e.Event.define("flick",{on:function(e,t,n){var s=e.on(r[i],this._onStart,this,e,t,n);t[h]=s},detach:function(e,t,n){var r=t[h],i=t[p];r&&(r.detach(),t[h]=null),i&&(i.detach(),t[p]=null)},processArgs:function(t){var n=t.length>3?e.merge(t.splice(3,1)[0]):{};return a in n||(n[a]=this.MIN_VELOCITY),f in n||(n[f]=this.MIN_DISTANCE),l in n||(n[l]=this.PREVENT_DEFAULT),n},_onStart:function(t,n,i,a){var f=!0,l,h,m,g=i._extra.preventDefault,y=t;t.touches&&(f=t.touches.length===1,t=t.touches[0]),f&&(g&&(!g.call||g(t))&&y.preventDefault(),t.flick={time:(new Date).getTime()},i[c]=t,l=i[p],m=n.get(v)===9?n:n.get(u),l||(l=m.on(r[s],e.bind(this._onEnd,this),null,n,i,a),i[p]=l),i[d]=m.once(r[o],e.bind(this._onMove,this),null,n,i,a))},_onMove:function(e,t,n,r){var i=n[c];i&&i.flick&&(i.flick.time=(new Date).getTime())},_onEnd:function(e,t,n,r){var i=(new Date).getTime(),s=n[c],o=!!s,u=e,h,p,v,m,g,y,b,w,E=n[d];E&&(E.detach(),delete n[d]),o&&(e.changedTouches&&(e.changedTouches.length===1&&e.touches.length===0?u=e.changedTouches[0]:o=!1),o&&(m=n._extra,v=m[l],v&&(!v.call||v(e))&&e.preventDefault(),h=s.flick.time,i=(new Date).getTime(),p=i-h,g=[u.pageX-s.pageX,u.pageY-s.pageY],m.axis?w=m.axis:w=Math.abs(g[0])>=Math.abs(g[1])?"x":"y",y=g[w==="x"?0:1],b=p!==0?y/p:0,isFinite(b)&&Math.abs(y)>=m[f]&&Math.abs(b)>=m[a]&&(e.type="flick",e.flick={time:p,distance:y,velocity:b,axis:w,start:s},r.fire(e)),n[c]=null))},MIN_VELOCITY:0,MIN_DISTANCE:0,PREVENT_DEFAULT:!1})},"3.17.2",{requires:["node-base","event-touch","event-synthetic"]});
diff --git a/js/yui3/event-focus/event-focus-min.js b/js/yui3/event-focus/event-focus-min.js
new file mode 100644
index 000000000..6f1f73f43
--- /dev/null
+++ b/js/yui3/event-focus/event-focus-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-focus",function(e,t){function u(t,r,u){var a="_"+t+"Notifiers";e.Event.define(t,{_useActivate:o,_attach:function(i,s,o){return e.DOM.isWindow(i)?n._attach([t,function(e){s.fire(e)},i]):n._attach([r,this._proxy,i,this,s,o],{capture:!0})},_proxy:function(t,r,i){var s=t.target,f=t.currentTarget,l=s.getData(a),c=e.stamp(f._node),h=o||s!==f,p;r.currentTarget=i?s:f,r.container=i?f:null,l?h=!0:(l={},s.setData(a,l),h&&(p=n._attach([u,this._notify,s._node]).sub,p.once=!0)),l[c]||(l[c]=[]),l[c].push(r),h||this._notify(t)},_notify:function(t,n){var r=t.currentTarget,i=r.getData(a),o=r.ancestors(),u=r.get("ownerDocument"),f=[],l=i?e.Object.keys(i).length:0,c,h,p,d,v,m,g,y,b,w;r.clearData(a),o.push(r),u&&o.unshift(u),o._nodes.reverse(),l&&(m=l,o.some(function(t){var n=e.stamp(t),r=i[n],s,o;if(r){l--;for(s=0,o=r.length;s<o;++s)r[s].handle.sub.filter&&f.push(r[s])}return!l}),l=m);while(l&&(c=o.shift())){d=e.stamp(c),h=i[d];if(h){for(g=0,y=h.length;g<y;++g){p=h[g],b=p.handle.sub,v=!0,t.currentTarget=c,b.filter&&(v=b.filter.apply(c,[c,t].concat(b.args||[])),f.splice(s(f,p),1)),v&&(t.container=p.container,w=p.fire(t));if(w===!1||t.stopped===2)break}delete h[d],l--}if(t.stopped!==2)for(g=0,y=f.length;g<y;++g){p=f[g],b=p.handle.sub,b.filter.apply(c,[c,t].concat(b.args||[]))&&(t.container=p.container,t.currentTarget=c,w=p.fire(t));if(w===!1||t.stopped===2||t.stopped&&f[g+1]&&f[g+1].container!==p.container)break}if(t.stopped)break}},on:function(e,t,n){t.handle=this._attach(e._node,n)},detach:function(e,t){t.handle.detach()},delegate:function(t,n,r,s){i(s)&&(n.filter=function(n){return e.Selector.test(n._node,s,t===n?null:t._node)}),n.handle=this._attach(t._node,r,!0)},detachDelegate:function(e,t){t.handle.detach()}},!0)}var n=e.Event,r=e.Lang,i=r.isString,s=e.Array.indexOf,o=function(){var t=!1,n=e.config.doc,r;return n&&(r=n.createElement("p"),r.setAttribute("onbeforeactivate",";"),t=r.onbeforeactivate!==undefined),t}();o?(u("focus","beforeactivate","focusin"),u("blur","beforedeactivate","focusout")):(u("focus","focus","focus"),u("blur","blur","blur"))},"3.17.2",{requires:["event-synthetic"]});
diff --git a/js/yui3/event-hover/event-hover-min.js b/js/yui3/event-hover/event-hover-min.js
new file mode 100644
index 000000000..0c547f678
--- /dev/null
+++ b/js/yui3/event-hover/event-hover-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-hover",function(e,t){var n=e.Lang.isFunction,r=function(){},i={processArgs:function(e){var t=n(e[2])?2:3;return n(e[t])?e.splice(t,1)[0]:r},on:function(e,t,n,r){var i=t.args?t.args.slice():[];i.unshift(null),t._detach=e[r?"delegate":"on"]({mouseenter:function(e){e.phase="over",n.fire(e)},mouseleave:function(e){var n=t.context||this;i[0]=e,e.type="hover",e.phase="out",t._extra.apply(n,i)}},r)},detach:function(e,t,n){t._detach.detach()}};i.delegate=i.on,i.detachDelegate=i.detach,e.Event.define("hover",i)},"3.17.2",{requires:["event-mouseenter"]});
diff --git a/js/yui3/event-key/event-key-min.js b/js/yui3/event-key/event-key-min.js
new file mode 100644
index 000000000..2c8b5cb29
--- /dev/null
+++ b/js/yui3/event-key/event-key-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-key",function(e,t){var n="+alt",r="+ctrl",i="+meta",s="+shift",o=e.Lang.trim,u={KEY_MAP:{enter:13,space:32,esc:27,backspace:8,tab:9,pageup:33,pagedown:34},_typeRE:/^(up|down|press):/,_keysRE:/^(?:up|down|press):|\+(alt|ctrl|meta|shift)/g,processArgs:function(t){var n=t.splice(3,1)[0],r=e.Array.hash(n.match(/\+(?:alt|ctrl|meta|shift)\b/g)||[]),i={type:this._typeRE.test(n)?RegExp.$1:null,mods:r,keys:null},s=n.replace(this._keysRE,""),u,a,f,l;if(s){s=s.split(","),i.keys={};for(l=s.length-1;l>=0;--l){u=o(s[l]);if(!u)continue;+u==u?i.keys[u]=r:(f=u.toLowerCase(),this.KEY_MAP[f]?(i.keys[this.KEY_MAP[f]]=r,i.type||(i.type="down")):(u=u.charAt(0),a=u.toUpperCase(),r["+shift"]&&(u=a),i.keys[u.charCodeAt(0)]=u===a?e.merge(r,{"+shift":!0}):r))}}return i.type||(i.type="press"),i},on:function(e,t,o,u){var a=t._extra,f="key"+a.type,l=a.keys,c=u?"delegate":"on";t._detach=e[c](f,function(e){var t=l?l[e.which]:a.mods;t&&(!t[n]||t[n]&&e.altKey)&&(!t[r]||t[r]&&e.ctrlKey)&&(!t[i]||t[i]&&e.metaKey)&&(!t[s]||t[s]&&e.shiftKey)&&o.fire(e)},u)},detach:function(e,t,n){t._detach.detach()}};u.delegate=u.on,u.detachDelegate=u.detach,e.Event.define("key",u,!0)},"3.17.2",{requires:["event-synthetic"]});
diff --git a/js/yui3/event-mouseenter/event-mouseenter-min.js b/js/yui3/event-mouseenter/event-mouseenter-min.js
new file mode 100644
index 000000000..b0d1006b8
--- /dev/null
+++ b/js/yui3/event-mouseenter/event-mouseenter-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-mouseenter",function(e,t){var n=e.Env.evt.dom_wrappers,r=e.DOM.contains,i=e.Array,s=function(){},o={proxyType:"mouseover",relProperty:"fromElement",_notify:function(t,i,s){var o=this._node,u=t.relatedTarget||t[i];o!==u&&!r(o,u)&&s.fire(new e.DOMEventFacade(t,o,n["event:"+e.stamp(o)+t.type]))},on:function(t,n,r){var i=e.Node.getDOMNode(t),s=[this.proxyType,this._notify,i,null,this.relProperty,r];n.handle=e.Event._attach(s,{facade:!1})},detach:function(e,t){t.handle.detach()},delegate:function(t,n,r,i){var o=e.Node.getDOMNode(t),u=[this.proxyType,s,o,null,r];n.handle=e.Event._attach(u,{facade:!1}),n.handle.sub.filter=i,n.handle.sub.relProperty=this.relProperty,n.handle.sub._notify=this._filterNotify},_filterNotify:function(t,n,s){n=n.slice(),this.args&&n.push.apply(n,this.args);var o=e.delegate._applyFilter(this.filter,n,s),u=n[0].relatedTarget||n[0][this.relProperty],a,f,l,c,h;if(o){o=i(o);for(f=0,l=o.length&&(!a||!a.stopped);f<l;++f){h=o[0];if(!r(h,u)){a||(a=new e.DOMEventFacade(n[0],h,s),a.container=e.one(s.el)),a.currentTarget=e.one(h),c=n[1].fire(a);if(c===!1)break}}}return c},detachDelegate:function(e,t){t.handle.detach()}};e.Event.define("mouseenter",o,!0),e.Event.define("mouseleave",e.merge(o,{proxyType:"mouseout",relProperty:"toElement"}),!0)},"3.17.2",{requires:["event-synthetic"]});
diff --git a/js/yui3/event-mousewheel/event-mousewheel-min.js b/js/yui3/event-mousewheel/event-mousewheel-min.js
new file mode 100644
index 000000000..94729ecad
--- /dev/null
+++ b/js/yui3/event-mousewheel/event-mousewheel-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-mousewheel",function(e,t){var n="DOMMouseScroll",r=function(t){var r=e.Array(t,0,!0),i;return e.UA.gecko?(r[0]=n,i=e.config.win):i=e.config.doc,r.length<3?r[2]=i:r.splice(2,0,i),r};e.Env.evt.plugins.mousewheel={on:function(){return e.Event._attach(r(arguments))},detach:function(){return e.Event.detach.apply(e.Event,r(arguments))}}},"3.17.2",{requires:["node-base"]});
diff --git a/js/yui3/event-move/event-move-min.js b/js/yui3/event-move/event-move-min.js
new file mode 100644
index 000000000..2a175595a
--- /dev/null
+++ b/js/yui3/event-move/event-move-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-move",function(e,t){var n=e.Event._GESTURE_MAP,r={start:n.start,end:n.end,move:n.move},i="start",s="move",o="end",u="gesture"+s,a=u+o,f=u+i,l="_msh",c="_mh",h="_meh",p="_dmsh",d="_dmh",v="_dmeh",m="_ms",g="_m",y="minTime",b="minDistance",w="preventDefault",E="button",S="ownerDocument",x="currentTarget",T="target",N="nodeType",C=e.config.win&&"msPointerEnabled"in e.config.win.navigator,k="msTouchActionCount",L="msInitTouchAction",A=function(t,n,r){var i=r?4:3,s=n.length>i?e.merge(n.splice(i,1)[0]):{};return w in s||(s[w]=t.PREVENT_DEFAULT),s},O=function(e,t){return t._extra.root||e.get(N)===9?e:e.get(S)},M=function(t){var n=t.getDOMNode();return t.compareTo(e.config.doc)&&n.documentElement?n.documentElement:!1},_=function(e,t,n){e.pageX=t.pageX,e.pageY=t.pageY,e.screenX=t.screenX,e.screenY=t.screenY,e.clientX=t.clientX,e.clientY=t.clientY,e[T]=e[T]||t[T],e[x]=e[x]||t[x],e[E]=n&&n[E]||1},D=function(t){var n=M(t)||t.getDOMNode(),r=t.getData(k);C&&(r||(r=0,t.setData(L,n.style.msTouchAction)),n.style.msTouchAction=e.Event._DEFAULT_TOUCH_ACTION,r++,t.setData(k,r))},P=function(e){var t=M(e)||e.getDOMNode(),n=e.getData(k),r=e.getData(L);C&&(n--,e.setData(k,n),n===0&&t.style.msTouchAction!==r&&(t.style.msTouchAction=r))},H=function(e,t){t&&(!t.call||t(e))&&e.preventDefault()},B=e.Event.define;e.Event._DEFAULT_TOUCH_ACTION="none",B(f,{on:function(e,t,n){D(e),t[l]=e.on(r[i],this._onStart,this,e,t,n)},delegate:function(e,t,n,s){var o=this;t[p]=e.delegate(r[i],function(r){o._onStart(r,e,t,n,!0)},s)},detachDelegate:function(e,t,n,r){var i=t[p];i&&(i.detach(),t[p]=null),P(e)},detach:function(e,t,n){var r=t[l];r&&(r.detach(),t[l]=null),P(e)},processArgs:function(e,t){var n=A(this,e,t);return y in n||(n[y]=this.MIN_TIME),b in n||(n[b]=this.MIN_DISTANCE),n},_onStart:function(t,n,i,u,a){a&&(n=t[x]);var f=i._extra,l=!0,c=f[y],h=f[b],p=f.button,d=f[w],v=O(n,i),m;t.touches?t.touches.length===1?_(t,t.touches[0],f):l=!1:l=p===undefined||p===t.button,l&&(H(t,d),c===0||h===0?this._start(t,n,u,f):(m=[t.pageX,t.pageY],c>0&&(f._ht=e.later(c,this,this._start,[t,n,u,f]),f._hme=v.on(r[o],e.bind(function(){this._cancel(f)},this))),h>0&&(f._hm=v.on(r[s],e.bind(function(e){(Math.abs(e.pageX-m[0])>h||Math.abs(e.pageY-m[1])>h)&&this._start(t,n,u,f)},this)))))},_cancel:function(e){e._ht&&(e._ht.cancel(),e._ht=null),e._hme&&(e._hme.detach(),e._hme=null),e._hm&&(e._hm.detach(),e._hm=null)},_start:function(e,t,n,r){r&&this._cancel(r),e.type=f,t.setData(m,e),n.fire(e)},MIN_TIME:0,MIN_DISTANCE:0,PREVENT_DEFAULT:!1}),B(u,{on:function(e,t,n){D(e);var i=O(e,t,r[s]),o=i.on(r[s],this._onMove,this,e,t,n);t[c]=o},delegate:function(e,t,n,i){var o=this;t[d]=e.delegate(r[s],function(r){o._onMove(r,e,t,n,!0)},i)},detach:function(e,t,n){var r=t[c];r&&(r.detach(),t[c]=null),P(e)},detachDelegate:function(e,t,n,r){var i=t[d];i&&(i.detach(),t[d]=null),P(e)},processArgs:function(e,t){return A(this,e,t)},_onMove:function(e,t,n,r,i){i&&(t=e[x]);var s=n._extra.standAlone||t.getData(m),o=n._extra.preventDefault;s&&(e.touches&&(e.touches.length===1?_(e,e.touches[0]):s=!1),s&&(H(e,o),e.type=u,r.fire(e)))},PREVENT_DEFAULT:!1}),B(a,{on:function(e,t,n){D(e);var i=O(e,t),s=i.on(r[o],this._onEnd,this,e,t,n);t[h]=s},delegate:function(e,t,n,i){var s=this;t[v]=e.delegate(r[o],function(r){s._onEnd(r,e,t,n,!0)},i)},detachDelegate:function(e,t,n,r){var i=t[v];i&&(i.detach(),t[v]=null),P(e)},detach:function(e,t,n){var r=t[h];r&&(r.detach(),t[h]=null),P(e)},processArgs:function(e,t){return A(this,e,t)},_onEnd:function(e,t,n,r,i){i&&(t=e[x]);var s=n._extra.standAlone||t.getData(g)||t.getData(m),o=n._extra.preventDefault;s&&(e.changedTouches&&(e.changedTouches.length===1?_(e,e.changedTouches[0]):s=!1),s&&(H(e,o),e.type=a,r.fire(e),t.clearData(m),t.clearData(g)))},PREVENT_DEFAULT:!1})},"3.17.2",{requires:["node-base","event-touch","event-synthetic"]});
diff --git a/js/yui3/event-outside/event-outside-min.js b/js/yui3/event-outside/event-outside-min.js
new file mode 100644
index 000000000..8e61fb7b6
--- /dev/null
+++ b/js/yui3/event-outside/event-outside-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-outside",function(e,t){var n=["blur","change","click","dblclick","focus","keydown","keypress","keyup","mousedown","mousemove","mouseout","mouseover","mouseup","select","submit"];e.Event.defineOutside=function(t,n){n=n||t+"outside";var r={on:function(n,r,i){r.handle=e.one("doc").on(t,function(e){this.isOutside(n,e.target)&&(e.currentTarget=n,i.fire(e))},this)},detach:function(e,t,n){t.handle.detach()},delegate:function(n,r,i,s){r.handle=e.one("doc").delegate(t,function(e){this.isOutside(n,e.target)&&i.fire(e)},s,this)},isOutside:function(e,t){return t!==e&&!t.ancestor(function(t){return t===e})}};r.detachDelegate=r.detach,e.Event.define(n,r)},e.Array.each(n,function(t){e.Event.defineOutside(t)})},"3.17.2",{requires:["event-synthetic"]});
diff --git a/js/yui3/event-resize/event-resize-min.js b/js/yui3/event-resize/event-resize-min.js
new file mode 100644
index 000000000..d8a9fa45c
--- /dev/null
+++ b/js/yui3/event-resize/event-resize-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-resize",function(e,t){e.Event.define("windowresize",{on:e.UA.gecko&&e.UA.gecko<1.91?function(t,n,r){n._handle=e.Event.attach("resize",function(e){r.fire(e)})}:function(t,n,r){var i=e.config.windowResizeDelay||100;n._handle=e.Event.attach("resize",function(t){n._timer&&n._timer.cancel(),n._timer=e.later(i,e,function(){r.fire(t)})})},detach:function(e,t){t._timer&&t._timer.cancel(),t._handle.detach()}})},"3.17.2",{requires:["node-base","event-synthetic"]});
diff --git a/js/yui3/event-simulate/event-simulate-min.js b/js/yui3/event-simulate/event-simulate-min.js
new file mode 100644
index 000000000..c2573b5b5
--- /dev/null
+++ b/js/yui3/event-simulate/event-simulate-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-simulate",function(e,t){(function(){function v(t,n,a,f,l,c,h,p,d,v,m){t||e.error("simulateKeyEvent(): Invalid target.");if(i(n)){n=n.toLowerCase();switch(n){case"textevent":n="keypress";break;case"keyup":case"keydown":case"keypress":break;default:e.error("simulateKeyEvent(): Event type '"+n+"' not supported.")}}else e.error("simulateKeyEvent(): Event type must be a string.");s(a)||(a=!0),s(f)||(f=!0),o(l)||(l=e.config.win),s(c)||(c=!1),s(h)||(h=!1),s(p)||(p=!1),s(d)||(d=!1),u(v)||(v=0),u(m)||(m=0);var g=null;if(r(e.config.doc.createEvent)){try{g=e.config.doc.createEvent("KeyEvents"),g.initKeyEvent(n,a,f,l,c,h,p,d,v,m)}catch(y){try{g=e.config.doc.createEvent("Events")}catch(b){g=e.config.doc.createEvent("UIEvents")}finally{g.initEvent(n,a,f),g.view=l,g.altKey=h,g.ctrlKey=c,g.shiftKey=p,g.metaKey=d,g.keyCode=v,g.charCode=m}}t.dispatchEvent(g)}else o(e.config.doc.createEventObject)?(g=e.config.doc.createEventObject(),g.bubbles=a,g.cancelable=f,g.view=l,g.ctrlKey=c,g.altKey=h,g.shiftKey=p,g.metaKey=d,g.keyCode=m>0?m:v,t.fireEvent("on"+n,g)):e.error("simulateKeyEvent(): No event simulation framework present.")}function m(t,n,l,c,h,p,d,v,m,g,y,b,w,E,S,x){t||e.error("simulateMouseEvent(): Invalid target."),i(n)?!a[n.toLowerCase()]&&!f[n]&&e.error("simulateMouseEvent(): Event type '"+n+"' not supported."):e.error("simulateMouseEvent(): Event type must be a string."),s(l)||(l=!0),s(c)||(c=n!=="mousemove"),o(h)||(h=e.config.win),u(p)||(p=1),u(d)||(d=0),u(v)||(v=0),u(m)||(m=0),u(g)||(g=0),s(y)||(y=!1),s(b)||(b=!1),s(w)||(w=!1),s(E)||(E=!1),u(S)||(S=0),x=x||null;var T=null;if(r(e.config.doc.createEvent))T=e.config.doc.createEvent("MouseEvents"),T.initMouseEvent?T.initMouseEvent(n,l,c,h,p,d,v,m,g,y,b,w,E,S,x):(T=e.config.doc.createEvent("UIEvents"),T.initEvent(n,l,c),T.view=h,T.detail=p,T.screenX=d,T.screenY=v,T.clientX=m,T.clientY=g,T.ctrlKey=y,T.altKey=b,T.metaKey=E,T.shiftKey=w,T.button=S,T.relatedTarget=x),x&&!T.relatedTarget&&(n==="mouseout"?T.toElement=x:n==="mouseover"&&(T.fromElement=x)),t.dispatchEvent(T);else if(o(e.config.doc.createEventObject)){T=e.config.doc.createEventObject(),T.bubbles=l,T.cancelable=c,T.view=h,T.detail=p,T.screenX=d,T.screenY=v,T.clientX=m,T.clientY=g,T.ctrlKey=y,T.altKey=b,T.metaKey=E,T.shiftKey=w;switch(S){case 0:T.button=1;break;case 1:T.button=4;break;case 2:break;default:T.button=0}T.relatedTarget=x,t.fireEvent("on"+n,T)}else e.error("simulateMouseEvent(): No event simulation framework present.")}function g(t,n,a,f,l,p){t||e.error("simulateUIEvent(): Invalid target."),i(n)?(n=n.toLowerCase(),c[n]||e.error("simulateUIEvent(): Event type '"+n+"' not supported.")):e.error("simulateUIEvent(): Event type must be a string.");var d=null;s(a)||(a=n in h),s(f)||(f=n==="submit"),o(l)||(l=e.config.win),u(p)||(p=1),r(e.config.doc.createEvent)?(d=e.config.doc.createEvent("UIEvents"),d.initUIEvent(n,a,f,l,p),t.dispatchEvent(d)):o(e.config.doc.createEventObject)?(d=e.config.doc.createEventObject(),d.bubbles=a,d.cancelable=f,d.view=l,d.detail=p,t.fireEvent("on"+n,d)):e.error("simulateUIEvent(): No event simulation framework present.")}function y(t,n,r,i,s,o,u,a,f,l,c,h,p,v,m,g){var y;(!e.UA.ios||e.UA.ios<2)&&e.error("simulateGestureEvent(): Native gesture DOM eventframe is not available in this platform."),t||e.error("simulateGestureEvent(): Invalid target."),e.Lang.isString(n)?(n=n.toLowerCase(),d[n]||e.error("simulateTouchEvent(): Event type '"+n+"' not supported.")):e.error("simulateGestureEvent(): Event type must be a string."),e.Lang.isBoolean(r)||(r=!0),e.Lang.isBoolean(i)||(i=!0),e.Lang.isObject(s)||(s=e.config.win),e.Lang.isNumber(o)||(o=2),e.Lang.isNumber(u)||(u=0),e.Lang.isNumber(a)||(a=0),e.Lang.isNumber(f)||(f=0),e.Lang.isNumber(l)||(l=0),e.Lang.isBoolean(c)||(c=!1),e.Lang.isBoolean(h)||(h=!1),e.Lang.isBoolean(p)||(p=!1),e.Lang.isBoolean(v)||(v=!1),e.Lang.isNumber(m)||(m=1),e.Lang.isNumber(g)||(g=0),y=e.config.doc.createEvent("GestureEvent"),y.initGestureEvent(n,r,i,s,o,u,a,f,l,c,h,p,v,t,m,g),t.dispatchEvent(y)}function b(t,n,r,i,s,o,u,a,f,l,c,h,d,v,m,g,y,b,w){var E;t||e.error("simulateTouchEvent(): Invalid target."),e.Lang.isString(n)?(n=n.toLowerCase(),p[n]||e.error("simulateTouchEvent(): Event type '"+n+"' not supported.")):e.error("simulateTouchEvent(): Event type must be a string."),n==="touchstart"||n==="touchmove"?m.length===0&&e.error("simulateTouchEvent(): No touch object in touches"):n==="touchend"&&y.length===0&&e.error("simulateTouchEvent(): No touch object in changedTouches"),e.Lang.isBoolean(r)||(r=!0),e.Lang.isBoolean(i)||(i=n!=="touchcancel"),e.Lang.isObject(s)||(s=e.config.win),e.Lang.isNumber(o)||(o=1),e.Lang.isNumber(u)||(u=0),e.Lang.isNumber(a)||(a=0),e.Lang.isNumber(f)||(f=0),e.Lang.isNumber(l)||(l=0),e.Lang.isBoolean(c)||(c=!1),e.Lang.isBoolean(h)||(h=!1),e.Lang.isBoolean(d)||(d=!1),e.Lang.isBoolean(v)||(v=!1),e.Lang.isNumber(b)||(b=1),e.Lang.isNumber(w)||(w=0),e.Lang.isFunction(e.config.doc.createEvent)?(e.UA.android?e.UA.android<4?(E=e.config.doc.createEvent("MouseEvents"),E.initMouseEvent(n,r,i,s,o,u,a,f,l,c,h,d,v,0,t),E.touches=m,E.targetTouches=g,E.changedTouches=y):(E=e.config.doc.createEvent("TouchEvent"),E.initTouchEvent(m,g,y,n,s,u,a,f,l,c,h,d,v)):e.UA.ios?e.UA.ios>=2?(E=e.config.doc.createEvent("TouchEvent"),E.initTouchEvent(n,r,i,s,o,u,a,f,l,c,h,d,v,m,g,y,b,w)):e.error("simulateTouchEvent(): No touch event simulation framework present for iOS, "+e.UA.ios+"."):e.error("simulateTouchEvent(): Not supported agent yet, "+e.UA.userAgent),t.dispatchEvent(E)):e.error("simulateTouchEvent(): No event simulation framework present.")}var t=e.Lang,n=e.config.win,r=t.isFunction,i=t.isString,s=t.isBoolean,o=t.isObject,u=t.isNumber,a={click:1,dblclick:1,mouseover:1,mouseout:1,mousedown:1,mouseup:1,mousemove:1,contextmenu:1},f=n&&n.PointerEvent?{pointerover:1,pointerout:1,pointerdown:1,pointerup:1,pointermove:1}:{MSPointerOver:1,MSPointerOut:1,MSPointerDown:1,MSPointerUp:1,MSPointerMove:1},l={keydown:1,keyup:1,keypress:1},c={submit:1,blur
+:1,change:1,focus:1,resize:1,scroll:1,select:1},h={scroll:1,resize:1,reset:1,submit:1,change:1,select:1,error:1,abort:1},p={touchstart:1,touchmove:1,touchend:1,touchcancel:1},d={gesturestart:1,gesturechange:1,gestureend:1};e.mix(h,a),e.mix(h,l),e.mix(h,p),e.Event.simulate=function(t,n,r){r=r||{},a[n]||f[n]?m(t,n,r.bubbles,r.cancelable,r.view,r.detail,r.screenX,r.screenY,r.clientX,r.clientY,r.ctrlKey,r.altKey,r.shiftKey,r.metaKey,r.button,r.relatedTarget):l[n]?v(t,n,r.bubbles,r.cancelable,r.view,r.ctrlKey,r.altKey,r.shiftKey,r.metaKey,r.keyCode,r.charCode):c[n]?g(t,n,r.bubbles,r.cancelable,r.view,r.detail):p[n]?e.config.win&&"ontouchstart"in e.config.win&&!e.UA.phantomjs&&!(e.UA.chrome&&e.UA.chrome<6)?b(t,n,r.bubbles,r.cancelable,r.view,r.detail,r.screenX,r.screenY,r.clientX,r.clientY,r.ctrlKey,r.altKey,r.shiftKey,r.metaKey,r.touches,r.targetTouches,r.changedTouches,r.scale,r.rotation):e.error("simulate(): Event '"+n+"' can't be simulated. Use gesture-simulate module instead."):e.UA.ios&&e.UA.ios>=2&&d[n]?y(t,n,r.bubbles,r.cancelable,r.view,r.detail,r.screenX,r.screenY,r.clientX,r.clientY,r.ctrlKey,r.altKey,r.shiftKey,r.metaKey,r.scale,r.rotation):e.error("simulate(): Event '"+n+"' can't be simulated.")}})()},"3.17.2",{requires:["event-base"]});
diff --git a/js/yui3/event-synthetic/event-synthetic-min.js b/js/yui3/event-synthetic/event-synthetic-min.js
new file mode 100644
index 000000000..aeca00886
--- /dev/null
+++ b/js/yui3/event-synthetic/event-synthetic-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-synthetic",function(e,t){function c(e,t){this.handle=e,this.emitFacade=t}function h(e,t,n){this.handles=[],this.el=e,this.key=n,this.domkey=t}function p(){this._init.apply(this,arguments)}var n=e.CustomEvent,r=e.Env.evt.dom_map,i=e.Array,s=e.Lang,o=s.isObject,u=s.isString,a=s.isArray,f=e.Selector.query,l=function(){};c.prototype.fire=function(t){var n=i(arguments,0,!0),r=this.handle,s=r.evt,u=r.sub,a=u.context,f=u.filter,l=t||{},c;if(this.emitFacade){if(!t||!t.preventDefault)l=s._getFacade(),o(t)&&!t.preventDefault?(e.mix(l,t,!0),n[0]=l):n.unshift(l);l.type=s.type,l.details=n.slice(),f&&(l.container=s.host)}else f&&o(t)&&t.currentTarget&&n.shift();return u.context=a||l.currentTarget||s.host,c=s.fire.apply(s,n),t.prevented&&s.preventedFn&&s.preventedFn.apply(s,n),t.stopped&&s.stoppedFn&&s.stoppedFn.apply(s,n),u.context=a,c},h.prototype={constructor:h,type:"_synth",fn:l,capture:!1,register:function(e){e.evt.registry=this,this.handles.push(e)},unregister:function(t){var n=this.handles,i=r[this.domkey],s;for(s=n.length-1;s>=0;--s)if(n[s].sub===t){n.splice(s,1);break}n.length||(delete i[this.key],e.Object.size(i)||delete r[this.domkey])},detachAll:function(){var e=this.handles,t=e.length;while(--t>=0)e[t].detach()}},e.mix(p,{Notifier:c,SynthRegistry:h,getRegistry:function(t,n,i){var s=t._node,o=e.stamp(s),u="event:"+o+n+"_synth",a=r[o];return i&&(a||(a=r[o]={}),a[u]||(a[u]=new h(s,o,u))),a&&a[u]||null},_deleteSub:function(e){if(e&&e.fn){var t=this.eventDef,r=e.filter?"detachDelegate":"detach";this._subscribers=[],n.keepDeprecatedSubs&&(this.subscribers={}),t[r](e.node,e,this.notifier,e.filter),this.registry.unregister(e),delete e.fn,delete e.node,delete e.context}},prototype:{constructor:p,_init:function(){var e=this.publishConfig||(this.publishConfig={});this.emitFacade="emitFacade"in e?e.emitFacade:!0,e.emitFacade=!1},processArgs:l,on:l,detach:l,delegate:l,detachDelegate:l,_on:function(t,n){var r=[],s=t.slice(),o=this.processArgs(t,n),a=t[2],l=n?"delegate":"on",c,h;return c=u(a)?f(a):i(a||e.one(e.config.win)),!c.length&&u(a)?(h=e.on("available",function(){e.mix(h,e[l].apply(e,s),!0)},a),h):(e.Array.each(c,function(i){var s=t.slice(),u;i=e.one(i),i&&(n&&(u=s.splice(3,1)[0]),s.splice(0,4,s[1],s[3]),(!this.preventDups||!this.getSubs(i,t,null,!0))&&r.push(this._subscribe(i,l,s,o,u)))},this),r.length===1?r[0]:new e.EventHandle(r))},_subscribe:function(t,n,r,i,s){var o=new e.CustomEvent(this.type,this.publishConfig),u=o.on.apply(o,r),a=new c(u,this.emitFacade),f=p.getRegistry(t,this.type,!0),l=u.sub;return l.node=t,l.filter=s,i&&this.applyArgExtras(i,l),e.mix(o,{eventDef:this,notifier:a,host:t,currentTarget:t,target:t,el:t._node,_delete:p._deleteSub},!0),u.notifier=a,f.register(u),this[n](t,l,a,s),u},applyArgExtras:function(e,t){t._extra=e},_detach:function(t){var n=t[2],r=u(n)?f(n):i(n),s,o,a,l,c;t.splice(2,1);for(o=0,a=r.length;o<a;++o){s=e.one(r[o]);if(s){l=this.getSubs(s,t);if(l)for(c=l.length-1;c>=0;--c)l[c].detach()}}},getSubs:function(e,t,n,r){var i=p.getRegistry(e,this.type),s=[],o,u,a,f;if(i){o=i.handles,n||(n=this.subMatch);for(u=0,a=o.length;u<a;++u){f=o[u];if(n.call(this,f.sub,t)){if(r)return f;s.push(o[u])}}}return s.length&&s},subMatch:function(e,t){return!t[1]||e.fn===t[1]}}},!0),e.SyntheticEvent=p,e.Event.define=function(t,n,r){var s,o,f;t&&t.type?(s=t,r=n):n&&(s=e.merge({type:t},n));if(s){if(r||!e.Node.DOM_EVENTS[s.type])o=function(){p.apply(this,arguments)},e.extend(o,p,s),f=new o,t=f.type,e.Node.DOM_EVENTS[t]=e.Env.evt.plugins[t]={eventDef:f,on:function(){return f._on(i(arguments))},delegate:function(){return f._on(i(arguments),!0)},detach:function(){return f._detach(i(arguments))}}}else(u(t)||a(t))&&e.Array.each(i(t),function(t){e.Node.DOM_EVENTS[t]=1});return f}},"3.17.2",{requires:["node-base","event-custom-complex"]});
diff --git a/js/yui3/event-tap/event-tap-min.js b/js/yui3/event-tap/event-tap-min.js
new file mode 100644
index 000000000..af59caaed
--- /dev/null
+++ b/js/yui3/event-tap/event-tap-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-tap",function(e,t){function a(t,n){n=n||e.Object.values(u),e.Array.each(n,function(e){var n=t[e];n&&(n.detach(),t[e]=null)})}var n=e.config.doc,r=e.Event._GESTURE_MAP,i=r.start,s="tap",o=/pointer/i,u={START:"Y_TAP_ON_START_HANDLE",END:"Y_TAP_ON_END_HANDLE",CANCEL:"Y_TAP_ON_CANCEL_HANDLE"};e.Event.define(s,{publishConfig:{preventedFn:function(e){var t=e.target.once("click",function(e){e.preventDefault()});setTimeout(function(){t.detach()},100)}},processArgs:function(e,t){if(!t){var n=e[3];return e.splice(3,1),n}},on:function(e,t,n){t[u.START]=e.on(i,this._start,this,e,t,n)},detach:function(e,t,n){a(t)},delegate:function(t,n,r,s){n[u.START]=e.delegate(i,function(e){this._start(e,t,n,r,!0)},t,s,this)},detachDelegate:function(e,t,n){a(t)},_start:function(e,t,n,i,s){var a={canceled:!1,eventType:e.type},f=n.preventMouse||!1;if(e.button&&e.button===3)return;if(e.touches&&e.touches.length!==1)return;a.node=s?e.currentTarget:t,e.touches?a.startXY=[e.touches[0].pageX,e.touches[0].pageY]:a.startXY=[e.pageX,e.pageY],e.touches?(n[u.END]=t.once("touchend",this._end,this,t,n,i,s,a),n[u.CANCEL]=t.once("touchcancel",this.detach,this,t,n,i,s,a),n.preventMouse=!0):a.eventType.indexOf("mouse")!==-1&&!f?(n[u.END]=t.once("mouseup",this._end,this,t,n,i,s,a),n[u.CANCEL]=t.once("mousecancel",this.detach,this,t,n,i,s,a)):a.eventType.indexOf("mouse")!==-1&&f?n.preventMouse=!1:o.test(a.eventType)&&(n[u.END]=t.once(r.end,this._end,this,t,n,i,s,a),n[u.CANCEL]=t.once(r.cancel,this.detach,this,t,n,i,s,a))},_end:function(e,t,n,r,i,o){var f=o.startXY,l,c,h=15;n._extra&&n._extra.sensitivity>=0&&(h=n._extra.sensitivity),e.changedTouches?(l=[e.changedTouches[0].pageX,e.changedTouches[0].pageY],c=[e.changedTouches[0].clientX,e.changedTouches[0].clientY]):(l=[e.pageX,e.pageY],c=[e.clientX,e.clientY]),Math.abs(l[0]-f[0])<=h&&Math.abs(l[1]-f[1])<=h&&(e.type=s,e.pageX=l[0],e.pageY=l[1],e.clientX=c[0],e.clientY=c[1],e.currentTarget=o.node,r.fire(e)),a(n,[u.END,u.CANCEL])}})},"3.17.2",{requires:["node-base","event-base","event-touch","event-synthetic"]});
diff --git a/js/yui3/event-touch/event-touch-min.js b/js/yui3/event-touch/event-touch-min.js
new file mode 100644
index 000000000..d60c48526
--- /dev/null
+++ b/js/yui3/event-touch/event-touch-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-touch",function(e,t){var n="scale",r="rotation",i="identifier",s=e.config.win,o={};e.DOMEventFacade.prototype._touch=function(t,s,o){var u,a,f,l,c;if(t.touches){this.touches=[],c={};for(u=0,a=t.touches.length;u<a;++u)l=t.touches[u],c[e.stamp(l)]=this.touches[u]=new e.DOMEventFacade(l,s,o)}if(t.targetTouches){this.targetTouches=[];for(u=0,a=t.targetTouches.length;u<a;++u)l=t.targetTouches[u],f=c&&c[e.stamp(l,!0)],this.targetTouches[u]=f||new e.DOMEventFacade(l,s,o)}if(t.changedTouches){this.changedTouches=[];for(u=0,a=t.changedTouches.length;u<a;++u)l=t.changedTouches[u],f=c&&c[e.stamp(l,!0)],this.changedTouches[u]=f||new e.DOMEventFacade(l,s,o)}n in t&&(this[n]=t[n]),r in t&&(this[r]=t[r]),i in t&&(this[i]=t[i])},e.Node.DOM_EVENTS&&e.mix(e.Node.DOM_EVENTS,{touchstart:1,touchmove:1,touchend:1,touchcancel:1,gesturestart:1,gesturechange:1,gestureend:1,MSPointerDown:1,MSPointerUp:1,MSPointerMove:1,MSPointerCancel:1,pointerdown:1,pointerup:1,pointermove:1,pointercancel:1}),s&&"ontouchstart"in s&&!(e.UA.chrome&&e.UA.chrome<6)?(o.start=["touchstart","mousedown"],o.end=["touchend","mouseup"],o.move=["touchmove","mousemove"],o.cancel=["touchcancel","mousecancel"]):s&&s.PointerEvent?(o.start="pointerdown",o.end="pointerup",o.move="pointermove",o.cancel="pointercancel"):s&&"msPointerEnabled"in s.navigator?(o.start="MSPointerDown",o.end="MSPointerUp",o.move="MSPointerMove",o.cancel="MSPointerCancel"):(o.start="mousedown",o.end="mouseup",o.move="mousemove",o.cancel="mousecancel"),e.Event._GESTURE_MAP=o},"3.17.2",{requires:["node-base"]});
diff --git a/js/yui3/event-valuechange/event-valuechange-min.js b/js/yui3/event-valuechange/event-valuechange-min.js
new file mode 100644
index 000000000..734fd495c
--- /dev/null
+++ b/js/yui3/event-valuechange/event-valuechange-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("event-valuechange",function(e,t){var n="_valuechange",r="value",i="nodeName",s,o={POLL_INTERVAL:50,TIMEOUT:1e4,_poll:function(t,r){var i=t._node,s=r.e,u=t._data&&t._data[n],a=0,f,l,c,h,p,d;if(!i||!u){o._stopPolling(t);return}l=u.prevVal,h=u.nodeName,u.isEditable?c=i.innerHTML:h==="input"||h==="textarea"?c=i.value:h==="select"&&(p=i.options[i.selectedIndex],c=p.value||p.text),c!==l&&(u.prevVal=c,f={_event:s,currentTarget:s&&s.currentTarget||t,newVal:c,prevVal:l,target:s&&s.target||t},e.Object.some(u.notifiers,function(e){var t=e.handle.evt,n;a!==1?e.fire(f):t.el===d&&e.fire(f),n=t&&t._facade?t._facade.stopped:0,n>a&&(a=n,a===1&&(d=t.el));if(a===2)return!0}),o._refreshTimeout(t))},_refreshTimeout:function(e,t){if(!e._node)return;var r=e.getData(n);o._stopTimeout(e),r.timeout=setTimeout(function(){o._stopPolling(e,t)},o.TIMEOUT)},_startPolling:function(t,s,u){var a,f;if(!t.test("input,textarea,select")&&!(f=o._isEditable(t)))return;a=t.getData(n),a||(a={nodeName:t.get(i).toLowerCase(),isEditable:f,prevVal:f?t.getDOMNode().innerHTML:t.get(r)},t.setData(n,a)),a.notifiers||(a.notifiers={});if(a.interval){if(!u.force){a.notifiers[e.stamp(s)]=s;return}o._stopPolling(t,s)}a.notifiers[e.stamp(s)]=s,a.interval=setInterval(function(){o._poll(t,u)},o.POLL_INTERVAL),o._refreshTimeout(t,s)},_stopPolling:function(t,r){if(!t._node)return;var i=t.getData(n)||{};clearInterval(i.interval),delete i.interval,o._stopTimeout(t),r?i.notifiers&&delete i.notifiers[e.stamp(r)]:i.notifiers={}},_stopTimeout:function(e){var t=e.getData(n)||{};clearTimeout(t.timeout),delete t.timeout},_isEditable:function(e){var t=e._node;return t.contentEditable==="true"||t.contentEditable===""},_onBlur:function(e,t){o._stopPolling(e.currentTarget,t)},_onFocus:function(e,t){var s=e.currentTarget,u=s.getData(n);u||(u={isEditable:o._isEditable(s),nodeName:s.get(i).toLowerCase()},s.setData(n,u)),u.prevVal=u.isEditable?s.getDOMNode().innerHTML:s.get(r),o._startPolling(s,t,{e:e})},_onKeyDown:function(e,t){o._startPolling(e.currentTarget,t,{e:e})},_onKeyUp:function(e,t){(e.charCode===229||e.charCode===197)&&o._startPolling(e.currentTarget,t,{e:e,force:!0})},_onMouseDown:function(e,t){o._startPolling(e.currentTarget,t,{e:e})},_onSubscribe:function(t,s,u,a){var f,l,c,h,p;l={blur:o._onBlur,focus:o._onFocus,keydown:o._onKeyDown,keyup:o._onKeyUp,mousedown:o._onMouseDown},f=u._valuechange={};if(a)f.delegated=!0,f.getNodes=function(){return h=t.all("input,textarea,select").filter(a),p=t.all('[contenteditable="true"],[contenteditable=""]').filter(a),h.concat(p)},f.getNodes().each(function(e){e.getData(n)||e.setData(n,{nodeName:e.get(i).toLowerCase(),isEditable:o._isEditable(e),prevVal:c?e.getDOMNode().innerHTML:e.get(r)})}),u._handles=e.delegate(l,t,a,null,u);else{c=o._isEditable(t);if(!t.test("input,textarea,select")&&!c)return;t.getData(n)||t.setData(n,{nodeName:t.get(i).toLowerCase(),isEditable:c,prevVal:c?t.getDOMNode().innerHTML:t.get(r)}),u._handles=t.on(l,null,null,u)}},_onUnsubscribe:function(e,t,n){var r=n._valuechange;n._handles&&n._handles.detach(),r.delegated?r.getNodes().each(function(e){o._stopPolling(e,n)}):o._stopPolling(e,n)}};s={detach:o._onUnsubscribe,on:o._onSubscribe,delegate:o._onSubscribe,detachDelegate:o._onUnsubscribe,publishConfig:{emitFacade:!0}},e.Event.define("valuechange",s),e.Event.define("valueChange",s),e.ValueChange=o},"3.17.2",{requires:["event-focus","event-synthetic"]});
diff --git a/js/yui3/exec-command/exec-command-min.js b/js/yui3/exec-command/exec-command-min.js
new file mode 100644
index 000000000..7e5a5d555
--- /dev/null
+++ b/js/yui3/exec-command/exec-command-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("exec-command",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)},r=function(t,n,r){var i=this.getInstance(),s=i.config.doc,o=s.selection.createRange(),u=s.queryCommandValue(t),a,f,l,c,h,p,d;u&&(a=o.htmlText,f=new RegExp(r,"g"),l=a.match(f),l&&(a=a.replace(r+";","").replace(r,""),o.pasteHTML('<var id="yui-ie-bs">'),c=s.getElementById("yui-ie-bs"),h=s.createElement("div"),p=s.createElement(n),h.innerHTML=a,c.parentNode!==i.config.doc.body&&(c=c.parentNode),d=h.childNodes,c.parentNode.replaceChild(p,c),e.each(d,function(e){p.appendChild(e)}),o.collapse(),o.moveToElementText&&o.moveToElementText(p),o.select())),this._command(t)};e.extend(n,e.Base,{_lastKey:null,_inst:null,command:function(e,t){var r=n.COMMANDS[e];return r?r.call(this,e,t):this._command(e,t)},_command:function(e,t){var n=this.getInstance();try{try{n.config.doc.execCommand("styleWithCSS",null,1)}catch(r){try{n.config.doc.execCommand("useCSS",null,0)}catch(i){}}n.config.doc.execCommand(e,null,t)}catch(s){}},getInstance:function(){return this._inst||(this._inst=this.get("host").getInstance()),this._inst},initializer:function(){e.mix(this.get("host"),{execCommand:function(e,t){return this.exec.command(e,t)},_execCommand:function(e,t){return this.exec._command(e,t)}}),this.get("host").on("dom:keypress",e.bind(function(e){this._lastKey=e.keyCode},this))},_wrapContent:function(e,t){var n=this.getInstance().host.editorPara&&!t?!0:!1;return n?e="<p>"+e+"</p>":e+="<br>",e}},{NAME:"execCommand",NS:"exec",ATTRS:{host:{value:!1}},COMMANDS:{wrap:function(e,t){var n=this.getInstance();return(new n.EditorSelection).wrapContent(t)},inserthtml:function(t,n){var r=this.getInstance();if(r.EditorSelection.hasCursor()||e.UA.ie)return(new r.EditorSelection).insertContent(n);this._command("inserthtml",n)},insertandfocus:function(e,t){var n=this.getInstance(),r,i;return n.EditorSelection.hasCursor()?(t+=n.EditorSelection.CURSOR,r=this.command("inserthtml",t),i=new n.EditorSelection,i.focusCursor(!0,!0)):this.command("inserthtml",t),r},insertbr:function(){var t=this.getInstance(),n=new t.EditorSelection,r="<var>|</var>",i=null,s=t.EditorSelection.ROOT,o=e.UA.webkit?"span.Apple-style-span,var":"var",u=function(e){var n=t.Node.create("<br>");return e.insert(n,"before"),n};n._selection.pasteHTML?n._selection.pasteHTML(r):this._command("inserthtml",r),s.all(o).each(function(t){var n=!0,r;e.UA.webkit&&(n=!1,t.get("innerHTML")==="|"&&(n=!0)),n&&(i=u(t),(!i.previous()||!i.previous().test("br"))&&e.UA.gecko&&(r=i.cloneNode(),i.insert(r,"after"),i=r),t.remove())}),e.UA.webkit&&i&&(u(i),n.selectNode(i))},insertimage:function(e,t){return this.command("inserthtml",'<img src="'+t+'">')},addclass:function(e,t){var n=this.getInstance();return(new n.EditorSelection).getSelected().addClass(t)},removeclass:function(e,t){var n=this.getInstance();return(new n.EditorSelection).getSelected().removeClass(t)},forecolor:function(t,n){var r=this.getInstance(),i=new r.EditorSelection,s;e.UA.ie||this._command("useCSS",!1);if(r.EditorSelection.hasCursor())return i.isCollapsed?(i.anchorNode&&i.anchorNode.get("innerHTML")==="&nbsp;"?(i.anchorNode.setStyle("color",n),s=i.anchorNode):(s=this.command("inserthtml",'<span style="color: '+n+'">'+r.EditorSelection.CURSOR+"</span>"),i.focusCursor(!0,!0)),s):this._command(t,n);this._command(t,n)},backcolor:function(t,n){var r=this.getInstance(),i=new r.EditorSelection,s;if(e.UA.gecko||e.UA.opera)t="hilitecolor";e.UA.ie||this._command("useCSS",!1);if(r.EditorSelection.hasCursor())return i.isCollapsed?(i.anchorNode&&i.anchorNode.get("innerHTML")==="&nbsp;"?(i.anchorNode.setStyle("backgroundColor",n),s=i.anchorNode):(s=this.command("inserthtml",'<span style="background-color: '+n+'">'+r.EditorSelection.CURSOR+"</span>"),i.focusCursor(!0,!0)),s):this._command(t,n);this._command(t,n)},hilitecolor:function(){return n.COMMANDS.backcolor.apply(this,arguments)},fontname2:function(e,t){this._command("fontname",t);var n=this.getInstance(),r=new n.EditorSelection;r.isCollapsed&&this._lastKey!==32&&r.anchorNode.test("font")&&r.anchorNode.set("face",t)},fontsize2:function(t,n){this._command("fontsize",n);var r=this.getInstance(),i=new r.EditorSelection,s;i.isCollapsed&&i.anchorNode&&this._lastKey!==32&&(e.UA.webkit&&i.anchorNode.getStyle("lineHeight")&&i.anchorNode.setStyle("lineHeight",""),i.anchorNode.test("font")?i.anchorNode.set("size",n):e.UA.gecko&&(s=i.anchorNode.ancestor(r.EditorSelection.DEFAULT_BLOCK_TAG),s&&s.setStyle("fontSize","")))},insertunorderedlist:function(){this.command("list","ul")},insertorderedlist:function(){this.command("list","ol")},list:function(t,n){var r=this.getInstance(),i,s=this,o="dir",u="yui3-touched",a,f,l,c,h,p,d,v,m,g,y=r.host.editorPara?!0:!1,b,w,E,S,x=r.EditorSelection.ROOT,T=new r.EditorSelection;t="insert"+(n==="ul"?"un":"")+"orderedlist";if(e.UA.ie&&e.UA.ie<11&&!T.isCollapsed){f=T._selection,i=f.htmlText,l=r.Node.create(i)||x;if(l.test("li")||l.one("li")){this._command(t,null);return}l.test(n)?(c=f.item?f.item(0):f.parentElement(),h=r.one(c),g=h.all("li"),p="<div>",g.each(function(e){p=s._wrapContent(e.get("innerHTML"))}),p+="</div>",d=r.Node.create(p),h.get("parentNode").test("div")&&(h=h.get("parentNode")),h&&h.hasAttribute(o)&&(y?d.all("p").setAttribute(o,h.getAttribute(o)):d.setAttribute(o,h.getAttribute(o))),y?h.replace(d.get("innerHTML")):h.replace(d),f.moveToElementText&&f.moveToElementText(d._node),f.select()):(v=e.one(f.parentElement()),v.test(r.EditorSelection.BLOCKS)||(v=v.ancestor(r.EditorSelection.BLOCKS)),v&&v.hasAttribute(o)&&(a=v.getAttribute(o)),i.indexOf("<br>")>-1?i=i.split(/<br>/i):(b=r.Node.create(i),ps=b?b.all("p"):null,ps&&ps.size()?(i=[],ps.each(function(e){i.push(e.get("innerHTML"))})):i=[i]),m="<"+n+' id="ie-list">',e.each(i,function(e){var t=r.Node.create(e);t&&t.test("p")&&(t.hasAttribute(o)&&(a=t.getAttribute(o)),e=t.get("innerHTML")),m+="<li>"+e+"</li>"}),m+="</"+n+">",f.pasteHTML(m),c=r.config.doc.getElementById("ie-list"),c.id="",a&&c.setAttribute
+(o,a),f.moveToElementText&&f.moveToElementText(c),f.select())}else e.UA.ie&&e.UA.ie<11?(v=r.one(T._selection.parentElement()),v.test("p")?(v&&v.hasAttribute(o)&&(a=v.getAttribute(o)),i=e.EditorSelection.getText(v),i===""?(w="",a&&(w=' dir="'+a+'"'),m=r.Node.create(e.Lang.sub("<{tag}{dir}><li></li></{tag}>",{tag:n,dir:w})),v.replace(m),T.selectNode(m.one("li"))):this._command(t,null)):this._command(t,null)):(x.all(n).addClass(u),T.anchorNode.test(r.EditorSelection.BLOCKS)?v=T.anchorNode:v=T.anchorNode.ancestor(r.EditorSelection.BLOCKS),v||(v=T.anchorNode.one(r.EditorSelection.BLOCKS)),v&&v.hasAttribute(o)&&(a=v.getAttribute(o)),v&&v.test(n)?(E=v.ancestor("p"),i=r.Node.create("<div/>"),c=v.all("li"),c.each(function(e){i.append(s._wrapContent(e.get("innerHTML"),E))}),a&&(y?i.all("p").setAttribute(o,a):i.setAttribute(o,a)),y&&(i=r.Node.create(i.get("innerHTML"))),S=i.get("firstChild"),v.replace(i),T.selectNode(S)):this._command(t,null),m=x.all(n),a&&m.size()&&m.each(function(e){e.hasClass(u)||e.setAttribute(o,a)}),m.removeClass(u))},justify:function(t,n){if(e.UA.webkit){var r=this.getInstance(),i=new r.EditorSelection,s=i.anchorNode,o,u=s.getStyle("backgroundColor");this._command(n),i=new r.EditorSelection,i.anchorNode.test("div")&&(o="<span>"+i.anchorNode.get("innerHTML")+"</span>",i.anchorNode.set("innerHTML",o),i.anchorNode.one("span").setStyle("backgroundColor",u),i.selectNode(i.anchorNode.one("span")))}else this._command(n)},justifycenter:function(){this.command("justify","justifycenter")},justifyleft:function(){this.command("justify","justifyleft")},justifyright:function(){this.command("justify","justifyright")},justifyfull:function(){this.command("justify","justifyfull")}}}),e.UA.ie&&e.UA.ie<11&&(n.COMMANDS.bold=function(){r.call(this,"bold","b","FONT-WEIGHT: bold")},n.COMMANDS.italic=function(){r.call(this,"italic","i","FONT-STYLE: italic")},n.COMMANDS.underline=function(){r.call(this,"underline","u","TEXT-DECORATION: underline")}),e.namespace("Plugin"),e.Plugin.ExecCommand=n},"3.17.2",{requires:["frame"]});
diff --git a/js/yui3/features/features-min.js b/js/yui3/features/features-min.js
new file mode 100644
index 000000000..223ee79e3
--- /dev/null
+++ b/js/yui3/features/features-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("features",function(e,t){var n={};e.mix(e.namespace("Features"),{tests:n,add:function(e,t,r){n[e]=n[e]||{},n[e][t]=r},all:function(t,r){var i=n[t],s=[];return i&&e.Object.each(i,function(n,i){s.push(i+":"+(e.Features.test(t,i,r)?1:0))}),s.length?s.join(";"):""},test:function(t,r,i){i=i||[];var s,o,u,a=n[t],f=a&&a[r];return!f||(s=f.result,e.Lang.isUndefined(s)&&(o=f.ua,o&&(s=e.UA[o]),u=f.test,u&&(!o||s)&&(s=u.apply(e,i)),f.result=s)),s}});var r=e.Features.add;r("load","0",{name:"app-transitions-native",test:function(e){var t=e.config.doc,n=t?t.documentElement:null;return n&&n.style?"MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style:!1},trigger:"app-transitions"}),r("load","1",{name:"autocomplete-list-keys",test:function(e){return!e.UA.ios&&!e.UA.android},trigger:"autocomplete-list"}),r("load","2",{name:"dd-gestures",trigger:"dd-drag",ua:"touchEnabled"}),r("load","3",{name:"dom-style-ie",test:function(e){var t=e.Features.test,n=e.Features.add,r=e.config.win,i=e.config.doc,s="documentElement",o=!1;return n("style","computedStyle",{test:function(){return r&&"getComputedStyle"in r}}),n("style","opacity",{test:function(){return i&&"opacity"in i[s].style}}),o=!t("style","opacity")&&!t("style","computedStyle"),o},trigger:"dom-style"}),r("load","4",{name:"editor-para-ie",trigger:"editor-para",ua:"ie",when:"instead"}),r("load","5",{name:"event-base-ie",test:function(e){var t=e.config.doc&&e.config.doc.implementation;return t&&!t.hasFeature("Events","2.0")},trigger:"node-base"}),r("load","6",{name:"graphics-canvas",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"}),r("load","7",{name:"graphics-canvas-default",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"}),r("load","8",{name:"graphics-svg",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"}),r("load","9",{name:"graphics-svg-default",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"}),r("load","10",{name:"graphics-vml",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"}),r("load","11",{name:"graphics-vml-default",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"}),r("load","12",{name:"history-hash-ie",test:function(e){var t=e.config.doc&&e.config.doc.documentMode;return e.UA.ie&&(!("onhashchange"in e.config.win)||!t||t<8)},trigger:"history-hash"}),r("load","13",{name:"io-nodejs",trigger:"io-base",ua:"nodejs"}),r("load","14",{name:"json-parse-shim",test:function(e){function i(e,t){return e==="ok"?!0:t}var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONParse!==!1&&!!n;if(r)try{r=n.parse('{"ok":false}',i).ok}catch(s){r=!1}return!r},trigger:"json-parse"}),r("load","15",{name:"json-stringify-shim",test:function(e){var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONStringify!==!1&&!!n;if(r)try{r="0"===n.stringify(0)}catch(i){r=!1}return!r},trigger:"json-stringify"}),r("load","16",{name:"scrollview-base-ie",trigger:"scrollview-base",ua:"ie"}),r("load","17",{name:"selector-css2",test:function(e){var t=e.config.doc,n=t&&!("querySelectorAll"in t);return n},trigger:"selector"}),r("load","18",{name:"transition-timer",test:function(e){var t=e.config.doc,n=t?t.documentElement:null,r=!0;return n&&n.style&&(r=!("MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style)),r},trigger:"transition"}),r("load","19",{name:"widget-base-ie",trigger:"widget-base",ua:"ie"}),r("load","20",{name:"yql-jsonp",test:function(e){return!e.UA.nodejs&&!e.UA.winjs},trigger:"yql"}),r("load","21",{name:"yql-nodejs",trigger:"yql",ua:"nodejs"}),r("load","22",{name:"yql-winjs",trigger:"yql",ua:"winjs"})},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/file-flash/file-flash-min.js b/js/yui3/file-flash/file-flash-min.js
new file mode 100644
index 000000000..2c7ef4c55
--- /dev/null
+++ b/js/yui3/file-flash/file-flash-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("file-flash",function(e,t){var n=function(e){n.superclass.constructor.apply(this,arguments)};e.extend(n,e.Base,{initializer:function(t){this.get("id")||this._set("id",e.guid("file"))},_swfEventHandler:function(e){if(e.id===this.get("id"))switch(e.type){case"uploadstart":this.fire("uploadstart",{uploader:this.get("uploader")});break;case"uploadprogress":this.fire("uploadprogress",{originEvent:e,bytesLoaded:e.bytesLoaded,bytesTotal:e.bytesTotal,percentLoaded:Math.min(100,Math.round(1e4*e.bytesLoaded/e.bytesTotal)/100)}),this._set("bytesUploaded",e.bytesLoaded);break;case"uploadcomplete":this.fire("uploadfinished",{originEvent:e});break;case"uploadcompletedata":this.fire("uploadcomplete",{originEvent:e,data:e.data});break;case"uploadcancel":this.fire("uploadcancel",{originEvent:e});break;case"uploaderror":this.fire("uploaderror",{originEvent:e,status:e.status,statusText:e.message,source:e.source})}},startUpload:function(e,t,n){if(this.get("uploader")){var r=this.get("uploader"),i=n||"Filedata",s=this.get("id"),o=t||null;this._set("bytesUploaded",0),r.on("uploadstart",this._swfEventHandler,this),r.on("uploadprogress",this._swfEventHandler,this),r.on("uploadcomplete",this._swfEventHandler,this),r.on("uploadcompletedata",this._swfEventHandler,this),r.on("uploaderror",this._swfEventHandler,this),r.callSWF("upload",[s,e,o,i])}},cancelUpload:function(){this.get("uploader")&&(this.get("uploader").callSWF("cancel",[this.get("id")]),this.fire("uploadcancel"))}},{NAME:"file",TYPE:"flash",ATTRS:{id:{writeOnce:"initOnly",value:null},size:{writeOnce:"initOnly",value:0},name:{writeOnce:"initOnly",value:null},dateCreated:{writeOnce:"initOnly",value:null},dateModified:{writeOnce:"initOnly",value:null},bytesUploaded:{readOnly:!0,value:0},type:{writeOnce:"initOnly",value:null},uploader:{writeOnce:"initOnly",value:null}}}),e.FileFlash=n},"3.17.2",{requires:["base"]});
diff --git a/js/yui3/file-html5/file-html5-min.js b/js/yui3/file-html5/file-html5-min.js
new file mode 100644
index 000000000..692a63559
--- /dev/null
+++ b/js/yui3/file-html5/file-html5-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("file-html5",function(e,t){var n=e.Lang,r=e.bind,i=e.config.win,s=function(e){var t=null;s.isValidFile(e)?t=e:s.isValidFile(e.file)?t=e.file:t=!1,s.superclass.constructor.apply(this,arguments),t&&s.canUpload()&&(this.get("file")||this._set("file",t),this.get("name")||this._set("name",t.name||t.fileName),this.get("size")!=(t.size||t.fileSize)&&this._set("size",t.size||t.fileSize),this.get("type")||this._set("type",t.type),t.hasOwnProperty("lastModifiedDate")&&!this.get("dateModified")&&this._set("dateModified",t.lastModifiedDate))};e.extend(s,e.Base,{initializer:function(t){this.get("id")||this._set("id",e.guid("file"))},_uploadEventHandler:function(e){var t=this.get("xhr");switch(e.type){case"progress":this.fire("uploadprogress",{originEvent:e,bytesLoaded:e.loaded,bytesTotal:this.get("size"),percentLoaded:Math.min(100,Math.round(1e4*e.loaded/this.get("size"))/100)}),this._set("bytesUploaded",e.loaded);break;case"load":if(t.status>=200&&t.status<=299){this.fire("uploadcomplete",{originEvent:e,data:e.target.responseText});var n=t.upload,r=this.get("boundEventHandler");n.removeEventListener("progress",r),n.removeEventListener("error",r),n.removeEventListener("abort",r),t.removeEventListener("load",r),t.removeEventListener("error",r),t.removeEventListener("readystatechange",r),this._set("xhr",null)}else this.fire("uploaderror",{originEvent:e,data:t.responseText,status:t.status,statusText:t.statusText,source:"http"});break;case"error":this.fire("uploaderror",{originEvent:e,data:t.responseText,status:t.status,statusText:t.statusText,source:"io"});break;case"abort":this.fire("uploadcancel",{originEvent:e});break;case"readystatechange":this.fire("readystatechange",{readyState:e.target.readyState,originEvent:e})}},startUpload:function(t,n,i){this._set("bytesUploaded",0),this._set("xhr",new XMLHttpRequest),this._set("boundEventHandler",r(this._uploadEventHandler,this));var s=new FormData,o=i||"Filedata",u=this.get("xhr"),a=this.get("xhr").upload,f=this.get("boundEventHandler");e.each(n,function(e,t){s.append(t,e)}),s.append(o,this.get("file")),u.addEventListener("loadstart",f,!1),a.addEventListener("progress",f,!1),u.addEventListener("load",f,!1),u.addEventListener("error",f,!1),a.addEventListener("error",f,!1),a.addEventListener("abort",f,!1),u.addEventListener("abort",f,!1),u.addEventListener("loadend",f,!1),u.addEventListener("readystatechange",f,!1),u.open("POST",t,!0),u.withCredentials=this.get("xhrWithCredentials"),e.each(this.get("xhrHeaders"),function(e,t){u.setRequestHeader(t,e)}),u.send(s),this.fire("uploadstart",{xhr:u})},cancelUpload:function(){var e=this.get("xhr");e&&e.abort()}},{NAME:"file",TYPE:"html5",ATTRS:{id:{writeOnce:"initOnly",value:null},size:{writeOnce:"initOnly",value:0},name:{writeOnce:"initOnly",value:null},dateCreated:{writeOnce:"initOnly",value:null},dateModified:{writeOnce:"initOnly",value:null},bytesUploaded:{readOnly:!0,value:0},type:{writeOnce:"initOnly",value:null},file:{writeOnce:"initOnly",value:null},xhr:{readOnly:!0,value:null},xhrHeaders:{value:{}},xhrWithCredentials:{value:!0},boundEventHandler:{readOnly:!0,value:null}},isValidFile:function(e){return i&&i.File&&e instanceof File},canUpload:function(){return i&&i.FormData&&i.XMLHttpRequest}}),e.FileHTML5=s},"3.17.2",{requires:["base"]});
diff --git a/js/yui3/file/file-min.js b/js/yui3/file/file-min.js
new file mode 100644
index 000000000..074d6a545
--- /dev/null
+++ b/js/yui3/file/file-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("file",function(e,t){var n=e.config.win;n&&n.File&&n.FormData&&n.XMLHttpRequest?e.File=e.FileHTML5:e.File=e.FileFlash},"3.17.2",{requires:["file-flash","file-html5"]});
diff --git a/js/yui3/frame/frame-min.js b/js/yui3/frame/frame-min.js
new file mode 100644
index 000000000..ae52bb6d5
--- /dev/null
+++ b/js/yui3/frame/frame-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("frame",function(e,t){var n=e.Lang,r="contentready",i="host",s=function(){s.superclass.constructor.apply(this,arguments)};e.extend(s,e.Plugin.Base,{_ready:null,_rendered:null,_iframe:null,_instance:null,_create:function(t){var n,r="",i,o=this.get("src")===s.ATTRS.src.value,u=this.get("extracss")?'<style id="extra_css">'+this.get("extracss")+"</style>":"";this._iframe=e.one(e.config.doc.createElement("iframe")),this._iframe.setAttrs(s.IFRAME_ATTRS),this._iframe.setStyle("visibility","hidden"),this._iframe.set("src",this.get("src")),this.get("container").append(this._iframe),this._iframe.set("height","99%"),o&&(r=e.Lang.sub(s.PAGE_HTML,{DIR:this.get("dir"),LANG:this.get("lang"),TITLE:this.get("title"),META:s.META,LINKED_CSS:this.get("linkedcss"),CONTENT:this.get("content"),BASE_HREF:this.get("basehref"),DEFAULT_CSS:s.DEFAULT_CSS,EXTRA_CSS:u}),e.config.doc.compatMode!=="BackCompat"&&(r=s.getDocType()+"\n"+r)),n=this._resolveWinDoc(),r&&(n.doc.open(),n.doc.write(r),n.doc.close()),n.doc.documentElement?t(n):i=e.later(1,this,function(){n.doc&&n.doc.documentElement&&(t(n),i.cancel())},null,!0)},_resolveWinDoc:function(t){var n=t?t:{};return n.win=e.Node.getDOMNode(this._iframe.get("contentWindow")),n.doc=e.Node.getDOMNode(this._iframe.get("contentWindow.document")),n.doc||(n.doc=e.config.doc),n.win||(n.win=e.config.win),n},_onDomEvent:function(t){var n,r;if(!e.Node.getDOMNode(this._iframe))return;t.frameX=t.frameY=0,(t.pageX>0||t.pageY>0)&&t.type.substring(0,3)!=="key"&&(r=this._instance.one("win"),n=this._iframe.getXY(),t.frameX=n[0]+t.pageX-r.get("scrollLeft"),t.frameY=n[1]+t.pageY-r.get("scrollTop")),t.frameTarget=t.target,t.frameCurrentTarget=t.currentTarget,t.frameEvent=t,this.fire("dom:"+t.type,t)},initializer:function(){var e=this.get(i);e&&(e.frame=this),this.publish("ready",{emitFacade:!0,defaultFn:this._defReadyFn})},destructor:function(){var e=this.getInstance();e.one("doc").detachAll(),e=null,this._iframe.remove()},_DOMPaste:function(e){var t=this.getInstance(),n="",r=t.config.win;e._event.originalTarget&&(n=e._event.originalTarget),e._event.clipboardData&&(n=e._event.clipboardData.getData("Text")),r.clipboardData&&(n=r.clipboardData.getData("Text"),n===""&&(r.clipboardData.setData("Text",n)||(n=null))),e.frameTarget=e.target,e.frameCurrentTarget=e.currentTarget,e.frameEvent=e,n?e.clipboardData={data:n,getData:function(){return n}}:e.clipboardData=null,this.fire("dom:paste",e)},_defReadyFn:function(){var t=this.getInstance();e.each(s.DOM_EVENTS,function(n,r){var i=e.bind(this._onDomEvent,this),o=e.UA.ie&&s.THROTTLE_TIME>0?e.throttle(i,s.THROTTLE_TIME):i;t.Node.DOM_EVENTS[r]||(t.Node.DOM_EVENTS[r]=1),n===1&&r!=="focus"&&r!=="blur"&&r!=="paste"&&(r.substring(0,3)==="key"?t.on(r,o,t.config.doc):t.on(r,i,t.config.doc))},this),t.Node.DOM_EVENTS.paste=1,t.on("paste",e.bind(this._DOMPaste,this),t.one("body")),t.on("focus",e.bind(this._onDomEvent,this),t.config.win),t.on("blur",e.bind(this._onDomEvent,this),t.config.win),t.__use=t.use,t.use=e.bind(this.use,this),this._iframe.setStyles({visibility:"inherit"}),t.one("body").setStyle("display","block")},_fixIECursors:function(){var e=this.getInstance(),t=e.all("table"),n=e.all("br"),r;t.size()&&n.size()&&(r=t.item(0).get("sourceIndex"),n.each(function(t){var n=t.get("parentNode"),i=n.get("children"),s=n.all(">br");n.test("div")&&(i.size()>2?t.replace(e.Node.create("<wbr>")):t.get("sourceIndex")>r?s.size()&&t.replace(e.Node.create("<wbr>")):s.size()>1&&t.replace(e.Node.create("<wbr>")))}))},_onContentReady:function(t){if(!this._ready){this._ready=!0;var n=this.getInstance(),r=e.clone(this.get("use"));this.fire("contentready"),t&&(n.config.doc=e.Node.getDOMNode(t.target)),r.push(e.bind(function(){n.EditorSelection&&(n.EditorSelection.DEFAULT_BLOCK_TAG=this.get("defaultblock")),this.get("designMode")&&(e.UA.ie?(n.config.doc.body.contentEditable="true",this._ieSetBodyHeight(),n.on("keyup",e.bind(this._ieSetBodyHeight,this),n.config.doc)):n.config.doc.designMode="on"),this.fire("ready")},this)),n.use.apply(n,r),n.one("doc").get("documentElement").addClass("yui-js-enabled")}},_ieHeightCounter:null,_ieSetBodyHeight:function(t){this._ieHeightCounter||(this._ieHeightCounter=0),this._ieHeightCounter++;var n=!1,r,i,s;t||(n=!0);if(t){switch(t.keyCode){case 8:case 13:n=!0}if(t.ctrlKey||t.shiftKey)n=!0}if(n)try{r=this.getInstance(),i=this._iframe.get("offsetHeight"),s=r.config.doc.body.scrollHeight,i>s?(i=i-15+"px",r.config.doc.body.style.height=i):r.config.doc.body.style.height="auto"}catch(t){this._ieHeightCounter<100&&e.later(200,this,this._ieSetBodyHeight)}},_resolveBaseHref:function(t){if(!t||t==="")t=e.config.doc.location.href,t.indexOf("?")!==-1&&(t=t.substring(0,t.indexOf("?"))),t=t.substring(0,t.lastIndexOf("/"))+"/";return t},_getHTML:function(e){if(this._ready){var t=this.getInstance();e=t.one("body").get("innerHTML")}return e},_setHTML:function(t){if(this._ready){var n=this.getInstance();n.one("body").set("innerHTML",t)}else this.once(r,e.bind(this._setHTML,this,t));return t},_getLinkedCSS:function(t){e.Lang.isArray(t)||(t=[t]);var n="";return this._ready?n=t:e.each(t,function(e){e&&(n+='<link rel="stylesheet" href="'+e+'" type="text/css">')}),n},_setLinkedCSS:function(e){if(this._ready){var t=this.getInstance();t.Get.css(e)}return e},_setExtraCSS:function(t){if(this._ready){var n=this.getInstance(),i=n.one("#extra_css");i&&i.remove(),n.one("head").append('<style id="extra_css">'+t+"</style>")}else this.once(r,e.bind(this._setExtraCSS,this,t));return t},_instanceLoaded:function(t){this._instance=t,this._onContentReady();var n=this._instance.config.doc;if(this.get("designMode")&&!e.UA.ie)try{n.execCommand("styleWithCSS",!1,!1),n.execCommand("insertbronreturn",!1,!1)}catch(r){}},use:function(){var t=this.getInstance(),n=e.Array(arguments),r=!1;return e.Lang.isFunction(n[n.length-1])&&(r=n.pop()),r&&n.push(function(){r.apply(t,arguments)}),t.__use.apply(t,n)},delegate:function(e,t,n,r){var i=this.getInstance();return i?(r||(r=n,n="body"),i.delegate
+(e,t,n,r)):!1},getInstance:function(){return this._instance},render:function(t){return this._rendered?this:(this._rendered=!0,t&&this.set("container",t),this._create(e.bind(function(t){var n,r,s=e.bind(function(e){this._instanceLoaded(e)},this),o=e.clone(this.get("use")),u={debug:!1,win:t.win,doc:t.doc},a=e.bind(function(){u=this._resolveWinDoc(u),n=YUI(u),n.host=this.get(i);try{n.use("node-base",s),r&&clearInterval(r)}catch(e){r=setInterval(function(){a()},350)}},this);o.push(a),e.use.apply(e,o)},this)),this)},_handleFocus:function(){var e=this.getInstance(),t=new e.EditorSelection,n,r,i,s;t.anchorNode&&(n=t.anchorNode,n.test("p")&&n.get("innerHTML")===""&&(n=n.get("parentNode")),r=n.get("childNodes"),r.size()&&(r.item(0).test("br")?t.selectNode(n,!0,!1):r.item(0).test("p")?(n=r.item(0).one("br.yui-cursor"),n&&(n=n.get("parentNode")),n||(n=r.item(0).get("firstChild")),n||(n=r.item(0)),n&&t.selectNode(n,!0,!1)):(i=e.one("br.yui-cursor"),i&&(s=i.get("parentNode"),s&&t.selectNode(s,!0,!1)))))},_validateLinkedCSS:function(e){return n.isString(e)||n.isArray(e)},focus:function(t){if(e.UA.ie&&e.UA.ie<9){try{e.one("win").focus(),this.getInstance()&&this.getInstance().one("win")&&this.getInstance().one("win").focus()}catch(n){}t===!0&&this._handleFocus(),e.Lang.isFunction(t)&&t()}else try{e.one("win").focus(),e.later(100,this,function(){this.getInstance()&&this.getInstance().one("win")&&this.getInstance().one("win").focus(),t===!0&&this._handleFocus(),e.Lang.isFunction(t)&&t()})}catch(r){}return this},show:function(){this._iframe.setStyles({position:"static",left:""});if(e.UA.gecko){try{this.getInstance()&&(this.getInstance().config.doc.designMode="on")}catch(t){}this.focus()}return this},hide:function(){return this._iframe.setStyles({position:"absolute",left:"-999999px"}),this}},{THROTTLE_TIME:100,DOM_EVENTS:{dblclick:1,click:1,paste:1,mouseup:1,mousedown:1,keyup:1,keydown:1,keypress:1,activate:1,deactivate:1,beforedeactivate:1,focusin:1,focusout:1},DEFAULT_CSS:"body { background-color: #fff; font: 13px/1.22 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small; } a, a:visited, a:hover { color: blue !important; text-decoration: underline !important; cursor: text !important; } img { cursor: pointer !important; border: none; }",IFRAME_ATTRS:{border:"0",frameBorder:"0",marginWidth:"0",marginHeight:"0",leftMargin:"0",topMargin:"0",allowTransparency:"true",width:"100%",height:"99%"},PAGE_HTML:'<html dir="{DIR}" lang="{LANG}"><head><title>{TITLE}</title>{META}<base href="{BASE_HREF}"/>{LINKED_CSS}<style id="editor_css">{DEFAULT_CSS}</style>{EXTRA_CSS}</head><body>{CONTENT}</body></html>',getDocType:function(){var t=e.config.doc.doctype,n=s.DOC_TYPE;return t?n="<!DOCTYPE "+t.name+(t.publicId?" "+t.publicId:"")+(t.systemId?" "+t.systemId:"")+">":e.config.doc.all&&(t=e.config.doc.all[0],t.nodeType&&t.nodeType===8&&t.nodeValue&&t.nodeValue.toLowerCase().indexOf("doctype")!==-1&&(n="<!"+t.nodeValue+">")),n},DOC_TYPE:'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',META:'<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/><meta http-equiv="X-UA-Compatible" content="IE=7">',NAME:"frame",NS:"frame",ATTRS:{title:{value:"Blank Page"},dir:{value:"ltr"},lang:{value:"en-US"},src:{value:"javascript"+(e.UA.ie?":false":":")+";"},designMode:{writeOnce:!0,value:!1},content:{validator:n.isString,value:"<br>",setter:"_setHTML",getter:"_getHTML"},basehref:{value:!1,getter:"_resolveBaseHref"},use:{writeOnce:!0,value:["node","node-style","selector-css3"]},container:{value:"body",setter:function(t){return e.one(t)}},node:{readOnly:!0,value:null,getter:function(){return this._iframe}},id:{writeOnce:!0,getter:function(t){return t||(t="iframe-"+e.guid()),t}},linkedcss:{validator:"_validateLinkedCSS",getter:"_getLinkedCSS",setter:"_setLinkedCSS"},extracss:{validator:n.isString,setter:"_setExtraCSS"},defaultblock:{value:"p"}}}),e.namespace("Plugin"),e.Plugin.Frame=s,e.Frame=s},"3.17.2",{requires:["base","node","plugin","selector-css3","yui-throttle"]});
diff --git a/js/yui3/gallery-datatable-paginator/gallery-datatable-paginator-min.js b/js/yui3/gallery-datatable-paginator/gallery-datatable-paginator-min.js
new file mode 100644
index 000000000..4e923ce0b
--- /dev/null
+++ b/js/yui3/gallery-datatable-paginator/gallery-datatable-paginator-min.js
@@ -0,0 +1,2 @@
+YUI.add("gallery-paginator-view",function(a){a.PaginatorModel=a.Base.create("paginatorModel",a.Model,[],{_npages:null,_subscr:null,initializer:function(){this._recalcPagnParams();this._subscr=[];this._subscr.push(this.after("totalItemsChange",this._recalcPagnParams));this._subscr.push(this.after("itemsPerPageChange",this._recalcPagnParams));this._subscr.push(this.on("pageChange",this._changePage));return this;},destructor:function(){a.Array.each(this._subscr,function(b){b.detach();});this._subscr=null;},_changePage:function(d){var b=d.newVal,c=true;if(b<1||!this.get("totalPages")||!this.get("itemsPerPage")){c=false;}if(this.get("totalPages")&&b>this.get("totalPages")){c=false;}if(c){this.set("lastPage",d.prevVal);}else{d.preventDefault();}},_recalcPagnParams:function(){var c=this.get("itemsPerPage"),b=this.get("totalItems");if(b&&c&&b>0&&c>0){np=Math.floor(b/c);if(b%c>0){np++;}this._npages=np;this.set("page",1);return true;}return false;},_getItemIndexStart:function(){return(this.get("page")-1)*this.get("itemsPerPage");},_getItemIndexEnd:function(){var b=this.get("totalItems"),c=this.get("itemIndexStart")+this.get("itemsPerPage");return(c>b)?b:c;}},{ATTRS:{totalItems:{value:null,validator:a.Lang.isNumber},itemsPerPage:{value:null,validator:a.Lang.isNumber},page:{value:1,validator:a.Lang.isNumber},lastPage:{value:null,validator:a.Lang.isNumber},totalPages:{value:null,validator:a.Lang.isNumber,getter:function(){return this._npages;}},itemIndexStart:{value:null,validator:a.Lang.isNumber,getter:"_getItemIndexStart"},itemIndexEnd:{value:null,validator:a.Lang.isNumber,getter:"_getItemIndexEnd"}}});a.PaginatorView=a.Base.create("paginatorView",a.View,[],{TMPL_PAGINATOR:'<a href="#" data-pglink="first" class="{pageLinkClass}" title="First Page">First</a> | '+'<a href="#" data-pglink="prev" class="{pageLinkClass}" title="Prior Page">Prev</a> | '+"{pageLinks}"+' | <a href="#" data-pglink="next" class="{pageLinkClass}" title="Next Page">Next</a> | '+'<a href="#" data-pglink="last" class="{pageLinkClass}" title="Last Page">Last</a>',TMPL_LINK:'<a href="#" data-pglink="{page}" class="{pageLinkClass}" title="Page {page}">{page}</a>',TMPL_basic:"{firstPage} {prevPage} {pageLinks} {nextPage} {lastPage}",TMPL_pglinks:"{pageLinks}",TMPL_selectRPP:'<select class="{selectRPPClass}"></select>',TMPL_selectPage:'<select class="{selectPageClass}"></select>',TMPL_inputRPP:'<input type="text" class="{inputRPPClass}" value="{itemsPerPage}"/>',TMPL_inputPage:'<input type="text" class="{inputPageClass}" value="{page}"/>',model:null,_pagHTML:null,_cssPre:"yui3-pagview",_classContainer:null,_classLinkPage:null,_classLinkPageList:null,_classLinkPageActive:null,_classSelectRPP:null,_classSelectPage:null,_classInputRPP:null,_classInputPage:null,_subscr:null,_myClassName:function(){if(arguments&&arguments.length>0){var c=this._cssPre;for(var b=0;b<arguments.length;b++){c+="-"+arguments[b];}return c;}return"";},initializer:function(){this._classContainer=this._myClassName("container");this._classLinkPage=this._myClassName("link","page");this._classLinkPageList=this._myClassName("link","page","list");this._classLinkPageActive=this._myClassName("link","page","active");this._classInputPage=this._myClassName("input","page");this._classSelectPage=this._myClassName("select","page");this._classSelectRPP=this._myClassName("select","rowsperpage");this._classInputRPP=this._myClassName("input","rowsperpage");var b=this.get("container");if(a.Lang.isString(b)&&c[0]==="#"){this.set("container",a.one(b));}b=this.get("container");if(b instanceof a.Node&&b.getHTML()){this._pagHTML=b.getHTML();}else{if(b instanceof a.Node&&this.get("paginatorTemplate")){var c=this.get("paginatorTemplate");if(c&&c[0]==="#"){this._pagHTML=a.one(c).getHTML();}else{if(c){this._pagHTML=c;}}}}this._bindUI();return this;},_bindUI:function(){var b=this.get("container");this._subscr=[];if(this.get("model")){this.model=this.get("model");this._subscr.push(this.model.after("pageChange",a.bind(this._modelPageChange,this)));this._subscr.push(this.model.after("itemsPerPageChange",a.bind(this._modelStateChange,this)));this._subscr.push(this.model.after("totalItemsChange",a.bind(this._modelStateChange,this)));}this._subscr.push(this.after("render",a.bind(this._updateRPPSelect,this)));this._subscr.push(b.delegate("click",this._clickChangePage,"."+this._classLinkPage,this));this._subscr.push(b.delegate("change",this._selectChangeRowOptions,"."+this._classSelectRPP,this));this._subscr.push(b.delegate("change",this._inputChangePage,"."+this._classInputPage,this));this._subscr.push(b.delegate("change",this._selectChangeRowOptions,"."+this._classInputRPP,this));this._subscr.push(this.after(["render","pageChange"],this.resizePaginator));return this;},destructor:function(){a.Array.each(this._subscr,function(b){b.detach();});this._subscr=null;},render:function(){var o=this.get("container"),j=this.get("model"),h=j.get("totalItems"),l=j.get("itemsPerPage"),d=j.get("totalPages"),b=j.get("page")||1;if(!h||!l||!o){return this;}var c="",e=this.get("pageLinkTemplate"),m=0,f=0;if(this._pagHTML.search(/{pageLinks}/)!==-1){for(var g=0;g<d;g++){plClass=this._classLinkPage+" "+this._classLinkPageList;if(g+1===b){plClass+=" "+this._classLinkPageActive;}m=g*l+1,f=m+l-1;if(f>=h){f=h;}c+=a.Lang.sub(e,{page:(g+1),pageLinkClass:plClass||"",pageStartIndex:m,pageEndIndex:f});}}var k=this._pagHTML;o.setStyle("visibility","hidden");o.setHTML("");k='<div class="{pagClass}" tabindex="-1">'+k+"</div>";var n=a.substitute(k,a.mix({pageLinks:c||"",pageLinkClass:this._classLinkPage,pagClass:this._classContainer,selectRowsPerPage:this.TMPL_selectRPP||"",selectPage:this.TMPL_selectPage||"",inputPage:this.TMPL_inputPage||"",inputRowsPerPage:this.TMPL_inputRPP||"",selectRPPClass:this._classSelectRPP,selectPageClass:this._classSelectPage,inputRPPClass:this._classInputRPP,inputPageClass:this._classInputPage},j.getAttrs()),null,true);o.append(n);o.setStyle("visibility","");this._processPageChange(b);this.fire("render");
+return this;},_processPageChange:function(b){var g=this.get("model"),c=g.get("totalPages"),f=g.get("lastPage"),e=this.get("maxPageLinks"),m=this.get("container"),j=this.get("linkListOffset"),k=m.all("."+this._classLinkPageList);if(k&&this.get("linkHighLight")){var i=(k&&(b-1)<k.size())?k.item(b-1):null;if(i){i.addClass(this._classLinkPageActive);}if(f&&f!==b){i=(k&&(f-1)<k.size())?k.item(f-1):null;if(i){i.removeClass(this._classLinkPageActive);}}}if(m.one("."+this._classInputPage)){m.one("."+this._classInputPage).set("value",b);}if(m.one("."+this._classInputRPP)){m.one("."+this._classInputRPP).set("value",g.get("itemsPerPage"));}if(b===1&&!this.get("circular")){this._disablePageSelector(["first","prev"]);this._disablePageSelector(["last","next"],true);}else{if(b===c&&!this.get("circular")){this._disablePageSelector(["first","prev"],true);this._disablePageSelector(["last","next"]);}else{this._disablePageSelector(["first","prev","last","next"],true);}}this.fire("pageChange",{state:g.getAttrs()});if(c<=e||!k||(k&&k.size()==0)){return;}var h=a.Node.create('<span class="'+this._myClassName("more")+'">'+this.get("pageLinkFiller")+"</span>"),d=a.Node.create('<span class="'+this._myClassName("more")+'">'+this.get("pageLinkFiller")+"</span>");m.all("."+this._myClassName("more")).remove();var l=this._calcOffset(b,j);k.each(function(o,n){if(n==0&&this.get("alwaysShowFirst")||n==c-1&&this.get("alwaysShowLast")){return true;}if(n+1<l.left||n+1>l.right){o.addClass(this._myClassName("hide"));}else{o.removeClass(this._myClassName("hide"));}},this);if(l.left-j>0){k.item(l.left-1).insert(h,"before");}if(l.right+j<=c){k.item(l.right-1).insert(d,"after");}return true;},_calcOffset:function(d,f){var c=this.get("model").get("totalPages"),b=(d-f<1)?1:(d-f),e=(d+f>c)?c:(d+f);return{left:b,right:e};},_disablePageSelector:function(b,e){b=(!a.Lang.isArray(b))?[b]:b;e=(e)?e:false;var d='[data-{suffix}="{sdata}"]',c=this.get("container");a.Array.each(b,function(g){var f=c.one(a.Lang.sub(d,{suffix:"pglink",sdata:g}));if(f){if(e){f.removeClass(this._myClassName("disabled"));}else{f.addClass(this._myClassName("disabled"));}}},this);},_setModel:function(b){if(!b){return;}this.model=b;return b;},_modelPageChange:function(c){var b=c.newVal;if(b){this._processPageChange(b);}},_modelStateChange:function(c){var b=c.newVal;if(b&&!c.silent){this.render();}},_updateRPPSelect:function(){var g=this.get("container"),c=this.get("model"),f=g.one("."+this._classSelectRPP),e=this.get("pageOptions");if(e&&f){if(a.Lang.isArray(e)){var d=f.getDOMNode().options;d.length=0;a.Array.each(e,function(h){var i=new Option(h);d[d.length]=i;});}}if(f){var b=(c&&c.get("itemsPerPage")===c.get("totalItems"))?true:false;var d=f.get("options");d.each(function(h){if(h.get("value")==c.get("itemsPerPage")||(h.get("value").search(/all/i)!==-1&&b)){h.set("selected",true);}},this);}if(g.one("."+this._classSelectPage)){this._updatePageSelect();}},_updatePageSelect:function(){var d=this.get("container"),b=this.get("model"),c=d.one("."+this._classSelectPage);},_inputChangePage:function(d){var b=d.target,f=+b.get("value")||1,c=this.get("model");if(f<1||f>c.get("totalPages")){f=1;b.set("value",f);}c.set("page",f);},_clickChangePage:function(h){var b=h.target,c=this.get("model");h.preventDefault();if(h.target.hasClass(this._myClassName("disabled"))||h.currentTarget.hasClass(this._myClassName("disabled"))){return;}var g=b.getData("pglink")||h.currentTarget.getData("pglink"),f=c.get("totalPages"),d=c.get("page");if(d&&d===g){return;}switch(g){case"first":g=1;break;case"last":g=f;break;case"prev":g=(!d)?1:(d===1)?f:d-1;break;case"next":g=(!d)?1:(d===f)?1:d+1;break;default:g=+g;}c.set("page",g);},_selectChangeRowOptions:function(c){var b=c.target,d=+b.get("value")||b.get("value");if(a.Lang.isString(d)&&d.toLowerCase()==="all"){d=this.get("model").get("totalItems");}this.get("model").set("itemsPerPage",d);this.render();}},{ATTRS:{model:{value:null,setter:"_setModel"},container:{value:null},pageOptions:{value:[10,20,"All"],validator:a.Lang.isArray},paginatorTemplate:{valueFn:function(){return this.TMPL_PAGINATOR;}},pageLinkTemplate:{valueFn:function(){return this.TMPL_LINK;}},linkHighLight:{value:true,validator:a.Lang.isBoolean},maxPageLinks:{value:9999,validator:a.Lang.isNumber},linkListOffset:{value:1,validator:a.Lang.isNumber},pageLinkFiller:{value:"...",validator:a.Lang.isString},alwaysShowFirst:{value:false,validator:a.Lang.isBoolean},alwaysShowLast:{value:false,validator:a.Lang.isBoolean},selectPageFormat:{value:"Page {page}",validator:a.Lang.isString},circular:{value:false,validator:a.Lang.isBoolean}}});},"gallery-2012.08.29-20-10",{skinnable:true,requires:["base-build","model","view","substitute"]});YUI.add("gallery-datatable-paginator",function(b){function a(){}a.ATTRS={paginator:{value:null,setter:"_setPaginator"},serverPaginationMap:{valueFn:"_defPagMap",setter:"_setPagMap",validator:b.Lang.isObject},paginationState:{valueFn:"_defPagState",setter:"_setPagState",getter:"_getPagState"},requestStringTemplate:{value:"",validator:b.Lang.isString},paginatorResize:{value:false,validator:b.Lang.isBoolean}};b.mix(a.prototype,{_mlistArray:null,_pagDataSrc:null,paginator:null,pagModel:null,initializer:function(){if(this.get("paginator")){this.paginator=this.get("paginator");this._eventHandles.paginator=[];this._eventHandles.paginator.push(this.data.after("response",this._afterMLResponse,this));if(this.paginator.get("model")){this.pagModel=this.get("paginator").get("model");this._eventHandles.paginator.push(this.pagModel.after("pageChange",b.bind(this._pageChangeListener,this)));}if(this.get("data")&&this.get("data").size()>0){this._setLocalData();}this._eventHandles.paginator.push(this.data.after(["load","change","add","remove","reset"],b.bind(this._dataChange,this)));this._eventHandles.paginator.push(b.Do.after(this._afterSyncUI,this,"_syncUI",this));this._eventHandles.paginator.push(this.after("renderView",this._notifyRender));}return this;},destructor:function(){b.Array.each(this._eventHandles.paginator,function(c){c.detach();});this._mlistArray=null;this._eventHandles.paginator=null;},processPageRequest:function(m,c){var j=this._mlistArray,o=this.get("paginator"),e=o.get("model"),p=e.get("itemsPerPage");var f,q,g;if(c){f=c.itemIndexStart;q=c.itemIndexEnd||f+p;}else{f=(m-1)*p;q=f+p;q=(q>j.length)?j.length:q;g=q-f+1;}if(this._pagDataSrc!=="local"){var l={},h=this._srvPagMapObj("itemIndexStart"),i=this._srvPagMapObj("totalItems"),d=this._srvPagMapObj("itemsPerPage");l[h]=f;l[d]=p;l["sortBy"]=b.JSON.stringify(this.get("sortBy")||{})||null;l=b.mix(l,this.pagModel.getAttrs(true));l["page"]=this.pagModel.get("page");}switch(this._pagDataSrc){case"ds":var n=this.get("requestStringTemplate")||"";this.datasource.load({request:b.Lang.sub(n,l)});break;case"mlist":case"rest":this.data.load(l);break;default:var k=j.slice(f,q);this.data.reset(k,{silent:true});this.syncUI();}this.resizePaginator();this.fire("pageUpdate",{state:c,view:o});},resizePaginator:function(){if(this.get("paginatorResize")!==true){return;}b.later(25,this,function(){this._syncPaginatorSize();});},dataReset:function(c){if(c instanceof b.ModelList){this._mlistArray=[];c.each(function(d){this._mlistArray.push(d.toJSON());},this);}else{if(b.Lang.isArray(c)){this._mlistArray=[];this._mlistArray=c;}}this.processPageRequest(this.pagModel.get("page"));return this;},_srvPagMapObj:function(f,d){var e=this.get("serverPaginationMap")||{},c=e[f];d=d||"to";if(c&&d=="to"&&c.toServer){c=c.toServer;}if(c&&d!="to"&&c.fromServer){c=c.fromServer;}return c;},_afterSyncUI:function(c){if(!this._pagDataSrc){this._dataChange({});}},_dataChange:function(c){if(this._pagDataSrc){return;}if(!this.datasource&&this.data.url&&!this._pagDataSrc){this._pagDataSrc="mlist";}if(this.datasource&&!this.data.url&&!this._pagDataSrc){this._pagDataSrc="ds";this._eventHandles.paginator.push(this.datasource.get("datasource").after(["*:response","response"],b.bind(this._afterDSResponse,this)));}if(!this._pagDataSrc&&c.models&&b.Lang.isArray(c.models)&&c.models.length>0){c.preventDefault();this._setLocalData(c);}},_setLocalData:function(){var c=this.get("data");this._pagDataSrc="local";this._mlistArray=[];c.each(function(d){this._mlistArray.push(d.toJSON());},this);this.pagModel.set("totalItems",c.size());this.processPageRequest(this.pagModel.get("page"));},_afterDSResponse:function(d){var f=d.response,c=this.get("serverPaginationMap")["totalItems"]||null;if(f.results&&f.results.length>0){if(c&&f.meta&&f.meta[c]&&f.meta[c]>0){this.pagModel.set("totalItems",f.meta[c]);}}this.resizePaginator();},_afterMLResponse:function(d){var c=this.get("serverPaginationMap")["totalItems"]||null;if(d.results&&d.results.length>0){if(c&&d.meta&&d.meta[c]&&d.meta[c]>0){this.pagModel.set("totalItems",d.meta[c]);}}this.resizePaginator();},_pageChangeListener:function(d){var c=+d.newVal||1;this.processPageRequest(c,this.pagModel.getAttrs(true));},_syncPaginatorSize:function(){var c=this.get("boundingBox").one("table");if(!c){return false;}this.paginator.get("container").setStyle("width",c.getComputedStyle("width"));this.fire("paginatorResize");return true;},_defPagMap:function(){return{totalItems:"totalItems",itemsPerPage:"itemsPerPage",page:"page",itemIndexStart:"itemIndexStart"};},_setPagMap:function(d){var c=this._defPagMap();return b.merge(c,d);},_defPagState:function(){var c={};if(this.get("paginator")&&this.get("paginator").model){c=this.get("paginator").model.getAttrs();c.sortBy=this.get("sortBy");}return c;},_getPagState:function(){var c=(this.pagModel)?this.pagModel.getAttrs(true):{};delete c.initialized;c.sortBy=this.get("sortBy");return c;},_setPagState:function(c){if(c.initialized!==undefined){delete c.initialized;}if(c.sortBy!==undefined){this.set("sortBy",c.sortBy);}if(this.pagModel){this.pagModel.setAttrs(c);}return c;},_setPaginator:function(c){if(!c){return;}this.paginator=c;this.initializer();return c;},_notifyRender:function(){if(this.get("paginatorResize")===true){this.resizePaginator();}this.fire("render");}});b.DataTable.Paginator=a;b.Base.mix(b.DataTable,[b.DataTable.Paginator]);},"gallery-2012.09.05-20-01",{requires:["base-build","datatable-base","event-custom"],skinnable:false}); \ No newline at end of file
diff --git a/js/yui3/gallery-datatable-row-expansion-bmo/assets/skins/sam/closed.png b/js/yui3/gallery-datatable-row-expansion-bmo/assets/skins/sam/closed.png
new file mode 100644
index 000000000..019c18e14
--- /dev/null
+++ b/js/yui3/gallery-datatable-row-expansion-bmo/assets/skins/sam/closed.png
Binary files differ
diff --git a/js/yui3/gallery-datatable-row-expansion-bmo/assets/skins/sam/gallery-datatable-row-expansion-bmo.css b/js/yui3/gallery-datatable-row-expansion-bmo/assets/skins/sam/gallery-datatable-row-expansion-bmo.css
new file mode 100644
index 000000000..7ea55b74d
--- /dev/null
+++ b/js/yui3/gallery-datatable-row-expansion-bmo/assets/skins/sam/gallery-datatable-row-expansion-bmo.css
@@ -0,0 +1 @@
+.yui3-skin-sam .yui3-datatable tr.row-expansion td.post-row-expansion{border-top:1px solid #cbcbcb}.yui3-skin-sam .yui3-datatable .row-toggle a.row-expand-nub{padding:0 8px;height:14px;margin-left:2px;*display:inline-block}.yui3-skin-sam .yui3-datatable .row-closed a.row-expand-nub{background:url(closed.png) no-repeat}.yui3-skin-sam .yui3-datatable .row-open a.row-expand-nub{background:url(open.png) no-repeat}#yui3-css-stamp.skin-sam-gallery-datatable-row-expansion{display:none}
diff --git a/js/yui3/gallery-datatable-row-expansion-bmo/assets/skins/sam/open.png b/js/yui3/gallery-datatable-row-expansion-bmo/assets/skins/sam/open.png
new file mode 100644
index 000000000..dc7805017
--- /dev/null
+++ b/js/yui3/gallery-datatable-row-expansion-bmo/assets/skins/sam/open.png
Binary files differ
diff --git a/js/yui3/gallery-datatable-row-expansion-bmo/gallery-datatable-row-expansion-bmo-min.js b/js/yui3/gallery-datatable-row-expansion-bmo/gallery-datatable-row-expansion-bmo-min.js
new file mode 100644
index 000000000..e3f87804c
--- /dev/null
+++ b/js/yui3/gallery-datatable-row-expansion-bmo/gallery-datatable-row-expansion-bmo-min.js
@@ -0,0 +1,383 @@
+YUI.add('gallery-datatable-row-expansion-bmo', function (Y, NAME) {
+
+"use strict";
+
+/**
+ * @module gallery-datatable-row-expansion
+ */
+
+/**********************************************************************
+ * <p>Plugin for DataTable to show additional information for each row via
+ * a twistdown. The result of the template is displayed spanning all the
+ * columns beyond the twistdown column.</p>
+ *
+ * <p>This class patches `getCell` and `getRow` to ignore the additional
+ * rows created by this plugin.</p>
+ *
+ * @main gallery-datatable-row-expansion
+ * @class DataTableRowExpansion
+ * @namespace Plugin
+ * @extends Plugin.Base
+ * @constructor
+ * @param config {Object} configuration
+ */
+function RowExpansion(
+ /* object */ config)
+{
+ RowExpansion.superclass.constructor.call(this, config);
+}
+
+RowExpansion.NAME = "DataTableRowExpansionPlugin";
+RowExpansion.NS = "rowexpander";
+
+RowExpansion.ATTRS =
+{
+ /**
+ * String template or function that returns a string.
+ *
+ * @attribute template
+ * @type {String|Function}
+ * @required
+ */
+ template:
+ {
+ value: '',
+ validator: function(value)
+ {
+ return (Y.Lang.isString(value) || Y.Lang.isFunction(value));
+ }
+ },
+
+ /**
+ * Id of a column (usually not displayed) that yields a
+ * unique value for each record. Used to maintain the twistdown state
+ * when paginating.
+ *
+ * @attribute uniqueIdKey
+ * @type {String}
+ * @required
+ */
+ uniqueIdKey:
+ {
+ value: '',
+ validator: Y.Lang.isString
+ }
+};
+
+/**
+ * The key used to indicate which column contains the twistdown.
+ *
+ * @property Y.RowExpansion.column_key
+ * @type {String}
+ * @value "row-expander"
+ */
+RowExpansion.column_key = 'row-expander';
+
+/**
+ * The class added to rows created by this plugin.
+ *
+ * @property Y.RowExpansion.row_class
+ * @type {String}
+ * @value "row-expansion"
+ */
+RowExpansion.row_class = 'row-expansion';
+
+function insertRow(o)
+{
+ var plugin = this.rowexpander;
+
+ var pre_cells = '';
+ for (var i=0; i<=plugin.col_count.pre; i++)
+ {
+ pre_cells += '<td class="yui3-datatable-cell pre-row-expansion">&nbsp;</td>';
+ }
+
+ var tmpl = plugin.get('template');
+ if (Y.Lang.isFunction(tmpl))
+ {
+ var s = tmpl.call(this, o.data);
+ }
+ else
+ {
+ var s = Y.Lang.sub(tmpl, o.data);
+ }
+
+ var row = o.cell.ancestor();
+ var extra_row = Y.Lang.sub(
+ '<tr class="{c}">' +
+ '{pre}' +
+ '<td colspan="{post}" class="yui3-datatable-cell post-row-expansion">{tmpl}</td>' +
+ '</tr>',
+ {
+ c: row.get('className') + ' ' + RowExpansion.row_class,
+ pre: pre_cells,
+ post: plugin.col_count.post,
+ tmpl: s
+ });
+
+ row.insert(extra_row, 'after');
+}
+
+function formatTwistdown(o)
+{
+ var plugin = this.rowexpander,
+ row_id = o.data[ plugin.get('uniqueIdKey') ],
+ open = plugin.open_rows[ row_id ];
+
+ o.td.addClass('row-toggle');
+ o.td.replaceClass('row-(open|closed)', open ? 'row-open' : 'row-closed');
+
+ o.td.on('click', function()
+ {
+ var open = plugin.open_rows[ row_id ] = ! plugin.open_rows[ row_id ];
+
+ if (open)
+ {
+ insertRow.call(this, o);
+ o.td.replaceClass('row-(open|closed)', open ? 'row-open' : 'row-closed');
+ }
+ else
+ {
+ o.cell.ancestor().next().remove();
+ o.td.replaceClass('row-(open|closed)', open ? 'row-open' : 'row-closed');
+ }
+ },
+ this);
+
+ o.cell.set('innerHTML', '<a class="row-expand-nub" href="javascript:void(0);"></a>');
+
+ if (open)
+ {
+ insertRow.call(this, o);
+ }
+}
+
+function analyzeColumns()
+{
+ function countColumns(result, col)
+ {
+ if (col.key == RowExpansion.column_key)
+ {
+ col.nodeFormatter = formatTwistdown;
+ result.found = true;
+ }
+ else if (col.children)
+ {
+ result = Y.reduce(col.children, result, countColumns);
+ }
+ else
+ {
+ result[ result.found ? 'post' : 'pre' ]++;
+ }
+ return result;
+ }
+
+ this.col_count = Y.reduce(
+ this.get('host').get('columns'),
+ { pre:0, post:0, found:false },
+ countColumns);
+}
+
+var shift_map =
+{
+ above: [-1, 0],
+ below: [ 1, 0],
+ next: [ 0, 1],
+ prev: [ 0, -1],
+ previous: [ 0, -1]
+};
+
+/*
+Returns the `<td>` Node from the given row and column index. Alternately,
+the `seed` can be a Node. If so, the nearest ancestor cell is returned.
+If the `seed` is a cell, it is returned. If there is no cell at the given
+coordinates, `null` is returned.
+
+Optionally, include an offset array or string to return a cell near the
+cell identified by the `seed`. The offset can be an array containing the
+number of rows to shift followed by the number of columns to shift, or one
+of "above", "below", "next", or "previous".
+
+<pre><code>// Previous cell in the previous row
+var cell = table.getCell(e.target, [-1, -1]);
+
+// Next cell
+var cell = table.getCell(e.target, 'next');
+var cell = table.getCell(e.taregt, [0, 1];</pre></code>
+
+@method getCell
+@param {Number[]|Node} seed Array of row and column indexes, or a Node that
+ is either the cell itself or a descendant of one.
+@param {Number[]|String} [shift] Offset by which to identify the returned
+ cell Node
+@return {Node}
+@since 3.5.0
+*/
+function getCell(seed, shift)
+{
+ var tbody = this.tbodyNode,
+ row, cell;
+
+ if (seed && tbody)
+ {
+ if (Y.Lang.isString(shift))
+ {
+ if (shift_map[shift])
+ {
+ shift = shift_map[shift];
+ }
+ else
+ {
+ throw Error('unknown shift in getCell: ' + shift);
+ }
+ }
+
+ if (Y.Lang.isArray(seed))
+ {
+ row = tbody.get('children').item(0);
+ cell = row && row.get('children').item(seed[1]);
+ if (shift)
+ {
+ shift[0] += seed[0];
+ }
+ else
+ {
+ shift = [ seed[0], 0 ];
+ }
+ }
+ else if (seed._node)
+ {
+ cell = seed.ancestor('.' + this.getClassName('cell'), true);
+ if (cell.ancestor('tr.' + RowExpansion.row_class))
+ {
+ throw Error('getCell cannot be called with an element from an expansion row');
+ }
+ }
+
+ if (cell && shift)
+ {
+ var firstRowIndex = tbody.get('firstChild.rowIndex');
+ if (Y.Lang.isArray(shift))
+ {
+ row = cell.ancestor();
+ var delta = Math.sign(shift[0]);
+ if (delta !== 0)
+ {
+ var rows = tbody.get('children');
+ var index = row.get('rowIndex') - firstRowIndex;
+ var count = Math.abs(shift[0]);
+ for (var i=0; i<count && row; i++)
+ {
+ index += delta;
+ row = rows.item(index);
+ if (row && row.hasClass(RowExpansion.row_class))
+ {
+ index += delta;
+ row = rows.item(index);
+ }
+ }
+ }
+
+ index = cell.get('cellIndex') + shift[1];
+ cell = row && row.get('children').item(index);
+ }
+ }
+ }
+
+ return (cell || null);
+}
+
+/*
+Returns the `<tr>` Node from the given row index, Model, or Model's
+`clientId`. If the rows haven't been rendered yet, or if the row can't be
+found by the input, `null` is returned.
+
+@method getRow
+@param {Number|String|Model} id Row index, Model instance, or clientId
+@return {Node}
+@since 3.5.0
+*/
+function getRow(id)
+{
+ var tbody = this.tbodyNode,
+ row = null;
+
+ if (tbody)
+ {
+ if (id)
+ {
+ id = this._idMap[id.get ? id.get('clientId') : id] || id;
+ }
+
+ row = Y.one(Y.Lang.isNumber(id) ? this.getCell([id,0]).ancestor() : '#' + id);
+ }
+
+ return row;
+}
+
+function replaceGetters()
+{
+ var view = this.get('host').view;
+ if (view instanceof Y.DataTable.TableView &&
+ view.body instanceof Y.DataTable.BodyView)
+ {
+ var body = view.body;
+
+ this.orig_getCell = body.getCell;
+ this.orig_getRow = body.getRow;
+
+ body.getCell = getCell;
+ body.getRow = getRow;
+ }
+}
+
+function restoreGetters()
+{
+ var view = this.get('host').view;
+ if (view.body && this.orig_getCell)
+ {
+ view.body.getCell = this.orig_getCell;
+ }
+
+ if (view.body && this.orig_getRow)
+ {
+ view.body.getRow = this.orig_getRow;
+ }
+}
+
+Y.extend(RowExpansion, Y.Plugin.Base,
+{
+ initializer: function(config)
+ {
+ this.open_rows = {};
+ this.on('uniqueIdKeyChange', function()
+ {
+ this.open_rows = {};
+ });
+
+ analyzeColumns.call(this);
+ this.afterHostEvent('columnsChange', analyzeColumns);
+
+ this.afterHostEvent('table:renderTable', replaceGetters);
+ },
+
+ destructor: function()
+ {
+ restoreGetters.call(this);
+ }
+});
+
+Y.namespace("Plugin");
+Y.Plugin.DataTableRowExpansion = RowExpansion;
+
+
+}, '@VERSION@', {
+ "skinnable": "true",
+ "requires": [
+ "datatable",
+ "plugin",
+ "gallery-funcprog",
+ "gallery-node-optimizations",
+ "gallery-math"
+ ]
+});
diff --git a/js/yui3/gallery-funcprog/gallery-funcprog-min.js b/js/yui3/gallery-funcprog/gallery-funcprog-min.js
new file mode 100644
index 000000000..5354b068c
--- /dev/null
+++ b/js/yui3/gallery-funcprog/gallery-funcprog-min.js
@@ -0,0 +1 @@
+YUI.add("gallery-funcprog",function(e,t){"use strict";function n(t,n){var r=e.Array(arguments,1,!0);switch(e.Array.test(n)){case 1:return e.Array[t].apply(null,r);case 2:return r[0]=e.Array(n,0,!0),e.Array[t].apply(null,r);default:return n&&n[t]&&n!==e?(r.shift(),n[t].apply(n,r)):e.Object[t].apply(null,r)}}e.mix(e,{every:function(e,t,r,i){return n("every",e,t,r,i)},filter:function(e,t,r,i){return n("filter",e,t,r,i)},find:function(e,t,r,i){return n("find",e,t,r,i)},map:function(e,t,r,i){return n("map",e,t,r,i)},partition:function(e,t,r,i){return n("partition",e,t,r,i)},reduce:function(e,t,r,i,s){return n("reduce",e,t,r,i,s)},reduceRight:function(e,t,r,i,s){return n("reduceRight",e,t,r,i,s)},reject:function(e,t,r,i){return n("reject",e,t,r,i)}}),e.mix(e.Array,{findIndexOf:function(t,n,r){var i=-1;return e.Array.some(t,function(e,s){if(n.call(r,e,s,t))return i=s,!0}),i}}),e.Array.reduceRight=e.Lang._isNative(Array.prototype.reduceRight)?function(e,t,n,r){return Array.prototype.reduceRight.call(e,function(e,t,i,s){return n.call(r,e,t,i,s)},t)}:function(e,t,n,r){var i=t;for(var s=e.length-1;s>=0;s--)i=n.call(r,i,e[s],s,e);return i}},"@VERSION@",{requires:["oop","array-extras","gallery-object-extras"],optional:["gallery-nodelist-extras2"]});
diff --git a/js/yui3/gallery-math/gallery-math-min.js b/js/yui3/gallery-math/gallery-math-min.js
new file mode 100644
index 000000000..1d9348e48
--- /dev/null
+++ b/js/yui3/gallery-math/gallery-math-min.js
@@ -0,0 +1 @@
+YUI.add("gallery-math",function(e,t){"use strict";e.mix(Math,{sign:function(e){return e<0?-1:e>0?1:0},add:function(){return e.Array.reduce(e.Array(arguments),0,function(t,n){return e.Lang.isArray(n)&&(n=Math.add.apply(this,n)),t+n})},addReciprocals:function(){return e.Array.reduce(e.Array(arguments),0,function(t,n){return e.Lang.isArray(n)?t+Math.addReciprocals.apply(this,n):t+1/n})},parallel:function(){return 1/Math.addReciprocals.apply(this,arguments)},multiply:function(){return e.Array.reduce(e.Array(arguments),1,function(t,n){return e.Lang.isArray(n)&&(n=Math.multiply.apply(this,n)),t*n})},degreesToRadians:function(e){return e*Math.PI/180},radiansToDegrees:function(e){return e*180/Math.PI},acosh:function(e){return Math.log(e+Math.sqrt(e*e-1))},asinh:function(e){return Math.log(e+Math.sqrt(e*e+1))},atanh:function(e){return Math.log((1+e)/(1-e))/2},cosh:function(e){var t=Math.exp(e);return(t+1/t)/2},sinh:function(e){var t=Math.exp(e);return(t-1/t)/2},tanh:function(e){var t=Math.exp(2*e);return(t-1)/(t+1)}})},"@VERSION@",{requires:["array-extras"]});
diff --git a/js/yui3/gallery-node-optimizations/gallery-node-optimizations-min.js b/js/yui3/gallery-node-optimizations/gallery-node-optimizations-min.js
new file mode 100644
index 000000000..297b60ac6
--- /dev/null
+++ b/js/yui3/gallery-node-optimizations/gallery-node-optimizations-min.js
@@ -0,0 +1 @@
+YUI.add("gallery-node-optimizations",function(e,t){"use strict";var n=/^([a-z]*)\.([-_a-z0-9]+)$/i,r=/^\.([-_a-z0-9]+)$/i,i=/^[a-z]+$/i;e.Node.class_re_prefix="(?:^|\\s)(?:",e.Node.class_re_suffix=")(?:\\s|$)";var s=e.Node.prototype.ancestor;e.Node.prototype.ancestor=function(t,n){if(e.Lang.isString(t)){var o=r.exec(t);if(o&&o.length)return this.getAncestorByClassName(o[1],n);if(i.test(t))return this.getAncestorByTagName(t,n)}return s.apply(this,arguments)},e.Node.prototype.getAncestorByClassName=function(t,n){var r=this._node;n||(r=r.parentNode);while(r&&!e.DOM.hasClass(r,t)){r=r.parentNode;if(!r||!r.tagName)return null}return e.one(r)},e.Node.prototype.getAncestorByTagName=function(t,n){var r=this._node;n||(r=r.parentNode),t=t.toLowerCase();while(r&&r.tagName.toLowerCase()!=t){r=r.parentNode;if(!r||!r.tagName)return null}return e.one(r)},e.Node.prototype.getElementsByClassName=function(t,n){var r=e.Node.getDOMNode(this).getElementsByTagName(n||"*"),i=new e.NodeList;for(var s=0;s<r.length;s++){var o=r[s];e.DOM.hasClass(o,t)&&i.push(o)}return i},e.Node.prototype.getFirstElementByClassName=function(t,n){if(!n||n=="*"||n=="div"){var r=[e.Node.getDOMNode(this)],i=[];while(r.length){for(var s=0;s<r.length;s++){var o=r[s],u=o.children||o.childNodes;for(var a=0;a<u.length;a++){var f=u[a];if(e.DOM.hasClass(f,t))return e.one(f);i.push(f)}}r=i,i=[]}}else{var l=e.Node.getDOMNode(this).getElementsByTagName(n||"*");for(var s=0;s<l.length;s++){var f=l[s];if(e.DOM.hasClass(f,t))return e.one(f)}}return null}},"@VERSION@",{requires:["node-base"]});
diff --git a/js/yui3/gallery-object-extras/gallery-object-extras-min.js b/js/yui3/gallery-object-extras/gallery-object-extras-min.js
new file mode 100644
index 000000000..608ae8f57
--- /dev/null
+++ b/js/yui3/gallery-object-extras/gallery-object-extras-min.js
@@ -0,0 +1 @@
+YUI.add("gallery-object-extras",function(e,t){"use strict";e.mix(e.Object,{every:function(e,t,n,r){for(var i in e)if((r||e.hasOwnProperty(i))&&!t.call(n,e[i],i,e))return!1;return!0},filter:function(e,t,n,r){var i={};for(var s in e){var o=e[s];(r||e.hasOwnProperty(s))&&t.call(n,o,s,e)&&(i[s]=o)}return i},find:function(e,t,n,r){for(var i in e){var s=e[i];if((r||e.hasOwnProperty(i))&&t.call(n,s,i,e))return s}return null},keyOf:function(e,t,n){for(var r in e)if((n||e.hasOwnProperty(r))&&e[r]===t)return r;return null},invoke:function(t,n){var r=e.Array(arguments,2,!0),i={};for(var s in t){var o=t[s];t.hasOwnProperty(s)&&e.Lang.isFunction(o[n])&&(i[s]=o[n].apply(o,r))}return i},map:function(e,t,n,r){var i={};for(var s in e)if(r||e.hasOwnProperty(s))i[s]=t.call(n,e[s],s,e);return i},partition:function(e,t,n,r){var i={matches:{},rejects:{}};for(var s in e){var o=e[s];if(r||e.hasOwnProperty(s)){var u=t.call(n,o,s,e)?i.matches:i.rejects;u[s]=o}}return i},reduce:function(e,t,n,r,i){var s=t;for(var o in e)if(i||e.hasOwnProperty(o))s=n.call(r,s,e[o],o,e);return s},reject:function(t,n,r,i){return e.Object.filter(t,function(e,t,i){return!n.call(r,e,t,i)},r,i)},zip:function(t,n){var r={};return e.Array.each(t,function(e,t){r[e.toString()]=n[t]}),r}}),e.Object.reduceRight=e.Object.reduce,e.mix(e.Array,{toObject:function(t,n){var r={};return e.Array.each(t,function(e){r[e[n]]=e}),r}})},"@VERSION@",{requires:[""],optional:["gallery-funcprog"]});
diff --git a/js/yui3/gallery-paginator-view/assets/skins/sam/gallery-paginator-view.css b/js/yui3/gallery-paginator-view/assets/skins/sam/gallery-paginator-view.css
new file mode 100644
index 000000000..11a371aa4
--- /dev/null
+++ b/js/yui3/gallery-paginator-view/assets/skins/sam/gallery-paginator-view.css
@@ -0,0 +1 @@
+.yui3-pagview-link-page{border:1px solid #b0c4de;padding:5px;text-decoration:none;color:blue}.yui3-pagview-link-page a{width:35px;height:35px;text-align:center;color:#324759;text-decoration:none;border:1px solid #324759;padding:5px;margin-top:5px;margin-right:3px;margin-left:3px;margin-bottom:5px}.yui3-pagview-bar{background:#fff url("http://yui.yahooapis.com/3.6.0/build/assets/skins/sam/sprite.png") repeat-x 0 0;background-image:-webkit-linear-gradient(transparent 40%,rgba(0,0,0,0.21));background-image:-moz-linear-gradient(top,transparent 40%,rgba(0,0,0,0.21));background-image:-ms-linear-gradient(transparent 40%,rgba(0,0,0,0.21));background-image:-o-linear-gradient(transparent 40%,rgba(0,0,0,0.21));background-image:linear-gradient(transparent 40%,rgba(0,0,0,0.21));-moz-border-bottom-colors:none;-moz-border-image:none;-moz-border-left-colors:none;-moz-border-right-colors:none;-moz-border-top-colors:none;border-color:-moz-use-text-color #cbcbcb -moz-use-text-color -moz-use-text-color;border:1px solid #cbcbcb;vertical-align:middle}.yui3-pagview-link-page-list{width:10px;margin:0 2px 0 2px}.yui3-pagview-container{outline:0}.yui3-pagview-link-page-active{background-color:#90ee90;color:black}.yui3-pagview-disabled{opacity:.2;cursor:default}.yui3-pagview-hide{display:none}.yui3-pagview-input-page{width:15px;height:13px;text-align:center}#yui3-css-stamp.skin-sam-gallery-paginator-view{display:none}
diff --git a/js/yui3/gesture-simulate/gesture-simulate-min.js b/js/yui3/gesture-simulate/gesture-simulate-min.js
new file mode 100644
index 000000000..7f9e45324
--- /dev/null
+++ b/js/yui3/gesture-simulate/gesture-simulate-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("gesture-simulate",function(e,t){function T(n){n||e.error(t+": invalid target node"),this.node=n,this.target=e.Node.getDOMNode(n);var r=this.node.getXY(),i=this._getDims();a=r[0]+i[0]/2,f=r[1]+i[1]/2}var t="gesture-simulate",n=e.config.win&&"ontouchstart"in e.config.win&&!e.UA.phantomjs&&!(e.UA.chrome&&e.UA.chrome<6),r={tap:1,doubletap:1,press:1,move:1,flick:1,pinch:1,rotate:1},i={touchstart:1,touchmove:1,touchend:1,touchcancel:1},s=e.config.doc,o,u=20,a,f,l={HOLD_TAP:10,DELAY_TAP:10,HOLD_PRESS:3e3,MIN_HOLD_PRESS:1e3,MAX_HOLD_PRESS:6e4,DISTANCE_MOVE:200,DURATION_MOVE:1e3,MAX_DURATION_MOVE:5e3,MIN_VELOCITY_FLICK:1.3,DISTANCE_FLICK:200,DURATION_FLICK:1e3,MAX_DURATION_FLICK:5e3,DURATION_PINCH:1e3},c="touchstart",h="touchmove",p="touchend",d="gesturestart",v="gesturechange",m="gestureend",g="mouseup",y="mousemove",b="mousedown",w="click",E="dblclick",S="x",x="y";T.prototype={_toRadian:function(e){return e*(Math.PI/180)},_getDims:function(){var e,t,n;return this.target.getBoundingClientRect?(e=this.target.getBoundingClientRect(),"height"in e?n=e.height:n=Math.abs(e.bottom-e.top),"width"in e?t=e.width:t=Math.abs(e.right-e.left)):(e=this.node.get("region"),t=e.width,n=e.height),[t,n]},_calculateDefaultPoint:function(t){var n;return!e.Lang.isArray(t)||t.length===0?t=[a,f]:(t.length==1&&(n=this._getDims[1],t[1]=n/2),t[0]=this.node.getX()+t[0],t[1]=this.node.getY()+t[1]),t},rotate:function(n,r,i,s,o,u,a){var f,l=i,c=s;if(!e.Lang.isNumber(l)||!e.Lang.isNumber(c)||l<0||c<0)f=this.target.offsetWidth<this.target.offsetHeight?this.target.offsetWidth/4:this.target.offsetHeight/4,l=f,c=f;e.Lang.isNumber(a)||e.error(t+"Invalid rotation detected."),this.pinch(n,r,l,c,o,u,a)},pinch:function(n,r,i,s,o,a,f){var g,y,b=u,w,E=0,S=i,x=s,T,N,C,k,L,A,O,M,_,D={start:[],end:[]},P={start:[],end:[]},H,B;r=this._calculateDefaultPoint(r),(!e.Lang.isNumber(S)||!e.Lang.isNumber(x)||S<0||x<0)&&e.error(t+"Invalid startRadius and endRadius detected.");if(!e.Lang.isNumber(o)||o<=0)o=l.DURATION_PINCH;if(!e.Lang.isNumber(a))a=0;else{a%=360;while(a<0)a+=360}e.Lang.isNumber(f)||(f=0),e.AsyncQueue.defaults.timeout=b,g=new e.AsyncQueue,N=r[0],C=r[1],O=a,M=a+f,D.start=[N+S*Math.sin(this._toRadian(O)),C-S*Math.cos(this._toRadian(O))],D.end=[N+x*Math.sin(this._toRadian(M)),C-x*Math.cos(this._toRadian(M))],P.start=[N-S*Math.sin(this._toRadian(O)),C+S*Math.cos(this._toRadian(O))],P.end=[N-x*Math.sin(this._toRadian(M)),C+x*Math.cos(this._toRadian(M))],k=1,L=s/i,g.add({fn:function(){var t,n,r,i;t={pageX:D.start[0],pageY:D.start[1],clientX:D.start[0],clientY:D.start[1]},n={pageX:P.start[0],pageY:P.start[1],clientX:P.start[0],clientY:P.start[1]},i=this._createTouchList([e.merge({identifier:E++},t),e.merge({identifier:E++},n)]),r={pageX:(D.start[0]+P.start[0])/2,pageY:(D.start[0]+P.start[1])/2,clientX:(D.start[0]+P.start[0])/2,clientY:(D.start[0]+P.start[1])/2},this._simulateEvent(this.target,c,e.merge({touches:i,targetTouches:i,changedTouches:i,scale:k,rotation:O},r)),e.UA.ios>=2&&this._simulateEvent(this.target,d,e.merge({scale:k,rotation:O},r))},timeout:0,context:this}),H=Math.floor(o/b),T=(x-S)/H,A=(L-k)/H,_=(M-O)/H,B=function(t){var n=S+T*t,r=N+n*Math.sin(this._toRadian(O+_*t)),i=C-n*Math.cos(this._toRadian(O+_*t)),s=N-n*Math.sin(this._toRadian(O+_*t)),o=C+n*Math.cos(this._toRadian(O+_*t)),u=(r+s)/2,a=(i+o)/2,f,l,c,p;f={pageX:r,pageY:i,clientX:r,clientY:i},l={pageX:s,pageY:o,clientX:s,clientY:o},p=this._createTouchList([e.merge({identifier:E++},f),e.merge({identifier:E++},l)]),c={pageX:u,pageY:a,clientX:u,clientY:a},this._simulateEvent(this.target,h,e.merge({touches:p,targetTouches:p,changedTouches:p,scale:k+A*t,rotation:O+_*t},c)),e.UA.ios>=2&&this._simulateEvent(this.target,v,e.merge({scale:k+A*t,rotation:O+_*t},c))};for(y=0;y<H;y++)g.add({fn:B,args:[y],context:this});g.add({fn:function(){var t=this._getEmptyTouchList(),n,r,i,s;n={pageX:D.end[0],pageY:D.end[1],clientX:D.end[0],clientY:D.end[1]},r={pageX:P.end[0],pageY:P.end[1],clientX:P.end[0],clientY:P.end[1]},s=this._createTouchList([e.merge({identifier:E++},n),e.merge({identifier:E++},r)]),i={pageX:(D.end[0]+P.end[0])/2,pageY:(D.end[0]+P.end[1])/2,clientX:(D.end[0]+P.end[0])/2,clientY:(D.end[0]+P.end[1])/2},e.UA.ios>=2&&this._simulateEvent(this.target,m,e.merge({scale:L,rotation:M},i)),this._simulateEvent(this.target,p,e.merge({touches:t,targetTouches:t,changedTouches:s,scale:L,rotation:M},i))},context:this}),n&&e.Lang.isFunction(n)&&g.add({fn:n,context:this.node}),g.run()},tap:function(t,r,i,s,o){var u=new e.AsyncQueue,a=this._getEmptyTouchList(),f,h,d,v,m;r=this._calculateDefaultPoint(r);if(!e.Lang.isNumber(i)||i<1)i=1;e.Lang.isNumber(s)||(s=l.HOLD_TAP),e.Lang.isNumber(o)||(o=l.DELAY_TAP),h={pageX:r[0],pageY:r[1],clientX:r[0],clientY:r[1]},f=this._createTouchList([e.merge({identifier:0},h)]),v=function(){this._simulateEvent(this.target,c,e.merge({touches:f,targetTouches:f,changedTouches:f},h))},m=function(){this._simulateEvent(this.target,p,e.merge({touches:a,targetTouches:a,changedTouches:f},h))};for(d=0;d<i;d++)u.add({fn:v,context:this,timeout:d===0?0:o}),u.add({fn:m,context:this,timeout:s});i>1&&!n&&u.add({fn:function(){this._simulateEvent(this.target,E,h)},context:this}),t&&e.Lang.isFunction(t)&&u.add({fn:t,context:this.node}),u.run()},flick:function(n,r,i,s,o){var u;r=this._calculateDefaultPoint(r),e.Lang.isString(i)?(i=i.toLowerCase(),i!==S&&i!==x&&e.error(t+"(flick): Only x or y axis allowed")):i=S,e.Lang.isNumber(s)||(s=l.DISTANCE_FLICK),e.Lang.isNumber(o)?o>l.MAX_DURATION_FLICK&&(o=l.MAX_DURATION_FLICK):o=l.DURATION_FLICK,Math.abs(s)/o<l.MIN_VELOCITY_FLICK&&(o=Math.abs(s)/l.MIN_VELOCITY_FLICK),u={start:e.clone(r),end:[i===S?r[0]+s:r[0],i===x?r[1]+s:r[1]]},this._move(n,u,o)},move:function(t,n,r){var i;e.Lang.isObject(n)?(e.Lang.isArray(n.point)?n.point=this._calculateDefaultPoint(n.point):n.point=this._calculateDefaultPoint([]),e.Lang.isNumber(n.xdist)||(n.xdist=l.DISTANCE_MOVE),e.Lang.isNumber(n.ydist)||(n.ydist=0)):n={point:this._calculateDefaultPoint([]),xdist:l.
+DISTANCE_MOVE,ydist:0},e.Lang.isNumber(r)?r>l.MAX_DURATION_MOVE&&(r=l.MAX_DURATION_MOVE):r=l.DURATION_MOVE,i={start:e.clone(n.point),end:[n.point[0]+n.xdist,n.point[1]+n.ydist]},this._move(t,i,r)},_move:function(t,n,r){var i,s,o=u,d,v,m,g=0,y;e.Lang.isNumber(r)?r>l.MAX_DURATION_MOVE&&(r=l.MAX_DURATION_MOVE):r=l.DURATION_MOVE,e.Lang.isObject(n)?(e.Lang.isArray(n.start)||(n.start=[a,f]),e.Lang.isArray(n.end)||(n.end=[a+l.DISTANCE_MOVE,f])):n={start:[a,f],end:[a+l.DISTANCE_MOVE,f]},e.AsyncQueue.defaults.timeout=o,i=new e.AsyncQueue,i.add({fn:function(){var t={pageX:n.start[0],pageY:n.start[1],clientX:n.start[0],clientY:n.start[1]},r=this._createTouchList([e.merge({identifier:g++},t)]);this._simulateEvent(this.target,c,e.merge({touches:r,targetTouches:r,changedTouches:r},t))},timeout:0,context:this}),d=Math.floor(r/o),v=(n.end[0]-n.start[0])/d,m=(n.end[1]-n.start[1])/d,y=function(t){var r=n.start[0]+v*t,i=n.start[1]+m*t,s={pageX:r,pageY:i,clientX:r,clientY:i},o=this._createTouchList([e.merge({identifier:g++},s)]);this._simulateEvent(this.target,h,e.merge({touches:o,targetTouches:o,changedTouches:o},s))};for(s=0;s<d;s++)i.add({fn:y,args:[s],context:this});i.add({fn:function(){var t={pageX:n.end[0],pageY:n.end[1],clientX:n.end[0],clientY:n.end[1]},r=this._createTouchList([e.merge({identifier:g},t)]);this._simulateEvent(this.target,h,e.merge({touches:r,targetTouches:r,changedTouches:r},t))},timeout:0,context:this}),i.add({fn:function(){var t={pageX:n.end[0],pageY:n.end[1],clientX:n.end[0],clientY:n.end[1]},r=this._getEmptyTouchList(),i=this._createTouchList([e.merge({identifier:g},t)]);this._simulateEvent(this.target,p,e.merge({touches:r,targetTouches:r,changedTouches:i},t))},context:this}),t&&e.Lang.isFunction(t)&&i.add({fn:t,context:this.node}),i.run()},_getEmptyTouchList:function(){return o||(o=this._createTouchList([])),o},_createTouchList:function(n){var r=[],i,o=this;return!!n&&e.Lang.isArray(n)?e.UA.android&&e.UA.android>=4||e.UA.ios&&e.UA.ios>=2?(e.each(n,function(t){t.identifier||(t.identifier=0),t.pageX||(t.pageX=0),t.pageY||(t.pageY=0),t.screenX||(t.screenX=0),t.screenY||(t.screenY=0),r.push(s.createTouch(e.config.win,o.target,t.identifier,t.pageX,t.pageY,t.screenX,t.screenY))}),i=s.createTouchList.apply(s,r)):e.UA.ios&&e.UA.ios<2?e.error(t+": No touch event simulation framework present."):(i=[],e.each(n,function(e){e.identifier||(e.identifier=0),e.clientX||(e.clientX=0),e.clientY||(e.clientY=0),e.pageX||(e.pageX=0),e.pageY||(e.pageY=0),e.screenX||(e.screenX=0),e.screenY||(e.screenY=0),i.push({target:o.target,identifier:e.identifier,clientX:e.clientX,clientY:e.clientY,pageX:e.pageX,pageY:e.pageY,screenX:e.screenX,screenY:e.screenY})}),i.item=function(e){return i[e]}):e.error(t+": Invalid touchPoints passed"),i},_simulateEvent:function(t,r,s){var o;i[r]?n?e.Event.simulate(t,r,s):this._isSingleTouch(s.touches,s.targetTouches,s.changedTouches)?(r={touchstart:b,touchmove:y,touchend:g}[r],s.button=0,s.relatedTarget=null,o=r===g?s.changedTouches:s.touches,s=e.mix(s,{screenX:o.item(0).screenX,screenY:o.item(0).screenY,clientX:o.item(0).clientX,clientY:o.item(0).clientY},!0),e.Event.simulate(t,r,s),r==g&&e.Event.simulate(t,w,s)):e.error("_simulateEvent(): Event '"+r+"' has multi touch objects that can't be simulated in your platform."):e.Event.simulate(t,r,s)},_isSingleTouch:function(e,t,n){return e&&e.length<=1&&t&&t.length<=1&&n&&n.length<=1}},e.GestureSimulation=T,e.GestureSimulation.defaults=l,e.GestureSimulation.GESTURES=r,e.Event.simulateGesture=function(n,i,s,o){n=e.one(n);var u=new e.GestureSimulation(n);i=i.toLowerCase(),!o&&e.Lang.isFunction(s)&&(o=s,s={}),s=s||{};if(r[i])switch(i){case"tap":u.tap(o,s.point,s.times,s.hold,s.delay);break;case"doubletap":u.tap(o,s.point,2);break;case"press":e.Lang.isNumber(s.hold)?s.hold<l.MIN_HOLD_PRESS?s.hold=l.MIN_HOLD_PRESS:s.hold>l.MAX_HOLD_PRESS&&(s.hold=l.MAX_HOLD_PRESS):s.hold=l.HOLD_PRESS,u.tap(o,s.point,1,s.hold);break;case"move":u.move(o,s.path,s.duration);break;case"flick":u.flick(o,s.point,s.axis,s.distance,s.duration);break;case"pinch":u.pinch(o,s.center,s.r1,s.r2,s.duration,s.start,s.rotation);break;case"rotate":u.rotate(o,s.center,s.r1,s.r2,s.duration,s.start,s.rotation)}else e.error(t+": Not a supported gesture simulation: "+i)}},"3.17.2",{requires:["async-queue","event-simulate","node-screen"]});
diff --git a/js/yui3/get-nodejs/get-nodejs-min.js b/js/yui3/get-nodejs/get-nodejs-min.js
new file mode 100644
index 000000000..65357c74c
--- /dev/null
+++ b/js/yui3/get-nodejs/get-nodejs-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("get",function(e,t){var n=require("module"),r=require("path"),i=require("fs"),s=require("request"),o=function(t,n,r){e.Lang.isFunction(t.onEnd)&&t.onEnd.call(e,n,r)},u=function(t){e.Lang.isFunction(t.onSuccess)&&t.onSuccess.call(e,t),o(t,"success","success")},a=function(t,n){n.errors=[n],e.Lang.isFunction(t.onFailure)&&t.onFailure.call(e,n,t),o(t,n,"fail")};e.Get=function(){},e.config.base=r.join(__dirname,"../"),YUI.require=require,YUI.process=process,e.Get._exec=function(e,t,i){e.charCodeAt(0)===65279&&(e=e.slice(1));var s=new n(t,module);s.filename=t,s.paths=n._nodeModulePaths(r.dirname(t)),typeof YUI._getLoadHook=="function"&&(e=YUI._getLoadHook(e,t)),s._compile("module.exports = function (YUI) {return (function () {"+e+"\n;return YUI;}).apply(global);"+"};",t),YUI=s.exports(YUI),s.loaded=!0,i(null,t)},e.Get._include=function(t,r){var o,u,a=this;if(t.match(/^https?:\/\//))o={url:t,timeout:a.timeout},s(o,function(n,i,s){n?r(n,t):e.Get._exec(s,t,r)});else{try{t=n._findPath(t,n._resolveLookupPaths(t,module.parent.parent)[1]);if(!e.config.useSync){i.readFile(t,"utf8",function(n,i){n?r(n,t):e.Get._exec(i,t,r)});return}u=i.readFileSync(t,"utf8")}catch(f){r(f,t);return}e.Get._exec(u,t,r)}},e.Get.js=function(t,n){var r=e.Array(t),i,s,o=r.length,f=0,l=function(){f===o&&u(n)};for(s=0;s<o;s++)i=r[s],e.Lang.isObject(i)&&(i=i.url),i=i.replace(/'/g,"%27"),e.Get._include(i,function(t,r){e.config||(e.config={debug:!0}),n.onProgress&&n.onProgress.call(n.context||e,r),t?a(n,t):(f++,l())});return{execute:function(){}}},e.Get.script=e.Get.js,e.Get.css=function(e,t){u(t)}},"@VERSION@");
diff --git a/js/yui3/get/get-min.js b/js/yui3/get/get-min.js
new file mode 100644
index 000000000..181988e15
--- /dev/null
+++ b/js/yui3/get/get-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("get",function(e,t){var n=e.Lang,r,i,s;e.Get=i={cssOptions:{attributes:{rel:"stylesheet"},doc:e.config.linkDoc||e.config.doc,pollInterval:50},jsOptions:{autopurge:!0,doc:e.config.scriptDoc||e.config.doc},options:{attributes:{charset:"utf-8"},purgethreshold:20},REGEX_CSS:/\.css(?:[?;].*)?$/i,REGEX_JS:/\.js(?:[?;].*)?$/i,_insertCache:{},_pending:null,_purgeNodes:[],_queue:[],abort:function(e){var t,n,r,i,s;if(!e.abort){n=e,s=this._pending,e=null;if(s&&s.transaction.id===n)e=s.transaction,this._pending=null;else for(t=0,i=this._queue.length;t<i;++t){r=this._queue[t].transaction;if(r.id===n){e=r,this._queue.splice(t,1);break}}}e&&e.abort()},css:function(e,t,n){return this._load("css",e,t,n)},js:function(e,t,n){return this._load("js",e,t,n)},load:function(e,t,n){return this._load(null,e,t,n)},_autoPurge:function(e){e&&this._purgeNodes.length>=e&&this._purge(this._purgeNodes)},_getEnv:function(){var t=e.config.doc,n=e.UA;return this._env={async:t&&t.createElement("script").async===!0||n.ie>=10,cssFail:n.gecko>=9||n.compareVersions(n.webkit,535.24)>=0,cssLoad:(!n.gecko&&!n.webkit||n.gecko>=9||n.compareVersions(n.webkit,535.24)>=0)&&!(n.chrome&&n.chrome<=18),preservesScriptOrder:!!(n.gecko||n.opera||n.ie&&n.ie>=10)}},_getTransaction:function(t,r){var i=[],o,u,a,f;n.isArray(t)||(t=[t]),r=e.merge(this.options,r),r.attributes=e.merge(this.options.attributes,r.attributes);for(o=0,u=t.length;o<u;++o){f=t[o],a={attributes:{}};if(typeof f=="string")a.url=f;else{if(!f.url)continue;e.mix(a,f,!1,null,0,!0),f=f.url}e.mix(a,r,!1,null,0,!0),a.type||(this.REGEX_CSS.test(f)?a.type="css":(!this.REGEX_JS.test(f),a.type="js")),e.mix(a,a.type==="js"?this.jsOptions:this.cssOptions,!1,null,0,!0),a.attributes.id||(a.attributes.id=e.guid()),a.win?a.doc=a.win.document:a.win=a.doc.defaultView||a.doc.parentWindow,a.charset&&(a.attributes.charset=a.charset),i.push(a)}return new s(i,r)},_load:function(e,t,n,r){var s;return typeof n=="function"&&(r=n,n={}),n||(n={}),n.type=e,n._onFinish=i._onTransactionFinish,this._env||this._getEnv(),s=this._getTransaction(t,n),this._queue.push({callback:r,transaction:s}),this._next(),s},_onTransactionFinish:function(){i._pending=null,i._next()},_next:function(){var e;if(this._pending)return;e=this._queue.shift(),e&&(this._pending=e,e.transaction.execute(e.callback))},_purge:function(t){var n=this._purgeNodes,r=t!==n,i,s;while(s=t.pop()){if(!s._yuiget_finished)continue;s.parentNode&&s.parentNode.removeChild(s),r&&(i=e.Array.indexOf(n,s),i>-1&&n.splice(i,1))}}},i.script=i.js,i.Transaction=s=function(t,n){var r=this;r.id=s._lastId+=1,r.data=n.data,r.errors=[],r.nodes=[],r.options=n,r.requests=t,r._callbacks=[],r._queue=[],r._reqsWaiting=0,r.tId=r.id,r.win=n.win||e.config.win},s._lastId=0,s.prototype={_state:"new",abort:function(e){this._pending=null,this._pendingCSS=null,this._pollTimer=clearTimeout(this._pollTimer),this._queue=[],this._reqsWaiting=0,this.errors.push({error:e||"Aborted"}),this._finish()},execute:function(e){var t=this,n=t.requests,r=t._state,i,s,o,u;if(r==="done"){e&&e(t.errors.length?t.errors:null,t);return}e&&t._callbacks.push(e);if(r==="executing")return;t._state="executing",t._queue=o=[],t.options.timeout&&(t._timeout=setTimeout(function(){t.abort("Timeout")},t.options.timeout)),t._reqsWaiting=n.length;for(i=0,s=n.length;i<s;++i)u=n[i],u.async||u.type==="css"?t._insert(u):o.push(u);t._next()},purge:function(){i._purge(this.nodes)},_createNode:function(e,t,n){var i=n.createElement(e),s,o;r||(o=n.createElement("div"),o.setAttribute("class","a"),r=o.className==="a"?{}:{"for":"htmlFor","class":"className"});for(s in t)t.hasOwnProperty(s)&&i.setAttribute(r[s]||s,t[s]);return i},_finish:function(){var e=this.errors.length?this.errors:null,t=this.options,n=t.context||this,r,i,s;if(this._state==="done")return;this._state="done";for(i=0,s=this._callbacks.length;i<s;++i)this._callbacks[i].call(n,e,this);r=this._getEventData(),e?(t.onTimeout&&e[e.length-1].error==="Timeout"&&t.onTimeout.call(n,r),t.onFailure&&t.onFailure.call(n,r)):t.onSuccess&&t.onSuccess.call(n,r),t.onEnd&&t.onEnd.call(n,r),t._onFinish&&t._onFinish()},_getEventData:function(t){return t?e.merge(this,{abort:this.abort,purge:this.purge,request:t,url:t.url,win:t.win}):this},_getInsertBefore:function(t){var n=t.doc,r=t.insertBefore,s,o;return r?typeof r=="string"?n.getElementById(r):r:(s=i._insertCache,o=e.stamp(n),(r=s[o])?r:(r=n.getElementsByTagName("base")[0])?s[o]=r:(r=n.head||n.getElementsByTagName("head")[0],r?(r.appendChild(n.createTextNode("")),s[o]=r.lastChild):s[o]=n.getElementsByTagName("script")[0]))},_insert:function(t){function c(){u._progress("Failed to load "+t.url,t)}function h(){f&&clearTimeout(f),u._progress(null,t)}var n=i._env,r=this._getInsertBefore(t),s=t.type==="js",o=t.node,u=this,a=e.UA,f,l;o||(s?l="script":!n.cssLoad&&a.gecko?l="style":l="link",o=t.node=this._createNode(l,t.attributes,t.doc)),s?(o.setAttribute("src",t.url),t.async?o.async=!0:(n.async&&(o.async=!1),n.preservesScriptOrder||(this._pending=t))):!n.cssLoad&&a.gecko?o.innerHTML=(t.attributes.charset?'@charset "'+t.attributes.charset+'";':"")+'@import "'+t.url+'";':o.setAttribute("href",t.url),s&&a.ie&&(a.ie<9||document.documentMode&&document.documentMode<9)?o.onreadystatechange=function(){/loaded|complete/.test(o.readyState)&&(o.onreadystatechange=null,h())}:!s&&!n.cssLoad?this._poll(t):(a.ie>=10?(o.onerror=function(){setTimeout(c,0)},o.onload=function(){setTimeout(h,0)}):(o.onerror=c,o.onload=h),!n.cssFail&&!s&&(f=setTimeout(c,t.timeout||3e3))),this.nodes.push(o),r.parentNode.insertBefore(o,r)},_next:function(){if(this._pending)return;this._queue.length?this._insert(this._queue.shift()):this._reqsWaiting||this._finish()},_poll:function(t){var n=this,r=n._pendingCSS,i=e.UA.webkit,s,o,u,a,f,l;if(t){r||(r=n._pendingCSS=[]),r.push(t);if(n._pollTimer)return}n._pollTimer=null;for(s=0;s<r.length;++s){f=r[s];if(i){l=f.doc.styleSheets,u=l.length,a=f.node.href;while(--u>=0)if(l[u].href===a){r.splice(s,1),s-=1,n._progress(null,f);break}
+}else try{o=!!f.node.sheet.cssRules,r.splice(s,1),s-=1,n._progress(null,f)}catch(c){}}r.length&&(n._pollTimer=setTimeout(function(){n._poll.call(n)},n.options.pollInterval))},_progress:function(e,t){var n=this.options;e&&(t.error=e,this.errors.push({error:e,request:t})),t.node._yuiget_finished=t.finished=!0,n.onProgress&&n.onProgress.call(n.context||this,this._getEventData(t)),t.autopurge&&(i._autoPurge(this.options.purgethreshold),i._purgeNodes.push(t.node)),this._pending===t&&(this._pending=null),this._reqsWaiting-=1,this._next()}}},"@VERSION@",{requires:["yui-base"]});
diff --git a/js/yui3/graphics-canvas-default/graphics-canvas-default-min.js b/js/yui3/graphics-canvas-default/graphics-canvas-default-min.js
new file mode 100644
index 000000000..05020e10b
--- /dev/null
+++ b/js/yui3/graphics-canvas-default/graphics-canvas-default-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("graphics-canvas-default",function(e,t){e.Graphic=e.CanvasGraphic,e.Shape=e.CanvasShape,e.Circle=e.CanvasCircle,e.Rect=e.CanvasRect,e.Ellipse=e.CanvasEllipse,e.Path=e.CanvasPath,e.Drawing=e.CanvasDrawing},"3.17.2");
diff --git a/js/yui3/graphics-canvas/graphics-canvas-min.js b/js/yui3/graphics-canvas/graphics-canvas-min.js
new file mode 100644
index 000000000..f48a9ac76
--- /dev/null
+++ b/js/yui3/graphics-canvas/graphics-canvas-min.js
@@ -0,0 +1,12 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("graphics-canvas",function(e,t){function T(){}function N(){N.superclass.constructor.apply(this,arguments)}var n="canvas",r="shape",i=/[a-z][^a-z]*/ig,s=/[\-]?[0-9]*[0-9|\.][0-9]*/g,o=e.config.doc,u=e.Lang,a=e.AttributeLite,f,l,c,h,p,d,v=e.DOM,m=e.Color,g=parseInt,y=parseFloat,b=u.isNumber,w=RegExp,E=m.toRGB,S=m.toHex,x=e.ClassNameManager.getClassName;T.prototype={_pathSymbolToMethod:{M:"moveTo",m:"relativeMoveTo",L:"lineTo",l:"relativeLineTo",C:"curveTo",c:"relativeCurveTo",Q:"quadraticCurveTo",q:"relativeQuadraticCurveTo",z:"closePath",Z:"closePath"},_currentX:0,_currentY:0,_toRGBA:function(e,t){return t=t!==undefined?t:1,m.re_RGB.test(e)||(e=S(e)),m.re_hex.exec(e)&&(e="rgba("+[g(w.$1,16),g(w.$2,16),g(w.$3,16)].join(",")+","+t+")"),e},_toRGB:function(e){return E(e)},setSize:function(e,t){this.get("autoSize")&&(e>this.node.getAttribute("width")&&(this.node.style.width=e+"px",this.node.setAttribute("width",e)),t>this.node.getAttribute("height")&&(this.node.style.height=t+"px",this.node.setAttribute("height",t)))},_updateCoords:function(e,t){this._xcoords.push(e),this._ycoords.push(t),this._currentX=e,this._currentY=t},_clearAndUpdateCoords:function(){var e=this._xcoords.pop()||0,t=this._ycoords.pop()||0;this._updateCoords(e,t)},_updateNodePosition:function(){var e=this.get("node"),t=this.get("x"),n=this.get("y");e.style.position="absolute",e.style.left=t+this._left+"px",e.style.top=n+this._top+"px"},_updateDrawingQueue:function(e){this._methods.push(e)},lineTo:function(){return this._lineTo.apply(this,[e.Array(arguments),!1]),this},relativeLineTo:function(){return this._lineTo.apply(this,[e.Array(arguments),!0]),this},_lineTo:function(e,t){var n=e[0],r,i,s,o,u=this._stroke&&this._strokeWeight?this._strokeWeight:0,a=t?parseFloat(this._currentX):0,f=t?parseFloat(this._currentY):0;this._lineToMethods||(this._lineToMethods=[]),i=e.length-1;if(typeof n=="string"||typeof n=="number")for(r=0;r<i;r+=2)s=parseFloat(e[r]),o=parseFloat(e[r+1]),s+=a,o+=f,this._updateDrawingQueue(["lineTo",s,o]),this._trackSize(s-u,o-u),this._trackSize(s+u,o+u),this._updateCoords(s,o);else for(r=0;r<i;r+=1)s=parseFloat(e[r][0]),o=parseFloat(e[r][1]),this._updateDrawingQueue(["lineTo",s,o]),this._lineToMethods[this._lineToMethods.length]=this._methods[this._methods.length-1],this._trackSize(s-u,o-u),this._trackSize(s+u,o+u),this._updateCoords(s,o);return this._drawingComplete=!1,this},moveTo:function(){return this._moveTo.apply(this,[e.Array(arguments),!1]),this},relativeMoveTo:function(){return this._moveTo.apply(this,[e.Array(arguments),!0]),this},_moveTo:function(e,t){var n=this._stroke&&this._strokeWeight?this._strokeWeight:0,r=t?parseFloat(this._currentX):0,i=t?parseFloat(this._currentY):0,s=parseFloat(e[0])+r,o=parseFloat(e[1])+i;return this._updateDrawingQueue(["moveTo",s,o]),this._trackSize(s-n,o-n),this._trackSize(s+n,o+n),this._updateCoords(s,o),this._drawingComplete=!1,this},curveTo:function(){return this._curveTo.apply(this,[e.Array(arguments),!1]),this},relativeCurveTo:function(){return this._curveTo.apply(this,[e.Array(arguments),!0]),this},_curveTo:function(e,t){var n,r,i,s,o,u,a,f,l,c,h,p,d,v,m,g=t?parseFloat(this._currentX):0,y=t?parseFloat(this._currentY):0;m=e.length-5;for(v=0;v<m;v+=6)i=parseFloat(e[v])+g,s=parseFloat(e[v+1])+y,o=parseFloat(e[v+2])+g,u=parseFloat(e[v+3])+y,a=parseFloat(e[v+4])+g,f=parseFloat(e[v+5])+y,this._updateDrawingQueue(["bezierCurveTo",i,s,o,u,a,f]),this._drawingComplete=!1,c=Math.max(a,Math.max(i,o)),p=Math.max(f,Math.max(s,u)),h=Math.min(a,Math.min(i,o)),d=Math.min(f,Math.min(s,u)),n=Math.abs(c-h),r=Math.abs(p-d),l=[[this._currentX,this._currentY],[i,s],[o,u],[a,f]],this._setCurveBoundingBox(l,n,r),this._currentX=a,this._currentY=f},quadraticCurveTo:function(){return this._quadraticCurveTo.apply(this,[e.Array(arguments),!1]),this},relativeQuadraticCurveTo:function(){return this._quadraticCurveTo.apply(this,[e.Array(arguments),!0]),this},_quadraticCurveTo:function(e,t){var n,r,i,s,o,u,a,f,l,c,h,p,d=e.length-3,v=t?parseFloat(this._currentX):0,m=t?parseFloat(this._currentY):0;for(p=0;p<d;p+=4)n=parseFloat(e[p])+v,r=parseFloat(e[p+1])+m,i=parseFloat(e[p+2])+v,s=parseFloat(e[p+3])+m,this._drawingComplete=!1,f=Math.max(i,n),c=Math.max(s,r),l=Math.min(i,n),h=Math.min(s,r),o=Math.abs(f-l),u=Math.abs(c-h),a=[[this._currentX,this._currentY],[n,r],[i,s]],this._setCurveBoundingBox(a,o,u),this._updateDrawingQueue(["quadraticCurveTo",n,r,i,s]),this._updateCoords(i,s);return this},drawCircle:function(e,t,n){var r=0,i=2*Math.PI,s=this._stroke&&this._strokeWeight?this._strokeWeight:0,o=n*2;return o+=s,this._drawingComplete=!1,this._trackSize(e+o,t+o),this._trackSize(e-s,t-s),this._updateCoords(e,t),this._updateDrawingQueue(["arc",e+n,t+n,n,r,i,!1]),this},drawDiamond:function(e,t,n,r){var i=n*.5,s=r*.5;return this.moveTo(e+i,t),this.lineTo(e+n,t+s),this.lineTo(e+i,t+r),this.lineTo(e,t+s),this.lineTo(e+i,t),this},drawEllipse:function(e,t,n,r){var i=8,s=-0.25*Math.PI,o=0,u,a=n/2,f=r/2,l,c=e+a,h=t+f,p,d,v,m,g,y,b=this._stroke&&this._strokeWeight?this._strokeWeight:0;p=c+Math.cos(0)*a,d=h+Math.sin(0)*f,this.moveTo(p,d);for(l=0;l<i;l++)o+=s,u=o-s/2,v=c+Math.cos(o)*a,m=h+Math.sin(o)*f,g=c+Math.cos(u)*(a/Math.cos(s/2)),y=h+Math.sin(u)*(f/Math.cos(s/2)),this._updateDrawingQueue(["quadraticCurveTo",g,y,v,m]);return this._trackSize(e+n+b,t+r+b),this._trackSize(e-b,t-b),this._updateCoords(e,t),this},drawRect:function(e,t,n,r){return this._drawingComplete=!1,this.moveTo(e,t),this.lineTo(e+n,t),this.lineTo(e+n,t+r),this.lineTo(e,t+r),this.lineTo(e,t),this},drawRoundRect:function(e,t,n,r,i,s){return this._drawingComplete=!1,this.moveTo(e,t+s),this.lineTo(e,t+r-s),this.quadraticCurveTo(e,t+r,e+i,t+r),this.lineTo(e+n-i,t+r),this.quadraticCurveTo(e+n,t+r,e+n,t+r-s),this.lineTo(e+n,t+s),this.quadraticCurveTo(e+n,t,e+n-i,t),this.lineTo(e+i,t),this.quadraticCurveTo(e,t,e,t+s),this},drawWedge:function(e,t,n,r,i,s){var o=this._stroke&&this._strokeWeight?this._strokeWeight:0,u,a,f,l,c,h,p,d,v,m,g,y=0;s=s||i
+,this._drawingComplete=!1,this._updateDrawingQueue(["moveTo",e,t]),s=s||i,Math.abs(r)>360&&(r=360),u=Math.ceil(Math.abs(r)/45),a=r/u,f=-(a/180)*Math.PI,l=n/180*Math.PI;if(u>0){h=e+Math.cos(n/180*Math.PI)*i,p=t+Math.sin(n/180*Math.PI)*s,this.lineTo(h,p);for(y=0;y<u;++y)l+=f,c=l-f/2,d=e+Math.cos(l)*i,v=t+Math.sin(l)*s,m=e+Math.cos(c)*(i/Math.cos(f/2)),g=t+Math.sin(c)*(s/Math.cos(f/2)),this._updateDrawingQueue(["quadraticCurveTo",m,g,d,v]);this._updateDrawingQueue(["lineTo",e,t])}return this._trackSize(-o,-o),this._trackSize(i*2+o,i*2+o),this},end:function(){return this._closePath(),this},closePath:function(){return this._updateDrawingQueue(["closePath"]),this._updateDrawingQueue(["beginPath"]),this},clear:function(){return this._initProps(),this.node&&this._context.clearRect(0,0,this.node.width,this.node.height),this},_getLinearGradient:function(){var t=e.Lang.isNumber,n=this.get("fill"),r=n.stops,i,s,o,u,a=r.length,f,l=0,c=0,h=this.get("width"),p=this.get("height"),d=n.rotation||0,v,m,g,y,b=l+h/2,w=c+p/2,S,x=Math.PI/180,T=parseFloat(parseFloat(Math.tan(d*x)).toFixed(8));Math.abs(T)*h/2>=p/2?(d<180?(g=c,y=c+p):(g=c+p,y=c),v=b-(w-g)/T,m=b-(w-y)/T):(d>90&&d<270?(v=l+h,m=l):(v=l,m=l+h),g=(T*(b-v)-w)*-1,y=(T*(b-m)-w)*-1),f=this._context.createLinearGradient(v,g,m,y);for(u=0;u<a;++u)o=r[u],i=o.opacity,s=o.color,S=o.offset,t(i)?(i=Math.max(0,Math.min(1,i)),s=this._toRGBA(s,i)):s=E(s),S=o.offset||u/(a-1),f.addColorStop(S,s);return f},_getRadialGradient:function(){var t=e.Lang.isNumber,n=this.get("fill"),r=n.r,i=n.fx,s=n.fy,o=n.stops,u,a,f,l,c=o.length,h,p=0,d=0,v=this.get("width"),m=this.get("height"),g,y,b,w,S,x,T,N,C,k,L,A,O;x=p+v/2,T=d+m/2,g=v*i,b=m*s,y=p+v/2,w=d+m/2,S=v*r,k=Math.sqrt(Math.pow(Math.abs(x-g),2)+Math.pow(Math.abs(T-b),2)),k>=S&&(A=k/S,A===1&&(A=1.01),N=(g-x)/A,C=(b-T)/A,N=N>0?Math.floor(N):Math.ceil(N),C=C>0?Math.floor(C):Math.ceil(C),g=x+N,b=T+C),r>=.5?(h=this._context.createRadialGradient(g,b,r,y,w,r*v),O=1):(h=this._context.createRadialGradient(g,b,r,y,w,v/2),O=r*2);for(l=0;l<c;++l)f=o[l],u=f.opacity,a=f.color,L=f.offset,t(u)?(u=Math.max(0,Math.min(1,u)),a=this._toRGBA(a,u)):a=E(a),L=f.offset||l/(c-1),L*=O,L<=1&&h.addColorStop(L,a);return h},_initProps:function(){this._methods=[],this._lineToMethods=[],this._xcoords=[0],this._ycoords=[0],this._width=0,this._height=0,this._left=0,this._top=0,this._right=0,this._bottom=0,this._currentX=0,this._currentY=0},_drawingComplete:!1,_createGraphic:function(){var t=e.config.doc.createElement("canvas");return t},getBezierData:function(e,t){var n=e.length,r=[],i,s;for(i=0;i<n;++i)r[i]=[e[i][0],e[i][1]];for(s=1;s<n;++s)for(i=0;i<n-s;++i)r[i][0]=(1-t)*r[i][0]+t*r[parseInt(i+1,10)][0],r[i][1]=(1-t)*r[i][1]+t*r[parseInt(i+1,10)][1];return[r[0][0],r[0][1]]},_setCurveBoundingBox:function(e,t,n){var r=0,i=this._currentX,s=i,o=this._currentY,u=o,a=Math.round(Math.sqrt(t*t+n*n)),f=1/a,l=this._stroke&&this._strokeWeight?this._strokeWeight:0,c;for(r=0;r<a;++r)c=this.getBezierData(e,f*r),i=isNaN(i)?c[0]:Math.min(c[0],i),s=isNaN(s)?c[0]:Math.max(c[0],s),o=isNaN(o)?c[1]:Math.min(c[1],o),u=isNaN(u)?c[1]:Math.max(c[1],u);i=Math.round(i*10)/10,s=Math.round(s*10)/10,o=Math.round(o*10)/10,u=Math.round(u*10)/10,this._trackSize(s+l,u+l),this._trackSize(i-l,o-l)},_trackSize:function(e,t){e>this._right&&(this._right=e),e<this._left&&(this._left=e),t<this._top&&(this._top=t),t>this._bottom&&(this._bottom=t),this._width=this._right-this._left,this._height=this._bottom-this._top}},e.CanvasDrawing=T,f=function(){this._transforms=[],this.matrix=new e.Matrix,f.superclass.constructor.apply(this,arguments)},f.NAME="shape",e.extend(f,e.GraphicBase,e.mix({init:function(){this.initializer.apply(this,arguments)},initializer:function(e){var t=this,n=e.graphic,r=this.get("data");t._initProps(),t.createNode(),t._xcoords=[0],t._ycoords=[0],n&&this._setGraphic(n),r&&t._parsePathData(r),t._updateHandler()},_setGraphic:function(t){var n;t instanceof e.CanvasGraphic?this._graphic=t:(n=new e.CanvasGraphic({render:t}),n._appendShape(this),this._graphic=n)},addClass:function(t){var n=this.get("node");e.DOM.addClass(n,t)},removeClass:function(t){var n=this.get("node");e.DOM.removeClass(n,t)},getXY:function(){var e=this.get("graphic"),t=e.getXY(),n=this.get("x"),r=this.get("y");return[t[0]+n,t[1]+r]},setXY:function(e){var t=this.get("graphic"),n=t.getXY(),r=e[0]-n[0],i=e[1]-n[1];this._set("x",r),this._set("y",i),this._updateNodePosition(r,i)},contains:function(t){var n=t instanceof e.Node?t._node:t;return n===this.node},test:function(t){return e.Selector.test(this.node,t)},compareTo:function(e){var t=this.node;return t===e},_getDefaultFill:function(){return{type:"solid",opacity:1,cx:.5,cy:.5,fx:.5,fy:.5,r:.5}},_getDefaultStroke:function(){return{weight:1,dashstyle:"none",color:"#000",opacity:1}},_left:0,_right:0,_top:0,_bottom:0,createNode:function(){var t=this,i=e.config.doc.createElement("canvas"),s=t.get("id"),o=t._camelCaseConcat,u=t.name;t._context=i.getContext("2d"),i.setAttribute("overflow","visible"),i.style.overflow="visible",t.get("visible")||(i.style.visibility="hidden"),i.setAttribute("id",s),s="#"+s,t.node=i,t.addClass(x(r)+" "+x(o(n,r))+" "+x(u)+" "+x(o(n,u)))},on:function(t,n){return e.Node.DOM_EVENTS[t]?e.on(t,n,"#"+this.get("id")):e.on.apply(this,arguments)},_setStrokeProps:function(t){var n,r,i,s,o,u;t?(n=t.color,r=y(t.weight),i=y(t.opacity),s=t.linejoin||"round",o=t.linecap||"butt",u=t.dashstyle,this._miterlimit=null,this._dashstyle=u&&e.Lang.isArray(u)&&u.length>1?u:null,this._strokeWeight=r,b(r)&&r>0?this._stroke=1:this._stroke=0,b(i)?this._strokeStyle=this._toRGBA(n,i):this._strokeStyle=n,this._linecap=o,s==="round"||s==="bevel"?this._linejoin=s:(s=parseInt(s,10),b(s)&&(this._miterlimit=Math.max(s,1),this._linejoin="miter"))):this._stroke=0},set:function(){var e=this;a.prototype.set.apply(e,arguments),e.initialized&&e._updateHandler()},_setFillProps:function(e){var t=b,n,r,i;e?(n=e.color,i=e.type,i==="linear"||i==="radial"?this._fillType=i:n?(r=e.opacity,t(r)?(r=Math
+.max(0,Math.min(1,r)),n=this._toRGBA(n,r)):n=E(n),this._fillColor=n,this._fillType="solid"):this._fillColor=null):(this._fillType=null,this._fillColor=null)},translate:function(e,t){this._translateX+=e,this._translateY+=t,this._addTransform("translate",arguments)},translateX:function(e){this._translateX+=e,this._addTransform("translateX",arguments)},translateY:function(e){this._translateY+=e,this._addTransform("translateY",arguments)},skew:function(){this._addTransform("skew",arguments)},skewX:function(){this._addTransform("skewX",arguments)},skewY:function(){this._addTransform("skewY",arguments)},rotate:function(){this._addTransform("rotate",arguments)},scale:function(){this._addTransform("scale",arguments)},_transform:"",_addTransform:function(t,n){n=e.Array(n),this._transform=u.trim(this._transform+" "+t+"("+n.join(", ")+")"),n.unshift(t),this._transforms.push(n),this.initialized&&this._updateTransform()},_updateTransform:function(){var e=this.node,t,n,r=this.get("transformOrigin"),i=this.matrix,s,o=this._transforms.length;if(this._transforms&&this._transforms.length>0){for(s=0;s<o;++s)t=this._transforms[s].shift(),t&&i[t].apply(i,this._transforms[s]);n=i.toCSSText()}this._graphic.addToRedrawQueue(this),r=100*r[0]+"% "+100*r[1]+"%",v.setStyle(e,"transformOrigin",r),n&&v.setStyle(e,"transform",n),this._transforms=[]},_updateHandler:function(){this._draw(),this._updateTransform()},_draw:function(){var e=this.node;this.clear(),this._closePath(),e.style.left=this.get("x")+"px",e.style.top=this.get("y")+"px"},_closePath:function(){if(!this._methods)return;var e=this.get("node"),t=this._right-this._left,n=this._bottom-this._top,r=this._context,i=[],s=this._methods.concat(),o,u,a,f,l,c=0;this._context.clearRect(0,0,e.width,e.height);if(this._methods){c=s.length;if(!c||c<1)return;for(o=0;o<c;++o){i[o]=s[o].concat(),f=i[o],l=f[0]==="quadraticCurveTo"||f[0]==="bezierCurveTo"?f.length:3;for(u=1;u<l;++u)u%2===0?f[u]=f[u]-this._top:f[u]=f[u]-this._left}e.setAttribute("width",Math.min(t,2e3)),e.setAttribute("height",Math.min(2e3,n)),r.beginPath();for(o=0;o<c;++o)f=i[o].concat(),f&&f.length>0&&(a=f.shift(),a&&(a==="closePath"?(r.closePath(),this._strokeAndFill(r)):a&&a==="lineTo"&&this._dashstyle?(f.unshift(this._xcoords[o]-this._left,this._ycoords[o]-this._top),this._drawDashedLine.apply(this,f)):r[a].apply(r,f)));this._strokeAndFill(r),this._drawingComplete=!0,this._clearAndUpdateCoords(),this._updateNodePosition(),this._methods=s}},_strokeAndFill:function(e){this._fillType&&(this._fillType==="linear"?e.fillStyle=this._getLinearGradient():this._fillType==="radial"?e.fillStyle=this._getRadialGradient():e.fillStyle=this._fillColor,e.closePath(),e.fill()),this._stroke&&(this._strokeWeight&&(e.lineWidth=this._strokeWeight),e.lineCap=this._linecap,e.lineJoin=this._linejoin,this._miterlimit&&(e.miterLimit=this._miterlimit),e.strokeStyle=this._strokeStyle,e.stroke())},_drawDashedLine:function(e,t,n,r){var i=this._context,s=this._dashstyle[0],o=this._dashstyle[1],u=s+o,a=n-e,f=r-t,l=Math.sqrt(Math.pow(a,2)+Math.pow(f,2)),c=Math.floor(Math.abs(l/u)),h=Math.atan2(f,a),p=e,d=t,v;a=Math.cos(h)*u,f=Math.sin(h)*u;for(v=0;v<c;++v)i.moveTo(p,d),i.lineTo(p+Math.cos(h)*s,d+Math.sin(h)*s),p+=a,d+=f;i.moveTo(p,d),l=Math.sqrt((n-p)*(n-p)+(r-d)*(r-d)),l>s?i.lineTo(p+Math.cos(h)*s,d+Math.sin(h)*s):l>0&&i.lineTo(p+Math.cos(h)*l,d+Math.sin(h)*l),i.moveTo(n,r)},getBounds:function(){var e=this._type,t=this.get("width"),n=this.get("height"),r=this.get("x"),i=this.get("y");return e==="path"&&(r+=this._left,i+=this._top,t=this._right-this._left,n=this._bottom-this._top),this._getContentRect(t,n,r,i)},_getContentRect:function(t,n,r,i){var s=this.get("transformOrigin"),o=s[0]*t,u=s[1]*n,a=this.matrix.getTransformArray(this.get("transform")),f=new e.Matrix,l,c=a.length,h,p,d;this._type==="path"&&(o+=r,u+=i),o=isNaN(o)?0:o,u=isNaN(u)?0:u,f.translate(o,u);for(l=0;l<c;l+=1)h=a[l],p=h.shift(),p&&f[p].apply(f,h);return f.translate(-o,-u),d=f.getContentRect(t,n,r,i),d},toFront:function(){var e=this.get("graphic");e&&e._toFront(this)},toBack:function(){var e=this.get("graphic");e&&e._toBack(this)},_parsePathData:function(t){var n,r,o,u=e.Lang.trim(t.match(i)),a,f,l,c=this._pathSymbolToMethod;if(u){this.clear(),f=u.length||0;for(a=0;a<f;a+=1)l=u[a],r=l.substr(0,1),o=l.substr(1).match(s),n=c[r],n&&(o?this[n].apply(this,o):this[n].apply(this));this.end()}},destroy:function(){var e=this.get("graphic");e?e.removeShape(this):this._destroy()},_destroy:function(){this.node&&(e.Event.purgeElement(this.node,!0),this.node.parentNode&&(this.node.style.visibility="",this.node.parentNode.removeChild(this.node)),this._context=null,this.node=null)}},e.CanvasDrawing.prototype)),f.ATTRS={transformOrigin:{valueFn:function(){return[.5,.5]}},transform:{setter:function(e){return this.matrix.init(),this._transforms=this.matrix.getTransformArray(e),this._transform=e,e},getter:function(){return this._transform}},node:{readOnly:!0,getter:function(){return this.node}},id:{valueFn:function(){return e.guid()},setter:function(e){var t=this.node;return t&&t.setAttribute("id",e),e}},width:{value:0},height:{value:0},x:{value:0},y:{value:0},visible:{value:!0,setter:function(e){var t=this.get("node"),n=e?"visible":"hidden";return t&&(t.style.visibility=n),e}},fill:{valueFn:"_getDefaultFill",setter:function(t){var n,r=this.get("fill")||this._getDefaultFill();return n=t?e.merge(r,t):null,n&&n.color&&(n.color===undefined||n.color==="none")&&(n.color=null),this._setFillProps(n),n}},stroke:{valueFn:"_getDefaultStroke",setter:function(t){var n=this.get("stroke")||this._getDefaultStroke(),r;return t&&t.hasOwnProperty("weight")&&(r=parseInt(t.weight,10),isNaN(r)||(t.weight=r)),t=t?e.merge(n,t):null,this._setStrokeProps(t),t}},autoSize:{value:!1},pointerEvents:{value:"visiblePainted"},data:{setter:function(e){return this.get("node")&&this._parsePathData(e),e}},graphic:{readOnly:!0,getter:function(){return this._graphic}}},e.CanvasShape=f,l=function(){l.superclass.constructor.apply
+(this,arguments)},l.NAME="path",e.extend(l,e.CanvasShape,{_type:"path",_draw:function(){this._closePath(),this._updateTransform()},createNode:function(){var t=this,i=e.config.doc.createElement("canvas"),s=t.name,o=t._camelCaseConcat,u=t.get("id");t._context=i.getContext("2d"),i.setAttribute("overflow","visible"),i.setAttribute("pointer-events","none"),i.style.pointerEvents="none",i.style.overflow="visible",i.setAttribute("id",u),u="#"+u,t.node=i,t.addClass(x(r)+" "+x(o(n,r))+" "+x(s)+" "+x(o(n,s)))},end:function(){return this._draw(),this}}),l.ATTRS=e.merge(e.CanvasShape.ATTRS,{width:{getter:function(){var e=this._stroke&&this._strokeWeight?this._strokeWeight*2:0;return this._width-e},setter:function(e){return this._width=e,e}},height:{getter:function(){var e=this._stroke&&this._strokeWeight?this._strokeWeight*2:0;return this._height-e},setter:function(e){return this._height=e,e}},path:{readOnly:!0,getter:function(){return this._path}}}),e.CanvasPath=l,c=function(){c.superclass.constructor.apply(this,arguments)},c.NAME="rect",e.extend(c,e.CanvasShape,{_type:"rect",_draw:function(){var e=this.get("width"),t=this.get("height");this.clear(),this.drawRect(0,0,e,t),this._closePath()}}),c.ATTRS=e.CanvasShape.ATTRS,e.CanvasRect=c,h=function(){h.superclass.constructor.apply(this,arguments)},h.NAME="ellipse",e.extend(h,f,{_type:"ellipse",_draw:function(){var e=this.get("width"),t=this.get("height");this.clear(),this.drawEllipse(0,0,e,t),this._closePath()}}),h.ATTRS=e.merge(f.ATTRS,{xRadius:{setter:function(e){this.set("width",e*2)},getter:function(){var e=this.get("width");return e&&(e*=.5),e}},yRadius:{setter:function(e){this.set("height",e*2)},getter:function(){var e=this.get("height");return e&&(e*=.5),e}}}),e.CanvasEllipse=h,p=function(){p.superclass.constructor.apply(this,arguments)},p.NAME="circle",e.extend(p,e.CanvasShape,{_type:"circle",_draw:function(){var e=this.get("radius");e&&(this.clear(),this.drawCircle(0,0,e),this._closePath())}}),p.ATTRS=e.merge(e.CanvasShape.ATTRS,{width:{setter:function(e){return this.set("radius",e/2),e},getter:function(){return this.get("radius")*2}},height:{setter:function(e){return this.set("radius",e/2),e},getter:function(){return this.get("radius")*2}},radius:{lazyAdd:!1}}),e.CanvasCircle=p,d=function(){d.superclass.constructor.apply(this,arguments)},d.NAME="canvasPieSlice",e.extend(d,e.CanvasShape,{_type:"path",_draw:function(){var e=this.get("cx"),t=this.get("cy"),n=this.get("startAngle"),r=this.get("arc"),i=this.get("radius");this.clear(),this._left=e,this._right=i,this._top=t,this._bottom=i,this.drawWedge(e,t,n,r,i),this.end()}}),d.ATTRS=e.mix({cx:{value:0},cy:{value:0},startAngle:{value:0},arc:{value:0},radius:{value:0}},e.CanvasShape.ATTRS),e.CanvasPieSlice=d,N.NAME="canvasGraphic",N.ATTRS={render:{},id:{valueFn:function(){return e.guid()},setter:function(e){var t=this._node;return t&&t.setAttribute("id",e),e}},shapes:{readOnly:!0,getter:function(){return this._shapes}},contentBounds:{readOnly:!0,getter:function(){return this._contentBounds}},node:{readOnly:!0,getter:function(){return this._node}},width:{setter:function(e){return this._node&&(this._node.style.width=e+"px"),e}},height:{setter:function(e){return this._node&&(this._node.style.height=e+"px"),e}},autoSize:{value:!1},preserveAspectRatio:{value:"xMidYMid"},resizeDown:{value:!1},x:{getter:function(){return this._x},setter:function(e){return this._x=e,this._node&&(this._node.style.left=e+"px"),e}},y:{getter:function(){return this._y},setter:function(e){return this._y=e,this._node&&(this._node.style.top=e+"px"),e}},autoDraw:{value:!0},visible:{value:!0,setter:function(e){return this._toggleVisible(e),e}}},e.extend(N,e.GraphicBase,{set:function(){var t=this,n=arguments[0],r={autoDraw:!0,autoSize:!0,preserveAspectRatio:!0,resizeDown:!0},i,s=!1;a.prototype.set.apply(t,arguments);if(t._state.autoDraw===!0&&e.Object.size(this._shapes)>0)if(u.isString&&r[n])s=!0;else if(u.isObject(n))for(i in r)if(r.hasOwnProperty(i)&&n[i]){s=!0;break}s&&t._redraw()},_x:0,_y:0,getXY:function(){var t=this._node,n;return t&&(n=e.DOM.getXY(t)),n},initializer:function(){var e=this.get("render"),t=this.get("visible")?"visible":"hidden",n=this.get("width")||0,r=this.get("height")||0;this._shapes={},this._redrawQueue={},this._contentBounds={left:0,top:0,right:0,bottom:0},this._node=o.createElement("div"),this._node.style.position="absolute",this._node.style.visibility=t,this.set("width",n),this.set("height",r),e&&this.render(e)},render:function(t){var n=t||o.body,r=this._node,i,s;return t instanceof e.Node?n=t._node:e.Lang.isString(t)&&(n=e.Selector.query(t,o.body,!0)),i=this.get("width")||parseInt(e.DOM.getComputedStyle(n,"width"),10),s=this.get("height")||parseInt(e.DOM.getComputedStyle(n,"height"),10),n.appendChild(r),r.style.display="block",r.style.position="absolute",r.style.left=this.get("x")+"px",r.style.top=this.get("y")+"px",this.set("width",i),this.set("height",s),this.parentNode=n,this},destroy:function(){this.removeAllShapes(),this._node&&(this._removeChildren(this._node),this._node.parentNode&&this._node.parentNode.removeChild(this._node),this._node=null)},addShape:function(e){e.graphic=this,this.get("visible")||(e.visible=!1);var t=this._getShapeClass(e.type),n=new t(e);return this._appendShape(n),n},_appendShape:function(e){var t=e.node,n=this._frag||this._node;this.get("autoDraw")?n.appendChild(t):this._getDocFrag().appendChild(t)},removeShape:function(e){return e instanceof f||u.isString(e)&&(e=this._shapes[e]),e&&e instanceof f&&(e._destroy(),delete this._shapes[e.get("id")]),this.get("autoDraw")&&this._redraw(),e},removeAllShapes:function(){var e=this._shapes,t;for(t in e)e.hasOwnProperty(t)&&e[t].destroy();this._shapes={}},clear:function(){this.removeAllShapes()},_removeChildren:function(e){if(e&&e.hasChildNodes()){var t;while(e.firstChild)t=e.firstChild,this._removeChildren(t),e.removeChild(t)}},_toggleVisible:function(e){var t,n=this._shapes,r=e?"visible":"hidden";if(n)for(t in n)n.hasOwnProperty(t)&&
+n[t].set("visible",e);this._node&&(this._node.style.visibility=r)},_getShapeClass:function(e){var t=this._shapeClass[e];return t?t:e},_shapeClass:{circle:e.CanvasCircle,rect:e.CanvasRect,path:e.CanvasPath,ellipse:e.CanvasEllipse,pieslice:e.CanvasPieSlice},getShapeById:function(e){var t=this._shapes[e];return t},batch:function(e){var t=this.get("autoDraw");this.set("autoDraw",!1),e(),this.set("autoDraw",t)},_getDocFrag:function(){return this._frag||(this._frag=o.createDocumentFragment()),this._frag},_redraw:function(){var t=this.get("autoSize"),n=this.get("preserveAspectRatio"),r=this.get("resizeDown")?this._getUpdatedContentBounds():this._contentBounds,i,s,o,u,a,f,l=0,c=0,h,p=this.get("node");t&&(t==="sizeContentToGraphic"?(i=r.right-r.left,s=r.bottom-r.top,o=parseFloat(v.getComputedStyle(p,"width")),u=parseFloat(v.getComputedStyle(p,"height")),h=new e.Matrix,n==="none"?(a=o/i,f=u/s):i/s!==o/u&&(i*u/s>o?(a=f=o/i,c=this._calculateTranslate(n.slice(5).toLowerCase(),s*o/i,u)):(a=f=u/s,l=this._calculateTranslate(n.slice(1,4).toLowerCase(),i*u/s,o))),v.setStyle(p,"transformOrigin","0% 0%"),l-=r.left*a,c-=r.top*f,h.translate(l,c),h.scale(a,f),v.setStyle(p,"transform",h.toCSSText())):(this.set("width",r.right),this.set("height",r.bottom))),this._frag&&(this._node.appendChild(this._frag),this._frag=null)},_calculateTranslate:function(e,t,n){var r=n-t,i;switch(e){case"mid":i=r*.5;break;case"max":i=r;break;default:i=0}return i},addToRedrawQueue:function(e){var t,n;this._shapes[e.get("id")]=e,this.get("resizeDown")||(t=e.getBounds(),n=this._contentBounds,n.left=n.left<t.left?n.left:t.left,n.top=n.top<t.top?n.top:t.top,n.right=n.right>t.right?n.right:t.right,n.bottom=n.bottom>t.bottom?n.bottom:t.bottom,this._contentBounds=n),this.get("autoDraw")&&this._redraw()},_getUpdatedContentBounds:function(){var e,t,n,r=this._shapes,i={};for(t in r)r.hasOwnProperty(t)&&(n=r[t],e=n.getBounds(),i.left=u.isNumber(i.left)?Math.min(i.left,e.left):e.left,i.top=u.isNumber(i.top)?Math.min(i.top,e.top):e.top,i.right=u.isNumber(i.right)?Math.max(i.right,e.right):e.right,i.bottom=u.isNumber(i.bottom)?Math.max(i.bottom,e.bottom):e.bottom);return i.left=u.isNumber(i.left)?i.left:0,i.top=u.isNumber(i.top)?i.top:0,i.right=u.isNumber(i.right)?i.right:0,i.bottom=u.isNumber(i.bottom)?i.bottom:0,this._contentBounds=i,i},_toFront:function(t){var n=this.get("node");t instanceof e.CanvasShape&&(t=t.get("node")),n&&t&&n.appendChild(t)},_toBack:function(t){var n=this.get("node"),r;t instanceof e.CanvasShape&&(t=t.get("node")),n&&t&&(r=n.firstChild,r?n.insertBefore(t,r):n.appendChild(t))}}),e.CanvasGraphic=N},"3.17.2",{requires:["graphics","color-base"]});
diff --git a/js/yui3/graphics-group/graphics-group-min.js b/js/yui3/graphics-group/graphics-group-min.js
new file mode 100644
index 000000000..8977444b5
--- /dev/null
+++ b/js/yui3/graphics-group/graphics-group-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("graphics-group",function(e,t){var n,r,i,s,o,u=e.Lang;n=function(){n.superclass.constructor.apply(this,arguments)},n.NAME="shapeGroup",e.extend(n,e.Path,{_draw:function(){var e=this.get("xvalues"),t=this.get("yvalues"),n,r,i,s,o=0,a,f=this.get("dimensions"),l=f.width,c=f.height,h=f.radius,p=f.yRadius,d=u.isArray(l),v=u.isArray(c),m=u.isArray(h),g=u.isArray(p);if(e&&t&&e.length>0){this.clear(),a=e.length;for(;o<a;++o)n=e[o],r=t[o],i=m?h[o]:h,s=g?p[o]:p,!isNaN(n)&&!isNaN(r)&&!isNaN(i)&&(this.drawShape({x:n,y:r,width:d?l[o]:l,height:v?c[o]:c,radius:i,yRadius:s}),this.closePath());this._closePath()}},_getRadiusCollection:function(e){var t=0,n=e.length,r=[];for(;t<n;++t)r[t]=e[t]*.5;return r}}),n.ATTRS=e.merge(e.Path.ATTRS,{dimensions:{getter:function(){var e=this._dimensions,t,n,r,i;return e.hasOwnProperty("radius")?e:(r=e.width,i=e.height,t=u.isArray(r)?this._getRadiusCollection(r):r*.5,n=u.isArray(i)?this._getRadiusCollection(i):i*.5,{width:r,height:i,radius:t,yRadius:n})},setter:function(e){return this._dimensions=e,e}},xvalues:{getter:function(){return this._xvalues},setter:function(e){this._xvalues=e}},yvalues:{getter:function(){return this._yvalues},setter:function(e){this._yvalues=e}}}),e.ShapeGroup=n,r=function(){r.superclass.constructor.apply(this,arguments)},r.NAME="circleGroup",e.extend(r,e.ShapeGroup,{drawShape:function(e){this.drawCircle(e.x,e.y,e.radius)}}),r.ATTRS=e.merge(e.ShapeGroup.ATTRS,{dimensions:{getter:function(){var e=this._dimensions,t,n,r,i;return e.hasOwnProperty("radius")?e:(r=e.width,i=e.height,t=u.isArray(r)?this._getRadiusCollection(r):r*.5,n=t,{width:r,height:i,radius:t,yRadius:n})}}}),r.ATTRS=e.ShapeGroup.ATTRS,e.CircleGroup=r,i=function(){i.superclass.constructor.apply(this,arguments)},i.NAME="rectGroup",e.extend(i,e.ShapeGroup,{drawShape:function(e){this.drawRect(e.x,e.y,e.width,e.height)}}),i.ATTRS=e.ShapeGroup.ATTRS,e.RectGroup=i,o=function(){o.superclass.constructor.apply(this,arguments)},o.NAME="diamondGroup",e.extend(o,e.ShapeGroup,{drawShape:function(e){this.drawDiamond(e.x,e.y,e.width,e.height)}}),o.ATTRS=e.ShapeGroup.ATTRS,e.DiamondGroup=o,s=function(){s.superclass.constructor.apply(this,arguments)},s.NAME="ellipseGroup",e.extend(s,e.ShapeGroup,{drawShape:function(e){this.drawEllipse(e.x,e.y,e.width,e.height)}}),s.ATTRS=e.ShapeGroup.ATTRS,e.EllipseGroup=s},"3.17.2",{requires:["graphics"]});
diff --git a/js/yui3/graphics-svg-default/graphics-svg-default-min.js b/js/yui3/graphics-svg-default/graphics-svg-default-min.js
new file mode 100644
index 000000000..b2c53c86c
--- /dev/null
+++ b/js/yui3/graphics-svg-default/graphics-svg-default-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("graphics-svg-default",function(e,t){e.Graphic=e.SVGGraphic,e.Shape=e.SVGShape,e.Circle=e.SVGCircle,e.Rect=e.SVGRect,e.Ellipse=e.SVGEllipse,e.Path=e.SVGPath,e.Drawing=e.SVGDrawing},"3.17.2");
diff --git a/js/yui3/graphics-svg/graphics-svg-min.js b/js/yui3/graphics-svg/graphics-svg-min.js
new file mode 100644
index 000000000..64757a809
--- /dev/null
+++ b/js/yui3/graphics-svg/graphics-svg-min.js
@@ -0,0 +1,12 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("graphics-svg",function(e,t){function g(){}var n="svg",r="shape",i=/[a-z][^a-z]*/ig,s=/[\-]?[0-9]*[0-9|\.][0-9]*/g,o=e.Lang,u=e.AttributeLite,a,f,l,c,h,p,d,v=e.config.doc,m=e.ClassNameManager.getClassName;g.prototype={_round:function(e){return Math.round(e*100)/100},_pathSymbolToMethod:{M:"moveTo",m:"relativeMoveTo",L:"lineTo",l:"relativeLineTo",C:"curveTo",c:"relativeCurveTo",Q:"quadraticCurveTo",q:"relativeQuadraticCurveTo",z:"closePath",Z:"closePath"},_currentX:0,_currentY:0,_type:"path",curveTo:function(){return this._curveTo.apply(this,[e.Array(arguments),!1]),this},relativeCurveTo:function(){return this._curveTo.apply(this,[e.Array(arguments),!0]),this},_curveTo:function(e,t){var n,r,i,s,o,u,a,f,l,c,h,p,d,v,m,g,y,b=t?"c":"C",w=t?parseFloat(this._currentX):0,E=t?parseFloat(this._currentY):0;this._pathArray=this._pathArray||[],this._pathType!==b?(this._pathType=b,y=[b],this._pathArray.push(y)):(y=this._pathArray[Math.max(0,this._pathArray.length-1)],y||(y=[],this._pathArray.push(y))),g=this._pathArray.length-1,this._pathArray[g]=this._pathArray[g].concat(e),m=e.length-5;for(v=0;v<m;v+=6)s=parseFloat(e[v])+w,o=parseFloat(e[v+1])+E,u=parseFloat(e[v+2])+w,a=parseFloat(e[v+3])+E,f=parseFloat(e[v+4])+w,l=parseFloat(e[v+5])+E,c=Math.max(f,Math.max(s,u)),p=Math.max(l,Math.max(o,a)),h=Math.min(f,Math.min(s,u)),d=Math.min(l,Math.min(o,a)),n=Math.abs(c-h),r=Math.abs(p-d),i=[[this._currentX,this._currentY],[s,o],[u,a],[f,l]],this._setCurveBoundingBox(i,n,r),this._currentX=f,this._currentY=l},quadraticCurveTo:function(){return this._quadraticCurveTo.apply(this,[e.Array(arguments),!1]),this},relativeQuadraticCurveTo:function(){return this._quadraticCurveTo.apply(this,[e.Array(arguments),!0]),this},_quadraticCurveTo:function(e,t){var n,r,i,s,o,u,a,f,l,c,h,p,d,v,m,g=t?"q":"Q",y=t?parseFloat(this._currentX):0,b=t?parseFloat(this._currentY):0;this._pathArray=this._pathArray||[],this._pathType!==g?(this._pathType=g,u=[g],this._pathArray.push(u)):(u=this._pathArray[Math.max(0,this._pathArray.length-1)],u||(u=[],this._pathArray.push(u))),o=this._pathArray.length-1,this._pathArray[o]=this._pathArray[o].concat(e),m=e.length-3;for(v=0;v<m;v+=4)n=parseFloat(e[v])+y,r=parseFloat(e[v+1])+b,i=parseFloat(e[v+2])+y,s=parseFloat(e[v+3])+b,c=Math.max(i,n),p=Math.max(s,r),h=Math.min(i,n),d=Math.min(s,r),a=Math.abs(c-h),f=Math.abs(p-d),l=[[this._currentX,this._currentY],[n,r],[i,s]],this._setCurveBoundingBox(l,a,f),this._currentX=i,this._currentY=s},drawRect:function(e,t,n,r){return this.moveTo(e,t),this.lineTo(e+n,t),this.lineTo(e+n,t+r),this.lineTo(e,t+r),this.lineTo(e,t),this},drawRoundRect:function(e,t,n,r,i,s){return this.moveTo(e,t+s),this.lineTo(e,t+r-s),this.quadraticCurveTo(e,t+r,e+i,t+r),this.lineTo(e+n-i,t+r),this.quadraticCurveTo(e+n,t+r,e+n,t+r-s),this.lineTo(e+n,t+s),this.quadraticCurveTo(e+n,t,e+n-i,t),this.lineTo(e+i,t),this.quadraticCurveTo(e,t,e,t+s),this},drawCircle:function(e,t,n){var r=n*2;return this._drawingComplete=!1,this._trackSize(e,t),this._trackSize(e+r,t+r),this._pathArray=this._pathArray||[],this._pathArray.push(["M",e+n,t]),this._pathArray.push(["A",n,n,0,1,0,e+n,t+r]),this._pathArray.push(["A",n,n,0,1,0,e+n,t]),this._currentX=e,this._currentY=t,this},drawEllipse:function(e,t,n,r){var i=n*.5,s=r*.5;return this._drawingComplete=!1,this._trackSize(e,t),this._trackSize(e+n,t+r),this._pathArray=this._pathArray||[],this._pathArray.push(["M",e+i,t]),this._pathArray.push(["A",i,s,0,1,0,e+i,t+r]),this._pathArray.push(["A",i,s,0,1,0,e+i,t]),this._currentX=e,this._currentY=t,this},drawDiamond:function(e,t,n,r){var i=n*.5,s=r*.5;return this.moveTo(e+i,t),this.lineTo(e+n,t+s),this.lineTo(e+i,t+r),this.lineTo(e,t+s),this.lineTo(e+i,t),this},drawWedge:function(e,t,n,r,i,s){var o,u,a,f,l,c,h,p,d,v,m,g,y=i*2,b,w;this._pathArray=this._pathArray||[],s=s||i,this._pathType!=="M"?(this._pathType="M",b=["M"],this._pathArray.push(b)):b=this._getCurrentArray(),w=this._pathArray.length-1,this._pathArray[w].push(e),this._pathArray[w].push(e),Math.abs(r)>360&&(r=360),o=Math.ceil(Math.abs(r)/45),u=r/o,a=-(u/180)*Math.PI,f=n/180*Math.PI;if(o>0){c=e+Math.cos(n/180*Math.PI)*i,h=t+Math.sin(n/180*Math.PI)*s,this._pathType="L",w++,this._pathArray[w]=["L"],this._pathArray[w].push(this._round(c)),this._pathArray[w].push(this._round(h)),w++,this._pathType="Q",this._pathArray[w]=["Q"];for(g=0;g<o;++g)f+=a,l=f-a/2,p=e+Math.cos(f)*i,d=t+Math.sin(f)*s,v=e+Math.cos(l)*(i/Math.cos(a/2)),m=t+Math.sin(l)*(s/Math.cos(a/2)),this._pathArray[w].push(this._round(v)),this._pathArray[w].push(this._round(m)),this._pathArray[w].push(this._round(p)),this._pathArray[w].push(this._round(d))}return this._currentX=e,this._currentY=t,this._trackSize(y,y),this},lineTo:function(){return this._lineTo.apply(this,[e.Array(arguments),!1]),this},relativeLineTo:function(){return this._lineTo.apply(this,[e.Array(arguments),!0]),this},_lineTo:function(e,t){var n=e[0],r,i,s,o,u,a,f=t?"l":"L",l=t?parseFloat(this._currentX):0,c=t?parseFloat(this._currentY):0;this._pathArray=this._pathArray||[],this._shapeType="path",i=e.length,this._pathType!==f?(this._pathType=f,o=[f],this._pathArray.push(o)):o=this._getCurrentArray(),s=this._pathArray.length-1;if(typeof n=="string"||typeof n=="number")for(r=0;r<i;r+=2)u=parseFloat(e[r]),a=parseFloat(e[r+1]),this._pathArray[s].push(u),this._pathArray[s].push(a),u+=l,a+=c,this._currentX=u,this._currentY=a,this._trackSize.apply(this,[u,a]);else for(r=0;r<i;++r)u=parseFloat(e[r][0]),a=parseFloat(e[r][1]),this._pathArray[s].push(u),this._pathArray[s].push(a),this._currentX=u,this._currentY=a,u+=l,a+=c,this._trackSize.apply(this,[u,a])},moveTo:function(){return this._moveTo.apply(this,[e.Array(arguments),!1]),this},relativeMoveTo:function(){return this._moveTo.apply(this,[e.Array(arguments),!0]),this},_moveTo:function(e,t){var n,r,i=parseFloat(e[0]),s=parseFloat(e[1]),o=t?"m":"M",u=t?parseFloat(this._currentX):0,a=t?parseFloat(this._currentY):0;this._pathArray=this._pathArray||[],this._pathType=o,r=[o],this._pathArray.push
+(r),n=this._pathArray.length-1,this._pathArray[n]=this._pathArray[n].concat([i,s]),i+=u,s+=a,this._currentX=i,this._currentY=s,this._trackSize(i,s)},end:function(){return this._closePath(),this},clear:function(){return this._currentX=0,this._currentY=0,this._width=0,this._height=0,this._left=0,this._right=0,this._top=0,this._bottom=0,this._pathArray=[],this._path="",this._pathType="",this},_closePath:function(){var t,n,r,i,s,o,u="",a=this.node,f=parseFloat(this._left),l=parseFloat(this._top),c=this.get("fill");if(this._pathArray){t=this._pathArray.concat();while(t&&t.length>0){n=t.shift(),i=n.length,r=n[0],r==="A"?u+=r+n[1]+","+n[2]:r==="z"||r==="Z"?u+=" z ":r==="C"||r==="c"?u+=r+(n[1]-f)+","+(n[2]-l):u+=" "+r+parseFloat(n[1]-f);switch(r){case"L":case"l":case"M":case"m":case"Q":case"q":for(o=2;o<i;++o)s=o%2===0?l:f,s=n[o]-s,u+=", "+parseFloat(s);break;case"A":s=" "+parseFloat(n[3])+" "+parseFloat(n[4]),s+=","+parseFloat(n[5])+" "+parseFloat(n[6]-f),s+=","+parseFloat(n[7]-l),u+=" "+s;break;case"C":case"c":for(o=3;o<i-1;o+=2)s=parseFloat(n[o]-f),s+=", ",s+=parseFloat(n[o+1]-l),u+=" "+s}}c&&c.color&&(u+="z"),e.Lang.trim(u),u&&a.setAttribute("d",u),this._path=u,this._fillChangeHandler(),this._strokeChangeHandler(),this._updateTransform()}},closePath:function(){return this._pathArray.push(["z"]),this},_getCurrentArray:function(){var e=this._pathArray[Math.max(0,this._pathArray.length-1)];return e||(e=[],this._pathArray.push(e)),e},getBezierData:function(e,t){var n=e.length,r=[],i,s;for(i=0;i<n;++i)r[i]=[e[i][0],e[i][1]];for(s=1;s<n;++s)for(i=0;i<n-s;++i)r[i][0]=(1-t)*r[i][0]+t*r[parseInt(i+1,10)][0],r[i][1]=(1-t)*r[i][1]+t*r[parseInt(i+1,10)][1];return[r[0][0],r[0][1]]},_setCurveBoundingBox:function(e,t,n){var r,i=this._currentX,s=i,o=this._currentY,u=o,a=Math.round(Math.sqrt(t*t+n*n)),f=1/a,l;for(r=0;r<a;++r)l=this.getBezierData(e,f*r),i=isNaN(i)?l[0]:Math.min(l[0],i),s=isNaN(s)?l[0]:Math.max(l[0],s),o=isNaN(o)?l[1]:Math.min(l[1],o),u=isNaN(u)?l[1]:Math.max(l[1],u);i=Math.round(i*10)/10,s=Math.round(s*10)/10,o=Math.round(o*10)/10,u=Math.round(u*10)/10,this._trackSize(s,u),this._trackSize(i,o)},_trackSize:function(e,t){e>this._right&&(this._right=e),e<this._left&&(this._left=e),t<this._top&&(this._top=t),t>this._bottom&&(this._bottom=t),this._width=this._right-this._left,this._height=this._bottom-this._top}},e.SVGDrawing=g,f=function(){this._transforms=[],this.matrix=new e.Matrix,this._normalizedMatrix=new e.Matrix,f.superclass.constructor.apply(this,arguments)},f.NAME="shape",e.extend(f,e.GraphicBase,e.mix({_x:0,_y:0,init:function(){this.initializer.apply(this,arguments)},initializer:function(e){var t=this,n=e.graphic,r=this.get("data");t.createNode(),n&&t._setGraphic(n),r&&t._parsePathData(r),t._updateHandler()},_setGraphic:function(t){var n;t instanceof e.SVGGraphic?this._graphic=t:(n=new e.SVGGraphic({render:t}),n._appendShape(this),this._graphic=n)},addClass:function(e){var t=this.node;t.className.baseVal=o.trim([t.className.baseVal,e].join(" "))},removeClass:function(e){var t=this.node,n=t.className.baseVal;n=n.replace(new RegExp(e+" "),e).replace(new RegExp(e),""),t.className.baseVal=n},getXY:function(){var e=this._graphic,t=e.getXY(),n=this._x,r=this._y;return[t[0]+n,t[1]+r]},setXY:function(e){var t=this._graphic,n=t.getXY();this._x=e[0]-n[0],this._y=e[1]-n[1],this.set("transform",this.get("transform"))},contains:function(t){var n=t instanceof e.Node?t._node:t;return n===this.node},compareTo:function(e){var t=this.node;return t===e},test:function(t){return e.Selector.test(this.node,t)},_getDefaultFill:function(){return{type:"solid",opacity:1,cx:.5,cy:.5,fx:.5,fy:.5,r:.5}},_getDefaultStroke:function(){return{weight:1,dashstyle:"none",color:"#000",opacity:1}},createNode:function(){var t=this,i=v.createElementNS("http://www.w3.org/2000/svg","svg:"+this._type),s=t.get("id"),o=t.name,u=t._camelCaseConcat,a=t.get("pointerEvents");t.node=i,t.addClass(m(r)+" "+m(u(n,r))+" "+m(o)+" "+m(u(n,o))),s&&i.setAttribute("id",s),a&&i.setAttribute("pointer-events",a),t.get("visible")||e.DOM.setStyle(i,"visibility","hidden"),e.DOM.setAttribute(this.node,"shape-rendering",this.get("shapeRendering"))},on:function(t,n){return e.Node.DOM_EVENTS[t]?e.on(t,n,"#"+this.get("id")):e.on.apply(this,arguments)},_strokeChangeHandler:function(){var e=this.node,t=this.get("stroke"),n,r,i,s;t&&t.weight&&t.weight>0?(s=t.linejoin||"round",n=parseFloat(t.opacity),r=t.dashstyle||"none",i=o.isArray(r)?r.toString():r,t.color=t.color||"#000000",t.weight=t.weight||1,t.opacity=o.isNumber(n)?n:1,t.linecap=t.linecap||"butt",e.setAttribute("stroke-dasharray",i),e.setAttribute("stroke",t.color),e.setAttribute("stroke-linecap",t.linecap),e.setAttribute("stroke-width",t.weight),e.setAttribute("stroke-opacity",t.opacity),s==="round"||s==="bevel"?e.setAttribute("stroke-linejoin",s):(s=parseInt(s,10),o.isNumber(s)&&(e.setAttribute("stroke-miterlimit",Math.max(s,1)),e.setAttribute("stroke-linejoin","miter")))):e.setAttribute("stroke","none")},_fillChangeHandler:function(){var e=this.node,t=this.get("fill"),n,r;t?(r=t.type,r==="linear"||r==="radial"?(this._setGradientFill(t),e.setAttribute("fill","url(#grad"+this.get("id")+")")):t.color?(n=parseFloat(t.opacity),n=o.isNumber(n)?n:1,e.setAttribute("fill",t.color),e.setAttribute("fill-opacity",n)):e.setAttribute("fill","none")):e.setAttribute("fill","none")},_setGradientFill:function(e){var t,n,r,i,s,u=o.isNumber,a=this._graphic,f=e.type,l=a.getGradientNode("grad"+this.get("id"),f),c=e.stops,h=this.get("width"),p=this.get("height"),d=e.rotation||0,v=Math.PI/180,m=parseFloat(parseFloat(Math.tan(d*v)).toFixed(8)),g,y,b,w,E="0%",S="100%",x="0%",T="0%",N=e.cx,C=e.cy,k=e.fx,L=e.fy,A=e.r,O=[];f==="linear"?(N=h/2,C=p/2,Math.abs(m)*h/2>=p/2?(d<180?(x=0,T=p):(x=p,T=0),E=N-(C-x)/m,S=N-(C-T)/m):(d>90&&d<270?(E=h,S=0):(E=0,S=h),x=(m*(N-E)-C)*-1,T=(m*(N-S)-C)*-1),E=Math.round(100*E/h),S=Math.round(100*S/h),x=Math.round(100*x/p),T=Math.round(100*T/p),E=u(E)?E:0,S=u(S)?S:100,x=u(x)?x:0,T=u(T)?T:0,l.setAttribute
+("spreadMethod","pad"),l.setAttribute("width",h),l.setAttribute("height",p),l.setAttribute("x1",E+"%"),l.setAttribute("x2",S+"%"),l.setAttribute("y1",x+"%"),l.setAttribute("y2",T+"%")):(l.setAttribute("cx",N*100+"%"),l.setAttribute("cy",C*100+"%"),l.setAttribute("fx",k*100+"%"),l.setAttribute("fy",L*100+"%"),l.setAttribute("r",A*100+"%")),y=c.length,b=0;for(g=0;g<y;++g)this._stops&&this._stops.length>0?(i=this._stops.shift(),s=!1):(i=a._createGraphicNode("stop"),s=!0),w=c[g],n=w.opacity,r=w.color,t=w.offset||g/(y-1),t=Math.round(t*100)+"%",n=u(n)?n:1,n=Math.max(0,Math.min(1,n)),b=(g+1)/y,i.setAttribute("offset",t),i.setAttribute("stop-color",r),i.setAttribute("stop-opacity",n),s&&l.appendChild(i),O.push(i);while(this._stops&&this._stops.length>0)l.removeChild(this._stops.shift());this._stops=O},_stops:null,set:function(){var e=this;u.prototype.set.apply(e,arguments),e.initialized&&e._updateHandler()},translate:function(){this._addTransform("translate",arguments)},translateX:function(){this._addTransform("translateX",arguments)},translateY:function(){this._addTransform("translateY",arguments)},skew:function(){this._addTransform("skew",arguments)},skewX:function(){this._addTransform("skewX",arguments)},skewY:function(){this._addTransform("skewY",arguments)},rotate:function(){this._addTransform("rotate",arguments)},scale:function(){this._addTransform("scale",arguments)},_addTransform:function(t,n){n=e.Array(n),this._transform=o.trim(this._transform+" "+t+"("+n.join(", ")+")"),n.unshift(t),this._transforms.push(n),this.initialized&&this._updateTransform()},_updateTransform:function(){var t=this._type==="path",n=this.node,r,i,s,o,u,a,f,l=this.matrix,c=this._normalizedMatrix,h,p=this._transforms.length;if(t||this._transforms&&this._transforms.length>0){o=this._x,u=this._y,s=this.get("transformOrigin"),a=o+s[0]*this.get("width"),f=u+s[1]*this.get("height"),t&&(this instanceof e.SVGPath||(a=this._left+s[0]*this.get("width"),f=this._top+s[1]*this.get("height")),c.init({dx:o+this._left,dy:u+this._top})),c.translate(a,f);for(h=0;h<p;++h)r=this._transforms[h].shift(),r&&(c[r].apply(c,this._transforms[h]),l[r].apply(l,this._transforms[h])),t&&this._transforms[h].unshift(r);c.translate(-a,-f),i="matrix("+c.a+","+c.b+","+c.c+","+c.d+","+c.dx+","+c.dy+")"}this._graphic.addToRedrawQueue(this),i&&n.setAttribute("transform",i),t||(this._transforms=[])},_draw:function(){var e=this.node;e.setAttribute("width",this.get("width")),e.setAttribute("height",this.get("height")),e.setAttribute("x",this._x),e.setAttribute("y",this._y),e.style.left=this._x+"px",e.style.top=this._y+"px",this._fillChangeHandler(),this._strokeChangeHandler(),this._updateTransform()},_updateHandler:function(){this._draw()},_transform:"",getBounds:function(){var e=this._type,t=this.get("stroke"),n=this.get("width"),r=this.get("height"),i=e==="path"?0:this._x,s=e==="path"?0:this._y,o=0;return t&&t.weight&&(o=t.weight,n=i+n+o-(i-o),r=s+r+o-(s-o),i-=o,s-=o),this._normalizedMatrix.getContentRect(n,r,i,s)},toFront:function(){var e=this.get("graphic");e&&e._toFront(this)},toBack:function(){var e=this.get("graphic");e&&e._toBack(this)},_parsePathData:function(t){var n,r,o,u=e.Lang.trim(t.match(i)),a,f,l,c=this._pathSymbolToMethod;if(u){this.clear(),f=u.length||0;for(a=0;a<f;a+=1)l=u[a],r=l.substr(0,1),o=l.substr(1).match(s),n=c[r],n&&(o?this[n].apply(this,o):this[n].apply(this));this.end()}},destroy:function(){var e=this.get("graphic");e?e.removeShape(this):this._destroy()},_destroy:function(){this.node&&(e.Event.purgeElement(this.node,!0),this.node.parentNode&&this.node.parentNode.removeChild(this.node),this.node=null)}},e.SVGDrawing.prototype)),f.ATTRS={transformOrigin:{valueFn:function(){return[.5,.5]}},transform:{setter:function(e){return this.matrix.init(),this._normalizedMatrix.init(),this._transforms=this.matrix.getTransformArray(e),this._transform=e,e},getter:function(){return this._transform}},id:{valueFn:function(){return e.guid()},setter:function(e){var t=this.node;return t&&t.setAttribute("id",e),e}},x:{getter:function(){return this._x},setter:function(e){var t=this.get("transform");this._x=e,t&&this.set("transform",t)}},y:{getter:function(){return this._y},setter:function(e){var t=this.get("transform");this._y=e,t&&this.set("transform",t)}},width:{value:0},height:{value:0},visible:{value:!0,setter:function(e){var t=e?"visible":"hidden";return this.node&&(this.node.style.visibility=t),e}},shapeRendering:{value:"auto",setter:function(t){return this.node&&e.DOM.setAttribute(this.node,"shape-rendering",t),t}},fill:{valueFn:"_getDefaultFill",setter:function(t){var n,r=this.get("fill")||this._getDefaultFill();return n=t?e.merge(r,t):null,n&&n.color&&(n.color===undefined||n.color==="none")&&(n.color=null),n}},stroke:{valueFn:"_getDefaultStroke",setter:function(t){var n=this.get("stroke")||this._getDefaultStroke(),r;return t&&t.hasOwnProperty("weight")&&(r=parseInt(t.weight,10),isNaN(r)||(t.weight=r)),t?e.merge(n,t):null}},pointerEvents:{valueFn:function(){var e="visiblePainted",t=this.node;return t&&t.setAttribute("pointer-events",e),e},setter:function(e){var t=this.node;return t&&t.setAttribute("pointer-events",e),e}},node:{readOnly:!0,getter:function(){return this.node}},data:{setter:function(e){return this.get("node")&&this._parsePathData(e),e}},graphic:{readOnly:!0,getter:function(){return this._graphic}}},e.SVGShape=f,h=function(){h.superclass.constructor.apply(this,arguments)},h.NAME="path",e.extend(h,e.SVGShape,{_left:0,_right:0,_top:0,_bottom:0,_type:"path",_path:""}),h.ATTRS=e.merge(e.SVGShape.ATTRS,{path:{readOnly:!0,getter:function(){return this._path}},width:{getter:function(){var e=Math.max(this._right-this._left,0);return e}},height:{getter:function(){return Math.max(this._bottom-this._top,0)}}}),e.SVGPath=h,c=function(){c.superclass.constructor.apply(this,arguments)},c.NAME="rect",e.extend(c,e.SVGShape,{_type:"rect"}),c.ATTRS=e.SVGShape.ATTRS,e.SVGRect=c,p=function(){p.superclass.constructor.apply(this,arguments)},p.NAME="ellipse"
+,e.extend(p,f,{_type:"ellipse",_draw:function(){var e=this.node,t=this.get("width"),n=this.get("height"),r=this.get("x"),i=this.get("y"),s=t*.5,o=n*.5,u=r+s,a=i+o;e.setAttribute("rx",s),e.setAttribute("ry",o),e.setAttribute("cx",u),e.setAttribute("cy",a),this._fillChangeHandler(),this._strokeChangeHandler(),this._updateTransform()}}),p.ATTRS=e.merge(f.ATTRS,{xRadius:{setter:function(e){this.set("width",e*2)},getter:function(){var e=this.get("width");return e&&(e*=.5),e}},yRadius:{setter:function(e){this.set("height",e*2)},getter:function(){var e=this.get("height");return e&&(e*=.5),e}}}),e.SVGEllipse=p,l=function(){l.superclass.constructor.apply(this,arguments)},l.NAME="circle",e.extend(l,e.SVGShape,{_type:"circle",_draw:function(){var e=this.node,t=this.get("x"),n=this.get("y"),r=this.get("radius"),i=t+r,s=n+r;e.setAttribute("r",r),e.setAttribute("cx",i),e.setAttribute("cy",s),this._fillChangeHandler(),this._strokeChangeHandler(),this._updateTransform()}}),l.ATTRS=e.merge(e.SVGShape.ATTRS,{width:{setter:function(e){return this.set("radius",e/2),e},getter:function(){return this.get("radius")*2}},height:{setter:function(e){return this.set("radius",e/2),e},getter:function(){return this.get("radius")*2}},radius:{value:0}}),e.SVGCircle=l,d=function(){d.superclass.constructor.apply(this,arguments)},d.NAME="svgPieSlice",e.extend(d,e.SVGShape,e.mix({_type:"path",_draw:function(){var e=this.get("cx"),t=this.get("cy"),n=this.get("startAngle"),r=this.get("arc"),i=this.get("radius");this.clear(),this.drawWedge(e,t,n,r,i),this.end()}},e.SVGDrawing.prototype)),d.ATTRS=e.mix({cx:{value:0},cy:{value:0},startAngle:{value:0},arc:{value:0},radius:{value:0}},e.SVGShape.ATTRS),e.SVGPieSlice=d,a=function(){a.superclass.constructor.apply(this,arguments)},a.NAME="svgGraphic",a.ATTRS={render:{},id:{valueFn:function(){return e.guid()},setter:function(e){var t=this._node;return t&&t.setAttribute("id",e),e}},shapes:{readOnly:!0,getter:function(){return this._shapes}},contentBounds:{readOnly:!0,getter:function(){return this._contentBounds}},node:{readOnly:!0,getter:function(){return this._node}},width:{setter:function(e){return this._node&&(this._node.style.width=e+"px"),e}},height:{setter:function(e){return this._node&&(this._node.style.height=e+"px"),e}},autoSize:{value:!1},preserveAspectRatio:{value:"xMidYMid"},resizeDown:{value:!1},x:{getter:function(){return this._x},setter:function(e){return this._x=e,this._node&&(this._node.style.left=e+"px"),e}},y:{getter:function(){return this._y},setter:function(e){return this._y=e,this._node&&(this._node.style.top=e+"px"),e}},autoDraw:{value:!0},visible:{value:!0,setter:function(e){return this._toggleVisible(e),e}},pointerEvents:{value:"none"}},e.extend(a,e.GraphicBase,{set:function(){var t=this,n=arguments[0],r={autoDraw:!0,autoSize:!0,preserveAspectRatio:!0,resizeDown:!0},i,s=!1;u.prototype.set.apply(t,arguments);if(t._state.autoDraw===!0&&e.Object.size(this._shapes)>0)if(o.isString&&r[n])s=!0;else if(o.isObject(n))for(i in r)if(r.hasOwnProperty(i)&&n[i]){s=!0;break}s&&t._redraw()},_x:0,_y:0,getXY:function(){var t=this._node,n;return t&&(n=e.DOM.getXY(t)),n},initializer:function(){var e=this.get("render"),t=this.get("visible")?"visible":"hidden";this._shapes={},this._contentBounds={left:0,top:0,right:0,bottom:0},this._gradients={},this._node=v.createElement("div"),this._node.style.position="absolute",this._node.style.left=this.get("x")+"px",this._node.style.top=this.get("y")+"px",this._node.style.visibility=t,this._contentNode=this._createGraphics(),this._contentNode.style.visibility=t,this._contentNode.setAttribute("id",this.get("id")),this._node.appendChild(this._contentNode),e&&this.render(e)},render:function(t){var n=t||v.body,r,i;return t instanceof e.Node?n=t._node:e.Lang.isString(t)&&(n=e.Selector.query(t,v.body,!0)),r=this.get("width")||parseInt(e.DOM.getComputedStyle(n,"width"),10),i=this.get("height")||parseInt(e.DOM.getComputedStyle(n,"height"),10),n.appendChild(this._node),this.set("width",r),this.set("height",i),this},destroy:function(){this.removeAllShapes(),this._contentNode&&(this._removeChildren(this._contentNode),this._contentNode.parentNode&&this._contentNode.parentNode.removeChild(this._contentNode),this._contentNode=null),this._node&&(this._removeChildren(this._node),this._node.parentNode&&this._node.parentNode.removeChild(this._node),this._node=null)},addShape:function(e){e.graphic=this,this.get("visible")||(e.visible=!1);var t=this._getShapeClass(e.type),n=new t(e);return this._appendShape(n),n},_appendShape:function(e){var t=e.node,n=this._frag||this._contentNode;this.get("autoDraw")?n.appendChild(t):this._getDocFrag().appendChild(t)},removeShape:function(e){return e instanceof f||o.isString(e)&&(e=this._shapes[e]),e&&e instanceof f&&(e._destroy(),delete this._shapes[e.get("id")]),this.get("autoDraw")&&this._redraw(),e},removeAllShapes:function(){var e=this._shapes,t;for(t in e)e.hasOwnProperty(t)&&e[t]._destroy();this._shapes={}},_removeChildren:function(e){if(e.hasChildNodes()){var t;while(e.firstChild)t=e.firstChild,this._removeChildren(t),e.removeChild(t)}},clear:function(){this.removeAllShapes()},_toggleVisible:function(e){var t,n=this._shapes,r=e?"visible":"hidden";if(n)for(t in n)n.hasOwnProperty(t)&&n[t].set("visible",e);this._contentNode&&(this._contentNode.style.visibility=r),this._node&&(this._node.style.visibility=r)},_getShapeClass:function(e){var t=this._shapeClass[e];return t?t:e},_shapeClass:{circle:e.SVGCircle,rect:e.SVGRect,path:e.SVGPath,ellipse:e.SVGEllipse,pieslice:e.SVGPieSlice},getShapeById:function(e){var t=this._shapes[e];return t},batch:function(e){var t=this.get("autoDraw");this.set("autoDraw",!1),e(),this.set("autoDraw",t)},_getDocFrag:function(){return this._frag||(this._frag=v.createDocumentFragment()),this._frag},_redraw:function(){var t=this.get("autoSize"),n=this.get("preserveAspectRatio"),r=this.get("resizeDown")?this._getUpdatedContentBounds():this._contentBounds,i=r.left,s=r.right,o=r.top,u=r.bottom,a=s-i,f=u-o
+,l,c,h,p,d;t?t==="sizeContentToGraphic"?(d=this._node,l=parseFloat(e.DOM.getComputedStyle(d,"width")),c=parseFloat(e.DOM.getComputedStyle(d,"height")),h=p=0,this._contentNode.setAttribute("preserveAspectRatio",n)):(l=a,c=f,h=i,p=o,this._state.width=a,this._state.height=f,this._node&&(this._node.style.width=a+"px",this._node.style.height=f+"px")):(l=a,c=f,h=i,p=o),this._contentNode&&(this._contentNode.style.left=h+"px",this._contentNode.style.top=p+"px",this._contentNode.setAttribute("width",l),this._contentNode.setAttribute("height",c),this._contentNode.style.width=l+"px",this._contentNode.style.height=c+"px",this._contentNode.setAttribute("viewBox",""+i+" "+o+" "+a+" "+f+"")),this._frag&&(this._contentNode&&this._contentNode.appendChild(this._frag),this._frag=null)},addToRedrawQueue:function(e){var t,n;this._shapes[e.get("id")]=e,this.get("resizeDown")||(t=e.getBounds(),n=this._contentBounds,n.left=n.left<t.left?n.left:t.left,n.top=n.top<t.top?n.top:t.top,n.right=n.right>t.right?n.right:t.right,n.bottom=n.bottom>t.bottom?n.bottom:t.bottom,n.width=n.right-n.left,n.height=n.bottom-n.top,this._contentBounds=n),this.get("autoDraw")&&this._redraw()},_getUpdatedContentBounds:function(){var e,t,n,r=this._shapes,i={};for(t in r)r.hasOwnProperty(t)&&(n=r[t],e=n.getBounds(),i.left=o.isNumber(i.left)?Math.min(i.left,e.left):e.left,i.top=o.isNumber(i.top)?Math.min(i.top,e.top):e.top,i.right=o.isNumber(i.right)?Math.max(i.right,e.right):e.right,i.bottom=o.isNumber(i.bottom)?Math.max(i.bottom,e.bottom):e.bottom);return i.left=o.isNumber(i.left)?i.left:0,i.top=o.isNumber(i.top)?i.top:0,i.right=o.isNumber(i.right)?i.right:0,i.bottom=o.isNumber(i.bottom)?i.bottom:0,this._contentBounds=i,i},_createGraphics:function(){var e=this._createGraphicNode("svg"),t=this.get("pointerEvents");return e.style.position="absolute",e.style.top="0px",e.style.left="0px",e.style.overflow="auto",e.setAttribute("overflow","auto"),e.setAttribute("pointer-events",t),e},_createGraphicNode:function(e,t){var n=v.createElementNS("http://www.w3.org/2000/svg","svg:"+e),r=t||"none";return e!=="defs"&&e!=="stop"&&e!=="linearGradient"&&e!=="radialGradient"&&n.setAttribute("pointer-events",r),n},getGradientNode:function(e,t){var n=this._gradients,r,i=t+"Gradient";return n.hasOwnProperty(e)&&n[e].tagName.indexOf(t)>-1?r=this._gradients[e]:(r=this._createGraphicNode(i),this._defs||(this._defs=this._createGraphicNode("defs"),this._contentNode.appendChild(this._defs)),this._defs.appendChild(r),e=e||"gradient"+Math.round(1e5*Math.random()),r.setAttribute("id",e),n.hasOwnProperty(e)&&this._defs.removeChild(n[e]),n[e]=r),r},_toFront:function(t){var n=this._contentNode;t instanceof e.SVGShape&&(t=t.get("node")),n&&t&&n.appendChild(t)},_toBack:function(t){var n=this._contentNode,r;t instanceof e.SVGShape&&(t=t.get("node")),n&&t&&(r=n.firstChild,r?n.insertBefore(t,r):n.appendChild(t))}}),e.SVGGraphic=a},"3.17.2",{requires:["graphics"]});
diff --git a/js/yui3/graphics-vml-default/graphics-vml-default-min.js b/js/yui3/graphics-vml-default/graphics-vml-default-min.js
new file mode 100644
index 000000000..8aabe364d
--- /dev/null
+++ b/js/yui3/graphics-vml-default/graphics-vml-default-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("graphics-vml-default",function(e,t){e.Graphic=e.VMLGraphic,e.Shape=e.VMLShape,e.Circle=e.VMLCircle,e.Rect=e.VMLRect,e.Ellipse=e.VMLEllipse,e.Path=e.VMLPath,e.Drawing=e.VMLDrawing},"3.17.2");
diff --git a/js/yui3/graphics-vml/graphics-vml-min.js b/js/yui3/graphics-vml/graphics-vml-min.js
new file mode 100644
index 000000000..e94059b69
--- /dev/null
+++ b/js/yui3/graphics-vml/graphics-vml-min.js
@@ -0,0 +1,12 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("graphics-vml",function(e,t){function E(){}var n="vml",r="shape",i=/[a-z][^a-z]*/ig,s=/[\-]?[0-9]*[0-9|\.][0-9]*/g,o=e.Lang,u=o.isNumber,a=o.isArray,f=e.DOM,l=e.Selector,c=e.config.doc,h=e.AttributeLite,p,d,v,m,g,y,b,w=e.ClassNameManager.getClassName;E.prototype={_pathSymbolToMethod:{M:"moveTo",m:"relativeMoveTo",L:"lineTo",l:"relativeLineTo",C:"curveTo",c:"relativeCurveTo",Q:"quadraticCurveTo",q:"relativeQuadraticCurveTo",z:"closePath",Z:"closePath"},_coordSpaceMultiplier:100,_round:function(e){return Math.round(e*this._coordSpaceMultiplier)},_addToPath:function(e){this._path=this._path||"",this._movePath&&(this._path+=this._movePath,this._movePath=null),this._path+=e},_currentX:0,_currentY:0,curveTo:function(){return this._curveTo.apply(this,[e.Array(arguments),!1]),this},relativeCurveTo:function(){return this._curveTo.apply(this,[e.Array(arguments),!0]),this},_curveTo:function(e,t){var n,r,i,s,o,u,a,f,l,c,h,p,d,v,m,g,y=t?" v ":" c ",b=t?parseFloat(this._currentX):0,w=t?parseFloat(this._currentY):0;m=e.length-5,g=y;for(v=0;v<m;v+=6)o=parseFloat(e[v]),u=parseFloat(e[v+1]),a=parseFloat(e[v+2]),f=parseFloat(e[v+3]),i=parseFloat(e[v+4]),s=parseFloat(e[v+5]),v>0&&(g+=", "),g=g+this._round(o)+", "+this._round(u)+", "+this._round(a)+", "+this._round(f)+", "+this._round(i)+", "+this._round(s),o+=b,u+=w,a+=b,f+=w,i+=b,s+=w,c=Math.max(i,Math.max(o,a)),p=Math.max(s,Math.max(u,f)),h=Math.min(i,Math.min(o,a)),d=Math.min(s,Math.min(u,f)),n=Math.abs(c-h),r=Math.abs(p-d),l=[[this._currentX,this._currentY],[o,u],[a,f],[i,s]],this._setCurveBoundingBox(l,n,r),this._currentX=i,this._currentY=s;this._addToPath(g)},quadraticCurveTo:function(){return this._quadraticCurveTo.apply(this,[e.Array(arguments),!1]),this},relativeQuadraticCurveTo:function(){return this._quadraticCurveTo.apply(this,[e.Array(arguments),!0]),this},_quadraticCurveTo:function(e,t){var n,r,i,s,o,u,a,f,l=this._currentX,c=this._currentY,h,p=e.length-3,d=[],v=t?parseFloat(this._currentX):0,m=t?parseFloat(this._currentY):0;for(h=0;h<p;h+=4)n=parseFloat(e[h])+v,r=parseFloat(e[h+1])+m,a=parseFloat(e[h+2])+v,f=parseFloat(e[h+3])+m,i=l+.67*(n-l),s=c+.67*(r-c),o=i+(a-l)*.34,u=s+(f-c)*.34,d.push(i),d.push(s),d.push(o),d.push(u),d.push(a),d.push(f);this._curveTo.apply(this,[d,!1])},drawRect:function(e,t,n,r){return this.moveTo(e,t),this.lineTo(e+n,t),this.lineTo(e+n,t+r),this.lineTo(e,t+r),this.lineTo(e,t),this._currentX=e,this._currentY=t,this},drawRoundRect:function(e,t,n,r,i,s){return this.moveTo(e,t+s),this.lineTo(e,t+r-s),this.quadraticCurveTo(e,t+r,e+i,t+r),this.lineTo(e+n-i,t+r),this.quadraticCurveTo(e+n,t+r,e+n,t+r-s),this.lineTo(e+n,t+s),this.quadraticCurveTo(e+n,t,e+n-i,t),this.lineTo(e+i,t),this.quadraticCurveTo(e,t,e,t+s),this},drawCircle:function(e,t,n){var r=0,i=360,s=n*2;return i*=65535,this._drawingComplete=!1,this._trackSize(e+s,t+s),this.moveTo(e+s,t+n),this._addToPath(" ae "+this._round(e+n)+", "+this._round(t+n)+", "+this._round(n)+", "+this._round(n)+", "+r+", "+i),this},drawEllipse:function(e,t,n,r){var i=0,s=360,o=n*.5,u=r*.5;return s*=65535,this._drawingComplete=!1,this._trackSize(e+n,t+r),this.moveTo(e+n,t+u),this._addToPath(" ae "+this._round(e+o)+", "+this._round(e+o)+", "+this._round(t+u)+", "+this._round(o)+", "+this._round(u)+", "+i+", "+s),this},drawDiamond:function(e,t,n,r){var i=n*.5,s=r*.5;return this.moveTo(e+i,t),this.lineTo(e+n,t+s),this.lineTo(e+i,t+r),this.lineTo(e,t+s),this.lineTo(e+i,t),this},drawWedge:function(e,t,n,r,i){var s=i*2;return Math.abs(r)>360&&(r=360),this._currentX=e,this._currentY=t,n*=-65535,r*=65536,n=Math.round(n),r=Math.round(r),this.moveTo(e,t),this._addToPath(" ae "+this._round(e)+", "+this._round(t)+", "+this._round(i)+" "+this._round(i)+", "+n+", "+r),this._trackSize(s,s),this},lineTo:function(){return this._lineTo.apply(this,[e.Array(arguments),!1]),this},relativeLineTo:function(){return this._lineTo.apply(this,[e.Array(arguments),!0]),this},_lineTo:function(e,t){var n=e[0],r,i,s,o,u=t?" r ":" l ",a=t?parseFloat(this._currentX):0,f=t?parseFloat(this._currentY):0;if(typeof n=="string"||typeof n=="number"){i=e.length-1;for(r=0;r<i;r+=2)s=parseFloat(e[r]),o=parseFloat(e[r+1]),u+=" "+this._round(s)+", "+this._round(o),s+=a,o+=f,this._currentX=s,this._currentY=o,this._trackSize.apply(this,[s,o])}else{i=e.length;for(r=0;r<i;r+=1)s=parseFloat(e[r][0]),o=parseFloat(e[r][1]),u+=" "+this._round(s)+", "+this._round(o),s+=a,o+=f,this._currentX=s,this._currentY=o,this._trackSize.apply(this,[s,o])}return this._addToPath(u),this},moveTo:function(){return this._moveTo.apply(this,[e.Array(arguments),!1]),this},relativeMoveTo:function(){return this._moveTo.apply(this,[e.Array(arguments),!0]),this},_moveTo:function(e,t){var n=parseFloat(e[0]),r=parseFloat(e[1]),i=t?" t ":" m ",s=t?parseFloat(this._currentX):0,o=t?parseFloat(this._currentY):0;this._movePath=i+this._round(n)+", "+this._round(r),n+=s,r+=o,this._trackSize(n,r),this._currentX=n,this._currentY=r},_closePath:function(){var e=this.get("fill"),t=this.get("stroke"),n=this.node,r=this.get("width"),i=this.get("height"),s=this._path,o="",u=this._coordSpaceMultiplier;this._fillChangeHandler(),this._strokeChangeHandler(),s&&(e&&e.color&&(o+=" x"),t&&(o+=" e")),s&&(n.path=s+o),!isNaN(r)&&!isNaN(i)&&(n.coordOrigin=this._left+", "+this._top,n.coordSize=r*u+", "+i*u,n.style.position="absolute",n.style.width=r+"px",n.style.height=i+"px"),this._path=s,this._movePath=null,this._updateTransform()},end:function(){return this._closePath(),this},closePath:function(){return this._addToPath(" x e"),this},clear:function(){return this._right=0,this._bottom=0,this._width=0,this._height=0,this._left=0,this._top=0,this._path="",this._movePath=null,this},getBezierData:function(e,t){var n=e.length,r=[],i,s;for(i=0;i<n;++i)r[i]=[e[i][0],e[i][1]];for(s=1;s<n;++s)for(i=0;i<n-s;++i)r[i][0]=(1-t)*r[i][0]+t*r[parseInt(i+1,10)][0],r[i][1]=(1-t)*r[i][1]+t*r[parseInt(i+1,10)][1];return[r[0][0],r[0][1]]},_setCurveBoundingBox:function(e,t,n){var r,i=this._currentX,s=i,o=this._currentY
+,u=o,a=Math.round(Math.sqrt(t*t+n*n)),f=1/a,l;for(r=0;r<a;++r)l=this.getBezierData(e,f*r),i=isNaN(i)?l[0]:Math.min(l[0],i),s=isNaN(s)?l[0]:Math.max(l[0],s),o=isNaN(o)?l[1]:Math.min(l[1],o),u=isNaN(u)?l[1]:Math.max(l[1],u);i=Math.round(i*10)/10,s=Math.round(s*10)/10,o=Math.round(o*10)/10,u=Math.round(u*10)/10,this._trackSize(s,u),this._trackSize(i,o)},_trackSize:function(e,t){e>this._right&&(this._right=e),e<this._left&&(this._left=e),t<this._top&&(this._top=t),t>this._bottom&&(this._bottom=t),this._width=this._right-this._left,this._height=this._bottom-this._top},_left:0,_right:0,_top:0,_bottom:0,_width:0,_height:0},e.VMLDrawing=E,p=function(){this._transforms=[],this.matrix=new e.Matrix,this._normalizedMatrix=new e.Matrix,p.superclass.constructor.apply(this,arguments)},p.NAME="shape",e.extend(p,e.GraphicBase,e.mix({_type:"shape",init:function(){this.initializer.apply(this,arguments)},initializer:function(e){var t=this,n=e.graphic,r=this.get("data");t.createNode(),n&&this._setGraphic(n),r&&t._parsePathData(r),this._updateHandler()},_setGraphic:function(t){var n;t instanceof e.VMLGraphic?this._graphic=t:(n=new e.VMLGraphic({render:t}),n._appendShape(this),this._graphic=n,this._appendStrokeAndFill())},_appendStrokeAndFill:function(){this._strokeNode&&this.node.appendChild(this._strokeNode),this._fillNode&&this.node.appendChild(this._fillNode)},createNode:function(){var e,t=this._camelCaseConcat,i=this.get("x"),s=this.get("y"),o=this.get("width"),u=this.get("height"),a,f,l=this.name,h,p=this.get("visible")?"visible":"hidden",d,v,m,g,y,b,E,S,x,T;a=this.get("id"),f=this._type==="path"?"shape":this._type,v=w(r)+" "+w(t(n,r))+" "+w(l)+" "+w(t(n,l))+" "+n+f,m=this._getStrokeProps(),x=this._getFillProps(),h="<"+f+' xmlns="urn:schemas-microsft.com:vml" id="'+a+'" class="'+v+'" style="behavior:url(#default#VML);display:inline-block;position:absolute;left:'+i+"px;top:"+s+"px;width:"+o+"px;height:"+u+"px;visibility:"+p+'"',m&&m.weight&&m.weight>0?(g=m.endcap,y=parseFloat(m.opacity),b=m.joinstyle,E=m.miterlimit,S=m.dashstyle,h+=' stroked="t" strokecolor="'+m.color+'" strokeWeight="'+m.weight+'px"',d='<stroke class="vmlstroke" xmlns="urn:schemas-microsft.com:vml" on="t" style="behavior:url(#default#VML);display:inline-block;" opacity="'+y+'"',g&&(d+=' endcap="'+g+'"'),b&&(d+=' joinstyle="'+b+'"'),E&&(d+=' miterlimit="'+E+'"'),S&&(d+=' dashstyle="'+S+'"'),d+="></stroke>",this._strokeNode=c.createElement(d),h+=' stroked="t"'):h+=' stroked="f"',x&&(x.node&&(T=x.node,this._fillNode=c.createElement(T)),x.color&&(h+=' fillcolor="'+x.color+'"'),h+=' filled="'+x.filled+'"'),h+=">",h+="</"+f+">",e=c.createElement(h),this.node=e,this._strokeFlag=!1,this._fillFlag=!1},addClass:function(e){var t=this.node;f.addClass(t,e)},removeClass:function(e){var t=this.node;f.removeClass(t,e)},getXY:function(){var e=this._graphic,t=e.getXY(),n=this.get("x"),r=this.get("y");return[t[0]+n,t[1]+r]},setXY:function(e){var t=this._graphic,n=t.getXY();this.set("x",e[0]-n[0]),this.set("y",e[1]-n[1])},contains:function(t){var n=t instanceof e.Node?t._node:t;return n===this.node},compareTo:function(e){var t=this.node;return t===e},test:function(e){return l.test(this.node,e)},_getStrokeProps:function(){var e,t=this.get("stroke"),n,r,i="",s,o=0,f,l,c;if(t&&t.weight&&t.weight>0){e={},l=t.linecap||"flat",c=t.linejoin||"round",l!=="round"&&l!=="square"&&(l="flat"),n=parseFloat(t.opacity),r=t.dashstyle||"none",t.color=t.color||"#000000",t.weight=t.weight||1,t.opacity=u(n)?n:1,e.stroked=!0,e.color=t.color,e.weight=t.weight,e.endcap=l,e.opacity=t.opacity;if(a(r)){i=[],f=r.length;for(o=0;o<f;++o)s=r[o],i[o]=s/t.weight}c==="round"||c==="bevel"?e.joinstyle=c:(c=parseInt(c,10),u(c)&&(e.miterlimit=Math.max(c,1),e.joinstyle="miter")),e.dashstyle=i}return e},_strokeChangeHandler:function(){if(!this._strokeFlag)return;var e=this.node,t=this.get("stroke"),n,r,i="",s,o=0,f,l,c;if(t&&t.weight&&t.weight>0){l=t.linecap||"flat",c=t.linejoin||"round",l!=="round"&&l!=="square"&&(l="flat"),n=parseFloat(t.opacity),r=t.dashstyle||"none",t.color=t.color||"#000000",t.weight=t.weight||1,t.opacity=u(n)?n:1,e.stroked=!0,e.strokeColor=t.color,e.strokeWeight=t.weight+"px",this._strokeNode||(this._strokeNode=this._createGraphicNode("stroke"),e.appendChild(this._strokeNode)),this._strokeNode.endcap=l,this._strokeNode.opacity=t.opacity;if(a(r)){i=[],f=r.length;for(o=0;o<f;++o)s=r[o],i[o]=s/t.weight}c==="round"||c==="bevel"?this._strokeNode.joinstyle=c:(c=parseInt(c,10),u(c)&&(this._strokeNode.miterlimit=Math.max(c,1),this._strokeNode.joinstyle="miter")),this._strokeNode.dashstyle=i,this._strokeNode.on=!0}else this._strokeNode&&(this._strokeNode.on=!1),e.stroked=!1;this._strokeFlag=!1},_getFillProps:function(){var e=this.get("fill"),t,n,r,i,s,o=!1;if(e){n={};if(e.type==="radial"||e.type==="linear"){t=parseFloat(e.opacity),t=u(t)?t:1,o=!0,r=this._getGradientFill(e),s='<fill xmlns="urn:schemas-microsft.com:vml" class="vmlfill" style="behavior:url(#default#VML);display:inline-block;" opacity="'+t+'"';for(i in r)r.hasOwnProperty(i)&&(s+=" "+i+'="'+r[i]+'"');s+=" />",n.node=s}else e.color&&(t=parseFloat(e.opacity),o=!0,n.color=e.color,u(t)&&(t=Math.max(Math.min(t,1),0),n.opacity=t,t<1&&(n.node='<fill xmlns="urn:schemas-microsft.com:vml" class="vmlfill" style="behavior:url(#default#VML);display:inline-block;" type="solid" opacity="'+t+'"/>')));n.filled=o}return n},_fillChangeHandler:function(){if(!this._fillFlag)return;var e=this.node,t=this.get("fill"),n,r,i=!1,s,o;if(t)if(t.type==="radial"||t.type==="linear"){i=!0,o=this._getGradientFill(t);if(this._fillNode)for(s in o)o.hasOwnProperty(s)&&(s==="colors"?this._fillNode.colors.value=o[s]:this._fillNode[s]=o[s]);else{r='<fill xmlns="urn:schemas-microsft.com:vml" class="vmlfill" style="behavior:url(#default#VML);display:inline-block;"';for(s in o)o.hasOwnProperty(s)&&(r+=" "+s+'="'+o[s]+'"');r+=" />",this._fillNode=c.createElement(r),e.appendChild(this._fillNode)}}else t.color&&(e.fillcolor=t.color,n=parseFloat(t.opacity),i=!0,u(n)&&
+n<1?(t.opacity=n,this._fillNode?(this._fillNode.getAttribute("type")!=="solid"&&(this._fillNode.type="solid"),this._fillNode.opacity=n):(r='<fill xmlns="urn:schemas-microsft.com:vml" class="vmlfill" style="behavior:url(#default#VML);display:inline-block;" type="solid" opacity="'+n+'"'+"/>",this._fillNode=c.createElement(r),e.appendChild(this._fillNode))):this._fillNode&&(this._fillNode.opacity=1,this._fillNode.type="solid"));e.filled=i,this._fillFlag=!1},_updateFillNode:function(e){this._fillNode||(this._fillNode=this._createGraphicNode("fill"),e.appendChild(this._fillNode))},_getGradientFill:function(e){var t={},n,r,i=e.type,s=this.get("width"),o=this.get("height"),a=u,f,l=e.stops,c=l.length,h,p,d,v,m="",g=e.cx,y=e.cy,b=e.fx,w=e.fy,E=e.r,S,x=e.rotation||0;i==="linear"?(x<=270?x=Math.abs(x-270):x<360?x=270+(360-x):x=270,t.type="gradient",t.angle=x):i==="radial"&&(n=s*E*2,r=o*E*2,b=E*2*(b-.5),w=E*2*(w-.5),b+=g,w+=y,t.focussize=n/s/10+"% "+r/o/10+"%",t.alignshape=!1,t.type="gradientradial",t.focus="100%",t.focusposition=Math.round(b*100)+"% "+Math.round(w*100)+"%");for(d=0;d<c;++d)f=l[d],p=f.color,h=f.opacity,h=a(h)?h:1,S=f.offset||d/(c-1),S*=E*2,S=Math.round(100*S)+"%",v=d>0?d+1:"",t["opacity"+v]=h+"",m+=", "+S+" "+p;return parseFloat(S)<100&&(m+=", 100% "+p),t.colors=m.substr(2),t},_addTransform:function(t,n){n=e.Array(n),this._transform=o.trim(this._transform+" "+t+"("+n.join(", ")+")"),n.unshift(t),this._transforms.push(n),this.initialized&&this._updateTransform()},_updateTransform:function(){var t=this.node,n,r,i,s=this.get("x"),o=this.get("y"),u,a,f=this.matrix,l=this._normalizedMatrix,h=this instanceof e.VMLPath,p,d=this._transforms.length;if(this._transforms&&this._transforms.length>0){i=this.get("transformOrigin"),h&&l.translate(this._left,this._top),u=i[0]-.5,a=i[1]-.5,u=Math.max(-0.5,Math.min(.5,u)),a=Math.max(-0.5,Math.min(.5,a));for(p=0;p<d;++p)n=this._transforms[p].shift(),n&&(l[n].apply(l,this._transforms[p]),f[n].apply(f,this._transforms[p]));h&&l.translate(-this._left,-this._top),r=l.a+","+l.c+","+l.b+","+l.d+","+0+","+0}this._graphic.addToRedrawQueue(this),r&&(this._skew||(this._skew=c.createElement('<skew class="vmlskew" xmlns="urn:schemas-microsft.com:vml" on="false" style="behavior:url(#default#VML);display:inline-block;"/>'),this.node.appendChild(this._skew)),this._skew.matrix=r,this._skew.on=!0,this._skew.origin=u+", "+a),this._type!=="path"&&(this._transforms=[]),t.style.left=s+this._getSkewOffsetValue(l.dx)+"px",t.style.top=o+this._getSkewOffsetValue(l.dy)+"px"},_getSkewOffsetValue:function(t){var n=e.MatrixUtil.sign(t),r=Math.abs(t);return t=Math.min(r,32767)*n,t},_translateX:0,_translateY:0,_transform:"",translate:function(e,t){this._translateX+=e,this._translateY+=t,this._addTransform("translate",arguments)},translateX:function(e){this._translateX+=e,this._addTransform("translateX",arguments)},translateY:function(e){this._translateY+=e,this._addTransform("translateY",arguments)},skew:function(){this._addTransform("skew",arguments)},skewX:function(){this._addTransform("skewX",arguments)},skewY:function(){this._addTransform("skewY",arguments)},rotate:function(){this._addTransform("rotate",arguments)},scale:function(){this._addTransform("scale",arguments)},on:function(t,n){return e.Node.DOM_EVENTS[t]?e.on(t,n,"#"+this.get("id")):e.on.apply(this,arguments)},_draw:function(){},_updateHandler:function(){var e=this,t=e.node;e._fillChangeHandler(),e._strokeChangeHandler(),t.style.width=this.get("width")+"px",t.style.height=this.get("height")+"px",this._draw(),e._updateTransform()},_createGraphicNode:function(e){return e=e||this._type,c.createElement("<"+e+' xmlns="urn:schemas-microsft.com:vml"'+' style="behavior:url(#default#VML);display:inline-block;"'+' class="vml'+e+'"'+"/>")},_getDefaultFill:function(){return{type:"solid",opacity:1,cx:.5,cy:.5,fx:.5,fy:.5,r:.5}},_getDefaultStroke:function(){return{weight:1,dashstyle:"none",color:"#000",opacity:1}},set:function(){var e=this;h.prototype.set.apply(e,arguments),e.initialized&&e._updateHandler()},getBounds:function(){var t=this instanceof e.VMLPath,n=this.get("width"),r=this.get("height"),i=this.get("x"),s=this.get("y");return t&&(i+=this._left,s+=this._top,n=this._right-this._left,r=this._bottom-this._top),this._getContentRect(n,r,i,s)},_getContentRect:function(t,n,r,i){var s=this.get("transformOrigin"),o=s[0]*t,u=s[1]*n,a=this.matrix.getTransformArray(this.get("transform")),f=new e.Matrix,l,c=a.length,h,p,d,v=this instanceof e.VMLPath;v&&f.translate(this._left,this._top),o=isNaN(o)?0:o,u=isNaN(u)?0:u,f.translate(o,u);for(l=0;l<c;l+=1)h=a[l],p=h.shift(),p&&f[p].apply(f,h);return f.translate(-o,-u),v&&f.translate(-this._left,-this._top),d=f.getContentRect(t,n,r,i),d},toFront:function(){var e=this.get("graphic");e&&e._toFront(this)},toBack:function(){var e=this.get("graphic");e&&e._toBack(this)},_parsePathData:function(t){var n,r,o,u=e.Lang.trim(t.match(i)),a,f,l,c=this._pathSymbolToMethod;if(u){this.clear(),f=u.length||0;for(a=0;a<f;a+=1)l=u[a],r=l.substr(0,1),o=l.substr(1).match(s),n=c[r],n&&(o?this[n].apply(this,o):this[n].apply(this));this.end()}},destroy:function(){var e=this.get("graphic");e?e.removeShape(this):this._destroy()},_destroy:function(){this.node&&(this._fillNode&&(this.node.removeChild(this._fillNode),this._fillNode=null),this._strokeNode&&(this.node.removeChild(this._strokeNode),this._strokeNode=null),e.Event.purgeElement(this.node,!0),this.node.parentNode&&this.node.parentNode.removeChild(this.node),this.node=null)}},e.VMLDrawing.prototype)),p.ATTRS={transformOrigin:{valueFn:function(){return[.5,.5]}},transform:{setter:function(e){var t,n,r;this.matrix.init(),this._normalizedMatrix.init(),this._transforms=this.matrix.getTransformArray(e),n=this._transforms.length;for(t=0;t<n;++t)r=this._transforms[t];return this._transform=e,e},getter:function(){return this._transform}},x:{value:0},y:{value:0},id:{valueFn:function(){return e.guid()},setter:function(e){var t=this.node;return t&&t.setAttribute("id",e),e}}
+,width:{value:0},height:{value:0},visible:{value:!0,setter:function(e){var t=this.node,n=e?"visible":"hidden";return t&&(t.style.visibility=n),e}},fill:{valueFn:"_getDefaultFill",setter:function(t){var n,r,i=this.get("fill")||this._getDefaultFill();if(t){t.hasOwnProperty("color")&&(t.type="solid");for(n in t)t.hasOwnProperty(n)&&(i[n]=t[n])}return r=i,r&&r.color&&(r.color===undefined||r.color==="none"?r.color=null:r.color.toLowerCase().indexOf("rgba")>-1&&(r.opacity=e.Color._getAlpha(r.color),r.color=e.Color.toHex(r.color))),this._fillFlag=!0,r}},stroke:{valueFn:"_getDefaultStroke",setter:function(t){var n,r,i,s=this.get("stroke")||this._getDefaultStroke();if(t){t.hasOwnProperty("weight")&&(i=parseInt(t.weight,10),isNaN(i)||(t.weight=i));for(n in t)t.hasOwnProperty(n)&&(s[n]=t[n])}return s.color&&s.color.toLowerCase().indexOf("rgba")>-1&&(s.opacity=e.Color._getAlpha(s.color),s.color=e.Color.toHex(s.color)),r=s,this._strokeFlag=!0,r}},autoSize:{value:!1},pointerEvents:{value:"visiblePainted"},node:{readOnly:!0,getter:function(){return this.node}},data:{setter:function(e){return this.get("node")&&this._parsePathData(e),e}},graphic:{readOnly:!0,getter:function(){return this._graphic}}},e.VMLShape=p,v=function(){v.superclass.constructor.apply(this,arguments)},v.NAME="path",e.extend(v,e.VMLShape),v.ATTRS=e.merge(e.VMLShape.ATTRS,{width:{getter:function(){var e=Math.max(this._right-this._left,0);return e}},height:{getter:function(){return Math.max(this._bottom-this._top,0)}},path:{readOnly:!0,getter:function(){return this._path}}}),e.VMLPath=v,m=function(){m.superclass.constructor.apply(this,arguments)},m.NAME="rect",e.extend(m,e.VMLShape,{_type:"rect"}),m.ATTRS=e.VMLShape.ATTRS,e.VMLRect=m,g=function(){g.superclass.constructor.apply(this,arguments)},g.NAME="ellipse",e.extend(g,e.VMLShape,{_type:"oval"}),g.ATTRS=e.merge(e.VMLShape.ATTRS,{xRadius:{lazyAdd:!1,getter:function(){var e=this.get("width");return e=Math.round(e/2*100)/100,e},setter:function(e){var t=e*2;return this.set("width",t),e}},yRadius:{lazyAdd:!1,getter:function(){var e=this.get("height");return e=Math.round(e/2*100)/100,e},setter:function(e){var t=e*2;return this.set("height",t),e}}}),e.VMLEllipse=g,d=function(){d.superclass.constructor.apply(this,arguments)},d.NAME="circle",e.extend(d,p,{_type:"oval"}),d.ATTRS=e.merge(p.ATTRS,{radius:{lazyAdd:!1,value:0},width:{setter:function(e){return this.set("radius",e/2),e},getter:function(){var e=this.get("radius"),t=e&&e>0?e*2:0;return t}},height:{setter:function(e){return this.set("radius",e/2),e},getter:function(){var e=this.get("radius"),t=e&&e>0?e*2:0;return t}}}),e.VMLCircle=d,b=function(){b.superclass.constructor.apply(this,arguments)},b.NAME="vmlPieSlice",e.extend(b,e.VMLShape,e.mix({_type:"shape",_draw:function(){var e=this.get("cx"),t=this.get("cy"),n=this.get("startAngle"),r=this.get("arc"),i=this.get("radius");this.clear(),this.drawWedge(e,t,n,r,i),this.end()}},e.VMLDrawing.prototype)),b.ATTRS=e.mix({cx:{value:0},cy:{value:0},startAngle:{value:0},arc:{value:0},radius:{value:0}},e.VMLShape.ATTRS),e.VMLPieSlice=b,y=function(){y.superclass.constructor.apply(this,arguments)},y.NAME="vmlGraphic",y.ATTRS={render:{},id:{valueFn:function(){return e.guid()},setter:function(e){var t=this._node;return t&&t.setAttribute("id",e),e}},shapes:{readOnly:!0,getter:function(){return this._shapes}},contentBounds:{readOnly:!0,getter:function(){return this._contentBounds}},node:{readOnly:!0,getter:function(){return this._node}},width:{setter:function(e){return this._node&&(this._node.style.width=e+"px"),e}},height:{setter:function(e){return this._node&&(this._node.style.height=e+"px"),e}},autoSize:{value:!1},preserveAspectRatio:{value:"xMidYMid"},resizeDown:{resizeDown:!1},x:{getter:function(){return this._x},setter:function(e){return this._x=e,this._node&&(this._node.style.left=e+"px"),e}},y:{getter:function(){return this._y},setter:function(e){return this._y=e,this._node&&(this._node.style.top=e+"px"),e}},autoDraw:{value:!0},visible:{value:!0,setter:function(e){return this._toggleVisible(e),e}}},e.extend(y,e.GraphicBase,{set:function(){var t=this,n=arguments[0],r={autoDraw:!0,autoSize:!0,preserveAspectRatio:!0,resizeDown:!0},i,s=!1;h.prototype.set.apply(t,arguments);if(t._state.autoDraw===!0&&e.Object.size(this._shapes)>0)if(o.isString&&r[n])s=!0;else if(o.isObject(n))for(i in r)if(r.hasOwnProperty(i)&&n[i]){s=!0;break}s&&t._redraw()},_x:0,_y:0,getXY:function(){var t=this.parentNode,n=this.get("x"),r=this.get("y"),i;return t?(i=e.DOM.getXY(t),i[0]+=n,i[1]+=r):i=e.DOM._getOffset(this._node),i},initializer:function(){var e=this.get("render"),t=this.get("visible")?"visible":"hidden";this._shapes={},this._contentBounds={left:0,top:0,right:0,bottom:0},this._node=this._createGraphic(),this._node.style.left=this.get("x")+"px",this._node.style.top=this.get("y")+"px",this._node.style.visibility=t,this._node.setAttribute("id",this.get("id")),e&&this.render(e)},render:function(t){var n=t||c.body,r=this._node,i,s;return t instanceof e.Node?n=t._node:e.Lang.isString(t)&&(n=e.Selector.query(t,c.body,!0)),i=this.get("width")||parseInt(e.DOM.getComputedStyle(n,"width"),10),s=this.get("height")||parseInt(e.DOM.getComputedStyle(n,"height"),10),n.appendChild(r),this.parentNode=n,this.set("width",i),this.set("height",s),this},destroy:function(){this.removeAllShapes(),this._node&&(this._removeChildren(this._node),this._node.parentNode&&this._node.parentNode.removeChild(this._node),this._node=null)},addShape:function(e){e.graphic=this,this.get("visible")||(e.visible=!1);var t=this._getShapeClass(e.type),n=new t(e);return this._appendShape(n),n._appendStrokeAndFill(),n},_appendShape:function(e){var t=e.node,n=this._frag||this._node;this.get("autoDraw")||this.get("autoSize")==="sizeContentToGraphic"?n.appendChild(t):this._getDocFrag().appendChild(t)},removeShape:function(e){e instanceof p||o.isString(e)&&(e=this._shapes[e]),e&&e instanceof p&&(e._destroy(),this._shapes[e.get("id")]=null,delete this._shapes[e.get("id")]
+),this.get("autoDraw")&&this._redraw()},removeAllShapes:function(){var e=this._shapes,t;for(t in e)e.hasOwnProperty(t)&&e[t].destroy();this._shapes={}},_removeChildren:function(e){if(e.hasChildNodes()){var t;while(e.firstChild)t=e.firstChild,this._removeChildren(t),e.removeChild(t)}},clear:function(){this.removeAllShapes(),this._removeChildren(this._node)},_toggleVisible:function(e){var t,n=this._shapes,r=e?"visible":"hidden";if(n)for(t in n)n.hasOwnProperty(t)&&n[t].set("visible",e);this._node&&(this._node.style.visibility=r),this._node&&(this._node.style.visibility=r)},setSize:function(e,t){e=Math.round(e),t=Math.round(t),this._node.style.width=e+"px",this._node.style.height=t+"px"},setPosition:function(e,t){e=Math.round(e),t=Math.round(t),this._node.style.left=e+"px",this._node.style.top=t+"px"},_createGraphic:function(){var e=c.createElement('<group xmlns="urn:schemas-microsft.com:vml" style="behavior:url(#default#VML);padding:0px 0px 0px 0px;display:block;position:absolute;top:0px;left:0px;zoom:1;"/>');return e},_createGraphicNode:function(e){return c.createElement("<"+e+' xmlns="urn:schemas-microsft.com:vml"'+' style="behavior:url(#default#VML);display:inline-block;zoom:1;"'+"/>")},getShapeById:function(e){return this._shapes[e]},_getShapeClass:function(e){var t=this._shapeClass[e];return t?t:e},_shapeClass:{circle:e.VMLCircle,rect:e.VMLRect,path:e.VMLPath,ellipse:e.VMLEllipse,pieslice:e.VMLPieSlice},batch:function(e){var t=this.get("autoDraw");this.set("autoDraw",!1),e.apply(),this.set("autoDraw",t)},_getDocFrag:function(){return this._frag||(this._frag=c.createDocumentFragment()),this._frag},addToRedrawQueue:function(e){var t,n;this._shapes[e.get("id")]=e,this.get("resizeDown")||(t=e.getBounds(),n=this._contentBounds,n.left=n.left<t.left?n.left:t.left,n.top=n.top<t.top?n.top:t.top,n.right=n.right>t.right?n.right:t.right,n.bottom=n.bottom>t.bottom?n.bottom:t.bottom,n.width=n.right-n.left,n.height=n.bottom-n.top,this._contentBounds=n),this.get("autoDraw")&&this._redraw()},_redraw:function(){var t=this.get("autoSize"),n,r=this.parentNode,i=parseFloat(e.DOM.getComputedStyle(r,"width")),s=parseFloat(e.DOM.getComputedStyle(r,"height")),o=0,u=0,a=this.get("resizeDown")?this._getUpdatedContentBounds():this._contentBounds,f=a.left,l=a.right,c=a.top,h=a.bottom,p=l-f,d=h-c,v,m,g,y,b,w=this.get("visible");this._node.style.visibility="hidden",t?(t==="sizeContentToGraphic"?(n=this.get("preserveAspectRatio"),n==="none"||p/d===i/s?(o=f,u=c,m=p,g=d):p*s/d>i?(v=s/i,m=p,g=p*v,b=i*(d/p)*(g/s),u=this._calculateCoordOrigin(n.slice(5).toLowerCase(),b,g),u=c+u,o=f):(v=i/s,m=d*v,g=d,y=s*(p/d)*(m/i),o=this._calculateCoordOrigin(n.slice(1,4).toLowerCase(),y,m),o+=f,u=c),this._node.style.width=i+"px",this._node.style.height=s+"px",this._node.coordOrigin=o+", "+u):(m=p,g=d,this._node.style.width=p+"px",this._node.style.height=d+"px",this._state.width=p,this._state.height=d),this._node.coordSize=m+", "+g):(this._node.style.width=i+"px",this._node.style.height=s+"px",this._node.coordSize=i+", "+s),this._frag&&(this._node.appendChild(this._frag),this._frag=null),w&&(this._node.style.visibility="visible")},_calculateCoordOrigin:function(e,t,n){var r;switch(e){case"min":r=0;break;case"mid":r=(t-n)/2;break;case"max":r=t-n}return r},_getUpdatedContentBounds:function(){var e,t,n,r=this._shapes,i={};for(t in r)r.hasOwnProperty(t)&&(n=r[t],e=n.getBounds(),i.left=o.isNumber(i.left)?Math.min(i.left,e.left):e.left,i.top=o.isNumber(i.top)?Math.min(i.top,e.top):e.top,i.right=o.isNumber(i.right)?Math.max(i.right,e.right):e.right,i.bottom=o.isNumber(i.bottom)?Math.max(i.bottom,e.bottom):e.bottom);return i.left=o.isNumber(i.left)?i.left:0,i.top=o.isNumber(i.top)?i.top:0,i.right=o.isNumber(i.right)?i.right:0,i.bottom=o.isNumber(i.bottom)?i.bottom:0,this._contentBounds=i,i},_toFront:function(t){var n=this._node;t instanceof e.VMLShape&&(t=t.get("node")),n&&t&&n.appendChild(t)},_toBack:function(t){var n=this._node,r;t instanceof e.VMLShape&&(t=t.get("node")),n&&t&&(r=n.firstChild,r?n.insertBefore(t,r):n.appendChild(t))}}),e.VMLGraphic=y},"3.17.2",{requires:["graphics","color-base"]});
diff --git a/js/yui3/graphics/graphics-min.js b/js/yui3/graphics/graphics-min.js
new file mode 100644
index 000000000..360a5e509
--- /dev/null
+++ b/js/yui3/graphics/graphics-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("graphics",function(e,t){var n="setter",r=e.Plugin.Host,i="value",s="valueFn",o="readOnly",u=e.Lang,a="string",f="writeOnce",l,c;c=function(){var t=this;t._ATTR_E_FACADE={},e.EventTarget.call(this,{emitFacade:!0}),t._state={},t.prototype=e.mix(c.prototype,t.prototype)},c.prototype={addAttrs:function(e){var t=this,r=this.constructor.ATTRS,a,l,c,h=t._state;for(l in r)r.hasOwnProperty(l)&&(a=r[l],a.hasOwnProperty(i)?h[l]=a.value:a.hasOwnProperty(s)&&(c=a.valueFn,u.isString(c)?h[l]=t[c].apply(t):h[l]=c.apply(t)));t._state=h;for(l in r)if(r.hasOwnProperty(l)){a=r[l];if(a.hasOwnProperty(o)&&a.readOnly)continue;a.hasOwnProperty(f)&&a.writeOnce&&(a.readOnly=!0),e&&e.hasOwnProperty(l)&&(a.hasOwnProperty(n)?t._state[l]=a.setter.apply(t,[e[l]]):t._state[l]=e[l])}},get:function(e){var t=this,n,r=t.constructor.ATTRS;if(r&&r[e])return n=r[e].getter,n?typeof n===a?t[n].apply(t):r[e].getter.apply(t):t._state[e];return null},set:function(){var e=arguments[0],t;if(u.isObject(e))for(t in e)e.hasOwnProperty(t)&&this._set(t,e[t]);else this._set.apply(this,arguments)},_set:function(e,t){var n=this,r,i,s=n.constructor.ATTRS;s&&s.hasOwnProperty(e)&&(r=s[e].setter,r&&(i=[t],typeof r===a?t=n[r].apply(n,i):t=s[e].setter.apply(n,i)),n._state[e]=t)}},e.mix(c,e.EventTarget,!1,null,1),e.AttributeLite=c,l=function(t){var n=this,r=e.Plugin&&e.Plugin.Host;n._initPlugins&&r&&r.call(n),n.name=n.constructor.NAME,n._eventPrefix=n.constructor.EVENT_PREFIX||n.constructor.NAME,c.call(n),n.addAttrs(t),n.init.apply(this,arguments),n._initPlugins&&n._initPlugins(t),n.initialized=!0},l.NAME="baseGraphic",l.prototype={init:function(){this.publish("init",{fireOnce:!0}),this.initializer.apply(this,arguments),this.fire("init",{cfg:arguments[0]})},_camelCaseConcat:function(e,t){return e+t.charAt(0).toUpperCase()+t.slice(1)}},e.mix(l,e.AttributeLite,!1,null,1),e.mix(l,r,!1,null,1),l.prototype.constructor=l,l.plug=r.plug,l.unplug=r.unplug,e.GraphicBase=l},"3.17.2",{requires:["node","event-custom","pluginhost","matrix","classnamemanager"]});
diff --git a/js/yui3/handlebars-base/handlebars-base-min.js b/js/yui3/handlebars-base/handlebars-base-min.js
new file mode 100644
index 000000000..9d3c31492
--- /dev/null
+++ b/js/yui3/handlebars-base/handlebars-base-min.js
@@ -0,0 +1,13 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("handlebars-base",function(e,t){
+/*!
+Handlebars.js - Copyright (C) 2011 Yehuda Katz
+https://raw.github.com/wycats/handlebars.js/master/LICENSE
+*/
+;var n=e.namespace("Handlebars");n.VERSION="1.0.0",n.COMPILER_REVISION=4,n.REVISION_CHANGES={1:"<= 1.0.rc.2",2:"== 1.0.0-rc.3",3:"== 1.0.0-rc.4",4:">= 1.0.0"},n.helpers={},n.partials={};var r=Object.prototype.toString,i="[object Function]",s="[object Object]";n.registerHelper=function(e,t,i){if(r.call(e)===s){if(i||t)throw new n.Exception("Arg not supported with multiple helpers");n.Utils.extend(this.helpers,e)}else i&&(t.not=i),this.helpers[e]=t},n.registerPartial=function(e,t){r.call(e)===s?n.Utils.extend(this.partials,e):this.partials[e]=t},n.registerHelper("helperMissing",function(e){if(arguments.length===2)return undefined;throw new Error("Missing helper: '"+e+"'")}),n.registerHelper("blockHelperMissing",function(e,t){var s=t.inverse||function(){},o=t.fn,u=r.call(e);return u===i&&(e=e.call(this)),e===!0?o(this):e===!1||e==null?s(this):u==="[object Array]"?e.length>0?n.helpers.each(e,t):s(this):o(e)}),n.K=function(){},n.createFrame=Object.create||function(e){n.K.prototype=e;var t=new n.K;return n.K.prototype=null,t},n.logger={DEBUG:0,INFO:1,WARN:2,ERROR:3,level:3,methodMap:{0:"debug",1:"info",2:"warn",3:"error"},log:function(e,t){if(n.logger.level<=e){var r=n.logger.methodMap[e];typeof console!="undefined"&&console[r]&&console[r].call(console,t)}}},n.log=function(e,t){n.logger.log(e,t)},n.registerHelper("each",function(e,t){var s=t.fn,o=t.inverse,u=0,a="",f,l=r.call(e);l===i&&(e=e.call(this)),t.data&&(f=n.createFrame(t.data));if(e&&typeof e=="object")if(e instanceof Array)for(var c=e.length;u<c;u++)f&&(f.index=u),a+=s(e[u],{data:f});else for(var h in e)e.hasOwnProperty(h)&&(f&&(f.key=h),a+=s(e[h],{data:f}),u++);return u===0&&(a=o(this)),a}),n.registerHelper("if",function(e,t){var s=r.call(e);return s===i&&(e=e.call(this)),!e||n.Utils.isEmpty(e)?t.inverse(this):t.fn(this)}),n.registerHelper("unless",function(e,t){return n.helpers["if"].call(this,e,{fn:t.inverse,inverse:t.fn})}),n.registerHelper("with",function(e,t){var s=r.call(e);s===i&&(e=e.call(this));if(!n.Utils.isEmpty(e))return t.fn(e)}),n.registerHelper("log",function(e,t){var r=t.data&&t.data.level!=null?parseInt(t.data.level,10):1;n.log(r,e)});var o=["description","fileName","lineNumber","message","name","number","stack"];n.Exception=function(e){var t=Error.prototype.constructor.apply(this,arguments);for(var n=0;n<o.length;n++)this[o[n]]=t[o[n]]},n.Exception.prototype=new Error,n.SafeString=function(e){this.string=e},n.SafeString.prototype.toString=function(){return this.string.toString()};var u={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","`":"&#x60;"},a=/[&<>"'`]/g,f=/[&<>"'`]/,l=function(e){return u[e]||"&amp;"};n.Utils={extend:function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},escapeExpression:function(e){return e instanceof n.SafeString?e.toString():e==null||e===!1?"":(e=e.toString(),f.test(e)?e.replace(a,l):e)},isEmpty:function(e){return!e&&e!==0?!0:r.call(e)==="[object Array]"&&e.length===0?!0:!1}},n.VM={template:function(e){var t={escapeExpression:n.Utils.escapeExpression,invokePartial:n.VM.invokePartial,programs:[],program:function(e,t,r){var i=this.programs[e];return r?i=n.VM.program(e,t,r):i||(i=this.programs[e]=n.VM.program(e,t)),i},merge:function(e,t){var r=e||t;return e&&t&&(r={},n.Utils.extend(r,t),n.Utils.extend(r,e)),r},programWithDepth:n.VM.programWithDepth,noop:n.VM.noop,compilerInfo:null};return function(r,i){i=i||{};var s=e.call(t,n,r,i.helpers,i.partials,i.data),o=t.compilerInfo||[],u=o[0]||1,a=n.COMPILER_REVISION;if(u!==a){if(u<a){var f=n.REVISION_CHANGES[a],l=n.REVISION_CHANGES[u];throw"Template was precompiled with an older version of Handlebars than the current runtime. Please update your precompiler to a newer version ("+f+") or downgrade your runtime to an older version ("+l+")."}throw"Template was precompiled with a newer version of Handlebars than the current runtime. Please update your runtime to a newer version ("+o[1]+")."}return s}},programWithDepth:function(e,t,n){var r=Array.prototype.slice.call(arguments,3),i=function(e,i){return i=i||{},t.apply(this,[e,i.data||n].concat(r))};return i.program=e,i.depth=r.length,i},program:function(e,t,n){var r=function(e,r){return r=r||{},t(e,r.data||n)};return r.program=e,r.depth=0,r},noop:function(){return""},invokePartial:function(e,t,r,i,s,o){var u={helpers:i,partials:s,data:o};if(e===undefined)throw new n.Exception("The partial "+t+" could not be found");if(e instanceof Function)return e(r,u);if(!n.compile)throw new n.Exception("The partial "+t+" could not be compiled when running in runtime-only mode");return s[t]=n.compile(e,{data:o!==undefined}),s[t](r,u)}},n.template=n.VM.template,n.VERSION+="-yui",n.revive=n.template,e.namespace("Template").Handlebars=n},"3.17.2",{requires:[]});
diff --git a/js/yui3/handlebars-compiler/handlebars-compiler-min.js b/js/yui3/handlebars-compiler/handlebars-compiler-min.js
new file mode 100644
index 000000000..e8f95a474
--- /dev/null
+++ b/js/yui3/handlebars-compiler/handlebars-compiler-min.js
@@ -0,0 +1,18 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("handlebars-compiler",function(e,t){
+/*!
+Handlebars.js - Copyright (C) 2011 Yehuda Katz
+https://raw.github.com/wycats/handlebars.js/master/LICENSE
+*/
+;var n=e.Handlebars,r=function(){function n(){this.yy={}}var e={trace:function(){},yy:{},symbols_:{error:2,root:3,program:4,EOF:5,simpleInverse:6,statements:7,statement:8,openInverse:9,closeBlock:10,openBlock:11,mustache:12,partial:13,CONTENT:14,COMMENT:15,OPEN_BLOCK:16,inMustache:17,CLOSE:18,OPEN_INVERSE:19,OPEN_ENDBLOCK:20,path:21,OPEN:22,OPEN_UNESCAPED:23,CLOSE_UNESCAPED:24,OPEN_PARTIAL:25,partialName:26,params:27,hash:28,dataName:29,param:30,STRING:31,INTEGER:32,BOOLEAN:33,hashSegments:34,hashSegment:35,ID:36,EQUALS:37,DATA:38,pathSegments:39,SEP:40,$accept:0,$end:1},terminals_:{2:"error",5:"EOF",14:"CONTENT",15:"COMMENT",16:"OPEN_BLOCK",18:"CLOSE",19:"OPEN_INVERSE",20:"OPEN_ENDBLOCK",22:"OPEN",23:"OPEN_UNESCAPED",24:"CLOSE_UNESCAPED",25:"OPEN_PARTIAL",31:"STRING",32:"INTEGER",33:"BOOLEAN",36:"ID",37:"EQUALS",38:"DATA",40:"SEP"},productions_:[0,[3,2],[4,2],[4,3],[4,2],[4,1],[4,1],[4,0],[7,1],[7,2],[8,3],[8,3],[8,1],[8,1],[8,1],[8,1],[11,3],[9,3],[10,3],[12,3],[12,3],[13,3],[13,4],[6,2],[17,3],[17,2],[17,2],[17,1],[17,1],[27,2],[27,1],[30,1],[30,1],[30,1],[30,1],[30,1],[28,1],[34,2],[34,1],[35,3],[35,3],[35,3],[35,3],[35,3],[26,1],[26,1],[26,1],[29,2],[21,1],[39,3],[39,1]],performAction:function(t,n,r,i,s,o,u){var a=o.length-1;switch(s){case 1:return o[a-1];case 2:this.$=new i.ProgramNode([],o[a]);break;case 3:this.$=new i.ProgramNode(o[a-2],o[a]);break;case 4:this.$=new i.ProgramNode(o[a-1],[]);break;case 5:this.$=new i.ProgramNode(o[a]);break;case 6:this.$=new i.ProgramNode([],[]);break;case 7:this.$=new i.ProgramNode([]);break;case 8:this.$=[o[a]];break;case 9:o[a-1].push(o[a]),this.$=o[a-1];break;case 10:this.$=new i.BlockNode(o[a-2],o[a-1].inverse,o[a-1],o[a]);break;case 11:this.$=new i.BlockNode(o[a-2],o[a-1],o[a-1].inverse,o[a]);break;case 12:this.$=o[a];break;case 13:this.$=o[a];break;case 14:this.$=new i.ContentNode(o[a]);break;case 15:this.$=new i.CommentNode(o[a]);break;case 16:this.$=new i.MustacheNode(o[a-1][0],o[a-1][1]);break;case 17:this.$=new i.MustacheNode(o[a-1][0],o[a-1][1]);break;case 18:this.$=o[a-1];break;case 19:this.$=new i.MustacheNode(o[a-1][0],o[a-1][1],o[a-2][2]==="&");break;case 20:this.$=new i.MustacheNode(o[a-1][0],o[a-1][1],!0);break;case 21:this.$=new i.PartialNode(o[a-1]);break;case 22:this.$=new i.PartialNode(o[a-2],o[a-1]);break;case 23:break;case 24:this.$=[[o[a-2]].concat(o[a-1]),o[a]];break;case 25:this.$=[[o[a-1]].concat(o[a]),null];break;case 26:this.$=[[o[a-1]],o[a]];break;case 27:this.$=[[o[a]],null];break;case 28:this.$=[[o[a]],null];break;case 29:o[a-1].push(o[a]),this.$=o[a-1];break;case 30:this.$=[o[a]];break;case 31:this.$=o[a];break;case 32:this.$=new i.StringNode(o[a]);break;case 33:this.$=new i.IntegerNode(o[a]);break;case 34:this.$=new i.BooleanNode(o[a]);break;case 35:this.$=o[a];break;case 36:this.$=new i.HashNode(o[a]);break;case 37:o[a-1].push(o[a]),this.$=o[a-1];break;case 38:this.$=[o[a]];break;case 39:this.$=[o[a-2],o[a]];break;case 40:this.$=[o[a-2],new i.StringNode(o[a])];break;case 41:this.$=[o[a-2],new i.IntegerNode(o[a])];break;case 42:this.$=[o[a-2],new i.BooleanNode(o[a])];break;case 43:this.$=[o[a-2],o[a]];break;case 44:this.$=new i.PartialNameNode(o[a]);break;case 45:this.$=new i.PartialNameNode(new i.StringNode(o[a]));break;case 46:this.$=new i.PartialNameNode(new i.IntegerNode(o[a]));break;case 47:this.$=new i.DataNode(o[a]);break;case 48:this.$=new i.IdNode(o[a]);break;case 49:o[a-2].push({part:o[a],separator:o[a-1]}),this.$=o[a-2];break;case 50:this.$=[{part:o[a]}]}},table:[{3:1,4:2,5:[2,7],6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],22:[1,14],23:[1,15],25:[1,16]},{1:[3]},{5:[1,17]},{5:[2,6],7:18,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,6],22:[1,14],23:[1,15],25:[1,16]},{5:[2,5],6:20,8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,5],22:[1,14],23:[1,15],25:[1,16]},{17:23,18:[1,22],21:24,29:25,36:[1,28],38:[1,27],39:26},{5:[2,8],14:[2,8],15:[2,8],16:[2,8],19:[2,8],20:[2,8],22:[2,8],23:[2,8],25:[2,8]},{4:29,6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,7],22:[1,14],23:[1,15],25:[1,16]},{4:30,6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,7],22:[1,14],23:[1,15],25:[1,16]},{5:[2,12],14:[2,12],15:[2,12],16:[2,12],19:[2,12],20:[2,12],22:[2,12],23:[2,12],25:[2,12]},{5:[2,13],14:[2,13],15:[2,13],16:[2,13],19:[2,13],20:[2,13],22:[2,13],23:[2,13],25:[2,13]},{5:[2,14],14:[2,14],15:[2,14],16:[2,14],19:[2,14],20:[2,14],22:[2,14],23:[2,14],25:[2,14]},{5:[2,15],14:[2,15],15:[2,15],16:[2,15],19:[2,15],20:[2,15],22:[2,15],23:[2,15],25:[2,15]},{17:31,21:24,29:25,36:[1,28],38:[1,27],39:26},{17:32,21:24,29:25,36:[1,28],38:[1,27],39:26},{17:33,21:24,29:25,36:[1,28],38:[1,27],39:26},{21:35,26:34,31:[1,36],32:[1,37],36:[1,28],39:26},{1:[2,1]},{5:[2,2],8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,2],22:[1,14],23:[1,15],25:[1,16]},{17:23,21:24,29:25,36:[1,28],38:[1,27],39:26},{5:[2,4],7:38,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,4],22:[1,14],23:[1,15],25:[1,16]},{5:[2,9],14:[2,9],15:[2,9],16:[2,9],19:[2,9],20:[2,9],22:[2,9],23:[2,9],25:[2,9]},{5:[2,23],14:[2,23],15:[2,23],16:[2,23],19:[2,23],20:[2,23],22:[2,23],23:[2,23],25:[2,23]},{18:[1,39]},{18:[2,27],21:44,24:[2,27],27:40,28:41,29:48,30:42,31:[1,45],32:[1,46],33:[1,47],34:43,35:49,36:[1,50],38:[1,27],39:26},{18:[2,28],24:[2,28]},{18:[2,48],24:[2,48],31:[2,48],32:[2,48],33:[2,48],36:[2,48],38:[2,48],40:[1,51]},{21:52,36:[1,28],39:26},{18:[2,50],24:[2,50],31:[2,50],32:[2,50],33:[2,50],36:[2,50],38:[2,50],40:[2,50]},{10:53,20:[1,54]},{10:55,20:[1,54]},{18:[1,56]},{18:[1,57]},{24:[1,58]},{18:[1,59],21:60,36:[1,28],39:26},{18:[2,44],36:[2,44]},{18:[2,45],36:[2,45]},{18:[2,46],36:[2,46]},{5:[2,3],8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,3],22:[1,14],23:[1,15],25:[1,16]},{14:[2,17],15:[2,17],16
+:[2,17],19:[2,17],20:[2,17],22:[2,17],23:[2,17],25:[2,17]},{18:[2,25],21:44,24:[2,25],28:61,29:48,30:62,31:[1,45],32:[1,46],33:[1,47],34:43,35:49,36:[1,50],38:[1,27],39:26},{18:[2,26],24:[2,26]},{18:[2,30],24:[2,30],31:[2,30],32:[2,30],33:[2,30],36:[2,30],38:[2,30]},{18:[2,36],24:[2,36],35:63,36:[1,64]},{18:[2,31],24:[2,31],31:[2,31],32:[2,31],33:[2,31],36:[2,31],38:[2,31]},{18:[2,32],24:[2,32],31:[2,32],32:[2,32],33:[2,32],36:[2,32],38:[2,32]},{18:[2,33],24:[2,33],31:[2,33],32:[2,33],33:[2,33],36:[2,33],38:[2,33]},{18:[2,34],24:[2,34],31:[2,34],32:[2,34],33:[2,34],36:[2,34],38:[2,34]},{18:[2,35],24:[2,35],31:[2,35],32:[2,35],33:[2,35],36:[2,35],38:[2,35]},{18:[2,38],24:[2,38],36:[2,38]},{18:[2,50],24:[2,50],31:[2,50],32:[2,50],33:[2,50],36:[2,50],37:[1,65],38:[2,50],40:[2,50]},{36:[1,66]},{18:[2,47],24:[2,47],31:[2,47],32:[2,47],33:[2,47],36:[2,47],38:[2,47]},{5:[2,10],14:[2,10],15:[2,10],16:[2,10],19:[2,10],20:[2,10],22:[2,10],23:[2,10],25:[2,10]},{21:67,36:[1,28],39:26},{5:[2,11],14:[2,11],15:[2,11],16:[2,11],19:[2,11],20:[2,11],22:[2,11],23:[2,11],25:[2,11]},{14:[2,16],15:[2,16],16:[2,16],19:[2,16],20:[2,16],22:[2,16],23:[2,16],25:[2,16]},{5:[2,19],14:[2,19],15:[2,19],16:[2,19],19:[2,19],20:[2,19],22:[2,19],23:[2,19],25:[2,19]},{5:[2,20],14:[2,20],15:[2,20],16:[2,20],19:[2,20],20:[2,20],22:[2,20],23:[2,20],25:[2,20]},{5:[2,21],14:[2,21],15:[2,21],16:[2,21],19:[2,21],20:[2,21],22:[2,21],23:[2,21],25:[2,21]},{18:[1,68]},{18:[2,24],24:[2,24]},{18:[2,29],24:[2,29],31:[2,29],32:[2,29],33:[2,29],36:[2,29],38:[2,29]},{18:[2,37],24:[2,37],36:[2,37]},{37:[1,65]},{21:69,29:73,31:[1,70],32:[1,71],33:[1,72],36:[1,28],38:[1,27],39:26},{18:[2,49],24:[2,49],31:[2,49],32:[2,49],33:[2,49],36:[2,49],38:[2,49],40:[2,49]},{18:[1,74]},{5:[2,22],14:[2,22],15:[2,22],16:[2,22],19:[2,22],20:[2,22],22:[2,22],23:[2,22],25:[2,22]},{18:[2,39],24:[2,39],36:[2,39]},{18:[2,40],24:[2,40],36:[2,40]},{18:[2,41],24:[2,41],36:[2,41]},{18:[2,42],24:[2,42],36:[2,42]},{18:[2,43],24:[2,43],36:[2,43]},{5:[2,18],14:[2,18],15:[2,18],16:[2,18],19:[2,18],20:[2,18],22:[2,18],23:[2,18],25:[2,18]}],defaultActions:{17:[2,1]},parseError:function(t,n){throw new Error(t)},parse:function(t){function v(e){r.length=r.length-2*e,i.length=i.length-e,s.length=s.length-e}function m(){var e;return e=n.lexer.lex()||1,typeof e!="number"&&(e=n.symbols_[e]||e),e}var n=this,r=[0],i=[null],s=[],o=this.table,u="",a=0,f=0,l=0,c=2,h=1;this.lexer.setInput(t),this.lexer.yy=this.yy,this.yy.lexer=this.lexer,this.yy.parser=this,typeof this.lexer.yylloc=="undefined"&&(this.lexer.yylloc={});var p=this.lexer.yylloc;s.push(p);var d=this.lexer.options&&this.lexer.options.ranges;typeof this.yy.parseError=="function"&&(this.parseError=this.yy.parseError);var g,y,b,w,E,S,x={},T,N,C,k;for(;;){b=r[r.length-1];if(this.defaultActions[b])w=this.defaultActions[b];else{if(g===null||typeof g=="undefined")g=m();w=o[b]&&o[b][g]}if(typeof w=="undefined"||!w.length||!w[0]){var L="";if(!l){k=[];for(T in o[b])this.terminals_[T]&&T>2&&k.push("'"+this.terminals_[T]+"'");this.lexer.showPosition?L="Parse error on line "+(a+1)+":\n"+this.lexer.showPosition()+"\nExpecting "+k.join(", ")+", got '"+(this.terminals_[g]||g)+"'":L="Parse error on line "+(a+1)+": Unexpected "+(g==1?"end of input":"'"+(this.terminals_[g]||g)+"'"),this.parseError(L,{text:this.lexer.match,token:this.terminals_[g]||g,line:this.lexer.yylineno,loc:p,expected:k})}}if(w[0]instanceof Array&&w.length>1)throw new Error("Parse Error: multiple actions possible at state: "+b+", token: "+g);switch(w[0]){case 1:r.push(g),i.push(this.lexer.yytext),s.push(this.lexer.yylloc),r.push(w[1]),g=null,y?(g=y,y=null):(f=this.lexer.yyleng,u=this.lexer.yytext,a=this.lexer.yylineno,p=this.lexer.yylloc,l>0&&l--);break;case 2:N=this.productions_[w[1]][1],x.$=i[i.length-N],x._$={first_line:s[s.length-(N||1)].first_line,last_line:s[s.length-1].last_line,first_column:s[s.length-(N||1)].first_column,last_column:s[s.length-1].last_column},d&&(x._$.range=[s[s.length-(N||1)].range[0],s[s.length-1].range[1]]),S=this.performAction.call(x,u,f,a,this.yy,w[1],i,s);if(typeof S!="undefined")return S;N&&(r=r.slice(0,-1*N*2),i=i.slice(0,-1*N),s=s.slice(0,-1*N)),r.push(this.productions_[w[1]][0]),i.push(x.$),s.push(x._$),C=o[r[r.length-2]][r[r.length-1]],r.push(C);break;case 3:return!0}}return!0}},t=function(){var e={EOF:1,parseError:function(t,n){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,n)},setInput:function(e){return this._input=e,this._more=this._less=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var e=this._input[0];this.yytext+=e,this.yyleng++,this.offset++,this.match+=e,this.matched+=e;var t=e.match(/(?:\r\n?|\n).*/g);return t?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),e},unput:function(e){var t=e.length,n=e.split(/(?:\r\n?|\n)/g);this._input=e+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-t-1),this.offset-=t;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-t},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-t]),this},more:function(){return this._more=!0,this},less:function(e){this.unput(this.match.slice(e))},pastInput:function(){var e=this.matched.substr(0,this.matched.length-this.match.length);return(e.length>20?"...":"")+e.substr(-20).replace(/\n/g,"")},upcomingInput:function(
+){var e=this.match;return e.length<20&&(e+=this._input.substr(0,20-e.length)),(e.substr(0,20)+(e.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var e=this.pastInput(),t=(new Array(e.length+1)).join("-");return e+this.upcomingInput()+"\n"+t+"^"},next:function(){if(this.done)return this.EOF;this._input||(this.done=!0);var e,t,n,r,i,s;this._more||(this.yytext="",this.match="");var o=this._currentRules();for(var u=0;u<o.length;u++){n=this._input.match(this.rules[o[u]]);if(n&&(!t||n[0].length>t[0].length)){t=n,r=u;if(!this.options.flex)break}}if(t){s=t[0].match(/(?:\r\n?|\n).*/g),s&&(this.yylineno+=s.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:s?s[s.length-1].length-s[s.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],e=this.performAction.call(this,this.yy,this,o[r],this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1);if(e)return e;return}return this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){var t=this.next();return typeof t!="undefined"?t:this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.pop()},_currentRules:function(){return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules},topState:function(){return this.conditionStack[this.conditionStack.length-2]},pushState:function(t){this.begin(t)}};return e.options={},e.performAction=function(t,n,r,i){var s=i;switch(r){case 0:return n.yytext="\\",14;case 1:n.yytext.slice(-1)!=="\\"&&this.begin("mu"),n.yytext.slice(-1)==="\\"&&(n.yytext=n.yytext.substr(0,n.yyleng-1),this.begin("emu"));if(n.yytext)return 14;break;case 2:return 14;case 3:return n.yytext.slice(-1)!=="\\"&&this.popState(),n.yytext.slice(-1)==="\\"&&(n.yytext=n.yytext.substr(0,n.yyleng-1)),14;case 4:return n.yytext=n.yytext.substr(0,n.yyleng-4),this.popState(),15;case 5:return 25;case 6:return 16;case 7:return 20;case 8:return 19;case 9:return 19;case 10:return 23;case 11:return 22;case 12:this.popState(),this.begin("com");break;case 13:return n.yytext=n.yytext.substr(3,n.yyleng-5),this.popState(),15;case 14:return 22;case 15:return 37;case 16:return 36;case 17:return 36;case 18:return 40;case 19:break;case 20:return this.popState(),24;case 21:return this.popState(),18;case 22:return n.yytext=n.yytext.substr(1,n.yyleng-2).replace(/\\"/g,'"'),31;case 23:return n.yytext=n.yytext.substr(1,n.yyleng-2).replace(/\\'/g,"'"),31;case 24:return 38;case 25:return 33;case 26:return 33;case 27:return 32;case 28:return 36;case 29:return n.yytext=n.yytext.substr(1,n.yyleng-2),36;case 30:return"INVALID";case 31:return 5}},e.rules=[/^(?:\\\\(?=(\{\{)))/,/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\{\{>)/,/^(?:\{\{#)/,/^(?:\{\{\/)/,/^(?:\{\{\^)/,/^(?:\{\{\s*else\b)/,/^(?:\{\{\{)/,/^(?:\{\{&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{)/,/^(?:=)/,/^(?:\.(?=[}\/ ]))/,/^(?:\.\.)/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}\}\})/,/^(?:\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@)/,/^(?:true(?=[}\s]))/,/^(?:false(?=[}\s]))/,/^(?:-?[0-9]+(?=[}\s]))/,/^(?:[^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=[=}\s\/.]))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/],e.conditions={mu:{rules:[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31],inclusive:!1},emu:{rules:[3],inclusive:!1},com:{rules:[4],inclusive:!1},INITIAL:{rules:[0,1,2,31],inclusive:!0}},e}();return e.lexer=t,n.prototype=e,e.Parser=n,new n}();n.Parser=r,n.parse=function(e){return e.constructor===n.AST.ProgramNode?e:(n.Parser.yy=n.AST,n.Parser.parse(e))},n.AST={},n.AST.ProgramNode=function(e,t){this.type="program",this.statements=e,t&&(this.inverse=new n.AST.ProgramNode(t))},n.AST.MustacheNode=function(e,t,n){this.type="mustache",this.escaped=!n,this.hash=t;var r=this.id=e[0],i=this.params=e.slice(1),s=this.eligibleHelper=r.isSimple;this.isHelper=s&&(i.length||t)},n.AST.PartialNode=function(e,t){this.type="partial",this.partialName=e,this.context=t},n.AST.BlockNode=function(e,t,r,i){var s=function(e,t){if(e.original!==t.original)throw new n.Exception(e.original+" doesn't match "+t.original)};s(e.id,i),this.type="block",this.mustache=e,this.program=t,this.inverse=r,this.inverse&&!this.program&&(this.isInverse=!0)},n.AST.ContentNode=function(e){this.type="content",this.string=e},n.AST.HashNode=function(e){this.type="hash",this.pairs=e},n.AST.IdNode=function(e){this.type="ID";var t="",r=[],i=0;for(var s=0,o=e.length;s<o;s++){var u=e[s].part;t+=(e[s].separator||"")+u;if(u===".."||u==="."||u==="this"){if(r.length>0)throw new n.Exception("Invalid path: "+t);u===".."?i++:this.isScoped=!0}else r.push(u)}this.original=t,this.parts=r,this.string=r.join("."),this.depth=i,this.isSimple=e.length===1&&!this.isScoped&&i===0,this.stringModeValue=this.string},n.AST.PartialNameNode=function(e){this.type="PARTIAL_NAME",this.name=e.original},n.AST.DataNode=function(e){this.type="DATA",this.id=e},n.AST.StringNode=function(e){this.type="STRING",this.original=this.string=this.stringModeValue=e},n.AST.IntegerNode=function(e){this.type="INTEGER",this.original=this.integer=e,this.stringModeValue=Number(e)},n.AST.BooleanNode=function(e){this.type="BOOLEAN",this.bool=e,this.stringModeValue=e==="true"},n.AST.CommentNode=function(e){this.type="comment",this.comment=e};var i=n.Compiler=function(){},s=n.JavaScriptCompiler=function(){};i.prototype={compiler:i,disassemble:function(){var e=this.opcodes,t,n=[],r,i;for(var s=0,o=e.length;s<o;s++){t=e[s];if(t.opcode==="DECLARE")n.push("DECLARE "+t.name+"="+t.value);else{
+r=[];for(var u=0;u<t.args.length;u++)i=t.args[u],typeof i=="string"&&(i='"'+i.replace("\n","\\n")+'"'),r.push(i);n.push(t.opcode+" "+r.join(" "))}}return n.join("\n")},equals:function(e){var t=this.opcodes.length;if(e.opcodes.length!==t)return!1;for(var n=0;n<t;n++){var r=this.opcodes[n],i=e.opcodes[n];if(r.opcode!==i.opcode||r.args.length!==i.args.length)return!1;for(var s=0;s<r.args.length;s++)if(r.args[s]!==i.args[s])return!1}t=this.children.length;if(e.children.length!==t)return!1;for(n=0;n<t;n++)if(!this.children[n].equals(e.children[n]))return!1;return!0},guid:0,compile:function(e,t){this.children=[],this.depths={list:[]},this.options=t;var n=this.options.knownHelpers;this.options.knownHelpers={helperMissing:!0,blockHelperMissing:!0,each:!0,"if":!0,unless:!0,"with":!0,log:!0};if(n)for(var r in n)this.options.knownHelpers[r]=n[r];return this.program(e)},accept:function(e){return this[e.type](e)},program:function(e){var t=e.statements,n;this.opcodes=[];for(var r=0,i=t.length;r<i;r++)n=t[r],this[n.type](n);return this.isSimple=i===1,this.depths.list=this.depths.list.sort(function(e,t){return e-t}),this},compileProgram:function(e){var t=(new this.compiler).compile(e,this.options),n=this.guid++,r;this.usePartial=this.usePartial||t.usePartial,this.children[n]=t;for(var i=0,s=t.depths.list.length;i<s;i++){r=t.depths.list[i];if(r<2)continue;this.addDepth(r-1)}return n},block:function(e){var t=e.mustache,n=e.program,r=e.inverse;n&&(n=this.compileProgram(n)),r&&(r=this.compileProgram(r));var i=this.classifyMustache(t);i==="helper"?this.helperMustache(t,n,r):i==="simple"?(this.simpleMustache(t),this.opcode("pushProgram",n),this.opcode("pushProgram",r),this.opcode("emptyHash"),this.opcode("blockValue")):(this.ambiguousMustache(t,n,r),this.opcode("pushProgram",n),this.opcode("pushProgram",r),this.opcode("emptyHash"),this.opcode("ambiguousBlockValue")),this.opcode("append")},hash:function(e){var t=e.pairs,n,r;this.opcode("pushHash");for(var i=0,s=t.length;i<s;i++)n=t[i],r=n[1],this.options.stringParams?(r.depth&&this.addDepth(r.depth),this.opcode("getContext",r.depth||0),this.opcode("pushStringParam",r.stringModeValue,r.type)):this.accept(r),this.opcode("assignToHash",n[0]);this.opcode("popHash")},partial:function(e){var t=e.partialName;this.usePartial=!0,e.context?this.ID(e.context):this.opcode("push","depth0"),this.opcode("invokePartial",t.name),this.opcode("append")},content:function(e){this.opcode("appendContent",e.string)},mustache:function(e){var t=this.options,n=this.classifyMustache(e);n==="simple"?this.simpleMustache(e):n==="helper"?this.helperMustache(e):this.ambiguousMustache(e),e.escaped&&!t.noEscape?this.opcode("appendEscaped"):this.opcode("append")},ambiguousMustache:function(e,t,n){var r=e.id,i=r.parts[0],s=t!=null||n!=null;this.opcode("getContext",r.depth),this.opcode("pushProgram",t),this.opcode("pushProgram",n),this.opcode("invokeAmbiguous",i,s)},simpleMustache:function(e){var t=e.id;t.type==="DATA"?this.DATA(t):t.parts.length?this.ID(t):(this.addDepth(t.depth),this.opcode("getContext",t.depth),this.opcode("pushContext")),this.opcode("resolvePossibleLambda")},helperMustache:function(e,t,n){var r=this.setupFullMustacheParams(e,t,n),i=e.id.parts[0];if(this.options.knownHelpers[i])this.opcode("invokeKnownHelper",r.length,i);else{if(this.options.knownHelpersOnly)throw new Error("You specified knownHelpersOnly, but used the unknown helper "+i);this.opcode("invokeHelper",r.length,i)}},ID:function(e){this.addDepth(e.depth),this.opcode("getContext",e.depth);var t=e.parts[0];t?this.opcode("lookupOnContext",e.parts[0]):this.opcode("pushContext");for(var n=1,r=e.parts.length;n<r;n++)this.opcode("lookup",e.parts[n])},DATA:function(e){this.options.data=!0;if(e.id.isScoped||e.id.depth)throw new n.Exception("Scoped data references are not supported: "+e.original);this.opcode("lookupData");var t=e.id.parts;for(var r=0,i=t.length;r<i;r++)this.opcode("lookup",t[r])},STRING:function(e){this.opcode("pushString",e.string)},INTEGER:function(e){this.opcode("pushLiteral",e.integer)},BOOLEAN:function(e){this.opcode("pushLiteral",e.bool)},comment:function(){},opcode:function(e){this.opcodes.push({opcode:e,args:[].slice.call(arguments,1)})},declare:function(e,t){this.opcodes.push({opcode:"DECLARE",name:e,value:t})},addDepth:function(e){if(isNaN(e))throw new Error("EWOT");if(e===0)return;this.depths[e]||(this.depths[e]=!0,this.depths.list.push(e))},classifyMustache:function(e){var t=e.isHelper,n=e.eligibleHelper,r=this.options;if(n&&!t){var i=e.id.parts[0];r.knownHelpers[i]?t=!0:r.knownHelpersOnly&&(n=!1)}return t?"helper":n?"ambiguous":"simple"},pushParams:function(e){var t=e.length,n;while(t--)n=e[t],this.options.stringParams?(n.depth&&this.addDepth(n.depth),this.opcode("getContext",n.depth||0),this.opcode("pushStringParam",n.stringModeValue,n.type)):this[n.type](n)},setupMustacheParams:function(e){var t=e.params;return this.pushParams(t),e.hash?this.hash(e.hash):this.opcode("emptyHash"),t},setupFullMustacheParams:function(e,t,n){var r=e.params;return this.pushParams(r),this.opcode("pushProgram",t),this.opcode("pushProgram",n),e.hash?this.hash(e.hash):this.opcode("emptyHash"),r}};var o=function(e){this.value=e};s.prototype={nameLookup:function(e,t){return/^[0-9]+$/.test(t)?e+"["+t+"]":s.isValidJavaScriptVariableName(t)?e+"."+t:e+"['"+t+"']"},appendToBuffer:function(e){return this.environment.isSimple?"return "+e+";":{appendToBuffer:!0,content:e,toString:function(){return"buffer += "+e+";"}}},initializeBuffer:function(){return this.quotedString("")},namespace:"Handlebars",compile:function(e,t,r,i){this.environment=e,this.options=t||{},n.log(n.logger.DEBUG,this.environment.disassemble()+"\n\n"),this.name=this.environment.name,this.isChild=!!r,this.context=r||{programs:[],environments:[],aliases:{}},this.preamble(),this.stackSlot=0,this.stackVars=[],this.registers={list:[]},this.compileStack=[],this.inlineStack=[],this.compileChildren(e,t);var s=e.opcodes,o;this.i=0;for(l=s.length;this.i<l;this.i++)o=s[this
+.i],o.opcode==="DECLARE"?this[o.name]=o.value:this[o.opcode].apply(this,o.args);return this.createFunctionContext(i)},nextOpcode:function(){var e=this.environment.opcodes;return e[this.i+1]},eat:function(){this.i=this.i+1},preamble:function(){var e=[];if(!this.isChild){var t=this.namespace,n="helpers = this.merge(helpers, "+t+".helpers);";this.environment.usePartial&&(n=n+" partials = this.merge(partials, "+t+".partials);"),this.options.data&&(n+=" data = data || {};"),e.push(n)}else e.push("");this.environment.isSimple?e.push(""):e.push(", buffer = "+this.initializeBuffer()),this.lastContext=0,this.source=e},createFunctionContext:function(e){var t=this.stackVars.concat(this.registers.list);t.length>0&&(this.source[1]=this.source[1]+", "+t.join(", "));if(!this.isChild)for(var r in this.context.aliases)this.context.aliases.hasOwnProperty(r)&&(this.source[1]=this.source[1]+", "+r+"="+this.context.aliases[r]);this.source[1]&&(this.source[1]="var "+this.source[1].substring(2)+";"),this.isChild||(this.source[1]+="\n"+this.context.programs.join("\n")+"\n"),this.environment.isSimple||this.source.push("return buffer;");var i=this.isChild?["depth0","data"]:["Handlebars","depth0","helpers","partials","data"];for(var s=0,o=this.environment.depths.list.length;s<o;s++)i.push("depth"+this.environment.depths.list[s]);var u=this.mergeSource();if(!this.isChild){var a=n.COMPILER_REVISION,f=n.REVISION_CHANGES[a];u="this.compilerInfo = ["+a+",'"+f+"'];\n"+u}if(e)return i.push(u),Function.apply(this,i);var l="function "+(this.name||"")+"("+i.join(",")+") {\n "+u+"}";return n.log(n.logger.DEBUG,l+"\n\n"),l},mergeSource:function(){var e="",t;for(var n=0,r=this.source.length;n<r;n++){var i=this.source[n];i.appendToBuffer?t?t=t+"\n + "+i.content:t=i.content:(t&&(e+="buffer += "+t+";\n ",t=undefined),e+=i+"\n ")}return e},blockValue:function(){this.context.aliases.blockHelperMissing="helpers.blockHelperMissing";var e=["depth0"];this.setupParams(0,e),this.replaceStack(function(t){return e.splice(1,0,t),"blockHelperMissing.call("+e.join(", ")+")"})},ambiguousBlockValue:function(){this.context.aliases.blockHelperMissing="helpers.blockHelperMissing";var e=["depth0"];this.setupParams(0,e);var t=this.topStack();e.splice(1,0,t),e[e.length-1]="options",this.source.push("if (!"+this.lastHelper+") { "+t+" = blockHelperMissing.call("+e.join(", ")+"); }")},appendContent:function(e){this.source.push(this.appendToBuffer(this.quotedString(e)))},append:function(){this.flushInline();var e=this.popStack();this.source.push("if("+e+" || "+e+" === 0) { "+this.appendToBuffer(e)+" }"),this.environment.isSimple&&this.source.push("else { "+this.appendToBuffer("''")+" }")},appendEscaped:function(){this.context.aliases.escapeExpression="this.escapeExpression",this.source.push(this.appendToBuffer("escapeExpression("+this.popStack()+")"))},getContext:function(e){this.lastContext!==e&&(this.lastContext=e)},lookupOnContext:function(e){this.push(this.nameLookup("depth"+this.lastContext,e,"context"))},pushContext:function(){this.pushStackLiteral("depth"+this.lastContext)},resolvePossibleLambda:function(){this.context.aliases.functionType='"function"',this.replaceStack(function(e){return"typeof "+e+" === functionType ? "+e+".apply(depth0) : "+e})},lookup:function(e){this.replaceStack(function(t){return t+" == null || "+t+" === false ? "+t+" : "+this.nameLookup(t,e,"context")})},lookupData:function(e){this.push("data")},pushStringParam:function(e,t){this.pushStackLiteral("depth"+this.lastContext),this.pushString(t),typeof e=="string"?this.pushString(e):this.pushStackLiteral(e)},emptyHash:function(){this.pushStackLiteral("{}"),this.options.stringParams&&(this.register("hashTypes","{}"),this.register("hashContexts","{}"))},pushHash:function(){this.hash={values:[],types:[],contexts:[]}},popHash:function(){var e=this.hash;this.hash=undefined,this.options.stringParams&&(this.register("hashContexts","{"+e.contexts.join(",")+"}"),this.register("hashTypes","{"+e.types.join(",")+"}")),this.push("{\n "+e.values.join(",\n ")+"\n }")},pushString:function(e){this.pushStackLiteral(this.quotedString(e))},push:function(e){return this.inlineStack.push(e),e},pushLiteral:function(e){this.pushStackLiteral(e)},pushProgram:function(e){e!=null?this.pushStackLiteral(this.programExpression(e)):this.pushStackLiteral(null)},invokeHelper:function(e,t){this.context.aliases.helperMissing="helpers.helperMissing";var n=this.lastHelper=this.setupHelper(e,t,!0),r=this.nameLookup("depth"+this.lastContext,t,"context");this.push(n.name+" || "+r),this.replaceStack(function(e){return e+" ? "+e+".call("+n.callParams+") "+": helperMissing.call("+n.helperMissingParams+")"})},invokeKnownHelper:function(e,t){var n=this.setupHelper(e,t);this.push(n.name+".call("+n.callParams+")")},invokeAmbiguous:function(e,t){this.context.aliases.functionType='"function"',this.pushStackLiteral("{}");var n=this.setupHelper(0,e,t),r=this.lastHelper=this.nameLookup("helpers",e,"helper"),i=this.nameLookup("depth"+this.lastContext,e,"context"),s=this.nextStack();this.source.push("if ("+s+" = "+r+") { "+s+" = "+s+".call("+n.callParams+"); }"),this.source.push("else { "+s+" = "+i+"; "+s+" = typeof "+s+" === functionType ? "+s+".apply(depth0) : "+s+"; }")},invokePartial:function(e){var t=[this.nameLookup("partials",e,"partial"),"'"+e+"'",this.popStack(),"helpers","partials"];this.options.data&&t.push("data"),this.context.aliases.self="this",this.push("self.invokePartial("+t.join(", ")+")")},assignToHash:function(e){var t=this.popStack(),n,r;this.options.stringParams&&(r=this.popStack(),n=this.popStack());var i=this.hash;n&&i.contexts.push("'"+e+"': "+n),r&&i.types.push("'"+e+"': "+r),i.values.push("'"+e+"': ("+t+")")},compiler:s,compileChildren:function(e,t){var n=e.children,r,i;for(var s=0,o=n.length;s<o;s++){r=n[s],i=new this.compiler;var u=this.matchExistingProgram(r);u==null?(this.context.programs.push(""),u=this.context.programs.length,r.index=u,r.name="program"+u,this.context.programs[u]=i.compile
+(r,t,this.context),this.context.environments[u]=r):(r.index=u,r.name="program"+u)}},matchExistingProgram:function(e){for(var t=0,n=this.context.environments.length;t<n;t++){var r=this.context.environments[t];if(r&&r.equals(e))return t}},programExpression:function(e){this.context.aliases.self="this";if(e==null)return"self.noop";var t=this.environment.children[e],n=t.depths.list,r,i=[t.index,t.name,"data"];for(var s=0,o=n.length;s<o;s++)r=n[s],r===1?i.push("depth0"):i.push("depth"+(r-1));return(n.length===0?"self.program(":"self.programWithDepth(")+i.join(", ")+")"},register:function(e,t){this.useRegister(e),this.source.push(e+" = "+t+";")},useRegister:function(e){this.registers[e]||(this.registers[e]=!0,this.registers.list.push(e))},pushStackLiteral:function(e){return this.push(new o(e))},pushStack:function(e){this.flushInline();var t=this.incrStack();return e&&this.source.push(t+" = "+e+";"),this.compileStack.push(t),t},replaceStack:function(e){var t="",n=this.isInline(),r;if(n){var i=this.popStack(!0);if(i instanceof o)r=i.value;else{var s=this.stackSlot?this.topStackName():this.incrStack();t="("+this.push(s)+" = "+i+"),",r=this.topStack()}}else r=this.topStack();var u=e.call(this,r);return n?((this.inlineStack.length||this.compileStack.length)&&this.popStack(),this.push("("+t+u+")")):(/^stack/.test(r)||(r=this.nextStack()),this.source.push(r+" = ("+t+u+");")),r},nextStack:function(){return this.pushStack()},incrStack:function(){return this.stackSlot++,this.stackSlot>this.stackVars.length&&this.stackVars.push("stack"+this.stackSlot),this.topStackName()},topStackName:function(){return"stack"+this.stackSlot},flushInline:function(){var e=this.inlineStack;if(e.length){this.inlineStack=[];for(var t=0,n=e.length;t<n;t++){var r=e[t];r instanceof o?this.compileStack.push(r):this.pushStack(r)}}},isInline:function(){return this.inlineStack.length},popStack:function(e){var t=this.isInline(),n=(t?this.inlineStack:this.compileStack).pop();return!e&&n instanceof o?n.value:(t||this.stackSlot--,n)},topStack:function(e){var t=this.isInline()?this.inlineStack:this.compileStack,n=t[t.length-1];return!e&&n instanceof o?n.value:n},quotedString:function(e){return'"'+e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\u2028/g,"\\u2028").replace(/\u2029/g,"\\u2029")+'"'},setupHelper:function(e,t,n){var r=[];this.setupParams(e,r,n);var i=this.nameLookup("helpers",t,"helper");return{params:r,name:i,callParams:["depth0"].concat(r).join(", "),helperMissingParams:n&&["depth0",this.quotedString(t)].concat(r).join(", ")}},setupParams:function(e,t,n){var r=[],i=[],s=[],o,u,a;r.push("hash:"+this.popStack()),u=this.popStack(),a=this.popStack();if(a||u)a||(this.context.aliases.self="this",a="self.noop"),u||(this.context.aliases.self="this",u="self.noop"),r.push("inverse:"+u),r.push("fn:"+a);for(var f=0;f<e;f++)o=this.popStack(),t.push(o),this.options.stringParams&&(s.push(this.popStack()),i.push(this.popStack()));return this.options.stringParams&&(r.push("contexts:["+i.join(",")+"]"),r.push("types:["+s.join(",")+"]"),r.push("hashContexts:hashContexts"),r.push("hashTypes:hashTypes")),this.options.data&&r.push("data:data"),r="{"+r.join(",")+"}",n?(this.register("options",r),t.push("options")):t.push(r),t.join(", ")}};var u="break else new var case finally return void catch for switch while continue function this with default if throw delete in try do instanceof typeof abstract enum int short boolean export interface static byte extends long super char final native synchronized class float package throws const goto private transient debugger implements protected volatile double import public let yield".split(" "),a=s.RESERVED_WORDS={};for(var f=0,l=u.length;f<l;f++)a[u[f]]=!0;s.isValidJavaScriptVariableName=function(e){return!s.RESERVED_WORDS[e]&&/^[a-zA-Z_$][0-9a-zA-Z_$]+$/.test(e)?!0:!1},n.precompile=function(e,t){if(e==null||typeof e!="string"&&e.constructor!==n.AST.ProgramNode)throw new n.Exception("You must pass a string or Handlebars AST to Handlebars.precompile. You passed "+e);t=t||{},"data"in t||(t.data=!0);var r=n.parse(e),o=(new i).compile(r,t);return(new s).compile(o,t)},n.compile=function(e,t){function o(){var r=n.parse(e),o=(new i).compile(r,t),u=(new s).compile(o,t,undefined,!0);return n.template(u)}if(e==null||typeof e!="string"&&e.constructor!==n.AST.ProgramNode)throw new n.Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed "+e);t=t||{},"data"in t||(t.data=!0);var r;return function(e,t){return r||(r=o()),r.call(this,e,t)}};var c=["debug","info","warn","error"];n.logger.log=function(e,t){},n.render=function(e,t,r){return n.compile(e)(t,r)}},"3.17.2",{requires:["handlebars-base"]});
diff --git a/js/yui3/highlight-accentfold/highlight-accentfold-min.js b/js/yui3/highlight-accentfold/highlight-accentfold-min.js
new file mode 100644
index 000000000..0fc9a65f5
--- /dev/null
+++ b/js/yui3/highlight-accentfold/highlight-accentfold-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("highlight-accentfold",function(e,t){var n=e.Text.AccentFold,r=e.Escape,i={},s=e.mix(e.Highlight,{allFold:function(t,o,u){var a=s._TEMPLATE,f=[],l=0,c,h,p,d,v;u=e.merge({escapeHTML:!1,replacer:function(e,n,r,i){var s;if(n&&!/\s/.test(r))return e;s=r.length,f.push([t.substring(l,i),t.substr(i,s)]),l=i+s}},u||i),s.all(n.fold(t),n.fold(o),u),l<t.length&&f.push([t.substr(l)]);for(h=0,p=f.length;h<p;++h){c=r.html(f[h][0]);if(d=f[h][1])c+=a.replace(/\{s\}/g,r.html(d));f[h]=c}return f.join("")},startFold:function(e,t){return s.allFold(e,t,{startsWith:!0})},wordsFold:function(e,t){var i=s._TEMPLATE;return s.words(e,n.fold(t),{mapper:function(e,t){return t.hasOwnProperty(n.fold(e))?i.replace(/\{s\}/g,r.html(e)):r.html(e)}})}})},"3.17.2",{requires:["highlight-base","text-accentfold"]});
diff --git a/js/yui3/highlight-base/highlight-base-min.js b/js/yui3/highlight-base/highlight-base-min.js
new file mode 100644
index 000000000..d9778a1e6
--- /dev/null
+++ b/js/yui3/highlight-base/highlight-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("highlight-base",function(e,t){var n=e.Array,r=e.Escape,i=e.Text.WordBreak,s=e.Lang.isArray,o={},u="(&[^;\\s]*)?",a={_REGEX:u+"(%needles)",_REPLACER:function(e,t,n){return t&&!/\s/.test(n)?e:a._TEMPLATE.replace(/\{s\}/g,n)},_START_REGEX:"^"+u+"(%needles)",_TEMPLATE:'<b class="'+e.ClassNameManager.getClassName("highlight")+'">{s}</b>',all:function(e,t,n){var i=[],u,f,l,c,h,p;n||(n=o),u=n.escapeHTML!==!1,h=n.startsWith?a._START_REGEX:a._REGEX,p=n.replacer||a._REPLACER,t=s(t)?t:[t];for(f=0,l=t.length;f<l;++f)c=t[f],c&&i.push(r.regex(u?r.html(c):c));return u&&(e=r.html(e)),i.length?e.replace(new RegExp(h.replace("%needles",i.join("|")),n.caseSensitive?"g":"gi"),p):e},allCase:function(t,n,r){return a.all(t,n,e.merge(r||o,{caseSensitive:!0}))},start:function(t,n,r){return a.all(t,n,e.merge(r||o,{startsWith:!0}))},startCase:function(e,t){return a.start(e,t,{caseSensitive:!0})},words:function(e,t,u){var f,l,c=a._TEMPLATE,h;return u||(u=o),f=!!u.caseSensitive,t=n.hash(s(t)?t:i.getUniqueWords(t,{ignoreCase:!f})),l=u.mapper||function(e,t){return t.hasOwnProperty(f?e:e.toLowerCase())?c.replace(/\{s\}/g,r.html(e)):r.html(e)},h=i.getWords(e,{includePunctuation:!0,includeWhitespace:!0}),n.map(h,function(e){return l(e,t)}).join("")},wordsCase:function(e,t){return a.words(e,t,{caseSensitive:!0})}};e.Highlight=a},"3.17.2",{requires:["array-extras","classnamemanager","escape","text-wordbreak"]});
diff --git a/js/yui3/history-base/history-base-min.js b/js/yui3/history-base/history-base-min.js
new file mode 100644
index 000000000..1f9047cc7
--- /dev/null
+++ b/js/yui3/history-base/history-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("history-base",function(e,t){function p(){this._init.apply(this,arguments)}function d(e){return n.type(e)==="object"}var n=e.Lang,r=e.Object,i=YUI.namespace("Env.History"),s=e.Array,o=e.config.doc,u=o.documentMode,a=e.config.win,f={merge:!0},l="change",c="add",h="replace";e.augment(p,e.EventTarget,null,null,{emitFacade:!0,prefix:"history",preventable:!1,queueable:!0}),i._state||(i._state={}),p.NAME="historyBase",p.SRC_ADD=c,p.SRC_REPLACE=h,p.html5=!!(a.history&&a.history.pushState&&a.history.replaceState&&("onpopstate"in a||e.UA.gecko>=2)&&(!e.UA.android||e.UA.android>=2.4)),p.nativeHashChange=("onhashchange"in a||"onhashchange"in o)&&(!u||u>7),e.mix(p.prototype,{_init:function(e){var t;e=this._config=e||{},this.force=!!e.force,t=this._initialState=this._initialState||e.initialState||null,this.publish(l,{broadcast:2,defaultFn:this._defChangeFn}),t&&this.replace(t)},add:function(){var e=s(arguments,0,!0);return e.unshift(c),this._change.apply(this,e)},addValue:function(e,t,n){var r={};return r[e]=t,this._change(c,r,n)},get:function(t){var n=i._state,s=d(n);return t?s&&r.owns(n,t)?n[t]:undefined:s?e.mix({},n,!0):n},replace:function(){var e=s(arguments,0,!0);return e.unshift(h),this._change.apply(this,e)},replaceValue:function(e,t,n){var r={};return r[e]=t,this._change(h,r,n)},_change:function(t,n,r){return r=r?e.merge(f,r):f,r.merge&&d(n)&&d(i._state)&&(n=e.merge(i._state,n)),this._resolveChanges(t,n,r),this},_fireEvents:function(e,t,n){this.fire(l,{_options:n,changed:t.changed,newVal:t.newState,prevVal:t.prevState,removed:t.removed,src:e}),r.each(t.changed,function(t,n){this._fireChangeEvent(e,n,t)},this),r.each(t.removed,function(t,n){this._fireRemoveEvent(e,n,t)},this)},_fireChangeEvent:function(e,t,n){this.fire(t+"Change",{newVal:n.newVal,prevVal:n.prevVal,src:e})},_fireRemoveEvent:function(e,t,n){this.fire(t+"Remove",{prevVal:n,src:e})},_resolveChanges:function(e,t,n){var s={},o,u=i._state,a={};t||(t={}),n||(n={}),d(t)&&d(u)?(r.each(t,function(e,t){var n=u[t];e!==n&&(s[t]={newVal:e,prevVal:n},o=!0)},this),r.each(u,function(e,n){if(!r.owns(t,n)||t[n]===null)delete t[n],a[n]=e,o=!0},this)):o=t!==u,(o||this.force)&&this._fireEvents(e,{changed:s,newState:t,prevState:u,removed:a},n)},_storeState:function(e,t){i._state=t||{}},_defChangeFn:function(e){this._storeState(e.src,e.newVal,e._options)}},!0),e.HistoryBase=p},"3.17.2",{requires:["event-custom-complex"]});
diff --git a/js/yui3/history-hash-ie/history-hash-ie-min.js b/js/yui3/history-hash-ie/history-hash-ie-min.js
new file mode 100644
index 000000000..9a8659509
--- /dev/null
+++ b/js/yui3/history-hash-ie/history-hash-ie-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("history-hash-ie",function(e,t){if(e.UA.ie&&!e.HistoryBase.nativeHashChange){var n=e.Do,r=YUI.namespace("Env.HistoryHash"),i=e.HistoryHash,s=r._iframe,o=e.config.win;i.getIframeHash=function(){if(!s||!s.contentWindow)return"";var e=i.hashPrefix,t=s.contentWindow.location.hash.substr(1);return e&&t.indexOf(e)===0?t.replace(e,""):t},i._updateIframe=function(e,t){var n=s&&s.contentWindow&&s.contentWindow.document,r=n&&n.location;if(!n||!r)return;t?r.replace(e.charAt(0)==="#"?e:"#"+e):(n.open().close(),r.hash=e)},n.before(i._updateIframe,i,"replaceHash",i,!0),s||e.on("domready",function(){var t=i.getHash();s=r._iframe=e.Node.getDOMNode(e.Node.create('<iframe src="javascript:0" style="display:none" height="0" width="0" tabindex="-1" title="empty"/>')),e.config.doc.documentElement.appendChild(s),i._updateIframe(t||"#"),e.on("hashchange",function(e){t=e.newHash,i.getIframeHash()!==t&&i._updateIframe(t)},o),e.later(50,null,function(){var e=i.getIframeHash();e!==t&&i.setHash(e)},null,!0)})}},"3.17.2",{requires:["history-hash","node-base"]});
diff --git a/js/yui3/history-hash/history-hash-min.js b/js/yui3/history-hash/history-hash-min.js
new file mode 100644
index 000000000..f90993499
--- /dev/null
+++ b/js/yui3/history-hash/history-hash-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("history-hash",function(e,t){function p(){p.superclass.constructor.apply(this,arguments)}var n=e.HistoryBase,r=e.Lang,i=e.Array,s=e.Object,o=YUI.namespace("Env.HistoryHash"),u="hash",a,f,l,c=e.config.win,h=e.config.useHistoryHTML5;e.extend(p,n,{_init:function(t){var n=p.parseHash();t=t||{},this._initialState=t.initialState?e.merge(t.initialState,n):n,e.after("hashchange",e.bind(this._afterHashChange,this),c),p.superclass._init.apply(this,arguments)},_change:function(e,t,n){return s.each(t,function(e,n){r.isValue(e)&&(t[n]=e.toString())}),p.superclass._change.call(this,e,t,n)},_storeState:function(e,t){var r=p.decode,i=p.createHash(t);p.superclass._storeState.apply(this,arguments),e!==u&&r(p.getHash())!==r(i)&&p[e===n.SRC_REPLACE?"replaceHash":"setHash"](i)},_afterHashChange:function(e){this._resolveChanges(u,p.parseHash(e.newHash),{})}},{NAME:"historyHash",SRC_HASH:u,hashPrefix:"",_REGEX_HASH:/([^\?#&=]+)=?([^&=]*)/g,createHash:function(e){var t=p.encode,n=[];return s.each(e,function(e,i){r.isValue(e)&&n.push(t(i)+"="+t(e))}),n.join("&")},decode:function(e){return decodeURIComponent(e.replace(/\+/g," "))},encode:function(e){return encodeURIComponent(e).replace(/%20/g,"+")},getHash:e.UA.gecko?function(){var t=e.getLocation(),n=/#(.*)$/.exec(t.href),r=n&&n[1]||"",i=p.hashPrefix;return i&&r.indexOf(i)===0?r.replace(i,""):r}:function(){var t=e.getLocation(),n=t.hash.substring(1),r=p.hashPrefix;return r&&n.indexOf(r)===0?n.replace(r,""):n},getUrl:function(){return location.href},parseHash:function(e){var t=p.decode,n,i,s,o,u,a={},f=p.hashPrefix,l;e=r.isValue(e)?e:p.getHash();if(f){l=e.indexOf(f);if(l===0||l===1&&e.charAt(0)==="#")e=e.replace(f,"")}o=e.match(p._REGEX_HASH)||[];for(n=0,i=o.length;n<i;++n)s=o[n],u=s.split("="),u.length>1?a[t(u[0])]=t(u[1]):a[t(s)]="";return a},replaceHash:function(t){var n=e.getLocation(),r=n.href.replace(/#.*$/,"");t.charAt(0)==="#"&&(t=t.substring(1)),n.replace(r+"#"+(p.hashPrefix||"")+t)},setHash:function(t){var n=e.getLocation();t.charAt(0)==="#"&&(t=t.substring(1)),n.hash=(p.hashPrefix||"")+t}}),a=o._notifiers,a||(a=o._notifiers=[]),e.Event.define("hashchange",{on:function(t,n,r){(t.compareTo(c)||t.compareTo(e.config.doc.body))&&a.push(r)},detach:function(e,t,n){var r=i.indexOf(a,n);r!==-1&&a.splice(r,1)}}),f=p.getHash(),l=p.getUrl(),n.nativeHashChange?o._hashHandle||(o._hashHandle=e.Event.attach("hashchange",function(e){var t=p.getHash(),n=p.getUrl();i.each(a.concat(),function(r){r.fire({_event:e,oldHash:f,oldUrl:l,newHash:t,newUrl:n})}),f=t,l=n},c)):o._hashPoll||(o._hashPoll=e.later(50,null,function(){var e=p.getHash(),t,n;f!==e&&(n=p.getUrl(),t={oldHash:f,oldUrl:l,newHash:e,newUrl:n},f=e,l=n,i.each(a.concat(),function(e){e.fire(t)}))},null,!0)),e.HistoryHash=p;if(h===!1||!e.History&&h!==!0&&(!n.html5||!e.HistoryHTML5))e.History=p},"3.17.2",{requires:["event-synthetic","history-base","yui-later"]});
diff --git a/js/yui3/history-html5/history-html5-min.js b/js/yui3/history-html5/history-html5-min.js
new file mode 100644
index 000000000..ed891ad1a
--- /dev/null
+++ b/js/yui3/history-html5/history-html5-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("history-html5",function(e,t){function a(){a.superclass.constructor.apply(this,arguments)}var n=e.HistoryBase,r=e.Lang,i=e.config.win,s=e.config.useHistoryHTML5,o="popstate",u=n.SRC_REPLACE;e.extend(a,n,{_init:function(t){var n;try{n=i.history.state}catch(s){n=null}e.Object.isEmpty(n)&&(n=null),t||(t={}),t.initialState&&r.type(t.initialState)==="object"&&r.type(n)==="object"?this._initialState=e.merge(t.initialState,n):this._initialState=n,e.on("popstate",this._onPopState,i,this),a.superclass._init.apply(this,arguments)},_storeState:function(t,n,r){t!==o&&i.history[t===u?"replaceState":"pushState"](n,r.title||e.config.doc.title||"",r.url||e.config.doc.URL),a.superclass._storeState.apply(this,arguments)},_onPopState:function(e){this._resolveChanges(o,e._event.state||null)}},{NAME:"historyhtml5",SRC_POPSTATE:o}),e.Node.DOM_EVENTS.popstate||(e.Node.DOM_EVENTS.popstate=1),e.HistoryHTML5=a;if(s===!0||s!==!1&&n.html5)e.History=a},"3.17.2",{optional:["json"],requires:["event-base","history-base","node-base"]});
diff --git a/js/yui3/imageloader/imageloader-min.js b/js/yui3/imageloader/imageloader-min.js
new file mode 100644
index 000000000..2a22996db
--- /dev/null
+++ b/js/yui3/imageloader/imageloader-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("imageloader",function(e,t){e.ImgLoadGroup=function(){this._init(),e.ImgLoadGroup.superclass.constructor.apply(this,arguments)},e.ImgLoadGroup.NAME="imgLoadGroup",e.ImgLoadGroup.ATTRS={name:{value:""},timeLimit:{value:null},foldDistance:{validator:e.Lang.isNumber,setter:function(e){return this._setFoldTriggers(),e},lazyAdd:!1},className:{value:null,setter:function(e){return this._className=e,e},lazyAdd:!1},classNameAction:{value:"default"}};var n={_init:function(){this._triggers=[],this._imgObjs={},this._timeout=null,this._classImageEls=null,this._className=null,this._areFoldTriggersSet=!1,this._maxKnownHLimit=0,e.on("domready",this._onloadTasks,this)},addTrigger:function(t,n){if(!t||!n)return this;var r=function(){this.fetch()};return this._triggers.push(e.on(n,r,t,this)),this},addCustomTrigger:function(t,n){if(!t)return this;var r=function(){this.fetch()};return e.Lang.isUndefined(n)?this._triggers.push(e.on(t,r,this)):this._triggers.push(n.on(t,r,this)),this},_setFoldTriggers:function(){if(this._areFoldTriggersSet)return;var t=function(){this._foldCheck()};this._triggers.push(e.on("scroll",t,window,this)),this._triggers.push(e.on("resize",t,window,this)),this._areFoldTriggersSet=!0},_onloadTasks:function(){var t=this.get("timeLimit");t&&t>0&&(this._timeout=setTimeout(this._getFetchTimeout(),t*1e3)),e.Lang.isUndefined(this.get("foldDistance"))||this._foldCheck()},_getFetchTimeout:function(){var e=this;return function(){e.fetch()}},registerImage:function(){var t=arguments[0].domId;return t?(this._imgObjs[t]=new e.ImgLoadImgObj(arguments[0]),this._imgObjs[t]):null},fetch:function(){this._clearTriggers(),this._fetchByClass();for(var e in this._imgObjs)this._imgObjs.hasOwnProperty(e)&&this._imgObjs[e].fetch()},_clearTriggers:function(){clearTimeout(this._timeout);for(var e=0,t=this._triggers.length;e<t;e++)this._triggers[e].detach()},_foldCheck:function(){var t=!0,n=e.DOM.viewportRegion(),r=n.bottom+this.get("foldDistance"),i,s,o,u,a;if(r<=this._maxKnownHLimit)return;this._maxKnownHLimit=r;for(i in this._imgObjs)this._imgObjs.hasOwnProperty(i)&&(s=this._imgObjs[i].fetch(r),t=t&&s);if(this._className){this._classImageEls===null&&(this._classImageEls=[],o=e.all("."+this._className),o.each(function(e){this._classImageEls.push({el:e,y:e.getY(),fetched:!1})},this)),o=this._classImageEls;for(u=0,a=o.length;u<a;u++){if(o[u].fetched)continue;o[u].y&&o[u].y<=r?(this._updateNodeClassName(o[u].el),o[u].fetched=!0):t=!1}}t&&this._clearTriggers()},_updateNodeClassName:function(e){var t;this.get("classNameAction")=="enhanced"&&e.get("tagName").toLowerCase()=="img"&&(t=e.getStyle("backgroundImage"),/url\(["']?(.*?)["']?\)/.test(t),t=RegExp.$1,e.set("src",t),e.setStyle("backgroundImage","")),e.removeClass(this._className)},_fetchByClass:function(){if(!this._className)return;e.all("."+this._className).each(e.bind(this._updateNodeClassName,this))}};e.extend(e.ImgLoadGroup,e.Base,n),e.ImgLoadImgObj=function(){e.ImgLoadImgObj.superclass.constructor.apply(this,arguments),this._init()},e.ImgLoadImgObj.NAME="imgLoadImgObj",e.ImgLoadImgObj.ATTRS={domId:{value:null,writeOnce:!0},bgUrl:{value:null},srcUrl:{value:null},width:{value:null},height:{value:null},setVisible:{value:!1},isPng:{value:!1},sizingMethod:{value:"scale"},enabled:{value:"true"}};var r={_init:function(){this._fetched=!1,this._imgEl=null,this._yPos=null},fetch:function(t){if(this._fetched)return!0;var n=this._getImgEl(),r;if(!n)return!1;if(t){r=this._getYPos();if(!r||r>t)return!1}return this.get("bgUrl")!==null?this.get("isPng")&&e.UA.ie&&e.UA.ie<=6?n.setStyle("filter",'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="'+this.get("bgUrl")+'", sizingMethod="'+this.get("sizingMethod")+'", enabled="'+this.get("enabled")+'")'):n.setStyle("backgroundImage","url('"+this.get("bgUrl")+"')"):this.get("srcUrl")!==null&&n.setAttribute("src",this.get("srcUrl")),this.get("setVisible")&&n.setStyle("visibility","visible"),this.get("width")&&n.setAttribute("width",this.get("width")),this.get("height")&&n.setAttribute("height",this.get("height")),this._fetched=!0,!0},_getImgEl:function(){return this._imgEl===null&&(this._imgEl=e.one("#"+this.get("domId"))),this._imgEl},_getYPos:function(){return this._yPos===null&&(this._yPos=this._getImgEl().getY()),this._yPos}};e.extend(e.ImgLoadImgObj,e.Base,r)},"3.17.2",{requires:["base-base","node-style","node-screen"]});
diff --git a/js/yui3/intl-base/intl-base-min.js b/js/yui3/intl-base/intl-base-min.js
new file mode 100644
index 000000000..d6409c1fe
--- /dev/null
+++ b/js/yui3/intl-base/intl-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("intl-base",function(e,t){var n=/[, ]/;e.mix(e.namespace("Intl"),{lookupBestLang:function(t,r){function a(e){var t;for(t=0;t<r.length;t+=1)if(e.toLowerCase()===r[t].toLowerCase())return r[t]}var i,s,o,u;e.Lang.isString(t)&&(t=t.split(n));for(i=0;i<t.length;i+=1){s=t[i];if(!s||s==="*")continue;while(s.length>0){o=a(s);if(o)return o;u=s.lastIndexOf("-");if(!(u>=0))break;s=s.substring(0,u),u>=2&&s.charAt(u-2)==="-"&&(s=s.substring(0,u-2))}}return""}})},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/intl/intl-min.js b/js/yui3/intl/intl-min.js
new file mode 100644
index 000000000..5ccdb3ece
--- /dev/null
+++ b/js/yui3/intl/intl-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("intl",function(e,t){var n={},r="yuiRootLang",i="yuiActiveLang",s=[];e.mix(e.namespace("Intl"),{_mod:function(e){return n[e]||(n[e]={}),n[e]},setLang:function(e,t){var n=this._mod(e),s=n[i],o=!!n[t];return o&&t!==s&&(n[i]=t,this.fire("intl:langChange",{module:e,prevVal:s,newVal:t===r?"":t})),o},getLang:function(e){var t=this._mod(e)[i];return t===r?"":t},add:function(e,t,n){t=t||r,this._mod(e)[t]=n,this.setLang(e,t)},get:function(t,n,r){var s=this._mod(t),o;return r=r||s[i],o=s[r]||{},n?o[n]:e.merge(o)},getAvailableLangs:function(t){var n=e.Env._loader,r=n&&n.moduleInfo[t],i=r&&r.lang;return i?i.concat():s}}),e.augment(e.Intl,e.EventTarget),e.Intl.publish("intl:langChange",{emitFacade:!0})},"3.17.2",{requires:["intl-base","event-custom"]});
diff --git a/js/yui3/io-base/io-base-min.js b/js/yui3/io-base/io-base-min.js
new file mode 100644
index 000000000..def5f3ba4
--- /dev/null
+++ b/js/yui3/io-base/io-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("io-base",function(e,t){function o(t){var n=this;n._uid="io:"+s++,n._init(t),e.io._map[n._uid]=n}var n=["start","complete","end","success","failure","progress"],r=["status","statusText","responseText","responseXML"],i=e.config.win,s=0;o.prototype={_id:0,_headers:{"X-Requested-With":"XMLHttpRequest"},_timeout:{},_init:function(t){var r=this,i,s;r.cfg=t||{},e.augment(r,e.EventTarget);for(i=0,s=n.length;i<s;++i)r.publish("io:"+n[i],e.merge({broadcast:1},t)),r.publish("io-trn:"+n[i],t)},_create:function(t,n){var r=this,s={id:e.Lang.isNumber(n)?n:r._id++,uid:r._uid},o=t.xdr?t.xdr.use:null,u=t.form&&t.form.upload?"iframe":null,a;return o==="native"&&(o=e.UA.ie&&!l?"xdr":null,r.setHeader("X-Requested-With")),a=o||u,s=a?e.merge(e.IO.customTransport(a),s):e.merge(e.IO.defaultTransport(),s),s.notify&&(t.notify=function(e,t,n){r.notify(e,t,n)}),a||i&&i.FormData&&t.data instanceof i.FormData&&(s.c.upload.onprogress=function(e){r.progress(s,e,t)},s.c.onload=function(e){r.load(s,e,t)},s.c.onerror=function(e){r.error(s,e,t)},s.upload=!0),s},_destroy:function(t){i&&!t.notify&&!t.xdr&&(u&&!t.upload?t.c.onreadystatechange=null:t.upload?(t.c.upload.onprogress=null,t.c.onload=null,t.c.onerror=null):e.UA.ie&&!t.e&&t.c.abort()),t=t.c=null},_evt:function(t,r,i){var s=this,o,u=i.arguments,a=s.cfg.emitFacade,f="io:"+t,l="io-trn:"+t;this.detach(l),r.e&&(r.c={status:0,statusText:r.e}),o=[a?{id:r.id,data:r.c,cfg:i,arguments:u}:r.id],a||(t===n[0]||t===n[2]?u&&o.push(u):(r.evt?o.push(r.evt):o.push(r.c),u&&o.push(u))),o.unshift(f),s.fire.apply(s,o),i.on&&(o[0]=l,s.once(l,i.on[t],i.context||e),s.fire.apply(s,o))},start:function(e,t){this._evt(n[0],e,t)},complete:function(e,t){this._evt(n[1],e,t)},end:function(e,t){this._evt(n[2],e,t),this._destroy(e)},success:function(e,t){this._evt(n[3],e,t),this.end(e,t)},failure:function(e,t){this._evt(n[4],e,t),this.end(e,t)},progress:function(e,t,r){e.evt=t,this._evt(n[5],e,r)},load:function(e,t,r){e.evt=t.target,this._evt(n[1],e,r)},error:function(e,t,r){e.evt=t,this._evt(n[4],e,r)},_retry:function(e,t,n){return this._destroy(e),n.xdr.use="flash",this.send(t,n,e.id)},_concat:function(e,t){return e+=(e.indexOf("?")===-1?"?":"&")+t,e},setHeader:function(e,t){t?this._headers[e]=t:delete this._headers[e]},_setHeaders:function(t,n){n=e.merge(this._headers,n),e.Object.each(n,function(e,r){e!=="disable"&&t.setRequestHeader(r,n[r])})},_startTimeout:function(e,t){var n=this;n._timeout[e.id]=setTimeout(function(){n._abort(e,"timeout")},t)},_clearTimeout:function(e){clearTimeout(this._timeout[e]),delete this._timeout[e]},_result:function(e,t){var n;try{n=e.c.status}catch(r){n=0}n>=200&&n<300||n===304||n===1223?this.success(e,t):this.failure(e,t)},_rS:function(e,t){var n=this;e.c.readyState===4&&(t.timeout&&n._clearTimeout(e.id),setTimeout(function(){n.complete(e,t),n._result(e,t)},0))},_abort:function(e,t){e&&e.c&&(e.e=t,e.c.abort())},send:function(t,n,i){var s,o,u,a,f,c,h=this,p=t,d={};n=n?e.Object(n):{},s=h._create(n,i),o=n.method?n.method.toUpperCase():"GET",f=n.sync,c=n.data,e.Lang.isObject(c)&&!c.nodeType&&!s.upload&&e.QueryString&&e.QueryString.stringify&&(n.data=c=e.QueryString.stringify(c));if(n.form){if(n.form.upload)return h.upload(s,t,n);c=h._serialize(n.form,c)}c||(c="");if(c)switch(o){case"GET":case"HEAD":case"DELETE":p=h._concat(p,c),c="";break;case"POST":case"PUT":n.headers=e.merge({"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},n.headers)}if(s.xdr)return h.xdr(p,s,n);if(s.notify)return s.c.send(s,t,n);!f&&!s.upload&&(s.c.onreadystatechange=function(){h._rS(s,n)});try{s.c.open(o,p,!f,n.username||null,n.password||null),h._setHeaders(s.c,n.headers||{}),h.start(s,n),n.xdr&&n.xdr.credentials&&l&&(s.c.withCredentials=!0),s.c.send(c);if(f){for(u=0,a=r.length;u<a;++u)d[r[u]]=s.c[r[u]];return d.getAllResponseHeaders=function(){return s.c.getAllResponseHeaders()},d.getResponseHeader=function(e){return s.c.getResponseHeader(e)},h.complete(s,n),h._result(s,n),d}}catch(v){if(s.xdr)return h._retry(s,t,n);h.complete(s,n),h._result(s,n)}return n.timeout&&h._startTimeout(s,n.timeout),{id:s.id,abort:function(){return s.c?h._abort(s,"abort"):!1},isInProgress:function(){return s.c?s.c.readyState%4:!1},io:h}}},e.io=function(t,n){var r=e.io._map["io:0"]||new o;return r.send.apply(r,[t,n])},e.io.header=function(t,n){var r=e.io._map["io:0"]||new o;r.setHeader(t,n)},e.IO=o,e.io._map={};var u=i&&i.XMLHttpRequest,a=i&&i.XDomainRequest,f=i&&i.ActiveXObject,l=u&&"withCredentials"in new XMLHttpRequest;e.mix(e.IO,{_default:"xhr",defaultTransport:function(t){if(!t){var n={c:e.IO.transports[e.IO._default](),notify:e.IO._default==="xhr"?!1:!0};return n}e.IO._default=t},transports:{xhr:function(){return u?new XMLHttpRequest:f?new ActiveXObject("Microsoft.XMLHTTP"):null},xdr:function(){return a?new XDomainRequest:null},iframe:function(){return{}},flash:null,nodejs:null},customTransport:function(t){var n={c:e.IO.transports[t]()};return n[t==="xdr"||t==="flash"?"xdr":"notify"]=!0,n}}),e.mix(e.IO.prototype,{notify:function(e,t,n){var r=this;switch(e){case"timeout":case"abort":case"transport error":t.c={status:0,statusText:e},e="failure";default:r[e].apply(r,[t,n])}}})},"3.17.2",{requires:["event-custom-base","querystring-stringify-simple"]});
diff --git a/js/yui3/io-form/io-form-min.js b/js/yui3/io-form/io-form-min.js
new file mode 100644
index 000000000..d8d0a439b
--- /dev/null
+++ b/js/yui3/io-form/io-form-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("io-form",function(e,t){var n=encodeURIComponent;e.IO.stringify=function(t,n){n=n||{};var r=e.IO.prototype._serialize({id:t,useDisabled:n.useDisabled},n.extra&&typeof n.extra=="object"?e.QueryString.stringify(n.extra):n.extra);return r},e.mix(e.IO.prototype,{_serialize:function(t,r){var i=[],s=t.useDisabled||!1,o=0,u=typeof t.id=="string"?t.id:t.id.getAttribute("id"),a,f,l,c,h,p,d,v,m,g;u||(u=e.guid("io:"),t.id.setAttribute("id",u)),f=e.config.doc.getElementById(u);if(!f||!f.elements)return r||"";for(p=0,d=f.elements.length;p<d;++p){a=f.elements[p],h=a.disabled,l=a.name;if(s?l:l&&!h){l=n(l)+"=",c=n(a.value);switch(a.type){case"select-one":a.selectedIndex>-1&&(g=a.options[a.selectedIndex],i[o++]=l+n(g.attributes.value&&g.attributes.value.specified?g.value:g.text));break;case"select-multiple":if(a.selectedIndex>-1)for(v=a.selectedIndex,m=a.options.length;v<m;++v)g=a.options[v],g.selected&&(i[o++]=l+n(g.attributes.value&&g.attributes.value.specified?g.value:g.text));break;case"radio":case"checkbox":a.checked&&(i[o++]=l+c);break;case"file":case undefined:case"reset":case"button":break;case"submit":default:i[o++]=l+c}}}return r&&(i[o++]=r),i.join("&")}},!0)},"3.17.2",{requires:["io-base","node-base"]});
diff --git a/js/yui3/io-nodejs/io-nodejs-min.js b/js/yui3/io-nodejs/io-nodejs-min.js
new file mode 100644
index 000000000..d582fda7d
--- /dev/null
+++ b/js/yui3/io-nodejs/io-nodejs-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("io-nodejs",function(e,t){e.IO.request||(e.IO.request=require("request").defaults({jar:!1}));var n=require("http").STATUS_CODES,r=function(e){var t=[];return Object.keys(e).forEach(function(n){t.push(n+": "+e[n])}),t.join("\n")};e.IO.transports.nodejs=function(){return{send:function(t,i,s){s.notify("start",t,s),s.method=s.method||"GET",s.method=s.method.toUpperCase();var o={method:s.method,uri:i};s.data&&(e.Lang.isString(s.data)&&(o.body=s.data),o.body&&o.method==="GET"&&(o.uri+=(o.uri.indexOf("?")>-1?"&":"?")+o.body,o.body="")),s.headers&&(o.headers=s.headers),s.timeout&&(o.timeout=s.timeout),s.request&&e.mix(o,s.request),e.IO.request(o,function(e,i){if(e){t.c=e,s.notify(e.code==="ETIMEDOUT"?"timeout":"failure",t,s);return}i&&(t.c={status:i.statusCode,statusCode:i.statusCode,statusText:n[i.statusCode],headers:i.headers,responseText:i.body||"",responseXML:null,getResponseHeader:function(e){return this.headers[e]},getAllResponseHeaders:function(){return r(this.headers)}}),s.notify("complete",t,s),s.notify(i&&i.statusCode>=200&&i.statusCode<=299?"success":"failure",t,s)});var u={io:t};return u}}},e.IO.defaultTransport("nodejs")},"3.17.2",{requires:["io-base"]});
diff --git a/js/yui3/io-queue/io-queue-min.js b/js/yui3/io-queue/io-queue-min.js
new file mode 100644
index 000000000..5c822a3f1
--- /dev/null
+++ b/js/yui3/io-queue/io-queue-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("io-queue",function(e,t){function r(e,t){return n.queue.apply(n,[e,t])}var n=e.io._map["io:0"]||new e.IO;e.mix(e.IO.prototype,{_q:new e.Queue,_qActiveId:null,_qInit:!1,_qState:1,_qShift:function(){var e=this,t=e._q.next();e._qActiveId=t.id,e._qState=0,e.send(t.uri,t.cfg,t.id)},queue:function(t,n){var r=this,i={uri:t,cfg:n,id:this._id++};return r._qInit||(e.on("io:complete",function(e,t){r._qNext(e)},r),r._qInit=!0),r._q.add(i),r._qState===1&&r._qShift(),i},_qNext:function(e){var t=this;t._qState=1,t._qActiveId===e&&t._q.size()>0&&t._qShift()},qPromote:function(e){this._q.promote(e)},qRemove:function(e){this._q.remove(e)},qEmpty:function(){this._q=new e.Queue},qStart:function(){var e=this;e._qState=1,e._q.size()>0&&e._qShift()},qStop:function(){this._qState=0},qSize:function(){return this._q.size()}},!0),r.start=function(){n.qStart()},r.stop=function(){n.qStop()},r.promote=function(e){n.qPromote(e)},r.remove=function(e){n.qRemove(e)},r.size=function(){n.qSize()},r.empty=function(){n.qEmpty()},e.io.queue=r},"3.17.2",{requires:["io-base","queue-promote"]});
diff --git a/js/yui3/io-upload-iframe/io-upload-iframe-min.js b/js/yui3/io-upload-iframe/io-upload-iframe-min.js
new file mode 100644
index 000000000..e1fcba324
--- /dev/null
+++ b/js/yui3/io-upload-iframe/io-upload-iframe-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("io-upload-iframe",function(e,t){function u(t,n,r){var i=e.Node.create('<iframe id="io_iframe'+t.id+'" name="io_iframe'+t.id+'" />');i._node.style.position="absolute",i._node.style.top="-1000px",i._node.style.left="-1000px",e.one("body").appendChild(i),e.on("load",function(){r._uploadComplete(t,n)},"#io_iframe"+t.id)}function a(t){e.Event.purgeElement("#io_iframe"+t,!1),e.one("body").removeChild(e.one("#io_iframe"+t))}var n=e.config.win,r=e.config.doc,i=r.documentMode&&r.documentMode>=8,s=decodeURIComponent,o=e.IO.prototype.end;e.mix(e.IO.prototype,{_addData:function(t,n){e.Lang.isObject(n)&&(n=e.QueryString.stringify(n));var i=[],o=n.split("="),u,a;for(u=0,a=o.length-1;u<a;u++)i[u]=r.createElement("input"),i[u].type="hidden",i[u].name=s(o[u].substring(o[u].lastIndexOf("&")+1)),i[u].value=u+1===a?s(o[u+1]):s(o[u+1].substring(0,o[u+1].lastIndexOf("&"))),t.appendChild(i[u]);return i},_removeData:function(e,t){var n,r;for(n=0,r=t.length;n<r;n++)e.removeChild(t[n])},_setAttrs:function(t,n,r){this._originalFormAttrs={action:t.getAttribute("action"),target:t.getAttribute("target")},t.setAttribute("action",r),t.setAttribute("method","POST"),t.setAttribute("target","io_iframe"+n),t.setAttribute(e.UA.ie&&!i?"encoding":"enctype","multipart/form-data")},_resetAttrs:function(t,n){e.Object.each(n,function(e,n){e?t.setAttribute(n,e):t.removeAttribute(n)})},_startUploadTimeout:function(e,t){var r=this;r._timeout[e.id]=n.setTimeout(function(){e.status=0,e.statusText="timeout",r.complete(e,t),r.end(e,t)},t.timeout)},_clearUploadTimeout:function(e){var t=this;n.clearTimeout(t._timeout[e]),delete t._timeout[e]},_uploadComplete:function(t,r){var i=this,s=e.one("#io_iframe"+t.id).get("contentWindow.document"),o=s.one("body"),u;r.timeout&&i._clearUploadTimeout(t.id);try{o?(u=o.one("pre:first-child"),t.c.responseText=u?u.get("text"):o.get("text")):t.c.responseXML=s._node}catch(f){t.e="upload failure"}i.complete(t,r),i.end(t,r),n.setTimeout(function(){a(t.id)},0)},_upload:function(t,n,i){var s=this,o=typeof i.form.id=="string"?r.getElementById(i.form.id):i.form.id,u;return s._setAttrs(o,t.id,n),i.data&&(u=s._addData(o,i.data)),i.timeout&&s._startUploadTimeout(t,i),o.submit(),s.start(t,i),i.data&&s._removeData(o,u),{id:t.id,abort:function(){t.status=0,t.statusText="abort";if(!e.one("#io_iframe"+t.id))return!1;a(t.id),s.complete(t,i),s.end(t,i)},isInProgress:function(){return e.one("#io_iframe"+t.id)?!0:!1},io:s}},upload:function(e,t,n){return u(e,n,this),this._upload(e,t,n)},end:function(e,t){var n,i;return t&&(n=t.form,n&&n.upload&&(i=this,n=typeof n.id=="string"?r.getElementById(n.id):n.id,n&&i._resetAttrs(n,this._originalFormAttrs))),o.call(this,e,t)}},!0)},"3.17.2",{requires:["io-base","node-base"]});
diff --git a/js/yui3/io-xdr/io-xdr-min.js b/js/yui3/io-xdr/io-xdr-min.js
new file mode 100644
index 000000000..f80d05637
--- /dev/null
+++ b/js/yui3/io-xdr/io-xdr-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("io-xdr",function(e,t){function a(e,t,n){var r='<object id="io_swf" type="application/x-shockwave-flash" data="'+e+'" width="0" height="0">'+'<param name="movie" value="'+e+'">'+'<param name="FlashVars" value="yid='+t+"&uid="+n+'">'+'<param name="allowScriptAccess" value="always">'+"</object>",i=s.createElement("div");s.body.appendChild(i),i.innerHTML=r}function f(t,n,r){return n==="flash"&&(t.c.responseText=decodeURI(t.c.responseText)),r==="xml"&&(t.c.responseXML=e.DataType.XML.parse(t.c.responseText)),t}function l(e,t){return e.c.abort(e.id,t)}function c(e){return u?i[e.id]!==4:e.c.isInProgress(e.id)}var n=e.publish("io:xdrReady",{fireOnce:!0}),r={},i={},s=e.config.doc,o=e.config.win,u=o&&o.XDomainRequest;e.mix(e.IO.prototype,{_transport:{},_ieEvt:function(e,t){var n=this,r=e.id,s="timeout";e.c.onprogress=function(){i[r]=3},e.c.onload=function(){i[r]=4,n.xdrResponse("success",e,t)},e.c.onerror=function(){i[r]=4,n.xdrResponse("failure",e,t)},e.c.ontimeout=function(){i[r]=4,n.xdrResponse(s,e,t)},e.c[s]=t[s]||0},xdr:function(t,n,i){var s=this;return i.xdr.use==="flash"?(r[n.id]=i,o.setTimeout(function(){try{n.c.send(t,{id:n.id,uid:n.uid,method:i.method,data:i.data,headers:i.headers})}catch(e){s.xdrResponse("transport error",n,i),delete r[n.id]}},e.io.xdr.delay)):u?(s._ieEvt(n,i),n.c.open(i.method||"GET",t),setTimeout(function(){n.c.send(i.data)},0)):n.c.send(t,n,i),{id:n.id,abort:function(){return n.c?l(n,i):!1},isInProgress:function(){return n.c?c(n.id):!1},io:s}},xdrResponse:function(e,t,n){n=r[t.id]?r[t.id]:n;var s=this,o=u?i:r,a=n.xdr.use,l=n.xdr.dataType;switch(e){case"start":s.start(t,n);break;case"success":s.success(f(t,a,l),n),delete o[t.id];break;case"timeout":case"abort":case"transport error":t.c={status:0,statusText:e};case"failure":s.failure(f(t,a,l),n),delete o[t.id]}},_xdrReady:function(t,r){e.fire(n,t,r)},transport:function(t){t.id==="flash"&&(a(e.UA.ie?t.src+"?d="+(new Date).valueOf().toString():t.src,e.id,t.uid),e.IO.transports.flash=function(){return s.getElementById("io_swf")})}}),e.io.xdrReady=function(t,n){var r=e.io._map[n];e.io.xdr.delay=0,r._xdrReady.apply(r,[t,n])},e.io.xdrResponse=function(t,n,r){var i=e.io._map[n.uid];i.xdrResponse.apply(i,[t,n,r])},e.io.transport=function(t){var n=e.io._map["io:0"]||new e.IO;t.uid=n._uid,n.transport.apply(n,[t])},e.io.xdr={delay:100}},"3.17.2",{requires:["io-base","datatype-xml-parse"]});
diff --git a/js/yui3/io-xdr/io.swf b/js/yui3/io-xdr/io.swf
new file mode 100644
index 000000000..dc1e59ebd
--- /dev/null
+++ b/js/yui3/io-xdr/io.swf
Binary files differ
diff --git a/js/yui3/json-parse-shim/json-parse-shim-min.js b/js/yui3/json-parse-shim/json-parse-shim-min.js
new file mode 100644
index 000000000..b4b3c1217
--- /dev/null
+++ b/js/yui3/json-parse-shim/json-parse-shim-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("json-parse-shim",function(Y,NAME){var _UNICODE_EXCEPTIONS=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,_ESCAPES=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,_VALUES=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,_BRACKETS=/(?:^|:|,)(?:\s*\[)+/g,_UNSAFE=/[^\],:{}\s]/,_escapeException=function(e){return"\\u"+("0000"+(+e.charCodeAt(0)).toString(16)).slice(-4)},_revive=function(e,t){var n=function(e,r){var i,s,o=e[r];if(o&&typeof o=="object")for(i in o)o.hasOwnProperty(i)&&(s=n(o,i),s===undefined?delete o[i]:o[i]=s);return t.call(e,r,o)};return typeof t=="function"?n({"":e},""):e};Y.JSON.parse=function(s,reviver){typeof s!="string"&&(s+=""),s=s.replace(_UNICODE_EXCEPTIONS,_escapeException);if(!_UNSAFE.test(s.replace(_ESCAPES,"@").replace(_VALUES,"]").replace(_BRACKETS,"")))return _revive(eval("("+s+")"),reviver);throw new SyntaxError("JSON.parse")},Y.JSON.parse.isShim=!0},"3.17.2",{requires:["json-parse"]});
diff --git a/js/yui3/json-parse/json-parse-min.js b/js/yui3/json-parse/json-parse-min.js
new file mode 100644
index 000000000..066f0584a
--- /dev/null
+++ b/js/yui3/json-parse/json-parse-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("json-parse",function(e,t){var n=e.config.global.JSON;e.namespace("JSON").parse=function(e,t,r){return n.parse(typeof e=="string"?e:e+"",t,r)}},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/json-stringify-shim/json-stringify-shim-min.js b/js/yui3/json-stringify-shim/json-stringify-shim-min.js
new file mode 100644
index 000000000..989adf874
--- /dev/null
+++ b/js/yui3/json-stringify-shim/json-stringify-shim-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("json-stringify-shim",function(e,t){function _(e){var t=typeof e;return d[t]||d[o.call(e)]||(t===a?e?a:f:u)}function D(e){return A[e]||(A[e]="\\u"+("0000"+(+e.charCodeAt(0)).toString(16)).slice(-4),O[e]=0),++O[e]===M&&(k.push([new RegExp(e,"g"),A[e]]),L=k.length),A[e]}function P(e){var t,n;for(t=0;t<L;t++)n=k[t],e=e.replace(n[0],n[1]);return N+e.replace(C,D)+N}function H(e,t){return e.replace(/^/gm,t)}var n=e.Lang,r=n.isFunction,i=n.isObject,s=n.isArray,o=Object.prototype.toString,u="undefined",a="object",f="null",l="string",c="number",h="boolean",p="date",d={"undefined":u,string:l,"[object String]":l,number:c,"[object Number]":c,"boolean":h,"[object Boolean]":h,"[object Date]":p,"[object RegExp]":a},v="",m="{",g="}",y="[",b="]",w=",",E=",\n",S="\n",x=":",T=": ",N='"',C=/[\x00-\x07\x0b\x0e-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,k=[[/\\/g,"\\\\"],[/\"/g,'\\"'],[/\x08/g,"\\b"],[/\x09/g,"\\t"],[/\x0a/g,"\\n"],[/\x0c/g,"\\f"],[/\x0d/g,"\\r"]],L=k.length,A={},O,M;e.JSON.stringify=function(n,u,d){function j(e,t){var n=e[t],o=_(n),C=[],A=d?T:x,O,M,D,B,F;i(n)&&r(n.toJSON)?n=n.toJSON(t):o===p&&(n=k(n)),r(N)&&(n=N.call(e,t,n)),n!==e[t]&&(o=_(n));switch(o){case p:case a:break;case l:return P(n);case c:return isFinite(n)?n+v:f;case h:return n+v;case f:return f;default:return undefined}for(M=L.length-1;M>=0;--M)if(L[M]===n)throw new Error("JSON.stringify. Cyclical reference");O=s(n),L.push(n);if(O)for(M=n.length-1;M>=0;--M)C[M]=j(n,M)||f;else{D=u||n,M=0;for(B in D)D.hasOwnProperty(B)&&(F=j(n,B),F&&(C[M++]=P(B)+A+F))}return L.pop(),d&&C.length?O?y+S+H(C.join(E),d)+S+b:m+S+H(C.join(E),d)+S+g:O?y+C.join(w)+b:m+C.join(w)+g}if(n===undefined)return undefined;var N=r(u)?u:null,C=o.call(d).match(/String|Number/)||[],k=e.JSON.dateToString,L=[],A,D,B;O={},M=e.JSON.charCacheThreshold;if(N||!s(u))u=undefined;if(u){A={};for(D=0,B=u.length;D<B;++D)A[u[D]]=!0;u=A}return d=C[0]==="Number"?(new Array(Math.min(Math.max(0,d),10)+1)).join(" "):(d||v).slice(0,10),j({"":n},"")},e.JSON.stringify.isShim=!0},"3.17.2",{requires:["json-stringify"]});
diff --git a/js/yui3/json-stringify/json-stringify-min.js b/js/yui3/json-stringify/json-stringify-min.js
new file mode 100644
index 000000000..584ea6d9b
--- /dev/null
+++ b/js/yui3/json-stringify/json-stringify-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("json-stringify",function(e,t){var n=":",r=e.config.global.JSON;e.mix(e.namespace("JSON"),{dateToString:function(e){function t(e){return e<10?"0"+e:e}return e.getUTCFullYear()+"-"+t(e.getUTCMonth()+1)+"-"+t(e.getUTCDate())+"T"+t(e.getUTCHours())+n+t(e.getUTCMinutes())+n+t(e.getUTCSeconds())+"Z"},stringify:function(){return r.stringify.apply(r,arguments)},charCacheThreshold:100})},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/jsonp-url/jsonp-url-min.js b/js/yui3/jsonp-url/jsonp-url-min.js
new file mode 100644
index 000000000..387752b01
--- /dev/null
+++ b/js/yui3/jsonp-url/jsonp-url-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("jsonp-url",function(e,t){var n=e.JSONPRequest,r=e.Object.getValue,i=function(){};e.mix(n.prototype,{_pattern:/\bcallback=(.*?)(?=&|$)/i,_template:"callback={callback}",_defaultCallback:function(t){var n=t.match(this._pattern),s=[],o=0,u,a,f;if(n){u=n[1].replace(/\[(['"])(.*?)\1\]/g,function(e,t,n){return s[o]=n,".@"+o++}).replace(/\[(\d+)\]/g,function(e,t){return s[o]=parseInt(t,10)|0,".@"+o++}).replace(/^\./,"");if(!/[^\w\.\$@]/.test(u)){a=u.split(".");for(o=a.length-1;o>=0;--o)a[o].charAt(0)==="@"&&(a[o]=s[parseInt(a[o].substr(1),10)]);f=r(e.config.win,a)||r(e,a)||r(e,a.slice(1))}}return f||i},_format:function(e,t){var n=/\{callback\}/,r,i;return n.test(e)?e.replace(n,t):(r=this._template.replace(n,t),this._pattern.test(e)?e.replace(this._pattern,r):(i=e.slice(-1),i!=="&"&&i!=="?"&&(e+=e.indexOf("?")>-1?"&":"?"),e+r))}},!0)},"3.17.2",{requires:["jsonp"]});
diff --git a/js/yui3/jsonp/jsonp-min.js b/js/yui3/jsonp/jsonp-min.js
new file mode 100644
index 000000000..bb36ae2ec
--- /dev/null
+++ b/js/yui3/jsonp/jsonp-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("jsonp",function(e,t){function r(){this._init.apply(this,arguments)}var n=e.Lang.isFunction;r.prototype={_init:function(t,r){this.url=t,this._requests={},this._timeouts={},r=n(r)?{on:{success:r}}:r||{};var i=r.on||{};i.success||(i.success=this._defaultCallback(t,r)),this._config=e.merge({context:this,args:[],format:this._format,allowCache:!1},r,{on:i})},_defaultCallback:function(){},send:function(){function u(e,r){return n(e)?function(n){var o=!0,u="_requests";r?(++t._timeouts[s],--t._requests[s]):(t._requests[s]||(o=!1,u="_timeouts"),--t[u][s]),!t._requests[s]&&!t._timeouts[s]&&delete YUI.Env.JSONP[s],o&&e.apply(i.context,[n].concat(i.args))}:null}var t=this,r=e.Array(arguments,0,!0),i=t._config,s=t._proxy||e.guid(),o;return i.allowCache&&(t._proxy=s),t._requests[s]===undefined&&(t._requests[s]=0),t._timeouts[s]===undefined&&(t._timeouts[s]=0),t._requests[s]++,r.unshift(t.url,"YUI.Env.JSONP."+s),o=i.format.apply(t,r),i.on.success?(YUI.Env.JSONP[s]=u(i.on.success),e.Get.js(o,{onFailure:u(i.on.failure),onTimeout:u(i.on.timeout,!0),timeout:i.timeout,charset:i.charset,attributes:i.attributes,async:i.async}).execute(),t):t},_format:function(e,t){return e.replace(/\{callback\}/,t)}},e.JSONPRequest=r,e.jsonp=function(t,n){var r=new e.JSONPRequest(t,n);return r.send.apply(r,e.Array(arguments,2,!0))},YUI.Env.JSONP||(YUI.Env.JSONP={})},"3.17.2",{requires:["get","oop"]});
diff --git a/js/yui3/lazy-model-list/lazy-model-list-min.js b/js/yui3/lazy-model-list/lazy-model-list-min.js
new file mode 100644
index 000000000..71d9f25b3
--- /dev/null
+++ b/js/yui3/lazy-model-list/lazy-model-list-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("lazy-model-list",function(e,t){var n=e.Attribute.prototype,r=YUI.namespace("Env.Model"),i=e.Lang,s=e.Array,o="add",u="error",a="reset";e.LazyModelList=e.Base.create("lazyModelList",e.ModelList,[],{initializer:function(){this.after("*:change",this._afterModelChange)},free:function(e){var t;return e?(t=i.isNumber(e)?e:this.indexOf(e),t>=0&&delete this._models[t]):this._models=[],this},get:function(e){return this.attrAdded(e)?n.get.apply(this,arguments):s.map(this._items,function(t){return t[e]})},getAsHTML:function(t){return this.attrAdded(t)?e.Escape.html(n.get.apply(this,arguments)):s.map(this._items,function(n){return e.Escape.html(n[t])})},getAsURL:function(e){return this.attrAdded(e)?encodeURIComponent(n.get.apply(this,arguments)):s.map(this._items,function(t){return encodeURIComponent(t[e])})},indexOf:function(e){return s.indexOf(e&&e._isYUIModel?this._models:this._items,e)},reset:function(t,n){t||(t=[]),n||(n={});var r=e.merge({src:"reset"},n);return t=t._isYUIModelList?t.map(this._modelToObject):s.map(t,this._modelToObject),r.models=t,n.silent?this._defResetFn(r):(this.comparator&&t.sort(e.bind(this._sort,this)),this.fire(a,r)),this},revive:function(e){var t,n,r;if(e||e===0)return this._revive(i.isNumber(e)?e:this.indexOf(e));r=[];for(t=0,n=this._items.length;t<n;t++)r.push(this._revive(t));return r},toJSON:function(){return this.toArray()},_add:function(t,n){var r;n||(n={}),t=this._modelToObject(t),"clientId"in t||(t.clientId=this._generateClientId());if(this._isInList(t)){this.fire(u,{error:"Model is already in the list.",model:t,src:"add"});return}return r=e.merge(n,{index:"index"in n?n.index:this._findIndex(t),model:t}),n.silent?this._defAddFn(r):this.fire(o,r),t},_clear:function(){s.each(this._models,this._detachList,this),this._clientIdMap={},this._idMap={},this._items=[],this._models=[]},_generateClientId:function(){return r.lastId||(r.lastId=0),this.model.NAME+"_"+(r.lastId+=1)},_isInList:function(e){return!!("clientId"in e&&this._clientIdMap[e.clientId]||"id"in e&&this._idMap[e.id])},_modelToObject:function(e){return e._isYUIModel&&(e=e.getAttrs(),delete e.destroyed,delete e.initialized),e},_remove:function(t,n){return t._isYUIModel&&(t=this.indexOf(t)),e.ModelList.prototype._remove.call(this,t,n)},_revive:function(e){var t,n;return e<0?null:(t=this._items[e],t?(n=this._models[e],n||(n=new this.model(t),n._set("clientId",t.clientId),this._attachList(n),this._models[e]=n),n):null)},_afterModelChange:function(e){var t=e.changed,n=this._clientIdMap[e.target.get("clientId")],r;if(n)for(r in t)t.hasOwnProperty(r)&&(n[r]=t[r].newVal)},_defAddFn:function(e){var t=e.model;this._clientIdMap[t.clientId]=t,i.isValue(t.id)&&(this._idMap[t.id]=t),this._items.splice(e.index,0,t)},_defRemoveFn:function(e){var t=e.index,n=e.model,r=this._models[t];delete this._clientIdMap[n.clientId],"id"in n&&delete this._idMap[n.id],r&&this._detachList(r),this._items.splice(t,1),this._models.splice(t,1)}})},"3.17.2",{requires:["model-list"]});
diff --git a/js/yui3/loader-base/loader-base-min.js b/js/yui3/loader-base/loader-base-min.js
new file mode 100644
index 000000000..8475a98eb
--- /dev/null
+++ b/js/yui3/loader-base/loader-base-min.js
@@ -0,0 +1,11 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("loader-base",function(e,t){(function(){var t=e.version,n="/build/",r=t+"/",i=e.Env.base,s="gallery-2014.05.29-15-46",o="2in3",u="4",a="2.9.0",f=i+"combo?",l={version:t,root:r,base:e.Env.base,comboBase:f,skin:{defaultSkin:"sam",base:"assets/skins/",path:"skin.css",after:["cssreset","cssfonts","cssgrids","cssbase","cssreset-context","cssfonts-context"]},groups:{},patterns:{}},c=l.groups,h=function(e,t,r){var s=o+"."+(e||u)+"/"+(t||a)+n,l=r&&r.base?r.base:i,h=r&&r.comboBase?r.comboBase:f;c.yui2.base=l+s,c.yui2.root=s,c.yui2.comboBase=h},p=function(e,t){var r=(e||s)+n,o=t&&t.base?t.base:i,u=t&&t.comboBase?t.comboBase:f;c.gallery.base=o+r,c.gallery.root=r,c.gallery.comboBase=u};c[t]={},c.gallery={ext:!1,combine:!0,comboBase:f,update:p,patterns:{"gallery-":{},"lang/gallery-":{},"gallerycss-":{type:"css"}}},c.yui2={combine:!0,ext:!1,comboBase:f,update:h,patterns:{"yui2-":{configFn:function(e){/-skin|reset|fonts|grids|base/.test(e.name)&&(e.type="css",e.path=e.path.replace(/\.js/,".css"),e.path=e.path.replace(/\/yui2-skin/,"/assets/skins/sam/yui2-skin"))}}}},p(),h(),YUI.Env[t]&&e.mix(l,YUI.Env[t],!1,["modules","groups","skin"],0,!0),YUI.Env[t]=l})();var n={},r=[],i=1024,s=YUI.Env,o=s._loaded,u="css",a="js",f="intl",l="sam",c=e.version,h="",p=e.Object,d=p.each,v=e.Array,m=s._loaderQueue,g=s[c],y="skin-",b=e.Lang,w=s.mods,E,S=function(e,t,n,r){var i=e+"/"+t;return r||(i+="-min"),i+="."+(n||u),i};YUI.Env._cssLoaded||(YUI.Env._cssLoaded={}),e.Env.meta=g,e.Loader=function(t){var n=this;t=t||{},E=g.md5,n.context=e,t.doBeforeLoader&&t.doBeforeLoader.apply(n,arguments),n.base=e.Env.meta.base+e.Env.meta.root,n.comboBase=e.Env.meta.comboBase,n.combine=t.base&&t.base.indexOf(n.comboBase.substr(0,20))>-1,n.comboSep="&",n.maxURLLength=i,n.ignoreRegistered=t.ignoreRegistered,n.root=e.Env.meta.root,n.timeout=0,n.forceMap={},n.allowRollup=!1,n.filters={},n.required={},n.patterns={},n.moduleInfo={},n.groups=e.merge(e.Env.meta.groups),n.skin=e.merge(e.Env.meta.skin),n.conditions={},n.config=t,n._internal=!0,n._populateConditionsCache(),n.loaded=o[c],n.async=!0,n._inspectPage(),n._internal=!1,n._config(t),n.forceMap=n.force?e.Array.hash(n.force):{},n.testresults=null,e.config.tests&&(n.testresults=e.config.tests),n.sorted=[],n.dirty=!0,n.inserted={},n.skipped={},n.tested={},n.ignoreRegistered&&n._resetModules()},e.Loader.prototype={getModuleInfo:function(t){var n=this.moduleInfo[t],r,i,o,a;return n?n:(r=g.modules,i=s._renderedMods,o=this._internal,i&&i.hasOwnProperty(t)&&!this.ignoreRegistered?this.moduleInfo[t]=e.merge(i[t]):r.hasOwnProperty(t)&&(this._internal=!0,a=this.addModule(r[t],t),a&&a.type===u&&this.isCSSLoaded(a.name,!0)&&(this.loaded[a.name]=!0),this._internal=o),this.moduleInfo[t])},_expandAliases:function(t){var n=[],r=YUI.Env.aliases,i,s;t=e.Array(t);for(i=0;i<t.length;i+=1)s=t[i],n.push.apply(n,r[s]?r[s]:[s]);return n},_populateConditionsCache:function(){var t=g.modules,n=s._conditions,r,i,o,u;if(n&&!this.ignoreRegistered)for(r in n)n.hasOwnProperty(r)&&(this.conditions[r]=e.merge(n[r]));else{for(r in t)if(t.hasOwnProperty(r)&&t[r].condition){o=this._expandAliases(t[r].condition.trigger);for(i=0;i<o.length;i+=1)u=o[i],this.conditions[u]=this.conditions[u]||{},this.conditions[u][t[r].name||r]=t[r].condition}s._conditions=this.conditions}},_resetModules:function(){var e=this,t,n,r,i,s;for(t in e.moduleInfo)if(e.moduleInfo.hasOwnProperty(t)&&e.moduleInfo[t]){r=e.moduleInfo[t],i=r.name,s=YUI.Env.mods[i]?YUI.Env.mods[i].details:null,s&&(e.moduleInfo[i]._reset=!0,e.moduleInfo[i].requires=s.requires||[],e.moduleInfo[i].optional=s.optional||[],e.moduleInfo[i].supersedes=s.supercedes||[]);if(r.defaults)for(n in r.defaults)r.defaults.hasOwnProperty(n)&&r[n]&&(r[n]=r.defaults[n]);r.langCache=undefined,r.skinCache=undefined,r.skinnable&&e._addSkin(e.skin.defaultSkin,r.name)}},REGEX_CSS:/\.css(?:[?;].*)?$/i,FILTER_DEFS:{RAW:{searchExp:"-min\\.js",replaceStr:".js"},DEBUG:{searchExp:"-min\\.js",replaceStr:"-debug.js"},COVERAGE:{searchExp:"-min\\.js",replaceStr:"-coverage.js"}},_inspectPage:function(){var e=this,t,n,r,i,s;for(s in w)w.hasOwnProperty(s)&&(t=w[s],t.details&&(n=e.getModuleInfo(t.name),r=t.details.requires,i=n&&n.requires,n?!n._inspected&&r&&i.length!==r.length&&delete n.expanded:n=e.addModule(t.details,s),n._inspected=!0))},_requires:function(e,t){var n,r,i,s,o=this.getModuleInfo(e),a=this.getModuleInfo(t);if(!o||!a)return!1;r=o.expanded_map,i=o.after_map;if(i&&t in i)return!0;i=a.after_map;if(i&&e in i)return!1;s=a.supersedes;if(s)for(n=0;n<s.length;n++)if(this._requires(e,s[n]))return!0;s=o.supersedes;if(s)for(n=0;n<s.length;n++)if(this._requires(t,s[n]))return!1;return r&&t in r?!0:o.ext&&o.type===u&&!a.ext&&a.type===u?!0:!1},_config:function(t){var n,r,i,s,o,u,a,f=this,l=[],c,h;if(t)for(n in t)if(t.hasOwnProperty(n)){i=t[n];if(n==="require")f.require(i);else if(n==="skin")typeof i=="string"&&(f.skin.defaultSkin=t.skin,i={defaultSkin:i}),e.mix(f.skin,i,!0);else if(n==="groups"){for(r in i)if(i.hasOwnProperty(r)){a=r,u=i[r],f.addGroup(u,a);if(u.aliases)for(s in u.aliases)u.aliases.hasOwnProperty(s)&&f.addAlias(u.aliases[s],s)}}else if(n==="modules")for(r in i)i.hasOwnProperty(r)&&f.addModule(i[r],r);else if(n==="aliases")for(r in i)i.hasOwnProperty(r)&&f.addAlias(i[r],r);else n==="gallery"?this.groups.gallery.update&&this.groups.gallery.update(i,t):n==="yui2"||n==="2in3"?this.groups.yui2.update&&this.groups.yui2.update(t["2in3"],t.yui2,t):f[n]=i}o=f.filter,b.isString(o)&&(o=o.toUpperCase(),f.filterName=o,f.filter=f.FILTER_DEFS[o],o==="DEBUG"&&f.require("yui-log","dump"));if(f.filterName&&f.coverage&&f.filterName==="COVERAGE"&&b.isArray(f.coverage)&&f.coverage.length){for(n=0;n<f.coverage.length;n++)c=f.coverage[n],h=f.getModuleInfo(c),h&&h.use?l=l.concat(h.use):l.push(c);f.filters=f.filters||{},e.Array.each(l,function(e){f.filters[e]=f.FILTER_DEFS.COVERAGE}),f.filterName="RAW",f.filter=f.FILTER_DEFS[f.filterName]}},formatSkin:function(e,t){var n=y+e;return t&&(n=n+"-"+t),n},_addSkin:function(
+e,t,n){var r,i,s,o=this.skin,u=t&&this.getModuleInfo(t),a=u&&u.ext;return t&&(i=this.formatSkin(e,t),this.getModuleInfo(i)||(r=u.pkg||t,s={skin:!0,name:i,group:u.group,type:"css",after:o.after,path:(n||r)+"/"+o.base+e+"/"+t+".css",ext:a},u.base&&(s.base=u.base),u.configFn&&(s.configFn=u.configFn),this.addModule(s,i))),i},addAlias:function(e,t){YUI.Env.aliases[t]=e,this.addModule({name:t,use:e})},addGroup:function(e,t){var n=e.modules,r=this,i,s;t=t||e.name,e.name=t,r.groups[t]=e;if(e.patterns)for(i in e.patterns)e.patterns.hasOwnProperty(i)&&(e.patterns[i].group=t,r.patterns[i]=e.patterns[i]);if(n)for(i in n)n.hasOwnProperty(i)&&(s=n[i],typeof s=="string"&&(s={name:i,fullpath:s}),s.group=t,r.addModule(s,i))},addModule:function(t,n){n=n||t.name,typeof t=="string"&&(t={name:n,fullpath:t});var r,i,o,f,l,c,p,d,m,g,y,b,w,E,x,T,N,C,k,L,A,O,M=this.moduleInfo[n],_=this.conditions,D;M&&M.temp&&(t=e.merge(M,t)),t.name=n;if(!t||!t.name)return null;t.type||(t.type=a,O=t.path||t.fullpath,O&&this.REGEX_CSS.test(O)&&(t.type=u)),!t.path&&!t.fullpath&&(t.path=S(n,n,t.type)),t.supersedes=t.supersedes||t.use,t.ext="ext"in t?t.ext:this._internal?!1:!0,r=t.submodules,this.moduleInfo[n]=t,t.requires=t.requires||[];if(this.requires)for(i=0;i<this.requires.length;i++)t.requires.push(this.requires[i]);if(t.group&&this.groups&&this.groups[t.group]){A=this.groups[t.group];if(A.requires)for(i=0;i<A.requires.length;i++)t.requires.push(A.requires[i])}t.defaults||(t.defaults={requires:t.requires?[].concat(t.requires):null,supersedes:t.supersedes?[].concat(t.supersedes):null,optional:t.optional?[].concat(t.optional):null}),t.skinnable&&t.ext&&t.temp&&(k=this._addSkin(this.skin.defaultSkin,n),t.requires.unshift(k)),t.requires.length&&(t.requires=this.filterRequires(t.requires)||[]);if(!t.langPack&&t.lang){y=v(t.lang);for(g=0;g<y.length;g++)T=y[g],b=this.getLangPackName(T,n),p=this.getModuleInfo(b),p||(p=this._addLangPack(T,t,b))}if(r){l=t.supersedes||[],o=0;for(i in r)if(r.hasOwnProperty(i)){c=r[i],c.path=c.path||S(n,i,t.type),c.pkg=n,c.group=t.group,c.supersedes&&(l=l.concat(c.supersedes)),p=this.addModule(c,i),l.push(i);if(p.skinnable){t.skinnable=!0,C=this.skin.overrides;if(C&&C[i])for(g=0;g<C[i].length;g++)k=this._addSkin(C[i][g],i,n),l.push(k);k=this._addSkin(this.skin.defaultSkin,i,n),l.push(k)}if(c.lang&&c.lang.length){y=v(c.lang);for(g=0;g<y.length;g++)T=y[g],b=this.getLangPackName(T,n),w=this.getLangPackName(T,i),p=this.getModuleInfo(b),p||(p=this._addLangPack(T,t,b)),E=E||v.hash(p.supersedes),w in E||p.supersedes.push(w),t.lang=t.lang||[],x=x||v.hash(t.lang),T in x||t.lang.push(T),b=this.getLangPackName(h,n),w=this.getLangPackName(h,i),p=this.getModuleInfo(b),p||(p=this._addLangPack(T,t,b)),w in E||p.supersedes.push(w)}o++}t.supersedes=v.dedupe(l),this.allowRollup&&(t.rollup=o<4?o:Math.min(o-1,4))}d=t.plugins;if(d)for(i in d)d.hasOwnProperty(i)&&(m=d[i],m.pkg=n,m.path=m.path||S(n,i,t.type),m.requires=m.requires||[],m.group=t.group,this.addModule(m,i),t.skinnable&&this._addSkin(this.skin.defaultSkin,i,n));if(t.condition){f=this._expandAliases(t.condition.trigger);for(i=0;i<f.length;i++)D=f[i],L=t.condition.when,_[D]=_[D]||{},_[D][n]=t.condition,L&&L!=="after"?L==="instead"&&(t.supersedes=t.supersedes||[],t.supersedes.push(D)):(t.after=t.after||[],t.after.push(D))}return t.supersedes&&(t.supersedes=this.filterRequires(t.supersedes)),t.after&&(t.after=this.filterRequires(t.after),t.after_map=v.hash(t.after)),t.configFn&&(N=t.configFn(t),N===!1&&(delete this.moduleInfo[n],delete s._renderedMods[n],t=null)),t&&(s._renderedMods||(s._renderedMods={}),s._renderedMods[n]=e.mix(s._renderedMods[n]||{},t),s._conditions=_),t},require:function(t){var n=typeof t=="string"?v(arguments):t;this.dirty=!0,this.required=e.merge(this.required,v.hash(this.filterRequires(n))),this._explodeRollups()},_explodeRollups:function(){var e=this,t,n,r,i,s,o,u,a=e.required;if(!e.allowRollup){for(r in a)if(a.hasOwnProperty(r)){t=e.getModule(r);if(t&&t.use){o=t.use.length;for(i=0;i<o;i++){n=e.getModule(t.use[i]);if(n&&n.use){u=n.use.length;for(s=0;s<u;s++)a[n.use[s]]=!0}else a[t.use[i]]=!0}}}e.required=a}},filterRequires:function(t){if(t){e.Lang.isArray(t)||(t=[t]),t=e.Array(t);var n=[],r,i,s,o;for(r=0;r<t.length;r++){i=this.getModule(t[r]);if(i&&i.use)for(s=0;s<i.use.length;s++)o=this.getModule(i.use[s]),o&&o.use&&o.name!==i.name?n=e.Array.dedupe([].concat(n,this.filterRequires(o.use))):n.push(i.use[s]);else n.push(t[r])}t=n}return t},_canBeAttached:function(t){return t=this.getModule(t),t&&t.test?(t.hasOwnProperty("_testResult")||(t._testResult=t.test(e)),t._testResult):!0},getRequires:function(t){if(!t)return r;if(t._parsed)return t.expanded||r;var n,i,s,o,u,a,l,c=this.testresults,m=t.name,g,y=w[m]&&w[m].details,b=t.optionalRequires,E,S,x,T,N,C,k,L,A,O,M=t.lang||t.intl,_=e.Features&&e.Features.tests.load,D,P;t.temp&&y&&(N=t,t=this.addModule(y,m),t.group=N.group,t.pkg=N.pkg,delete t.expanded),P=!!this.lang&&t.langCache!==this.lang||t.skinCache!==this.skin.defaultSkin;if(t.expanded&&!P)return t.expanded;if(b)for(n=0,o=b.length;n<o;n++)this._canBeAttached(b[n])&&t.requires.push(b[n]);E=[],D={},T=this.filterRequires(t.requires),t.lang&&(E.unshift("intl"),T.unshift("intl"),M=!0),C=this.filterRequires(t.optional),t._parsed=!0,t.langCache=this.lang,t.skinCache=this.skin.defaultSkin;for(n=0;n<T.length;n++)if(!D[T[n]]){E.push(T[n]),D[T[n]]=!0,i=this.getModule(T[n]);if(i){u=this.getRequires(i),M=M||i.expanded_map&&f in i.expanded_map;for(s=0;s<u.length;s++)E.push(u[s])}}T=this.filterRequires(t.supersedes);if(T)for(n=0;n<T.length;n++)if(!D[T[n]]){t.submodules&&E.push(T[n]),D[T[n]]=!0,i=this.getModule(T[n]);if(i){u=this.getRequires(i),M=M||i.expanded_map&&f in i.expanded_map;for(s=0;s<u.length;s++)E.push(u[s])}}if(C&&this.loadOptional)for(n=0;n<C.length;n++)if(!D[C[n]]){E.push(C[n]),D[C[n]]=!0,i=this.getModuleInfo(C[n]);if(i){u=this.getRequires(i),M=M||i.expanded_map&&f in i.expanded_map;for(s=0;s<u.length;s++)E.push(u[s])}}g=this.conditions[m];if(g){t._parsed=!1
+;if(c&&_)d(c,function(e,t){var n=_[t].name;!D[n]&&_[t].trigger===m&&e&&_[t]&&(D[n]=!0,E.push(n))});else for(n in g)if(g.hasOwnProperty(n)&&!D[n]){x=g[n],S=x&&(!x.ua&&!x.test||x.ua&&e.UA[x.ua]||x.test&&x.test(e,T));if(S){D[n]=!0,E.push(n),i=this.getModule(n);if(i){u=this.getRequires(i);for(s=0;s<u.length;s++)E.push(u[s])}}}}if(t.skinnable){L=this.skin.overrides;for(n in YUI.Env.aliases)YUI.Env.aliases.hasOwnProperty(n)&&e.Array.indexOf(YUI.Env.aliases[n],m)>-1&&(A=n);if(L&&(L[m]||A&&L[A])){O=m,L[A]&&(O=A);for(n=0;n<L[O].length;n++)k=this._addSkin(L[O][n],m),this.isCSSLoaded(k,this._boot)||E.push(k)}else k=this._addSkin(this.skin.defaultSkin,m),this.isCSSLoaded(k,this._boot)||E.push(k)}return t._parsed=!1,M&&(t.lang&&!t.langPack&&e.Intl&&(l=e.Intl.lookupBestLang(this.lang||h,t.lang),a=this.getLangPackName(l,m),a&&E.unshift(a)),E.unshift(f)),t.expanded_map=v.hash(E),t.expanded=p.keys(t.expanded_map),t.expanded},isCSSLoaded:function(t,n){if(!t||!YUI.Env.cssStampEl||!n&&this.ignoreRegistered)return!1;var r=YUI.Env.cssStampEl,i=!1,s=YUI.Env._cssLoaded[t],o=r.currentStyle;return s!==undefined?s:(r.className=t,o||(o=e.config.doc.defaultView.getComputedStyle(r,null)),o&&o.display==="none"&&(i=!0),r.className="",YUI.Env._cssLoaded[t]=i,i)},getProvides:function(t){var r=this.getModule(t),i,s;return r?(r&&!r.provides&&(i={},s=r.supersedes,s&&v.each(s,function(t){e.mix(i,this.getProvides(t))},this),i[t]=!0,r.provides=i),r.provides):n},calculate:function(e,t){if(e||t||this.dirty)e&&this._config(e),this._init||this._setup(),this._explode(),this.allowRollup?this._rollup():this._explodeRollups(),this._reduce(),this._sort()},_addLangPack:function(t,n,r){var i=n.name,s,o,u=this.getModuleInfo(r);return u||(s=S(n.pkg||i,r,a,!0),o={path:s,intl:!0,langPack:!0,ext:n.ext,group:n.group,supersedes:[]},n.root&&(o.root=n.root),n.base&&(o.base=n.base),n.configFn&&(o.configFn=n.configFn),this.addModule(o,r),t&&(e.Env.lang=e.Env.lang||{},e.Env.lang[t]=e.Env.lang[t]||{},e.Env.lang[t][i]=!0)),this.getModuleInfo(r)},_setup:function(){var t=this.moduleInfo,n,r,i,o,u,a;for(n in t)t.hasOwnProperty(n)&&(o=t[n],o&&(o.requires=v.dedupe(o.requires),o.lang&&(a=this.getLangPackName(h,n),this._addLangPack(null,o,a))));u={},this.ignoreRegistered||e.mix(u,s.mods),this.ignore&&e.mix(u,v.hash(this.ignore));for(i in u)u.hasOwnProperty(i)&&e.mix(u,this.getProvides(i));if(this.force)for(r=0;r<this.force.length;r++)this.force[r]in u&&delete u[this.force[r]];e.mix(this.loaded,u),this._init=!0},getLangPackName:function(e,t){return"lang/"+t+(e?"_"+e:"")},_explode:function(){var t=this.required,n,r,i={},s=this,o,u;s.dirty=!1,s._explodeRollups(),t=s.required;for(o in t)t.hasOwnProperty(o)&&(i[o]||(i[o]=!0,n=s.getModule(o),n&&(u=n.expound,u&&(t[u]=s.getModule(u),r=s.getRequires(t[u]),e.mix(t,v.hash(r))),r=s.getRequires(n),e.mix(t,v.hash(r)))))},_patternTest:function(e,t){return e.indexOf(t)>-1},getModule:function(t){if(!t)return null;var n,r,i,s=this.getModuleInfo(t),o=this.patterns;if(!s||s&&s.ext)for(i in o)if(o.hasOwnProperty(i)){n=o[i],n.test||(n.test=this._patternTest);if(n.test(t,i)){r=n;break}}return s?r&&s&&r.configFn&&!s.configFn&&(s.configFn=r.configFn,s.configFn(s)):r&&(n.action?n.action.call(this,t,i):(s=this.addModule(e.merge(r,{test:void 0,temp:!0}),t),r.configFn&&(s.configFn=r.configFn))),s},_rollup:function(){},_reduce:function(e){e=e||this.required;var t,n,r,i,s=this.loadType,o=this.ignore?v.hash(this.ignore):!1;for(t in e)if(e.hasOwnProperty(t)){i=this.getModule(t),((this.loaded[t]||w[t])&&!this.forceMap[t]&&!this.ignoreRegistered||s&&i&&i.type!==s)&&delete e[t],o&&o[t]&&delete e[t],r=i&&i.supersedes;if(r)for(n=0;n<r.length;n++)r[n]in e&&delete e[r[n]]}return e},_finish:function(e,t){m.running=!1;var n=this.onEnd;n&&n.call(this.context,{msg:e,data:this.data,success:t}),this._continue()},_onSuccess:function(){var t=this,n=e.merge(t.skipped),r,i=[],s=t.requireRegistration,o,u,f,l;for(f in n)n.hasOwnProperty(f)&&delete t.inserted[f];t.skipped={};for(f in t.inserted)t.inserted.hasOwnProperty(f)&&(l=t.getModule(f),!l||!s||l.type!==a||f in YUI.Env.mods?e.mix(t.loaded,t.getProvides(f)):i.push(f));r=t.onSuccess,u=i.length?"notregistered":"success",o=!i.length,r&&r.call(t.context,{msg:u,data:t.data,success:o,failed:i,skipped:n}),t._finish(u,o)},_onProgress:function(e){var t=this,n;if(e.data&&e.data.length)for(n=0;n<e.data.length;n++)e.data[n]=t.getModule(e.data[n].name);t.onProgress&&t.onProgress.call(t.context,{name:e.url,data:e.data})},_onFailure:function(e){var t=this.onFailure,n=[],r=0,i=e.errors.length;for(r;r<i;r++)n.push(e.errors[r].error);n=n.join(","),t&&t.call(this.context,{msg:n,data:this.data,success:!1}),this._finish(n,!1)},_onTimeout:function(e){var t=this.onTimeout;t&&t.call(this.context,{msg:"timeout",data:this.data,success:!1,transaction:e})},_sort:function(){var e,t=this.required,n={};this.sorted=[];for(e in t)!n[e]&&t.hasOwnProperty(e)&&this._visit(e,n)},_visit:function(e,t){var n,r,i,s,o,u,a,f,l;t[e]=!0,n=this.required,i=this.moduleInfo[e],r=this.conditions[e]||{};if(i){o=i.expanded||i.requires;for(f=0,l=o.length;f<l;++f)s=o[f],u=r[s],a=u&&(!u.when||u.when==="after"),n[s]&&!t[s]&&!a&&this._visit(s,t)}this.sorted.push(e)},_insert:function(t,n,r,i){t&&this._config(t);var s=this.resolve(!i),o=this,f=0,l=0,c={},h,p;o._refetch=[],r&&(s[r===a?u:a]=[]),o.fetchCSS||(s.css=[]),s.js.length&&f++,s.css.length&&f++,p=function(t){l++;var n={},r=0,i=0,s="",u,a,p;if(t&&t.errors)for(r=0;r<t.errors.length;r++)t.errors[r].request?s=t.errors[r].request.url:s=t.errors[r],n[s]=s;if(t&&t.data&&t.data.length&&t.type==="success")for(r=0;r<t.data.length;r++){o.inserted[t.data[r].name]=!0;if(t.data[r].lang||t.data[r].skinnable)delete o.inserted[t.data[r].name],o._refetch.push(t.data[r].name)}if(l===f){o._loading=null;if(o._refetch.length){for(r=0;r<o._refetch.length;r++){h=o.getRequires(o.getModule(o._refetch[r]));for(i=0;i<h.length;i++)o.inserted[h[i]]||(c[h[i]]=h[i])}c=e.Object.keys(c);if(c.length){o.require(c),p=o.resolve(!0);if(p.cssMods.length
+){for(r=0;r<p.cssMods.length;r++)a=p.cssMods[r].name,delete YUI.Env._cssLoaded[a],o.isCSSLoaded(a)&&(o.inserted[a]=!0,delete o.required[a]);o.sorted=[],o._sort()}t=null,o._insert()}}t&&t.fn&&(u=t.fn,delete t.fn,u.call(o,t))}},this._loading=!0;if(!s.js.length&&!s.css.length){l=-1,p({fn:o._onSuccess});return}s.css.length&&e.Get.css(s.css,{data:s.cssMods,attributes:o.cssAttributes,insertBefore:o.insertBefore,charset:o.charset,timeout:o.timeout,context:o,onProgress:function(e){o._onProgress.call(o,e)},onTimeout:function(e){o._onTimeout.call(o,e)},onSuccess:function(e){e.type="success",e.fn=o._onSuccess,p.call(o,e)},onFailure:function(e){e.type="failure",e.fn=o._onFailure,p.call(o,e)}}),s.js.length&&e.Get.js(s.js,{data:s.jsMods,insertBefore:o.insertBefore,attributes:o.jsAttributes,charset:o.charset,timeout:o.timeout,autopurge:!1,context:o,async:o.async,onProgress:function(e){o._onProgress.call(o,e)},onTimeout:function(e){o._onTimeout.call(o,e)},onSuccess:function(e){e.type="success",e.fn=o._onSuccess,p.call(o,e)},onFailure:function(e){e.type="failure",e.fn=o._onFailure,p.call(o,e)}})},_continue:function(){!m.running&&m.size()>0&&(m.running=!0,m.next()())},insert:function(t,n,r){var i=this,s=e.merge(this);delete s.require,delete s.dirty,m.add(function(){i._insert(s,t,n,r)}),this._continue()},loadNext:function(){return},_filter:function(e,t,n){var r=this.filter,i=t&&t in this.filters,s=i&&this.filters[t],o=n||(this.getModuleInfo(t)||{}).group||null;return o&&this.groups[o]&&this.groups[o].filter&&(s=this.groups[o].filter,i=!0),e&&(i&&(r=b.isString(s)?this.FILTER_DEFS[s.toUpperCase()]||null:s),r&&(e=e.replace(new RegExp(r.searchExp,"g"),r.replaceStr))),e},_url:function(e,t,n){return this._filter((n||this.base||"")+e,t)},resolve:function(e,t){var r=this,s={js:[],jsMods:[],css:[],cssMods:[]},o;(r.skin.overrides||r.skin.defaultSkin!==l||r.ignoreRegistered)&&r._resetModules(),e&&r.calculate(),t=t||r.sorted,o=function(e){if(e){var t=e.group&&r.groups[e.group]||n,i;t.async===!1&&(e.async=t.async),i=e.fullpath?r._filter(e.fullpath,e.name):r._url(e.path,e.name,t.base||e.base);if(e.attributes||e.async===!1)i={url:i,async:e.async},e.attributes&&(i.attributes=e.attributes);s[e.type].push(i),s[e.type+"Mods"].push(e)}};var f=r.ignoreRegistered?{}:r.inserted,c={},h,p,d,v,m,g,y,b;for(b=0,y=t.length;b<y;b++){g=r.getModule(t[b]);if(!g||f[g.name])continue;m=r.groups[g.group],d=r.comboBase;if(m){if(!m.combine||g.fullpath){o(g);continue}g.combine=!0,typeof m.root=="string"&&(g.root=m.root),d=m.comboBase||d,v=m.comboSep,h=m.maxURLLength}else if(!r.combine){o(g);continue}if(!g.combine&&g.ext){o(g);continue}c[d]=c[d]||{js:[],jsMods:[],css:[],cssMods:[]},p=c[d],p.group=g.group,p.comboSep=v||r.comboSep,p.maxURLLength=h||r.maxURLLength,p[g.type+"Mods"].push(g)}var w,E,S,x,T,N,C;for(d in c)if(c.hasOwnProperty(d)){p=c[d],v=p.comboSep,h=p.maxURLLength;for(C in p)if(C===a||C===u){E=p[C+"Mods"],T=[];for(b=0,y=E.length;b<y;b+=1)g=E[b],N=(typeof g.root=="string"?g.root:r.root)+(g.path||g.fullpath),T.push(r._filter(N,g.name));S=d+T.join(v),x=S.length,h<=d.length&&(h=i);if(T.length)if(x>h){w=[];for(b=0,y=T.length;b<y;b++)w.push(T[b]),S=d+w.join(v),S.length>h&&(N=w.pop(),S=d+w.join(v),s[C].push(r._filter(S,null,p.group)),w=[],N&&w.push(N));w.length&&(S=d+w.join(v),s[C].push(r._filter(S,null,p.group)))}else s[C].push(r._filter(S,null,p.group));s[C+"Mods"]=s[C+"Mods"].concat(E)}}return s},load:function(e){if(!e)return;var t=this,n=t.resolve(!0);t.data=n,t.onEnd=function(){e.apply(t.context||t,arguments)},t.insert()}}},"@VERSION@",{requires:["get","features"]});
diff --git a/js/yui3/loader-rollup/loader-rollup-min.js b/js/yui3/loader-rollup/loader-rollup-min.js
new file mode 100644
index 000000000..2f8f3578a
--- /dev/null
+++ b/js/yui3/loader-rollup/loader-rollup-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("loader-rollup",function(e,t){e.Loader.prototype._rollup=function(){var e,t,n,r,i=this.required,s,o=this.moduleInfo,u,a,f;if(this.dirty||!this.rollups){this.rollups={};for(e in o)o.hasOwnProperty(e)&&(n=this.getModule(e),n&&n.rollup&&(this.rollups[e]=n))}for(;;){u=!1;for(e in this.rollups)if(this.rollups.hasOwnProperty(e)&&!i[e]&&(!this.loaded[e]||this.forceMap[e])){n=this.getModule(e),r=n.supersedes||[],s=!1;if(!n.rollup)continue;a=0;for(t=0;t<r.length;t++){f=o[r[t]];if(this.loaded[r[t]]&&!this.forceMap[r[t]]){s=!1;break}if(i[r[t]]&&n.type===f.type){a++,s=a>=n.rollup;if(s)break}}s&&(i[e]=!0,u=!0,this.getRequires(n))}if(!u)break}}},"@VERSION@",{requires:["loader-base"]});
diff --git a/js/yui3/loader-yui3/loader-yui3-min.js b/js/yui3/loader-yui3/loader-yui3-min.js
new file mode 100644
index 000000000..75b95aa10
--- /dev/null
+++ b/js/yui3/loader-yui3/loader-yui3-min.js
@@ -0,0 +1,13 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("loader-yui3",function(e,t){YUI.Env[e.version].modules=YUI.Env[e.version].modules||{},e.mix(YUI.Env[e.version].modules,{"align-plugin":{requires:["node-screen","node-pluginhost"]},anim:{use:["anim-base","anim-color","anim-curve","anim-easing","anim-node-plugin","anim-scroll","anim-xy"]},"anim-base":{requires:["base-base","node-style","color-base"]},"anim-color":{requires:["anim-base"]},"anim-curve":{requires:["anim-xy"]},"anim-easing":{requires:["anim-base"]},"anim-node-plugin":{requires:["node-pluginhost","anim-base"]},"anim-scroll":{requires:["anim-base"]},"anim-shape":{requires:["anim-base","anim-easing","anim-color","matrix"]},"anim-shape-transform":{use:["anim-shape"]},"anim-xy":{requires:["anim-base","node-screen"]},app:{use:["app-base","app-content","app-transitions","lazy-model-list","model","model-list","model-sync-rest","model-sync-local","router","view","view-node-map"]},"app-base":{requires:["classnamemanager","pjax-base","router","view"]},"app-content":{requires:["app-base","pjax-content"]},"app-transitions":{requires:["app-base"]},"app-transitions-css":{type:"css"},"app-transitions-native":{condition:{name:"app-transitions-native",test:function(e){var t=e.config.doc,n=t?t.documentElement:null;return n&&n.style?"MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style:!1},trigger:"app-transitions"},requires:["app-transitions","app-transitions-css","parallel","transition"]},"array-extras":{requires:["yui-base"]},"array-invoke":{requires:["yui-base"]},arraylist:{requires:["yui-base"]},"arraylist-add":{requires:["arraylist"]},"arraylist-filter":{requires:["arraylist"]},arraysort:{requires:["yui-base"]},"async-queue":{requires:["event-custom"]},attribute:{use:["attribute-base","attribute-complex"]},"attribute-base":{requires:["attribute-core","attribute-observable","attribute-extras"]},"attribute-complex":{requires:["attribute-base"]},"attribute-core":{requires:["oop"]},"attribute-events":{use:["attribute-observable"]},"attribute-extras":{requires:["oop"]},"attribute-observable":{requires:["event-custom"]},autocomplete:{use:["autocomplete-base","autocomplete-sources","autocomplete-list","autocomplete-plugin"]},"autocomplete-base":{optional:["autocomplete-sources"],requires:["array-extras","base-build","escape","event-valuechange","node-base"]},"autocomplete-filters":{requires:["array-extras","text-wordbreak"]},"autocomplete-filters-accentfold":{requires:["array-extras","text-accentfold","text-wordbreak"]},"autocomplete-highlighters":{requires:["array-extras","highlight-base"]},"autocomplete-highlighters-accentfold":{requires:["array-extras","highlight-accentfold"]},"autocomplete-list":{after:["autocomplete-sources"],lang:["en","es","hu","it"],requires:["autocomplete-base","event-resize","node-screen","selector-css3","shim-plugin","widget","widget-position","widget-position-align"],skinnable:!0},"autocomplete-list-keys":{condition:{name:"autocomplete-list-keys",test:function(e){return!e.UA.ios&&!e.UA.android},trigger:"autocomplete-list"},requires:["autocomplete-list","base-build"]},"autocomplete-plugin":{requires:["autocomplete-list","node-pluginhost"]},"autocomplete-sources":{optional:["io-base","json-parse","jsonp","yql"],requires:["autocomplete-base"]},axes:{use:["axis-numeric","axis-category","axis-time","axis-stacked"]},"axes-base":{use:["axis-numeric-base","axis-category-base","axis-time-base","axis-stacked-base"]},axis:{requires:["dom","widget","widget-position","widget-stack","graphics","axis-base"]},"axis-base":{requires:["classnamemanager","datatype-number","datatype-date","base","event-custom"]},"axis-category":{requires:["axis","axis-category-base"]},"axis-category-base":{requires:["axis-base"]},"axis-numeric":{requires:["axis","axis-numeric-base"]},"axis-numeric-base":{requires:["axis-base"]},"axis-stacked":{requires:["axis-numeric","axis-stacked-base"]},"axis-stacked-base":{requires:["axis-numeric-base"]},"axis-time":{requires:["axis","axis-time-base"]},"axis-time-base":{requires:["axis-base"]},base:{use:["base-base","base-pluginhost","base-build"]},"base-base":{requires:["attribute-base","base-core","base-observable"]},"base-build":{requires:["base-base"]},"base-core":{requires:["attribute-core"]},"base-observable":{requires:["attribute-observable","base-core"]},"base-pluginhost":{requires:["base-base","pluginhost"]},button:{requires:["button-core","cssbutton","widget"]},"button-core":{requires:["attribute-core","classnamemanager","node-base","escape"]},"button-group":{requires:["button-plugin","cssbutton","widget"]},"button-plugin":{requires:["button-core","cssbutton","node-pluginhost"]},cache:{use:["cache-base","cache-offline","cache-plugin"]},"cache-base":{requires:["base"]},"cache-offline":{requires:["cache-base","json"]},"cache-plugin":{requires:["plugin","cache-base"]},calendar:{requires:["calendar-base","calendarnavigator"],skinnable:!0},"calendar-base":{lang:["de","en","es","es-AR","fr","hu","it","ja","nb-NO","nl","pt-BR","ru","zh-Hans","zh-Hans-CN","zh-Hant","zh-Hant-HK","zh-HANT-TW"],requires:["widget","datatype-date","datatype-date-math","cssgrids"],skinnable:!0},calendarnavigator:{requires:["plugin","classnamemanager","datatype-date","node"],skinnable:!0},charts:{use:["charts-base"]},"charts-base":{requires:["dom","event-mouseenter","event-touch","graphics-group","axes","series-pie","series-line","series-marker","series-area","series-spline","series-column","series-bar","series-areaspline","series-combo","series-combospline","series-line-stacked","series-marker-stacked","series-area-stacked","series-spline-stacked","series-column-stacked","series-bar-stacked","series-areaspline-stacked","series-combo-stacked","series-combospline-stacked"]},"charts-legend":{requires:["charts-base"]},classnamemanager:{requires:["yui-base"]},"clickable-rail":{requires:["slider-base"]},collection:{use:["array-extras","arraylist","arraylist-add","arraylist-filter","array-invoke"]},color:{use:["color-base","color-hsl","color-harmony"]},"color-base"
+:{requires:["yui-base"]},"color-harmony":{requires:["color-hsl"]},"color-hsl":{requires:["color-base"]},"color-hsv":{requires:["color-base"]},console:{lang:["en","es","hu","it","ja"],requires:["yui-log","widget"],skinnable:!0},"console-filters":{requires:["plugin","console"],skinnable:!0},"content-editable":{requires:["node-base","editor-selection","stylesheet","plugin"]},controller:{use:["router"]},cookie:{requires:["yui-base"]},"createlink-base":{requires:["editor-base"]},cssbase:{after:["cssreset","cssfonts","cssgrids","cssreset-context","cssfonts-context","cssgrids-context"],type:"css"},"cssbase-context":{after:["cssreset","cssfonts","cssgrids","cssreset-context","cssfonts-context","cssgrids-context"],type:"css"},cssbutton:{type:"css"},cssfonts:{type:"css"},"cssfonts-context":{type:"css"},cssgrids:{optional:["cssnormalize"],type:"css"},"cssgrids-base":{optional:["cssnormalize"],type:"css"},"cssgrids-responsive":{optional:["cssnormalize"],requires:["cssgrids","cssgrids-responsive-base"],type:"css"},"cssgrids-units":{optional:["cssnormalize"],requires:["cssgrids-base"],type:"css"},cssnormalize:{type:"css"},"cssnormalize-context":{type:"css"},cssreset:{type:"css"},"cssreset-context":{type:"css"},dataschema:{use:["dataschema-base","dataschema-json","dataschema-xml","dataschema-array","dataschema-text"]},"dataschema-array":{requires:["dataschema-base"]},"dataschema-base":{requires:["base"]},"dataschema-json":{requires:["dataschema-base","json"]},"dataschema-text":{requires:["dataschema-base"]},"dataschema-xml":{requires:["dataschema-base"]},datasource:{use:["datasource-local","datasource-io","datasource-get","datasource-function","datasource-cache","datasource-jsonschema","datasource-xmlschema","datasource-arrayschema","datasource-textschema","datasource-polling"]},"datasource-arrayschema":{requires:["datasource-local","plugin","dataschema-array"]},"datasource-cache":{requires:["datasource-local","plugin","cache-base"]},"datasource-function":{requires:["datasource-local"]},"datasource-get":{requires:["datasource-local","get"]},"datasource-io":{requires:["datasource-local","io-base"]},"datasource-jsonschema":{requires:["datasource-local","plugin","dataschema-json"]},"datasource-local":{requires:["base"]},"datasource-polling":{requires:["datasource-local"]},"datasource-textschema":{requires:["datasource-local","plugin","dataschema-text"]},"datasource-xmlschema":{requires:["datasource-local","plugin","datatype-xml","dataschema-xml"]},datatable:{use:["datatable-core","datatable-table","datatable-head","datatable-body","datatable-base","datatable-column-widths","datatable-message","datatable-mutable","datatable-sort","datatable-datasource"]},"datatable-base":{requires:["datatable-core","datatable-table","datatable-head","datatable-body","base-build","widget"],skinnable:!0},"datatable-body":{requires:["datatable-core","view","classnamemanager"]},"datatable-column-widths":{requires:["datatable-base"]},"datatable-core":{requires:["escape","model-list","node-event-delegate"]},"datatable-datasource":{requires:["datatable-base","plugin","datasource-local"]},"datatable-foot":{requires:["datatable-core","view"]},"datatable-formatters":{requires:["datatable-body","datatype-number-format","datatype-date-format","escape"]},"datatable-head":{requires:["datatable-core","view","classnamemanager"]},"datatable-highlight":{requires:["datatable-base","event-hover"],skinnable:!0},"datatable-keynav":{requires:["datatable-base"]},"datatable-message":{lang:["en","fr","es","hu","it"],requires:["datatable-base"],skinnable:!0},"datatable-mutable":{requires:["datatable-base"]},"datatable-paginator":{lang:["en","fr"],requires:["model","view","paginator-core","datatable-foot","datatable-paginator-templates"],skinnable:!0},"datatable-paginator-templates":{requires:["template"]},"datatable-scroll":{requires:["datatable-base","datatable-column-widths","dom-screen"],skinnable:!0},"datatable-sort":{lang:["en","fr","es","hu"],requires:["datatable-base"],skinnable:!0},"datatable-table":{requires:["datatable-core","datatable-head","datatable-body","view","classnamemanager"]},datatype:{use:["datatype-date","datatype-number","datatype-xml"]},"datatype-date":{use:["datatype-date-parse","datatype-date-format","datatype-date-math"]},"datatype-date-format":{lang:["ar","ar-JO","ca","ca-ES","da","da-DK","de","de-AT","de-DE","el","el-GR","en","en-AU","en-CA","en-GB","en-IE","en-IN","en-JO","en-MY","en-NZ","en-PH","en-SG","en-US","es","es-AR","es-BO","es-CL","es-CO","es-EC","es-ES","es-MX","es-PE","es-PY","es-US","es-UY","es-VE","fi","fi-FI","fr","fr-BE","fr-CA","fr-FR","hi","hi-IN","hu","id","id-ID","it","it-IT","ja","ja-JP","ko","ko-KR","ms","ms-MY","nb","nb-NO","nl","nl-BE","nl-NL","pl","pl-PL","pt","pt-BR","ro","ro-RO","ru","ru-RU","sv","sv-SE","th","th-TH","tr","tr-TR","vi","vi-VN","zh-Hans","zh-Hans-CN","zh-Hant","zh-Hant-HK","zh-Hant-TW"]},"datatype-date-math":{requires:["yui-base"]},"datatype-date-parse":{},"datatype-number":{use:["datatype-number-parse","datatype-number-format"]},"datatype-number-format":{},"datatype-number-parse":{requires:["escape"]},"datatype-xml":{use:["datatype-xml-parse","datatype-xml-format"]},"datatype-xml-format":{},"datatype-xml-parse":{},dd:{use:["dd-ddm-base","dd-ddm","dd-ddm-drop","dd-drag","dd-proxy","dd-constrain","dd-drop","dd-scroll","dd-delegate"]},"dd-constrain":{requires:["dd-drag"]},"dd-ddm":{requires:["dd-ddm-base","event-resize"]},"dd-ddm-base":{requires:["node","base","yui-throttle","classnamemanager"]},"dd-ddm-drop":{requires:["dd-ddm"]},"dd-delegate":{requires:["dd-drag","dd-drop-plugin","event-mouseenter"]},"dd-drag":{requires:["dd-ddm-base"]},"dd-drop":{requires:["dd-drag","dd-ddm-drop"]},"dd-drop-plugin":{requires:["dd-drop"]},"dd-gestures":{condition:{name:"dd-gestures",trigger:"dd-drag",ua:"touchEnabled"},requires:["dd-drag","event-synthetic","event-gestures"]},"dd-plugin":{optional:["dd-constrain","dd-proxy"],requires:["dd-drag"]},"dd-proxy":{requires:["dd-drag"]},"dd-scroll":{requires
+:["dd-drag"]},dial:{lang:["en","es","hu"],requires:["widget","dd-drag","event-mouseenter","event-move","event-key","transition","intl"],skinnable:!0},dom:{use:["dom-base","dom-screen","dom-style","selector-native","selector"]},"dom-base":{requires:["dom-core"]},"dom-core":{requires:["oop","features"]},"dom-screen":{requires:["dom-base","dom-style"]},"dom-style":{requires:["dom-base"]},"dom-style-ie":{condition:{name:"dom-style-ie",test:function(e){var t=e.Features.test,n=e.Features.add,r=e.config.win,i=e.config.doc,s="documentElement",o=!1;return n("style","computedStyle",{test:function(){return r&&"getComputedStyle"in r}}),n("style","opacity",{test:function(){return i&&"opacity"in i[s].style}}),o=!t("style","opacity")&&!t("style","computedStyle"),o},trigger:"dom-style"},requires:["dom-style","color-base"]},dump:{requires:["yui-base"]},editor:{use:["frame","editor-selection","exec-command","editor-base","editor-para","editor-br","editor-bidi","editor-tab","createlink-base"]},"editor-base":{requires:["base","frame","node","exec-command","editor-selection"]},"editor-bidi":{requires:["editor-base"]},"editor-br":{requires:["editor-base"]},"editor-inline":{requires:["editor-base","content-editable"]},"editor-lists":{requires:["editor-base"]},"editor-para":{requires:["editor-para-base"]},"editor-para-base":{requires:["editor-base"]},"editor-para-ie":{condition:{name:"editor-para-ie",trigger:"editor-para",ua:"ie",when:"instead"},requires:["editor-para-base"]},"editor-selection":{requires:["node"]},"editor-tab":{requires:["editor-base"]},escape:{requires:["yui-base"]},event:{after:["node-base"],use:["event-base","event-delegate","event-synthetic","event-mousewheel","event-mouseenter","event-key","event-focus","event-resize","event-hover","event-outside","event-touch","event-move","event-flick","event-valuechange","event-tap"]},"event-base":{after:["node-base"],requires:["event-custom-base"]},"event-base-ie":{after:["event-base"],condition:{name:"event-base-ie",test:function(e){var t=e.config.doc&&e.config.doc.implementation;return t&&!t.hasFeature("Events","2.0")},trigger:"node-base"},requires:["node-base"]},"event-contextmenu":{requires:["event-synthetic","dom-screen"]},"event-custom":{use:["event-custom-base","event-custom-complex"]},"event-custom-base":{requires:["oop"]},"event-custom-complex":{requires:["event-custom-base"]},"event-delegate":{requires:["node-base"]},"event-flick":{requires:["node-base","event-touch","event-synthetic"]},"event-focus":{requires:["event-synthetic"]},"event-gestures":{use:["event-flick","event-move"]},"event-hover":{requires:["event-mouseenter"]},"event-key":{requires:["event-synthetic"]},"event-mouseenter":{requires:["event-synthetic"]},"event-mousewheel":{requires:["node-base"]},"event-move":{requires:["node-base","event-touch","event-synthetic"]},"event-outside":{requires:["event-synthetic"]},"event-resize":{requires:["node-base","event-synthetic"]},"event-simulate":{requires:["event-base"]},"event-synthetic":{requires:["node-base","event-custom-complex"]},"event-tap":{requires:["node-base","event-base","event-touch","event-synthetic"]},"event-touch":{requires:["node-base"]},"event-valuechange":{requires:["event-focus","event-synthetic"]},"exec-command":{requires:["frame"]},features:{requires:["yui-base"]},file:{requires:["file-flash","file-html5"]},"file-flash":{requires:["base"]},"file-html5":{requires:["base"]},frame:{requires:["base","node","plugin","selector-css3","yui-throttle"]},"gesture-simulate":{requires:["async-queue","event-simulate","node-screen"]},get:{requires:["yui-base"]},graphics:{requires:["node","event-custom","pluginhost","matrix","classnamemanager"]},"graphics-canvas":{condition:{name:"graphics-canvas",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"},requires:["graphics","color-base"]},"graphics-canvas-default":{condition:{name:"graphics-canvas-default",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"}},"graphics-group":{requires:["graphics"]},"graphics-svg":{condition:{name:"graphics-svg",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"},requires:["graphics"]},"graphics-svg-default":{condition:{name:"graphics-svg-default",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"}},"graphics-vml":{condition:{name:"graphics-vml",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"},requires:["graphics","color-base"]},"graphics-vml-default":{condition:{name:"graphics-vml-default",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"}},handlebars:{use:["handlebars-compiler"]},"handlebars-base":{requires:[]},"handlebars-compiler":{requires:["handlebars-base"]},highlight:{use:["highlight-base","highlight-accentfold"]},"highlight-accentfold":{requires:["highlight-base","text-accentfold"
+]},"highlight-base":{requires:["array-extras","classnamemanager","escape","text-wordbreak"]},history:{use:["history-base","history-hash","history-html5"]},"history-base":{requires:["event-custom-complex"]},"history-hash":{after:["history-html5"],requires:["event-synthetic","history-base","yui-later"]},"history-hash-ie":{condition:{name:"history-hash-ie",test:function(e){var t=e.config.doc&&e.config.doc.documentMode;return e.UA.ie&&(!("onhashchange"in e.config.win)||!t||t<8)},trigger:"history-hash"},requires:["history-hash","node-base"]},"history-html5":{optional:["json"],requires:["event-base","history-base","node-base"]},imageloader:{requires:["base-base","node-style","node-screen"]},intl:{requires:["intl-base","event-custom"]},"intl-base":{requires:["yui-base"]},io:{use:["io-base","io-xdr","io-form","io-upload-iframe","io-queue"]},"io-base":{requires:["event-custom-base","querystring-stringify-simple"]},"io-form":{requires:["io-base","node-base"]},"io-nodejs":{condition:{name:"io-nodejs",trigger:"io-base",ua:"nodejs"},requires:["io-base"]},"io-queue":{requires:["io-base","queue-promote"]},"io-upload-iframe":{requires:["io-base","node-base"]},"io-xdr":{requires:["io-base","datatype-xml-parse"]},json:{use:["json-parse","json-stringify"]},"json-parse":{requires:["yui-base"]},"json-parse-shim":{condition:{name:"json-parse-shim",test:function(e){function i(e,t){return e==="ok"?!0:t}var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONParse!==!1&&!!n;if(r)try{r=n.parse('{"ok":false}',i).ok}catch(s){r=!1}return!r},trigger:"json-parse"},requires:["json-parse"]},"json-stringify":{requires:["yui-base"]},"json-stringify-shim":{condition:{name:"json-stringify-shim",test:function(e){var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONStringify!==!1&&!!n;if(r)try{r="0"===n.stringify(0)}catch(i){r=!1}return!r},trigger:"json-stringify"},requires:["json-stringify"]},jsonp:{requires:["get","oop"]},"jsonp-url":{requires:["jsonp"]},"lazy-model-list":{requires:["model-list"]},loader:{use:["loader-base","loader-rollup","loader-yui3"]},"loader-base":{requires:["get","features"]},"loader-rollup":{requires:["loader-base"]},"loader-yui3":{requires:["loader-base"]},matrix:{requires:["yui-base"]},model:{requires:["base-build","escape","json-parse"]},"model-list":{requires:["array-extras","array-invoke","arraylist","base-build","escape","json-parse","model"]},"model-sync-local":{requires:["model","json-stringify"]},"model-sync-rest":{requires:["model","io-base","json-stringify"]},node:{use:["node-base","node-event-delegate","node-pluginhost","node-screen","node-style"]},"node-base":{requires:["event-base","node-core","dom-base","dom-style"]},"node-core":{requires:["dom-core","selector"]},"node-event-delegate":{requires:["node-base","event-delegate"]},"node-event-html5":{requires:["node-base"]},"node-event-simulate":{requires:["node-base","event-simulate","gesture-simulate"]},"node-flick":{requires:["classnamemanager","transition","event-flick","plugin"],skinnable:!0},"node-focusmanager":{requires:["attribute","node","plugin","node-event-simulate","event-key","event-focus"]},"node-load":{requires:["node-base","io-base"]},"node-menunav":{requires:["node","classnamemanager","plugin","node-focusmanager"],skinnable:!0},"node-pluginhost":{requires:["node-base","pluginhost"]},"node-screen":{requires:["dom-screen","node-base"]},"node-scroll-info":{requires:["array-extras","base-build","event-resize","node-pluginhost","plugin","selector"]},"node-style":{requires:["dom-style","node-base"]},oop:{requires:["yui-base"]},overlay:{requires:["widget","widget-stdmod","widget-position","widget-position-align","widget-stack","widget-position-constrain"],skinnable:!0},paginator:{requires:["paginator-core"]},"paginator-core":{requires:["base"]},"paginator-url":{requires:["paginator"]},panel:{requires:["widget","widget-autohide","widget-buttons","widget-modality","widget-position","widget-position-align","widget-position-constrain","widget-stack","widget-stdmod"],skinnable:!0},parallel:{requires:["yui-base"]},pjax:{requires:["pjax-base","pjax-content"]},"pjax-base":{requires:["classnamemanager","node-event-delegate","router"]},"pjax-content":{requires:["io-base","node-base","router"]},"pjax-plugin":{requires:["node-pluginhost","pjax","plugin"]},plugin:{requires:["base-base"]},pluginhost:{use:["pluginhost-base","pluginhost-config"]},"pluginhost-base":{requires:["yui-base"]},"pluginhost-config":{requires:["pluginhost-base"]},promise:{requires:["timers"]},querystring:{use:["querystring-parse","querystring-stringify"]},"querystring-parse":{requires:["yui-base","array-extras"]},"querystring-parse-simple":{requires:["yui-base"]},"querystring-stringify":{requires:["yui-base"]},"querystring-stringify-simple":{requires:["yui-base"]},"queue-promote":{requires:["yui-base"]},"range-slider":{requires:["slider-base","slider-value-range","clickable-rail"]},recordset:{use:["recordset-base","recordset-sort","recordset-filter","recordset-indexer"]},"recordset-base":{requires:["base","arraylist"]},"recordset-filter":{requires:["recordset-base","array-extras","plugin"]},"recordset-indexer":{requires:["recordset-base","plugin"]},"recordset-sort":{requires:["arraysort","recordset-base","plugin"]},resize:{use:["resize-base","resize-proxy","resize-constrain"]},"resize-base":{requires:["base","widget","event","oop","dd-drag","dd-delegate","dd-drop"],skinnable:!0},"resize-constrain":{requires:["plugin","resize-base"]},"resize-plugin":{optional:["resize-constrain"],requires:["resize-base","plugin"]},"resize-proxy":{requires:["plugin","resize-base"]},router:{optional:["querystring-parse"],requires:["array-extras","base-build","history"]},scrollview:{requires:["scrollview-base","scrollview-scrollbars"]},"scrollview-base":{requires:["widget","event-gestures","event-mousewheel","transition"],skinnable:!0},"scrollview-base-ie":{condition:{name:"scrollview-base-ie"
+,trigger:"scrollview-base",ua:"ie"},requires:["scrollview-base"]},"scrollview-list":{requires:["plugin","classnamemanager"],skinnable:!0},"scrollview-paginator":{requires:["plugin","classnamemanager"]},"scrollview-scrollbars":{requires:["classnamemanager","transition","plugin"],skinnable:!0},selector:{requires:["selector-native"]},"selector-css2":{condition:{name:"selector-css2",test:function(e){var t=e.config.doc,n=t&&!("querySelectorAll"in t);return n},trigger:"selector"},requires:["selector-native"]},"selector-css3":{requires:["selector-native","selector-css2"]},"selector-native":{requires:["dom-base"]},"series-area":{requires:["series-cartesian","series-fill-util"]},"series-area-stacked":{requires:["series-stacked","series-area"]},"series-areaspline":{requires:["series-area","series-curve-util"]},"series-areaspline-stacked":{requires:["series-stacked","series-areaspline"]},"series-bar":{requires:["series-marker","series-histogram-base"]},"series-bar-stacked":{requires:["series-stacked","series-bar"]},"series-base":{requires:["graphics","axis-base"]},"series-candlestick":{requires:["series-range"]},"series-cartesian":{requires:["series-base"]},"series-column":{requires:["series-marker","series-histogram-base"]},"series-column-stacked":{requires:["series-stacked","series-column"]},"series-combo":{requires:["series-cartesian","series-line-util","series-plot-util","series-fill-util"]},"series-combo-stacked":{requires:["series-stacked","series-combo"]},"series-combospline":{requires:["series-combo","series-curve-util"]},"series-combospline-stacked":{requires:["series-combo-stacked","series-curve-util"]},"series-curve-util":{},"series-fill-util":{},"series-histogram-base":{requires:["series-cartesian","series-plot-util"]},"series-line":{requires:["series-cartesian","series-line-util"]},"series-line-stacked":{requires:["series-stacked","series-line"]},"series-line-util":{},"series-marker":{requires:["series-cartesian","series-plot-util"]},"series-marker-stacked":{requires:["series-stacked","series-marker"]},"series-ohlc":{requires:["series-range"]},"series-pie":{requires:["series-base","series-plot-util"]},"series-plot-util":{},"series-range":{requires:["series-cartesian"]},"series-spline":{requires:["series-line","series-curve-util"]},"series-spline-stacked":{requires:["series-stacked","series-spline"]},"series-stacked":{requires:["axis-stacked"]},"shim-plugin":{requires:["node-style","node-pluginhost"]},slider:{use:["slider-base","slider-value-range","clickable-rail","range-slider"]},"slider-base":{requires:["widget","dd-constrain","event-key"],skinnable:!0},"slider-value-range":{requires:["slider-base"]},sortable:{requires:["dd-delegate","dd-drop-plugin","dd-proxy"]},"sortable-scroll":{requires:["dd-scroll","sortable"]},stylesheet:{requires:["yui-base"]},substitute:{optional:["dump"],requires:["yui-base"]},swf:{requires:["event-custom","node","swfdetect","escape"]},swfdetect:{requires:["yui-base"]},tabview:{requires:["widget","widget-parent","widget-child","tabview-base","node-pluginhost","node-focusmanager"],skinnable:!0},"tabview-base":{requires:["node-event-delegate","classnamemanager"]},"tabview-plugin":{requires:["tabview-base"]},template:{use:["template-base","template-micro"]},"template-base":{requires:["yui-base"]},"template-micro":{requires:["escape"]},test:{requires:["event-simulate","event-custom","json-stringify"]},"test-console":{requires:["console-filters","test","array-extras"],skinnable:!0},text:{use:["text-accentfold","text-wordbreak"]},"text-accentfold":{requires:["array-extras","text-data-accentfold"]},"text-data-accentfold":{requires:["yui-base"]},"text-data-wordbreak":{requires:["yui-base"]},"text-wordbreak":{requires:["array-extras","text-data-wordbreak"]},timers:{requires:["yui-base"]},transition:{requires:["node-style"]},"transition-timer":{condition:{name:"transition-timer",test:function(e){var t=e.config.doc,n=t?t.documentElement:null,r=!0;return n&&n.style&&(r=!("MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style)),r},trigger:"transition"},requires:["transition"]},tree:{requires:["base-build","tree-node"]},"tree-labelable":{requires:["tree"]},"tree-lazy":{requires:["base-pluginhost","plugin","tree"]},"tree-node":{},"tree-openable":{requires:["tree"]},"tree-selectable":{requires:["tree"]},"tree-sortable":{requires:["tree"]},uploader:{requires:["uploader-html5","uploader-flash"]},"uploader-flash":{requires:["swfdetect","escape","widget","base","cssbutton","node","event-custom","uploader-queue"]},"uploader-html5":{requires:["widget","node-event-simulate","file-html5","uploader-queue"]},"uploader-queue":{requires:["base"]},view:{requires:["base-build","node-event-delegate"]},"view-node-map":{requires:["view"]},widget:{use:["widget-base","widget-htmlparser","widget-skin","widget-uievents"]},"widget-anim":{requires:["anim-base","plugin","widget"]},"widget-autohide":{requires:["base-build","event-key","event-outside","widget"]},"widget-base":{requires:["attribute","base-base","base-pluginhost","classnamemanager","event-focus","node-base","node-style"],skinnable:!0},"widget-base-ie":{condition:{name:"widget-base-ie",trigger:"widget-base",ua:"ie"},requires:["widget-base"]},"widget-buttons":{requires:["button-plugin","cssbutton","widget-stdmod"]},"widget-child":{requires:["base-build","widget"]},"widget-htmlparser":{requires:["widget-base"]},"widget-modality":{requires:["base-build","event-outside","widget"],skinnable:!0},"widget-parent":{requires:["arraylist","base-build","widget"]},"widget-position":{requires:["base-build","node-screen","widget"]},"widget-position-align":{requires:["widget-position"]},"widget-position-constrain":{requires:["widget-position"]},"widget-skin":{requires:["widget-base"]},"widget-stack":{requires:["base-build","widget"],skinnable:!0},"widget-stdmod":{requires:["base-build","widget"]},"widget-uievents":{requires:["node-event-delegate","widget-base"]},yql:{requires:["oop"]},"yql-jsonp":{condition:{name:"yql-jsonp",test
+:function(e){return!e.UA.nodejs&&!e.UA.winjs},trigger:"yql"},requires:["yql","jsonp","jsonp-url"]},"yql-nodejs":{condition:{name:"yql-nodejs",trigger:"yql",ua:"nodejs"},requires:["yql"]},"yql-winjs":{condition:{name:"yql-winjs",trigger:"yql",ua:"winjs"},requires:["yql"]},yui:{},"yui-base":{},"yui-later":{requires:["yui-base"]},"yui-log":{requires:["yui-base"]},"yui-throttle":{requires:["yui-base"]}}),YUI.Env[e.version].md5="45357bb11eddf7fd0a89c0b756599df2"},"@VERSION@",{requires:["loader-base"]});
diff --git a/js/yui3/loader/loader-min.js b/js/yui3/loader/loader-min.js
new file mode 100644
index 000000000..96e4ae82d
--- /dev/null
+++ b/js/yui3/loader/loader-min.js
@@ -0,0 +1,16 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("loader-base",function(e,t){(function(){var t=e.version,n="/build/",r=t+"/",i=e.Env.base,s="gallery-2014.05.29-15-46",o="2in3",u="4",a="2.9.0",f=i+"combo?",l={version:t,root:r,base:e.Env.base,comboBase:f,skin:{defaultSkin:"sam",base:"assets/skins/",path:"skin.css",after:["cssreset","cssfonts","cssgrids","cssbase","cssreset-context","cssfonts-context"]},groups:{},patterns:{}},c=l.groups,h=function(e,t,r){var s=o+"."+(e||u)+"/"+(t||a)+n,l=r&&r.base?r.base:i,h=r&&r.comboBase?r.comboBase:f;c.yui2.base=l+s,c.yui2.root=s,c.yui2.comboBase=h},p=function(e,t){var r=(e||s)+n,o=t&&t.base?t.base:i,u=t&&t.comboBase?t.comboBase:f;c.gallery.base=o+r,c.gallery.root=r,c.gallery.comboBase=u};c[t]={},c.gallery={ext:!1,combine:!0,comboBase:f,update:p,patterns:{"gallery-":{},"lang/gallery-":{},"gallerycss-":{type:"css"}}},c.yui2={combine:!0,ext:!1,comboBase:f,update:h,patterns:{"yui2-":{configFn:function(e){/-skin|reset|fonts|grids|base/.test(e.name)&&(e.type="css",e.path=e.path.replace(/\.js/,".css"),e.path=e.path.replace(/\/yui2-skin/,"/assets/skins/sam/yui2-skin"))}}}},p(),h(),YUI.Env[t]&&e.mix(l,YUI.Env[t],!1,["modules","groups","skin"],0,!0),YUI.Env[t]=l})();var n={},r=[],i=1024,s=YUI.Env,o=s._loaded,u="css",a="js",f="intl",l="sam",c=e.version,h="",p=e.Object,d=p.each,v=e.Array,m=s._loaderQueue,g=s[c],y="skin-",b=e.Lang,w=s.mods,E,S=function(e,t,n,r){var i=e+"/"+t;return r||(i+="-min"),i+="."+(n||u),i};YUI.Env._cssLoaded||(YUI.Env._cssLoaded={}),e.Env.meta=g,e.Loader=function(t){var n=this;t=t||{},E=g.md5,n.context=e,t.doBeforeLoader&&t.doBeforeLoader.apply(n,arguments),n.base=e.Env.meta.base+e.Env.meta.root,n.comboBase=e.Env.meta.comboBase,n.combine=t.base&&t.base.indexOf(n.comboBase.substr(0,20))>-1,n.comboSep="&",n.maxURLLength=i,n.ignoreRegistered=t.ignoreRegistered,n.root=e.Env.meta.root,n.timeout=0,n.forceMap={},n.allowRollup=!1,n.filters={},n.required={},n.patterns={},n.moduleInfo={},n.groups=e.merge(e.Env.meta.groups),n.skin=e.merge(e.Env.meta.skin),n.conditions={},n.config=t,n._internal=!0,n._populateConditionsCache(),n.loaded=o[c],n.async=!0,n._inspectPage(),n._internal=!1,n._config(t),n.forceMap=n.force?e.Array.hash(n.force):{},n.testresults=null,e.config.tests&&(n.testresults=e.config.tests),n.sorted=[],n.dirty=!0,n.inserted={},n.skipped={},n.tested={},n.ignoreRegistered&&n._resetModules()},e.Loader.prototype={getModuleInfo:function(t){var n=this.moduleInfo[t],r,i,o,a;return n?n:(r=g.modules,i=s._renderedMods,o=this._internal,i&&i.hasOwnProperty(t)&&!this.ignoreRegistered?this.moduleInfo[t]=e.merge(i[t]):r.hasOwnProperty(t)&&(this._internal=!0,a=this.addModule(r[t],t),a&&a.type===u&&this.isCSSLoaded(a.name,!0)&&(this.loaded[a.name]=!0),this._internal=o),this.moduleInfo[t])},_expandAliases:function(t){var n=[],r=YUI.Env.aliases,i,s;t=e.Array(t);for(i=0;i<t.length;i+=1)s=t[i],n.push.apply(n,r[s]?r[s]:[s]);return n},_populateConditionsCache:function(){var t=g.modules,n=s._conditions,r,i,o,u;if(n&&!this.ignoreRegistered)for(r in n)n.hasOwnProperty(r)&&(this.conditions[r]=e.merge(n[r]));else{for(r in t)if(t.hasOwnProperty(r)&&t[r].condition){o=this._expandAliases(t[r].condition.trigger);for(i=0;i<o.length;i+=1)u=o[i],this.conditions[u]=this.conditions[u]||{},this.conditions[u][t[r].name||r]=t[r].condition}s._conditions=this.conditions}},_resetModules:function(){var e=this,t,n,r,i,s;for(t in e.moduleInfo)if(e.moduleInfo.hasOwnProperty(t)&&e.moduleInfo[t]){r=e.moduleInfo[t],i=r.name,s=YUI.Env.mods[i]?YUI.Env.mods[i].details:null,s&&(e.moduleInfo[i]._reset=!0,e.moduleInfo[i].requires=s.requires||[],e.moduleInfo[i].optional=s.optional||[],e.moduleInfo[i].supersedes=s.supercedes||[]);if(r.defaults)for(n in r.defaults)r.defaults.hasOwnProperty(n)&&r[n]&&(r[n]=r.defaults[n]);r.langCache=undefined,r.skinCache=undefined,r.skinnable&&e._addSkin(e.skin.defaultSkin,r.name)}},REGEX_CSS:/\.css(?:[?;].*)?$/i,FILTER_DEFS:{RAW:{searchExp:"-min\\.js",replaceStr:".js"},DEBUG:{searchExp:"-min\\.js",replaceStr:"-debug.js"},COVERAGE:{searchExp:"-min\\.js",replaceStr:"-coverage.js"}},_inspectPage:function(){var e=this,t,n,r,i,s;for(s in w)w.hasOwnProperty(s)&&(t=w[s],t.details&&(n=e.getModuleInfo(t.name),r=t.details.requires,i=n&&n.requires,n?!n._inspected&&r&&i.length!==r.length&&delete n.expanded:n=e.addModule(t.details,s),n._inspected=!0))},_requires:function(e,t){var n,r,i,s,o=this.getModuleInfo(e),a=this.getModuleInfo(t);if(!o||!a)return!1;r=o.expanded_map,i=o.after_map;if(i&&t in i)return!0;i=a.after_map;if(i&&e in i)return!1;s=a.supersedes;if(s)for(n=0;n<s.length;n++)if(this._requires(e,s[n]))return!0;s=o.supersedes;if(s)for(n=0;n<s.length;n++)if(this._requires(t,s[n]))return!1;return r&&t in r?!0:o.ext&&o.type===u&&!a.ext&&a.type===u?!0:!1},_config:function(t){var n,r,i,s,o,u,a,f=this,l=[],c,h;if(t)for(n in t)if(t.hasOwnProperty(n)){i=t[n];if(n==="require")f.require(i);else if(n==="skin")typeof i=="string"&&(f.skin.defaultSkin=t.skin,i={defaultSkin:i}),e.mix(f.skin,i,!0);else if(n==="groups"){for(r in i)if(i.hasOwnProperty(r)){a=r,u=i[r],f.addGroup(u,a);if(u.aliases)for(s in u.aliases)u.aliases.hasOwnProperty(s)&&f.addAlias(u.aliases[s],s)}}else if(n==="modules")for(r in i)i.hasOwnProperty(r)&&f.addModule(i[r],r);else if(n==="aliases")for(r in i)i.hasOwnProperty(r)&&f.addAlias(i[r],r);else n==="gallery"?this.groups.gallery.update&&this.groups.gallery.update(i,t):n==="yui2"||n==="2in3"?this.groups.yui2.update&&this.groups.yui2.update(t["2in3"],t.yui2,t):f[n]=i}o=f.filter,b.isString(o)&&(o=o.toUpperCase(),f.filterName=o,f.filter=f.FILTER_DEFS[o],o==="DEBUG"&&f.require("yui-log","dump"));if(f.filterName&&f.coverage&&f.filterName==="COVERAGE"&&b.isArray(f.coverage)&&f.coverage.length){for(n=0;n<f.coverage.length;n++)c=f.coverage[n],h=f.getModuleInfo(c),h&&h.use?l=l.concat(h.use):l.push(c);f.filters=f.filters||{},e.Array.each(l,function(e){f.filters[e]=f.FILTER_DEFS.COVERAGE}),f.filterName="RAW",f.filter=f.FILTER_DEFS[f.filterName]}},formatSkin:function(e,t){var n=y+e;return t&&(n=n+"-"+t),n},_addSkin:function(
+e,t,n){var r,i,s,o=this.skin,u=t&&this.getModuleInfo(t),a=u&&u.ext;return t&&(i=this.formatSkin(e,t),this.getModuleInfo(i)||(r=u.pkg||t,s={skin:!0,name:i,group:u.group,type:"css",after:o.after,path:(n||r)+"/"+o.base+e+"/"+t+".css",ext:a},u.base&&(s.base=u.base),u.configFn&&(s.configFn=u.configFn),this.addModule(s,i))),i},addAlias:function(e,t){YUI.Env.aliases[t]=e,this.addModule({name:t,use:e})},addGroup:function(e,t){var n=e.modules,r=this,i,s;t=t||e.name,e.name=t,r.groups[t]=e;if(e.patterns)for(i in e.patterns)e.patterns.hasOwnProperty(i)&&(e.patterns[i].group=t,r.patterns[i]=e.patterns[i]);if(n)for(i in n)n.hasOwnProperty(i)&&(s=n[i],typeof s=="string"&&(s={name:i,fullpath:s}),s.group=t,r.addModule(s,i))},addModule:function(t,n){n=n||t.name,typeof t=="string"&&(t={name:n,fullpath:t});var r,i,o,f,l,c,p,d,m,g,y,b,w,E,x,T,N,C,k,L,A,O,M=this.moduleInfo[n],_=this.conditions,D;M&&M.temp&&(t=e.merge(M,t)),t.name=n;if(!t||!t.name)return null;t.type||(t.type=a,O=t.path||t.fullpath,O&&this.REGEX_CSS.test(O)&&(t.type=u)),!t.path&&!t.fullpath&&(t.path=S(n,n,t.type)),t.supersedes=t.supersedes||t.use,t.ext="ext"in t?t.ext:this._internal?!1:!0,r=t.submodules,this.moduleInfo[n]=t,t.requires=t.requires||[];if(this.requires)for(i=0;i<this.requires.length;i++)t.requires.push(this.requires[i]);if(t.group&&this.groups&&this.groups[t.group]){A=this.groups[t.group];if(A.requires)for(i=0;i<A.requires.length;i++)t.requires.push(A.requires[i])}t.defaults||(t.defaults={requires:t.requires?[].concat(t.requires):null,supersedes:t.supersedes?[].concat(t.supersedes):null,optional:t.optional?[].concat(t.optional):null}),t.skinnable&&t.ext&&t.temp&&(k=this._addSkin(this.skin.defaultSkin,n),t.requires.unshift(k)),t.requires.length&&(t.requires=this.filterRequires(t.requires)||[]);if(!t.langPack&&t.lang){y=v(t.lang);for(g=0;g<y.length;g++)T=y[g],b=this.getLangPackName(T,n),p=this.getModuleInfo(b),p||(p=this._addLangPack(T,t,b))}if(r){l=t.supersedes||[],o=0;for(i in r)if(r.hasOwnProperty(i)){c=r[i],c.path=c.path||S(n,i,t.type),c.pkg=n,c.group=t.group,c.supersedes&&(l=l.concat(c.supersedes)),p=this.addModule(c,i),l.push(i);if(p.skinnable){t.skinnable=!0,C=this.skin.overrides;if(C&&C[i])for(g=0;g<C[i].length;g++)k=this._addSkin(C[i][g],i,n),l.push(k);k=this._addSkin(this.skin.defaultSkin,i,n),l.push(k)}if(c.lang&&c.lang.length){y=v(c.lang);for(g=0;g<y.length;g++)T=y[g],b=this.getLangPackName(T,n),w=this.getLangPackName(T,i),p=this.getModuleInfo(b),p||(p=this._addLangPack(T,t,b)),E=E||v.hash(p.supersedes),w in E||p.supersedes.push(w),t.lang=t.lang||[],x=x||v.hash(t.lang),T in x||t.lang.push(T),b=this.getLangPackName(h,n),w=this.getLangPackName(h,i),p=this.getModuleInfo(b),p||(p=this._addLangPack(T,t,b)),w in E||p.supersedes.push(w)}o++}t.supersedes=v.dedupe(l),this.allowRollup&&(t.rollup=o<4?o:Math.min(o-1,4))}d=t.plugins;if(d)for(i in d)d.hasOwnProperty(i)&&(m=d[i],m.pkg=n,m.path=m.path||S(n,i,t.type),m.requires=m.requires||[],m.group=t.group,this.addModule(m,i),t.skinnable&&this._addSkin(this.skin.defaultSkin,i,n));if(t.condition){f=this._expandAliases(t.condition.trigger);for(i=0;i<f.length;i++)D=f[i],L=t.condition.when,_[D]=_[D]||{},_[D][n]=t.condition,L&&L!=="after"?L==="instead"&&(t.supersedes=t.supersedes||[],t.supersedes.push(D)):(t.after=t.after||[],t.after.push(D))}return t.supersedes&&(t.supersedes=this.filterRequires(t.supersedes)),t.after&&(t.after=this.filterRequires(t.after),t.after_map=v.hash(t.after)),t.configFn&&(N=t.configFn(t),N===!1&&(delete this.moduleInfo[n],delete s._renderedMods[n],t=null)),t&&(s._renderedMods||(s._renderedMods={}),s._renderedMods[n]=e.mix(s._renderedMods[n]||{},t),s._conditions=_),t},require:function(t){var n=typeof t=="string"?v(arguments):t;this.dirty=!0,this.required=e.merge(this.required,v.hash(this.filterRequires(n))),this._explodeRollups()},_explodeRollups:function(){var e=this,t,n,r,i,s,o,u,a=e.required;if(!e.allowRollup){for(r in a)if(a.hasOwnProperty(r)){t=e.getModule(r);if(t&&t.use){o=t.use.length;for(i=0;i<o;i++){n=e.getModule(t.use[i]);if(n&&n.use){u=n.use.length;for(s=0;s<u;s++)a[n.use[s]]=!0}else a[t.use[i]]=!0}}}e.required=a}},filterRequires:function(t){if(t){e.Lang.isArray(t)||(t=[t]),t=e.Array(t);var n=[],r,i,s,o;for(r=0;r<t.length;r++){i=this.getModule(t[r]);if(i&&i.use)for(s=0;s<i.use.length;s++)o=this.getModule(i.use[s]),o&&o.use&&o.name!==i.name?n=e.Array.dedupe([].concat(n,this.filterRequires(o.use))):n.push(i.use[s]);else n.push(t[r])}t=n}return t},_canBeAttached:function(t){return t=this.getModule(t),t&&t.test?(t.hasOwnProperty("_testResult")||(t._testResult=t.test(e)),t._testResult):!0},getRequires:function(t){if(!t)return r;if(t._parsed)return t.expanded||r;var n,i,s,o,u,a,l,c=this.testresults,m=t.name,g,y=w[m]&&w[m].details,b=t.optionalRequires,E,S,x,T,N,C,k,L,A,O,M=t.lang||t.intl,_=e.Features&&e.Features.tests.load,D,P;t.temp&&y&&(N=t,t=this.addModule(y,m),t.group=N.group,t.pkg=N.pkg,delete t.expanded),P=!!this.lang&&t.langCache!==this.lang||t.skinCache!==this.skin.defaultSkin;if(t.expanded&&!P)return t.expanded;if(b)for(n=0,o=b.length;n<o;n++)this._canBeAttached(b[n])&&t.requires.push(b[n]);E=[],D={},T=this.filterRequires(t.requires),t.lang&&(E.unshift("intl"),T.unshift("intl"),M=!0),C=this.filterRequires(t.optional),t._parsed=!0,t.langCache=this.lang,t.skinCache=this.skin.defaultSkin;for(n=0;n<T.length;n++)if(!D[T[n]]){E.push(T[n]),D[T[n]]=!0,i=this.getModule(T[n]);if(i){u=this.getRequires(i),M=M||i.expanded_map&&f in i.expanded_map;for(s=0;s<u.length;s++)E.push(u[s])}}T=this.filterRequires(t.supersedes);if(T)for(n=0;n<T.length;n++)if(!D[T[n]]){t.submodules&&E.push(T[n]),D[T[n]]=!0,i=this.getModule(T[n]);if(i){u=this.getRequires(i),M=M||i.expanded_map&&f in i.expanded_map;for(s=0;s<u.length;s++)E.push(u[s])}}if(C&&this.loadOptional)for(n=0;n<C.length;n++)if(!D[C[n]]){E.push(C[n]),D[C[n]]=!0,i=this.getModuleInfo(C[n]);if(i){u=this.getRequires(i),M=M||i.expanded_map&&f in i.expanded_map;for(s=0;s<u.length;s++)E.push(u[s])}}g=this.conditions[m];if(g){t._parsed=!1
+;if(c&&_)d(c,function(e,t){var n=_[t].name;!D[n]&&_[t].trigger===m&&e&&_[t]&&(D[n]=!0,E.push(n))});else for(n in g)if(g.hasOwnProperty(n)&&!D[n]){x=g[n],S=x&&(!x.ua&&!x.test||x.ua&&e.UA[x.ua]||x.test&&x.test(e,T));if(S){D[n]=!0,E.push(n),i=this.getModule(n);if(i){u=this.getRequires(i);for(s=0;s<u.length;s++)E.push(u[s])}}}}if(t.skinnable){L=this.skin.overrides;for(n in YUI.Env.aliases)YUI.Env.aliases.hasOwnProperty(n)&&e.Array.indexOf(YUI.Env.aliases[n],m)>-1&&(A=n);if(L&&(L[m]||A&&L[A])){O=m,L[A]&&(O=A);for(n=0;n<L[O].length;n++)k=this._addSkin(L[O][n],m),this.isCSSLoaded(k,this._boot)||E.push(k)}else k=this._addSkin(this.skin.defaultSkin,m),this.isCSSLoaded(k,this._boot)||E.push(k)}return t._parsed=!1,M&&(t.lang&&!t.langPack&&e.Intl&&(l=e.Intl.lookupBestLang(this.lang||h,t.lang),a=this.getLangPackName(l,m),a&&E.unshift(a)),E.unshift(f)),t.expanded_map=v.hash(E),t.expanded=p.keys(t.expanded_map),t.expanded},isCSSLoaded:function(t,n){if(!t||!YUI.Env.cssStampEl||!n&&this.ignoreRegistered)return!1;var r=YUI.Env.cssStampEl,i=!1,s=YUI.Env._cssLoaded[t],o=r.currentStyle;return s!==undefined?s:(r.className=t,o||(o=e.config.doc.defaultView.getComputedStyle(r,null)),o&&o.display==="none"&&(i=!0),r.className="",YUI.Env._cssLoaded[t]=i,i)},getProvides:function(t){var r=this.getModule(t),i,s;return r?(r&&!r.provides&&(i={},s=r.supersedes,s&&v.each(s,function(t){e.mix(i,this.getProvides(t))},this),i[t]=!0,r.provides=i),r.provides):n},calculate:function(e,t){if(e||t||this.dirty)e&&this._config(e),this._init||this._setup(),this._explode(),this.allowRollup?this._rollup():this._explodeRollups(),this._reduce(),this._sort()},_addLangPack:function(t,n,r){var i=n.name,s,o,u=this.getModuleInfo(r);return u||(s=S(n.pkg||i,r,a,!0),o={path:s,intl:!0,langPack:!0,ext:n.ext,group:n.group,supersedes:[]},n.root&&(o.root=n.root),n.base&&(o.base=n.base),n.configFn&&(o.configFn=n.configFn),this.addModule(o,r),t&&(e.Env.lang=e.Env.lang||{},e.Env.lang[t]=e.Env.lang[t]||{},e.Env.lang[t][i]=!0)),this.getModuleInfo(r)},_setup:function(){var t=this.moduleInfo,n,r,i,o,u,a;for(n in t)t.hasOwnProperty(n)&&(o=t[n],o&&(o.requires=v.dedupe(o.requires),o.lang&&(a=this.getLangPackName(h,n),this._addLangPack(null,o,a))));u={},this.ignoreRegistered||e.mix(u,s.mods),this.ignore&&e.mix(u,v.hash(this.ignore));for(i in u)u.hasOwnProperty(i)&&e.mix(u,this.getProvides(i));if(this.force)for(r=0;r<this.force.length;r++)this.force[r]in u&&delete u[this.force[r]];e.mix(this.loaded,u),this._init=!0},getLangPackName:function(e,t){return"lang/"+t+(e?"_"+e:"")},_explode:function(){var t=this.required,n,r,i={},s=this,o,u;s.dirty=!1,s._explodeRollups(),t=s.required;for(o in t)t.hasOwnProperty(o)&&(i[o]||(i[o]=!0,n=s.getModule(o),n&&(u=n.expound,u&&(t[u]=s.getModule(u),r=s.getRequires(t[u]),e.mix(t,v.hash(r))),r=s.getRequires(n),e.mix(t,v.hash(r)))))},_patternTest:function(e,t){return e.indexOf(t)>-1},getModule:function(t){if(!t)return null;var n,r,i,s=this.getModuleInfo(t),o=this.patterns;if(!s||s&&s.ext)for(i in o)if(o.hasOwnProperty(i)){n=o[i],n.test||(n.test=this._patternTest);if(n.test(t,i)){r=n;break}}return s?r&&s&&r.configFn&&!s.configFn&&(s.configFn=r.configFn,s.configFn(s)):r&&(n.action?n.action.call(this,t,i):(s=this.addModule(e.merge(r,{test:void 0,temp:!0}),t),r.configFn&&(s.configFn=r.configFn))),s},_rollup:function(){},_reduce:function(e){e=e||this.required;var t,n,r,i,s=this.loadType,o=this.ignore?v.hash(this.ignore):!1;for(t in e)if(e.hasOwnProperty(t)){i=this.getModule(t),((this.loaded[t]||w[t])&&!this.forceMap[t]&&!this.ignoreRegistered||s&&i&&i.type!==s)&&delete e[t],o&&o[t]&&delete e[t],r=i&&i.supersedes;if(r)for(n=0;n<r.length;n++)r[n]in e&&delete e[r[n]]}return e},_finish:function(e,t){m.running=!1;var n=this.onEnd;n&&n.call(this.context,{msg:e,data:this.data,success:t}),this._continue()},_onSuccess:function(){var t=this,n=e.merge(t.skipped),r,i=[],s=t.requireRegistration,o,u,f,l;for(f in n)n.hasOwnProperty(f)&&delete t.inserted[f];t.skipped={};for(f in t.inserted)t.inserted.hasOwnProperty(f)&&(l=t.getModule(f),!l||!s||l.type!==a||f in YUI.Env.mods?e.mix(t.loaded,t.getProvides(f)):i.push(f));r=t.onSuccess,u=i.length?"notregistered":"success",o=!i.length,r&&r.call(t.context,{msg:u,data:t.data,success:o,failed:i,skipped:n}),t._finish(u,o)},_onProgress:function(e){var t=this,n;if(e.data&&e.data.length)for(n=0;n<e.data.length;n++)e.data[n]=t.getModule(e.data[n].name);t.onProgress&&t.onProgress.call(t.context,{name:e.url,data:e.data})},_onFailure:function(e){var t=this.onFailure,n=[],r=0,i=e.errors.length;for(r;r<i;r++)n.push(e.errors[r].error);n=n.join(","),t&&t.call(this.context,{msg:n,data:this.data,success:!1}),this._finish(n,!1)},_onTimeout:function(e){var t=this.onTimeout;t&&t.call(this.context,{msg:"timeout",data:this.data,success:!1,transaction:e})},_sort:function(){var e,t=this.required,n={};this.sorted=[];for(e in t)!n[e]&&t.hasOwnProperty(e)&&this._visit(e,n)},_visit:function(e,t){var n,r,i,s,o,u,a,f,l;t[e]=!0,n=this.required,i=this.moduleInfo[e],r=this.conditions[e]||{};if(i){o=i.expanded||i.requires;for(f=0,l=o.length;f<l;++f)s=o[f],u=r[s],a=u&&(!u.when||u.when==="after"),n[s]&&!t[s]&&!a&&this._visit(s,t)}this.sorted.push(e)},_insert:function(t,n,r,i){t&&this._config(t);var s=this.resolve(!i),o=this,f=0,l=0,c={},h,p;o._refetch=[],r&&(s[r===a?u:a]=[]),o.fetchCSS||(s.css=[]),s.js.length&&f++,s.css.length&&f++,p=function(t){l++;var n={},r=0,i=0,s="",u,a,p;if(t&&t.errors)for(r=0;r<t.errors.length;r++)t.errors[r].request?s=t.errors[r].request.url:s=t.errors[r],n[s]=s;if(t&&t.data&&t.data.length&&t.type==="success")for(r=0;r<t.data.length;r++){o.inserted[t.data[r].name]=!0;if(t.data[r].lang||t.data[r].skinnable)delete o.inserted[t.data[r].name],o._refetch.push(t.data[r].name)}if(l===f){o._loading=null;if(o._refetch.length){for(r=0;r<o._refetch.length;r++){h=o.getRequires(o.getModule(o._refetch[r]));for(i=0;i<h.length;i++)o.inserted[h[i]]||(c[h[i]]=h[i])}c=e.Object.keys(c);if(c.length){o.require(c),p=o.resolve(!0);if(p.cssMods.length
+){for(r=0;r<p.cssMods.length;r++)a=p.cssMods[r].name,delete YUI.Env._cssLoaded[a],o.isCSSLoaded(a)&&(o.inserted[a]=!0,delete o.required[a]);o.sorted=[],o._sort()}t=null,o._insert()}}t&&t.fn&&(u=t.fn,delete t.fn,u.call(o,t))}},this._loading=!0;if(!s.js.length&&!s.css.length){l=-1,p({fn:o._onSuccess});return}s.css.length&&e.Get.css(s.css,{data:s.cssMods,attributes:o.cssAttributes,insertBefore:o.insertBefore,charset:o.charset,timeout:o.timeout,context:o,onProgress:function(e){o._onProgress.call(o,e)},onTimeout:function(e){o._onTimeout.call(o,e)},onSuccess:function(e){e.type="success",e.fn=o._onSuccess,p.call(o,e)},onFailure:function(e){e.type="failure",e.fn=o._onFailure,p.call(o,e)}}),s.js.length&&e.Get.js(s.js,{data:s.jsMods,insertBefore:o.insertBefore,attributes:o.jsAttributes,charset:o.charset,timeout:o.timeout,autopurge:!1,context:o,async:o.async,onProgress:function(e){o._onProgress.call(o,e)},onTimeout:function(e){o._onTimeout.call(o,e)},onSuccess:function(e){e.type="success",e.fn=o._onSuccess,p.call(o,e)},onFailure:function(e){e.type="failure",e.fn=o._onFailure,p.call(o,e)}})},_continue:function(){!m.running&&m.size()>0&&(m.running=!0,m.next()())},insert:function(t,n,r){var i=this,s=e.merge(this);delete s.require,delete s.dirty,m.add(function(){i._insert(s,t,n,r)}),this._continue()},loadNext:function(){return},_filter:function(e,t,n){var r=this.filter,i=t&&t in this.filters,s=i&&this.filters[t],o=n||(this.getModuleInfo(t)||{}).group||null;return o&&this.groups[o]&&this.groups[o].filter&&(s=this.groups[o].filter,i=!0),e&&(i&&(r=b.isString(s)?this.FILTER_DEFS[s.toUpperCase()]||null:s),r&&(e=e.replace(new RegExp(r.searchExp,"g"),r.replaceStr))),e},_url:function(e,t,n){return this._filter((n||this.base||"")+e,t)},resolve:function(e,t){var r=this,s={js:[],jsMods:[],css:[],cssMods:[]},o;(r.skin.overrides||r.skin.defaultSkin!==l||r.ignoreRegistered)&&r._resetModules(),e&&r.calculate(),t=t||r.sorted,o=function(e){if(e){var t=e.group&&r.groups[e.group]||n,i;t.async===!1&&(e.async=t.async),i=e.fullpath?r._filter(e.fullpath,e.name):r._url(e.path,e.name,t.base||e.base);if(e.attributes||e.async===!1)i={url:i,async:e.async},e.attributes&&(i.attributes=e.attributes);s[e.type].push(i),s[e.type+"Mods"].push(e)}};var f=r.ignoreRegistered?{}:r.inserted,c={},h,p,d,v,m,g,y,b;for(b=0,y=t.length;b<y;b++){g=r.getModule(t[b]);if(!g||f[g.name])continue;m=r.groups[g.group],d=r.comboBase;if(m){if(!m.combine||g.fullpath){o(g);continue}g.combine=!0,typeof m.root=="string"&&(g.root=m.root),d=m.comboBase||d,v=m.comboSep,h=m.maxURLLength}else if(!r.combine){o(g);continue}if(!g.combine&&g.ext){o(g);continue}c[d]=c[d]||{js:[],jsMods:[],css:[],cssMods:[]},p=c[d],p.group=g.group,p.comboSep=v||r.comboSep,p.maxURLLength=h||r.maxURLLength,p[g.type+"Mods"].push(g)}var w,E,S,x,T,N,C;for(d in c)if(c.hasOwnProperty(d)){p=c[d],v=p.comboSep,h=p.maxURLLength;for(C in p)if(C===a||C===u){E=p[C+"Mods"],T=[];for(b=0,y=E.length;b<y;b+=1)g=E[b],N=(typeof g.root=="string"?g.root:r.root)+(g.path||g.fullpath),T.push(r._filter(N,g.name));S=d+T.join(v),x=S.length,h<=d.length&&(h=i);if(T.length)if(x>h){w=[];for(b=0,y=T.length;b<y;b++)w.push(T[b]),S=d+w.join(v),S.length>h&&(N=w.pop(),S=d+w.join(v),s[C].push(r._filter(S,null,p.group)),w=[],N&&w.push(N));w.length&&(S=d+w.join(v),s[C].push(r._filter(S,null,p.group)))}else s[C].push(r._filter(S,null,p.group));s[C+"Mods"]=s[C+"Mods"].concat(E)}}return s},load:function(e){if(!e)return;var t=this,n=t.resolve(!0);t.data=n,t.onEnd=function(){e.apply(t.context||t,arguments)},t.insert()}}},"@VERSION@",{requires:["get","features"]}),YUI.add("loader-rollup",function(e,t){e.Loader.prototype._rollup=function(){var e,t,n,r,i=this.required,s,o=this.moduleInfo,u,a,f;if(this.dirty||!this.rollups){this.rollups={};for(e in o)o.hasOwnProperty(e)&&(n=this.getModule(e),n&&n.rollup&&(this.rollups[e]=n))}for(;;){u=!1;for(e in this.rollups)if(this.rollups.hasOwnProperty(e)&&!i[e]&&(!this.loaded[e]||this.forceMap[e])){n=this.getModule(e),r=n.supersedes||[],s=!1;if(!n.rollup)continue;a=0;for(t=0;t<r.length;t++){f=o[r[t]];if(this.loaded[r[t]]&&!this.forceMap[r[t]]){s=!1;break}if(i[r[t]]&&n.type===f.type){a++,s=a>=n.rollup;if(s)break}}s&&(i[e]=!0,u=!0,this.getRequires(n))}if(!u)break}}},"@VERSION@",{requires:["loader-base"]}),YUI.add("loader-yui3",function(e,t){YUI.Env[e.version].modules=YUI.Env[e.version].modules||{},e.mix(YUI.Env[e.version].modules,{"align-plugin":{requires:["node-screen","node-pluginhost"]},anim:{use:["anim-base","anim-color","anim-curve","anim-easing","anim-node-plugin","anim-scroll","anim-xy"]},"anim-base":{requires:["base-base","node-style","color-base"]},"anim-color":{requires:["anim-base"]},"anim-curve":{requires:["anim-xy"]},"anim-easing":{requires:["anim-base"]},"anim-node-plugin":{requires:["node-pluginhost","anim-base"]},"anim-scroll":{requires:["anim-base"]},"anim-shape":{requires:["anim-base","anim-easing","anim-color","matrix"]},"anim-shape-transform":{use:["anim-shape"]},"anim-xy":{requires:["anim-base","node-screen"]},app:{use:["app-base","app-content","app-transitions","lazy-model-list","model","model-list","model-sync-rest","model-sync-local","router","view","view-node-map"]},"app-base":{requires:["classnamemanager","pjax-base","router","view"]},"app-content":{requires:["app-base","pjax-content"]},"app-transitions":{requires:["app-base"]},"app-transitions-css":{type:"css"},"app-transitions-native":{condition:{name:"app-transitions-native",test:function(e){var t=e.config.doc,n=t?t.documentElement:null;return n&&n.style?"MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style:!1},trigger:"app-transitions"},requires:["app-transitions","app-transitions-css","parallel","transition"]},"array-extras":{requires:["yui-base"]},"array-invoke":{requires:["yui-base"]},arraylist:{requires:["yui-base"]},"arraylist-add":{requires:["arraylist"]},"arraylist-filter":{requires:["arraylist"]},arraysort:{requires:["yui-base"]},"async-queue":{requires:["event-custom"]},attribute:{use:["attribute-base"
+,"attribute-complex"]},"attribute-base":{requires:["attribute-core","attribute-observable","attribute-extras"]},"attribute-complex":{requires:["attribute-base"]},"attribute-core":{requires:["oop"]},"attribute-events":{use:["attribute-observable"]},"attribute-extras":{requires:["oop"]},"attribute-observable":{requires:["event-custom"]},autocomplete:{use:["autocomplete-base","autocomplete-sources","autocomplete-list","autocomplete-plugin"]},"autocomplete-base":{optional:["autocomplete-sources"],requires:["array-extras","base-build","escape","event-valuechange","node-base"]},"autocomplete-filters":{requires:["array-extras","text-wordbreak"]},"autocomplete-filters-accentfold":{requires:["array-extras","text-accentfold","text-wordbreak"]},"autocomplete-highlighters":{requires:["array-extras","highlight-base"]},"autocomplete-highlighters-accentfold":{requires:["array-extras","highlight-accentfold"]},"autocomplete-list":{after:["autocomplete-sources"],lang:["en","es","hu","it"],requires:["autocomplete-base","event-resize","node-screen","selector-css3","shim-plugin","widget","widget-position","widget-position-align"],skinnable:!0},"autocomplete-list-keys":{condition:{name:"autocomplete-list-keys",test:function(e){return!e.UA.ios&&!e.UA.android},trigger:"autocomplete-list"},requires:["autocomplete-list","base-build"]},"autocomplete-plugin":{requires:["autocomplete-list","node-pluginhost"]},"autocomplete-sources":{optional:["io-base","json-parse","jsonp","yql"],requires:["autocomplete-base"]},axes:{use:["axis-numeric","axis-category","axis-time","axis-stacked"]},"axes-base":{use:["axis-numeric-base","axis-category-base","axis-time-base","axis-stacked-base"]},axis:{requires:["dom","widget","widget-position","widget-stack","graphics","axis-base"]},"axis-base":{requires:["classnamemanager","datatype-number","datatype-date","base","event-custom"]},"axis-category":{requires:["axis","axis-category-base"]},"axis-category-base":{requires:["axis-base"]},"axis-numeric":{requires:["axis","axis-numeric-base"]},"axis-numeric-base":{requires:["axis-base"]},"axis-stacked":{requires:["axis-numeric","axis-stacked-base"]},"axis-stacked-base":{requires:["axis-numeric-base"]},"axis-time":{requires:["axis","axis-time-base"]},"axis-time-base":{requires:["axis-base"]},base:{use:["base-base","base-pluginhost","base-build"]},"base-base":{requires:["attribute-base","base-core","base-observable"]},"base-build":{requires:["base-base"]},"base-core":{requires:["attribute-core"]},"base-observable":{requires:["attribute-observable","base-core"]},"base-pluginhost":{requires:["base-base","pluginhost"]},button:{requires:["button-core","cssbutton","widget"]},"button-core":{requires:["attribute-core","classnamemanager","node-base","escape"]},"button-group":{requires:["button-plugin","cssbutton","widget"]},"button-plugin":{requires:["button-core","cssbutton","node-pluginhost"]},cache:{use:["cache-base","cache-offline","cache-plugin"]},"cache-base":{requires:["base"]},"cache-offline":{requires:["cache-base","json"]},"cache-plugin":{requires:["plugin","cache-base"]},calendar:{requires:["calendar-base","calendarnavigator"],skinnable:!0},"calendar-base":{lang:["de","en","es","es-AR","fr","hu","it","ja","nb-NO","nl","pt-BR","ru","zh-Hans","zh-Hans-CN","zh-Hant","zh-Hant-HK","zh-HANT-TW"],requires:["widget","datatype-date","datatype-date-math","cssgrids"],skinnable:!0},calendarnavigator:{requires:["plugin","classnamemanager","datatype-date","node"],skinnable:!0},charts:{use:["charts-base"]},"charts-base":{requires:["dom","event-mouseenter","event-touch","graphics-group","axes","series-pie","series-line","series-marker","series-area","series-spline","series-column","series-bar","series-areaspline","series-combo","series-combospline","series-line-stacked","series-marker-stacked","series-area-stacked","series-spline-stacked","series-column-stacked","series-bar-stacked","series-areaspline-stacked","series-combo-stacked","series-combospline-stacked"]},"charts-legend":{requires:["charts-base"]},classnamemanager:{requires:["yui-base"]},"clickable-rail":{requires:["slider-base"]},collection:{use:["array-extras","arraylist","arraylist-add","arraylist-filter","array-invoke"]},color:{use:["color-base","color-hsl","color-harmony"]},"color-base":{requires:["yui-base"]},"color-harmony":{requires:["color-hsl"]},"color-hsl":{requires:["color-base"]},"color-hsv":{requires:["color-base"]},console:{lang:["en","es","hu","it","ja"],requires:["yui-log","widget"],skinnable:!0},"console-filters":{requires:["plugin","console"],skinnable:!0},"content-editable":{requires:["node-base","editor-selection","stylesheet","plugin"]},controller:{use:["router"]},cookie:{requires:["yui-base"]},"createlink-base":{requires:["editor-base"]},cssbase:{after:["cssreset","cssfonts","cssgrids","cssreset-context","cssfonts-context","cssgrids-context"],type:"css"},"cssbase-context":{after:["cssreset","cssfonts","cssgrids","cssreset-context","cssfonts-context","cssgrids-context"],type:"css"},cssbutton:{type:"css"},cssfonts:{type:"css"},"cssfonts-context":{type:"css"},cssgrids:{optional:["cssnormalize"],type:"css"},"cssgrids-base":{optional:["cssnormalize"],type:"css"},"cssgrids-responsive":{optional:["cssnormalize"],requires:["cssgrids","cssgrids-responsive-base"],type:"css"},"cssgrids-units":{optional:["cssnormalize"],requires:["cssgrids-base"],type:"css"},cssnormalize:{type:"css"},"cssnormalize-context":{type:"css"},cssreset:{type:"css"},"cssreset-context":{type:"css"},dataschema:{use:["dataschema-base","dataschema-json","dataschema-xml","dataschema-array","dataschema-text"]},"dataschema-array":{requires:["dataschema-base"]},"dataschema-base":{requires:["base"]},"dataschema-json":{requires:["dataschema-base","json"]},"dataschema-text":{requires:["dataschema-base"]},"dataschema-xml":{requires:["dataschema-base"]},datasource:{use:["datasource-local","datasource-io","datasource-get","datasource-function","datasource-cache","datasource-jsonschema","datasource-xmlschema","datasource-arrayschema"
+,"datasource-textschema","datasource-polling"]},"datasource-arrayschema":{requires:["datasource-local","plugin","dataschema-array"]},"datasource-cache":{requires:["datasource-local","plugin","cache-base"]},"datasource-function":{requires:["datasource-local"]},"datasource-get":{requires:["datasource-local","get"]},"datasource-io":{requires:["datasource-local","io-base"]},"datasource-jsonschema":{requires:["datasource-local","plugin","dataschema-json"]},"datasource-local":{requires:["base"]},"datasource-polling":{requires:["datasource-local"]},"datasource-textschema":{requires:["datasource-local","plugin","dataschema-text"]},"datasource-xmlschema":{requires:["datasource-local","plugin","datatype-xml","dataschema-xml"]},datatable:{use:["datatable-core","datatable-table","datatable-head","datatable-body","datatable-base","datatable-column-widths","datatable-message","datatable-mutable","datatable-sort","datatable-datasource"]},"datatable-base":{requires:["datatable-core","datatable-table","datatable-head","datatable-body","base-build","widget"],skinnable:!0},"datatable-body":{requires:["datatable-core","view","classnamemanager"]},"datatable-column-widths":{requires:["datatable-base"]},"datatable-core":{requires:["escape","model-list","node-event-delegate"]},"datatable-datasource":{requires:["datatable-base","plugin","datasource-local"]},"datatable-foot":{requires:["datatable-core","view"]},"datatable-formatters":{requires:["datatable-body","datatype-number-format","datatype-date-format","escape"]},"datatable-head":{requires:["datatable-core","view","classnamemanager"]},"datatable-highlight":{requires:["datatable-base","event-hover"],skinnable:!0},"datatable-keynav":{requires:["datatable-base"]},"datatable-message":{lang:["en","fr","es","hu","it"],requires:["datatable-base"],skinnable:!0},"datatable-mutable":{requires:["datatable-base"]},"datatable-paginator":{lang:["en","fr"],requires:["model","view","paginator-core","datatable-foot","datatable-paginator-templates"],skinnable:!0},"datatable-paginator-templates":{requires:["template"]},"datatable-scroll":{requires:["datatable-base","datatable-column-widths","dom-screen"],skinnable:!0},"datatable-sort":{lang:["en","fr","es","hu"],requires:["datatable-base"],skinnable:!0},"datatable-table":{requires:["datatable-core","datatable-head","datatable-body","view","classnamemanager"]},datatype:{use:["datatype-date","datatype-number","datatype-xml"]},"datatype-date":{use:["datatype-date-parse","datatype-date-format","datatype-date-math"]},"datatype-date-format":{lang:["ar","ar-JO","ca","ca-ES","da","da-DK","de","de-AT","de-DE","el","el-GR","en","en-AU","en-CA","en-GB","en-IE","en-IN","en-JO","en-MY","en-NZ","en-PH","en-SG","en-US","es","es-AR","es-BO","es-CL","es-CO","es-EC","es-ES","es-MX","es-PE","es-PY","es-US","es-UY","es-VE","fi","fi-FI","fr","fr-BE","fr-CA","fr-FR","hi","hi-IN","hu","id","id-ID","it","it-IT","ja","ja-JP","ko","ko-KR","ms","ms-MY","nb","nb-NO","nl","nl-BE","nl-NL","pl","pl-PL","pt","pt-BR","ro","ro-RO","ru","ru-RU","sv","sv-SE","th","th-TH","tr","tr-TR","vi","vi-VN","zh-Hans","zh-Hans-CN","zh-Hant","zh-Hant-HK","zh-Hant-TW"]},"datatype-date-math":{requires:["yui-base"]},"datatype-date-parse":{},"datatype-number":{use:["datatype-number-parse","datatype-number-format"]},"datatype-number-format":{},"datatype-number-parse":{requires:["escape"]},"datatype-xml":{use:["datatype-xml-parse","datatype-xml-format"]},"datatype-xml-format":{},"datatype-xml-parse":{},dd:{use:["dd-ddm-base","dd-ddm","dd-ddm-drop","dd-drag","dd-proxy","dd-constrain","dd-drop","dd-scroll","dd-delegate"]},"dd-constrain":{requires:["dd-drag"]},"dd-ddm":{requires:["dd-ddm-base","event-resize"]},"dd-ddm-base":{requires:["node","base","yui-throttle","classnamemanager"]},"dd-ddm-drop":{requires:["dd-ddm"]},"dd-delegate":{requires:["dd-drag","dd-drop-plugin","event-mouseenter"]},"dd-drag":{requires:["dd-ddm-base"]},"dd-drop":{requires:["dd-drag","dd-ddm-drop"]},"dd-drop-plugin":{requires:["dd-drop"]},"dd-gestures":{condition:{name:"dd-gestures",trigger:"dd-drag",ua:"touchEnabled"},requires:["dd-drag","event-synthetic","event-gestures"]},"dd-plugin":{optional:["dd-constrain","dd-proxy"],requires:["dd-drag"]},"dd-proxy":{requires:["dd-drag"]},"dd-scroll":{requires:["dd-drag"]},dial:{lang:["en","es","hu"],requires:["widget","dd-drag","event-mouseenter","event-move","event-key","transition","intl"],skinnable:!0},dom:{use:["dom-base","dom-screen","dom-style","selector-native","selector"]},"dom-base":{requires:["dom-core"]},"dom-core":{requires:["oop","features"]},"dom-screen":{requires:["dom-base","dom-style"]},"dom-style":{requires:["dom-base"]},"dom-style-ie":{condition:{name:"dom-style-ie",test:function(e){var t=e.Features.test,n=e.Features.add,r=e.config.win,i=e.config.doc,s="documentElement",o=!1;return n("style","computedStyle",{test:function(){return r&&"getComputedStyle"in r}}),n("style","opacity",{test:function(){return i&&"opacity"in i[s].style}}),o=!t("style","opacity")&&!t("style","computedStyle"),o},trigger:"dom-style"},requires:["dom-style","color-base"]},dump:{requires:["yui-base"]},editor:{use:["frame","editor-selection","exec-command","editor-base","editor-para","editor-br","editor-bidi","editor-tab","createlink-base"]},"editor-base":{requires:["base","frame","node","exec-command","editor-selection"]},"editor-bidi":{requires:["editor-base"]},"editor-br":{requires:["editor-base"]},"editor-inline":{requires:["editor-base","content-editable"]},"editor-lists":{requires:["editor-base"]},"editor-para":{requires:["editor-para-base"]},"editor-para-base":{requires:["editor-base"]},"editor-para-ie":{condition:{name:"editor-para-ie",trigger:"editor-para",ua:"ie",when:"instead"},requires:["editor-para-base"]},"editor-selection":{requires:["node"]},"editor-tab":{requires:["editor-base"]},escape:{requires:["yui-base"]},event:{after:["node-base"],use:["event-base","event-delegate","event-synthetic","event-mousewheel","event-mouseenter","event-key","event-focus","event-resize"
+,"event-hover","event-outside","event-touch","event-move","event-flick","event-valuechange","event-tap"]},"event-base":{after:["node-base"],requires:["event-custom-base"]},"event-base-ie":{after:["event-base"],condition:{name:"event-base-ie",test:function(e){var t=e.config.doc&&e.config.doc.implementation;return t&&!t.hasFeature("Events","2.0")},trigger:"node-base"},requires:["node-base"]},"event-contextmenu":{requires:["event-synthetic","dom-screen"]},"event-custom":{use:["event-custom-base","event-custom-complex"]},"event-custom-base":{requires:["oop"]},"event-custom-complex":{requires:["event-custom-base"]},"event-delegate":{requires:["node-base"]},"event-flick":{requires:["node-base","event-touch","event-synthetic"]},"event-focus":{requires:["event-synthetic"]},"event-gestures":{use:["event-flick","event-move"]},"event-hover":{requires:["event-mouseenter"]},"event-key":{requires:["event-synthetic"]},"event-mouseenter":{requires:["event-synthetic"]},"event-mousewheel":{requires:["node-base"]},"event-move":{requires:["node-base","event-touch","event-synthetic"]},"event-outside":{requires:["event-synthetic"]},"event-resize":{requires:["node-base","event-synthetic"]},"event-simulate":{requires:["event-base"]},"event-synthetic":{requires:["node-base","event-custom-complex"]},"event-tap":{requires:["node-base","event-base","event-touch","event-synthetic"]},"event-touch":{requires:["node-base"]},"event-valuechange":{requires:["event-focus","event-synthetic"]},"exec-command":{requires:["frame"]},features:{requires:["yui-base"]},file:{requires:["file-flash","file-html5"]},"file-flash":{requires:["base"]},"file-html5":{requires:["base"]},frame:{requires:["base","node","plugin","selector-css3","yui-throttle"]},"gesture-simulate":{requires:["async-queue","event-simulate","node-screen"]},get:{requires:["yui-base"]},graphics:{requires:["node","event-custom","pluginhost","matrix","classnamemanager"]},"graphics-canvas":{condition:{name:"graphics-canvas",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"},requires:["graphics","color-base"]},"graphics-canvas-default":{condition:{name:"graphics-canvas-default",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"}},"graphics-group":{requires:["graphics"]},"graphics-svg":{condition:{name:"graphics-svg",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"},requires:["graphics"]},"graphics-svg-default":{condition:{name:"graphics-svg-default",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"}},"graphics-vml":{condition:{name:"graphics-vml",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"},requires:["graphics","color-base"]},"graphics-vml-default":{condition:{name:"graphics-vml-default",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"}},handlebars:{use:["handlebars-compiler"]},"handlebars-base":{requires:[]},"handlebars-compiler":{requires:["handlebars-base"]},highlight:{use:["highlight-base","highlight-accentfold"]},"highlight-accentfold":{requires:["highlight-base","text-accentfold"]},"highlight-base":{requires:["array-extras","classnamemanager","escape","text-wordbreak"]},history:{use:["history-base","history-hash","history-html5"]},"history-base":{requires:["event-custom-complex"]},"history-hash":{after:["history-html5"],requires:["event-synthetic","history-base","yui-later"]},"history-hash-ie":{condition:{name:"history-hash-ie",test:function(e){var t=e.config.doc&&e.config.doc.documentMode;return e.UA.ie&&(!("onhashchange"in e.config.win)||!t||t<8)},trigger:"history-hash"},requires:["history-hash","node-base"]},"history-html5":{optional:["json"],requires:["event-base","history-base","node-base"]},imageloader:{requires:["base-base","node-style","node-screen"]},intl:{requires:["intl-base","event-custom"]},"intl-base":{requires:["yui-base"]},io:{use:["io-base","io-xdr","io-form","io-upload-iframe","io-queue"]},"io-base":{requires:["event-custom-base","querystring-stringify-simple"]},"io-form":{requires:["io-base","node-base"]},"io-nodejs":{condition:{name:"io-nodejs",trigger:"io-base",ua:"nodejs"},requires:["io-base"]},"io-queue":{requires:["io-base","queue-promote"]},"io-upload-iframe":{requires:["io-base","node-base"]},"io-xdr":{requires:["io-base","datatype-xml-parse"]},json:{use:["json-parse","json-stringify"]},"json-parse":{requires:["yui-base"]},"json-parse-shim":{condition:{name:"json-parse-shim",test:function(e){function i(e,t){return e==="ok"?!0:t}var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONParse!==!1&&!!n;if(r)try{r=n.parse('{"ok":false}',i).ok}catch(s){r=!1}return!r},trigger:"json-parse"},requires:["json-parse"]},"json-stringify":{requires:["yui-base"]},"json-stringify-shim":{condition:{name:"json-stringify-shim"
+,test:function(e){var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONStringify!==!1&&!!n;if(r)try{r="0"===n.stringify(0)}catch(i){r=!1}return!r},trigger:"json-stringify"},requires:["json-stringify"]},jsonp:{requires:["get","oop"]},"jsonp-url":{requires:["jsonp"]},"lazy-model-list":{requires:["model-list"]},loader:{use:["loader-base","loader-rollup","loader-yui3"]},"loader-base":{requires:["get","features"]},"loader-rollup":{requires:["loader-base"]},"loader-yui3":{requires:["loader-base"]},matrix:{requires:["yui-base"]},model:{requires:["base-build","escape","json-parse"]},"model-list":{requires:["array-extras","array-invoke","arraylist","base-build","escape","json-parse","model"]},"model-sync-local":{requires:["model","json-stringify"]},"model-sync-rest":{requires:["model","io-base","json-stringify"]},node:{use:["node-base","node-event-delegate","node-pluginhost","node-screen","node-style"]},"node-base":{requires:["event-base","node-core","dom-base","dom-style"]},"node-core":{requires:["dom-core","selector"]},"node-event-delegate":{requires:["node-base","event-delegate"]},"node-event-html5":{requires:["node-base"]},"node-event-simulate":{requires:["node-base","event-simulate","gesture-simulate"]},"node-flick":{requires:["classnamemanager","transition","event-flick","plugin"],skinnable:!0},"node-focusmanager":{requires:["attribute","node","plugin","node-event-simulate","event-key","event-focus"]},"node-load":{requires:["node-base","io-base"]},"node-menunav":{requires:["node","classnamemanager","plugin","node-focusmanager"],skinnable:!0},"node-pluginhost":{requires:["node-base","pluginhost"]},"node-screen":{requires:["dom-screen","node-base"]},"node-scroll-info":{requires:["array-extras","base-build","event-resize","node-pluginhost","plugin","selector"]},"node-style":{requires:["dom-style","node-base"]},oop:{requires:["yui-base"]},overlay:{requires:["widget","widget-stdmod","widget-position","widget-position-align","widget-stack","widget-position-constrain"],skinnable:!0},paginator:{requires:["paginator-core"]},"paginator-core":{requires:["base"]},"paginator-url":{requires:["paginator"]},panel:{requires:["widget","widget-autohide","widget-buttons","widget-modality","widget-position","widget-position-align","widget-position-constrain","widget-stack","widget-stdmod"],skinnable:!0},parallel:{requires:["yui-base"]},pjax:{requires:["pjax-base","pjax-content"]},"pjax-base":{requires:["classnamemanager","node-event-delegate","router"]},"pjax-content":{requires:["io-base","node-base","router"]},"pjax-plugin":{requires:["node-pluginhost","pjax","plugin"]},plugin:{requires:["base-base"]},pluginhost:{use:["pluginhost-base","pluginhost-config"]},"pluginhost-base":{requires:["yui-base"]},"pluginhost-config":{requires:["pluginhost-base"]},promise:{requires:["timers"]},querystring:{use:["querystring-parse","querystring-stringify"]},"querystring-parse":{requires:["yui-base","array-extras"]},"querystring-parse-simple":{requires:["yui-base"]},"querystring-stringify":{requires:["yui-base"]},"querystring-stringify-simple":{requires:["yui-base"]},"queue-promote":{requires:["yui-base"]},"range-slider":{requires:["slider-base","slider-value-range","clickable-rail"]},recordset:{use:["recordset-base","recordset-sort","recordset-filter","recordset-indexer"]},"recordset-base":{requires:["base","arraylist"]},"recordset-filter":{requires:["recordset-base","array-extras","plugin"]},"recordset-indexer":{requires:["recordset-base","plugin"]},"recordset-sort":{requires:["arraysort","recordset-base","plugin"]},resize:{use:["resize-base","resize-proxy","resize-constrain"]},"resize-base":{requires:["base","widget","event","oop","dd-drag","dd-delegate","dd-drop"],skinnable:!0},"resize-constrain":{requires:["plugin","resize-base"]},"resize-plugin":{optional:["resize-constrain"],requires:["resize-base","plugin"]},"resize-proxy":{requires:["plugin","resize-base"]},router:{optional:["querystring-parse"],requires:["array-extras","base-build","history"]},scrollview:{requires:["scrollview-base","scrollview-scrollbars"]},"scrollview-base":{requires:["widget","event-gestures","event-mousewheel","transition"],skinnable:!0},"scrollview-base-ie":{condition:{name:"scrollview-base-ie",trigger:"scrollview-base",ua:"ie"},requires:["scrollview-base"]},"scrollview-list":{requires:["plugin","classnamemanager"],skinnable:!0},"scrollview-paginator":{requires:["plugin","classnamemanager"]},"scrollview-scrollbars":{requires:["classnamemanager","transition","plugin"],skinnable:!0},selector:{requires:["selector-native"]},"selector-css2":{condition:{name:"selector-css2",test:function(e){var t=e.config.doc,n=t&&!("querySelectorAll"in t);return n},trigger:"selector"},requires:["selector-native"]},"selector-css3":{requires:["selector-native","selector-css2"]},"selector-native":{requires:["dom-base"]},"series-area":{requires:["series-cartesian","series-fill-util"]},"series-area-stacked":{requires:["series-stacked","series-area"]},"series-areaspline":{requires:["series-area","series-curve-util"]},"series-areaspline-stacked":{requires:["series-stacked","series-areaspline"]},"series-bar":{requires:["series-marker","series-histogram-base"]},"series-bar-stacked":{requires:["series-stacked","series-bar"]},"series-base":{requires:["graphics","axis-base"]},"series-candlestick":{requires:["series-range"]},"series-cartesian":{requires:["series-base"]},"series-column":{requires:["series-marker","series-histogram-base"]},"series-column-stacked":{requires:["series-stacked","series-column"]},"series-combo":{requires:["series-cartesian","series-line-util","series-plot-util","series-fill-util"]},"series-combo-stacked":{requires:["series-stacked","series-combo"]},"series-combospline":{requires:["series-combo","series-curve-util"]},"series-combospline-stacked":{requires:["series-combo-stacked","series-curve-util"]},"series-curve-util":{},"series-fill-util":{},"series-histogram-base":{requires:["series-cartesian","series-plot-util"
+]},"series-line":{requires:["series-cartesian","series-line-util"]},"series-line-stacked":{requires:["series-stacked","series-line"]},"series-line-util":{},"series-marker":{requires:["series-cartesian","series-plot-util"]},"series-marker-stacked":{requires:["series-stacked","series-marker"]},"series-ohlc":{requires:["series-range"]},"series-pie":{requires:["series-base","series-plot-util"]},"series-plot-util":{},"series-range":{requires:["series-cartesian"]},"series-spline":{requires:["series-line","series-curve-util"]},"series-spline-stacked":{requires:["series-stacked","series-spline"]},"series-stacked":{requires:["axis-stacked"]},"shim-plugin":{requires:["node-style","node-pluginhost"]},slider:{use:["slider-base","slider-value-range","clickable-rail","range-slider"]},"slider-base":{requires:["widget","dd-constrain","event-key"],skinnable:!0},"slider-value-range":{requires:["slider-base"]},sortable:{requires:["dd-delegate","dd-drop-plugin","dd-proxy"]},"sortable-scroll":{requires:["dd-scroll","sortable"]},stylesheet:{requires:["yui-base"]},substitute:{optional:["dump"],requires:["yui-base"]},swf:{requires:["event-custom","node","swfdetect","escape"]},swfdetect:{requires:["yui-base"]},tabview:{requires:["widget","widget-parent","widget-child","tabview-base","node-pluginhost","node-focusmanager"],skinnable:!0},"tabview-base":{requires:["node-event-delegate","classnamemanager"]},"tabview-plugin":{requires:["tabview-base"]},template:{use:["template-base","template-micro"]},"template-base":{requires:["yui-base"]},"template-micro":{requires:["escape"]},test:{requires:["event-simulate","event-custom","json-stringify"]},"test-console":{requires:["console-filters","test","array-extras"],skinnable:!0},text:{use:["text-accentfold","text-wordbreak"]},"text-accentfold":{requires:["array-extras","text-data-accentfold"]},"text-data-accentfold":{requires:["yui-base"]},"text-data-wordbreak":{requires:["yui-base"]},"text-wordbreak":{requires:["array-extras","text-data-wordbreak"]},timers:{requires:["yui-base"]},transition:{requires:["node-style"]},"transition-timer":{condition:{name:"transition-timer",test:function(e){var t=e.config.doc,n=t?t.documentElement:null,r=!0;return n&&n.style&&(r=!("MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style)),r},trigger:"transition"},requires:["transition"]},tree:{requires:["base-build","tree-node"]},"tree-labelable":{requires:["tree"]},"tree-lazy":{requires:["base-pluginhost","plugin","tree"]},"tree-node":{},"tree-openable":{requires:["tree"]},"tree-selectable":{requires:["tree"]},"tree-sortable":{requires:["tree"]},uploader:{requires:["uploader-html5","uploader-flash"]},"uploader-flash":{requires:["swfdetect","escape","widget","base","cssbutton","node","event-custom","uploader-queue"]},"uploader-html5":{requires:["widget","node-event-simulate","file-html5","uploader-queue"]},"uploader-queue":{requires:["base"]},view:{requires:["base-build","node-event-delegate"]},"view-node-map":{requires:["view"]},widget:{use:["widget-base","widget-htmlparser","widget-skin","widget-uievents"]},"widget-anim":{requires:["anim-base","plugin","widget"]},"widget-autohide":{requires:["base-build","event-key","event-outside","widget"]},"widget-base":{requires:["attribute","base-base","base-pluginhost","classnamemanager","event-focus","node-base","node-style"],skinnable:!0},"widget-base-ie":{condition:{name:"widget-base-ie",trigger:"widget-base",ua:"ie"},requires:["widget-base"]},"widget-buttons":{requires:["button-plugin","cssbutton","widget-stdmod"]},"widget-child":{requires:["base-build","widget"]},"widget-htmlparser":{requires:["widget-base"]},"widget-modality":{requires:["base-build","event-outside","widget"],skinnable:!0},"widget-parent":{requires:["arraylist","base-build","widget"]},"widget-position":{requires:["base-build","node-screen","widget"]},"widget-position-align":{requires:["widget-position"]},"widget-position-constrain":{requires:["widget-position"]},"widget-skin":{requires:["widget-base"]},"widget-stack":{requires:["base-build","widget"],skinnable:!0},"widget-stdmod":{requires:["base-build","widget"]},"widget-uievents":{requires:["node-event-delegate","widget-base"]},yql:{requires:["oop"]},"yql-jsonp":{condition:{name:"yql-jsonp",test:function(e){return!e.UA.nodejs&&!e.UA.winjs},trigger:"yql"},requires:["yql","jsonp","jsonp-url"]},"yql-nodejs":{condition:{name:"yql-nodejs",trigger:"yql",ua:"nodejs"},requires:["yql"]},"yql-winjs":{condition:{name:"yql-winjs",trigger:"yql",ua:"winjs"},requires:["yql"]},yui:{},"yui-base":{},"yui-later":{requires:["yui-base"]},"yui-log":{requires:["yui-base"]},"yui-throttle":{requires:["yui-base"]}}),YUI.Env[e.version].md5="45357bb11eddf7fd0a89c0b756599df2"},"@VERSION@",{requires:["loader-base"]}),YUI.add("loader",function(e,t){},"@VERSION@",{use:["loader-base","loader-rollup","loader-yui3"]});
diff --git a/js/yui3/matrix/matrix-min.js b/js/yui3/matrix/matrix-min.js
new file mode 100644
index 000000000..a9c7e4640
--- /dev/null
+++ b/js/yui3/matrix/matrix-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("matrix",function(e,t){var n={_rounder:1e5,_round:function(e){return e=Math.round(e*n._rounder)/n._rounder,e},rad2deg:function(e){var t=e*(180/Math.PI);return t},deg2rad:function(e){var t=e*(Math.PI/180);return t},angle2rad:function(e){return typeof e=="string"&&e.indexOf("rad")>-1?e=parseFloat(e):e=n.deg2rad(parseFloat(e)),e},convertTransformToArray:function(e){var t=[[e.a,e.c,e.dx],[e.b,e.d,e.dy],[0,0,1]];return t},getDeterminant:function(e){var t=0,r=e.length,i=0,s;if(r==2)return e[0][0]*e[1][1]-e[0][1]*e[1][0];for(;i<r;++i)s=e[i][0],i%2===0||i===0?t+=s*n.getDeterminant(n.getMinors(e,i,0)):t-=s*n.getDeterminant(n.getMinors(e,i,0));return t},inverse:function(e){var t=0,r=e.length,i=0,s,o,u=[],a=[];if(r===2)t=e[0][0]*e[1][1]-e[0][1]*e[1][0],o=[[e[1][1]*t,-e[1][0]*t],[-e[0][1]*t,e[0][0]*t]];else{t=n.getDeterminant(e);for(;i<r;++i){u[i]=[];for(s=0;s<r;++s)a=n.getMinors(e,s,i),u[i][s]=n.getDeterminant(a),(i+s)%2!==0&&i+s!==0&&(u[i][s]*=-1)}o=n.scalarMultiply(u,1/t)}return o},scalarMultiply:function(e,t){var r=0,i,s=e.length;for(;r<s;++r)for(i=0;i<s;++i)e[r][i]=n._round(e[r][i]*t);return e},transpose:function(e){var t=e.length,n=0,r=0,i=[];for(;n<t;++n){i[n]=[];for(r=0;r<t;++r)i[n].push(e[r][n])}return i},getMinors:function(e,t,n){var r=[],i=e.length,s=0,o,u;for(;s<i;++s)if(s!==t){u=[];for(o=0;o<i;++o)o!==n&&u.push(e[s][o]);r.push(u)}return r},sign:function(e){return e===0?1:e/Math.abs(e)},vectorMatrixProduct:function(e,t){var n,r,i=e.length,s=[],o;for(n=0;n<i;++n){o=0;for(r=0;r<i;++r)o+=e[n]*t[n][r];s[n]=o}return s},decompose:function(e){var t=parseFloat(e[0][0]),r=parseFloat(e[1][0]),i=parseFloat(e[0][1]),s=parseFloat(e[1][1]),o=parseFloat(e[0][2]),u=parseFloat(e[1][2]),a,f,l,c;return t*s-r*i===0?!1:(f=n._round(Math.sqrt(t*t+r*r)),t/=f,r/=f,c=n._round(t*i+r*s),i-=t*c,s-=r*c,l=n._round(Math.sqrt(i*i+s*s)),i/=l,s/=l,c/=l,c=n._round(n.rad2deg(Math.atan(c))),a=n._round(n.rad2deg(Math.atan2(e[1][0],e[0][0]))),[["translate",o,u],["rotate",a],["skewX",c],["scale",f,l]])},getTransformArray:function(e){var t=/\s*([a-z]*)\(([\w,\.,\-,\s]*)\)/gi,r=[],i,s,o,u=n.transformMethods;while(s=t.exec(e))u.hasOwnProperty(s[1])?(i=s[2].split(","),i.unshift(s[1]),r.push(i)):s[1]=="matrix"&&(i=s[2].split(","),o=n.decompose([[i[0],i[2],i[4]],[i[1],i[3],i[5]],[0,0,1]]),r.push(o[0]),r.push(o[1]),r.push(o[2]),r.push(o[3]));return r},getTransformFunctionArray:function(e){var t;switch(e){case"skew":t=[e,0,0];break;case"scale":t=[e,1,1];break;case"scaleX":t=[e,1];break;case"scaleY":t=[e,1];break;case"translate":t=[e,0,0];break;default:t=[e,0]}return t},compareTransformSequence:function(e,t){var n=0,r=e.length,i=t.length,s=r===i;if(s)for(;n<r;++n)if(e[n][0]!=t[n][0]){s=!1;break}return s},transformMethods:{rotate:"rotate",skew:"skew",skewX:"skewX",skewY:"skewY",translate:"translate",translateX:"translateX",translateY:"tranlsateY",scale:"scale",scaleX:"scaleX",scaleY:"scaleY"}};e.MatrixUtil=n;var r=function(e){this.init(e)};r.prototype={_rounder:1e5,multiply:function(e,t,n,r,i,s){var o=this,u=o.a*e+o.c*t,a=o.b*e+o.d*t,f=o.a*n+o.c*r,l=o.b*n+o.d*r,c=o.a*i+o.c*s+o.dx,h=o.b*i+o.d*s+o.dy;return o.a=this._round(u),o.b=this._round(a),o.c=this._round(f),o.d=this._round(l),o.dx=this._round(c),o.dy=this._round(h),this},applyCSSText:function(e){var t=/\s*([a-z]*)\(([\w,\.,\-,\s]*)\)/gi,n,r;e=e.replace(/matrix/g,"multiply");while(r=t.exec(e))typeof this[r[1]]=="function"&&(n=r[2].split(","),this[r[1]].apply(this,n))},getTransformArray:function(e){var t=/\s*([a-z]*)\(([\w,\.,\-,\s]*)\)/gi,n=[],r,i;e=e.replace(/matrix/g,"multiply");while(i=t.exec(e))typeof this[i[1]]=="function"&&(r=i[2].split(","),r.unshift(i[1]),n.push(r));return n},_defaults:{a:1,b:0,c:0,d:1,dx:0,dy:0},_round:function(e){return e=Math.round(e*this._rounder)/this._rounder,e},init:function(e){var t=this._defaults,n;e=e||{};for(n in t)t.hasOwnProperty(n)&&(this[n]=n in e?e[n]:t[n]);this._config=e},scale:function(e,t){return this.multiply(e,0,0,t,0,0),this},skew:function(e,t){return e=e||0,t=t||0,e!==undefined&&(e=Math.tan(this.angle2rad(e))),t!==undefined&&(t=Math.tan(this.angle2rad(t))),this.multiply(1,t,e,1,0,0),this},skewX:function(e){return this.skew(e),this},skewY:function(e){return this.skew(null,e),this},toCSSText:function(){var e=this,t="matrix("+e.a+","+e.b+","+e.c+","+e.d+","+e.dx+","+e.dy+")";return t},toFilterText:function(){var e=this,t="progid:DXImageTransform.Microsoft.Matrix(";return t+="M11="+e.a+","+"M21="+e.b+","+"M12="+e.c+","+"M22="+e.d+","+'sizingMethod="auto expand")',t+="",t},rad2deg:function(e){var t=e*(180/Math.PI);return t},deg2rad:function(e){var t=e*(Math.PI/180);return t},angle2rad:function(e){return typeof e=="string"&&e.indexOf("rad")>-1?e=parseFloat(e):e=this.deg2rad(parseFloat(e)),e},rotate:function(e,t,n){var r=this.angle2rad(e),i=Math.sin(r),s=Math.cos(r);return this.multiply(s,i,0-i,s,0,0),this},translate:function(e,t){return e=parseFloat(e)||0,t=parseFloat(t)||0,this.multiply(1,0,0,1,e,t),this},translateX:function(e){return this.translate(e),this},translateY:function(e){return this.translate(null,e),this},identity:function(){var e=this._config,t=this._defaults,n;for(n in e)n in t&&(this[n]=t[n]);return this},getMatrixArray:function(){var e=this,t=[[e.a,e.c,e.dx],[e.b,e.d,e.dy],[0,0,1]];return t},getContentRect:function(e,t,n,r){var i=isNaN(n)?0:n,s=isNaN(r)?0:r,o=i+e,u=s+t,a=this,f=a.a,l=a.b,c=a.c,h=a.d,p=a.dx,d=a.dy,v=f*i+c*s+p,m=l*i+h*s+d,g=f*o+c*s+p,y=l*o+h*s+d,b=f*i+c*u+p,w=l*i+h*u+d,E=f*o+c*u+p,S=l*o+h*u+d;return{left:Math.min(b,Math.min(v,Math.min(g,E))),right:Math.max(b,Math.max(v,Math.max(g,E))),top:Math.min(y,Math.min(S,Math.min(w,m))),bottom:Math.max(y,Math.max(S,Math.max(w,m)))}},getDeterminant:function(){return e.MatrixUtil.getDeterminant(this.getMatrixArray())},inverse:function(){return e.MatrixUtil.inverse(this.getMatrixArray())},transpose:function(){return e.MatrixUtil.transpose(this.getMatrixArray())},decompose:function(){return e.MatrixUtil.decompose(this.getMatrixArray())}},e.Matrix=r},"3.17.2",{requires:["yui-base"
+]});
diff --git a/js/yui3/model-list/model-list-min.js b/js/yui3/model-list/model-list-min.js
new file mode 100644
index 000000000..3b707459e
--- /dev/null
+++ b/js/yui3/model-list/model-list-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("model-list",function(e,t){function c(){c.superclass.constructor.apply(this,arguments)}var n=e.Attribute.prototype,r=e.Lang,i=e.Array,s="add",o="create",u="error",a="load",f="remove",l="reset";e.ModelList=e.extend(c,e.Base,{model:e.Model,_isYUIModelList:!0,initializer:function(t){t||(t={});var n=this.model=t.model||this.model;typeof n=="string"&&(this.model=e.Object.getValue(e,n.split(".")),this.model||e.error("ModelList: Model class not found: "+n)),this.publish(s,{defaultFn:this._defAddFn}),this.publish(l,{defaultFn:this._defResetFn}),this.publish(f,{defaultFn:this._defRemoveFn}),this.after("*:idChange",this._afterIdChange),this._clear(),t.items&&this.add(t.items,{silent:!0})},destructor:function(){this._clear()},add:function(t,n){var s=t._isYUIModelList;return s||r.isArray(t)?i.map(s?t.toArray():t,function(t,r){var i=n||{};return"index"in i&&(i=e.merge(i,{index:i.index+r})),this._add(t,i)},this):this._add(t,n)},create:function(t,n,r){var i=this;return typeof n=="function"&&(r=n,n={}),n||(n={}),t._isYUIModel||(t=new this.model(t)),i.fire(o,e.merge(n,{model:t})),t.save(n,function(e){e||i.add(t,n),r&&r.apply(null,arguments)})},each:function(e,t){var n=this._items.concat(),r,i,s;for(r=0,s=n.length;r<s;r++)i=n[r],e.call(t||i,i,r,this);return this},filter:function(e,t){var n=[],r=this._items,i,s,o,u;typeof e=="function"&&(t=e,e={});for(i=0,o=r.length;i<o;++i)s=r[i],t.call(this,s,i,this)&&n.push(s);return e.asList?(u=new this.constructor({model:this.model}),n.length&&u.add(n,{silent:!0}),u):n},get:function(e){return this.attrAdded(e)?n.get.apply(this,arguments):this.invoke("get",e)},getAsHTML:function(t){return this.attrAdded(t)?e.Escape.html(n.get.apply(this,arguments)):this.invoke("getAsHTML",t)},getAsURL:function(e){return this.attrAdded(e)?encodeURIComponent(n.get.apply(this,arguments)):this.invoke("getAsURL",e)},getByClientId:function(e){return this._clientIdMap[e]||null},getById:function(e){return this._idMap[e]||null},invoke:function(e){var t=[this._items,e].concat(i(arguments,1,!0));return i.invoke.apply(i,t)},load:function(e,t){var n=this;return typeof e=="function"&&(t=e,e={}),e||(e={}),this.sync("read",e,function(r,i){var s={options:e,response:i},o;r?(s.error=r,s.src="load",n.fire(u,s)):(n._loadEvent||(n._loadEvent=n.publish(a,{preventable:!1})),o=s.parsed=n._parse(i),n.reset(o,e),n.fire(a,s)),t&&t.apply(null,arguments)}),this},map:function(e,t){return i.map(this._items,e,t)},parse:function(t){if(typeof t=="string")try{return e.JSON.parse(t)||[]}catch(n){return this.fire(u,{error:n,response:t,src:"parse"}),null}return t||[]},remove:function(e,t){var n=e._isYUIModelList;return n||r.isArray(e)?(e=i.map(n?e.toArray():e,function(e){return r.isNumber(e)?this.item(e):e},this),i.map(e,function(e){return this._remove(e,t)},this)):this._remove(e,t)},reset:function(t,n){t||(t=[]),n||(n={});var r=e.merge({src:"reset"},n);return t._isYUIModelList?t=t.toArray():t=i.map(t,function(e){return e._isYUIModel?e:new this.model(e)},this),r.models=t,n.silent?this._defResetFn(r):(this.comparator&&t.sort(e.bind(this._sort,this)),this.fire(l,r)),this},some:function(e,t){var n=this._items.concat(),r,i,s;for(r=0,s=n.length;r<s;r++){i=n[r];if(e.call(t||i,i,r,this))return!0}return!1},sort:function(t){if(!this.comparator)return this;var n=this._items.concat(),r;return t||(t={}),n.sort(e.rbind(this._sort,this,t)),r=e.merge(t,{models:n,src:"sort"}),t.silent?this._defResetFn(r):this.fire(l,r),this},sync:function(){var e=i(arguments,0,!0).pop();typeof e=="function"&&e()},toArray:function(){return this._items.concat()},toJSON:function(){return this.map(function(e){return e.toJSON()})},_add:function(t,n){var i,o;n||(n={}),t._isYUIModel||(t=new this.model(t)),o=t.get("id");if(this._clientIdMap[t.get("clientId")]||r.isValue(o)&&this._idMap[o]){this.fire(u,{error:"Model is already in the list.",model:t,src:"add"});return}return i=e.merge(n,{index:"index"in n?n.index:this._findIndex(t),model:t}),n.silent?this._defAddFn(i):this.fire(s,i),t},_attachList:function(e){e.lists.push(this),e.addTarget(this)},_clear:function(){i.each(this._items,this._detachList,this),this._clientIdMap={},this._idMap={},this._items=[]},_compare:function(e,t){return e<t?-1:e>t?1:0},_detachList:function(e){var t=i.indexOf(e.lists,this);t>-1&&(e.lists.splice(t,1),e.removeTarget(this))},_findIndex:function(e){var t=this._items,n=t.length,r=0,i,s,o;if(!this.comparator||!n)return n;o=this.comparator(e);while(r<n)s=r+n>>1,i=t[s],this._compare(this.comparator(i),o)<0?r=s+1:n=s;return r},_parse:function(e){return this.parse(e)},_remove:function(t,n){var i,s;n||(n={}),r.isNumber(t)?(i=t,t=this.item(i)):i=this.indexOf(t);if(i===-1||!t){this.fire(u,{error:"Model is not in the list.",index:i,model:t,src:"remove"});return}return s=e.merge(n,{index:i,model:t}),n.silent?this._defRemoveFn(s):this.fire(f,s),t},_sort:function(e,t,n){var r=this._compare(this.comparator(e),this.comparator(t));return r===0?r:n&&n.descending?-r:r},_afterIdChange:function(e){var t=e.newVal,n=e.prevVal,i=e.target;if(r.isValue(n)){if(this._idMap[n]!==i)return;delete this._idMap[n]}else if(this.indexOf(i)===-1)return;r.isValue(t)&&(this._idMap[t]=i)},_defAddFn:function(e){var t=e.model,n=t.get("id");this._clientIdMap[t.get("clientId")]=t,r.isValue(n)&&(this._idMap[n]=t),this._attachList(t),this._items.splice(e.index,0,t)},_defRemoveFn:function(e){var t=e.model,n=t.get("id");this._detachList(t),delete this._clientIdMap[t.get("clientId")],r.isValue(n)&&delete this._idMap[n],this._items.splice(e.index,1)},_defResetFn:function(e){if(e.src==="sort"){this._items=e.models.concat();return}this._clear(),e.models.length&&this.add(e.models,{silent:!0})}},{NAME:"modelList"}),e.augment(c,e.ArrayList)},"3.17.2",{requires:["array-extras","array-invoke","arraylist","base-build","escape","json-parse","model"]});
diff --git a/js/yui3/model-sync-local/model-sync-local-min.js b/js/yui3/model-sync-local/model-sync-local-min.js
new file mode 100644
index 000000000..3c8fc773e
--- /dev/null
+++ b/js/yui3/model-sync-local/model-sync-local-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("model-sync-local",function(e,t){function n(){}n._NON_ATTRS_CFG=["root"],n._hasLocalStorage=function(){var t=e.config.win.localStorage,n=e.guid();try{return t.setItem(n,n),t.removeItem(n),!0}catch(r){return!1}}(),n._data=n._data||{},n._store=n._store||{},n.prototype={root:"",storage:null,initializer:function(t){var r,i;t||(t={}),"root"in t&&(this.root=t.root||""),!this.root&&this.model&&this.model.prototype.root&&(this.root=this.model.prototype.root),n._hasLocalStorage&&(this.storage=e.config.win.localStorage,r=this.storage.getItem(this.root)),r?(n._store[this.root]=r.split("|")||[],e.Array.each(n._store[this.root],function(t){n._data[t]=e.JSON.parse(this.storage.getItem(t))},this)):n._store[this.root]||(n._store[this.root]=[])},sync:function(e,t,n){t||(t={});var r,i;try{switch(e){case"read":this._isYUIModelList?r=this._index(t):r=this._show(t);break;case"create":r=this._create(t);break;case"update":r=this._update(t);break;case"delete":r=this._destroy(t)}}catch(s){i=s.message}r?n(null,r):i?n(i):n("Data not found in LocalStorage")},generateID:function(t){return e.guid(t+"_")},_index:function(){var t=n._store[this.root],r=e.Array.map(t,function(e){return n._data[e]});return r},_show:function(){return n._data[this.get("id")]||null},_create:function(){var t=this.toJSON();return t.id=this.generateID(this.root),n._data[t.id]=t,this.storage&&this.storage.setItem(t.id,e.JSON.stringify(t)),n._store[this.root].push(t.id),this._save(),t},_update:function(){var t=this.toJSON(),r=this.get("id");return n._data[r]=t,this.storage&&this.storage.setItem(r,t),e.Array.indexOf(n._store[this.root],r)===-1&&n._store[this.root].push(r),this._save(),t},_destroy:function(){var t=this.get("id"),r=this.storage;if(!n._data[t])return;return delete n._data[t],r&&r.removeItem(t),n._store[this.root]=e.Array.filter(n._store[this.root],function(e){return e.id!=t}),this._save(),this.toJSON()},_save:function(){n._hasLocalStorage&&this.storage&&this.storage.setItem(this.root,n._store[this.root].join("|"))}},e.namespace("ModelSync").Local=n},"3.17.2",{requires:["model","json-stringify"]});
diff --git a/js/yui3/model-sync-rest/model-sync-rest-min.js b/js/yui3/model-sync-rest/model-sync-rest-min.js
new file mode 100644
index 000000000..9f6c7c8f9
--- /dev/null
+++ b/js/yui3/model-sync-rest/model-sync-rest-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("model-sync-rest",function(e,t){function r(){}var n=e.Lang;r.CSRF_TOKEN=YUI.Env.CSRF_TOKEN,r.EMULATE_HTTP=!1,r.HTTP_HEADERS={Accept:"application/json","Content-Type":"application/json"},r.HTTP_METHODS={create:"POST",read:"GET",update:"PUT","delete":"DELETE"},r.HTTP_TIMEOUT=3e4,r._NON_ATTRS_CFG=["root","url"],r.prototype={root:"",url:"",initializer:function(e){e||(e={}),"root"in e&&(this.root=e.root||""),"url"in e&&(this.url=e.url||"")},getURL:function(t,n){var r=this.root,i=this.url;return this._isYUIModelList?i?this._substituteURL(i,e.merge(this.getAttrs(),n)):this.model.prototype.root:r&&(t==="create"||this.isNew())?r:i?this._substituteURL(i,e.merge(this.getAttrs(),n)):this._joinURL(this.getAsURL("id")||"")},parseIOResponse:function(e){return e.responseText},serialize:function(t){return e.JSON.stringify(this)},sync:function(t,n,i){n||(n={});var s=this.getURL(t,n),o=r.HTTP_METHODS[t],u=e.merge(r.HTTP_HEADERS,n.headers),a=n.timeout||r.HTTP_TIMEOUT,f=n.csrfToken||r.CSRF_TOKEN,l;o==="POST"||o==="PUT"?l=this.serialize(t):delete u["Content-Type"],r.EMULATE_HTTP&&(o==="PUT"||o==="DELETE")&&(u["X-HTTP-Method-Override"]=o,o="POST"),f&&(o==="POST"||o==="PUT"||o==="DELETE")&&(u["X-CSRF-Token"]=f),this._sendSyncIORequest({action:t,callback:i,entity:l,headers:u,method:o,timeout:a,url:s})},_joinURL:function(e){var t=this.root;return!t&&!e?"":(e.charAt(0)==="/"&&(e=e.substring(1)),t&&t.charAt(t.length-1)==="/"?t+e+"/":t+"/"+e)},_parse:function(e){return typeof this.parseIOResponse=="function"&&(e=this.parseIOResponse(e)),this.parse(e)},_sendSyncIORequest:function(t){return e.io(t.url,{arguments:{action:t.action,callback:t.callback,url:t.url},context:this,data:t.entity,headers:t.headers,method:t.method,timeout:t.timeout,on:{start:this._onSyncIOStart,failure:this._onSyncIOFailure,success:this._onSyncIOSuccess,end:this._onSyncIOEnd}})},_substituteURL:function(t,r){if(!t)return"";var i={};return e.Object.each(r,function(e,t){if(n.isString(e)||n.isNumber(e))i[t]=encodeURIComponent(e)}),n.sub(t,i)},_onSyncIOEnd:function(e,t){},_onSyncIOFailure:function(e,t,n){var r=n.callback;r&&r({code:t.status,msg:t.statusText},t)},_onSyncIOSuccess:function(e,t,n){var r=n.callback;r&&r(null,t)},_onSyncIOStart:function(e,t){}},e.namespace("ModelSync").REST=r},"3.17.2",{requires:["model","io-base","json-stringify"]});
diff --git a/js/yui3/model/model-min.js b/js/yui3/model/model-min.js
new file mode 100644
index 000000000..f204be701
--- /dev/null
+++ b/js/yui3/model/model-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("model",function(e,t){function l(){l.superclass.constructor.apply(this,arguments)}var n=YUI.namespace("Env.Model"),r=e.Lang,i=e.Array,s=e.Object,o="change",u="error",a="load",f="save";e.Model=e.extend(l,e.Base,{idAttribute:"id",_allowAdHocAttrs:!0,_isYUIModel:!0,initializer:function(e){this.changed={},this.lastChange={},this.lists=[]},destroy:function(e,t){var n=this;return typeof e=="function"&&(t=e,e=null),n.onceAfter("destroy",function(){function r(r){r||i.each(n.lists.concat(),function(t){t.remove(n,e)}),t&&t.apply(null,arguments)}e&&(e.remove||e["delete"])?n.sync("delete",e,r):r()}),l.superclass.destroy.call(n)},generateClientId:function(){return n.lastId||(n.lastId=0),this.constructor.NAME+"_"+(n.lastId+=1)},getAsHTML:function(t){var n=this.get(t);return e.Escape.html(r.isValue(n)?String(n):"")},getAsURL:function(e){var t=this.get(e);return encodeURIComponent(r.isValue(t)?String(t):"")},isModified:function(){return this.isNew()||!s.isEmpty(this.changed)},isNew:function(){return!r.isValue(this.get("id"))},load:function(e,t){var n=this;return typeof e=="function"&&(t=e,e={}),e||(e={}),n.sync("read",e,function(r,i){var s={options:e,response:i},o;r?(s.error=r,s.src="load",n.fire(u,s)):(n._loadEvent||(n._loadEvent=n.publish(a,{preventable:!1})),o=s.parsed=n._parse(i),n.setAttrs(o,e),n.changed={},n.fire(a,s)),t&&t.apply(null,arguments)}),n},parse:function(t){if(typeof t=="string")try{return e.JSON.parse(t)}catch(n){return this.fire(u,{error:n,response:t,src:"parse"}),null}return t},save:function(e,t){var n=this;return typeof e=="function"&&(t=e,e={}),e||(e={}),n._validate(n.toJSON(),function(r){if(r){t&&t.call(null,r);return}n.sync(n.isNew()?"create":"update",e,function(r,i){var s={options:e,response:i},o;r?(s.error=r,s.src="save",n.fire(u,s)):(n._saveEvent||(n._saveEvent=n.publish(f,{preventable:!1})),i&&(o=s.parsed=n._parse(i),n.setAttrs(o,e)),n.changed={},n.fire(f,s)),t&&t.apply(null,arguments)})}),n},set:function(e,t,n){var r={};return r[e]=t,this.setAttrs(r,n)},setAttrs:function(t,n){var r=this.idAttribute,i,u,a,f,l;n=e.merge(n),l=n._transaction={},r!=="id"&&(t=e.merge(t),s.owns(t,r)?t.id=t[r]:s.owns(t,"id")&&(t[r]=t.id));for(a in t)s.owns(t,a)&&this._setAttr(a,t[a],n);if(!s.isEmpty(l)){i=this.changed,f=this.lastChange={};for(a in l)s.owns(l,a)&&(u=l[a],i[a]=u.newVal,f[a]={newVal:u.newVal,prevVal:u.prevVal,src:u.src||null});n.silent||(this._changeEvent||(this._changeEvent=this.publish(o,{preventable:!1})),n.changed=f,this.fire(o,n))}return this},sync:function(){var e=i(arguments,0,!0).pop();typeof e=="function"&&e()},toJSON:function(){var e=this.getAttrs();return delete e.clientId,delete e.destroyed,delete e.initialized,this.idAttribute!=="id"&&delete e.id,e},undo:function(e,t){var n=this.lastChange,r=this.idAttribute,o={},u;return e||(e=s.keys(n)),i.each(e,function(e){s.owns(n,e)&&(e=e===r?"id":e,u=!0,o[e]=n[e].prevVal)}),u?this.setAttrs(o,t):this},validate:function(e,t){t&&t()},addAttr:function(e,t,n){var i=this.idAttribute,s,o;return i&&e===i&&(s=this._isLazyAttr("id")||this._getAttrCfg("id"),o=t.value===t.defaultValue?null:t.value,r.isValue(o)||(o=s.value===s.defaultValue?null:s.value,r.isValue(o)||(o=r.isValue(t.defaultValue)?t.defaultValue:s.defaultValue)),t.value=o,s.value!==o&&(s.value=o,this._isLazyAttr("id")?this._state.add("id","lazy",s):this._state.add("id","value",o))),l.superclass.addAttr.apply(this,arguments)},_parse:function(e){return this.parse(e)},_validate:function(e,t){function i(i){if(r.isValue(i)){n.fire(u,{attributes:e,error:i,src:"validate"}),t(i);return}t()}var n=this;n.validate.length===1?i(n.validate(e,i)):n.validate(e,i)},_setAttrVal:function(e,t,n,r,i,s){var o=l.superclass._setAttrVal.apply(this,arguments),u=i&&i._transaction,a=s&&s.initializing;return o&&u&&!a&&(u[e]={newVal:this.get(e),prevVal:n,src:i.src||null}),o}},{NAME:"model",ATTRS:{clientId:{valueFn:"generateClientId",readOnly:!0},id:{value:null}}})},"3.17.2",{requires:["base-build","escape","json-parse"]});
diff --git a/js/yui3/node-base/node-base-min.js b/js/yui3/node-base/node-base-min.js
new file mode 100644
index 000000000..3fef060ca
--- /dev/null
+++ b/js/yui3/node-base/node-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("node-base",function(e,t){var n=["hasClass","addClass","removeClass","replaceClass","toggleClass"];e.Node.importMethod(e.DOM,n),e.NodeList.importMethod(e.Node.prototype,n);var r=e.Node,i=e.DOM;r.create=function(t,n){return n&&n._node&&(n=n._node),e.one(i.create(t,n))},e.mix(r.prototype,{create:r.create,insert:function(e,t){return this._insert(e,t),this},_insert:function(e,t){var n=this._node,r=null;return typeof t=="number"?t=this._node.childNodes[t]:t&&t._node&&(t=t._node),e&&typeof e!="string"&&(e=e._node||e._nodes||e),r=i.addHTML(n,e,t),r},prepend:function(e){return this.insert(e,0)},append:function(e){return this.insert(e,null)},appendChild:function(e){return r.scrubVal(this._insert(e))},insertBefore:function(t,n){return e.Node.scrubVal(this._insert(t,n))},appendTo:function(t){return e.one(t).append(this),this},setContent:function(e){return this._insert(e,"replace"),this},getContent:function(){var e=this;return e._node.nodeType===11&&(e=e.create("<div/>").append(e.cloneNode(!0))),e.get("innerHTML")}}),e.Node.prototype.setHTML=e.Node.prototype.setContent,e.Node.prototype.getHTML=e.Node.prototype.getContent,e.NodeList.importMethod(e.Node.prototype,["append","insert","appendChild","insertBefore","prepend","setContent","getContent","setHTML","getHTML"]);var r=e.Node,i=e.DOM;r.ATTRS={text:{getter:function(){return i.getText(this._node)},setter:function(e){return i.setText(this._node,e),e}},"for":{getter:function(){return i.getAttribute(this._node,"for")},setter:function(e){return i.setAttribute(this._node,"for",e),e}},options:{getter:function(){return this._node.getElementsByTagName("option")}},children:{getter:function(){var t=this._node,n=t.children,r,i,s;if(!n){r=t.childNodes,n=[];for(i=0,s=r.length;i<s;++i)r[i].tagName&&(n[n.length]=r[i])}return e.all(n)}},value:{getter:function(){return i.getValue(this._node)},setter:function(e){return i.setValue(this._node,e),e}}},e.Node.importMethod(e.DOM,["setAttribute","getAttribute"]);var r=e.Node,s=e.NodeList;r.DOM_EVENTS={abort:1,beforeunload:1,blur:1,change:1,click:1,close:1,command:1,contextmenu:1,copy:1,cut:1,dblclick:1,DOMMouseScroll:1,drag:1,dragstart:1,dragenter:1,dragover:1,dragleave:1,dragend:1,drop:1,error:1,focus:1,key:1,keydown:1,keypress:1,keyup:1,load:1,message:1,mousedown:1,mouseenter:1,mouseleave:1,mousemove:1,mousemultiwheel:1,mouseout:1,mouseover:1,mouseup:1,mousewheel:1,orientationchange:1,paste:1,reset:1,resize:1,select:1,selectstart:1,submit:1,scroll:1,textInput:1,unload:1,invalid:1},e.mix(r.DOM_EVENTS,e.Env.evt.plugins),e.augment(r,e.EventTarget),e.mix(r.prototype,{purge:function(t,n){return e.Event.purgeElement(this._node,t,n),this}}),e.mix(e.NodeList.prototype,{_prepEvtArgs:function(t,n,r){var i=e.Array(arguments,0,!0);return i.length<2?i[2]=this._nodes:i.splice(2,0,this._nodes),i[3]=r||this,i},on:function(t,n,r){return e.on.apply(e,this._prepEvtArgs.apply(this,arguments))},once:function(t,n,r){return e.once.apply(e,this._prepEvtArgs.apply(this,arguments))},after:function(t,n,r){return e.after.apply(e,this._prepEvtArgs.apply(this,arguments))},onceAfter:function(t,n,r){return e.onceAfter.apply(e,this._prepEvtArgs.apply(this,arguments))}}),s.importMethod(e.Node.prototype,["detach","detachAll"]),e.mix(e.Node.ATTRS,{offsetHeight:{setter:function(t){return e.DOM.setHeight(this._node,t),t},getter:function(){return this._node.offsetHeight}},offsetWidth:{setter:function(t){return e.DOM.setWidth(this._node,t),t},getter:function(){return this._node.offsetWidth}}}),e.mix(e.Node.prototype,{sizeTo:function(t,n){var r;arguments.length<2&&(r=e.one(t),t=r.get("offsetWidth"),n=r.get("offsetHeight")),this.setAttrs({offsetWidth:t,offsetHeight:n})}}),e.config.doc.documentElement.hasAttribute||(e.Node.prototype.hasAttribute=function(e){return e==="value"&&this.get("value")!==""?!0:!!this._node.attributes[e]&&!!this._node.attributes[e].specified}),e.Node.prototype.focus=function(){try{this._node.focus()}catch(e){}return this},e.Node.ATTRS.type={setter:function(e){if(e==="hidden")try{this._node.type="hidden"}catch(t){this._node.style.display="none",this._inputType="hidden"}else try{this._node.type=e}catch(t){}return e},getter:function(){return this._inputType||this._node.type},_bypassProxy:!0},e.config.doc.createElement("form").elements.nodeType&&(e.Node.ATTRS.elements={getter:function(){return this.all("input, textarea, button, select")}}),e.mix(e.Node.prototype,{_initData:function(){"_data"in this||(this._data={})},getData:function(t){this._initData();var n=this._data,r=n;return arguments.length?t in n?r=n[t]:r=this._getDataAttribute(t):typeof n=="object"&&n!==null&&(r={},e.Object.each(n,function(e,t){r[t]=e}),r=this._getDataAttributes(r)),r},_getDataAttributes:function(e){e=e||{};var t=0,n=this._node.attributes,r=n.length,i=this.DATA_PREFIX,s=i.length,o;while(t<r)o=n[t].name,o.indexOf(i)===0&&(o=o.substr(s),o in e||(e[o]=this._getDataAttribute(o))),t+=1;return e},_getDataAttribute:function(e){e=this.DATA_PREFIX+e;var t=this._node,n=t.attributes,r=n&&n[e]&&n[e].value;return r},setData:function(e,t){return this._initData(),arguments.length>1?this._data[e]=t:this._data=e,this},clearData:function(e){return"_data"in this&&(typeof e!="undefined"?delete this._data[e]:delete this._data),this}}),e.mix(e.NodeList.prototype,{getData:function(e){var t=arguments.length?[e]:[];return this._invoke("getData",t,!0)},setData:function(e,t){var n=arguments.length>1?[e,t]:[e];return this._invoke("setData",n)},clearData:function(e){var t=arguments.length?[e]:[];return this._invoke("clearData",[e])}})},"3.17.2",{requires:["event-base","node-core","dom-base","dom-style"]});
diff --git a/js/yui3/node-core/node-core-min.js b/js/yui3/node-core/node-core-min.js
new file mode 100644
index 000000000..f5ad30567
--- /dev/null
+++ b/js/yui3/node-core/node-core-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("node-core",function(e,t){var n=".",r="nodeName",i="nodeType",s="ownerDocument",o="tagName",u="_yuid",a={},f=Array.prototype.slice,l=e.DOM,c=function(t){if(!this.getDOMNode)return new c(t);if(typeof t=="string"){t=c._fromString(t);if(!t)return null}var n=t.nodeType!==9?t.uniqueID:t[u];n&&c._instances[n]&&c._instances[n]._node!==t&&(t[u]=null),n=n||e.stamp(t),n||(n=e.guid()),this[u]=n,this._node=t,this._stateProxy=t,this._initPlugins&&this._initPlugins()},h=function(t){var n=null;return t&&(n=typeof t=="string"?function(n){return e.Selector.test(n,t)}:function(n){return t(e.one(n))}),n};c.ATTRS={},c.DOM_EVENTS={},c._fromString=function(t){return t&&(t.indexOf("doc")===0?t=e.config.doc:t.indexOf("win")===0?t=e.config.win:t=e.Selector.query(t,null,!0)),t||null},c.NAME="node",c.re_aria=/^(?:role$|aria-)/,c.SHOW_TRANSITION="fadeIn",c.HIDE_TRANSITION="fadeOut",c._instances={},c.getDOMNode=function(e){return e?e.nodeType?e:e._node||null:null},c.scrubVal=function(t,n){if(t){if(typeof t=="object"||typeof t=="function")if(i in t||l.isWindow(t))t=e.one(t);else if(t.item&&!t._nodes||t[0]&&t[0][i])t=e.all(t)}else typeof t=="undefined"?t=n:t===null&&(t=null);return t},c.addMethod=function(e,t,n){e&&t&&typeof t=="function"&&(c.prototype[e]=function(){var e=f.call(arguments),r=this,i;return e[0]&&e[0]._node&&(e[0]=e[0]._node),e[1]&&e[1]._node&&(e[1]=e[1]._node),e.unshift(r._node),i=t.apply(n||r,e),i&&(i=c.scrubVal(i,r)),typeof i!="undefined"||(i=r),i})},c.importMethod=function(t,n,r){typeof n=="string"?(r=r||n,c.addMethod(r,t[n],t)):e.Array.each(n,function(e){c.importMethod(t,e)})},c.one=function(t){var n=null,r,i;if(t){if(typeof t=="string"){t=c._fromString(t);if(!t)return null}else if(t.getDOMNode)return t;if(t.nodeType||e.DOM.isWindow(t)){i=t.uniqueID&&t.nodeType!==9?t.uniqueID:t._yuid,n=c._instances[i],r=n?n._node:null;if(!n||r&&t!==r)n=new c(t),t.nodeType!=11&&(c._instances[n[u]]=n)}}return n},c.DEFAULT_SETTER=function(t,r){var i=this._stateProxy,s;return t.indexOf(n)>-1?(s=t,t=t.split(n),e.Object.setValue(i,t,r)):typeof i[t]!="undefined"&&(i[t]=r),r},c.DEFAULT_GETTER=function(t){var r=this._stateProxy,i;return t.indexOf&&t.indexOf(n)>-1?i=e.Object.getValue(r,t.split(n)):typeof r[t]!="undefined"&&(i=r[t]),i},e.mix(c.prototype,{DATA_PREFIX:"data-",toString:function(){var e=this[u]+": not bound to a node",t=this._node,n,i,s;return t&&(n=t.attributes,i=n&&n.id?t.getAttribute("id"):null,s=n&&n.className?t.getAttribute("className"):null,e=t[r],i&&(e+="#"+i),s&&(e+="."+s.replace(" ",".")),e+=" "+this[u]),e},get:function(e){var t;return this._getAttr?t=this._getAttr(e):t=this._get(e),t?t=c.scrubVal(t,this):t===null&&(t=null),t},_get:function(e){var t=c.ATTRS[e],n;return t&&t.getter?n=t.getter.call(this):c.re_aria.test(e)?n=this._node.getAttribute(e,2):n=c.DEFAULT_GETTER.apply(this,arguments),n},set:function(e,t){var n=c.ATTRS[e];return this._setAttr?this._setAttr.apply(this,arguments):n&&n.setter?n.setter.call(this,t,e):c.re_aria.test(e)?this._node.setAttribute(e,t):c.DEFAULT_SETTER.apply(this,arguments),this},setAttrs:function(t){return this._setAttrs?this._setAttrs(t):e.Object.each(t,function(e,t){this.set(t,e)},this),this},getAttrs:function(t){var n={};return this._getAttrs?this._getAttrs(t):e.Array.each(t,function(e,t){n[e]=this.get(e)},this),n},compareTo:function(e){var t=this._node;return e&&e._node&&(e=e._node),t===e},inDoc:function(e){var t=this._node;if(t){e=e?e._node||e:t[s];if(e.documentElement)return l.contains(e.documentElement,t)}return!1},getById:function(t){var n=this._node,r=l.byId(t,n[s]);return r&&l.contains(n,r)?r=e.one(r):r=null,r},ancestor:function(t,n,r){return arguments.length===2&&(typeof n=="string"||typeof n=="function")&&(r=n),e.one(l.ancestor(this._node,h(t),n,h(r)))},ancestors:function(t,n,r){return arguments.length===2&&(typeof n=="string"||typeof n=="function")&&(r=n),e.all(l.ancestors(this._node,h(t),n,h(r)))},previous:function(t,n){return e.one(l.elementByAxis(this._node,"previousSibling",h(t),n))},next:function(t,n){return e.one(l.elementByAxis(this._node,"nextSibling",h(t),n))},siblings:function(t){return e.all(l.siblings(this._node,h(t)))},one:function(t){return e.one(e.Selector.query(t,this._node,!0))},all:function(t){var n;return this._node&&(n=e.all(e.Selector.query(t,this._node)),n._query=t,n._queryRoot=this._node),n||e.all([])},test:function(t){return e.Selector.test(this._node,t)},remove:function(e){var t=this._node;return t&&t.parentNode&&t.parentNode.removeChild(t),e&&this.destroy(),this},replace:function(e){var t=this._node;return typeof e=="string"&&(e=c.create(e)),t.parentNode.replaceChild(c.getDOMNode(e),t),this},replaceChild:function(t,n){return typeof t=="string"&&(t=l.create(t)),e.one(this._node.replaceChild(c.getDOMNode(t),c.getDOMNode(n)))},destroy:function(t){var n=e.config.doc.uniqueID?"uniqueID":"_yuid",r;this.purge(),this.unplug&&this.unplug(),this.clearData(),t&&e.NodeList.each(this.all("*"),function(t){r=c._instances[t[n]],r?r.destroy():e.Event.purgeElement(t)}),this._node=null,this._stateProxy=null,delete c._instances[this._yuid]},invoke:function(e,t,n,r,i,s){var o=this._node,u;return t&&t._node&&(t=t._node),n&&n._node&&(n=n._node),u=o[e](t,n,r,i,s),c.scrubVal(u,this)},swap:e.config.doc.documentElement.swapNode?function(e){this._node.swapNode(c.getDOMNode(e))}:function(e){e=c.getDOMNode(e);var t=this._node,n=e.parentNode,r=e.nextSibling;return r===t?n.insertBefore(t,e):e===t.nextSibling?n.insertBefore(e,t):(t.parentNode.replaceChild(e,t),l.addHTML(n,t,r)),this},hasMethod:function(e){var t=this._node;return!(!(t&&e in t&&typeof t[e]!="unknown")||typeof t[e]!="function"&&String(t[e]).indexOf("function")!==1)},isFragment:function(){return this.get("nodeType")===11},empty:function(){return this.get("childNodes").remove().destroy(!0),this},getDOMNode:function(){return this._node}},!0),e.Node=c,e.one=c.one;var p=function(t){var n=[];t&&(typeof t=="string"?(this._query=t,t=e.Selector.query(t)):t.nodeType||l.isWindow(t)?t=[t]:t._node?t=[t._node]:
+t[0]&&t[0]._node?(e.Array.each(t,function(e){e._node&&n.push(e._node)}),t=n):t=e.Array(t,0,!0)),this._nodes=t||[]};p.NAME="NodeList",p.getDOMNodes=function(e){return e&&e._nodes?e._nodes:e},p.each=function(t,n,r){var i=t._nodes;i&&i.length&&e.Array.each(i,n,r||t)},p.addMethod=function(t,n,r){t&&n&&(p.prototype[t]=function(){var t=[],i=arguments;return e.Array.each(this._nodes,function(s){var o=s.uniqueID&&s.nodeType!==9?"uniqueID":"_yuid",u=e.Node._instances[s[o]],a,f;u||(u=p._getTempNode(s)),a=r||u,f=n.apply(a,i),f!==undefined&&f!==u&&(t[t.length]=f)}),t.length?t:this})},p.importMethod=function(t,n,r){typeof n=="string"?(r=r||n,p.addMethod(n,t[n])):e.Array.each(n,function(e){p.importMethod(t,e)})},p._getTempNode=function(t){var n=p._tempNode;return n||(n=e.Node.create("<div></div>"),p._tempNode=n),n._node=t,n._stateProxy=t,n},e.mix(p.prototype,{_invoke:function(e,t,n){var r=n?[]:this;return this.each(function(i){var s=i[e].apply(i,t);n&&r.push(s)}),r},item:function(t){return e.one((this._nodes||[])[t])},each:function(t,n){var r=this;return e.Array.each(this._nodes,function(i,s){return i=e.one(i),t.call(n||i,i,s,r)}),r},batch:function(t,n){var r=this;return e.Array.each(this._nodes,function(i,s){var o=e.Node._instances[i[u]];return o||(o=p._getTempNode(i)),t.call(n||o,o,s,r)}),r},some:function(t,n){var r=this;return e.Array.some(this._nodes,function(i,s){return i=e.one(i),n=n||i,t.call(n,i,s,r)})},toFrag:function(){return e.one(e.DOM._nl2frag(this._nodes))},indexOf:function(t){return e.Array.indexOf(this._nodes,e.Node.getDOMNode(t))},filter:function(t){return e.all(e.Selector.filter(this._nodes,t))},modulus:function(t,n){n=n||0;var r=[];return p.each(this,function(e,i){i%t===n&&r.push(e)}),e.all(r)},odd:function(){return this.modulus(2,1)},even:function(){return this.modulus(2)},destructor:function(){},refresh:function(){var t,n=this._nodes,r=this._query,i=this._queryRoot;return r&&(i||n&&n[0]&&n[0].ownerDocument&&(i=n[0].ownerDocument),this._nodes=e.Selector.query(r,i)),this},size:function(){return this._nodes.length},isEmpty:function(){return this._nodes.length<1},toString:function(){var e="",t=this[u]+": not bound to any nodes",n=this._nodes,i;return n&&n[0]&&(i=n[0],e+=i[r],i.id&&(e+="#"+i.id),i.className&&(e+="."+i.className.replace(" ",".")),n.length>1&&(e+="...["+n.length+" items]")),e||t},getDOMNodes:function(){return this._nodes}},!0),p.importMethod(e.Node.prototype,["destroy","empty","remove","set"]),p.prototype.get=function(t){var n=[],r=this._nodes,i=!1,s=p._getTempNode,o,u;return r[0]&&(o=e.Node._instances[r[0]._yuid]||s(r[0]),u=o._get(t),u&&u.nodeType&&(i=!0)),e.Array.each(r,function(r){o=e.Node._instances[r._yuid],o||(o=s(r)),u=o._get(t),i||(u=e.Node.scrubVal(u,o)),n.push(u)}),i?e.all(n):n},e.NodeList=p,e.all=function(e){return new p(e)},e.Node.all=e.all;var d=e.NodeList,v=Array.prototype,m={concat:1,pop:0,push:0,shift:0,slice:1,splice:1,unshift:0};e.Object.each(m,function(t,n){d.prototype[n]=function(){var r=[],i=0,s,o;while(typeof (s=arguments[i++])!="undefined")r.push(s._node||s._nodes||s);return o=v[n].apply(this._nodes,r),t?o=e.all(o):o=e.Node.scrubVal(o),o}}),e.Array.each(["removeChild","hasChildNodes","cloneNode","hasAttribute","scrollIntoView","getElementsByTagName","focus","blur","submit","reset","select","createCaption"],function(t){e.Node.prototype[t]=function(e,n,r){var i=this.invoke(t,e,n,r);return i}}),e.Node.prototype.removeAttribute=function(e){var t=this._node;return t&&t.removeAttribute(e,0),this},e.Node.importMethod(e.DOM,["contains","setAttribute","getAttribute","wrap","unwrap","generateID"]),e.NodeList.importMethod(e.Node.prototype,["getAttribute","setAttribute","removeAttribute","unwrap","wrap","generateID"])},"3.17.2",{requires:["dom-core","selector"]});
diff --git a/js/yui3/node-event-delegate/node-event-delegate-min.js b/js/yui3/node-event-delegate/node-event-delegate-min.js
new file mode 100644
index 000000000..6f0c747cc
--- /dev/null
+++ b/js/yui3/node-event-delegate/node-event-delegate-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("node-event-delegate",function(e,t){e.Node.prototype.delegate=function(t){var n=e.Array(arguments,0,!0),r=e.Lang.isObject(t)&&!e.Lang.isArray(t)?1:2;return n.splice(r,0,this._node),e.delegate.apply(e,n)}},"3.17.2",{requires:["node-base","event-delegate"]});
diff --git a/js/yui3/node-event-html5/node-event-html5-min.js b/js/yui3/node-event-html5/node-event-html5-min.js
new file mode 100644
index 000000000..fdacd3310
--- /dev/null
+++ b/js/yui3/node-event-html5/node-event-html5-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("node-event-html5",function(e,t){e.mix(e.Node.DOM_EVENTS,{DOMActivate:1,DOMContentLoaded:1,afterprint:1,beforeprint:1,canplay:1,canplaythrough:1,durationchange:1,emptied:1,ended:1,formchange:1,forminput:1,hashchange:1,input:1,invalid:1,loadedmetadata:1,loadeddata:1,loadstart:1,offline:1,online:1,pagehide:1,pageshow:1,pause:1,play:1,playing:1,popstate:1,progress:1,ratechange:1,readystatechange:1,redo:1,seeking:1,seeked:1,show:1,stalled:1,suspend:1,timeupdate:1,undo:1,volumechange:1,waiting:1})},"3.17.2",{requires:["node-base"]});
diff --git a/js/yui3/node-event-simulate/node-event-simulate-min.js b/js/yui3/node-event-simulate/node-event-simulate-min.js
new file mode 100644
index 000000000..04d582f9b
--- /dev/null
+++ b/js/yui3/node-event-simulate/node-event-simulate-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("node-event-simulate",function(e,t){e.Node.prototype.simulate=function(t,n){e.Event.simulate(e.Node.getDOMNode(this),t,n)},e.Node.prototype.simulateGesture=function(t,n,r){e.Event.simulateGesture(this,t,n,r)}},"3.17.2",{requires:["node-base","event-simulate","gesture-simulate"]});
diff --git a/js/yui3/node-flick/assets/node-flick-core.css b/js/yui3/node-flick/assets/node-flick-core.css
new file mode 100644
index 000000000..b5b27e19f
--- /dev/null
+++ b/js/yui3/node-flick/assets/node-flick-core.css
@@ -0,0 +1,15 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-flick {
+ position:relative;
+ overflow:hidden;
+}
+
+.yui3-flick-content {
+ position:relative;
+}
diff --git a/js/yui3/node-flick/assets/skins/sam/node-flick.css b/js/yui3/node-flick/assets/skins/sam/node-flick.css
new file mode 100644
index 000000000..7e9615540
--- /dev/null
+++ b/js/yui3/node-flick/assets/skins/sam/node-flick.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-flick{position:relative;overflow:hidden}.yui3-flick-content{position:relative}#yui3-css-stamp.skin-sam-node-flick{display:none}
diff --git a/js/yui3/node-flick/node-flick-min.js b/js/yui3/node-flick/node-flick-min.js
new file mode 100644
index 000000000..3e8b066c0
--- /dev/null
+++ b/js/yui3/node-flick/node-flick-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("node-flick",function(e,t){function b(e){b.superclass.constructor.apply(this,arguments)}var n="host",r="parentNode",i="boundingBox",s="offsetHeight",o="offsetWidth",u="scrollHeight",a="scrollWidth",f="bounce",l="minDistance",c="minVelocity",h="bounceDistance",p="deceleration",d="step",v="duration",m="easing",g="flick",y=e.ClassNameManager.getClassName;b.ATTRS={deceleration:{value:.98},bounce:{value:.7},bounceDistance:{value:150},minVelocity:{value:0},minDistance:{value:10},boundingBox:{valueFn:function(){return this.get(n).get(r)}},step:{value:10},duration:{value:null},easing:{value:null}},b.NAME="pluginFlick",b.NS="flick",e.extend(b,e.Plugin.Base,{initializer:function(t){this._node=this.get(n),this._renderClasses(),this.setBounds(),this._node.on(g,e.bind(this._onFlick,this),{minDistance:this.get(l),minVelocity:this.get(c)})},setBounds:function(){var e=this.get(i),t=this._node,n=e.get(s),r=e.get(o),f=t.get(u),l=t.get(a);f>n&&(this._maxY=f-n,this._minY=0,this._scrollY=!0),l>r&&(this._maxX=l-r,this._minX=0,this._scrollX=!0),this._x=this._y=0,t.set("top",this._y+"px"),t.set("left",this._x+"px")},_renderClasses:function(){this.get(i).addClass(b.CLASS_NAMES.box),this._node.addClass(b.CLASS_NAMES.content)},_onFlick:function(e){this._v=e.flick.velocity,this._flick=!0,this._flickAnim()},_flickAnim:function(){var t=this._y,n=this._x,r=this._maxY,i=this._minY,s=this._maxX,o=this._minX,u=this._v,a=this.get(d),l=this.get(p),c=this.get(f);this._v=u*l,this._snapToEdge=!1,this._scrollX&&(n-=u*a),this._scrollY&&(t-=u*a),Math.abs(u).toFixed(4)<=b.VELOCITY_THRESHOLD?(this._flick=!1,this._killTimer(!this._exceededYBoundary&&!this._exceededXBoundary),this._scrollX&&(n<o?(this._snapToEdge=!0,this._setX(o)):n>s&&(this._snapToEdge=!0,this._setX(s))),this._scrollY&&(t<i?(this._snapToEdge=!0,this._setY(i)):t>r&&(this._snapToEdge=!0,this._setY(r)))):(this._scrollX&&(n<o||n>s)&&(this._exceededXBoundary=!0,this._v*=c),this._scrollY&&(t<i||t>r)&&(this._exceededYBoundary=!0,this._v*=c),this._scrollX&&this._setX(n),this._scrollY&&this._setY(t),this._flickTimer=e.later(a,this,this._flickAnim))},_setX:function(e){this._move(e,null,this.get(v),this.get(m))},_setY:function(e){this._move(null,e,this.get(v),this.get(m))},_move:function(e,t,n,r){e!==null?e=this._bounce(e):e=this._x,t!==null?t=this._bounce(t):t=this._y,n=n||this._snapToEdge?b.SNAP_DURATION:0,r=r||this._snapToEdge?b.SNAP_EASING:b.EASING,this._x=e,this._y=t,this._anim(e,t,n,r)},_anim:function(t,n,r,i){var s=t*-1,o=n*-1,u={duration:r/1e3,easing:i};e.Transition.useNative?u.transform="translate("+s+"px,"+o+"px)":(u.left=s+"px",u.top=o+"px"),this._node.transition(u)},_bounce:function(e,t){var n=this.get(f),r=this.get(h),i=n?-r:0;return t=n?t+r:t,n||(e<i?e=i:e>t&&(e=t)),e},_killTimer:function(){this._flickTimer&&this._flickTimer.cancel()}},{VELOCITY_THRESHOLD:.015,SNAP_DURATION:400,EASING:"cubic-bezier(0, 0.1, 0, 1.0)",SNAP_EASING:"ease-out",CLASS_NAMES:{box:y(b.NS),content:y(b.NS,"content")}}),e.Plugin.Flick=b},"3.17.2",{requires:["classnamemanager","transition","event-flick","plugin"],skinnable:!0});
diff --git a/js/yui3/node-focusmanager/node-focusmanager-min.js b/js/yui3/node-focusmanager/node-focusmanager-min.js
new file mode 100644
index 000000000..738e557f1
--- /dev/null
+++ b/js/yui3/node-focusmanager/node-focusmanager-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("node-focusmanager",function(e,t){var n="activeDescendant",r="id",i="disabled",s="tabIndex",o="focused",u="focusClass",a="circular",f="UI",l="key",c=n+"Change",h="host",p={37:!0,38:!0,39:!0,40:!0},d={a:!0,button:!0,input:!0,object:!0},v=e.Lang,m=e.UA,g=function(){g.superclass.constructor.apply(this,arguments)};g.ATTRS={focused:{value:!1,readOnly:!0},descendants:{getter:function(e){return this.get(h).all(e)}},activeDescendant:{setter:function(t){var n=v.isNumber,i=e.Attribute.INVALID_VALUE,s=this._descendantsMap,o=this._descendants,u,a,f;return n(t)?(u=t,a=u):t instanceof e.Node&&s?(u=s[t.get(r)],n(u)?a=u:a=i):a=i,o&&(f=o.item(u),f&&f.get("disabled")&&(a=i)),a}},keys:{value:{next:null,previous:null}},focusClass:{},circular:{value:!0}},e.extend(g,e.Plugin.Base,{_stopped:!0,_descendants:null,_descendantsMap:null,_focusedNode:null,_lastNodeIndex:0,_eventHandlers:null,_initDescendants:function(){var t=this.get("descendants"),o={},u=-1,a,f=this.get(n),l,c,h=0;v.isUndefined(f)&&(f=-1);if(t){a=t.size();for(h=0;h<a;h++)l=t.item(h),u===-1&&!l.get(i)&&(u=h),f<0&&parseInt(l.getAttribute(s,2),10)===0&&(f=h),l&&l.set(s,-1),c=l.get(r),c||(c=e.guid(),l.set(r,c)),o[c]=h;f<0&&(f=0),l=t.item(f);if(!l||l.get(i))l=t.item(u),f=u;this._lastNodeIndex=a-1,this._descendants=t,this._descendantsMap=o,this.set(n,f),l&&l.set(s,0)}},_isDescendant:function(e){return e.get(r)in this._descendantsMap},_removeFocusClass:function(){var e=this._focusedNode,t=this.get(u),n;t&&(n=v.isString(t)?t:t.className),e&&n&&e.removeClass(n)},_detachKeyHandler:function(){var e=this._prevKeyHandler,t=this._nextKeyHandler;e&&e.detach(),t&&t.detach()},_preventScroll:function(e){p[e.keyCode]&&this._isDescendant(e.target)&&e.preventDefault()},_fireClick:function(e){var t=e.target,n=t.get("nodeName").toLowerCase();e.keyCode===13&&(!d[n]||n==="a"&&!t.getAttribute("href"))&&t.simulate("click")},_attachKeyHandler:function(){this._detachKeyHandler();var t=this.get("keys.next"),n=this.get("keys.previous"),r=this.get(h),i=this._eventHandlers;n&&(this._prevKeyHandler=e.on(l,e.bind(this._focusPrevious,this),r,n)),t&&(this._nextKeyHandler=e.on(l,e.bind(this._focusNext,this),r,t)),m.opera&&i.push(r.on("keypress",this._preventScroll,this)),m.opera||i.push(r.on("keypress",this._fireClick,this))},_detachEventHandlers:function(){this._detachKeyHandler();var t=this._eventHandlers;t&&(e.Array.each(t,function(e){e.detach()}),this._eventHandlers=null)},_attachEventHandlers:function(){var t=this._descendants,n,r,i;t&&t.size()&&(n=this._eventHandlers||[],r=this.get(h).get("ownerDocument"),n.length===0&&(n.push(r.on("focus",this._onDocFocus,this)),n.push(r.on("mousedown",this._onDocMouseDown,this)),n.push(this.after("keysChange",this._attachKeyHandler)),n.push(this.after("descendantsChange",this._initDescendants)),n.push(this.after(c,this._afterActiveDescendantChange)),i=this.after("focusedChange",e.bind(function(e){e.newVal&&(this._attachKeyHandler(),i.detach())},this)),n.push(i)),this._eventHandlers=n)},_onDocMouseDown:function(e){var t=this.get(h),n=e.target,r=t.contains(n),i,s=function(e){var n=!1;return e.compareTo(t)||(n=this._isDescendant(e)?e:s.call(this,e.get("parentNode"))),n};r&&(i=s.call(this,n),i?n=i:!i&&this.get(o)&&(this._set(o,!1),this._onDocFocus(e))),r&&this._isDescendant(n)?this.focus(n):m.webkit&&this.get(o)&&(!r||r&&!this._isDescendant(n))&&(this._set(o,!1),this._onDocFocus(e))},_onDocFocus:function(e){var t=this._focusTarget||e.target,n=this.get(o),r=this.get(u),i=this._focusedNode,s;this._focusTarget&&(this._focusTarget=null),this.get(h).contains(t)?(s=this._isDescendant(t),!n&&s?n=!0:n&&!s&&(n=!1)):n=!1,r&&(i&&(!i.compareTo(t)||!n)&&this._removeFocusClass(),s&&n&&(r.fn?(t=r.fn(t),t.addClass(r.className)):t.addClass(r),this._focusedNode=t)),this._set(o,n)},_focusNext:function(e,t){var r=t||this.get(n),i;this._isDescendant(e.target)&&r<=this._lastNodeIndex&&(r+=1,r===this._lastNodeIndex+1&&this.get(a)&&(r=0),i=this._descendants.item(r),i&&(i.get("disabled")?this._focusNext(e,r):this.focus(r))),this._preventScroll(e)},_focusPrevious:function(e,t){var r=t||this.get(n),i;this._isDescendant(e.target)&&r>=0&&(r-=1,r===-1&&this.get(a)&&(r=this._lastNodeIndex),i=this._descendants.item(r),i&&(i.get("disabled")?this._focusPrevious(e,r):this.focus(r))),this._preventScroll(e)},_afterActiveDescendantChange:function(e){var t=this._descendants.item(e.prevVal);t&&t.set(s,-1),t=this._descendants.item(e.newVal),t&&t.set(s,0)},initializer:function(e){this.start()},destructor:function(){this.stop(),this.get(h).focusManager=null},focus:function(e){v.isUndefined(e)&&(e=this.get(n)),this.set(n,e,{src:f});var t=this._descendants.item(this.get(n));t&&(t.focus(),m.opera&&t.get("nodeName").toLowerCase()==="button"&&(this._focusTarget=t))},blur:function(){var e;this.get(o)&&(e=this._descendants.item(this.get(n)),e&&(e.blur(),this._removeFocusClass()),this._set(o,!1,{src:f}))},start:function(){this._stopped&&(this._initDescendants(),this._attachEventHandlers(),this._stopped=!1)},stop:function(){this._stopped||(this._detachEventHandlers(),this._descendants=null,this._focusedNode=null,this._lastNodeIndex=0,this._stopped=!0)},refresh:function(){this._initDescendants(),this._eventHandlers||this._attachEventHandlers()}}),g.NAME="nodeFocusManager",g.NS="focusManager",e.namespace("Plugin"),e.Plugin.NodeFocusManager=g},"3.17.2",{requires:["attribute","node","plugin","node-event-simulate","event-key","event-focus"]});
diff --git a/js/yui3/node-load/node-load-min.js b/js/yui3/node-load/node-load-min.js
new file mode 100644
index 000000000..bcfa287f3
--- /dev/null
+++ b/js/yui3/node-load/node-load-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("node-load",function(e,t){e.Node.prototype._ioComplete=function(t,n,r){var i=r[0],s=r[1],o,u;n&&n.responseText&&(u=n.responseText,i&&(o=e.DOM.create(u),u=e.Selector.query(i,o)),this.setContent(u)),s&&s.call(this,t,n)},e.Node.prototype.load=function(t,n,r){typeof n=="function"&&(r=n,n=null);var i={context:this,on:{complete:this._ioComplete},arguments:[n,r]};return e.io(t,i),this}},"3.17.2",{requires:["node-base","io-base"]});
diff --git a/js/yui3/node-menunav/assets/node-menunav-core.css b/js/yui3/node-menunav/assets/node-menunav-core.css
new file mode 100644
index 000000000..53a4ae6d0
--- /dev/null
+++ b/js/yui3/node-menunav/assets/node-menunav-core.css
@@ -0,0 +1,176 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-menu .yui3-menu {
+
+ position: absolute;
+ z-index: 1;
+
+}
+
+
+.yui3-menu .yui3-shim {
+
+ /*
+ Styles for the <iframe> shim used to prevent <select> elements from poking through
+ submenus in IE < 7. Note: For peformance, creation of the <iframe> shim for each submenu
+ is deferred until it is initially made visible by the user.
+ */
+
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: -1;
+ opacity: 0;
+ filter: alpha(opacity=0); /* For IE since it doesn't implement the CSS3 "opacity" property. */
+ border: none;
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ width: 100%;
+
+}
+
+.yui3-menu-hidden {
+
+ /*
+ Position hidden menus outside the viewport boundaries to prevent them from
+ triggering scrollbars on the viewport.
+ */
+
+ top: -10000px;
+ left: -10000px;
+
+ /*
+ Using "visibility:hidden" over "display" none because:
+
+ 1) As the "position" property for submenus is set to "absolute", they are out of
+ the document flow and take up no space. Therefore, from that perspective use of
+ "display:none" is redundant.
+
+ 2) According to MSDN use of "display:none" is more expensive:
+ "Display is the more expensive of the two CSS properties, so if you are
+ making elements appear and disappear often, visibility will be faster."
+ (See http://msdn.microsoft.com/en-us/library/bb264005(VS.85).aspx)
+ */
+
+ visibility: hidden;
+
+}
+
+.yui3-menu li {
+
+ list-style-type: none;
+
+}
+
+.yui3-menu ul,
+.yui3-menu li {
+
+ margin: 0;
+ padding: 0;
+
+}
+
+.yui3-menu-label,
+.yui3-menuitem-content {
+
+ text-align: left;
+ white-space: nowrap;
+ display: block;
+
+}
+
+.yui3-menu-horizontal li {
+
+ float: left;
+ width: auto;
+
+}
+
+.yui3-menu-horizontal li li {
+
+ float: none;
+
+}
+
+.yui3-menu-horizontal ul {
+
+ /*
+ Use of "zoom" sets the "hasLayout" property to "true" in IE (< 8). When "hasLayout" is
+ set to "true", an element can clear its floated descendents. For more:
+ http://msdn.microsoft.com/en-gb/library/ms533776(VS.85).aspx
+ */
+
+ *zoom: 1;
+
+}
+
+.yui3-menu-horizontal ul ul {
+
+ /*
+ No need to clear <ul>s of submenus of horizontal menus since <li>s of submenus
+ aren't floated.
+ */
+
+ *zoom: normal;
+
+}
+
+.yui3-menu-horizontal>.yui3-menu-content>ul:after {
+
+ /* Self-clearing solution for Opera, Webkit, Gecko and IE > 7 */
+
+ content: "";
+ display: block;
+ clear: both;
+ line-height: 0;
+ font-size: 0;
+ visibility: hidden;
+
+}
+
+
+/*
+ The following two rules are for IE 7. Triggering "hasLayout" (via use of "zoom") prevents
+ first-tier submenus from hiding when the mouse is moving from an menu label in a root menu to
+ its corresponding submenu.
+*/
+
+.yui3-menu-content {
+
+ *zoom: 1;
+
+}
+
+
+.yui3-menu-hidden .yui3-menu-content {
+
+ *zoom: normal;
+
+}
+
+
+/*
+ The following two rules are for IE 6 (Standards Mode and Quirks Mode) and IE 7 (Quirks Mode
+ only). Triggering "hasLayout" (via use of "zoom") fixes a bug in IE where mousing mousing off
+ the text node of menuitem or menu label will incorrectly trigger the mouseout event.
+*/
+
+.yui3-menuitem-content,
+.yui3-menu-label {
+
+ _zoom: 1;
+
+}
+
+.yui3-menu-hidden .yui3-menuitem-content,
+.yui3-menu-hidden .yui3-menu-label {
+
+ _zoom: normal;
+
+}
diff --git a/js/yui3/node-menunav/assets/skins/night/horizontal-menu-submenu-indicator.png b/js/yui3/node-menunav/assets/skins/night/horizontal-menu-submenu-indicator.png
new file mode 100644
index 000000000..7940437b7
--- /dev/null
+++ b/js/yui3/node-menunav/assets/skins/night/horizontal-menu-submenu-indicator.png
Binary files differ
diff --git a/js/yui3/node-menunav/assets/skins/night/node-menunav.css b/js/yui3/node-menunav/assets/skins/night/node-menunav.css
new file mode 100644
index 000000000..9fdbee6b0
--- /dev/null
+++ b/js/yui3/node-menunav/assets/skins/night/node-menunav.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-menu .yui3-menu{position:absolute;z-index:1}.yui3-menu .yui3-shim{position:absolute;top:0;left:0;z-index:-1;opacity:0;filter:alpha(opacity=0);border:0;margin:0;padding:0;height:100%;width:100%}.yui3-menu-hidden{top:-10000px;left:-10000px;visibility:hidden}.yui3-menu li{list-style-type:none}.yui3-menu ul,.yui3-menu li{margin:0;padding:0}.yui3-menu-label,.yui3-menuitem-content{text-align:left;white-space:nowrap;display:block}.yui3-menu-horizontal li{float:left;width:auto}.yui3-menu-horizontal li li{float:none}.yui3-menu-horizontal ul{*zoom:1}.yui3-menu-horizontal ul ul{*zoom:normal}.yui3-menu-horizontal>.yui3-menu-content>ul:after{content:"";display:block;clear:both;line-height:0;font-size:0;visibility:hidden}.yui3-menu-content{*zoom:1}.yui3-menu-hidden .yui3-menu-content{*zoom:normal}.yui3-menuitem-content,.yui3-menu-label{_zoom:1}.yui3-menu-hidden .yui3-menuitem-content,.yui3-menu-hidden .yui3-menu-label{_zoom:normal}.yui3-skin-night .yui3-menu-content,.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-content{font-size:100%;line-height:2.25;*line-height:1.45;border:solid 1px #303030;background:#151515}.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-content{font-size:100%}.yui3-skin-night .yui3-menu-horizontal .yui3-menu-content{line-height:2;*line-height:1.9;background-color:#3b3c3d;background:-moz-linear-gradient(0% 100% 90deg,#242526 0,#3b3c3d 96%,#2c2d2f 100%);background:-webkit-gradient(linear,left bottom,left top,from(#242526),color-stop(0.96,#3b3c3d),to(#2c2d2f));padding:0}.yui3-skin-night .yui3-menu ul,.yui3-skin-night .yui3-menu ul ul{margin-top:3px;padding-top:3px;border-top:solid 1px #303030}.yui3-skin-night .yui3-menu ul.first-of-type{border:0;margin:0;padding:0}.yui3-skin-night .yui3-menu-horizontal ul{padding:0;margin:0;border:0}.yui3-skin-night .yui3-menu li,.yui3-skin-night .yui3-menu .yui3-menu li{_border-bottom:solid 1px #151515}.yui3-skin-night .yui3-menu-horizontal li{_border-bottom:0}.yui3-skin-night .yui3-menubuttonnav li{border-right:solid 1px #ccc}.yui3-skin-night .yui3-splitbuttonnav li{border-right:solid 1px #303030}.yui3-skin-night .yui3-menubuttonnav li li,.yui3-skin-night .yui3-splitbuttonnav li li{border-right:0}.yui3-skin-night .yui3-menu-label,.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-label,.yui3-skin-night .yui3-menuitem-content,.yui3-skin-night .yui3-menu .yui3-menu .yui3-menuitem-content{padding:0 1em;color:#fff;text-decoration:none;cursor:default;float:none;border:0;margin:0}.yui3-skin-night .yui3-menu-horizontal .yui3-menu-label,.yui3-skin-night .yui3-menu-horizontal .yui3-menuitem-content{padding:0 10px;border-style:solid;border-color:#303030;border-width:1px 0;margin:-1px 0;float:left;width:auto}.yui3-skin-night .yui3-menu-label,.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-label{background:url(vertical-menu-submenu-indicator.png) right center no-repeat}.yui3-skin-night .yui3-menu-horizontal .yui3-menu-label{background:0}.yui3-skin-night .yui3-menubuttonnav .yui3-menu-label,.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label{background-image:none}.yui3-skin-night .yui3-menubuttonnav .yui3-menu-label{padding-right:0}.yui3-skin-night .yui3-menubuttonnav .yui3-menu-label em{font-style:normal;padding-right:20px;display:block;background:url(horizontal-menu-submenu-indicator.png) right center no-repeat}.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label{padding:0}.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label a{float:left;width:auto;color:#fff;text-decoration:none;cursor:default;padding:0 5px 0 10px}.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label .yui3-menu-toggle{padding:0;border-left:solid 1px #303030;width:15px;overflow:hidden;text-indent:-1000px;background:url(horizontal-menu-submenu-indicator.png) 3px center no-repeat}.yui3-skin-night .yui3-menu-label-active,.yui3-skin-night .yui3-menu-label-menuvisible,.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-label-active,.yui3-skin-night .yui3-menu .yui3-menu .yui3-menu-label-menuvisible{background-color:#292a2a}.yui3-skin-night .yui3-menuitem-active .yui3-menuitem-content,.yui3-skin-night .yui3-menu .yui3-menu .yui3-menuitem-active .yui3-menuitem-content{background-image:none;background-color:#292a2a;background:-moz-linear-gradient(0% 100% 90deg,#252626 0,#333434 100%);background:-webkit-gradient(linear,left top,left bottom,from(#333434),to(#252626));border-left-width:0;margin-left:0}.yui3-skin-night .yui3-menu-horizontal .yui3-menu-label-active,.yui3-skin-night .yui3-menu-horizontal .yui3-menuitem-active .yui3-menuitem-content,.yui3-skin-night .yui3-menu-horizontal .yui3-menu-label-menuvisible{border-color:#303030;background-color:#555658;background:-moz-linear-gradient(0% 100% 90deg,#343536 0,#555658 96%,#3e3f41 100%);background:-webkit-gradient(linear,left bottom,left top,from(#343536),color-stop(0.96,#555658),to(#3e3f41))}.yui3-skin-night .yui3-menubuttonnav .yui3-menu-label-active,.yui3-skin-night .yui3-menubuttonnav .yui3-menuitem-active .yui3-menuitem-content,.yui3-skin-night .yui3-menubuttonnav .yui3-menu-label-menuvisible,.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label-active,.yui3-skin-night .yui3-splitbuttonnav .yui3-menuitem-active .yui3-menuitem-content,.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label-menuvisible{border-left-width:1px;margin-left:-1px}.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label-menuvisible{border-color:#303030;background:transparent}.yui3-skin-night .yui3-splitbuttonnav .yui3-menu-label-menuvisible .yui3-menu-toggle{border-color:#303030;background-color:#505050}#yui3-css-stamp.skin-night-node-menunav{display:none}
diff --git a/js/yui3/node-menunav/assets/skins/night/vertical-menu-submenu-indicator.png b/js/yui3/node-menunav/assets/skins/night/vertical-menu-submenu-indicator.png
new file mode 100644
index 000000000..d997b8e4c
--- /dev/null
+++ b/js/yui3/node-menunav/assets/skins/night/vertical-menu-submenu-indicator.png
Binary files differ
diff --git a/js/yui3/node-menunav/assets/skins/sam/horizontal-menu-submenu-indicator.png b/js/yui3/node-menunav/assets/skins/sam/horizontal-menu-submenu-indicator.png
new file mode 100644
index 000000000..a2482ac79
--- /dev/null
+++ b/js/yui3/node-menunav/assets/skins/sam/horizontal-menu-submenu-indicator.png
Binary files differ
diff --git a/js/yui3/node-menunav/assets/skins/sam/horizontal-menu-submenu-toggle.png b/js/yui3/node-menunav/assets/skins/sam/horizontal-menu-submenu-toggle.png
new file mode 100644
index 000000000..4379f817d
--- /dev/null
+++ b/js/yui3/node-menunav/assets/skins/sam/horizontal-menu-submenu-toggle.png
Binary files differ
diff --git a/js/yui3/node-menunav/assets/skins/sam/node-menunav.css b/js/yui3/node-menunav/assets/skins/sam/node-menunav.css
new file mode 100644
index 000000000..5e78d5abf
--- /dev/null
+++ b/js/yui3/node-menunav/assets/skins/sam/node-menunav.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-menu .yui3-menu{position:absolute;z-index:1}.yui3-menu .yui3-shim{position:absolute;top:0;left:0;z-index:-1;opacity:0;filter:alpha(opacity=0);border:0;margin:0;padding:0;height:100%;width:100%}.yui3-menu-hidden{top:-10000px;left:-10000px;visibility:hidden}.yui3-menu li{list-style-type:none}.yui3-menu ul,.yui3-menu li{margin:0;padding:0}.yui3-menu-label,.yui3-menuitem-content{text-align:left;white-space:nowrap;display:block}.yui3-menu-horizontal li{float:left;width:auto}.yui3-menu-horizontal li li{float:none}.yui3-menu-horizontal ul{*zoom:1}.yui3-menu-horizontal ul ul{*zoom:normal}.yui3-menu-horizontal>.yui3-menu-content>ul:after{content:"";display:block;clear:both;line-height:0;font-size:0;visibility:hidden}.yui3-menu-content{*zoom:1}.yui3-menu-hidden .yui3-menu-content{*zoom:normal}.yui3-menuitem-content,.yui3-menu-label{_zoom:1}.yui3-menu-hidden .yui3-menuitem-content,.yui3-menu-hidden .yui3-menu-label{_zoom:normal}.yui3-skin-sam .yui3-menu-content,.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-content{font-size:93%;line-height:1.5;*line-height:1.45;border:solid 1px #808080;background:#fff;padding:3px 0}.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-content{font-size:100%}.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-content{line-height:2;*line-height:1.9;background:url(../../../../assets/skins/sam/sprite.png) repeat-x 0 0;padding:0}.yui3-skin-sam .yui3-menu ul,.yui3-skin-sam .yui3-menu ul ul{margin-top:3px;padding-top:3px;border-top:solid 1px #ccc}.yui3-skin-sam .yui3-menu ul.first-of-type{border:0;margin:0;padding:0}.yui3-skin-sam .yui3-menu-horizontal ul{padding:0;margin:0;border:0}.yui3-skin-sam .yui3-menu li,.yui3-skin-sam .yui3-menu .yui3-menu li{_border-bottom:solid 1px #fff}.yui3-skin-sam .yui3-menu-horizontal li{_border-bottom:0}.yui3-skin-sam .yui3-menubuttonnav li{border-right:solid 1px #ccc}.yui3-skin-sam .yui3-splitbuttonnav li{border-right:solid 1px #808080}.yui3-skin-sam .yui3-menubuttonnav li li,.yui3-skin-sam .yui3-splitbuttonnav li li{border-right:0}.yui3-skin-sam .yui3-menu-label,.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label,.yui3-skin-sam .yui3-menuitem-content,.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menuitem-content{padding:0 1em;color:#000;text-decoration:none;cursor:default;float:none;border:0;margin:0}.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label,.yui3-skin-sam .yui3-menu-horizontal .yui3-menuitem-content{padding:0 10px;border-style:solid;border-color:#808080;border-width:1px 0;margin:-1px 0;float:left;width:auto}.yui3-skin-sam .yui3-menu-label,.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label{background:url(vertical-menu-submenu-indicator.png) right center no-repeat}.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label{background:url(../../../../assets/skins/sam/sprite.png) repeat-x 0 0}.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label,.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label{background-image:none}.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label{padding-right:0}.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label em{font-style:normal;padding-right:20px;display:block;background:url(horizontal-menu-submenu-indicator.png) right center no-repeat}.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label{padding:0}.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label a{float:left;width:auto;color:#000;text-decoration:none;cursor:default;padding:0 5px 0 10px}.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label .yui3-menu-toggle{padding:0;border-left:solid 1px #ccc;width:15px;overflow:hidden;text-indent:-1000px;background:url(horizontal-menu-submenu-indicator.png) 3px center no-repeat}.yui3-skin-sam .yui3-menu-label-active,.yui3-skin-sam .yui3-menu-label-menuvisible,.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label-active,.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label-menuvisible{background-color:#b3d4ff}.yui3-skin-sam .yui3-menuitem-active .yui3-menuitem-content,.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menuitem-active .yui3-menuitem-content{background-image:none;background-color:#b3d4ff;border-left-width:0;margin-left:0}.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label-active,.yui3-skin-sam .yui3-menu-horizontal .yui3-menuitem-active .yui3-menuitem-content,.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label-menuvisible{border-color:#7d98b8;background:url(../../../../assets/skins/sam/sprite.png) repeat-x 0 -1700px}.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label-active,.yui3-skin-sam .yui3-menubuttonnav .yui3-menuitem-active .yui3-menuitem-content,.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label-menuvisible,.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-active,.yui3-skin-sam .yui3-splitbuttonnav .yui3-menuitem-active .yui3-menuitem-content,.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-menuvisible{border-left-width:1px;margin-left:-1px}.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-menuvisible{border-color:#808080;background:transparent}.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-menuvisible .yui3-menu-toggle{border-color:#7d98b8;background:url(horizontal-menu-submenu-toggle.png) left center no-repeat}#yui3-css-stamp.skin-sam-node-menunav{display:none}
diff --git a/js/yui3/node-menunav/assets/skins/sam/vertical-menu-submenu-indicator.png b/js/yui3/node-menunav/assets/skins/sam/vertical-menu-submenu-indicator.png
new file mode 100644
index 000000000..cfc46b8ac
--- /dev/null
+++ b/js/yui3/node-menunav/assets/skins/sam/vertical-menu-submenu-indicator.png
Binary files differ
diff --git a/js/yui3/node-menunav/node-menunav-min.js b/js/yui3/node-menunav/node-menunav-min.js
new file mode 100644
index 000000000..86edb62ea
--- /dev/null
+++ b/js/yui3/node-menunav/node-menunav-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("node-menunav",function(e,t){var n=e.UA,r=e.later,i=e.ClassNameManager.getClassName,s="menu",o="menuitem",u="hidden",a="parentNode",f="children",l="offsetHeight",c="offsetWidth",h="px",p="id",d=".",v="handledMouseOut",m="handledMouseOver",g="active",y="label",b="a",w="mousedown",E="keydown",S="click",x="",T="first-of-type",N="role",C="presentation",k="descendants",L="UI",A="activeDescendant",O="useARIA",M="aria-hidden",_="content",D="host",P=A+"Change",H="autoSubmenuDisplay",B="mouseOutHideDelay",j=i(s),F=i(s,u),I=i(s,"horizontal"),q=i(s,y),R=i(s,y,g),U=i(s,y,s+"visible"),z=i(o),W=i(o,g),X=d+j,V=d+i(s,"toggle"),$=d+i(s,_),J=d+q,K=">"+$+">ul>li>a",Q=">"+$+">ul>li>"+J+">a:first-child",G=function(e){var t=e.previous(),n;return t||(n=e.get(a).get(f),t=n.item(n.size()-1)),t},Y=function(e){var t=e.next();return t||(t=e.get(a).get(f).item(0)),t},Z=function(e){var t=!1;return e&&(t=e.get("nodeName").toLowerCase()===b),t},et=function(e){return e.hasClass(z)},tt=function(e){return e.hasClass(q)},nt=function(e){return e.hasClass(I)},rt=function(e){return e.hasClass(U)},it=function(e){return Z(e)?e:e.one(b)},st=function(e,t,n){var r;return e&&(e.hasClass(t)&&(r=e),!r&&n&&(r=e.ancestor(d+t))),r},ot=function(e){return e.ancestor(X)},ut=function(e,t){return st(e,j,t)},at=function(e,t){var n;return e&&(n=st(e,z,t)),n},ft=function(e,t){var n;return e&&(t?n=st(e,q,t):n=st(e,q)||e.one(d+q)),n},lt=function(e,t){var n;return e&&(n=at(e,t)||ft(e,t)),n},ct=function(e){return lt(e.one("li"))},ht=function(e){return et(e)?W:R},pt=function(e,t){return e&&!e[m]&&(e.compareTo(t)||e.contains(t))},dt=function(e,t){return e&&!e[v]&&!e.compareTo(t)&&!e.contains(t)},vt=function(){vt.superclass.constructor.apply(this,arguments)};vt.NAME="nodeMenuNav",vt.NS="menuNav",vt.SHIM_TEMPLATE_TITLE="Menu Stacking Shim",vt.SHIM_TEMPLATE='<iframe frameborder="0" tabindex="-1" class="'+i("shim")+'" title="'+vt.SHIM_TEMPLATE_TITLE+'" src="javascript:false;"></iframe>',vt.ATTRS={useARIA:{value:!0,writeOnce:!0,lazyAdd:!1,setter:function(t){var n=this.get(D),r,u,a,f;t&&(n.set(N,s),n.all("ul,li,"+$).set(N,C),n.all(d+i(o,_)).set(N,o),n.all(d+q).each(function(t){r=t,u=t.one(V),u&&(u.set(N,C),r=u.previous()),r.set(N,o),r.set("aria-haspopup",!0),a=t.next(),a&&(a.set(N,s),r=a.previous(),u=r.one(V),u&&(r=u),f=e.stamp(r),r.get(p)||r.set(p,f),a.set("aria-labelledby",f),a.set(M,!0))}))}},autoSubmenuDisplay:{value:!0,writeOnce:!0},submenuShowDelay:{value:250,writeOnce:!0},submenuHideDelay:{value:250,writeOnce:!0},mouseOutHideDelay:{value:750,writeOnce:!0}},e.extend(vt,e.Plugin.Base,{_rootMenu:null,_activeItem:null,_activeMenu:null,_hasFocus:!1,_blockMouseEvent:!1,_currentMouseX:0,_movingToSubmenu:!1,_showSubmenuTimer:null,_hideSubmenuTimer:null,_hideAllSubmenusTimer:null,_firstItem:null,initializer:function(t){var n=this,r=this.get(D),i=[],s;r&&(n._rootMenu=r,r.all("ul:first-child").addClass(T),r.all(X).addClass(F),i.push(r.on("mouseover",n._onMouseOver,n)),i.push(r.on("mouseout",n._onMouseOut,n)),i.push(r.on("mousemove",n._onMouseMove,n)),i.push(r.on(w,n._toggleSubmenuDisplay,n)),i.push(e.on("key",n._toggleSubmenuDisplay,r,"down:13",n)),i.push(r.on(S,n._toggleSubmenuDisplay,n)),i.push(r.on("keypress",n._onKeyPress,n)),i.push(r.on(E,n._onKeyDown,n)),s=r.get("ownerDocument"),i.push(s.on(w,n._onDocMouseDown,n)),i.push(s.on("focus",n._onDocFocus,n)),this._eventHandlers=i,n._initFocusManager())},destructor:function(){var t=this._eventHandlers;t&&(e.Array.each(t,function(e){e.detach()}),this._eventHandlers=null),this.get(D).unplug("focusManager")},_isRoot:function(e){return this._rootMenu.compareTo(e)},_getTopmostSubmenu:function(e){var t=this,n=ot(e),r;return n?t._isRoot(n)?r=e:r=t._getTopmostSubmenu(n):r=e,r},_clearActiveItem:function(){var e=this,t=e._activeItem;t&&t.removeClass(ht(t)),e._activeItem=null},_setActiveItem:function(e){var t=this;e&&(t._clearActiveItem(),e.addClass(ht(e)),t._activeItem=e)},_focusItem:function(e){var t=this,n,r;e&&t._hasFocus&&(n=ot(e),r=it(e),n&&!n.compareTo(t._activeMenu)&&(t._activeMenu=n,t._initFocusManager()),t._focusManager.focus(r))},_showMenu:function(t){var r=ot(t),i=t.get(a),s=i.getXY();this.get(O)&&t.set(M,!1),nt(r)?s[1]=s[1]+i.get(l):s[0]=s[0]+i.get(c),t.setXY(s),n.ie&&n.ie<8&&(n.ie===6&&!t.hasIFrameShim&&(t.appendChild(e.Node.create(vt.SHIM_TEMPLATE)),t.hasIFrameShim=!0),t.setStyles({height:x,width:x}),t.setStyles({height:t.get(l)+h,width:t.get(c)+h})),t.previous().addClass(U),t.removeClass(F)},_hideMenu:function(e,t){var n=this,r=e.previous(),i;r.removeClass(U),t&&(n._focusItem(r),n._setActiveItem(r)),i=e.one(d+W),i&&i.removeClass(W),e.setStyles({left:x,top:x}),e.addClass(F),n.get(O)&&e.set(M,!0)},_hideAllSubmenus:function(t){var n=this;t.all(X).each(e.bind(function(e){n._hideMenu(e)},n))},_cancelShowSubmenuTimer:function(){var e=this,t=e._showSubmenuTimer;t&&(t.cancel(),e._showSubmenuTimer=null)},_cancelHideSubmenuTimer:function(){var e=this,t=e._hideSubmenuTimer;t&&(t.cancel(),e._hideSubmenuTimer=null)},_initFocusManager:function(){var t=this,n=t._rootMenu,r=t._activeMenu||n,i=t._isRoot(r)?x:"#"+r.get("id"),s=t._focusManager,o,u,a;nt(r)?(u=i+K+","+i+Q,o={next:"down:39",previous:"down:37"}):(u=i+K,o={next:"down:40",previous:"down:38"}),s?(s.set(A,-1),s.set(k,u),s.set("keys",o)):(n.plug(e.Plugin.NodeFocusManager,{descendants:u,keys:o,circular:!0}),s=n.focusManager,a="#"+n.get("id")+X+" a,"+V,n.all(a).set("tabIndex",-1),s.on(P,this._onActiveDescendantChange,s,this),s.after(P,this._afterActiveDescendantChange,s,this),t._focusManager=s)},_onActiveDescendantChange:function(e,t){e.src===L&&t._activeMenu&&!t._movingToSubmenu&&t._hideAllSubmenus(t._activeMenu)},_afterActiveDescendantChange:function(e,t){var n;e.src===L&&(n=lt(this.get(k).item(e.newVal),!0),t._setActiveItem(n))},_onDocFocus:function(e){var t=this,n=t._activeItem,r=e.target,i;t._rootMenu.contains(r)?t._hasFocus?(i=ot(r),t._activeMenu.compareTo(i)||(t._activeMenu=i,t._initFocusManager(),t._focusManager.set(A,r),t._setActiveItem(lt(r,!0)))):(t._hasFocus=!0
+,n=lt(r,!0),n&&t._setActiveItem(n)):(t._clearActiveItem(),t._cancelShowSubmenuTimer(),t._hideAllSubmenus(t._rootMenu),t._activeMenu=t._rootMenu,t._initFocusManager(),t._focusManager.set(A,0),t._hasFocus=!1)},_onMenuMouseOver:function(e,t){var n=this,r=n._hideAllSubmenusTimer;r&&(r.cancel(),n._hideAllSubmenusTimer=null),n._cancelHideSubmenuTimer(),e&&!e.compareTo(n._activeMenu)&&(n._activeMenu=e,n._hasFocus&&n._initFocusManager()),n._movingToSubmenu&&nt(e)&&(n._movingToSubmenu=!1)},_hideAndFocusLabel:function(){var e=this,t=e._activeMenu,n;e._hideAllSubmenus(e._rootMenu),t&&(n=e._getTopmostSubmenu(t),e._focusItem(n.previous()))},_onMenuMouseOut:function(e,t){var n=this,i=n._activeMenu,s=t.relatedTarget,o=n._activeItem,u,a;i&&!i.contains(s)&&(u=ot(i),u&&!u.contains(s)?n.get(B)>0&&(n._cancelShowSubmenuTimer(),n._hideAllSubmenusTimer=r(n.get(B),n,n._hideAndFocusLabel)):o&&(a=ot(o),n._isRoot(a)||n._focusItem(a.previous())))},_onMenuLabelMouseOver:function(e,t){var n=this,i=n._activeMenu,s=n._isRoot(i),o=n.get(H)&&s||!s,u=n.get("submenuShowDelay"),a,f=function(t){n._cancelHideSubmenuTimer(),n._cancelShowSubmenuTimer(),rt(e)||(a=e.next(),a&&(n._hideAllSubmenus(i),n._showSubmenuTimer=r(t,n,n._showMenu,a)))};n._focusItem(e),n._setActiveItem(e),o&&(n._movingToSubmenu?n._hoverTimer=r(u,n,function(){f(0)}):f(u))},_onMenuLabelMouseOut:function(e,t){var n=this,i=n._isRoot(n._activeMenu),s=n.get(H)&&i||!i,o=t.relatedTarget,u=e.next(),a=n._hoverTimer;a&&a.cancel(),n._clearActiveItem(),s&&(n._movingToSubmenu&&!n._showSubmenuTimer&&u?n._hideSubmenuTimer=r(n.get("submenuHideDelay"),n,n._hideMenu,u):!n._movingToSubmenu&&u&&(!o||o&&!u.contains(o)&&!o.compareTo(u))&&(n._cancelShowSubmenuTimer(),n._hideMenu(u)))},_onMenuItemMouseOver:function(e,t){var n=this,r=n._activeMenu,i=n._isRoot(r),s=n.get(H)&&i||!i;n._focusItem(e),n._setActiveItem(e),s&&!n._movingToSubmenu&&n._hideAllSubmenus(r)},_onMenuItemMouseOut:function(e,t){this._clearActiveItem()},_onVerticalMenuKeyDown:function(e){var t=this,n=t._activeMenu,r=t._rootMenu,i=e.target,s=!1,o=e.keyCode,u,f,l,c;switch(o){case 37:f=ot(n),f&&nt(f)?(t._hideMenu(n),l=G(n.get(a)),c=lt(l),c&&(tt(c)?(u=c.next(),u?(t._showMenu(u),t._focusItem(ct(u)),t._setActiveItem(ct(u))):(t._focusItem(c),t._setActiveItem(c))):(t._focusItem(c),t._setActiveItem(c)))):t._isRoot(n)||t._hideMenu(n,!0),s=!0;break;case 39:tt(i)?(u=i.next(),u&&(t._showMenu(u),t._focusItem(ct(u)),t._setActiveItem(ct(u)))):nt(r)&&(u=t._getTopmostSubmenu(n),l=Y(u.get(a)),c=lt(l),t._hideAllSubmenus(r),c&&(tt(c)?(u=c.next(),u?(t._showMenu(u),t._focusItem(ct(u)),t._setActiveItem(ct(u))):(t._focusItem(c),t._setActiveItem(c))):(t._focusItem(c),t._setActiveItem(c)))),s=!0}s&&e.preventDefault()},_onHorizontalMenuKeyDown:function(e){var t=this,n=t._activeMenu,r=e.target,i=lt(r,!0),s=!1,o=e.keyCode,u;o===40&&(t._hideAllSubmenus(n),tt(i)&&(u=i.next(),u&&(t._showMenu(u),t._focusItem(ct(u)),t._setActiveItem(ct(u))),s=!0)),s&&e.preventDefault()},_onMouseMove:function(e){var t=this;r(10,t,function(){t._currentMouseX=e.pageX})},_onMouseOver:function(e){var t=this,n,r,i,s,o;t._blockMouseEvent?t._blockMouseEvent=!1:(n=e.target,r=ut(n,!0),i=ft(n,!0),o=at(n,!0),pt(r,n)&&(t._onMenuMouseOver(r,e),r[m]=!0,r[v]=!1,s=ot(r),s&&(s[v]=!0,s[m]=!1)),pt(i,n)&&(t._onMenuLabelMouseOver(i,e),i[m]=!0,i[v]=!1),pt(o,n)&&(t._onMenuItemMouseOver(o,e),o[m]=!0,o[v]=!1))},_onMouseOut:function(e){var t=this,n=t._activeMenu,r=!1,i,s,o,u,a,f;t._movingToSubmenu=n&&!nt(n)&&e.pageX-5>t._currentMouseX,i=e.target,s=e.relatedTarget,o=ut(i,!0),u=ft(i,!0),f=at(i,!0),dt(u,s)&&(t._onMenuLabelMouseOut(u,e),u[v]=!0,u[m]=!1),dt(f,s)&&(t._onMenuItemMouseOut(f,e),f[v]=!0,f[m]=!1),u&&(a=u.next(),a&&s&&(s.compareTo(a)||a.contains(s))&&(r=!0));if(dt(o,s)||r)t._onMenuMouseOut(o,e),o[v]=!0,o[m]=!1},_toggleSubmenuDisplay:function(e){var t=this,r=e.target,i=ft(r,!0),s=e.type,o,u,a,f,l,c;if(i){o=Z(r)?r:r.ancestor(Z);if(o){a=o.getAttribute("href",2),f=a.indexOf("#"),l=a.length;if(f===0&&l>1){c=a.substr(1,l),u=i.next();if(u&&u.get(p)===c){if(s===w||s===E)(n.opera||n.gecko||n.ie)&&s===E&&!t._preventClickHandle&&(t._preventClickHandle=t._rootMenu.on("click",function(e){e.preventDefault(),t._preventClickHandle.detach(),t._preventClickHandle=null})),s==w&&(e.preventDefault(),e.stopImmediatePropagation(),t._hasFocus=!0),t._isRoot(ot(r))?rt(i)?(t._hideMenu(u),t._focusItem(i),t._setActiveItem(i)):(t._hideAllSubmenus(t._rootMenu),t._showMenu(u),t._focusItem(ct(u)),t._setActiveItem(ct(u))):t._activeItem==i?(t._showMenu(u),t._focusItem(ct(u)),t._setActiveItem(ct(u))):i._clickHandle||(i._clickHandle=i.on("click",function(){t._hideAllSubmenus(t._rootMenu),t._hasFocus=!1,t._clearActiveItem(),i._clickHandle.detach(),i._clickHandle=null}));s===S&&e.preventDefault()}}}}},_onKeyPress:function(e){switch(e.keyCode){case 37:case 38:case 39:case 40:e.preventDefault()}},_onKeyDown:function(e){var t=this,i=t._activeItem,s=e.target,o=ot(s),u;o&&(t._activeMenu=o,nt(o)?t._onHorizontalMenuKeyDown(e):t._onVerticalMenuKeyDown(e),e.keyCode===27&&(t._isRoot(o)?i&&(tt(i)&&rt(i)?(u=i.next(),u&&t._hideMenu(u)):(t._focusManager.blur(),t._clearActiveItem(),t._hasFocus=!1)):(n.opera?r(0,t,function(){t._hideMenu(o,!0)}):t._hideMenu(o,!0),e.stopPropagation(),t._blockMouseEvent=n.gecko?!0:!1)))},_onDocMouseDown:function(e){var t=this,r=t._rootMenu,i=e.target;!r.compareTo(i)&&!r.contains(i)&&(t._hideAllSubmenus(r),n.webkit&&(t._hasFocus=!1,t._clearActiveItem()))}}),e.namespace("Plugin"),e.Plugin.NodeMenuNav=vt},"3.17.2",{requires:["node","classnamemanager","plugin","node-focusmanager"],skinnable:!0});
diff --git a/js/yui3/node-pluginhost/node-pluginhost-min.js b/js/yui3/node-pluginhost/node-pluginhost-min.js
new file mode 100644
index 000000000..89e793718
--- /dev/null
+++ b/js/yui3/node-pluginhost/node-pluginhost-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("node-pluginhost",function(e,t){e.Node.plug=function(){var t=e.Array(arguments);return t.unshift(e.Node),e.Plugin.Host.plug.apply(e.Base,t),e.Node},e.Node.unplug=function(){var t=e.Array(arguments);return t.unshift(e.Node),e.Plugin.Host.unplug.apply(e.Base,t),e.Node},e.mix(e.Node,e.Plugin.Host,!1,null,1),e.Object.each(e.Node._instances,function(t){e.Plugin.Host.apply(t)}),e.NodeList.prototype.plug=function(){var t=arguments;return e.NodeList.each(this,function(n){e.Node.prototype.plug.apply(e.one(n),t)}),this},e.NodeList.prototype.unplug=function(){var t=arguments;return e.NodeList.each(this,function(n){e.Node.prototype.unplug.apply(e.one(n),t)}),this}},"3.17.2",{requires:["node-base","pluginhost"]});
diff --git a/js/yui3/node-screen/node-screen-min.js b/js/yui3/node-screen/node-screen-min.js
new file mode 100644
index 000000000..feb4cdbb1
--- /dev/null
+++ b/js/yui3/node-screen/node-screen-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("node-screen",function(e,t){e.each(["winWidth","winHeight","docWidth","docHeight","docScrollX","docScrollY"],function(t){e.Node.ATTRS[t]={getter:function(){var n=Array.prototype.slice.call(arguments);return n.unshift(e.Node.getDOMNode(this)),e.DOM[t].apply(this,n)}}}),e.Node.ATTRS.scrollLeft={getter:function(){var t=e.Node.getDOMNode(this);return"scrollLeft"in t?t.scrollLeft:e.DOM.docScrollX(t)},setter:function(t){var n=e.Node.getDOMNode(this);n&&("scrollLeft"in n?n.scrollLeft=t:(n.document||n.nodeType===9)&&e.DOM._getWin(n).scrollTo(t,e.DOM.docScrollY(n)))}},e.Node.ATTRS.scrollTop={getter:function(){var t=e.Node.getDOMNode(this);return"scrollTop"in t?t.scrollTop:e.DOM.docScrollY(t)},setter:function(t){var n=e.Node.getDOMNode(this);n&&("scrollTop"in n?n.scrollTop=t:(n.document||n.nodeType===9)&&e.DOM._getWin(n).scrollTo(e.DOM.docScrollX(n),t))}},e.Node.importMethod(e.DOM,["getXY","setXY","getX","setX","getY","setY","swapXY"]),e.Node.ATTRS.region={getter:function(){var t=this.getDOMNode(),n;return t&&!t.tagName&&t.nodeType===9&&(t=t.documentElement),e.DOM.isWindow(t)?n=e.DOM.viewportRegion(t):n=e.DOM.region(t),n}},e.Node.ATTRS.viewportRegion={getter:function(){return e.DOM.viewportRegion(e.Node.getDOMNode(this))}},e.Node.importMethod(e.DOM,"inViewportRegion"),e.Node.prototype.intersect=function(t,n){var r=e.Node.getDOMNode(this);return e.instanceOf(t,e.Node)&&(t=e.Node.getDOMNode(t)),e.DOM.intersect(r,t,n)},e.Node.prototype.inRegion=function(t,n,r){var i=e.Node.getDOMNode(this);return e.instanceOf(t,e.Node)&&(t=e.Node.getDOMNode(t)),e.DOM.inRegion(i,t,n,r)}},"3.17.2",{requires:["dom-screen","node-base"]});
diff --git a/js/yui3/node-scroll-info/node-scroll-info-min.js b/js/yui3/node-scroll-info/node-scroll-info-min.js
new file mode 100644
index 000000000..92c80aaee
--- /dev/null
+++ b/js/yui3/node-scroll-info/node-scroll-info-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("node-scroll-info",function(e,t){var n=e.config.doc,r=e.config.win,i="scroll",s="scrollDown",o="scrollLeft",u="scrollRight",a="scrollUp",f="scrollToBottom",l="scrollToLeft",c="scrollToRight",h="scrollToTop";e.Plugin.ScrollInfo=e.Base.create("scrollInfoPlugin",e.Plugin.Base,[],{initializer:function(e){this._host=e.host,this._hostIsBody=this._host.get("nodeName").toLowerCase()==="body",this._scrollDelay=this.get("scrollDelay"),this._scrollMargin=this.get("scrollMargin"),this._scrollNode=this._getScrollNode(),this.refreshDimensions(),this._lastScroll=this.getScrollInfo(),this._bind()},destructor:function(){(new e.EventHandle(this._events)).detach(),this._events=null},getOffscreenNodes:function(t,n){typeof n=="undefined"&&(n=this._scrollMargin);var r=e.Selector.query(t||"*",this._host._node);return new e.NodeList(e.Array.filter(r,function(e){return!this._isElementOnscreen(e,n)},this))},getOnscreenNodes:function(t,n){typeof n=="undefined"&&(n=this._scrollMargin);var r=e.Selector.query(t||"*",this._host._node);return new e.NodeList(e.Array.filter(r,function(e){return this._isElementOnscreen(e,n)},this))},getScrollInfo:function(){var e=this._scrollNode,t=this._lastScroll,n=this._scrollMargin,r=e.scrollLeft,i=e.scrollHeight,s=e.scrollTop,o=e.scrollWidth,u=s+this._height,a=r+this._width;return{atBottom:u>i-n,atLeft:r<n,atRight:a>o-n,atTop:s<n,isScrollDown:t&&s>t.scrollTop,isScrollLeft:t&&r<t.scrollLeft,isScrollRight:t&&r>t.scrollLeft,isScrollUp:t&&s<t.scrollTop,scrollBottom:u,scrollHeight:i,scrollLeft:r,scrollRight:a,scrollTop:s,scrollWidth:o}},isNodeOnscreen:function(t,n){return t=e.one(t),!!t&&!!this._isElementOnscreen(t._node,n)},refreshDimensions:function(){var t=n.documentElement;e.UA.ios||e.UA.android&&e.UA.chrome?(this._winHeight=r.innerHeight,this._winWidth=r.innerWidth):(this._winHeight=t.clientHeight,this._winWidth=t.clientWidth),this._hostIsBody?(this._height=this._winHeight,this._width=this._winWidth):(this._height=this._scrollNode.clientHeight,this._width=this._scrollNode.clientWidth),this._refreshHostBoundingRect()},_bind:function(){var t=e.one("win");this._events=[this.after({scrollDelayChange:this._afterScrollDelayChange,scrollMarginChange:this._afterScrollMarginChange}),t.on("windowresize",this._afterResize,this)],this._hostIsBody?this._events.push(t.after("scroll",this._afterHostScroll,this)):this._events.push(t.after("scroll",this._afterWindowScroll,this),this._host.after("scroll",this._afterHostScroll,this))},_getScrollNode:function(){return this._hostIsBody&&!e.UA.webkit?n.documentElement:e.Node.getDOMNode(this._host)},_isElementOnscreen:function(e,t){var n=this._hostRect,r=e.getBoundingClientRect();return typeof t=="undefined"&&(t=this._scrollMargin),!(r.top>n.bottom+t||r.bottom<n.top-t||r.right<n.left-t||r.left>n.right+t)},_refreshHostBoundingRect:function(){var e=this._winHeight,t=this._winWidth,n;this._hostIsBody?(n={bottom:e,height:e,left:0,right:t,top:0,width:t},this._isHostOnscreen=!0):n=this._scrollNode.getBoundingClientRect(),this._hostRect=n},_triggerScroll:function(t){var n=this.getScrollInfo(),r=e.merge(t,n),p=this._lastScroll;this._lastScroll=n,this.fire(i,r),n.isScrollLeft?this.fire(o,r):n.isScrollRight&&this.fire(u,r),n.isScrollUp?this.fire(a,r):n.isScrollDown&&this.fire(s,r),n.atBottom&&(!p.atBottom||n.scrollHeight>p.scrollHeight)&&this.fire(f,r),n.atLeft&&!p.atLeft&&this.fire(l,r),n.atRight&&(!p.atRight||n.scrollWidth>p.scrollWidth)&&this.fire(c,r),n.atTop&&!p.atTop&&this.fire(h,r)},_afterHostScroll:function(e){var t=this;clearTimeout(this._scrollTimeout),this._scrollTimeout=setTimeout(function(){t._triggerScroll(e)},this._scrollDelay)},_afterResize:function(){this.refreshDimensions()},_afterScrollDelayChange:function(e){this._scrollDelay=e.newVal},_afterScrollMarginChange:function(e){this._scrollMargin=e.newVal},_afterWindowScroll:function(){this._refreshHostBoundingRect()}},{NS:"scrollInfo",ATTRS:{scrollDelay:{value:50},scrollMargin:{value:50}}})},"3.17.2",{requires:["array-extras","base-build","event-resize","node-pluginhost","plugin","selector"]});
diff --git a/js/yui3/node-style/node-style-min.js b/js/yui3/node-style/node-style-min.js
new file mode 100644
index 000000000..69079e2d3
--- /dev/null
+++ b/js/yui3/node-style/node-style-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("node-style",function(e,t){(function(e){e.mix(e.Node.prototype,{setStyle:function(t,n){return e.DOM.setStyle(this._node,t,n),this},setStyles:function(t){return e.DOM.setStyles(this._node,t),this},getStyle:function(t){return e.DOM.getStyle(this._node,t)},getComputedStyle:function(t){return e.DOM.getComputedStyle(this._node,t)}}),e.NodeList.importMethod(e.Node.prototype,["getStyle","getComputedStyle","setStyle","setStyles"])})(e);var n=e.Node;e.mix(n.prototype,{show:function(e){return e=arguments[arguments.length-1],this.toggleView(!0,e),this},_show:function(){this.removeAttribute("hidden"),this.setStyle("display","")},_isHidden:function(){return this.hasAttribute("hidden")||e.DOM.getComputedStyle(this._node,"display")==="none"},toggleView:function(e,t){return this._toggleView.apply(this,arguments),this},_toggleView:function(e,t){return t=arguments[arguments.length-1],typeof e!="boolean"&&(e=this._isHidden()?1:0),e?this._show():this._hide(),typeof t=="function"&&t.call(this),this},hide:function(e){return e=arguments[arguments.length-1],this.toggleView(!1,e),this},_hide:function(){this.setAttribute("hidden","hidden"),this.setStyle("display","none")}}),e.NodeList.importMethod(e.Node.prototype,["show","hide","toggleView"])},"3.17.2",{requires:["dom-style","node-base"]});
diff --git a/js/yui3/oop/oop-min.js b/js/yui3/oop/oop-min.js
new file mode 100644
index 000000000..1b62fd524
--- /dev/null
+++ b/js/yui3/oop/oop-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("oop",function(e,t){function a(t,n,i,s,o){if(t&&t[o]&&t!==e)return t[o].call(t,n,i);switch(r.test(t)){case 1:return r[o](t,n,i);case 2:return r[o](e.Array(t,0,!0),n,i);default:return e.Object[o](t,n,i,s)}}var n=e.Lang,r=e.Array,i=Object.prototype,s="_~yuim~_",o=i.hasOwnProperty,u=i.toString;e.augment=function(t,n,r,i,s){var a=t.prototype,f=a&&n,l=n.prototype,c=a||t,h,p,d,v,m;return s=s?e.Array(s):[],f&&(p={},d={},v={},h=function(e,t){if(r||!(t in a))u.call(e)==="[object Function]"?(v[t]=e,p[t]=d[t]=function(){return m(this,e,arguments)}):p[t]=e},m=function(e,t,r){for(var i in v)o.call(v,i)&&e[i]===d[i]&&(e[i]=v[i]);return n.apply(e,s),t.apply(e,r)},i?e.Array.each(i,function(e){e in l&&h(l[e],e)}):e.Object.each(l,h,null,!0)),e.mix(c,p||l,r,i),f||n.apply(c,s),t},e.aggregate=function(t,n,r,i){return e.mix(t,n,r,i,0,!0)},e.extend=function(t,n,r,s){(!n||!t)&&e.error("extend failed, verify dependencies");var o=n.prototype,u=e.Object(o);return t.prototype=u,u.constructor=t,t.superclass=o,n!=Object&&o.constructor==i.constructor&&(o.constructor=n),r&&e.mix(u,r,!0),s&&e.mix(t,s,!0),t},e.each=function(e,t,n,r){return a(e,t,n,r,"each")},e.some=function(e,t,n,r){return a(e,t,n,r,"some")},e.clone=function(t,r,i,o,u,a){var f,l,c;if(!n.isObject(t)||e.instanceOf(t,YUI)||t.addEventListener||t.attachEvent)return t;l=a||{};switch(n.type(t)){case"date":return new Date(t);case"regexp":return t;case"function":return t;case"array":f=[];break;default:if(t[s])return l[t[s]];c=e.guid(),f=r?{}:e.Object(t),t[s]=c,l[c]=t}return e.each(t,function(n,a){(a||a===0)&&(!i||i.call(o||this,n,a,this,t)!==!1)&&a!==s&&a!="prototype"&&(this[a]=e.clone(n,r,i,o,u||t,l))},f),a||(e.Object.each(l,function(e,t){if(e[s])try{delete e[s]}catch(n){e[s]=null}},this),l=null),f},e.bind=function(t,r){var i=arguments.length>2?e.Array(arguments,2,!0):null;return function(){var s=n.isString(t)?r[t]:t,o=i?i.concat(e.Array(arguments,0,!0)):arguments;return s.apply(r||s,o)}},e.rbind=function(t,r){var i=arguments.length>2?e.Array(arguments,2,!0):null;return function(){var s=n.isString(t)?r[t]:t,o=i?e.Array(arguments,0,!0).concat(i):arguments;return s.apply(r||s,o)}}},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/overlay/assets/overlay-core.css b/js/yui3/overlay/assets/overlay-core.css
new file mode 100644
index 000000000..bf0e5aea5
--- /dev/null
+++ b/js/yui3/overlay/assets/overlay-core.css
@@ -0,0 +1,18 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-overlay {
+ position:absolute;
+}
+
+.yui3-overlay-hidden {
+ visibility:hidden
+}
+
+.yui3-widget-tmp-forcesize .yui3-overlay-content {
+ overflow:hidden !important;
+} \ No newline at end of file
diff --git a/js/yui3/overlay/assets/skins/night/overlay.css b/js/yui3/overlay/assets/skins/night/overlay.css
new file mode 100644
index 000000000..04b9df09a
--- /dev/null
+++ b/js/yui3/overlay/assets/skins/night/overlay.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-overlay{position:absolute}.yui3-overlay-hidden{visibility:hidden}.yui3-widget-tmp-forcesize .yui3-overlay-content{overflow:hidden!important}.yui3-skin-night{background-color:#000;font-family:HelveticaNeue,arial,helvetica,clean,sans-serif;color:#fff}.yui3-skin-night .yui3-overlay-content ul,ol,li{margin:0;padding:0;list-style:none;zoom:1}.yui3-skin-night .yui3-overlay-content li{*float:left}.yui3-skin-night .yui3-overlay-content{background-color:#6d6e6e;-moz-box-shadow:0 0 17px rgba(0,0,0,0.58);-webkit-box-shadow:0 0 17px rgba(0,0,0,0.58);box-shadow:0 0 17px rgba(0,0,0,0.58);-moz-border-radius:7px;-webkit-border-radius:7px;border-radius:7px}.yui3-skin-night .yui3-overlay-content .yui3-widget-hd{background-color:#6d6e6e;-moz-border-radius:7px 7px 0 0;-webkit-border-radius:7px 7px 0 0;border-radius:7px 7px 0 0;color:#fff;margin:0;padding:20px 22px 0;font-size:147%}.yui3-skin-night .yui3-overlay-content .yui3-widget-bd{padding:11px 22px 17px;font-size:92%}.yui3-skin-night .yui3-overlay .yui3-widget-bd li{margin:.04em}.yui3-skin-night .yui3-overlay-content .yui3-widget-ft{background-color:#575858;border-top:solid 1px #494a4a;-moz-border-radius:0 0 7px 7px;-webkit-border-radius:0 0 7px 7px;border-radius:0 0 7px 7px;padding:17px 25px 20px;text-align:center}.yui3-skin-night .yui3-overlay-content .yui3-widget-ft li{margin:3px;display:inline-block}.yui3-skin-night .yui3-overlay-content .yui3-widget-ft li a{border:solid 1px #1b1c1c;border-radius:6px;-moz-box-shadow:0 1px #677478;-webkit-box-shadow:0 1px #677478;box-shadow:0 1px #677478;text-shadow:0 -1px 0 rgba(0,0,0,0.7);font-size:85%;text-align:center;color:#fff;padding:6px 28px;background-color:#2b2d2d;background:-moz-linear-gradient(0% 100% 90deg,#242526 0,#3b3c3d 96%,#2c2d2f 100%);background:-webkit-gradient(linear,left bottom,left top,from(#242526),color-stop(0.96,#3b3c3d),to(#2c2d2f))}.yui3-skin-night .yui3-overlay .yui3-widget-ft li:first-child{margin-left:0}.yui3-skin-night .yui3-overlay .yui3-widget-ft li:last-child{margin-right:0}.yui3-skin-night .yui3-overlay .yui3-widget-ft li:last-child a{border:solid 1px #520e00;-moz-box-shadow:0 1px #7d5d57;-webkit-box-shadow:0 1px #7d5d57;box-shadow:0 1px #7d5d57;background-color:#901704;background:-moz-linear-gradient(100% 0 270deg,#ab1c0b,#7b1400);background:-webkit-gradient(linear,left top,left bottom,from(#ab1c0b),to(#7b1400));margin-right:0}#yui3-widget-mask{background-color:#000;opacity:.5}#yui3-css-stamp.skin-night-overlay{display:none}
diff --git a/js/yui3/overlay/assets/skins/sam/overlay.css b/js/yui3/overlay/assets/skins/sam/overlay.css
new file mode 100644
index 000000000..11d130bec
--- /dev/null
+++ b/js/yui3/overlay/assets/skins/sam/overlay.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-overlay{position:absolute}.yui3-overlay-hidden{visibility:hidden}.yui3-widget-tmp-forcesize .yui3-overlay-content{overflow:hidden!important}#yui3-css-stamp.skin-sam-overlay{display:none}
diff --git a/js/yui3/overlay/overlay-min.js b/js/yui3/overlay/overlay-min.js
new file mode 100644
index 000000000..10df1f7a3
--- /dev/null
+++ b/js/yui3/overlay/overlay-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("overlay",function(e,t){e.Overlay=e.Base.create("overlay",e.Widget,[e.WidgetStdMod,e.WidgetPosition,e.WidgetStack,e.WidgetPositionAlign,e.WidgetPositionConstrain])},"3.17.2",{requires:["widget","widget-stdmod","widget-position","widget-position-align","widget-stack","widget-position-constrain"],skinnable:!0});
diff --git a/js/yui3/paginator-core/paginator-core-min.js b/js/yui3/paginator-core/paginator-core-min.js
new file mode 100644
index 000000000..78f9014df
--- /dev/null
+++ b/js/yui3/paginator-core/paginator-core-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("paginator-core",function(e,t){var n=e.namespace("Paginator").Core=function(){};n.ATTRS={page:{value:1},totalPages:{readOnly:!0,getter:"_getTotalPagesFn"},itemsPerPage:{value:10},totalItems:{value:0}},e.mix(n.prototype,{prevPage:function(){return this.hasPrevPage()&&this.set("page",this.get("page")-1),this},nextPage:function(){return this.hasNextPage()&&this.set("page",this.get("page")+1),this},hasPrevPage:function(){return this.get("page")>1},hasNextPage:function(){return!this.get("totalItems")||this.get("page")<this.get("totalPages")},_getTotalPagesFn:function(){var e=this.get("itemsPerPage");return e<1?1:Math.ceil(this.get("totalItems")/e)}})},"3.17.2",{requires:["base"]});
diff --git a/js/yui3/paginator-url/paginator-url-min.js b/js/yui3/paginator-url/paginator-url-min.js
new file mode 100644
index 000000000..7a35b4de9
--- /dev/null
+++ b/js/yui3/paginator-url/paginator-url-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("paginator-url",function(e,t){function n(){}n.ATTRS={pageUrl:{}},n.prototype={prevPageUrl:function(){return this.hasPrevPage()&&this.formatPageUrl(this.get("page")-1)||null},nextPageUrl:function(){return this.hasNextPage()&&this.formatPageUrl(this.get("page")+1)||null},formatPageUrl:function(t){var n=this.get("pageUrl");return n?e.Lang.sub(n,{page:t||this.get("page")}):null}},e.namespace("Paginator").Url=n,e.Base.mix(e.Paginator,[n])},"3.17.2",{requires:["paginator"]});
diff --git a/js/yui3/paginator/paginator-min.js b/js/yui3/paginator/paginator-min.js
new file mode 100644
index 000000000..27423bce6
--- /dev/null
+++ b/js/yui3/paginator/paginator-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("paginator",function(e,t){e.Paginator=e.mix(e.Base.create("paginator",e.Base,[e.Paginator.Core]),e.Paginator)},"3.17.2",{requires:["paginator-core"]});
diff --git a/js/yui3/panel/assets/panel-core.css b/js/yui3/panel/assets/panel-core.css
new file mode 100644
index 000000000..380d6807f
--- /dev/null
+++ b/js/yui3/panel/assets/panel-core.css
@@ -0,0 +1,29 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-panel {
+ position: absolute;
+}
+.yui3-panel-hidden {
+ visibility: hidden;
+}
+.yui3-widget-tmp-forcesize .yui3-panel-content {
+ overflow: hidden !important;
+}
+.yui3-panel .yui3-widget-hd {
+ position: relative;
+}
+.yui3-panel .yui3-widget-hd .yui3-widget-buttons {
+ position: absolute;
+ top: 0;
+ right: 0;
+}
+.yui3-panel .yui3-widget-ft .yui3-widget-buttons {
+ display: inline-block;
+ *display: inline;
+ zoom: 1;
+}
diff --git a/js/yui3/panel/assets/skins/night/panel.css b/js/yui3/panel/assets/skins/night/panel.css
new file mode 100644
index 000000000..05e526045
--- /dev/null
+++ b/js/yui3/panel/assets/skins/night/panel.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-panel{position:absolute}.yui3-panel-hidden{visibility:hidden}.yui3-widget-tmp-forcesize .yui3-panel-content{overflow:hidden!important}.yui3-panel .yui3-widget-hd{position:relative}.yui3-panel .yui3-widget-hd .yui3-widget-buttons{position:absolute;top:0;right:0}.yui3-panel .yui3-widget-ft .yui3-widget-buttons{display:inline-block;*display:inline;zoom:1}.yui3-skin-night .yui3-panel{color:#fff;font-family:HelveticaNeue,arial,helvetica,clean,sans-serif}.yui3-skin-night .yui3-panel-content{background:#6d6e6e;-webkit-box-shadow:0 0 20px #000;-moz-box-shadow:0 0 20px #000;box-shadow:0 0 20px #000;border:1px solid black;-webkit-border-radius:7px;-moz-border-radius:7px;border-radius:7px}.yui3-skin-night .yui3-panel .yui3-widget-hd{padding:11px 57px 11px 22px;min-height:17px;_height:17px;-webkit-border-top-left-radius:7px;-webkit-border-top-right-radius:7px;-moz-border-radius-topleft:7px;-moz-border-radius-topright:7px;border-top-left-radius:7px;border-top-right-radius:7px;font-weight:bold;color:white;background-color:#555658;background:-moz-linear-gradient(0% 100% 90deg,#343536 0,#555658 96%,#3e3f41 100%);background:-webkit-gradient(linear,left bottom,left top,from(#343536),color-stop(0.96,#555658),to(#3e3f41))}.yui3-skin-night .yui3-panel .yui3-widget-hd .yui3-widget-buttons{padding:11px}.yui3-skin-night .yui3-panel .yui3-widget-bd{padding:11px 22px 17px}.yui3-skin-night .yui3-panel .yui3-widget-ft{background-color:#575858;border-top:1px solid #494a4a;padding:6px 16px 8px;text-align:center;-webkit-border-bottom-right-radius:7px;-webkit-border-bottom-left-radius:7px;-moz-border-radius-bottomright:7px;-moz-border-radius-bottomleft:7px;border-bottom-right-radius:7px;border-bottom-left-radius:7px}.yui3-skin-night .yui3-panel .yui3-widget-ft .yui3-widget-buttons{bottom:0;position:relative;right:auto;width:100%;text-align:center;padding-bottom:0;margin-left:-5px;margin-right:-5px}.yui3-skin-night .yui3-panel .yui3-widget-ft .yui3-button{margin:5px}.yui3-skin-night .yui3-panel .yui3-widget-hd .yui3-button-close{background:transparent;filter:none;border:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;width:22px;height:17px;padding:0;overflow:hidden;vertical-align:top;*font-size:0;*line-height:0;*letter-spacing:-1000px;*color:#86a5ec;*background:url(sprite_icons.png) no-repeat center 3px}.yui3-skin-night .yui3-panel .yui3-widget-hd .yui3-button-close:hover{background-color:#333}.yui3-skin-night .yui3-panel .yui3-widget-hd .yui3-button-close:before{content:url(sprite_icons.png);display:inline-block;text-align:center;font-size:0;line-height:0;width:22px;margin:3px 0 0 1px}.yui3-skin-night .yui3-panel-hidden .yui3-widget-hd .yui3-button-close{display:none}#yui3-css-stamp.skin-night-panel{display:none}
diff --git a/js/yui3/panel/assets/skins/night/sprite_icons.png b/js/yui3/panel/assets/skins/night/sprite_icons.png
new file mode 100644
index 000000000..34fd6cdc6
--- /dev/null
+++ b/js/yui3/panel/assets/skins/night/sprite_icons.png
Binary files differ
diff --git a/js/yui3/panel/assets/skins/sam/panel.css b/js/yui3/panel/assets/skins/sam/panel.css
new file mode 100644
index 000000000..a2cd74652
--- /dev/null
+++ b/js/yui3/panel/assets/skins/sam/panel.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-panel{position:absolute}.yui3-panel-hidden{visibility:hidden}.yui3-widget-tmp-forcesize .yui3-panel-content{overflow:hidden!important}.yui3-panel .yui3-widget-hd{position:relative}.yui3-panel .yui3-widget-hd .yui3-widget-buttons{position:absolute;top:0;right:0}.yui3-panel .yui3-widget-ft .yui3-widget-buttons{display:inline-block;*display:inline;zoom:1}.yui3-skin-sam .yui3-panel-content{-webkit-box-shadow:0 0 5px #333;-moz-box-shadow:0 0 5px #333;box-shadow:0 0 5px #333;border:1px solid black;background:white}.yui3-skin-sam .yui3-panel .yui3-widget-hd{padding:8px 28px 8px 8px;min-height:13px;_height:13px;color:white;background-color:#3961c5;background:-moz-linear-gradient(0% 100% 90deg,#2647a0 7%,#3d67ce 50%,#426fd9 100%);background:-webkit-gradient(linear,left bottom,left top,from(#2647a0),color-stop(0.07,#2647a0),color-stop(0.5,#3d67ce),to(#426fd9))}.yui3-skin-sam .yui3-panel .yui3-widget-hd .yui3-widget-buttons{padding:8px}.yui3-skin-sam .yui3-panel .yui3-widget-bd{padding:10px}.yui3-skin-sam .yui3-panel .yui3-widget-ft{background:#edf5ff;padding:8px;text-align:right}.yui3-skin-sam .yui3-panel .yui3-widget-ft .yui3-button{margin-left:8px}.yui3-skin-sam .yui3-panel .yui3-widget-hd .yui3-button-close{background:transparent;filter:none;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;width:13px;height:13px;padding:0;overflow:hidden;vertical-align:top;*font-size:0;*line-height:0;*letter-spacing:-1000px;*color:#86a5ec;*background:url(sprite_icons.png) no-repeat 1px 1px}.yui3-skin-sam .yui3-panel .yui3-widget-hd .yui3-button-close:before{content:url(sprite_icons.png);display:inline-block;text-align:center;font-size:0;line-height:0;width:13px;margin:1px 0 0 1px}.yui3-skin-sam .yui3-panel-hidden .yui3-widget-hd .yui3-button-close{display:none}#yui3-css-stamp.skin-sam-panel{display:none}
diff --git a/js/yui3/panel/assets/skins/sam/sprite_icons.png b/js/yui3/panel/assets/skins/sam/sprite_icons.png
new file mode 100644
index 000000000..89944ba0f
--- /dev/null
+++ b/js/yui3/panel/assets/skins/sam/sprite_icons.png
Binary files differ
diff --git a/js/yui3/panel/panel-min.js b/js/yui3/panel/panel-min.js
new file mode 100644
index 000000000..46345592b
--- /dev/null
+++ b/js/yui3/panel/panel-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("panel",function(e,t){var n=e.ClassNameManager.getClassName;e.Panel=e.Base.create("panel",e.Widget,[e.WidgetPosition,e.WidgetStdMod,e.WidgetAutohide,e.WidgetButtons,e.WidgetModality,e.WidgetPositionAlign,e.WidgetPositionConstrain,e.WidgetStack],{BUTTONS:{close:{label:"Close",action:"hide",section:"header",template:'<button type="button" />',classNames:n("button","close")}}},{ATTRS:{buttons:{value:["close"]}}})},"3.17.2",{requires:["widget","widget-autohide","widget-buttons","widget-modality","widget-position","widget-position-align","widget-position-constrain","widget-stack","widget-stdmod"],skinnable:!0});
diff --git a/js/yui3/parallel/parallel-min.js b/js/yui3/parallel/parallel-min.js
new file mode 100644
index 000000000..88c3a735e
--- /dev/null
+++ b/js/yui3/parallel/parallel-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("parallel",function(e,t){e.Parallel=function(t){this.config=t||{},this.results=[],this.context=this.config.context||e,this.total=0,this.finished=0},e.Parallel.prototype={results:null,total:null,finished:null,add:function(t){var n=this,r=n.total;return n.total+=1,function(){n.finished++,n.results[r]=t&&t.apply(n.context,arguments)||(arguments.length===1?arguments[0]:e.Array(arguments)),n.test()}},test:function(){var e=this;e.finished>=e.total&&e.callback&&e.callback.call(e.context,e.results,e.data)},done:function(e,t){this.callback=e,this.data=t,this.test()}}},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/pjax-base/pjax-base-min.js b/js/yui3/pjax-base/pjax-base-min.js
new file mode 100644
index 000000000..06b652a33
--- /dev/null
+++ b/js/yui3/pjax-base/pjax-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("pjax-base",function(e,t){function s(){}var n=e.config.win,r=e.ClassNameManager.getClassName("pjax"),i="navigate";s.prototype={initializer:function(){this.publish(i,{defaultFn:this._defNavigateFn}),this.get("html5")&&this._pjaxBindUI()},destructor:function(){this._pjaxEvents&&this._pjaxEvents.detach()},navigate:function(t,n){return t=this._resolveURL(t),this._navigate(t,n)?!0:(this._hasSameOrigin(t)||e.error("Security error: The new URL must be of the same origin as the current URL."),!1)},_isLinkSameOrigin:function(t){var n=e.getLocation(),r=n.protocol,i=n.hostname,s=parseInt(n.port,10)||null,o;return t.get("protocol")!==r||t.get("hostname")!==i?!1:(o=parseInt(t.get("port"),10)||null,r==="http:"?(s||(s=80),o||(o=80)):r==="https:"&&(s||(s=443),o||(o=443)),o===s)},_navigate:function(t,r){t=this._upgradeURL(t);if(!this.hasRoute(t))return!1;r=e.merge(r,{url:t});var s=this._getURL(),o,u;u=t.replace(/(#.*)$/,function(e,t,n){return o=t,e.substring(n)});if(o&&u===s.replace(/#.*$/,"")){if(!this.get("navigateOnHash"))return!1;r.hash=o}return"replace"in r||(r.replace=t===s),this.get("html5")||r.force?this.fire(i,r):n&&(r.replace?n.location.replace(t):n.location=t),!0},_pjaxBindUI:function(){this._pjaxEvents||(this._pjaxEvents=e.one("body").delegate("click",this._onLinkClick,this.get("linkSelector"),this))},_defNavigateFn:function(e){this[e.replace?"replace":"save"](e.url),n&&this.get("scrollToTop")&&setTimeout(function(){n.scroll(0,0)},1)},_onLinkClick:function(e){var t,n,r;if(e.button!==1||e.ctrlKey||e.metaKey)return;t=e.currentTarget;if(t.get("tagName").toUpperCase()!=="A")return;if(!this._isLinkSameOrigin(t))return;n=t.get("href"),n&&(r=this._navigate(n,{originEvent:e}),r&&e.preventDefault())}},s.ATTRS={linkSelector:{value:"a."+r,writeOnce:"initOnly"},navigateOnHash:{value:!1},scrollToTop:{value:!0}},e.PjaxBase=s},"3.17.2",{requires:["classnamemanager","node-event-delegate","router"]});
diff --git a/js/yui3/pjax-content/pjax-content-min.js b/js/yui3/pjax-content/pjax-content-min.js
new file mode 100644
index 000000000..698f8d259
--- /dev/null
+++ b/js/yui3/pjax-content/pjax-content-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("pjax-content",function(e,t){function n(){}n.prototype={getContent:function(t){var n={},r=this.get("contentSelector"),i=e.Node.create(t||""),s=this.get("titleSelector"),o;return r&&i?n.node=i.all(r).toFrag():n.node=i,s&&i&&(o=i.one(s),o&&(n.title=o.get("text"))),n},loadContent:function(t,n,r){var i=t.url;this._request&&this._request.abort(),this.get("addPjaxParam")&&(i=i.replace(/([^#]*)(#.*)?$/,function(e,t,n){return t+=(t.indexOf("?")>-1?"&":"?")+"pjax=1",t+(n||"")})),this._request=e.io(i,{arguments:{route:{req:t,res:n,next:r},url:i},context:this,headers:{"X-PJAX":"true"},timeout:this.get("timeout"),on:{complete:this._onPjaxIOComplete,end:this._onPjaxIOEnd}})},_onPjaxIOComplete:function(e,t,n){var r=this.getContent(t.responseText),i=n.route,s=i.req,o=i.res;s.ioURL=n.url,o.content=r,o.ioResponse=t,i.next()},_onPjaxIOEnd:function(){this._request=null}},n.ATTRS={addPjaxParam:{value:!0},contentSelector:{value:null},titleSelector:{value:"title"},timeout:{value:3e4}},e.PjaxContent=n},"3.17.2",{requires:["io-base","node-base","router"]});
diff --git a/js/yui3/pjax-plugin/pjax-plugin-min.js b/js/yui3/pjax-plugin/pjax-plugin-min.js
new file mode 100644
index 000000000..389ac2315
--- /dev/null
+++ b/js/yui3/pjax-plugin/pjax-plugin-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("pjax-plugin",function(e,t){e.Plugin.Pjax=e.Base.create("pjaxPlugin",e.Pjax,[e.Plugin.Base],{initializer:function(e){this.set("container",e.host)}},{NS:"pjax"})},"3.17.2",{requires:["node-pluginhost","pjax","plugin"]});
diff --git a/js/yui3/pjax/pjax-min.js b/js/yui3/pjax/pjax-min.js
new file mode 100644
index 000000000..91442352a
--- /dev/null
+++ b/js/yui3/pjax/pjax-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("pjax",function(e,t){var n=["loadContent","_defaultRoute"],r="error",i="load";e.Pjax=e.Base.create("pjax",e.Router,[e.PjaxBase,e.PjaxContent],{initializer:function(){this.publish(r,{defaultFn:this._defCompleteFn}),this.publish(i,{defaultFn:this._defCompleteFn})},_defaultRoute:function(e,t,n){var s=t.ioResponse,o=s.status,u=o>=200&&o<300?i:r;this.fire(u,{content:t.content,responseText:s.responseText,status:o,url:e.ioURL}),n()},_defCompleteFn:function(t){var n=this.get("container"),r=t.content;n&&r.node&&n.setHTML(r.node),r.title&&e.config.doc&&(e.config.doc.title=r.title)}},{ATTRS:{container:{value:null,setter:e.one},routes:{value:[{path:"*",callbacks:n}]}},defaultRoute:n})},"3.17.2",{requires:["pjax-base","pjax-content"]});
diff --git a/js/yui3/plugin/plugin-min.js b/js/yui3/plugin/plugin-min.js
new file mode 100644
index 000000000..1b11437fa
--- /dev/null
+++ b/js/yui3/plugin/plugin-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("plugin",function(e,t){function n(t){!this.hasImpl||!this.hasImpl(e.Plugin.Base)?n.superclass.constructor.apply(this,arguments):n.prototype.initializer.apply(this,arguments)}n.ATTRS={host:{writeOnce:!0}},n.NAME="plugin",n.NS="plugin",e.extend(n,e.Base,{_handles:null,initializer:function(e){this._handles=[]},destructor:function(){if(this._handles)for(var e=0,t=this._handles.length;e<t;e++)this._handles[e].detach()},doBefore:function(e,t,n){var r=this.get("host"),i;return e in r?i=this.beforeHostMethod(e,t,n):r.on&&(i=this.onHostEvent(e,t,n)),i},doAfter:function(e,t,n){var r=this.get("host"),i;return e in r?i=this.afterHostMethod(e,t,n):r.after&&(i=this.afterHostEvent(e,t,n)),i},onHostEvent:function(e,t,n){var r=this.get("host").on(e,t,n||this);return this._handles.push(r),r},onceHostEvent:function(e,t,n){var r=this.get("host").once(e,t,n||this);return this._handles.push(r),r},afterHostEvent:function(e,t,n){var r=this.get("host").after(e,t,n||this);return this._handles.push(r),r},onceAfterHostEvent:function(e,t,n){var r=this.get("host").onceAfter(e,t,n||this);return this._handles.push(r),r},beforeHostMethod:function(t,n,r){var i=e.Do.before(n,this.get("host"),t,r||this);return this._handles.push(i),i},afterHostMethod:function(t,n,r){var i=e.Do.after(n,this.get("host"),t,r||this);return this._handles.push(i),i},toString:function(){return this.constructor.NAME+"["+this.constructor.NS+"]"}}),e.namespace("Plugin").Base=n},"3.17.2",{requires:["base-base"]});
diff --git a/js/yui3/pluginhost-base/pluginhost-base-min.js b/js/yui3/pluginhost-base/pluginhost-base-min.js
new file mode 100644
index 000000000..d55bc4afb
--- /dev/null
+++ b/js/yui3/pluginhost-base/pluginhost-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("pluginhost-base",function(e,t){function r(){this._plugins={}}var n=e.Lang;r.prototype={plug:function(e,t){var r,i,s;if(n.isArray(e))for(r=0,i=e.length;r<i;r++)this.plug(e[r]);else e&&!n.isFunction(e)&&(t=e.cfg,e=e.fn),e&&e.NS&&(s=e.NS,t=t||{},t.host=this,this.hasPlugin(s)?this[s].setAttrs&&this[s].setAttrs(t):(this[s]=new e(t),this._plugins[s]=e));return this},unplug:function(e){var t=e,r=this._plugins;if(e)n.isFunction(e)&&(t=e.NS,t&&(!r[t]||r[t]!==e)&&(t=null)),t&&(this[t]&&(this[t].destroy&&this[t].destroy(),delete this[t]),r[t]&&delete r[t]);else for(t in this._plugins)this._plugins.hasOwnProperty(t)&&this.unplug(t);return this},hasPlugin:function(e){return this._plugins[e]&&this[e]},_initPlugins:function(e){this._plugins=this._plugins||{},this._initConfigPlugins&&this._initConfigPlugins(e)},_destroyPlugins:function(){this.unplug()}},e.namespace("Plugin").Host=r},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/pluginhost-config/pluginhost-config-min.js b/js/yui3/pluginhost-config/pluginhost-config-min.js
new file mode 100644
index 000000000..c6e940a8b
--- /dev/null
+++ b/js/yui3/pluginhost-config/pluginhost-config-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("pluginhost-config",function(e,t){var n=e.Plugin.Host,r=e.Lang;n.prototype._initConfigPlugins=function(t){var n=this._getClasses?this._getClasses():[this.constructor],r=[],i={},s,o,u,a,f;for(o=n.length-1;o>=0;o--)s=n[o],a=s._UNPLUG,a&&e.mix(i,a,!0),u=s._PLUG,u&&e.mix(r,u,!0);for(f in r)r.hasOwnProperty(f)&&(i[f]||this.plug(r[f]));t&&t.plugins&&this.plug(t.plugins)},n.plug=function(t,n,i){var s,o,u,a;if(t!==e.Base){t._PLUG=t._PLUG||{},r.isArray(n)||(i&&(n={fn:n,cfg:i}),n=[n]);for(o=0,u=n.length;o<u;o++)s=n[o],a=s.NAME||s.fn.NAME,t._PLUG[a]=s}},n.unplug=function(t,n){var i,s,o,u;if(t!==e.Base){t._UNPLUG=t._UNPLUG||{},r.isArray(n)||(n=[n]);for(s=0,o=n.length;s<o;s++)i=n[s],u=i.NAME,t._PLUG[u]?delete t._PLUG[u]:t._UNPLUG[u]=i}}},"3.17.2",{requires:["pluginhost-base"]});
diff --git a/js/yui3/promise/promise-min.js b/js/yui3/promise/promise-min.js
new file mode 100644
index 000000000..be791c9a9
--- /dev/null
+++ b/js/yui3/promise/promise-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("promise",function(e,t){function i(e){if(!(this instanceof i))return new i(e);var t=new i.Resolver(this);this._resolver=t;try{e.call(this,function(e){t.resolve(e)},function(e){t.reject(e)})}catch(n){t.reject(n)}}function s(e){this._callbacks=[],this._errbacks=[],this.promise=e,this._status="pending",this._result=null}var n=e.Lang,r=[].slice;e.mix(i.prototype,{then:function(e,t){var n=this.constructor,r=this._resolver;return new n(function(n,s){r._addCallbacks(typeof e=="function"?i._wrap(n,s,e):n,typeof t=="function"?i._wrap(n,s,t):s)})},"catch":function(e){return this.then(undefined,e)},getStatus:function(){return this._resolver.getStatus()}}),i._wrap=function(e,t,n){return function(r){var i;try{i=n(r)}catch(s){t(s);return}e(i)}},i.isPromise=function(e){var t;try{t=e.then}catch(n){}return typeof t=="function"},i.resolve=function(e){return i.isPromise(e)&&e.constructor===this?e:new this(function(t){t(e)})},i.reject=function(e){return new this(function(t,n){n(e)})},i.all=function(e){var t=this;return new t(function(r,i){function f(e){return function(t){a[e]=t,s--,s||r(a)}}if(!n.isArray(e)){i(new TypeError("Promise.all expects an array of values or promises"));return}var s=e.length,o=0,u=e.length,a=[];if(u<1)return r(a);for(;o<u;o++)t.resolve(e[o]).then(f(o),i)})},i.race=function(e){var t=this;return new t(function(r,i){if(!n.isArray(e)){i(new TypeError("Promise.race expects an array of values or promises"));return}for(var s=0,o=e.length;s<o;s++)t.resolve(e[s]).then(r,i)})},e.Promise=i,e.mix(s.prototype,{fulfill:function(e){this._status==="pending"&&(this._result=e,this._status="fulfilled"),this._status==="fulfilled"&&(this._notify(this._callbacks,this._result),this._callbacks=[],this._errbacks=null)},reject:function(e){this._status==="pending"&&(this._result=e,this._status="rejected"),this._status==="rejected"&&(this._notify(this._errbacks,this._result),this._callbacks=null,this._errbacks=[])},resolve:function(e){var t=this;i.isPromise(e)?e.then(function(e){t.resolve(e)},function(e){t.reject(e)}):this.fulfill(e)},then:function(e,t){return this.promise.then(e,t)},_addCallbacks:function(e,t){var n=this._callbacks,r=this._errbacks,i=this._status,s=this._result;n&&typeof e=="function"&&n.push(e),r&&typeof t=="function"&&r.push(t),i==="fulfilled"?this.fulfill(s):i==="rejected"&&this.reject(s)},getStatus:function(){return this._status},_notify:function(t,n){t.length&&e.soon(function(){var e,r;for(e=0,r=t.length;e<r;++e)t[e](n)})}},!0),e.Promise.Resolver=s,e.when=function(e,t,n){return e=i.resolve(e),t||n?e.then(t,n):e},e.batch=function(){return i.all(r.call(arguments))}},"3.17.2",{requires:["timers"]});
diff --git a/js/yui3/querystring-parse-simple/querystring-parse-simple-min.js b/js/yui3/querystring-parse-simple/querystring-parse-simple-min.js
new file mode 100644
index 000000000..720139874
--- /dev/null
+++ b/js/yui3/querystring-parse-simple/querystring-parse-simple-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("querystring-parse-simple",function(e,t){var n=e.namespace("QueryString");n.parse=function(e,t,r){t=t||"&",r=r||"=";for(var i={},s=0,o=e.split(t),u=o.length,a;s<u;s++)a=o[s].split(r),a.length>0&&(i[n.unescape(a.shift())]=n.unescape(a.join(r)));return i},n.unescape=function(e){return decodeURIComponent(e.replace(/\+/g," "))}},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/querystring-parse/querystring-parse-min.js b/js/yui3/querystring-parse/querystring-parse-min.js
new file mode 100644
index 000000000..72c91b902
--- /dev/null
+++ b/js/yui3/querystring-parse/querystring-parse-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("querystring-parse",function(e,t){var n=e.namespace("QueryString"),r=function(t){return function r(i,s){var o,u,a,f,l;return arguments.length!==2?(i=i.split(t),r(n.unescape(i.shift()),n.unescape(i.join(t)))):(i=i.replace(/^\s+|\s+$/g,""),e.Lang.isString(s)&&(s=s.replace(/^\s+|\s+$/g,""),isNaN(s)||(u=+s,s===u.toString(10)&&(s=u))),o=/(.*)\[([^\]]*)\]$/.exec(i),o?(f=o[2],a=o[1],f?(l={},l[f]=s,r(a,l)):r(a,[s])):(l={},i&&(l[i]=s),l))}},i=function(t,n){return t?e.Lang.isArray(t)?t.concat(n):!e.Lang.isObject(t)||!e.Lang.isObject(n)?[t].concat(n):s(t,n):n},s=function(e,t){for(var n in t)n&&t.hasOwnProperty(n)&&(e[n]=i(e[n],t[n]));return e};n.parse=function(t,n,s){return e.Array.reduce(e.Array.map(t.split(n||"&"),r(s||"=")),{},i)},n.unescape=function(e){return decodeURIComponent(e.replace(/\+/g," "))}},"3.17.2",{requires:["yui-base","array-extras"]});
diff --git a/js/yui3/querystring-stringify-simple/querystring-stringify-simple-min.js b/js/yui3/querystring-stringify-simple/querystring-stringify-simple-min.js
new file mode 100644
index 000000000..ae60af4d1
--- /dev/null
+++ b/js/yui3/querystring-stringify-simple/querystring-stringify-simple-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("querystring-stringify-simple",function(e,t){var n=e.namespace("QueryString"),r=encodeURIComponent;n.stringify=function(t,n){var i=[],s=n&&n.arrayKey?!0:!1,o,u,a;for(o in t)if(t.hasOwnProperty(o))if(e.Lang.isArray(t[o]))for(u=0,a=t[o].length;u<a;u++)i.push(r(s?o+"[]":o)+"="+r(t[o][u]));else i.push(r(o)+"="+r(t[o]));return i.join("&")}},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/querystring-stringify/querystring-stringify-min.js b/js/yui3/querystring-stringify/querystring-stringify-min.js
new file mode 100644
index 000000000..8c1d9df8c
--- /dev/null
+++ b/js/yui3/querystring-stringify/querystring-stringify-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("querystring-stringify",function(e,t){var n=e.namespace("QueryString"),r=[],i=e.Lang;n.escape=encodeURIComponent,n.stringify=function(e,t,s){var o,u,a,f,l,c,h=t&&t.sep?t.sep:"&",p=t&&t.eq?t.eq:"=",d=t&&t.arrayKey?t.arrayKey:!1;if(i.isNull(e)||i.isUndefined(e)||i.isFunction(e))return s?n.escape(s)+p:"";if(i.isBoolean(e)||Object.prototype.toString.call(e)==="[object Boolean]")e=+e;if(i.isNumber(e)||i.isString(e))return n.escape(s)+p+n.escape(e);if(i.isArray(e)){c=[],s=d?s+"[]":s,f=e.length;for(a=0;a<f;a++)c.push(n.stringify(e[a],t,s));return c.join(h)}for(a=r.length-1;a>=0;--a)if(r[a]===e)throw new Error("QueryString.stringify. Cyclical reference");r.push(e),c=[],o=s?s+"[":"",u=s?"]":"";for(a in e)e.hasOwnProperty(a)&&(l=o+a+u,c.push(n.stringify(e[a],t,l)));return r.pop(),c=c.join(h),!c&&s?s+"=":c}},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/queue-promote/queue-promote-min.js b/js/yui3/queue-promote/queue-promote-min.js
new file mode 100644
index 000000000..c77e1cd9a
--- /dev/null
+++ b/js/yui3/queue-promote/queue-promote-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("queue-promote",function(e,t){e.mix(e.Queue.prototype,{indexOf:function(t){return e.Array.indexOf(this._q,t)},promote:function(e){var t=this.indexOf(e);t>-1&&this._q.unshift(this._q.splice(t,1)[0])},remove:function(e){var t=this.indexOf(e);t>-1&&this._q.splice(t,1)}})},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/range-slider/assets/thumb-x-oblong-dark.png b/js/yui3/range-slider/assets/thumb-x-oblong-dark.png
new file mode 100644
index 000000000..bc0aa14ce
--- /dev/null
+++ b/js/yui3/range-slider/assets/thumb-x-oblong-dark.png
Binary files differ
diff --git a/js/yui3/range-slider/assets/thumb-x-oblong.png b/js/yui3/range-slider/assets/thumb-x-oblong.png
new file mode 100644
index 000000000..670ba1ea1
--- /dev/null
+++ b/js/yui3/range-slider/assets/thumb-x-oblong.png
Binary files differ
diff --git a/js/yui3/range-slider/assets/thumb-x-oblong2-dark.png b/js/yui3/range-slider/assets/thumb-x-oblong2-dark.png
new file mode 100644
index 000000000..20f126029
--- /dev/null
+++ b/js/yui3/range-slider/assets/thumb-x-oblong2-dark.png
Binary files differ
diff --git a/js/yui3/range-slider/assets/thumb-x-oblong2.png b/js/yui3/range-slider/assets/thumb-x-oblong2.png
new file mode 100644
index 000000000..76e34e60a
--- /dev/null
+++ b/js/yui3/range-slider/assets/thumb-x-oblong2.png
Binary files differ
diff --git a/js/yui3/range-slider/assets/thumb-y-oblong-dark.png b/js/yui3/range-slider/assets/thumb-y-oblong-dark.png
new file mode 100644
index 000000000..a0eed7087
--- /dev/null
+++ b/js/yui3/range-slider/assets/thumb-y-oblong-dark.png
Binary files differ
diff --git a/js/yui3/range-slider/assets/thumb-y-oblong.png b/js/yui3/range-slider/assets/thumb-y-oblong.png
new file mode 100644
index 000000000..e63c8d7d8
--- /dev/null
+++ b/js/yui3/range-slider/assets/thumb-y-oblong.png
Binary files differ
diff --git a/js/yui3/range-slider/assets/thumb-y-oblong2-dark.png b/js/yui3/range-slider/assets/thumb-y-oblong2-dark.png
new file mode 100644
index 000000000..e91ffb7b3
--- /dev/null
+++ b/js/yui3/range-slider/assets/thumb-y-oblong2-dark.png
Binary files differ
diff --git a/js/yui3/range-slider/assets/thumb-y-oblong2.png b/js/yui3/range-slider/assets/thumb-y-oblong2.png
new file mode 100644
index 000000000..89a466727
--- /dev/null
+++ b/js/yui3/range-slider/assets/thumb-y-oblong2.png
Binary files differ
diff --git a/js/yui3/range-slider/range-slider-min.js b/js/yui3/range-slider/range-slider-min.js
new file mode 100644
index 000000000..98e3e5b7d
--- /dev/null
+++ b/js/yui3/range-slider/range-slider-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("range-slider",function(e,t){e.Slider=e.Base.build("slider",e.SliderBase,[e.SliderValueRange,e.ClickableRail])},"3.17.2",{requires:["slider-base","slider-value-range","clickable-rail"]});
diff --git a/js/yui3/recordset-base/recordset-base-min.js b/js/yui3/recordset-base/recordset-base-min.js
new file mode 100644
index 000000000..88535e304
--- /dev/null
+++ b/js/yui3/recordset-base/recordset-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("recordset-base",function(e,t){var n=e.Base.create("record",e.Base,[],{_setId:function(){return e.guid()},initializer:function(){},destructor:function(){},getValue:function(e){return e===undefined?this.get("data"):this.get("data")[e]}},{ATTRS:{id:{valueFn:"_setId"},data:{value:null}}});e.Record=n;var r=e.ArrayList,i=e.Lang,s=e.Base.create("recordset",e.Base,[],{initializer:function(){this._items||(this._items=[]),this.publish({add:{defaultFn:this._defAddFn},remove:{defaultFn:this._defRemoveFn},empty:{defaultFn:this._defEmptyFn},update:{defaultFn:this._defUpdateFn}}),this._buildHashTable(this.get("key")),this.after(["recordsChange","add","remove","update","empty"],this._updateHash)},getRecord:function(e){return i.isString(e)?this.get("table")[e]:i.isNumber(e)?this._items[e]:null},getRecordByIndex:function(e){return this._items[e]},getRecordsByIndex:function(e,t){var n=0,r=[];t=i.isNumber(t)&&t>0?t:1;for(;n<t;n++)r.push(this._items[e+n]);return r},getLength:function(){return this.size()},getValuesByKey:function(e){var t=0,n=this._items.length,r=[];for(;t<n;t++)r.push(this._items[t].getValue(e));return r},add:function(e,t){var n=[],r,s=0;r=i.isNumber(t)&&t>-1?t:this._items.length;if(i.isArray(e))for(;s<e.length;s++)n[s]=this._changeToRecord(e[s]);else i.isObject(e)&&(n[0]=this._changeToRecord(e));return this.fire("add",{added:n,index:r}),this},remove:function(e,t){var n=[];return e=e>-1?e:this._items.length-1,t=t>0?t:1,n=this._items.slice(e,e+t),this.fire("remove",{removed:n,range:t,index:e}),this},empty:function(){return this.fire("empty",{}),this},update:function(e,t){var n,r,s=0;r=i.isArray(e)?e:[e],n=this._items.slice(t,t+r.length);for(;s<r.length;s++)r[s]=this._changeToRecord(r[s]);return this.fire("update",{updated:r,overwritten:n,index:t}),this},_defAddFn:function(e){this._items.splice.apply(this._items,[e.index,0].concat(e.added))},_defRemoveFn:function(e){this._items.splice(e.index,e.range||1)},_defUpdateFn:function(e){for(var t=0;t<e.updated.length;t++)this._items[e.index+t]=this._changeToRecord(e.updated[t])},_defEmptyFn:function(e){this._items=[]},_updateHash:function(e){var t="_hash",n=e.type.replace(/.*:/,""),r;t+=n.charAt(0).toUpperCase()+n.slice(1),r=this[t]&&this[t](this.get("table"),this.get("key"),e),r&&this.set("table",r)},_hashRecordsChange:function(e,t,n){return this._buildHashTable(t)},_buildHashTable:function(e){return this._hashAdd({},e,{added:this._items})},_hashAdd:function(e,t,n){var r=n.added,i,s;for(i=0,s=n.added.length;i<s;++i)e[r[i].get(t)]=r[i];return e},_hashRemove:function(e,t,n){for(var r=n.removed.length-1;r>=0;--r)delete e[n.removed[r].get(t)];return e},_hashUpdate:function(e,t,n){return n.overwritten&&n.overwritten.length&&(e=this._hashRemove(e,t,{removed:n.overwritten})),this._hashAdd(e,t,{added:n.updated})},_hashEmpty:function(){return{}},_initHashTable:function(){return this._hashAdd({},this.get("key"),{added:this._items||[]})},_changeToRecord:function(t){return t instanceof e.Record?t:new e.Record({data:t})},_setRecords:function(t){if(!e.Lang.isArray(t))return e.Attribute.INVALID_VALUE;var n=[],r,i;for(r=0,i=t.length;r<i;++r)n[r]=this._changeToRecord(t[r]);return this._items=n}},{ATTRS:{records:{lazyAdd:!1,getter:function(){return e.Array(this._items)},setter:"_setRecords"},table:{valueFn:"_initHashTable"},key:{value:"id",readOnly:!0}}});e.augment(s,r),e.Recordset=s},"3.17.2",{requires:["base","arraylist"]});
diff --git a/js/yui3/recordset-filter/recordset-filter-min.js b/js/yui3/recordset-filter/recordset-filter-min.js
new file mode 100644
index 000000000..8e31ccfff
--- /dev/null
+++ b/js/yui3/recordset-filter/recordset-filter-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("recordset-filter",function(e,t){function i(e){i.superclass.constructor.apply(this,arguments)}var n=e.Array,r=e.Lang;e.mix(i,{NS:"filter",NAME:"recordsetFilter",ATTRS:{}}),e.extend(i,e.Plugin.Base,{filter:function(t,i){var s=this.get("host").get("records"),o;return i&&r.isString(t)&&(o=t,t=function(e){return e.getValue(o)===i}),new e.Recordset({records:n.filter(s,t)})},reject:function(t){return new e.Recordset({records:n.reject(this.get("host").get("records"),t)})},grep:function(t){return new e.Recordset({records:n.grep(this.get("host").get("records"),t)})}}),e.namespace("Plugin").RecordsetFilter=i},"3.17.2",{requires:["recordset-base","array-extras","plugin"]});
diff --git a/js/yui3/recordset-indexer/recordset-indexer-min.js b/js/yui3/recordset-indexer/recordset-indexer-min.js
new file mode 100644
index 000000000..53ee70e45
--- /dev/null
+++ b/js/yui3/recordset-indexer/recordset-indexer-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("recordset-indexer",function(e,t){function n(e){n.superclass.constructor.apply(this,arguments)}e.mix(n,{NS:"indexer",NAME:"recordsetIndexer",ATTRS:{hashTables:{value:{}},keys:{value:{}}}}),e.extend(n,e.Plugin.Base,{initializer:function(t){var n=this.get("host");this.onHostEvent("add",e.bind("_defAddHash",this),n),this.onHostEvent("remove",e.bind("_defRemoveHash",this),n),this.onHostEvent("update",e.bind("_defUpdateHash",this),n)},destructor:function(e){},_setHashTable:function(e){var t=this.get("host"),n={},r=0,i=t.getLength();for(;r<i;r++)n[t._items[r].getValue(e)]=t._items[r];return n},_defAddHash:function(t){var n=this.get("hashTables");e.each(n,function(n,r){e.each(t.added||t.updated,function(e){e.getValue(r)&&(n[e.getValue(r)]=e)})})},_defRemoveHash:function(t){var n=this.get("hashTables"),r;e.each(n,function(n,i){e.each(t.removed||t.overwritten,function(e){r=e.getValue(i),r&&n[r]===e&&delete n[r]})})},_defUpdateHash:function(e){e.added=e.updated,e.removed=e.overwritten,this._defAddHash(e),this._defRemoveHash(e)},createTable:function(e){var t=this.get("hashTables");return t[e]=this._setHashTable(e),this.set("hashTables",t),t[e]},getTable:function(e){return this.get("hashTables")[e]}}),e.namespace("Plugin").RecordsetIndexer=n},"3.17.2",{requires:["recordset-base","plugin"]});
diff --git a/js/yui3/recordset-sort/recordset-sort-min.js b/js/yui3/recordset-sort/recordset-sort-min.js
new file mode 100644
index 000000000..2e4d3597b
--- /dev/null
+++ b/js/yui3/recordset-sort/recordset-sort-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("recordset-sort",function(e,t){function i(e,t,n){i.superclass.constructor.apply(this,arguments)}var n=e.ArraySort.compare,r=e.Lang.isValue;e.mix(i,{NS:"sort",NAME:"recordsetSort",ATTRS:{lastSortProperties:{value:{field:undefined,desc:!0,sorter:undefined},validator:function(e){return r(e.field)&&r(e.desc)&&r(e.sorter)}},defaultSorter:{value:function(e,t,r,i){var s=n(e.getValue(r),t.getValue(r),i);return s===0?n(e.get("id"),t.get("id"),i):s}},isSorted:{value:!1}}}),e.extend(i,e.Plugin.Base,{initializer:function(t){var n=this,r=this.get("host");this.publish("sort",{defaultFn:e.bind("_defSortFn",this)}),this.on("sort",function(){n.set("isSorted",!0)}),this.onHostEvent("add",function(){n.set("isSorted",!1)},r),this.onHostEvent("update",function(){n.set("isSorted",!1)},r)},destructor:function(e){},_defSortFn:function(e){this.get("host")._items.sort(function(t,n){return e.sorter(t,n,e.field,e.desc)}),this.set("lastSortProperties",e)},sort:function(e,t,n){this.fire("sort",{field:e,desc:t,sorter:n||this.get("defaultSorter")})},resort:function(){var e=this.get("lastSortProperties");this.fire("sort",{field:e.field,desc:e.desc,sorter:e.sorter||this.get("defaultSorter")})},reverse:function(){this.get("host")._items.reverse()},flip:function(){var e=this.get("lastSortProperties");r(e.field)&&this.fire("sort",{field:e.field,desc:!e.desc,sorter:e.sorter||this.get("defaultSorter")})}}),e.namespace("Plugin").RecordsetSort=i},"3.17.2",{requires:["arraysort","recordset-base","plugin"]});
diff --git a/js/yui3/resize-base/assets/resize-base-core.css b/js/yui3/resize-base/assets/resize-base-core.css
new file mode 100644
index 000000000..c43c2f3fd
--- /dev/null
+++ b/js/yui3/resize-base/assets/resize-base-core.css
@@ -0,0 +1,248 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-resize,.yui3-resize-wrapper {
+ z-index: 0;
+ zoom: 1;
+}
+
+.yui3-resize-handle {
+ position: absolute;
+ display: block;
+ z-index: 100;
+ zoom: 1;
+}
+
+.yui3-resize-proxy {
+ position: absolute;
+ border: 1px dashed #000;
+ position: absolute;
+ z-index: 10000;
+}
+
+.yui3-resize-hidden-handles .yui3-resize-handle {
+ opacity: 0;
+ filter: alpha(opacity=0);
+}
+
+.yui3-resize-handle-t,
+.yui3-resize-handle-b {
+ width: 100%;
+ left: 0;
+ height: 6px;
+}
+
+.yui3-resize-handle-l,
+.yui3-resize-handle-r {
+ height: 100%;
+ top: 0;
+ width: 6px;
+}
+
+.yui3-resize-handle-t {
+ cursor: n-resize;
+ top: 0;
+}
+
+.yui3-resize-handle-b {
+ cursor: s-resize;
+ bottom: 0;
+}
+
+.yui3-resize-handle-l {
+ cursor: w-resize;
+ left: 0;
+}
+
+.yui3-resize-handle-r {
+ cursor: e-resize;
+ right: 0;
+}
+
+.yui3-resize-handle-inner {
+ position: absolute;
+ zoom: 1;
+}
+
+/* Smartphones (portrait and landscape) -----------
+Adding larger hit-area ':after' objects to handles
+*/
+@media only screen
+and (min-device-width : 320px)
+and (max-device-width : 480px) {
+/* Styles */
+ .yui3-resize-handle-inner:after {
+ content: "";
+ width: 40px;
+ height: 40px;
+ position: absolute;
+ }
+
+ .yui3-resize-handle-inner-r,
+ .yui3-resize-handle-inner-l,
+ .yui3-resize-handle-inner-t,
+ .yui3-resize-handle-inner-b,
+ .yui3-resize-handle-inner-tr,
+ .yui3-resize-handle-inner-br,
+ .yui3-resize-handle-inner-tl,
+ .yui3-resize-handle-inner-bl {
+ overflow: visible !important;
+ }
+
+ .yui3-resize-handle-inner-r:after {
+ top: -12px;
+ right: 0;
+ }
+ .yui3-resize-handle-inner-l:after {
+ top: -12px;
+ left: 0;
+ }
+ .yui3-resize-handle-inner-t:after {
+ top: 0;
+ left: -12px;
+ }
+ .yui3-resize-handle-inner-b:after {
+ bottom: 0;
+ left: -12px;
+ }
+ .yui3-resize-handle-inner-tr:after {
+ top: 0;
+ right: 0;
+ }
+ .yui3-resize-handle-inner-br:after {
+ bottom: 0;
+ right: 0;
+ }
+ .yui3-resize-handle-inner-tl:after {
+ top: 0;
+ left: 0;
+ }
+ .yui3-resize-handle-inner-bl:after {
+ bottom: 0;
+ left: 0;
+ }
+}
+
+/* iPads (portrait and landscape) -----------
+Adding larger hit-area ':after' objects to handles
+*/
+@media only screen
+and (min-device-width : 768px)
+and (max-device-width : 1024px) {
+/* Styles */
+ .yui3-resize-handle-inner:after {
+ content: "";
+ width: 30px;
+ height: 30px;
+ position: absolute;
+ }
+
+ .yui3-resize-handle-inner-r,
+ .yui3-resize-handle-inner-l,
+ .yui3-resize-handle-inner-t,
+ .yui3-resize-handle-inner-b,
+ .yui3-resize-handle-inner-tr,
+ .yui3-resize-handle-inner-br,
+ .yui3-resize-handle-inner-tl,
+ .yui3-resize-handle-inner-bl {
+ overflow: visible !important;
+ }
+
+ .yui3-resize-handle-inner-r:after {
+ top: -6px;
+ right: 0;
+ }
+ .yui3-resize-handle-inner-l:after {
+ top: -6px;
+ left: 0;
+ }
+ .yui3-resize-handle-inner-t:after {
+ top: 0;
+ left: -6px;
+ }
+ .yui3-resize-handle-inner-b:after {
+ bottom: 0;
+ left: -6px;
+ }
+ .yui3-resize-handle-inner-tr:after {
+ top: 0;
+ right: 0;
+ }
+ .yui3-resize-handle-inner-br:after {
+ bottom: 0;
+ right: 0;
+ }
+ .yui3-resize-handle-inner-tl:after {
+ top: 0;
+ left: 0;
+ }
+ .yui3-resize-handle-inner-bl:after {
+ bottom: 0;
+ left: 0;
+ }
+}
+
+.yui3-resize-handle-inner-t,
+.yui3-resize-handle-inner-b {
+ margin-left: -8px;
+ left: 50%;
+}
+
+.yui3-resize-handle-inner-l,
+.yui3-resize-handle-inner-r {
+ margin-top: -8px;
+ top: 50%;
+}
+
+.yui3-resize-handle-inner-t {
+ top: -4px;
+}
+
+.yui3-resize-handle-inner-b {
+ bottom: -4px;
+}
+
+.yui3-resize-handle-inner-l {
+ left: -4px;
+}
+
+.yui3-resize-handle-inner-r {
+ right: -4px;
+}
+
+.yui3-resize-handle-tr,
+.yui3-resize-handle-br,
+.yui3-resize-handle-tl,
+.yui3-resize-handle-bl {
+ height: 15px;
+ width: 15px;
+ z-index: 200;
+}
+
+.yui3-resize-handle-tr {
+ cursor: ne-resize;
+ top: 0;
+ right: 0;
+}
+
+.yui3-resize-handle-tl {
+ cursor: nw-resize;
+ top: 0;
+ left: 0;
+}
+
+.yui3-resize-handle-br {
+ cursor: se-resize;
+ bottom: 0;
+ right: 0;
+}
+
+.yui3-resize-handle-bl {
+ cursor: sw-resize;
+ bottom: 0;
+ left: 0;
+} \ No newline at end of file
diff --git a/js/yui3/resize-base/assets/skins/night/arrows.png b/js/yui3/resize-base/assets/skins/night/arrows.png
new file mode 100644
index 000000000..2942681f4
--- /dev/null
+++ b/js/yui3/resize-base/assets/skins/night/arrows.png
Binary files differ
diff --git a/js/yui3/resize-base/assets/skins/night/resize-base.css b/js/yui3/resize-base/assets/skins/night/resize-base.css
new file mode 100644
index 000000000..f0a5b41fe
--- /dev/null
+++ b/js/yui3/resize-base/assets/skins/night/resize-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-resize,.yui3-resize-wrapper{z-index:0;zoom:1}.yui3-resize-handle{position:absolute;display:block;z-index:100;zoom:1}.yui3-resize-proxy{position:absolute;border:1px dashed #000;position:absolute;z-index:10000}.yui3-resize-hidden-handles .yui3-resize-handle{opacity:0;filter:alpha(opacity=0)}.yui3-resize-handle-t,.yui3-resize-handle-b{width:100%;left:0;height:6px}.yui3-resize-handle-l,.yui3-resize-handle-r{height:100%;top:0;width:6px}.yui3-resize-handle-t{cursor:n-resize;top:0}.yui3-resize-handle-b{cursor:s-resize;bottom:0}.yui3-resize-handle-l{cursor:w-resize;left:0}.yui3-resize-handle-r{cursor:e-resize;right:0}.yui3-resize-handle-inner{position:absolute;zoom:1}@media only screen and (min-device-width :320px) and (max-device-width :480px){.yui3-resize-handle-inner:after{content:"";width:40px;height:40px;position:absolute}.yui3-resize-handle-inner-r,.yui3-resize-handle-inner-l,.yui3-resize-handle-inner-t,.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-tr,.yui3-resize-handle-inner-br,.yui3-resize-handle-inner-tl,.yui3-resize-handle-inner-bl{overflow:visible!important}.yui3-resize-handle-inner-r:after{top:-12px;right:0}.yui3-resize-handle-inner-l:after{top:-12px;left:0}.yui3-resize-handle-inner-t:after{top:0;left:-12px}.yui3-resize-handle-inner-b:after{bottom:0;left:-12px}.yui3-resize-handle-inner-tr:after{top:0;right:0}.yui3-resize-handle-inner-br:after{bottom:0;right:0}.yui3-resize-handle-inner-tl:after{top:0;left:0}.yui3-resize-handle-inner-bl:after{bottom:0;left:0}}@media only screen and (min-device-width :768px) and (max-device-width :1024px){.yui3-resize-handle-inner:after{content:"";width:30px;height:30px;position:absolute}.yui3-resize-handle-inner-r,.yui3-resize-handle-inner-l,.yui3-resize-handle-inner-t,.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-tr,.yui3-resize-handle-inner-br,.yui3-resize-handle-inner-tl,.yui3-resize-handle-inner-bl{overflow:visible!important}.yui3-resize-handle-inner-r:after{top:-6px;right:0}.yui3-resize-handle-inner-l:after{top:-6px;left:0}.yui3-resize-handle-inner-t:after{top:0;left:-6px}.yui3-resize-handle-inner-b:after{bottom:0;left:-6px}.yui3-resize-handle-inner-tr:after{top:0;right:0}.yui3-resize-handle-inner-br:after{bottom:0;right:0}.yui3-resize-handle-inner-tl:after{top:0;left:0}.yui3-resize-handle-inner-bl:after{bottom:0;left:0}}.yui3-resize-handle-inner-t,.yui3-resize-handle-inner-b{margin-left:-8px;left:50%}.yui3-resize-handle-inner-l,.yui3-resize-handle-inner-r{margin-top:-8px;top:50%}.yui3-resize-handle-inner-t{top:-4px}.yui3-resize-handle-inner-b{bottom:-4px}.yui3-resize-handle-inner-l{left:-4px}.yui3-resize-handle-inner-r{right:-4px}.yui3-resize-handle-tr,.yui3-resize-handle-br,.yui3-resize-handle-tl,.yui3-resize-handle-bl{height:15px;width:15px;z-index:200}.yui3-resize-handle-tr{cursor:ne-resize;top:0;right:0}.yui3-resize-handle-tl{cursor:nw-resize;top:0;left:0}.yui3-resize-handle-br{cursor:se-resize;bottom:0;right:0}.yui3-resize-handle-bl{cursor:sw-resize;bottom:0;left:0}.yui3-resize-handle-inner-r,.yui3-resize-handle-inner-l,.yui3-resize-handle-inner-t,.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-tr,.yui3-resize-handle-inner-br,.yui3-resize-handle-inner-tl,.yui3-resize-handle-inner-bl{background-repeat:no-repeat;background:url(arrows.png) no-repeat 0 0;display:block;height:15px;overflow:hidden;text-indent:-99999em;width:15px}.yui3-resize-handle-inner-br{background-position:-30px 0;bottom:-2px;right:-2px}.yui3-resize-handle-inner-tr{background-position:-58px 0;bottom:0;right:-2px}.yui3-resize-handle-inner-bl{background-position:-75px 0;bottom:-2px;right:-2px}.yui3-resize-handle-inner-tl{background-position:-47px 0;bottom:0;right:-2px}.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-t{background-position:-15px 0}#yui3-css-stamp.skin-night-resize-base{display:none}
diff --git a/js/yui3/resize-base/assets/skins/sam/arrows.png b/js/yui3/resize-base/assets/skins/sam/arrows.png
new file mode 100644
index 000000000..2942681f4
--- /dev/null
+++ b/js/yui3/resize-base/assets/skins/sam/arrows.png
Binary files differ
diff --git a/js/yui3/resize-base/assets/skins/sam/resize-base.css b/js/yui3/resize-base/assets/skins/sam/resize-base.css
new file mode 100644
index 000000000..5a83e6e8c
--- /dev/null
+++ b/js/yui3/resize-base/assets/skins/sam/resize-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-resize,.yui3-resize-wrapper{z-index:0;zoom:1}.yui3-resize-handle{position:absolute;display:block;z-index:100;zoom:1}.yui3-resize-proxy{position:absolute;border:1px dashed #000;position:absolute;z-index:10000}.yui3-resize-hidden-handles .yui3-resize-handle{opacity:0;filter:alpha(opacity=0)}.yui3-resize-handle-t,.yui3-resize-handle-b{width:100%;left:0;height:6px}.yui3-resize-handle-l,.yui3-resize-handle-r{height:100%;top:0;width:6px}.yui3-resize-handle-t{cursor:n-resize;top:0}.yui3-resize-handle-b{cursor:s-resize;bottom:0}.yui3-resize-handle-l{cursor:w-resize;left:0}.yui3-resize-handle-r{cursor:e-resize;right:0}.yui3-resize-handle-inner{position:absolute;zoom:1}@media only screen and (min-device-width :320px) and (max-device-width :480px){.yui3-resize-handle-inner:after{content:"";width:40px;height:40px;position:absolute}.yui3-resize-handle-inner-r,.yui3-resize-handle-inner-l,.yui3-resize-handle-inner-t,.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-tr,.yui3-resize-handle-inner-br,.yui3-resize-handle-inner-tl,.yui3-resize-handle-inner-bl{overflow:visible!important}.yui3-resize-handle-inner-r:after{top:-12px;right:0}.yui3-resize-handle-inner-l:after{top:-12px;left:0}.yui3-resize-handle-inner-t:after{top:0;left:-12px}.yui3-resize-handle-inner-b:after{bottom:0;left:-12px}.yui3-resize-handle-inner-tr:after{top:0;right:0}.yui3-resize-handle-inner-br:after{bottom:0;right:0}.yui3-resize-handle-inner-tl:after{top:0;left:0}.yui3-resize-handle-inner-bl:after{bottom:0;left:0}}@media only screen and (min-device-width :768px) and (max-device-width :1024px){.yui3-resize-handle-inner:after{content:"";width:30px;height:30px;position:absolute}.yui3-resize-handle-inner-r,.yui3-resize-handle-inner-l,.yui3-resize-handle-inner-t,.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-tr,.yui3-resize-handle-inner-br,.yui3-resize-handle-inner-tl,.yui3-resize-handle-inner-bl{overflow:visible!important}.yui3-resize-handle-inner-r:after{top:-6px;right:0}.yui3-resize-handle-inner-l:after{top:-6px;left:0}.yui3-resize-handle-inner-t:after{top:0;left:-6px}.yui3-resize-handle-inner-b:after{bottom:0;left:-6px}.yui3-resize-handle-inner-tr:after{top:0;right:0}.yui3-resize-handle-inner-br:after{bottom:0;right:0}.yui3-resize-handle-inner-tl:after{top:0;left:0}.yui3-resize-handle-inner-bl:after{bottom:0;left:0}}.yui3-resize-handle-inner-t,.yui3-resize-handle-inner-b{margin-left:-8px;left:50%}.yui3-resize-handle-inner-l,.yui3-resize-handle-inner-r{margin-top:-8px;top:50%}.yui3-resize-handle-inner-t{top:-4px}.yui3-resize-handle-inner-b{bottom:-4px}.yui3-resize-handle-inner-l{left:-4px}.yui3-resize-handle-inner-r{right:-4px}.yui3-resize-handle-tr,.yui3-resize-handle-br,.yui3-resize-handle-tl,.yui3-resize-handle-bl{height:15px;width:15px;z-index:200}.yui3-resize-handle-tr{cursor:ne-resize;top:0;right:0}.yui3-resize-handle-tl{cursor:nw-resize;top:0;left:0}.yui3-resize-handle-br{cursor:se-resize;bottom:0;right:0}.yui3-resize-handle-bl{cursor:sw-resize;bottom:0;left:0}.yui3-resize-handle-inner-r,.yui3-resize-handle-inner-l,.yui3-resize-handle-inner-t,.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-tr,.yui3-resize-handle-inner-br,.yui3-resize-handle-inner-tl,.yui3-resize-handle-inner-bl{background-repeat:no-repeat;background:url(arrows.png) no-repeat 0 0;display:block;height:15px;overflow:hidden;text-indent:-99999em;width:15px}.yui3-resize-handle-inner-br{background-position:-30px 0;bottom:-2px;right:-2px}.yui3-resize-handle-inner-tr{background-position:-58px 0;bottom:0;right:-2px}.yui3-resize-handle-inner-bl{background-position:-75px 0;bottom:-2px;right:-2px}.yui3-resize-handle-inner-tl{background-position:-47px 0;bottom:0;right:-2px}.yui3-resize-handle-inner-b,.yui3-resize-handle-inner-t{background-position:-15px 0}#yui3-css-stamp.skin-sam-resize-base{display:none}
diff --git a/js/yui3/resize-base/resize-base-min.js b/js/yui3/resize-base/resize-base-min.js
new file mode 100644
index 000000000..758c18f4d
--- /dev/null
+++ b/js/yui3/resize-base/resize-base-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("resize-base",function(e,t){function Lt(){Lt.superclass.constructor.apply(this,arguments)}var n=e.Lang,r=n.isArray,i=n.isBoolean,s=n.isNumber,o=n.isString,u=e.Array,a=n.trim,f=u.indexOf,l=",",c=".",h="",p="{handle}",d=" ",v="active",m="activeHandle",g="activeHandleNode",y="all",b="autoHide",w="border",E="bottom",S="className",x="color",T="defMinHeight",N="defMinWidth",C="handle",k="handles",L="handlesWrapper",A="hidden",O="inner",M="left",_="margin",D="node",P="nodeName",H="none",B="offsetHeight",j="offsetWidth",F="padding",I="parentNode",q="position",R="relative",U="resize",z="resizing",W="right",X="static",V="style",$="top",J="width",K="wrap",Q="wrapper",G="wrapTypes",Y="resize:mouseUp",Z="resize:resize",et="resize:align",tt="resize:end",nt="resize:start",rt="t",it="tr",st="r",ot="br",ut="b",at="bl",ft="l",lt="tl",ct=function(){return Array.prototype.slice.call(arguments).join(d)},ht=function(e){return Math.round(parseFloat(e))||0},pt=function(e,t){return e.getComputedStyle(t)},dt=function(e){return C+e.toUpperCase()},vt=function(t){return t instanceof e.Node},mt=e.cached(function(e){return e.substring(0,1).toUpperCase()+e.substring(1)}),gt=e.cached(function(){var e=[],t=u(arguments,0,!0);return u.each(t,function(t,n){n>0&&(t=mt(t)),e.push(t)}),e.join(h)}),yt=e.ClassNameManager.getClassName,bt=yt(U),wt=yt(U,C),Et=yt(U,C,v),St=yt(U,C,O),xt=yt(U,C,O,p),Tt=yt(U,C,p),Nt=yt(U,A,k),Ct=yt(U,k,Q),kt=yt(U,Q);e.mix(Lt,{NAME:U,ATTRS:{activeHandle:{value:null,validator:function(t){return e.Lang.isString(t)||e.Lang.isNull(t)}},activeHandleNode:{value:null,validator:vt},autoHide:{value:!1,validator:i},defMinHeight:{value:15,validator:s},defMinWidth:{value:15,validator:s},handles:{setter:"_setHandles",value:y},handlesWrapper:{readOnly:!0,setter:e.one,valueFn:"_valueHandlesWrapper"},node:{setter:e.one},resizing:{value:!1,validator:i},wrap:{setter:"_setWrap",value:!1,validator:i},wrapTypes:{readOnly:!0,value:/^canvas|textarea|input|select|button|img|iframe|table|embed$/i},wrapper:{readOnly:!0,valueFn:"_valueWrapper",writeOnce:!0}},RULES:{b:function(e,t,n){var r=e.info,i=e.originalInfo;r.offsetHeight=i.offsetHeight+n},l:function(e,t){var n=e.info,r=e.originalInfo;n.left=r.left+t,n.offsetWidth=r.offsetWidth-t},r:function(e,t){var n=e.info,r=e.originalInfo;n.offsetWidth=r.offsetWidth+t},t:function(e,t,n){var r=e.info,i=e.originalInfo;r.top=i.top+n,r.offsetHeight=i.offsetHeight-n},tr:function(){this.t.apply(this,arguments),this.r.apply(this,arguments)},bl:function(){this.b.apply(this,arguments),this.l.apply(this,arguments)},br:function(){this.b.apply(this,arguments),this.r.apply(this,arguments)},tl:function(){this.t.apply(this,arguments),this.l.apply(this,arguments)}},capitalize:gt}),e.Resize=e.extend(Lt,e.Base,{ALL_HANDLES:[rt,it,st,ot,ut,at,ft,lt],REGEX_CHANGE_HEIGHT:/^(t|tr|b|bl|br|tl)$/i,REGEX_CHANGE_LEFT:/^(tl|l|bl)$/i,REGEX_CHANGE_TOP:/^(tl|t|tr)$/i,REGEX_CHANGE_WIDTH:/^(bl|br|l|r|tl|tr)$/i,HANDLES_WRAP_TEMPLATE:'<div class="'+Ct+'"></div>',WRAP_TEMPLATE:'<div class="'+kt+'"></div>',HANDLE_TEMPLATE:'<div class="'+ct(wt,Tt)+'">'+'<div class="'+ct(St,xt)+'">&nbsp;</div>'+"</div>",totalHSurrounding:0,totalVSurrounding:0,nodeSurrounding:null,wrapperSurrounding:null,changeHeightHandles:!1,changeLeftHandles:!1,changeTopHandles:!1,changeWidthHandles:!1,delegate:null,info:null,lastInfo:null,originalInfo:null,initializer:function(){this._eventHandles=[],this.renderer()},renderUI:function(){var e=this;e._renderHandles()},bindUI:function(){var e=this;e._createEvents(),e._bindDD(),e._bindHandle()},syncUI:function(){var e=this;this.get(D).addClass(bt),e._setHideHandlesUI(e.get(b))},destructor:function(){var t=this,n=t.get(D),r=t.get(Q),i=r.get(I);e.each(t._eventHandles,function(e){e.detach()}),t._eventHandles.length=0,t.eachHandle(function(e){t.delegate.dd.destroy(),e.remove(!0)}),t.delegate.destroy(),t.get(K)&&(t._copyStyles(r,n),i&&i.insertBefore(n,r),r.remove(!0)),n.removeClass(bt),n.removeClass(Nt)},renderer:function(){this.renderUI(),this.bindUI(),this.syncUI()},eachHandle:function(t){var n=this;e.each(n.get(k),function(e,r){var i=n.get(dt(e));t.apply(n,[i,e,r])})},_bindDD:function(){var t=this;t.delegate=new e.DD.Delegate({bubbleTargets:t,container:t.get(L),dragConfig:{clickPixelThresh:0,clickTimeThresh:0,useShim:!0,move:!1},nodes:c+wt,target:!1}),t._eventHandles.push(t.on("drag:drag",t._handleResizeEvent),t.on("drag:dropmiss",t._handleMouseUpEvent),t.on("drag:end",t._handleResizeEndEvent),t.on("drag:start",t._handleResizeStartEvent))},_bindHandle:function(){var t=this,n=t.get(Q);t._eventHandles.push(n.on("mouseenter",e.bind(t._onWrapperMouseEnter,t)),n.on("mouseleave",e.bind(t._onWrapperMouseLeave,t)),n.delegate("mouseenter",e.bind(t._onHandleMouseEnter,t),c+wt),n.delegate("mouseleave",e.bind(t._onHandleMouseLeave,t),c+wt))},_createEvents:function(){var e=this,t=function(t,n){e.publish(t,{defaultFn:n,queuable:!1,emitFacade:!0,bubbles:!0,prefix:U})};t(nt,this._defResizeStartFn),t(Z,this._defResizeFn),t(et,this._defResizeAlignFn),t(tt,this._defResizeEndFn),t(Y,this._defMouseUpFn)},_renderHandles:function(){var e=this,t=e.get(Q),n=e.get(L);e.eachHandle(function(e){n.append(e)}),t.append(n)},_buildHandle:function(t){var n=this;return e.Node.create(e.Lang.sub(n.HANDLE_TEMPLATE,{handle:t}))},_calcResize:function(){var t=this,n=t.handle,r=t.info,i=t.originalInfo,s=r.actXY[0]-i.actXY[0],o=r.actXY[1]-i.actXY[1];n&&e.Resize.RULES[n]&&e.Resize.RULES[n](t,s,o)},_checkSize:function(e,t){var n=this,r=n.info,i=n.originalInfo,s=e===B?$:M;r[e]=t;if(s===M&&n.changeLeftHandles||s===$&&n.changeTopHandles)r[s]=i[s]+i[e]-t},_copyStyles:function(t,n){var r=t.getStyle(q).toLowerCase(),i=this._getBoxSurroundingInfo(t),s;r===X&&(r=R),s={position:r,left:pt(t,M),top:pt(t,$)},e.mix(s,i.margin),e.mix(s,i.border),n.setStyles(s),t.setStyles({border:0,margin:0}),n.sizeTo(t.get(j)+i.totalHBorder,t.get(B)+i.totalVBorder)},_extractHandleName:e.cached(function(e){var t=e.get(S),n=t.match(new RegExp(yt(U,C,"(\\w{1,2})\\b")));return n?n[1
+]:null}),_getInfo:function(e,t){var n=[0,0],r=t.dragEvent.target,i=e.getXY(),s=i[0],o=i[1],u=e.get(B),a=e.get(j);return t&&(n=r.actXY.length?r.actXY:r.lastXY),{actXY:n,bottom:o+u,left:s,offsetHeight:u,offsetWidth:a,right:s+a,top:o}},_getBoxSurroundingInfo:function(t){var n={padding:{},margin:{},border:{}};return vt(t)&&e.each([$,W,E,M],function(e){var r=gt(F,e),i=gt(_,e),s=gt(w,e,J),o=gt(w,e,x),u=gt(w,e,V);n.border[o]=pt(t,o),n.border[u]=pt(t,u),n.border[s]=pt(t,s),n.margin[i]=pt(t,i),n.padding[r]=pt(t,r)}),n.totalHBorder=ht(n.border.borderLeftWidth)+ht(n.border.borderRightWidth),n.totalHPadding=ht(n.padding.paddingLeft)+ht(n.padding.paddingRight),n.totalVBorder=ht(n.border.borderBottomWidth)+ht(n.border.borderTopWidth),n.totalVPadding=ht(n.padding.paddingBottom)+ht(n.padding.paddingTop),n},_syncUI:function(){var t=this,n=t.info,r=t.wrapperSurrounding,i=t.get(Q),s=t.get(D);i.sizeTo(n.offsetWidth,n.offsetHeight),(t.changeLeftHandles||t.changeTopHandles)&&i.setXY([n.left,n.top]),i.compareTo(s)||s.sizeTo(n.offsetWidth-r.totalHBorder,n.offsetHeight-r.totalVBorder),e.UA.webkit&&s.setStyle(U,H)},_updateChangeHandleInfo:function(e){var t=this;t.changeHeightHandles=t.REGEX_CHANGE_HEIGHT.test(e),t.changeLeftHandles=t.REGEX_CHANGE_LEFT.test(e),t.changeTopHandles=t.REGEX_CHANGE_TOP.test(e),t.changeWidthHandles=t.REGEX_CHANGE_WIDTH.test(e)},_updateInfo:function(e){var t=this;t.info=t._getInfo(t.get(Q),e)},_updateSurroundingInfo:function(){var e=this,t=e.get(D),n=e.get(Q),r=e._getBoxSurroundingInfo(t),i=e._getBoxSurroundingInfo(n);e.nodeSurrounding=r,e.wrapperSurrounding=i,e.totalVSurrounding=r.totalVPadding+i.totalVBorder,e.totalHSurrounding=r.totalHPadding+i.totalHBorder},_setActiveHandlesUI:function(e){var t=this,n=t.get(g);n&&(e?(t.eachHandle(function(e){e.removeClass(Et)}),n.addClass(Et)):n.removeClass(Et))},_setHandles:function(t){var n=this,i=[];return r(t)?i=t:o(t)&&(t.toLowerCase()===y?i=n.ALL_HANDLES:e.each(t.split(l),function(e){var t=a(e);f(n.ALL_HANDLES,t)>-1&&i.push(t)})),i},_setHideHandlesUI:function(e){var t=this,n=t.get(Q);t.get(z)||(e?n.addClass(Nt):n.removeClass(Nt))},_setWrap:function(e){var t=this,n=t.get(D),r=n.get(P),i=t.get(G);return i.test(r)&&(e=!0),e},_defMouseUpFn:function(){var e=this;e.set(z,!1)},_defResizeFn:function(e){var t=this;t._resize(e)},_resize:function(e){var t=this;t._handleResizeAlignEvent(e.dragEvent),t._syncUI()},_defResizeAlignFn:function(e){var t=this;t._resizeAlign(e)},_resizeAlign:function(e){var t=this,n,r,i;t.lastInfo=t.info,t._updateInfo(e),n=t.info,t._calcResize(),t.con||(r=t.get(T)+t.totalVSurrounding,i=t.get(N)+t.totalHSurrounding,n.offsetHeight<=r&&t._checkSize(B,r),n.offsetWidth<=i&&t._checkSize(j,i))},_defResizeEndFn:function(e){var t=this;t._resizeEnd(e)},_resizeEnd:function(e){var t=this,n=e.dragEvent.target;n.actXY=[],t._syncUI(),t._setActiveHandlesUI(!1),t.set(m,null),t.set(g,null),t.handle=null},_defResizeStartFn:function(e){var t=this;t._resizeStart(e)},_resizeStart:function(e){var t=this,n=t.get(Q);t.handle=t.get(m),t.set(z,!0),t._updateSurroundingInfo(),t.originalInfo=t._getInfo(n,e),t._updateInfo(e)},_handleMouseUpEvent:function(e){this.fire(Y,{dragEvent:e,info:this.info})},_handleResizeEvent:function(e){this.fire(Z,{dragEvent:e,info:this.info})},_handleResizeAlignEvent:function(e){this.fire(et,{dragEvent:e,info:this.info})},_handleResizeEndEvent:function(e){this.fire(tt,{dragEvent:e,info:this.info})},_handleResizeStartEvent:function(e){this.get(m)||this._setHandleFromNode(e.target.get("node")),this.fire(nt,{dragEvent:e,info:this.info})},_onWrapperMouseEnter:function(){var e=this;e.get(b)&&e._setHideHandlesUI(!1)},_onWrapperMouseLeave:function(){var e=this;e.get(b)&&e._setHideHandlesUI(!0)},_setHandleFromNode:function(e){var t=this,n=t._extractHandleName(e);t.get(z)||(t.set(m,n),t.set(g,e),t._setActiveHandlesUI(!0),t._updateChangeHandleInfo(n))},_onHandleMouseEnter:function(e){this._setHandleFromNode(e.currentTarget)},_onHandleMouseLeave:function(){var e=this;e.get(z)||e._setActiveHandlesUI(!1)},_valueHandlesWrapper:function(){return e.Node.create(this.HANDLES_WRAP_TEMPLATE)},_valueWrapper:function(){var t=this,n=t.get(D),r=n.get(I),i=n;return t.get(K)&&(i=e.Node.create(t.WRAP_TEMPLATE),r&&r.insertBefore(i,n),i.append(n),t._copyStyles(n,i),n.setStyles({position:X,left:0,top:0})),i}}),e.each(e.Resize.prototype.ALL_HANDLES,function(t){e.Resize.ATTRS[dt(t)]={setter:function(){return this._buildHandle(t)},value:null,writeOnce:!0}})},"3.17.2",{requires:["base","widget","event","oop","dd-drag","dd-delegate","dd-drop"],skinnable:!0});
diff --git a/js/yui3/resize-constrain/assets/skins/night/arrows.png b/js/yui3/resize-constrain/assets/skins/night/arrows.png
new file mode 100644
index 000000000..2942681f4
--- /dev/null
+++ b/js/yui3/resize-constrain/assets/skins/night/arrows.png
Binary files differ
diff --git a/js/yui3/resize-constrain/assets/skins/sam/arrows.png b/js/yui3/resize-constrain/assets/skins/sam/arrows.png
new file mode 100644
index 000000000..2942681f4
--- /dev/null
+++ b/js/yui3/resize-constrain/assets/skins/sam/arrows.png
Binary files differ
diff --git a/js/yui3/resize-constrain/resize-constrain-min.js b/js/yui3/resize-constrain/resize-constrain-min.js
new file mode 100644
index 000000000..f8dc90696
--- /dev/null
+++ b/js/yui3/resize-constrain/resize-constrain-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("resize-constrain",function(e,t){function B(){B.superclass.constructor.apply(this,arguments)}var n=e.Lang,r=n.isBoolean,i=n.isNumber,s=n.isString,o=e.Resize.capitalize,u=function(t){return t instanceof e.Node},a=function(e){return parseFloat(e)||0},f="borderBottomWidth",l="borderLeftWidth",c="borderRightWidth",h="borderTopWidth",p="border",d="bottom",v="con",m="constrain",g="host",y="left",b="maxHeight",w="maxWidth",E="minHeight",S="minWidth",x="node",T="offsetHeight",N="offsetWidth",C="preserveRatio",k="region",L="resizeConstrained",A="right",O="tickX",M="tickY",_="top",D="width",P="view",H="viewportRegion";e.mix(B,{NAME:L,NS:v,ATTRS:{constrain:{setter:function(t){return t&&(u(t)||s(t)||t.nodeType)&&(t=e.one(t)),t}},minHeight:{value:15,validator:i},minWidth:{value:15,validator:i},maxHeight:{value:Infinity,validator:i},maxWidth:{value:Infinity,validator:i},preserveRatio:{value:!1,validator:r},tickX:{value:!1},tickY:{value:!1}}}),e.extend(B,e.Plugin.Base,{constrainSurrounding:null,initializer:function(){var t=this,n=t.get(g);n.delegate.dd.plug(e.Plugin.DDConstrained,{tickX:t.get(O),tickY:t.get(M)}),n.after("resize:align",e.bind(t._handleResizeAlignEvent,t)),n.on("resize:start",e.bind(t._handleResizeStartEvent,t))},_checkConstrain:function(e,t,n){var r=this,i,s,u,f,l=r.get(g),c=l.info,h=r.constrainSurrounding.border,d=r._getConstrainRegion();d&&(i=c[e]+c[n],s=d[t]-a(h[o(p,t,D)]),i>=s&&(c[n]-=i-s),u=c[e],f=d[e]+a(h[o(p,e,D)]),u<=f&&(c[e]+=f-u,c[n]-=f-u))},_checkHeight:function(){var e=this,t=e.get(g),n=t.info,r=e.get(b)+t.totalVSurrounding,i=e.get(E)+t.totalVSurrounding;e._checkConstrain(_,d,T),n.offsetHeight>r&&t._checkSize(T,r),n.offsetHeight<i&&t._checkSize(T,i)},_checkRatio:function(){var t=this,n=t.get(g),r=n.info,s=n.originalInfo,o=s.offsetWidth,u=s.offsetHeight,p=s.top,d=s.left,v=s.bottom,y=s.right,b=function(){return r.offsetWidth/o},w=function(){return r.offsetHeight/u},E=n.changeHeightHandles,S,x,T,N,C,k;t.get(m)&&n.changeHeightHandles&&n.changeWidthHandles&&(T=t._getConstrainRegion(),x=t.constrainSurrounding.border,S=T.bottom-a(x[f])-v,N=d-(T.left+a(x[l])),C=T.right-a(x[c])-y,k=p-(T.top+a(x[h])),n.changeLeftHandles&&n.changeTopHandles?E=k<N:n.changeLeftHandles?E=S<N:n.changeTopHandles?E=k<C:E=S<C),E?(r.offsetWidth=o*w(),t._checkWidth(),r.offsetHeight=u*b()):(r.offsetHeight=u*b(),t._checkHeight(),r.offsetWidth=o*w()),n.changeTopHandles&&(r.top=p+(u-r.offsetHeight)),n.changeLeftHandles&&(r.left=d+(o-r.offsetWidth)),e.each(r,function(e,t){i(e)&&(r[t]=Math.round(e))})},_checkRegion:function(){var t=this,n=t.get(g),r=t._getConstrainRegion();return e.DOM.inRegion(null,r,!0,n.info)},_checkWidth:function(){var e=this,t=e.get(g),n=t.info,r=e.get(w)+t.totalHSurrounding,i=e.get(S)+t.totalHSurrounding;e._checkConstrain(y,A,N),n.offsetWidth<i&&t._checkSize(N,i),n.offsetWidth>r&&t._checkSize(N,r)},_getConstrainRegion:function(){var e=this,t=e.get(g),n=t.get(x),r=e.get(m),i=null;return r&&(r===P?i=n.get(H):u(r)?i=r.get(k):i=r),i},_handleResizeAlignEvent:function(){var e=this,t=e.get(g);e._checkHeight(),e._checkWidth(),e.get(C)&&e._checkRatio(),e.get(m)&&!e._checkRegion()&&(t.info=t.lastInfo)},_handleResizeStartEvent:function(){var e=this,t=e.get(m),n=e.get(g);e.constrainSurrounding=n._getBoxSurroundingInfo(t)}}),e.namespace("Plugin"),e.Plugin.ResizeConstrained=B},"3.17.2",{requires:["plugin","resize-base"]});
diff --git a/js/yui3/resize-plugin/assets/skins/night/arrows.png b/js/yui3/resize-plugin/assets/skins/night/arrows.png
new file mode 100644
index 000000000..2942681f4
--- /dev/null
+++ b/js/yui3/resize-plugin/assets/skins/night/arrows.png
Binary files differ
diff --git a/js/yui3/resize-plugin/assets/skins/sam/arrows.png b/js/yui3/resize-plugin/assets/skins/sam/arrows.png
new file mode 100644
index 000000000..2942681f4
--- /dev/null
+++ b/js/yui3/resize-plugin/assets/skins/sam/arrows.png
Binary files differ
diff --git a/js/yui3/resize-plugin/resize-plugin-min.js b/js/yui3/resize-plugin/resize-plugin-min.js
new file mode 100644
index 000000000..03a7904da
--- /dev/null
+++ b/js/yui3/resize-plugin/resize-plugin-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("resize-plugin",function(e,t){var n=function(t){t.node=e.Widget&&t.host instanceof e.Widget?t.host.get("boundingBox"):t.host,t.host instanceof e.Widget?t.widget=t.host:t.widget=!1,n.superclass.constructor.call(this,t)};n.NAME="resize-plugin",n.NS="resize",n.ATTRS={node:{value:undefined},widget:{value:undefined}},e.extend(n,e.Resize,{initializer:function(e){this.set("node",e.node),this.set("widget",e.widget),this.on("resize:resize",function(e){this._correctDimensions(e)})},_correctDimensions:function(e){var t=this.get("node"),n={old:t.getX(),cur:e.currentTarget.info.left},r={old:t.getY(),cur:e.currentTarget.info.top};this.get("widget")&&this._setWidgetProperties(e,n,r),this._isDifferent(n.old,n.cur)&&t.set("x",n.cur),this._isDifferent(r.old,r.cur)&&t.set("y",r.cur)},_setWidgetProperties:function(t,n,r){var i=this.get("widget"),s=i.get("height"),o=i.get("width"),u=t.currentTarget.info.offsetWidth-t.currentTarget.totalHSurrounding,a=t.currentTarget.info.offsetHeight-t.currentTarget.totalVSurrounding;this._isDifferent(s,a)&&i.set("height",a),this._isDifferent(o,u)&&i.set("width",u),i.hasImpl&&i.hasImpl(e.WidgetPosition)&&(this._isDifferent(i.get("x"),n.cur)&&i.set("x",n.cur),this._isDifferent(i.get("y"),r.cur)&&i.set("y",r.cur))},_isDifferent:function(e,t){var n=!1;return e!==t&&(n=t),n}}),e.namespace("Plugin"),e.Plugin.Resize=n},"3.17.2",{requires:["resize-base","plugin"],optional:["resize-constrain"]});
diff --git a/js/yui3/resize-proxy/assets/skins/night/arrows.png b/js/yui3/resize-proxy/assets/skins/night/arrows.png
new file mode 100644
index 000000000..2942681f4
--- /dev/null
+++ b/js/yui3/resize-proxy/assets/skins/night/arrows.png
Binary files differ
diff --git a/js/yui3/resize-proxy/assets/skins/sam/arrows.png b/js/yui3/resize-proxy/assets/skins/sam/arrows.png
new file mode 100644
index 000000000..2942681f4
--- /dev/null
+++ b/js/yui3/resize-proxy/assets/skins/sam/arrows.png
Binary files differ
diff --git a/js/yui3/resize-proxy/resize-proxy-min.js b/js/yui3/resize-proxy/resize-proxy-min.js
new file mode 100644
index 000000000..6e482dc2c
--- /dev/null
+++ b/js/yui3/resize-proxy/resize-proxy-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("resize-proxy",function(e,t){function d(){d.superclass.constructor.apply(this,arguments)}var n="activeHandleNode",r="cursor",i="dragCursor",s="host",o="parentNode",u="proxy",a="proxyNode",f="resize",l="resize-proxy",c="wrapper",h=e.ClassNameManager.getClassName,p=h(f,u);e.mix(d,{NAME:l,NS:u,ATTRS:{proxyNode:{setter:e.one,valueFn:function(){return e.Node.create(this.PROXY_TEMPLATE)}}}}),e.extend(d,e.Plugin.Base,{PROXY_TEMPLATE:'<div class="'+p+'"></div>',initializer:function(){var e=this;e.afterHostEvent("resize:start",e._afterResizeStart),e.beforeHostMethod("_resize",e._beforeHostResize),e.afterHostMethod("_resizeEnd",e._afterHostResizeEnd)},destructor:function(){var e=this;e.get(a).remove(!0)},_afterHostResizeEnd:function(e){var t=this,n=e.dragEvent.target;n.actXY=[],t._syncProxyUI(),t.get(a).hide()},_afterResizeStart:function(){var e=this;e._renderProxy()},_beforeHostResize:function(t){var n=this,r=this.get(s);return r._handleResizeAlignEvent(t.dragEvent),n._syncProxyUI(),new e.Do.Prevent},_renderProxy:function(){var e=this,t=this.get(s),n=e.get(a);n.inDoc()||t.get(c).get(o).append(n.hide())},_syncProxyUI:function(){var e=this,t=this.get(s),o=t.info,u=t.get(n),f=e.get(a),l=u.getStyle(r);f.show().setStyle(r,l),t.delegate.dd.set(i,l),f.sizeTo(o.offsetWidth,o.offsetHeight),f.setXY([o.left,o.top])}}),e.namespace("Plugin"),e.Plugin.ResizeProxy=d},"3.17.2",{requires:["plugin","resize-base"]});
diff --git a/js/yui3/router/router-min.js b/js/yui3/router/router-min.js
new file mode 100644
index 000000000..3d739b1e5
--- /dev/null
+++ b/js/yui3/router/router-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("router",function(e,t){function c(){c.superclass.constructor.apply(this,arguments)}var n=e.HistoryHash,r=e.QueryString,i=e.Array,s=e.Lang,o=e.Object,u=e.config.win,a=[],f=[],l="ready";e.Router=e.extend(c,e.Base,{_regexURL:/^((?:[^\/#?:]+:\/\/|\/\/)[^\/]*)?([^?#]*)(\?[^#]*)?(#.*)?$/,_regexPathParam:/([:*])([\w\-]+)?/g,_regexUrlQuery:/\?([^#]*).*$/,_regexUrlOrigin:/^(?:[^\/#?:]+:\/\/|\/\/)[^\/]*/,initializer:function(t){var n=this;n._html5=n.get("html5"),n._params={},n._routes=[],n._url=n._getURL(),n._setRoutes(t&&t.routes?t.routes:n.get("routes")),n._html5?(n._history=new e.HistoryHTML5({force:!0}),n._historyEvents=e.after("history:change",n._afterHistoryChange,n)):n._historyEvents=e.on("hashchange",n._afterHistoryChange,u,n),n.publish(l,{defaultFn:n._defReadyFn,fireOnce:!0,preventable:!1}),n.once("initializedChange",function(){e.once("load",function(){setTimeout(function(){n.fire(l,{dispatched:!!n._dispatched})},20)})}),a.push(this)},destructor:function(){var e=i.indexOf(a,this);e>-1&&a.splice(e,1),this._historyEvents&&this._historyEvents.detach()},dispatch:function(){return this.once(l,function(){var e,t;this._ready=!0,this.upgrade()||(e=this._getRequest("dispatch"),t=this._getResponse(e),this._dispatch(e,t))}),this},getPath:function(){return this._getPath()},hasRoute:function(e){var t,n,r;return this._hasSameOrigin(e)?(this._html5||(e=this._upgradeURL(e)),t=this.removeQuery(e.replace(this._regexUrlOrigin,"")),r=this.match(t),r.length?(n=this.removeRoot(t),!!i.filter(r,function(e){return this._getParamValues(e,n)},this).length):!1):!1},match:function(e){var t=this.get("root");if(t){if(!this._pathHasRoot(t,e))return[];e=this.removeRoot(e)}return i.filter(this._routes,function(t){return e.search(t.regex)>-1})},param:function(e,t){return this._params[e]=t,this},removeRoot:function(e){var t=this.get("root"),n;e=e.replace(this._regexUrlOrigin,"");if(!t)return e;n=this.removeQuery(e);if(n===t||this._pathHasRoot(t,n))e=e.substring(t.length);return e.charAt(0)==="/"?e:"/"+e},removeQuery:function(e){return e.replace(/\?.*$/,"")},replace:function(e){return this._queue(e,!0)},route:function(t,n){n=i(arguments,1,!0);var r,o;return typeof t=="string"||s.isRegExp(t)?(n=i.flatten(n),r=[],o=this._getRegex(t,r),t={callbacks:n,keys:r,path:t,regex:o}):(n=i.flatten([t.callbacks||t.callback||[]].concat(n)),r=t.keys,o=t.regex||t.regexp,o||(r=[],o=this._getRegex(t.path,r)),t=e.merge(t,{callbacks:n,keys:r,path:t.path||o,regex:o})),this._routes.push(t),this},save:function(e){return this._queue(e)},upgrade:function(){if(!this._html5)return!1;var e=this._getHashPath();return e?(this.once(l,function(){this.replace(e)}),!0):!1},_decode:function(e){return decodeURIComponent(e.replace(/\+/g," "))},_dequeue:function(){var t=this,n;return YUI.Env.windowLoaded?(n=f.shift(),n?n():this):(e.once("load",function(){t._dequeue()}),this)},_dispatch:function(t,n){function a(f){var l,c,h;if(f)f==="route"?(s=[],a()):e.error(f);else if(l=s.shift())typeof l=="string"&&(c=l,l=r[c],l||e.error("Router: Callback not found: "+c,null,"router")),t.pendingCallbacks=s.length,l.call(r,t,n,a);else if(h=i.shift()){u=r._getParamValues(h,o);if(!u){a("route");return}t.params=u,t.route=h,t.pendingRoutes=i.length,s=h.callbacks.concat(),a()}}var r=this,i=r.match(t.path),s=[],o,u;return r._dispatching=r._dispatched=!0,!i||!i.length?(r._dispatching=!1,r):(o=r.removeRoot(t.path),a(),r._dispatching=!1,r._dequeue())},_getHashPath:function(e){return e||(e=n.getHash()),e&&e.charAt(0)==="/"?this._joinURL(e):""},_getOrigin:function(){var t=e.getLocation();return t.origin||t.protocol+"//"+t.host},_getParams:function(){return e.merge(this._params)},_getParamValues:function(e,t){var n,r,o;return n=i.map(e.regex.exec(t)||[],function(e){return e&&this._decode(e)||""},this),n.length-1!==e.keys.length?n:(o=i.hash(e.keys,n.slice(1)),r=i.every(e.keys,function(e){var t=this._params[e],n=o[e];return t&&n&&typeof n=="string"?(n=s.isRegExp(t)?t.exec(n):t.call(this,n,e),n!==!1&&s.isValue(n)?(o[e]=n,!0):!1):!0},this),r?o:!1)},_getPath:function(){var t=!this._html5&&this._getHashPath()||e.getLocation().pathname;return this.removeQuery(t)},_getPathRoot:function(){var t="/",n=e.getLocation().pathname,r;return n.charAt(n.length-1)===t?n:(r=n.split(t),r.pop(),r.join(t)+t)},_getQuery:function(){var t=e.getLocation(),r,i;return this._html5?t.search.substring(1):(r=n.getHash(),i=r.match(this._regexUrlQuery),r&&i?i[1]:t.search.substring(1))},_getRegex:function(e,t){return s.isRegExp(e)?e:e==="*"?/.*/:(e=e.replace(this._regexPathParam,function(e,n,r){return r?(t.push(r),n==="*"?"(.*?)":"([^/#?]+)"):n==="*"?".*":e}),new RegExp("^"+e+"$"))},_getRequest:function(e){return{path:this._getPath(),query:this._parseQuery(this._getQuery()),url:this._getURL(),router:this,src:e}},_getResponse:function(e){return{req:e}},_getRoutes:function(){return this._routes.concat()},_getURL:function(){var t=e.getLocation().toString();return this._html5||(t=this._upgradeURL(t)),t},_hasSameOrigin:function(t){var n=(t&&t.match(this._regexUrlOrigin)||[])[0];return n&&n.indexOf("//")===0&&(n=e.getLocation().protocol+n),!n||n===this._getOrigin()},_joinURL:function(e){var t=this.get("root");return e=this.removeRoot(e),e.charAt(0)==="/"&&(e=e.substring(1)),t&&t.charAt(t.length-1)==="/"?t+e:t+"/"+e},_normalizePath:function(e){var t="..",n="/",r,i,s,o,u,a;if(!e||e===n)return n;o=e.split(n),a=[];for(r=0,i=o.length;r<i;++r)u=o[r],u===t?a.pop():u&&a.push(u);return s=n+a.join(n),s!==n&&e.charAt(e.length-1)===n&&(s+=n),s},_parseQuery:r&&r.parse?r.parse:function(e){var t=this._decode,n=e.split("&"),r=0,i=n.length,s={},o;for(;r<i;++r)o=n[r].split("="),o[0]&&(s[t(o[0])]=t(o[1]||""));return s},_pathHasRoot:function(e,t){var n=e.charAt(e.length-1)==="/"?e:e+"/";return t.indexOf(n)===0},_queue:function(){var t=arguments,n=this;return f.push(function(){return n._html5?e.UA.ios&&e.UA.ios<5?n._save.apply(n,t):setTimeout(function(){n._save.apply(n,t)},1):(n._dispatching=!0,n._save.apply(n,t)),n}),this._dispatching?this:this.
+_dequeue()},_resolvePath:function(t){return t?(t.charAt(0)!=="/"&&(t=this._getPathRoot()+t),this._normalizePath(t)):e.getLocation().pathname},_resolveURL:function(t){var n=t&&t.match(this._regexURL),r,i,s,o,u;return n?(r=n[1],i=n[2],s=n[3],o=n[4],r?(r.indexOf("//")===0&&(r=e.getLocation().protocol+r),r+(i||"/")+(s||"")+(o||"")):(u=this._getOrigin()+this._resolvePath(i),i||s?u+(s||"")+(o||""):(s=this._getQuery(),u+(s?"?"+s:"")+(o||"")))):e.getLocation().toString()},_save:function(t,r){var i=typeof t=="string",s,o,u;if(i&&!this._hasSameOrigin(t))return e.error("Security error: The new URL must be of the same origin as the current URL."),this;i&&(t=this._joinURL(t)),this._ready=!0;if(this._html5)this._history[r?"replace":"add"](null,{url:t});else{s=e.getLocation().pathname,o=this.get("root"),u=n.getHash(),i||(t=u);if(o===s||o===this._getPathRoot())t=this.removeRoot(t);t===u?e.Router.dispatch():n[r?"replaceHash":"setHash"](t)}return this},_setParams:function(t){return this._params={},o.each(t,function(e,t){this.param(t,e)},this),e.merge(this._params)},_setRoutes:function(e){return this._routes=[],i.each(e,function(e){this.route(e)},this),this._routes.concat()},_upgradeURL:function(t){if(!this._hasSameOrigin(t))return t;var n=(t.match(/#(.*)$/)||[])[1]||"",r=e.HistoryHash.hashPrefix,i;r&&n.indexOf(r)===0&&(n=n.replace(r,""));if(n){i=this._getHashPath(n);if(i)return this._resolveURL(i)}return t},_afterHistoryChange:function(e){var t=this,n=e.src,r=t._url,i=t._getURL(),s,o;t._url=i;if(n==="popstate"&&(!t._ready||r.replace(/#.*$/,"")===i.replace(/#.*$/,"")))return;s=t._getRequest(n),o=t._getResponse(s),t._dispatch(s,o)},_defReadyFn:function(e){this._ready=!0}},{NAME:"router",ATTRS:{html5:{valueFn:function(){return e.Router.html5},writeOnce:"initOnly"},params:{value:{},getter:"_getParams",setter:"_setParams"},root:{value:""},routes:{value:[],getter:"_getRoutes",setter:"_setRoutes"}},html5:e.HistoryBase.html5&&(!e.UA.android||e.UA.android>=3),_instances:a,dispatch:function(){var e,t,n,r,i;for(e=0,t=a.length;e<t;e+=1)n=a[e],n&&(r=n._getRequest("dispatch"),i=n._getResponse(r),n._dispatch(r,i))}}),e.Controller=e.Router},"3.17.2",{optional:["querystring-parse"],requires:["array-extras","base-build","history"]});
diff --git a/js/yui3/scrollview-base-ie/scrollview-base-ie-min.js b/js/yui3/scrollview-base-ie/scrollview-base-ie-min.js
new file mode 100644
index 000000000..d3f53cca9
--- /dev/null
+++ b/js/yui3/scrollview-base-ie/scrollview-base-ie-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("scrollview-base-ie",function(e,t){e.mix(e.ScrollView.prototype,{_fixIESelect:function(t,n){this._cbDoc=n.get("ownerDocument"),this._nativeBody=e.Node.getDOMNode(e.one("body",this._cbDoc)),n.on("mousedown",function(){this._selectstart=this._nativeBody.onselectstart,this._nativeBody.onselectstart=this._iePreventSelect,this._cbDoc.once("mouseup",this._ieRestoreSelect,this)},this)},_iePreventSelect:function(){return!1},_ieRestoreSelect:function(){this._nativeBody.onselectstart=this._selectstart}},!0)},"3.17.2",{requires:["scrollview-base"]});
diff --git a/js/yui3/scrollview-base/assets/scrollview-base-core.css b/js/yui3/scrollview-base/assets/scrollview-base-core.css
new file mode 100644
index 000000000..74a762516
--- /dev/null
+++ b/js/yui3/scrollview-base/assets/scrollview-base-core.css
@@ -0,0 +1,21 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-scrollview {
+ position: relative;
+ overflow: hidden;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+}
+
+.yui3-scrollview-hidden {
+ display:none;
+}
+
+.yui3-scrollview-content {
+ position:relative;
+} \ No newline at end of file
diff --git a/js/yui3/scrollview-base/assets/skins/night/scrollview-base.css b/js/yui3/scrollview-base/assets/skins/night/scrollview-base.css
new file mode 100644
index 000000000..ba1572531
--- /dev/null
+++ b/js/yui3/scrollview-base/assets/skins/night/scrollview-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-scrollview{position:relative;overflow:hidden;-webkit-user-select:none;-moz-user-select:none}.yui3-scrollview-hidden{display:none}.yui3-scrollview-content{position:relative}#yui3-css-stamp.skin-night-scrollview-base{display:none}
diff --git a/js/yui3/scrollview-base/assets/skins/sam/scrollview-base.css b/js/yui3/scrollview-base/assets/skins/sam/scrollview-base.css
new file mode 100644
index 000000000..ffa5f7d26
--- /dev/null
+++ b/js/yui3/scrollview-base/assets/skins/sam/scrollview-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-scrollview{position:relative;overflow:hidden;-webkit-user-select:none;-moz-user-select:none}.yui3-scrollview-hidden{display:none}.yui3-scrollview-content{position:relative}.yui3-skin-sam .yui3-scrollview{-webkit-tap-highlight-color:rgba(255,255,255,0)}#yui3-css-stamp.skin-sam-scrollview-base{display:none}
diff --git a/js/yui3/scrollview-base/scrollview-base-min.js b/js/yui3/scrollview-base/scrollview-base-min.js
new file mode 100644
index 000000000..d86730542
--- /dev/null
+++ b/js/yui3/scrollview-base/scrollview-base-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("scrollview-base",function(e,t){function F(){F.superclass.constructor.apply(this,arguments)}var n=e.ClassNameManager.getClassName,r=e.config.doc,i=e.UA.ie,s=e.Transition.useNative,o=e.Transition._VENDOR_PREFIX,u="scrollview",a={vertical:n(u,"vert"),horizontal:n(u,"horiz")},f="scrollEnd",l="flick",c="drag",h="mousewheel",p="ui",d="top",v="left",m="px",g="axis",y="scrollY",b="scrollX",w="bounce",E="disabled",S="deceleration",x="x",T="y",N="boundingBox",C="contentBox",k="gesturemove",L="start",A="end",O="",M="0s",_="snapDuration",D="snapEasing",P="easing",H="frameDuration",B="bounceRange",j=function(e,t,n){return Math.min(Math.max(e,t),n)};e.ScrollView=e.extend(F,e.Widget,{_forceHWTransforms:e.UA.webkit?!0:!1,_prevent:{start:!1,move:!0,end:!1},lastScrolledAmt:0,_minScrollX:null,_maxScrollX:null,_minScrollY:null,_maxScrollY:null,initializer:function(){var e=this;e._bb=e.get(N),e._cb=e.get(C),e._cAxis=e.get(g),e._cBounce=e.get(w),e._cBounceRange=e.get(B),e._cDeceleration=e.get(S),e._cFrameDuration=e.get(H)},bindUI:function(){var e=this;e._bindFlick(e.get(l)),e._bindDrag(e.get(c)),e._bindMousewheel(!0),e._bindAttrs(),i&&e._fixIESelect(e._bb,e._cb),F.SNAP_DURATION&&e.set(_,F.SNAP_DURATION),F.SNAP_EASING&&e.set(D,F.SNAP_EASING),F.EASING&&e.set(P,F.EASING),F.FRAME_STEP&&e.set(H,F.FRAME_STEP),F.BOUNCE_RANGE&&e.set(B,F.BOUNCE_RANGE)},_bindAttrs:function(){var e=this,t=e._afterScrollChange,n=e._afterDimChange;e.after({scrollEnd:e._afterScrollEnd,disabledChange:e._afterDisabledChange,flickChange:e._afterFlickChange,dragChange:e._afterDragChange,axisChange:e._afterAxisChange,scrollYChange:t,scrollXChange:t,heightChange:n,widthChange:n})},_bindDrag:function(t){var n=this,r=n._bb;r.detach(c+"|*"),t&&r.on(c+"|"+k+L,e.bind(n._onGestureMoveStart,n))},_bindFlick:function(t){var n=this,r=n._bb;r.detach(l+"|*"),t&&(r.on(l+"|"+l,e.bind(n._flick,n),t),n._bindDrag(n.get(c)))},_bindMousewheel:function(t){var n=this,i=n._bb;i.detach(h+"|*"),t&&e.one(r).on(h,e.bind(n._mousewheel,n))},syncUI:function(){var e=this,t=e._getScrollDims(),n=t.offsetWidth,r=t.offsetHeight,i=t.scrollWidth,s=t.scrollHeight;e._cAxis===undefined&&(e._cAxis={x:i>n,y:s>r},e._set(g,e._cAxis)),e.rtl=e._cb.getComputedStyle("direction")==="rtl",e._cDisabled=e.get(E),e._uiDimensionsChange(),e._isOutOfBounds()&&e._snapBack()},_getScrollDims:function(){var e=this,t=e._cb,n=e._bb,r=F._TRANSITION,i=e.get(b),o=e.get(y),u,a;return s&&(t.setStyle(r.DURATION,M),t.setStyle(r.PROPERTY,O)),u=e._forceHWTransforms,e._forceHWTransforms=!1,e._moveTo(t,0,0),a={offsetWidth:n.get("offsetWidth"),offsetHeight:n.get("offsetHeight"),scrollWidth:n.get("scrollWidth"),scrollHeight:n.get("scrollHeight")},e._moveTo(t,-i,-o),e._forceHWTransforms=u,a},_uiDimensionsChange:function(){var e=this,t=e._bb,n=e._getScrollDims(),r=n.offsetWidth,i=n.offsetHeight,s=n.scrollWidth,o=n.scrollHeight,u=e.rtl,f=e._cAxis,l=u?Math.min(0,-(s-r)):0,c=u?0:Math.max(0,s-r),h=0,p=Math.max(0,o-i);f&&f.x&&t.addClass(a.horizontal),f&&f.y&&t.addClass(a.vertical),e._setBounds({minScrollX:l,maxScrollX:c,minScrollY:h,maxScrollY:p})},_setBounds:function(e){var t=this;t._minScrollX=e.minScrollX,t._maxScrollX=e.maxScrollX,t._minScrollY=e.minScrollY,t._maxScrollY=e.maxScrollY},_getBounds:function(){var e=this;return{minScrollX:e._minScrollX,maxScrollX:e._maxScrollX,minScrollY:e._minScrollY,maxScrollY:e._maxScrollY}},scrollTo:function(t,n,r,i,o){if(this._cDisabled)return;var u=this,a=u._cb,f=F._TRANSITION,l=e.bind(u._onTransEnd,u),c=0,h=0,g={},w;r=r||0,i=i||u.get(P),o=o||a,t!==null&&(u.set(b,t,{src:p}),c=-t),n!==null&&(u.set(y,n,{src:p}),h=-n),w=u._transform(c,h),s&&o.setStyle(f.DURATION,M).setStyle(f.PROPERTY,O),r===0?s?o.setStyle("transform",w):(t!==null&&o.setStyle(v,c+m),n!==null&&o.setStyle(d,h+m)):(g.easing=i,g.duration=r/1e3,s?g.transform=w:(g.left=c+m,g.top=h+m),o.transition(g,l))},_transform:function(e,t){var n="translate("+e+"px, "+t+"px)";return this._forceHWTransforms&&(n+=" translateZ(0)"),n},_moveTo:function(e,t,n){s?e.setStyle("transform",this._transform(t,n)):(e.setStyle(v,t+m),e.setStyle(d,n+m))},_onTransEnd:function(){var e=this;e._isOutOfBounds()?e._snapBack():e.fire(f)},_onGestureMoveStart:function(t){if(this._cDisabled)return!1;var n=this,r=n._bb,i=n.get(b),s=n.get(y),o=t.clientX,u=t.clientY;n._prevent.start&&t.preventDefault(),n._flickAnim&&(n._cancelFlick(),n._onTransEnd()),n.lastScrolledAmt=0,n._gesture={axis:null,startX:i,startY:s,startClientX:o,startClientY:u,endClientX:null,endClientY:null,deltaX:null,deltaY:null,flick:null,onGestureMove:r.on(c+"|"+k,e.bind(n._onGestureMove,n)),onGestureMoveEnd:r.on(c+"|"+k+A,e.bind(n._onGestureMoveEnd,n))}},_onGestureMove:function(e){var t=this,n=t._gesture,r=t._cAxis,i=r.x,s=r.y,o=n.startX,u=n.startY,a=n.startClientX,f=n.startClientY,l=e.clientX,c=e.clientY;t._prevent.move&&e.preventDefault(),n.deltaX=a-l,n.deltaY=f-c,n.axis===null&&(n.axis=Math.abs(n.deltaX)>Math.abs(n.deltaY)?x:T),n.axis===x&&i?t.set(b,o+n.deltaX):n.axis===T&&s&&t.set(y,u+n.deltaY)},_onGestureMoveEnd:function(e){var t=this,n=t._gesture,r=n.flick,i=e.clientX,s=e.clientY,o;t._prevent.end&&e.preventDefault(),n.endClientX=i,n.endClientY=s,n.onGestureMove.detach(),n.onGestureMoveEnd.detach(),r||n.deltaX!==null&&n.deltaY!==null&&(o=t._isOutOfBounds(),o?t._snapBack():(!t.pages||t.pages&&!t.pages.get(g)[n.axis])&&t._onTransEnd())},_flick:function(e){if(this._cDisabled)return!1;var t=this,n=t._cAxis,r=e.flick,i=r.axis,s=r.velocity,o=i===x?b:y,u=t.get(o);t._gesture&&(t._gesture.flick=r),n[i]&&t._flickFrame(s,i,u)},_flickFrame:function(t,n,r){var i=this,s=n===x?b:y,o=i._getBounds(),u=i._cBounce,a=i._cBounceRange,f=i._cDeceleration,l=i._cFrameDuration,c=t*f,h=r-l*c,p=n===x?o.minScrollX:o.minScrollY,d=n===x?o.maxScrollX:o.maxScrollY,v=h<p,m=h<d,g=h>p,w=h>d,E=h<p-a,S=v&&h>p-a,T=w&&h<d+a,N=h>d+a,C;if(S||T)c*=u;C=Math.abs(c).toFixed(4)<.015,C||E||N?(i._flickAnim&&i._cancelFlick(),g&&m?i._onTransEnd():i._snapBack()):(i._flickAnim=e.later(l,i,"_flickFrame",[c,n,h]),i.set(s,h))},_cancelFlick:function(
+){var e=this;e._flickAnim&&(e._flickAnim.cancel(),delete e._flickAnim)},_mousewheel:function(e){var t=this,n=t.get(y),r=t._getBounds(),i=t._bb,s=10,o=e.wheelDelta>0,u=n-(o?1:-1)*s;u=j(u,r.minScrollY,r.maxScrollY),i.contains(e.target)&&t._cAxis[T]&&(t.lastScrolledAmt=0,t.set(y,u),t.scrollbars&&(t.scrollbars._update(),t.scrollbars.flash()),t._onTransEnd(),e.preventDefault())},_isOutOfBounds:function(e,t){var n=this,r=n._cAxis,i=r.x,s=r.y,o=e||n.get(b),u=t||n.get(y),a=n._getBounds(),f=a.minScrollX,l=a.minScrollY,c=a.maxScrollX,h=a.maxScrollY;return i&&(o<f||o>c)||s&&(u<l||u>h)},_snapBack:function(){var e=this,t=e.get(b),n=e.get(y),r=e._getBounds(),i=r.minScrollX,s=r.minScrollY,o=r.maxScrollX,u=r.maxScrollY,a=j(n,s,u),f=j(t,i,o),l=e.get(_),c=e.get(D);f!==t?e.set(b,f,{duration:l,easing:c}):a!==n?e.set(y,a,{duration:l,easing:c}):e._onTransEnd()},_afterScrollChange:function(e){if(e.src===F.UI_SRC)return!1;var t=this,n=e.duration,r=e.easing,i=e.newVal,s=[];t.lastScrolledAmt=t.lastScrolledAmt+(e.newVal-e.prevVal),e.attrName===b?(s.push(i),s.push(t.get(y))):(s.push(t.get(b)),s.push(i)),s.push(n),s.push(r),t.scrollTo.apply(t,s)},_afterFlickChange:function(e){this._bindFlick(e.newVal)},_afterDisabledChange:function(e){this._cDisabled=e.newVal},_afterAxisChange:function(e){this._cAxis=e.newVal},_afterDragChange:function(e){this._bindDrag(e.newVal)},_afterDimChange:function(){this._uiDimensionsChange()},_afterScrollEnd:function(){var e=this;e._flickAnim&&e._cancelFlick()},_axisSetter:function(t){if(e.Lang.isString(t))return{x:t.match(/x/i)?!0:!1,y:t.match(/y/i)?!0:!1}},_setScroll:function(t){return this._cDisabled&&(t=e.Attribute.INVALID_VALUE),t},_setScrollX:function(e){return this._setScroll(e,x)},_setScrollY:function(e){return this._setScroll(e,T)}},{NAME:"scrollview",ATTRS:{axis:{setter:"_axisSetter",writeOnce:"initOnly"},scrollX:{value:0,setter:"_setScrollX"},scrollY:{value:0,setter:"_setScrollY"},deceleration:{value:.93},bounce:{value:.1},flick:{value:{minDistance:10,minVelocity:.3}},drag:{value:!0},snapDuration:{value:400},snapEasing:{value:"ease-out"},easing:{value:"cubic-bezier(0, 0.1, 0, 1.0)"},frameDuration:{value:15},bounceRange:{value:150}},CLASS_NAMES:a,UI_SRC:p,_TRANSITION:{DURATION:o?o+"TransitionDuration":"transitionDuration",PROPERTY:o?o+"TransitionProperty":"transitionProperty"},BOUNCE_RANGE:!1,FRAME_STEP:!1,EASING:!1,SNAP_EASING:!1,SNAP_DURATION:!1})},"3.17.2",{requires:["widget","event-gestures","event-mousewheel","transition"],skinnable:!0});
diff --git a/js/yui3/scrollview-list/assets/scrollview-list-core.css b/js/yui3/scrollview-list/assets/scrollview-list-core.css
new file mode 100644
index 000000000..b870e2c78
--- /dev/null
+++ b/js/yui3/scrollview-list/assets/scrollview-list-core.css
@@ -0,0 +1,7 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
diff --git a/js/yui3/scrollview-list/assets/skins/night/scrollview-list.css b/js/yui3/scrollview-list/assets/skins/night/scrollview-list.css
new file mode 100644
index 000000000..7e2fe81bb
--- /dev/null
+++ b/js/yui3/scrollview-list/assets/skins/night/scrollview-list.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night .yui3-scrollview{-webkit-tap-highlight-color:rgba(0,0,0,0)}.yui3-skin-night .yui3-scrollview{color:#fff;background-color:#000}.yui3-skin-night .yui3-scrollview-vert .yui3-scrollview-content{border-top:0;background-color:#000;font-family:HelveticaNeue,arial,helvetica,clean,sans-serif;color:#fff}.yui3-skin-night .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-item{*zoom:1}.yui3-skin-night .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-list{*zoom:1;list-style:none;padding:0;margin:0}.yui3-skin-night .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-item{border-bottom:1px solid #303030;padding:15px 20px 16px;font-size:100%;font-weight:bold;background-color:#151515;cursor:pointer}.yui3-skin-night .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-list.selected{background-color:#2c2d2e;background:-moz-linear-gradient(0% 100% 90deg,#252626 0,#333434 100%);background:-webkit-gradient(linear,left top,left bottom,from(#333434),to(#252626));border-top:solid 1px #4b4b4b;border-bottom:solid 1px #3e3f3f;margin-top:-1px}#yui3-css-stamp.skin-night-scrollview-list{display:none}
diff --git a/js/yui3/scrollview-list/assets/skins/sam/scrollview-list.css b/js/yui3/scrollview-list/assets/skins/sam/scrollview-list.css
new file mode 100644
index 000000000..3cd9ed811
--- /dev/null
+++ b/js/yui3/scrollview-list/assets/skins/sam/scrollview-list.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-scrollview{-webkit-tap-highlight-color:rgba(255,255,255,0)}.yui3-skin-sam .yui3-scrollview{background-color:white}.yui3-skin-sam .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-item{*zoom:1}.yui3-skin-sam .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-list{*zoom:1;list-style:none;padding:0;margin:0}.yui3-skin-sam .yui3-scrollview-vert .yui3-scrollview-content{border-top:0;background-color:white;font-family:HelveticaNeue,arial,helvetica,clean,sans-serif;color:black}.yui3-skin-sam .yui3-scrollview-vert .yui3-scrollview-content .yui3-scrollview-item{border-bottom:1px solid #303030;padding:15px 20px 16px;font-size:100%;font-weight:bold;background-color:white;cursor:pointer}#yui3-css-stamp.skin-sam-scrollview-list{display:none}
diff --git a/js/yui3/scrollview-list/scrollview-list-min.js b/js/yui3/scrollview-list/scrollview-list-min.js
new file mode 100644
index 000000000..708e29436
--- /dev/null
+++ b/js/yui3/scrollview-list/scrollview-list-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("scrollview-list",function(e,t){function a(){a.superclass.constructor.apply(this,arguments)}var n=e.ClassNameManager.getClassName,r="scrollview",i=n(r,"list"),s=n(r,"item"),o="contentBox",u="host";a.NAME="pluginList",a.NS="list",a.ATTRS={isAttached:{value:!1,validator:e.Lang.isBoolean}},e.namespace("Plugin").ScrollViewList=e.extend(a,e.Plugin.Base,{initializer:function(){this._host=this.get(u),this.afterHostEvent("render",this._addClassesToList)},_addClassesToList:function(){if(!this.get("isAttached")){var e=this._host.get(o),t,n;e.hasChildNodes()&&(t=e.all("> ul"),n=e.all("> ul > li"),t.each(function(e){e.addClass(i)}),n.each(function(e){e.addClass(s)}),this.set("isAttached",!0),this._host.syncUI())}}})},"3.17.2",{requires:["plugin","classnamemanager"],skinnable:!0});
diff --git a/js/yui3/scrollview-paginator/scrollview-paginator-min.js b/js/yui3/scrollview-paginator/scrollview-paginator-min.js
new file mode 100644
index 000000000..98040b762
--- /dev/null
+++ b/js/yui3/scrollview-paginator/scrollview-paginator-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("scrollview-paginator",function(e,t){function g(){g.superclass.constructor.apply(this,arguments)}var n=e.ClassNameManager.getClassName,r="scrollview",i=n(r,"hidden"),s=n(r,"paged"),o=e.ScrollView?e.ScrollView.UI_SRC:"ui",u="index",a="scrollX",f="scrollY",l="total",c="disabled",h="host",p="selector",d="axis",v="x",m="y";e.extend(g,e.Plugin.Base,{initializer:function(e){var t=this,n=t.get(h);t._pageDims=[],t._pageBuffer=1,t._optimizeMemory=!1,t._host=n,t._bb=n._bb,t._cb=n._cb,t._cIndex=t.get(u),t._cAxis=t.get(d),e._optimizeMemory&&(t._optimizeMemory=e._optimizeMemory),e._pageBuffer&&(t._pageBuffer=e._pageBuffer),t._bindAttrs()},_bindAttrs:function(){var e=this;e.after({indexChange:e._afterIndexChange,axisChange:e._afterAxisChange}),e.beforeHostMethod("scrollTo",e._beforeHostScrollTo),e.beforeHostMethod("_mousewheel",e._beforeHostMousewheel),e.beforeHostMethod("_flick",e._beforeHostFlick),e.afterHostMethod("_onGestureMoveEnd",e._afterHostGestureMoveEnd),e.afterHostMethod("_uiDimensionsChange",e._afterHostUIDimensionsChange),e.afterHostMethod("syncUI",e._afterHostSyncUI),e.afterHostEvent("render",e._afterHostRender),e.afterHostEvent("scrollEnd",e._afterHostScrollEnded)},_afterHostRender:function(){var e=this,t=e._bb,n=e._host,r=e._cIndex,i=e._cAxis,o=e._getPageNodes(),u=o.size(),a=e._pageDims[r];i[m]?n._maxScrollX=a.maxScrollX:i[v]&&(n._maxScrollY=a.maxScrollY),e.set(l,u),r!==0&&e.scrollToIndex(r,0),t.addClass(s),e._optimize()},_afterHostSyncUI:function(){var e=this,t=e._host,n=e._getPageNodes(),r=n.size();e.set(l,r),e._cAxis===undefined&&e._set(d,t.get(d))},_afterHostUIDimensionsChange:function(){var e=this,t=e._host,n=t._getScrollDims(),r=n.offsetWidth,i=n.offsetHeight,s=e._getPageNodes();s.each(function(t,n){var s=t.get("scrollWidth"),o=t.get("scrollHeight"),u=Math.max(0,s-r),a=Math.max(0,o-i);e._pageDims[n]?(e._pageDims[n].maxScrollX=u,e._pageDims[n].maxScrollY=a):e._pageDims[n]={scrollX:0,scrollY:0,maxScrollX:u,maxScrollY:a,width:s,height:o}})},_beforeHostScrollTo:function(t,n,r,i,s){var o=this,u=o._host,a=u._gesture,f=o._cIndex,l=o._cAxis,c=o._getPageNodes(),h;return a&&(h=a.axis,h===m?t=null:n=null,l[h]===!1&&(s=c.item(f))),new e.Do.AlterArgs("new args",[t,n,r,i,s])},_afterHostGestureMoveEnd:function(){if(this._host._gesture.flick)return;var e=this,t=e._host,n=t._gesture,r=e._cIndex,i=e._cAxis,s=n.axis,o=s===v,a=n[o?"deltaX":"deltaY"],f=a>0,l=e._pageDims[r],c=l[o?"width":"height"]/2,h=Math.abs(a)>=c,p=i[s],d=t.rtl;p&&(h?e[d===f?"prev":"next"]():e.scrollToIndex(e.get(u)))},_beforeHostMousewheel:function(t){var n=this,r=n._host,i=r._bb,s=t.wheelDelta<0,o=n._cAxis;if(i.contains(t.target)&&o[m])return n[s?"next":"prev"](),t.preventDefault(),new e.Do.Prevent},_beforeHostFlick:function(t){if(this._host.get(c))return!1;if(this._host._isOutOfBounds())return new e.Do.Prevent;var n=this,r=n._host,i=r._gesture,s=n.get(d),o=t.flick,u=o.velocity,a=o.axis||!1,f=u<0,l=s[a],h=r.rtl;i&&(i.flick=o);if(l){n[h===f?"prev":"next"]();if(s[a])return new e.Do.Prevent}},_afterHostScrollEnded:function(){var e=this,t=e._host,n=e._cIndex,r=t.get(a),i=t.get(f),s=e._cAxis;s[m]?e._pageDims[n].scrollX=r:e._pageDims[n].scrollY=i,e._optimize()},_afterIndexChange:function(e){var t=this,n=t._host,r=e.newVal,i=t._pageDims[r],s=n._cAxis,u=t._cAxis;t._cIndex=r,s[v]&&s[m]&&(u[m]?(n._maxScrollX=i.maxScrollX,n.set(a,i.scrollX,{src:o})):u[v]&&(n._maxScrollY=i.maxScrollY,n.set(f,i.scrollY,{src:o}))),e.src!==o&&t.scrollToIndex(r)},_optimize:function(){if(!this._optimizeMemory)return!1;var e=this,t=e._cIndex,n=e._getStage(t);e._showNodes(n.visible),e._hideNodes(n.hidden)},_getStage:function(e){var t=this,n=t._pageBuffer,r=t.get(l),i=t._getPageNodes(),s=Math.max(0,e-n),o=Math.min(r,e+1+n);return{visible:i.splice(s,o-s),hidden:i}},_showNodes:function(e){e&&e.removeClass(i).setStyle("visibility","")},_hideNodes:function(e){e&&e.addClass(i).setStyle("visibility","hidden")},_getPageNodes:function(){var e=this,t=e._host,n=t._cb,r=e.get(p),i=r?n.all(r):n.get("children");return i},next:function(){var e=this,t=e._host,n=e._cIndex,r=n+1,i=e.get(l);if(t.get(c))return;if(r>=i)return;e.set(u,r)},prev:function(){var e=this,t=e._host,n=e._cIndex,r=n-1;if(t.get(c))return;if(r<0)return;e.set(u,r)},scrollTo:function(){return this.scrollToIndex.apply(this,arguments)},scrollToIndex:function(e,t,n){var r=this,i=r._host,s=r._getPageNodes().item(e),l=r._cAxis[v]?a:f,c=s.get(l===a?"offsetLeft":"offsetTop");t=t!==undefined?t:g.TRANSITION.duration,n=n!==undefined?n:g.TRANSITION.easing,r.set(u,e,{src:o}),r._showNodes(s),i.set(l,c,{duration:t,easing:n})},_axisSetter:function(t){if(e.Lang.isString(t))return{x:t.match(/x/i)?!0:!1,y:t.match(/y/i)?!0:!1}},_afterAxisChange:function(e){this._cAxis=e.newVal}},{NAME:"pluginScrollViewPaginator",NS:"pages",ATTRS:{axis:{setter:"_axisSetter",writeOnce:"initOnly"},selector:{value:null},index:{value:0},total:{value:0}},TRANSITION:{duration:300,easing:"ease-out"}}),e.namespace("Plugin").ScrollViewPaginator=g},"3.17.2",{requires:["plugin","classnamemanager"]});
diff --git a/js/yui3/scrollview-scrollbars/assets/scrollview-scrollbars-core.css b/js/yui3/scrollview-scrollbars/assets/scrollview-scrollbars-core.css
new file mode 100644
index 000000000..3bc707b2d
--- /dev/null
+++ b/js/yui3/scrollview-scrollbars/assets/scrollview-scrollbars-core.css
@@ -0,0 +1,102 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-scrollview-scrollbar {
+ opacity: 1;
+ position: absolute;
+ width: 6px;
+ height: 10px;
+}
+
+.yui3-scrollview-scrollbar {
+ top: 0;
+ right: 1px;
+}
+
+.yui3-scrollview-scrollbar-horiz {
+ top:auto;
+ height: 8px;
+ width: 20px;
+ bottom: 1px;
+ left: 0;
+}
+
+.yui3-scrollview-scrollbar .yui3-scrollview-child {
+ position: absolute;
+ right: 0px;
+ display: block;
+ width: 100%;
+ height: 4px;
+}
+
+.yui3-scrollview-scrollbar .yui3-scrollview-first {
+ top: 0;
+}
+
+.yui3-scrollview-scrollbar .yui3-scrollview-last {
+ top: 0;
+}
+
+.yui3-scrollview-scrollbar .yui3-scrollview-middle {
+ position: absolute;
+ top: 4px;
+ height: 1px;
+}
+
+.yui3-scrollview-scrollbar-horiz .yui3-scrollview-child {
+ display:-moz-inline-stack;
+ display:inline-block;
+ zoom:1;
+ *display:inline;
+
+ top: 0;
+ left: 0;
+ bottom: auto;
+ right: auto;
+}
+
+.yui3-scrollview-scrollbar-horiz .yui3-scrollview-first,
+.yui3-scrollview-scrollbar-horiz .yui3-scrollview-last {
+ width: 4px;
+ height: 6px;
+}
+
+.yui3-scrollview-scrollbar-horiz .yui3-scrollview-middle {
+ top: 0;
+ left: 4px;
+ width: 1px;
+ height: 6px;
+}
+
+.yui3-scrollview-scrollbar-vert-basic {
+ height:auto;
+}
+
+.yui3-scrollview-scrollbar-vert-basic .yui3-scrollview-child {
+ position:static;
+ _overflow:hidden;
+ _line-height:4px;
+}
+
+.yui3-scrollview-scrollbar-horiz-basic {
+ width:auto;
+ white-space:nowrap;
+ line-height:6px;
+ _overflow:hidden;
+}
+
+.yui3-scrollview-scrollbar-horiz-basic .yui3-scrollview-child {
+ position:static;
+
+ padding:0;
+ margin:0;
+
+ top:auto;
+ left:auto;
+ right:auto;
+ bottom:auto;
+} \ No newline at end of file
diff --git a/js/yui3/scrollview-scrollbars/assets/skins/night/scrollview-scrollbars.css b/js/yui3/scrollview-scrollbars/assets/skins/night/scrollview-scrollbars.css
new file mode 100644
index 000000000..0ad911acb
--- /dev/null
+++ b/js/yui3/scrollview-scrollbars/assets/skins/night/scrollview-scrollbars.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-scrollview-scrollbar{opacity:1;position:absolute;width:6px;height:10px}.yui3-scrollview-scrollbar{top:0;right:1px}.yui3-scrollview-scrollbar-horiz{top:auto;height:8px;width:20px;bottom:1px;left:0}.yui3-scrollview-scrollbar .yui3-scrollview-child{position:absolute;right:0;display:block;width:100%;height:4px}.yui3-scrollview-scrollbar .yui3-scrollview-first{top:0}.yui3-scrollview-scrollbar .yui3-scrollview-last{top:0}.yui3-scrollview-scrollbar .yui3-scrollview-middle{position:absolute;top:4px;height:1px}.yui3-scrollview-scrollbar-horiz .yui3-scrollview-child{display:-moz-inline-stack;display:inline-block;zoom:1;*display:inline;top:0;left:0;bottom:auto;right:auto}.yui3-scrollview-scrollbar-horiz .yui3-scrollview-first,.yui3-scrollview-scrollbar-horiz .yui3-scrollview-last{width:4px;height:6px}.yui3-scrollview-scrollbar-horiz .yui3-scrollview-middle{top:0;left:4px;width:1px;height:6px}.yui3-scrollview-scrollbar-vert-basic{height:auto}.yui3-scrollview-scrollbar-vert-basic .yui3-scrollview-child{position:static;_overflow:hidden;_line-height:4px}.yui3-scrollview-scrollbar-horiz-basic{width:auto;white-space:nowrap;line-height:6px;_overflow:hidden}.yui3-scrollview-scrollbar-horiz-basic .yui3-scrollview-child{position:static;padding:0;margin:0;top:auto;left:auto;right:auto;bottom:auto}.yui3-skin-night .yui3-scrollview-scrollbar{-webkit-transform:translate3d(0,0,0);-moz-transform:translate(0,0)}.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-first,.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-middle,.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-last{border-radius:3px;-webkit-border-radius:3px;-moz-border-radius:3px;background-color:#808080;opacity:.3;filter:alpha(opacity=30)}.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-first,.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-last{border-bottom-right-radius:0;border-bottom-left-radius:0;-webkit-border-bottom-right-radius:0;-webkit-border-bottom-left-radius:0;-moz-border-radius-bottomright:0;-moz-border-radius-bottomleft:0}.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-last{border-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;-webkit-border-radius:0;-webkit-border-bottom-right-radius:3px;-webkit-border-bottom-left-radius:3px;-webkit-transform:translate3d(0,0,0);-moz-border-radius:0;-moz-border-radius-bottomright:3px;-moz-border-radius-bottomleft:3px;-moz-transform:translate(0,0)}.yui3-skin-night .yui3-scrollview-scrollbar .yui3-scrollview-middle{border-radius:0;-webkit-border-radius:0;-moz-border-radius:0;-webkit-transform:translate3d(0,0,0) scaleY(1);-webkit-transform-origin:0 0;-moz-transform:translate(0,0) scaleY(1);-moz-transform-origin:0 0}.yui3-skin-night .yui3-scrollview-scrollbar-horiz .yui3-scrollview-first,.yui3-skin-night .yui3-scrollview-scrollbar-horiz .yui3-scrollview-last{border-top-right-radius:0;border-bottom-left-radius:3px;-webkit-border-top-right-radius:0;-webkit-border-bottom-left-radius:3px;-moz-border-radius-topright:0;-moz-border-radius-bottomleft:3px}.yui3-skin-night .yui3-scrollview-scrollbar-horiz .yui3-scrollview-last{border-bottom-left-radius:0;border-top-right-radius:3px;-webkit-border-bottom-left-radius:0;-webkit-border-top-right-radius:3px;-moz-border-radius-bottomleft:0;-moz-border-radius-topright:3px}.yui3-skin-night .yui3-scrollview-scrollbar-horiz .yui3-scrollview-middle{-webkit-transform:translate3d(0,0,0) scaleX(1);-webkit-transform-origin:0 0;-moz-transform:translate(0,0) scaleX(1);-moz-transform-origin:0 0}.yui3-skin-night .yui3-scrollview-scrollbar-vert-basic .yui3-scrollview-child,.yui3-skin-night .yui3-scrollview-scrollbar-horiz-basic .yui3-scrollview-child{background-color:#aaa;background-image:none}#yui3-css-stamp.skin-night-scrollview-scrollbars{display:none}
diff --git a/js/yui3/scrollview-scrollbars/assets/skins/sam/scrollview-scrollbars.css b/js/yui3/scrollview-scrollbars/assets/skins/sam/scrollview-scrollbars.css
new file mode 100644
index 000000000..63c18b87e
--- /dev/null
+++ b/js/yui3/scrollview-scrollbars/assets/skins/sam/scrollview-scrollbars.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-scrollview-scrollbar{opacity:1;position:absolute;width:6px;height:10px}.yui3-scrollview-scrollbar{top:0;right:1px}.yui3-scrollview-scrollbar-horiz{top:auto;height:8px;width:20px;bottom:1px;left:0}.yui3-scrollview-scrollbar .yui3-scrollview-child{position:absolute;right:0;display:block;width:100%;height:4px}.yui3-scrollview-scrollbar .yui3-scrollview-first{top:0}.yui3-scrollview-scrollbar .yui3-scrollview-last{top:0}.yui3-scrollview-scrollbar .yui3-scrollview-middle{position:absolute;top:4px;height:1px}.yui3-scrollview-scrollbar-horiz .yui3-scrollview-child{display:-moz-inline-stack;display:inline-block;zoom:1;*display:inline;top:0;left:0;bottom:auto;right:auto}.yui3-scrollview-scrollbar-horiz .yui3-scrollview-first,.yui3-scrollview-scrollbar-horiz .yui3-scrollview-last{width:4px;height:6px}.yui3-scrollview-scrollbar-horiz .yui3-scrollview-middle{top:0;left:4px;width:1px;height:6px}.yui3-scrollview-scrollbar-vert-basic{height:auto}.yui3-scrollview-scrollbar-vert-basic .yui3-scrollview-child{position:static;_overflow:hidden;_line-height:4px}.yui3-scrollview-scrollbar-horiz-basic{width:auto;white-space:nowrap;line-height:6px;_overflow:hidden}.yui3-scrollview-scrollbar-horiz-basic .yui3-scrollview-child{position:static;padding:0;margin:0;top:auto;left:auto;right:auto;bottom:auto}.yui3-skin-sam .yui3-scrollview-scrollbar{-webkit-transform:translate3d(0,0,0);-moz-transform:translate(0,0)}.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-first,.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-middle,.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-last{border-radius:3px;-webkit-border-radius:3px;-moz-border-radius:3px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAABCAYAAAD9yd/wAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABJJREFUeNpiZGBgSGPAAgACDAAIkABoFyloZQAAAABJRU5ErkJggg==)}.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-first,.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-last{border-bottom-right-radius:0;border-bottom-left-radius:0;-webkit-border-bottom-right-radius:0;-webkit-border-bottom-left-radius:0;-moz-border-radius-bottomright:0;-moz-border-radius-bottomleft:0}.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-last{border-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;-webkit-border-radius:0;-webkit-border-bottom-right-radius:3px;-webkit-border-bottom-left-radius:3px;-webkit-transform:translate3d(0,0,0);-moz-border-radius:0;-moz-border-radius-bottomright:3px;-moz-border-radius-bottomleft:3px;-moz-transform:translate(0,0)}.yui3-skin-sam .yui3-scrollview-scrollbar .yui3-scrollview-middle{border-radius:0;-webkit-border-radius:0;-moz-border-radius:0;-webkit-transform:translate3d(0,0,0) scaleY(1);-webkit-transform-origin-y:0;-moz-transform:translate(0,0) scaleY(1);-moz-transform-origin:0 0}.yui3-skin-sam .yui3-scrollview-scrollbar-horiz .yui3-scrollview-first,.yui3-skin-sam .yui3-scrollview-scrollbar-horiz .yui3-scrollview-last{border-top-right-radius:0;border-bottom-left-radius:3px;-webkit-border-top-right-radius:0;-webkit-border-bottom-left-radius:3px;-moz-border-radius-topright:0;-moz-border-radius-bottomleft:3px}.yui3-skin-sam .yui3-scrollview-scrollbar-horiz .yui3-scrollview-last{border-bottom-left-radius:0;border-top-right-radius:3px;-webkit-border-bottom-left-radius:0;-webkit-border-top-right-radius:3px;-moz-border-radius-bottomleft:0;-moz-border-radius-topright:3px}.yui3-skin-sam .yui3-scrollview-scrollbar-horiz .yui3-scrollview-middle{-webkit-transform:translate3d(0,0,0) scaleX(1);-webkit-transform-origin:0 0;-moz-transform:translate(0,0) scaleX(1);-moz-transform-origin:0 0}.yui3-skin-sam .yui3-scrollview-scrollbar-vert-basic .yui3-scrollview-child,.yui3-skin-sam .yui3-scrollview-scrollbar-horiz-basic .yui3-scrollview-child{background-color:#aaa;background-image:none}#yui3-css-stamp.skin-sam-scrollview-scrollbars{display:none}
diff --git a/js/yui3/scrollview-scrollbars/scrollview-scrollbars-min.js b/js/yui3/scrollview-scrollbars/scrollview-scrollbars-min.js
new file mode 100644
index 000000000..5d1586db4
--- /dev/null
+++ b/js/yui3/scrollview-scrollbars/scrollview-scrollbars-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("scrollview-scrollbars",function(e,t){function L(){L.superclass.constructor.apply(this,arguments)}var n=e.ClassNameManager.getClassName,r,i=e.Transition,s=i.useNative,o="scrollbar",u="scrollview",a="verticalNode",f="horizontalNode",l="childCache",c="top",h="left",p="width",d="height",v="_sbh",m="_sbv",g=e.ScrollView._TRANSITION.PROPERTY,y="transform",b="translateX(",w="translateY(",E="scaleX(",S="scaleY(",x="scrollX",T="scrollY",N="px",C=")",k=N+C;L.CLASS_NAMES={showing:n(u,o,"showing"),scrollbar:n(u,o),scrollbarV:n(u,o,"vert"),scrollbarH:n(u,o,"horiz"),scrollbarVB:n(u,o,"vert","basic"),scrollbarHB:n(u,o,"horiz","basic"),child:n(u,"child"),first:n(u,"first"),middle:n(u,"middle"),last:n(u,"last")},r=L.CLASS_NAMES,L.NAME="pluginScrollViewScrollbars",L.NS="scrollbars",L.SCROLLBAR_TEMPLATE=["<div>",'<span class="'+r.child+" "+r.first+'"></span>','<span class="'+r.child+" "+r.middle+'"></span>','<span class="'+r.child+" "+r.last+'"></span>',"</div>"].join(""),L.ATTRS={verticalNode:{setter:"_setNode",valueFn:"_defaultNode"},horizontalNode:{setter:"_setNode",valueFn:"_defaultNode"}},e.namespace("Plugin").ScrollViewScrollbars=e.extend(L,e.Plugin.Base,{initializer:function(){this._host=this.get("host"),this.afterHostEvent("scrollEnd",this._hostScrollEnd),this.afterHostMethod("scrollTo",this._update),this.afterHostMethod("_uiDimensionsChange",this._hostDimensionsChange)},_hostDimensionsChange:function(){var t=this._host,n=t._cAxis,r=t.get(x),i=t.get(T);this._dims=t._getScrollDims(),n&&n.y&&this._renderBar(this.get(a),!0,"vert"),n&&n.x&&this._renderBar(this.get(f),!0,"horiz"),this._update(r,i),e.later(500,this,"flash",!0)},_hostScrollEnd:function(){var e=this._host,t=e.get(x),n=e.get(T);this.flash(),this._update(t,n)},_renderBar:function(e,t){var n=e.inDoc(),i=this._host._bb,s=e.getData("isHoriz")?r.scrollbarHB:r.scrollbarVB;t&&!n?(i.append(e),e.toggleClass(s,this._basic),this._setChildCache(e)):!t&&n&&(e.remove(),this._clearChildCache(e))},_setChildCache:function(e){var t=e.get("children"),n=t.item(0),r=t.item(1),i=t.item(2),s=e.getData("isHoriz")?"offsetWidth":"offsetHeight";e.setStyle(g,y),r.setStyle(g,y),i.setStyle(g,y),e.setData(l,{fc:n,lc:i,mc:r,fcSize:n&&n.get(s),lcSize:i&&i.get(s)})},_clearChildCache:function(e){e.clearData(l)},_updateBar:function(e,t,n,r){var i=this._host,o=this._basic,u=0,a=1,f=e.getData(l),g=f.lc,L=f.mc,A=f.fcSize,O=f.lcSize,M,_,D,P,H,B,j,F,I,q;r?(B=p,j=h,F=v,I=this._dims.offsetWidth,q=this._dims.scrollWidth,P=b,H=E,t=t!==undefined?t:i.get(x)):(B=d,j=c,F=m,I=this._dims.offsetHeight,q=this._dims.scrollHeight,P=w,H=S,t=t!==undefined?t:i.get(T)),u=Math.floor(I*(I/q)),a=Math.floor(t/(q-I)*(I-u)),u>I&&(u=1),a>I-u?u-=a-(I-u):a<0?(u=a+u,a=0):isNaN(a)&&(a=0),M=u-(A+O),M<0&&(M=0),M===0&&a!==0&&(a=I-(A+O)-1),n!==0?(D={duration:n},s?D.transform=P+a+k:D[j]=a+N,e.transition(D)):s?e.setStyle(y,P+a+k):e.setStyle(j,a+N);if(this[F]!==M){this[F]=M;if(M>0){n!==0?(D={duration:n},s?D.transform=H+M+C:D[B]=M+N,L.transition(D)):s?L.setStyle(y,H+M+C):L.setStyle(B,M+N);if(!r||!o)_=u-O,n!==0?(D={duration:n},s?D.transform=P+_+k:D[j]=_,g.transition(D)):s?g.setStyle(y,P+_+k):g.setStyle(j,_+N)}}},_update:function(e,t,n){var r=this.get(a),i=this.get(f),s=this._host,o=s._cAxis;n=(n||0)/1e3,this._showing||this.show(),o&&o.y&&r&&t!==null&&this._updateBar(r,t,n,!1),o&&o.x&&i&&e!==null&&this._updateBar(i,e,n,!0)},show:function(e){this._show(!0,e)},hide:function(e){this._show(!1,e)},_show:function(e,t){var n=this.get(a),r=this.get(f),i=t?.6:0,s=e?1:0,o;this._showing=e,this._flashTimer&&this._flashTimer.cancel(),o={duration:i,opacity:s},n&&n._node&&n.transition(o),r&&r._node&&r.transition(o)},flash:function(){this.show(!0),this._flashTimer=e.later(800,this,"hide",!0)},_setNode:function(t,n){var i=n===f;return t=e.one(t),t&&(t.addClass(r.scrollbar),t.addClass(i?r.scrollbarH:r.scrollbarV),t.setData("isHoriz",i)),t},_defaultNode:function(){return e.Node.create(L.SCROLLBAR_TEMPLATE)},_basic:e.UA.ie&&e.UA.ie<=8})},"3.17.2",{requires:["classnamemanager","transition","plugin"],skinnable:!0});
diff --git a/js/yui3/scrollview/scrollview-min.js b/js/yui3/scrollview/scrollview-min.js
new file mode 100644
index 000000000..0e180fb45
--- /dev/null
+++ b/js/yui3/scrollview/scrollview-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("scrollview",function(e,t){e.Base.plug(e.ScrollView,e.Plugin.ScrollViewScrollbars)},"3.17.2",{requires:["scrollview-base","scrollview-scrollbars"]});
diff --git a/js/yui3/selector-css2/selector-css2-min.js b/js/yui3/selector-css2/selector-css2-min.js
new file mode 100644
index 000000000..7f7e4c9ec
--- /dev/null
+++ b/js/yui3/selector-css2/selector-css2-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("selector-css2",function(e,t){var n="parentNode",r="tagName",i="attributes",s="combinator",o="pseudos",u=e.Selector,a={_reRegExpTokens:/([\^\$\?\[\]\*\+\-\.\(\)\|\\])/,SORT_RESULTS:!0,_isXML:function(){var t=e.config.doc.createElement("div").tagName!=="DIV";return t}(),shorthand:{"\\#(-?[_a-z0-9]+[-\\w\\uE000]*)":"[id=$1]","\\.(-?[_a-z]+[-\\w\\uE000]*)":"[className~=$1]"},operators:{"":function(t,n){return e.DOM.getAttribute(t,n)!==""},"~=":"(?:^|\\s+){val}(?:\\s+|$)","|=":"^{val}-?"},pseudos:{"first-child":function(t){return e.DOM._children(t[n])[0]===t}},_bruteQuery:function(t,n,r){var i=[],s=[],o,a=u._tokenize(t),f=a[a.length-1],l=e.DOM._getDoc(n),c,h,p,d,v;if(f){h=f.id,p=f.className,d=f.tagName||"*";if(n.getElementsByTagName)h&&(n.all||n.nodeType===9||e.DOM.inDoc(n))?s=e.DOM.allById(h,n):p?s=n.getElementsByClassName(p):s=n.getElementsByTagName(d);else{o=[],c=n.firstChild,v=d==="*";while(c){while(c)c.tagName>"@"&&(v||c.tagName===d)&&s.push(c),o.push(c),c=c.firstChild;while(o.length>0&&!c)c=o.pop().nextSibling}}s.length&&(i=u._filterNodes(s,a,r))}return i},_filterNodes:function(t,n,r){var i=0,s,o=n.length,a=o-1,f=[],l=t[0],c=l,h=e.Selector.getters,p,d,v,m,g,y,b,w;for(i=0;c=l=t[i++];){a=o-1,m=null;e:while(c&&c.tagName){v=n[a],b=v.tests,s=b.length;if(s&&!g)while(w=b[--s]){p=w[1],h[w[0]]?y=h[w[0]](c,w[0]):(y=c[w[0]],w[0]==="tagName"&&!u._isXML&&(y=y.toUpperCase()),typeof y!="string"&&y!==undefined&&y.toString?y=y.toString():y===undefined&&c.getAttribute&&(y=c.getAttribute(w[0],2)));if(p==="="&&y!==w[2]||typeof p!="string"&&p.test&&!p.test(y)||!p.test&&typeof p=="function"&&!p(c,w[0],w[2])){if(c=c[m])while(c&&(!c.tagName||v.tagName&&v.tagName!==c.tagName))c=c[m];continue e}}a--;if(!!g||!(d=v.combinator)){f.push(l);if(r)return f;break}m=d.axis,c=c[m];while(c&&!c.tagName)c=c[m];d.direct&&(m=null)}}return l=c=null,f},combinators:{" ":{axis:"parentNode"},">":{axis:"parentNode",direct:!0},"+":{axis:"previousSibling",direct:!0}},_parsers:[{name:i,re:/^\uE003(-?[a-z]+[\w\-]*)+([~\|\^\$\*!=]=?)?['"]?([^\uE004'"]*)['"]?\uE004/i,fn:function(t,n){var r=t[2]||"",i=u.operators,s=t[3]?t[3].replace(/\\/g,""):"",o;if(t[1]==="id"&&r==="="||t[1]==="className"&&e.config.doc.documentElement.getElementsByClassName&&(r==="~="||r==="="))n.prefilter=t[1],t[3]=s,n[t[1]]=t[1]==="id"?t[3]:s;r in i&&(o=i[r],typeof o=="string"&&(t[3]=s.replace(u._reRegExpTokens,"\\$1"),o=new RegExp(o.replace("{val}",t[3]))),t[2]=o);if(!n.last||n.prefilter!==t[1])return t.slice(1)}},{name:r,re:/^((?:-?[_a-z]+[\w-]*)|\*)/i,fn:function(e,t){var n=e[1];u._isXML||(n=n.toUpperCase()),t.tagName=n;if(n!=="*"&&(!t.last||t.prefilter))return[r,"=",n];t.prefilter||(t.prefilter="tagName")}},{name:s,re:/^\s*([>+~]|\s)\s*/,fn:function(e,t){}},{name:o,re:/^:([\-\w]+)(?:\uE005['"]?([^\uE005]*)['"]?\uE006)*/i,fn:function(e,t){var n=u[o][e[1]];return n?(e[2]&&(e[2]=e[2].replace(/\\/g,"")),[e[2],n]):!1}}],_getToken:function(e){return{tagName:null,id:null,className:null,attributes:{},combinator:null,tests:[]}},_tokenize:function(t){t=t||"",t=u._parseSelector(e.Lang.trim(t));var n=u._getToken(),r=t,i=[],o=!1,a,f,l,c;e:do{o=!1;for(l=0;c=u._parsers[l++];)if(a=c.re.exec(t)){c.name!==s&&(n.selector=t),t=t.replace(a[0],""),t.length||(n.last=!0),u._attrFilters[a[1]]&&(a[1]=u._attrFilters[a[1]]),f=c.fn(a,n);if(f===!1){o=!1;break e}f&&n.tests.push(f);if(!t.length||c.name===s)i.push(n),n=u._getToken(n),c.name===s&&(n.combinator=e.Selector.combinators[a[1]]);o=!0}}while(o&&t.length);if(!o||t.length)i=[];return i},_replaceMarkers:function(e){return e=e.replace(/\[/g,"\ue003"),e=e.replace(/\]/g,"\ue004"),e=e.replace(/\(/g,"\ue005"),e=e.replace(/\)/g,"\ue006"),e},_replaceShorthand:function(t){var n=e.Selector.shorthand,r;for(r in n)n.hasOwnProperty(r)&&(t=t.replace(new RegExp(r,"gi"),n[r]));return t},_parseSelector:function(t){var n=e.Selector._replaceSelector(t),t=n.selector;return t=e.Selector._replaceShorthand(t),t=e.Selector._restore("attr",t,n.attrs),t=e.Selector._restore("pseudo",t,n.pseudos),t=e.Selector._replaceMarkers(t),t=e.Selector._restore("esc",t,n.esc),t},_attrFilters:{"class":"className","for":"htmlFor"},getters:{href:function(t,n){return e.DOM.getAttribute(t,n)},id:function(t,n){return e.DOM.getId(t)}}};e.mix(e.Selector,a,!0),e.Selector.getters.src=e.Selector.getters.rel=e.Selector.getters.href,e.Selector.useNative&&e.config.doc.querySelector&&(e.Selector.shorthand["\\.(-?[_a-z]+[-\\w]*)"]="[class~=$1]")},"3.17.2",{requires:["selector-native"]});
diff --git a/js/yui3/selector-css3/selector-css3-min.js b/js/yui3/selector-css3/selector-css3-min.js
new file mode 100644
index 000000000..07a655db2
--- /dev/null
+++ b/js/yui3/selector-css3/selector-css3-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("selector-css3",function(e,t){e.Selector._reNth=/^(?:([\-]?\d*)(n){1}|(odd|even)$)*([\-+]?\d*)$/,e.Selector._getNth=function(t,n,r,i){e.Selector._reNth.test(n);var s=parseInt(RegExp.$1,10),o=RegExp.$2,u=RegExp.$3,a=parseInt(RegExp.$4,10)||0,f=[],l=e.DOM._children(t.parentNode,r),c;u?(s=2,c="+",o="n",a=u==="odd"?1:0):isNaN(s)&&(s=o?1:0);if(s===0)return i&&(a=l.length-a+1),l[a-1]===t?!0:!1;s<0&&(i=!!i,s=Math.abs(s));if(!i){for(var h=a-1,p=l.length;h<p;h+=s)if(h>=0&&l[h]===t)return!0}else for(var h=l.length-a,p=l.length;h>=0;h-=s)if(h<p&&l[h]===t)return!0;return!1},e.mix(e.Selector.pseudos,{root:function(e){return e===e.ownerDocument.documentElement},"nth-child":function(t,n){return e.Selector._getNth(t,n)},"nth-last-child":function(t,n){return e.Selector._getNth(t,n,null,!0)},"nth-of-type":function(t,n){return e.Selector._getNth(t,n,t.tagName)},"nth-last-of-type":function(t,n){return e.Selector._getNth(t,n,t.tagName,!0)},"last-child":function(t){var n=e.DOM._children(t.parentNode);return n[n.length-1]===t},"first-of-type":function(t){return e.DOM._children(t.parentNode,t.tagName)[0]===t},"last-of-type":function(t){var n=e.DOM._children(t.parentNode,t.tagName);return n[n.length-1]===t},"only-child":function(t){var n=e.DOM._children(t.parentNode);return n.length===1&&n[0]===t},"only-of-type":function(t){var n=e.DOM._children(t.parentNode,t.tagName);return n.length===1&&n[0]===t},empty:function(e){return e.childNodes.length===0},not:function(t,n){return!e.Selector.test(t,n)},contains:function(e,t){var n=e.innerText||e.textContent||"";return n.indexOf(t)>-1},checked:function(e){return e.checked===!0||e.selected===!0},enabled:function(e){return e.disabled!==undefined&&!e.disabled},disabled:function(e){return e.disabled}}),e.mix(e.Selector.operators,{"^=":"^{val}","$=":"{val}$","*=":"{val}"}),e.Selector.combinators["~"]={axis:"previousSibling"}},"3.17.2",{requires:["selector-native","selector-css2"]});
diff --git a/js/yui3/selector-native/selector-native-min.js b/js/yui3/selector-native/selector-native-min.js
new file mode 100644
index 000000000..ce6873649
--- /dev/null
+++ b/js/yui3/selector-native/selector-native-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("selector-native",function(e,t){(function(e){e.namespace("Selector");var t="compareDocumentPosition",n="ownerDocument",r={_types:{esc:{token:"\ue000",re:/\\[:\[\]\(\)#\.\'\>+~"]/gi},attr:{token:"\ue001",re:/(\[[^\]]*\])/g},pseudo:{token:"\ue002",re:/(\([^\)]*\))/g}},useNative:!0,_escapeId:function(e){return e&&(e=e.replace(/([:\[\]\(\)#\.'<>+~"])/g,"\\$1")),e},_compare:"sourceIndex"in e.config.doc.documentElement?function(e,t){var n=e.sourceIndex,r=t.sourceIndex;return n===r?0:n>r?1:-1}:e.config.doc.documentElement[t]?function(e,n){return e[t](n)&4?-1:1}:function(e,t){var r,i,s;return e&&t&&(r=e[n].createRange(),r.setStart(e,0),i=t[n].createRange(),i.setStart(t,0),s=r.compareBoundaryPoints(1,i)),s},_sort:function(t){return t&&(t=e.Array(t,0,!0),t.sort&&t.sort(r._compare)),t},_deDupe:function(e){var t=[],n,r;for(n=0;r=e[n++];)r._found||(t[t.length]=r,r._found=!0);for(n=0;r=t[n++];)r._found=null,r.removeAttribute("_found");return t},query:function(t,n,i,s){n=n||e.config.doc;var o=[],u=e.Selector.useNative&&e.config.doc.querySelector&&!s,a=[[t,n]],f,l,c,h=u?e.Selector._nativeQuery:e.Selector._bruteQuery;if(t&&h){!s&&(!u||n.tagName)&&(a=r._splitQueries(t,n));for(c=0;f=a[c++];)l=h(f[0],f[1],i),i||(l=e.Array(l,0,!0)),l&&(o=o.concat(l));a.length>1&&(o=r._sort(r._deDupe(o)))}return i?o[0]||null:o},_replaceSelector:function(t){var n=e.Selector._parse("esc",t),i,s;return t=e.Selector._replace("esc",t),s=e.Selector._parse("pseudo",t),t=r._replace("pseudo",t),i=e.Selector._parse("attr",t),t=e.Selector._replace("attr",t),{esc:n,attrs:i,pseudos:s,selector:t}},_restoreSelector:function(t){var n=t.selector;return n=e.Selector._restore("attr",n,t.attrs),n=e.Selector._restore("pseudo",n,t.pseudos),n=e.Selector._restore("esc",n,t.esc),n},_replaceCommas:function(t){var n=e.Selector._replaceSelector(t),t=n.selector;return t&&(t=t.replace(/,/g,"\ue007"),n.selector=t,t=e.Selector._restoreSelector(n)),t},_splitQueries:function(t,n){t.indexOf(",")>-1&&(t=e.Selector._replaceCommas(t));var r=t.split("\ue007"),i=[],s="",o,u,a;if(n){n.nodeType===1&&(o=e.Selector._escapeId(e.DOM.getId(n)),o||(o=e.guid(),e.DOM.setId(n,o)),s='[id="'+o+'"] ');for(u=0,a=r.length;u<a;++u)t=s+r[u],i.push([t,n])}return i},_nativeQuery:function(t,n,r){if((e.UA.webkit||e.UA.opera)&&t.indexOf(":checked")>-1&&e.Selector.pseudos&&e.Selector.pseudos.checked)return e.Selector.query(t,n,r,!0);try{return n["querySelector"+(r?"":"All")](t)}catch(i){return e.Selector.query(t,n,r,!0)}},filter:function(t,n){var r=[],i,s;if(t&&n)for(i=0;s=t[i++];)e.Selector.test(s,n)&&(r[r.length]=s);return r},test:function(t,r,i){var s=!1,o=!1,u,a,f,l,c,h,p,d,v;if(t&&t.tagName)if(typeof r=="function")s=r.call(t,t);else{u=r.split(","),!i&&!e.DOM.inDoc(t)&&(a=t.parentNode,a?i=a:(c=t[n].createDocumentFragment(),c.appendChild(t),i=c,o=!0)),i=i||t[n],h=e.Selector._escapeId(e.DOM.getId(t)),h||(h=e.guid(),e.DOM.setId(t,h));for(p=0;v=u[p++];){v+='[id="'+h+'"]',l=e.Selector.query(v,i);for(d=0;f=l[d++];)if(f===t){s=!0;break}if(s)break}o&&c.removeChild(t)}return s},ancestor:function(t,n,r){return e.DOM.ancestor(t,function(t){return e.Selector.test(t,n)},r)},_parse:function(t,n){return n.match(e.Selector._types[t].re)},_replace:function(t,n){var r=e.Selector._types[t];return n.replace(r.re,r.token)},_restore:function(t,n,r){if(r){var i=e.Selector._types[t].token,s,o;for(s=0,o=r.length;s<o;++s)n=n.replace(i,r[s])}return n}};e.mix(e.Selector,r,!0)})(e)},"3.17.2",{requires:["dom-base"]});
diff --git a/js/yui3/selector/selector-min.js b/js/yui3/selector/selector-min.js
new file mode 100644
index 000000000..e70e84ed9
--- /dev/null
+++ b/js/yui3/selector/selector-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("selector",function(e,t){},"3.17.2",{requires:["selector-native"]});
diff --git a/js/yui3/series-area-stacked/series-area-stacked-min.js b/js/yui3/series-area-stacked/series-area-stacked-min.js
new file mode 100644
index 000000000..58045565e
--- /dev/null
+++ b/js/yui3/series-area-stacked/series-area-stacked-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-area-stacked",function(e,t){e.StackedAreaSeries=e.Base.create("stackedAreaSeries",e.AreaSeries,[e.StackingUtil],{setAreaData:function(){e.StackedAreaSeries.superclass.setAreaData.apply(this),this._stackCoordinates.apply(this)},drawSeries:function(){this.drawFill.apply(this,this._getStackedClosingPoints())}},{ATTRS:{type:{value:"stackedArea"}}})},"3.17.2",{requires:["series-stacked","series-area"]});
diff --git a/js/yui3/series-area/series-area-min.js b/js/yui3/series-area/series-area-min.js
new file mode 100644
index 000000000..f1a3d22a8
--- /dev/null
+++ b/js/yui3/series-area/series-area-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-area",function(e,t){e.AreaSeries=e.Base.create("areaSeries",e.CartesianSeries,[e.Fills],{drawSeries:function(){this.drawFill.apply(this,this._getClosingPoints())},_setStyles:function(t){return t.area||(t={area:t}),e.AreaSeries.superclass._setStyles.apply(this,[t])},_getDefaultStyles:function(){var t=this._mergeStyles({area:this._getAreaDefaults()},e.AreaSeries.superclass._getDefaultStyles());return t}},{ATTRS:{type:{value:"area"}}})},"3.17.2",{requires:["series-cartesian","series-fill-util"]});
diff --git a/js/yui3/series-areaspline-stacked/series-areaspline-stacked-min.js b/js/yui3/series-areaspline-stacked/series-areaspline-stacked-min.js
new file mode 100644
index 000000000..76892fb57
--- /dev/null
+++ b/js/yui3/series-areaspline-stacked/series-areaspline-stacked-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-areaspline-stacked",function(e,t){e.StackedAreaSplineSeries=e.Base.create("stackedAreaSplineSeries",e.AreaSeries,[e.CurveUtil,e.StackingUtil],{drawSeries:function(){this._stackCoordinates(),this.drawStackedAreaSpline()}},{ATTRS:{type:{value:"stackedAreaSpline"}}})},"3.17.2",{requires:["series-stacked","series-areaspline"]});
diff --git a/js/yui3/series-areaspline/series-areaspline-min.js b/js/yui3/series-areaspline/series-areaspline-min.js
new file mode 100644
index 000000000..f252a9212
--- /dev/null
+++ b/js/yui3/series-areaspline/series-areaspline-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-areaspline",function(e,t){e.AreaSplineSeries=e.Base.create("areaSplineSeries",e.AreaSeries,[e.CurveUtil],{drawSeries:function(){this.drawAreaSpline()}},{ATTRS:{type:{value:"areaSpline"}}})},"3.17.2",{requires:["series-area","series-curve-util"]});
diff --git a/js/yui3/series-bar-stacked/series-bar-stacked-min.js b/js/yui3/series-bar-stacked/series-bar-stacked-min.js
new file mode 100644
index 000000000..7d3f4f8c9
--- /dev/null
+++ b/js/yui3/series-bar-stacked/series-bar-stacked-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-bar-stacked",function(e,t){var n=e.Lang;e.StackedBarSeries=e.Base.create("stackedBarSeries",e.BarSeries,[e.StackingUtil],{drawSeries:function(){if(this.get("xcoords").length<1)return;var e=n.isNumber,t=this._copyObject(this.get("styles").marker),r=t.width,i=t.height,s=this.get("xcoords"),o=this.get("ycoords"),u=0,a=s.length,f=o[0],l=this.get("seriesTypeCollection"),c,h=this.get("order"),p=this.get("graphOrder"),d,v,m,g,y,b,w,E=h===0,S=a*i,x={width:[],height:[]},T=[],N=[],C=this.get("groupMarkers");n.isArray(t.fill.color)&&(b=t.fill.color.concat()),n.isArray(t.border.color)&&(w=t.border.color.concat()),this._createMarkerCache(),S>this.get("height")&&(c=this.get("height")/S,i*=c,i=Math.max(i,1));if(!E){m=l[h-1],g=m.get("negativeBaseValues"),y=m.get("positiveBaseValues");if(!g||!y)E=!0,y=[],g=[]}else g=[],y=[];this.set("negativeBaseValues",g),this.set("positiveBaseValues",y);for(u=0;u<a;++u){f=o[u],d=s[u];if(!e(f)||!e(d)){E&&(y[u]=this._leftOrigin,g[u]=this._leftOrigin),this._markers.push(null);continue}E?(r=Math.abs(d-this._leftOrigin),d>this._leftOrigin?(y[u]=d,g[u]=this._leftOrigin,d-=r):d<this._leftOrigin?(y[u]=this._leftOrigin,g[u]=d):(y[u]=d,g[u]=this._leftOrigin)):d<this._leftOrigin?(d=g[u]-(this._leftOrigin-s[u]),r=g[u]-d,g[u]=d):d>=this._leftOrigin&&(d+=y[u]-this._leftOrigin,r=d-y[u],y[u]=d,d-=r),!isNaN(r)&&r>0?(f-=i/2,C?(x.width[u]=r,x.height[u]=i,T.push(d),N.push(f)):(t.width=r,t.height=i,t.x=d,t.y=f,b&&(t.fill.color=b[u%b.length]),w&&(t.border.color=w[u%w.length]),v=this.getMarker(t,p,u))):C||this._markers.push(null)}C?this._createGroupMarker({fill:t.fill,border:t.border,dimensions:x,xvalues:T,yvalues:N,shape:t.shape}):this._clearMarkerCache()},updateMarkerState:function(e,t){if(this._markers[t]){var r=this._getState(e),i=this.get("ycoords"),s=this._markers[t],o=this.get("styles").marker,u=o.height,a=r==="off"||!o[r]?this._copyObject(o):this._copyObject(o[r]),f,l;a.y=i[t]-u/2,a.x=s.get("x"),a.width=s.get("width"),a.id=s.get("id"),f=a.fill.color,l=a.border.color,n.isArray(f)?a.fill.color=f[t%f.length]:a.fill.color=this._getItemColor(a.fill.color,t),n.isArray(l)?a.border.color=l[t%l.length]:a.border.color=this._getItemColor(a.border.color,t),s.set(a)}},_getPlotDefaults:function(){var e={fill:{type:"solid",alpha:1,colors:null,alphas:null,ratios:null},border:{weight:0,alpha:1},width:24,height:24,shape:"rect",padding:{top:0,left:0,right:0,bottom:0}};return e.fill.color=this._getDefaultColor(this.get("graphOrder"),"fill"),e.border.color=this._getDefaultColor(this.get("graphOrder"),"border"),e}},{ATTRS:{type:{value:"stackedBar"},direction:{value:"vertical"},negativeBaseValues:{value:null},positiveBaseValues:{value:null}}})},"3.17.2",{requires:["series-stacked","series-bar"]});
diff --git a/js/yui3/series-bar/series-bar-min.js b/js/yui3/series-bar/series-bar-min.js
new file mode 100644
index 000000000..6784aab10
--- /dev/null
+++ b/js/yui3/series-bar/series-bar-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-bar",function(e,t){e.BarSeries=e.Base.create("barSeries",e.MarkerSeries,[e.Histogram],{_getMarkerDimensions:function(e,t,n,r){var i={top:t+r};return e>=this._leftOrigin?(i.left=this._leftOrigin,i.calculatedSize=e-i.left):(i.left=e,i.calculatedSize=this._leftOrigin-e),i},updateMarkerState:function(e,t){if(this._markers&&this._markers[t]){var n=this._copyObject(this.get("styles").marker),r,i=this._getState(e),s=this.get("xcoords"),o=this.get("ycoords"),u=this._markers[t],a,f=this.get("seriesTypeCollection"),l=f?f.length:0,c,h=0,p=0,d,v=0,m=[],g=this.get("order"),y;r=i==="off"||!n[i]?n:n[i],r.fill.color=this._getItemColor(r.fill.color,t),r.border.color=this._getItemColor(r.border.color,t),y=this._getMarkerDimensions(s[t],o[t],n.height,p),r.width=y.calculatedSize,r.height=Math.min(this._maxSize,r.height),u.set(r);for(;v<l;++v)m[v]=o[t]+h,c=f[v].get("styles").marker,h+=Math.min(this._maxSize,c.height),g>v&&(p=h),p-=h/2;for(v=0;v<l;++v)a=f[v].get("markers"),a&&(d=a[t],d&&d!==undefined&&d.set("y",m[v]-h/2))}}},{ATTRS:{type:{value:"bar"},direction:{value:"vertical"}}})},"3.17.2",{requires:["series-marker","series-histogram-base"]});
diff --git a/js/yui3/series-base/series-base-min.js b/js/yui3/series-base/series-base-min.js
new file mode 100644
index 000000000..9de27128a
--- /dev/null
+++ b/js/yui3/series-base/series-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-base",function(e,t){e.SeriesBase=e.Base.create("seriesBase",e.Base,[e.Renderer],{render:function(){this._setCanvas(),this.addListeners(),this.validate()},_setCanvas:function(){var e=this.get("graph"),t=e.get("graphic");this.set("graphic",t)},_getChart:function(){var e,t=this.get("graph");return t&&(e=t.get("chart")),e||(e=this.get("graphic")),e},getTotalValues:function(){var e=this.get("direction")==="vertical"?"x":"y",t=this.get(e+"Axis").getTotalByKey(this.get(e+"Key"));return t},_getDefaultStyles:function(){return{padding:{top:0,left:0,right:0,bottom:0}}},_handleVisibleChange:function(){this._toggleVisible(this.get("visible"))},destructor:function(){var t,n=this.get("markers");this.get("rendered")&&(this._stylesChangeHandle&&this._stylesChangeHandle.detach(),this._widthChangeHandle&&this._widthChangeHandle.detach(),this._heightChangeHandle&&this._heightChangeHandle.detach(),this._visibleChangeHandle&&this._visibleChangeHandle.detach());while(n&&n.length>0)t=n.shift(),t&&t instanceof e.Shape&&t.destroy();this._path&&(this._path.destroy(),this._path=null),this._lineGraphic&&(this._lineGraphic.destroy(),this._lineGraphic=null),this._groupMarker&&(this._groupMarker.destroy(),this._groupMarker=null)},_defaultLineColors:["#426ab3","#d09b2c","#000000","#b82837","#b384b5","#ff7200","#779de3","#cbc8ba","#7ed7a6","#007a6c"],_defaultFillColors:["#6084d0","#eeb647","#6c6b5f","#d6484f","#ce9ed1","#ff9f3b","#93b7ff","#e0ddd0","#94ecba","#309687"],_defaultBorderColors:["#205096","#b38206","#000000","#94001e","#9d6fa0","#e55b00","#5e85c9","#adab9e","#6ac291","#006457"],_defaultSliceColors:["#66007f","#a86f41","#295454","#996ab2","#e8cdb7","#90bdbd","#000000","#c3b8ca","#968373","#678585"],_getDefaultColor:function(e,t){var n={line:this._defaultLineColors,fill:this._defaultFillColors,border:this._defaultBorderColors,slice:this._defaultSliceColors},r=n[t]||n.fill,i=r.length;return e=e||0,e>=i&&(e%=i),t=t||"fill",n[t][e]}},{ATTRS:{width:{readOnly:!0,getter:function(){return this.get("graphic").get("width")}},height:{readOnly:!0,getter:function(){return this.get("graphic").get("height")}},graphic:{lazyAdd:!1,setter:function(e){return this.get("rendered")||this.set("rendered",!0),e}},chart:{getter:function(){var e,t=this.get("graph");return t&&(e=t.get("chart")),e}},graph:{},rendered:{value:!1},visible:{value:!0},groupMarkers:{getter:function(){var e,t=this._groupMarkers;return t||(e=this.get("graph"),e&&(t=e.get("groupMarkers"))),t},setter:function(e){return this._groupMarkers=e,e}}}})},"3.17.2",{requires:["graphics","axis-base"]});
diff --git a/js/yui3/series-candlestick/series-candlestick-min.js b/js/yui3/series-candlestick/series-candlestick-min.js
new file mode 100644
index 000000000..95c8cfdc1
--- /dev/null
+++ b/js/yui3/series-candlestick/series-candlestick-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-candlestick",function(e,t){function n(){n.superclass.constructor.apply(this,arguments)}n.NAME="candlestickSeries",n.ATTRS={type:{value:"candlestick"},graphic:{lazyAdd:!1,setter:function(e){return this.get("rendered")||this.set("rendered",!0),this.set("upcandle",e.addShape({type:"path"})),this.set("downcandle",e.addShape({type:"path"})),this.set("wick",e.addShape({type:"path"})),e}},upcandle:{},downcandle:{},wick:{}},e.extend(n,e.RangeSeries,{_drawMarkers:function(t,n,r,i,s,o,u,a,f){var l=this.get("upcandle"),c=this.get("downcandle"),h,p=this.get("wick"),d=f.wick,v=d.width,m,g,y,b,w,E,S,x,T,N,C=f.padding.left,k,L,A=e.Lang.isNumber;l.set(f.upcandle),c.set(f.downcandle),p.set({fill:d.fill,stroke:d.stroke,shapeRendering:d.shapeRendering}),l.clear(),c.clear(),p.clear();for(L=0;L<o;L+=1)m=Math.round(t[L]+C),E=m-a,S=m+a,g=Math.round(n[L]),y=Math.round(r[L]),b=Math.round(i[L]),w=Math.round(s[L]),k=g>w,x=k?w:g,T=k?g:w,N=T-x,h=k?l:c,h&&A(E)&&A(x)&&A(u)&&A(N)&&h.drawRect(E,x,u,N),A(m)&&A(y)&&A(b)&&p.drawRect(m-v/2,y,v,b-y);l.end(),c.end(),p.end(),p.toBack()},_toggleVisible:function(e){this.get("upcandle").set("visible",e),this.get("downcandle").set("visible",e),this.get("wick").set("visible",e)},destructor:function(){var e=this.get("upcandle"),t=this.get("downcandle"),n=this.get("wick");e&&e.destroy(),t&&t.destroy(),n&&n.destroy()},_getDefaultStyles:function(){var e={upcandle:{shapeRendering:"crispEdges",fill:{color:"#00aa00",alpha:1},stroke:{color:"#000000",alpha:1,weight:0}},downcandle:{shapeRendering:"crispEdges",fill:{color:"#aa0000",alpha:1},stroke:{color:"#000000",alpha:1,weight:0}},wick:{shapeRendering:"crispEdges",width:1,fill:{color:"#000000",alpha:1},stroke:{color:"#000000",alpha:1,weight:0}}};return this._mergeStyles(e,n.superclass._getDefaultStyles())}}),e.CandlestickSeries=n},"3.17.2",{requires:["series-range"]});
diff --git a/js/yui3/series-cartesian/series-cartesian-min.js b/js/yui3/series-cartesian/series-cartesian-min.js
new file mode 100644
index 000000000..7682357ca
--- /dev/null
+++ b/js/yui3/series-cartesian/series-cartesian-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-cartesian",function(e,t){var n=e.Lang;e.CartesianSeries=e.Base.create("cartesianSeries",e.SeriesBase,[],{_xDisplayName:null,_yDisplayName:null,_leftOrigin:null,_bottomOrigin:null,addListeners:function(){var t=this.get("xAxis"),n=this.get("yAxis");t&&(this._xDataReadyHandle=t.after("dataReady",e.bind(this._xDataChangeHandler,this)),this._xDataUpdateHandle=t.after("dataUpdate",e.bind(this._xDataChangeHandler,this))),n&&(this._yDataReadyHandle=n.after("dataReady",e.bind(this._yDataChangeHandler,this)),this._yDataUpdateHandle=n.after("dataUpdate",e.bind(this._yDataChangeHandler,this))),this._xAxisChangeHandle=this.after("xAxisChange",this._xAxisChangeHandler),this._yAxisChangeHandle=this.after("yAxisChange",this._yAxisChangeHandler),this._stylesChangeHandle=this.after("stylesChange",function(){var e=this._updateAxisBase();e&&this.draw()}),this._widthChangeHandle=this.after("widthChange",function(){var e=this._updateAxisBase();e&&this.draw()}),this._heightChangeHandle=this.after("heightChange",function(){var e=this._updateAxisBase();e&&this.draw()}),this._visibleChangeHandle=this.after("visibleChange",this._handleVisibleChange)},_xAxisChangeHandler:function(){var t=this.get("xAxis");t.after("dataReady",e.bind(this._xDataChangeHandler,this)),t.after("dataUpdate",e.bind(this._xDataChangeHandler,this))},_yAxisChangeHandler:function(){var t=this.get("yAxis");t.after("dataReady",e.bind(this._yDataChangeHandler,this)),t.after("dataUpdate",e.bind(this._yDataChangeHandler,this))},GUID:"yuicartesianseries",_xDataChangeHandler:function(){var e=this._updateAxisBase();e&&this.draw()},_yDataChangeHandler:function(){var e=this._updateAxisBase();e&&this.draw()},_updateAxisBase:function(){var t=this.get("xAxis"),r=this.get("yAxis"),i=this.get("xKey"),s=this.get("yKey"),o,u,a,f,l;return!t||!r||!i||!s?l=!1:(u=t.getDataByKey(i),o=r.getDataByKey(s),n.isArray(i)?a=u&&e.Object.size(u)>0?this._checkForDataByKey(u,i):!1:a=u?!0:!1,n.isArray(s)?f=o&&e.Object.size(o)>0?this._checkForDataByKey(o,s):!1:f=o?!0:!1,l=a&&f,l&&(this.set("xData",u),this.set("yData",o))),l},_checkForDataByKey:function(e,t){var n,r=t.length,i=!1;for(n=0;n<r;n+=1)if(e[t[n]]){i=!0;break}return i},validate:function(){this.get("xData")&&this.get("yData")||this._updateAxisBase()?this.draw():this.fire("drawingComplete")},setAreaData:function(){var e=this.get("width"),t=this.get("height"),n=this.get("xAxis"),r=this.get("yAxis"),i=this._copyData(this.get("xData")),s=this._copyData(this.get("yData")),o=this.get("direction"),u=o==="vertical"?s.length:i.length,a=n.getEdgeOffset(n.getTotalMajorUnits(),e),f=r.getEdgeOffset(r.getTotalMajorUnits(),t),l=this.get("styles").padding,c=l.left,h=l.top,p=e-(c+l.right+a*2),d=t-(h+l.bottom+f*2),v=n.get("maximum"),m=n.get("minimum"),g=r.get("maximum"),y=r.get("minimum"),b=this.get("graphic"),w=r.get("type"),E=w==="numeric"||w==="stacked",S,x,T=n.getOrigin(),N=r.getOrigin();b.set("width",e),b.set("height",t),a+=c,f=E?f+d+h+l.bottom:h+f,this._leftOrigin=Math.round(n._getCoordFromValue(m,v,p,T,a,!1)),this._bottomOrigin=Math.round(r._getCoordFromValue(y,g,d,N,f,E)),S=this._getCoords(m,v,p,i,n,a,!1),x=this._getCoords(y,g,d,s,r,f,E),this.set("xcoords",S),this.set("ycoords",x),this._dataLength=u,this._setXMarkerPlane(S,u),this._setYMarkerPlane(x,u)},_getCoords:function(e,t,r,i,s,o,u){var a,f;if(n.isArray(i))a=s._getCoordsFromValues(e,t,r,i,o,u);else{a={};for(f in i)i.hasOwnProperty(f)&&(a[f]=this._getCoords.apply(this,[e,t,r,i[f],s,o,u]))}return a},_copyData:function(e){var t,r;if(n.isArray(e))t=e.concat();else{t={};for(r in e)e.hasOwnProperty(r)&&(t[r]=e[r].concat())}return t},_setXMarkerPlane:function(e,t){var r=0,i=[],s=this.get("xMarkerPlaneOffset"),o;if(n.isArray(e)){for(r=0;r<t;r+=1)o=e[r],i.push({start:o-s,end:o+s});this.set("xMarkerPlane",i)}},_setYMarkerPlane:function(e,t){var r=0,i=[],s=this.get("yMarkerPlaneOffset"),o;if(n.isArray(e)){for(r=0;r<t;r+=1)o=e[r],i.push({start:o-s,end:o+s});this.set("yMarkerPlane",i)}},_getFirstValidIndex:function(e){var t,r=-1,i=e.length;while(!n.isNumber(t)&&r<i)r+=1,t=e[r];return r},_getLastValidIndex:function(e){var t,r=e.length,i=-1;while(!n.isNumber(t)&&r>i)r-=1,t=e[r];return r},draw:function(){var e=this.get("width"),t=this.get("height"),n,r;if(this.get("rendered")&&isFinite(e)&&isFinite(t)&&e>0&&t>0&&(this.get("xData")&&this.get("yData")||this._updateAxisBase())){if(this._drawing){this._callLater=!0;return}this._drawing=!0,this._callLater=!1,this.setAreaData(),n=this.get("xcoords"),r=this.get("ycoords"),n&&r&&n.length>0&&this.drawSeries(),this._drawing=!1,this._callLater?this.draw():(this._toggleVisible(this.get("visible")),this.fire("drawingComplete"))}},_defaultPlaneOffset:4,destructor:function(){this.get("rendered")&&(this._xDataReadyHandle&&this._xDataReadyHandle.detach(),this._xDataUpdateHandle&&this._xDataUpdateHandle.detach(),this._yDataReadyHandle&&this._yDataReadyHandle.detach(),this._yDataUpdateHandle&&this._yDataUpdateHandle.detach(),this._xAxisChangeHandle&&this._xAxisChangeHandle.detach(),this._yAxisChangeHandle&&this._yAxisChangeHandle.detach())}},{ATTRS:{seriesTypeCollection:{},xDisplayName:{getter:function(){return this._xDisplayName||this.get("xKey")},setter:function(e){return this._xDisplayName=e.toString(),e}},yDisplayName:{getter:function(){return this._yDisplayName||this.get("yKey")},setter:function(e){return this._yDisplayName=e.toString(),e}},categoryDisplayName:{lazyAdd:!1,getter:function(){return this.get("direction")==="vertical"?this.get("yDisplayName"):this.get("xDisplayName")},setter:function(e){return this.get("direction")==="vertical"?this._yDisplayName=e:this._xDisplayName=e,e}},valueDisplayName:{lazyAdd:!1,getter:function(){return this.get("direction")==="vertical"?this.get("xDisplayName"):this.get("yDisplayName")},setter:function(e){return this.get("direction")==="vertical"?this._xDisplayName=e:this._yDisplayName=e,e}},type:{value:"cartesian"},order:{},graphOrder:{},xcoords:{},ycoords:{},xAxis:{},yAxis:{},xKey:{setter:function(e){return n
+.isArray(e)?e:e.toString()}},yKey:{setter:function(e){return n.isArray(e)?e:e.toString()}},xData:{},yData:{},xMarkerPlane:{},yMarkerPlane:{},xMarkerPlaneOffset:{getter:function(){var e=this.get("styles").marker;return e&&e.width&&isFinite(e.width)?e.width*.5:this._defaultPlaneOffset}},yMarkerPlaneOffset:{getter:function(){var e=this.get("styles").marker;return e&&e.height&&isFinite(e.height)?e.height*.5:this._defaultPlaneOffset}},direction:{value:"horizontal"}}})},"3.17.2",{requires:["series-base"]});
diff --git a/js/yui3/series-column-stacked/series-column-stacked-min.js b/js/yui3/series-column-stacked/series-column-stacked-min.js
new file mode 100644
index 000000000..4622099e4
--- /dev/null
+++ b/js/yui3/series-column-stacked/series-column-stacked-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-column-stacked",function(e,t){var n=e.Lang;e.StackedColumnSeries=e.Base.create("stackedColumnSeries",e.ColumnSeries,[e.StackingUtil],{drawSeries:function(){if(this.get("xcoords").length<1)return;var e=n.isNumber,t=this._copyObject(this.get("styles").marker),r=t.width,i=t.height,s=this.get("xcoords"),o=this.get("ycoords"),u=0,a=s.length,f=o[0],l=this.get("seriesTypeCollection"),c,h=this.get("order"),p=this.get("graphOrder"),d,v,m,g,y,b,w,E=h===0,S=a*r,x={width:[],height:[]},T=[],N=[],C=this.get("groupMarkers");n.isArray(t.fill.color)&&(m=t.fill.color.concat()),n.isArray(t.border.color)&&(g=t.border.color.concat()),this._createMarkerCache(),S>this.get("width")&&(c=this.get("width")/S,r*=c,r=Math.max(r,1));if(!E){y=l[h-1],b=y.get("negativeBaseValues"),w=y.get("positiveBaseValues");if(!b||!w)E=!0,w=[],b=[]}else b=[],w=[];this.set("negativeBaseValues",b),this.set("positiveBaseValues",w);for(u=0;u<a;++u){d=s[u],f=o[u];if(!e(f)||!e(d)){E&&(b[u]=this._bottomOrigin,w[u]=this._bottomOrigin),this._markers.push(null);continue}E?(i=Math.abs(this._bottomOrigin-f),f<this._bottomOrigin?(w[u]=f,b[u]=this._bottomOrigin):f>this._bottomOrigin?(w[u]=this._bottomOrigin,b[u]=f,f-=i):(w[u]=f,b[u]=f)):f>this._bottomOrigin?(f+=b[u]-this._bottomOrigin,i=f-b[u],b[u]=f,f-=i):f<=this._bottomOrigin&&(f=w[u]-(this._bottomOrigin-f),i=w[u]-f,w[u]=f),!isNaN(i)&&i>0?(d-=r/2,C?(x.width[u]=r,x.height[u]=i,T.push(d),N.push(f)):(t.width=r,t.height=i,t.x=d,t.y=f,m&&(t.fill.color=m[u%m.length]),g&&(t.border.color=g[u%g.length]),v=this.getMarker(t,p,u))):C||this._markers.push(null)}C?this._createGroupMarker({fill:t.fill,border:t.border,dimensions:x,xvalues:T,yvalues:N,shape:t.shape}):this._clearMarkerCache()},updateMarkerState:function(e,t){if(this._markers&&this._markers[t]){var r,i,s=this._getState(e),o=this.get("xcoords"),u=this._markers[t],a=0,f,l;r=this.get("styles").marker,a=r.width*.5,i=s==="off"||!r[s]?this._copyObject(r):this._copyObject(r[s]),i.height=u.get("height"),i.x=o[t]-a,i.y=u.get("y"),i.id=u.get("id"),f=i.fill.color,l=i.border.color,n.isArray(f)?i.fill.color=f[t%f.length]:i.fill.color=this._getItemColor(i.fill.color,t),n.isArray(l)?i.border.color=l[t%l.length]:i.border.color=this._getItemColor(i.border.color,t),u.set(i)}},_getPlotDefaults:function(){var e={fill:{type:"solid",alpha:1,colors:null,alphas:null,ratios:null},border:{weight:0,alpha:1},width:24,height:24,shape:"rect",padding:{top:0,left:0,right:0,bottom:0}};return e.fill.color=this._getDefaultColor(this.get("graphOrder"),"fill"),e.border.color=this._getDefaultColor(this.get("graphOrder"),"border"),e}},{ATTRS:{type:{value:"stackedColumn"},negativeBaseValues:{value:null},positiveBaseValues:{value:null}}})},"3.17.2",{requires:["series-stacked","series-column"]});
diff --git a/js/yui3/series-column/series-column-min.js b/js/yui3/series-column/series-column-min.js
new file mode 100644
index 000000000..c91e05741
--- /dev/null
+++ b/js/yui3/series-column/series-column-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-column",function(e,t){e.ColumnSeries=e.Base.create("columnSeries",e.MarkerSeries,[e.Histogram],{_getMarkerDimensions:function(e,t,n,r){var i={left:e+r};return this._bottomOrigin>=t?(i.top=t,i.calculatedSize=this._bottomOrigin-i.top):(i.top=this._bottomOrigin,i.calculatedSize=t-this._bottomOrigin),i},updateMarkerState:function(e,t){if(this._markers&&this._markers[t]){var n=this._copyObject(this.get("styles").marker),r,i=this._getState(e),s=this.get("xcoords"),o=this.get("ycoords"),u=this._markers[t],a,f,l=this.get("seriesTypeCollection"),c=l?l.length:0,h=0,p=0,d,v=0,m=[],g=this.get("order"),y;r=i==="off"||!n[i]?this._copyObject(n):this._copyObject(n[i]),r.fill.color=this._getItemColor(r.fill.color,t),r.border.color=this._getItemColor(r.border.color,t),y=this._getMarkerDimensions(s[t],o[t],n.width,p),r.height=y.calculatedSize,r.width=Math.min(this._maxSize,r.width),u.set(r);for(;v<c;++v)m[v]=s[t]+h,f=l[v].get("styles").marker,h+=Math.min(this._maxSize,f.width),g>v&&(p=h),p-=h/2;for(v=0;v<c;++v)a=l[v].get("markers"),a&&(d=a[t],d&&d!==undefined&&d.set("x",m[v]-h/2))}}},{ATTRS:{type:{value:"column"}}})},"3.17.2",{requires:["series-marker","series-histogram-base"]});
diff --git a/js/yui3/series-combo-stacked/series-combo-stacked-min.js b/js/yui3/series-combo-stacked/series-combo-stacked-min.js
new file mode 100644
index 000000000..e7a0d8860
--- /dev/null
+++ b/js/yui3/series-combo-stacked/series-combo-stacked-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-combo-stacked",function(e,t){e.StackedComboSeries=e.Base.create("stackedComboSeries",e.ComboSeries,[e.StackingUtil],{setAreaData:function(){e.StackedComboSeries.superclass.setAreaData.apply(this),this._stackCoordinates.apply(this)},drawSeries:function(){this.get("showAreaFill")&&this.drawFill.apply(this,this._getStackedClosingPoints()),this.get("showLines")&&this.drawLines(),this.get("showMarkers")&&this.drawPlots()}},{ATTRS:{type:{value:"stackedCombo"},showAreaFill:{value:!0}}})},"3.17.2",{requires:["series-stacked","series-combo"]});
diff --git a/js/yui3/series-combo/series-combo-min.js b/js/yui3/series-combo/series-combo-min.js
new file mode 100644
index 000000000..8d4a31baf
--- /dev/null
+++ b/js/yui3/series-combo/series-combo-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-combo",function(e,t){e.ComboSeries=e.Base.create("comboSeries",e.CartesianSeries,[e.Fills,e.Lines,e.Plots],{drawSeries:function(){this.get("showAreaFill")&&this.drawFill.apply(this,this._getClosingPoints()),this.get("showLines")&&this.drawLines(),this.get("showMarkers")&&this.drawPlots()},_toggleVisible:function(e){var t,n,r,i;this.get("showAreaFill")&&this._path&&this._path.set("visible",e),this.get("showLines")&&this._lineGraphic&&this._lineGraphic.set("visible",e);if(this.get("showMarkers")){t=this.get("markers");if(t){i=0,r=t.length;for(;i<r;++i)n=t[i],n&&n.set("visible",e)}}},_getDefaultStyles:function(){var t=e.ComboSeries.superclass._getDefaultStyles();return t.line=this._getLineDefaults(),t.marker=this._getPlotDefaults(),t.area=this._getAreaDefaults(),t}},{ATTRS:{type:{value:"combo"},showAreaFill:{value:!1},showLines:{value:!0},showMarkers:{value:!0},marker:{lazyAdd:!1,getter:function(){return this.get("styles").marker},setter:function(e){this.set("styles",{marker:e})}},line:{lazyAdd:!1,getter:function(){return this.get("styles").line},setter:function(e){this.set("styles",{line:e})}},area:{lazyAdd:!1,getter:function(){return this.get("styles").area},setter:function(e){this.set("styles",{area:e})}}}})},"3.17.2",{requires:["series-cartesian","series-line-util","series-plot-util","series-fill-util"]});
diff --git a/js/yui3/series-combospline-stacked/series-combospline-stacked-min.js b/js/yui3/series-combospline-stacked/series-combospline-stacked-min.js
new file mode 100644
index 000000000..687e05d33
--- /dev/null
+++ b/js/yui3/series-combospline-stacked/series-combospline-stacked-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-combospline-stacked",function(e,t){e.StackedComboSplineSeries=e.Base.create("stackedComboSplineSeries",e.StackedComboSeries,[e.CurveUtil],{drawSeries:function(){this.get("showAreaFill")&&this.drawStackedAreaSpline(),this.get("showLines")&&this.drawSpline(),this.get("showMarkers")&&this.drawPlots()}},{ATTRS:{type:{value:"stackedComboSpline"},showAreaFill:{value:!0}}})},"3.17.2",{requires:["series-combo-stacked","series-curve-util"]});
diff --git a/js/yui3/series-combospline/series-combospline-min.js b/js/yui3/series-combospline/series-combospline-min.js
new file mode 100644
index 000000000..a45632901
--- /dev/null
+++ b/js/yui3/series-combospline/series-combospline-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-combospline",function(e,t){e.ComboSplineSeries=e.Base.create("comboSplineSeries",e.ComboSeries,[e.CurveUtil],{drawSeries:function(){this.get("showAreaFill")&&this.drawAreaSpline(),this.get("showLines")&&this.drawSpline(),this.get("showMarkers")&&this.drawPlots()}},{ATTRS:{type:{value:"comboSpline"}}})},"3.17.2",{requires:["series-combo","series-curve-util"]});
diff --git a/js/yui3/series-curve-util/series-curve-util-min.js b/js/yui3/series-curve-util/series-curve-util-min.js
new file mode 100644
index 000000000..5c264497f
--- /dev/null
+++ b/js/yui3/series-curve-util/series-curve-util-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-curve-util",function(e,t){function n(){}n.prototype={getCurveControlPoints:function(e,t){var n=[],r=1,i=e.length-1,s=[],o=[];if(i<1)return null;n[0]={startx:e[0],starty:t[0],endx:e[1],endy:t[1]};if(i===1)return n[0].ctrlx1=(2*e[0]+e[1])/3,n[0].ctrly2=(2*t[0]+t[1])/3,n[0].ctrlx2=2*n[0].ctrlx1-e[0],n[0].ctrly2=2*n[0].ctrly1-t[0],n;for(;r<i;++r)n.push({startx:Math.round(e[r]),starty:Math.round(t[r]),endx:Math.round(e[r+1]),endy:Math.round(t[r+1])}),s[r]=4*e[r]+2*e[r+1],o[r]=4*t[r]+2*t[r+1];s[0]=e[0]+2*e[1],s[i-1]=(8*e[i-1]+e[i])/2,s=this.getControlPoints(s.concat()),o[0]=t[0]+2*t[1],o[i-1]=(8*t[i-1]+t[i])/2,o=this.getControlPoints(o.concat());for(r=0;r<i;++r)n[r].ctrlx1=Math.round(s[r]),n[r].ctrly1=Math.round(o[r]),r<i-1?(n[r].ctrlx2=Math.round(2*e[r+1]-s[r+1]),n[r].ctrly2=Math.round(2*t[r+1]-o[r+1])):(n[r].ctrlx2=Math.round((e[i]+s[i-1])/2),n[r].ctrly2=Math.round((t[i]+o[i-1])/2));return n},getControlPoints:function(e){var t=e.length,n=[],r=[],i=2,s=1;n[0]=e[0]/i;for(;s<t;++s)r[s]=1/i,i=(s<t-1?4:3.5)-r[s],n[s]=(e[s]-n[s-1])/i;for(s=1;s<t;++s)n[t-s-1]-=r[t-s]*n[t-s];return n}},e.CurveUtil=n},"3.17.2");
diff --git a/js/yui3/series-fill-util/series-fill-util-min.js b/js/yui3/series-fill-util/series-fill-util-min.js
new file mode 100644
index 000000000..e2bf8d426
--- /dev/null
+++ b/js/yui3/series-fill-util/series-fill-util-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-fill-util",function(e,t){function r(){}var n=e.Lang;r.ATTRS={area:{getter:function(){return this._defaults||this._getAreaDefaults()},setter:function(t){var n=this._defaults||this._getAreaDefaults();this._defaults=e.merge(n,t)}}},r.prototype={_getPath:function(){var e=this._path;return e||(e=this.get("graphic").addShape({type:"path"}),this._path=e),e},_toggleVisible:function(e){this._path&&this._path.set("visible",e)},drawFill:function(e,t){if(e.length<1)return;var r=n.isNumber,i=e.length,s=e[0],o=t[0],u=s,a=o,f,l,c,h=!0,p=0,d=this.get("styles").area,v=this._getPath(),m=d.color||this._getDefaultColor(this.get("graphOrder"),"slice");v.clear(),v.set("fill",{color:m,opacity:d.alpha}),v.set("stroke",{weight:0});for(;p<i;p=++p){f=e[p],l=t[p],c=r(f)&&r(l);if(!c)continue;h?(this._firstValidX=f,this._firstValidY=l,h=!1,v.moveTo(f,l)):v.lineTo(f,l),u=f,a=l}this._lastValidX=u,this._lastValidY=a,v.end()},drawAreaSpline:function(){if(this.get("xcoords").length<1)return;var e=this.get("xcoords"),t=this.get("ycoords"),n=this.getCurveControlPoints(e,t),r=n.length,i,s,o,u,a,f,l=0,c=e[0],h=t[0],p=this.get("styles").area,d=this._getPath(),v=p.color||this._getDefaultColor(this.get("graphOrder"),"slice");d.set("fill",{color:v,opacity:p.alpha}),d.set("stroke",{weight:0}),d.moveTo(c,h);for(;l<r;l=++l)a=n[l].endx,f=n[l].endy,i=n[l].ctrlx1,s=n[l].ctrlx2,o=n[l].ctrly1,u=n[l].ctrly2,d.curveTo(i,o,s,u,a,f);this.get("direction")==="vertical"?(d.lineTo(this._leftOrigin,f),d.lineTo(this._leftOrigin,h)):(d.lineTo(a,this._bottomOrigin),d.lineTo(c,this._bottomOrigin)),d.lineTo(c,h),d.end()},drawStackedAreaSpline:function(){if(this.get("xcoords").length<1)return;var e=this.get("xcoords"),t=this.get("ycoords"),n,r=this.get("order"),i=this.get("seriesTypeCollection"),s,o,u,a,f,l,c,h,p,d=0,v,m,g=this.get("styles").area,y=this._getPath(),b=g.color||this._getDefaultColor(this.get("graphOrder"),"slice");v=e[0],m=t[0],n=this.getCurveControlPoints(e,t),u=n.length,y.set("fill",{color:b,opacity:g.alpha}),y.set("stroke",{weight:0}),y.moveTo(v,m);for(;d<u;d=++d)h=n[d].endx,p=n[d].endy,a=n[d].ctrlx1,f=n[d].ctrlx2,l=n[d].ctrly1,c=n[d].ctrly2,y.curveTo(a,l,f,c,h,p);if(r>0){s=i[r-1].get("xcoords").concat().reverse(),o=i[r-1].get("ycoords").concat().reverse(),n=this.getCurveControlPoints(s,o),d=0,u=n.length,y.lineTo(s[0],o[0]);for(;d<u;d=++d)h=n[d].endx,p=n[d].endy,a=n[d].ctrlx1,f=n[d].ctrlx2,l=n[d].ctrly1,c=n[d].ctrly2,y.curveTo(a,l,f,c,h,p)}else this.get("direction")==="vertical"?(y.lineTo(this._leftOrigin,t[t.length-1]),y.lineTo(this._leftOrigin,m)):(y.lineTo(e[e.length-1],this._bottomOrigin),y.lineTo(v,this._bottomOrigin));y.lineTo(v,m),y.end()},_defaults:null,_getClosingPoints:function(){var e=this.get("xcoords").concat(),t=this.get("ycoords").concat(),n,r;return this.get("direction")==="vertical"?(r=this._getLastValidIndex(e),n=this._getFirstValidIndex(e),t.push(t[r]),t.push(t[n]),e.push(this._leftOrigin),e.push(this._leftOrigin)):(r=this._getLastValidIndex(t),n=this._getFirstValidIndex(t),e.push(e[r]),e.push(e[n]),t.push(this._bottomOrigin),t.push(this._bottomOrigin)),e.push(e[0]),t.push(t[0]),[e,t]},_getHighestValidOrder:function(e,t,n,r){var i=r==="vertical"?"stackedXCoords":"stackedYCoords",s;while(isNaN(s)&&n>-1)n-=1,n>-1&&(s=e[n].get(i)[t]);return n},_getCoordsByOrderAndIndex:function(e,t,n,r){var i,s;return r==="vertical"?(i=n<0?this._leftOrigin:e[n].get("stackedXCoords")[t],s=this.get("stackedYCoords")[t]):(i=this.get("stackedXCoords")[t],s=n<0?this._bottomOrigin:e[n].get("stackedYCoords")[t]),[i,s]},_getStackedClosingPoints:function(){var e=this.get("order"),t=this.get("direction"),n=this.get("seriesTypeCollection"),r,i,s=this.get("stackedXCoords"),o=this.get("stackedYCoords"),u,a,f,l,c,h,p,d,v,m,g,y;if(e<1)return this._getClosingPoints();a=n[e-1],c=a.get("stackedXCoords").concat(),h=a.get("stackedYCoords").concat(),t==="vertical"?(r=this._getFirstValidIndex(s),i=this._getLastValidIndex(s),f=a._getFirstValidIndex(c),l=a._getLastValidIndex(c)):(r=this._getFirstValidIndex(o),i=this._getLastValidIndex(o),f=a._getFirstValidIndex(h),l=a._getLastValidIndex(h)),l>=r&&f<=i?(f=Math.max(r,f),l=Math.min(i,l),c=c.slice(f,l+1),h=h.slice(f,l+1),u=f):u=i,d=[s[r]],v=[o[r]],m=r;while((isNaN(g)||g<e-1)&&m<=u)y=g,g=this._getHighestValidOrder(n,m,e,t),!isNaN(y)&&g>y&&(p=this._getCoordsByOrderAndIndex(n,m,y,t),d.push(p[0]),v.push(p[1])),p=this._getCoordsByOrderAndIndex(n,m,g,t),d.push(p[0]),v.push(p[1]),m+=1;c&&c.length>0&&l>r&&f<i&&(d=d.concat(c),v=v.concat(h),g=e-1),m=Math.max(r,l),e-=1,g=NaN;while(m<=i)y=g,g=this._getHighestValidOrder(n,m,e,t),isNaN(y)||(g>y?(p=this._getCoordsByOrderAndIndex(n,m,y,t),d.push(p[0]),v.push(p[1])):g<y&&(p=this._getCoordsByOrderAndIndex(n,m-1,g,t),d.push(p[0]),v.push(p[1]))),p=this._getCoordsByOrderAndIndex(n,m,g,t),d.push(p[0]),v.push(p[1]),m+=1;return d.reverse(),v.reverse(),[s.concat(d),o.concat(v)]},_getAreaDefaults:function(){return{}}},e.augment(r,e.Attribute),e.Fills=r},"3.17.2");
diff --git a/js/yui3/series-histogram-base/series-histogram-base-min.js b/js/yui3/series-histogram-base/series-histogram-base-min.js
new file mode 100644
index 000000000..a4be5e7a4
--- /dev/null
+++ b/js/yui3/series-histogram-base/series-histogram-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-histogram-base",function(e,t){function r(){}var n=e.Lang;r.prototype={drawSeries:function(){if(this.get("xcoords").length<1)return;var e=this._copyObject(this.get("styles").marker),t=this.get("graphic"),r,i,s=this.get("xcoords"),o=this.get("ycoords"),u=0,a=s.length,f=o[0],l=this.get("seriesTypeCollection"),c=l?l.length:0,h=0,p=0,d=0,v,m,g=this.get("order"),y=this.get("graphOrder"),b,w,E,S,x,T=null,N=null,C=[],k=[],L,A,O,M,_={width:[],height:[]},D=[],P=[],H=this.get("groupMarkers");n.isArray(e.fill.color)&&(T=e.fill.color.concat()),n.isArray(e.border.color)&&(N=e.border.color.concat()),this.get("direction")==="vertical"?(E="height",S="width"):(E="width",S="height"),r=e[E],i=e[S],this._createMarkerCache(),this._maxSize=t.get(E);if(l&&c>1){for(;u<c;++u)m=l[u],h+=m.get("styles").marker[E],g>u&&(d=h);p=a*h,p>this._maxSize&&(v=t.get(E)/p,h*=v,d*=v,r*=v,r=Math.max(r,1),this._maxSize=r)}else h=e[E],p=a*h,p>this._maxSize&&(h=this._maxSize/a,this._maxSize=h);d-=h/2;for(u=0;u<a;++u){L=s[u]-h/2,A=L+h,O=o[u]-h/2,M=O+h,C.push({start:L,end:A}),k.push({start:O,end:M});if(!H&&(isNaN(s[u])||isNaN(o[u]))){this._markers.push(null);continue}x=this._getMarkerDimensions(s[u],o[u],i,d),!isNaN(x.calculatedSize)&&x.calculatedSize>0?(f=x.top,b=x.left,H?(_[E][u]=r,_[S][u]=x.calculatedSize,D.push(b),P.push(f)):(e[E]=r,e[S]=x.calculatedSize,e.x=b,e.y=f,T&&(e.fill.color=T[u%T.length]),N&&(e.border.color=N[u%N.length]),w=this.getMarker(e,y,u))):H||this._markers.push(null)}this.set("xMarkerPlane",C),this.set("yMarkerPlane",k),H?this._createGroupMarker({fill:e.fill,border:e.border,dimensions:_,xvalues:D,yvalues:P,shape:e.shape}):this._clearMarkerCache()},_defaultFillColors:["#66007f","#a86f41","#295454","#996ab2","#e8cdb7","#90bdbd","#000000","#c3b8ca","#968373","#678585"],_getPlotDefaults:function(){var e={fill:{type:"solid",alpha:1,colors:null,alphas:null,ratios:null},border:{weight:0,alpha:1},width:12,height:12,shape:"rect",padding:{top:0,left:0,right:0,bottom:0}};return e.fill.color=this._getDefaultColor(this.get("graphOrder"),"fill"),e.border.color=this._getDefaultColor(this.get("graphOrder"),"border"),e}},e.Histogram=r},"3.17.2",{requires:["series-cartesian","series-plot-util"]});
diff --git a/js/yui3/series-line-stacked/series-line-stacked-min.js b/js/yui3/series-line-stacked/series-line-stacked-min.js
new file mode 100644
index 000000000..6afef9598
--- /dev/null
+++ b/js/yui3/series-line-stacked/series-line-stacked-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-line-stacked",function(e,t){e.StackedLineSeries=e.Base.create("stackedLineSeries",e.LineSeries,[e.StackingUtil],{setAreaData:function(){e.StackedLineSeries.superclass.setAreaData.apply(this),this._stackCoordinates.apply(this)}},{ATTRS:{type:{value:"stackedLine"}}})},"3.17.2",{requires:["series-stacked","series-line"]});
diff --git a/js/yui3/series-line-util/series-line-util-min.js b/js/yui3/series-line-util/series-line-util-min.js
new file mode 100644
index 000000000..09a38bac8
--- /dev/null
+++ b/js/yui3/series-line-util/series-line-util-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-line-util",function(e,t){function r(){}var n=e.Lang;r.prototype={_lineDefaults:null,_getGraphic:function(){var e=this.get("graphic")||this.get("graph").get("graphic");return this._lineGraphic||(this._lineGraphic=e.addShape({type:"path"})),this._lineGraphic.clear(),this._lineGraphic},_toggleVisible:function(e){this._lineGraphic&&this._lineGraphic.set("visible",e)},drawLines:function(){if(this.get("xcoords").length<1)return;var e=n.isNumber,t,r,i=this.get("direction"),s,o,u,a=!0,f,l,c,h,p,d=this.get("styles").line,v=d.lineType,m=d.color||this._getDefaultColor(this.get("graphOrder"),"line"),g=d.alpha,y=d.dashLength,b=d.gapSpace,w=d.connectDiscontinuousPoints,E=d.discontinuousType,S=d.discontinuousDashLength,x=d.discontinuousGapSpace,T=this._getGraphic();this._stacked?(t=this.get("stackedXCoords"),r=this.get("stackedYCoords")):(t=this.get("xcoords"),r=this.get("ycoords")),s=i==="vertical"?r.length:t.length,T.set("stroke",{weight:d.weight,color:m,opacity:g});for(p=0;p<s;p=++p){c=t[p],h=r[p],u=e(c)&&e(h);if(!u){o=u;continue}a?(a=!1,T.moveTo(c,h)):o?v!=="dashed"?T.lineTo(c,h):this.drawDashedLine(T,f,l,c,h,y,b):w?E!=="solid"?this.drawDashedLine(T,f,l,c,h,S,x):T.lineTo(c,h):T.moveTo(c,h),f=c,l=h,o=!0}T.end()},drawSpline:function(){if(this.get("xcoords").length<1)return;var e=this.get("xcoords"),t=this.get("ycoords"),n=this.getCurveControlPoints(e,t),r=n.length,i,s,o,u,a,f,l=0,c=this.get("styles").line,h=this._getGraphic(),p=c.alpha,d=c.color||this._getDefaultColor(this.get("graphOrder"),"line");h.set("stroke",{weight:c.weight,color:d,opacity:p}),h.moveTo(e[0],t[0]);for(;l<r;l=++l)a=n[l].endx,f=n[l].endy,i=n[l].ctrlx1,s=n[l].ctrlx2,o=n[l].ctrly1,u=n[l].ctrly2,h.curveTo(i,o,s,u,a,f);h.end()},drawDashedLine:function(e,t,n,r,i,s,o){s=s||10,o=o||10;var u=s+o,a=r-t,f=i-n,l=Math.sqrt(Math.pow(a,2)+Math.pow(f,2)),c=Math.floor(Math.abs(l/u)),h=Math.atan2(f,a),p=t,d=n,v;a=Math.cos(h)*u,f=Math.sin(h)*u;for(v=0;v<c;++v)e.moveTo(p,d),e.lineTo(p+Math.cos(h)*s,d+Math.sin(h)*s),p+=a,d+=f;e.moveTo(p,d),l=Math.sqrt((r-p)*(r-p)+(i-d)*(i-d)),l>s?e.lineTo(p+Math.cos(h)*s,d+Math.sin(h)*s):l>0&&e.lineTo(p+Math.cos(h)*l,d+Math.sin(h)*l),e.moveTo(r,i)},_getLineDefaults:function(){return{alpha:1,weight:6,lineType:"solid",dashLength:10,gapSpace:10,connectDiscontinuousPoints:!0,discontinuousType:"solid",discontinuousDashLength:10,discontinuousGapSpace:10}}},e.augment(r,e.Attribute),e.Lines=r},"3.17.2");
diff --git a/js/yui3/series-line/series-line-min.js b/js/yui3/series-line/series-line-min.js
new file mode 100644
index 000000000..4fc2cb2b8
--- /dev/null
+++ b/js/yui3/series-line/series-line-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-line",function(e,t){e.LineSeries=e.Base.create("lineSeries",e.CartesianSeries,[e.Lines],{drawSeries:function(){this.drawLines()},_setStyles:function(t){return t.line||(t={line:t}),e.LineSeries.superclass._setStyles.apply(this,[t])},_getDefaultStyles:function(){var t=this._mergeStyles({line:this._getLineDefaults()},e.LineSeries.superclass._getDefaultStyles());return t}},{ATTRS:{type:{value:"line"}}})},"3.17.2",{requires:["series-cartesian","series-line-util"]});
diff --git a/js/yui3/series-marker-stacked/series-marker-stacked-min.js b/js/yui3/series-marker-stacked/series-marker-stacked-min.js
new file mode 100644
index 000000000..042622ff8
--- /dev/null
+++ b/js/yui3/series-marker-stacked/series-marker-stacked-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-marker-stacked",function(e,t){e.StackedMarkerSeries=e.Base.create("stackedMarkerSeries",e.MarkerSeries,[e.StackingUtil],{setAreaData:function(){e.StackedMarkerSeries.superclass.setAreaData.apply(this),this._stackCoordinates.apply(this)}},{ATTRS:{type:{value:"stackedMarker"}}})},"3.17.2",{requires:["series-stacked","series-marker"]});
diff --git a/js/yui3/series-marker/series-marker-min.js b/js/yui3/series-marker/series-marker-min.js
new file mode 100644
index 000000000..8b49ee381
--- /dev/null
+++ b/js/yui3/series-marker/series-marker-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-marker",function(e,t){e.MarkerSeries=e.Base.create("markerSeries",e.CartesianSeries,[e.Plots],{_setStyles:function(t){return t.marker||(t={marker:t}),t=this._parseMarkerStyles(t),e.MarkerSeries.superclass._mergeStyles.apply(this,[t,this._getDefaultStyles()])}},{ATTRS:{type:{value:"marker"}}})},"3.17.2",{requires:["series-cartesian","series-plot-util"]});
diff --git a/js/yui3/series-ohlc/series-ohlc-min.js b/js/yui3/series-ohlc/series-ohlc-min.js
new file mode 100644
index 000000000..ff685f693
--- /dev/null
+++ b/js/yui3/series-ohlc/series-ohlc-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-ohlc",function(e,t){function n(){n.superclass.constructor.apply(this,arguments)}n.NAME="ohlcSeries",n.ATTRS={type:{value:"ohlc"},graphic:{lazyAdd:!1,setter:function(e){return this.get("rendered")||this.set("rendered",!0),this.set("upmarker",e.addShape({type:"path"})),this.set("downmarker",e.addShape({type:"path"})),e}},upmarker:{},downmarker:{}},e.extend(n,e.RangeSeries,{_drawMarkers:function(e,t,n,r,i,s,o,u,a){var f=this.get("upmarker"),l=this.get("downmarker"),c,h,p,d,v,m,g=a.padding.left,y,b,w,E,S;f.set(a.upmarker),l.set(a.downmarker),f.clear(),l.clear();for(E=0;E<s;E+=1)w=e[E]+g,v=w-u,m=w+u,c=t[E],h=n[E],p=r[E],d=i[E],b=c>d,S=p-h,y=b?f:l,y.moveTo(v,c),y.lineTo(w,c),y.moveTo(w,h),y.lineTo(w,p),y.moveTo(w,d),y.lineTo(m,d);f.end(),l.end()},_toggleVisible:function(e){this.get("upmarker").set("visible",e),this.get("downmarker").set("visible",e)},destructor:function(){var e=this.get("upmarker"),t=this.get("downmarker");e&&e.destroy(),t&&t.destroy()},_getDefaultStyles:function(){var e={upmarker:{stroke:{color:"#00aa00",alpha:1,weight:1}},downmarker:{stroke:{color:"#aa0000",alpha:1,weight:1}}};return this._mergeStyles(e,n.superclass._getDefaultStyles())}}),e.OHLCSeries=n},"3.17.2",{requires:["series-range"]});
diff --git a/js/yui3/series-pie/series-pie-min.js b/js/yui3/series-pie/series-pie-min.js
new file mode 100644
index 000000000..43eaa90af
--- /dev/null
+++ b/js/yui3/series-pie/series-pie-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-pie",function(e,t){var n=e.config,r=n.doc,i=e.ClassNameManager.getClassName,s=i("seriesmarker");e.PieSeries=e.Base.create("pieSeries",e.SeriesBase,[e.Plots],{_map:null,_image:null,_setMap:function(){var e="pieHotSpotMapi_"+Math.round(1e5*Math.random()),t=this.get("graph"),n,i,s;t?i=t.get("contentBox"):(n=this.get("graphic"),i=n.get("node"));if(this._image){i.removeChild(this._image);while(this._areaNodes&&this._areaNodes.length>0)s=this._areaNodes.shift(),this._map.removeChild(s);i.removeChild(this._map)}this._image=r.createElement("img"),this._image.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAABCAYAAAD9yd/wAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABJJREFUeNpiZGBgSGPAAgACDAAIkABoFyloZQAAAABJRU5ErkJggg==",i.appendChild(this._image),this._image.style.position="absolute",this._image.style.left="0px",this._image.style.top="0px",this._image.setAttribute("usemap","#"+e),this._image.style.zIndex=3,this._image.style.opacity=0,this._image.setAttribute("alt","imagemap"),this._map=r.createElement("map"),i.appendChild(this._map),this._map.setAttribute("name",e),this._map.setAttribute("id",e),this._areaNodes=[]},_categoryDisplayName:null,_valueDisplayName:null,addListeners:function(){var t=this.get("categoryAxis"),n=this.get("valueAxis");t&&(t.after("dataReady",e.bind(this._categoryDataChangeHandler,this)),t.after("dataUpdate",e.bind(this._categoryDataChangeHandler,this))),n&&(n.after("dataReady",e.bind(this._valueDataChangeHandler,this)),n.after("dataUpdate",e.bind(this._valueDataChangeHandler,this))),this.after("categoryAxisChange",this.categoryAxisChangeHandler),this.after("valueAxisChange",this.valueAxisChangeHandler),this._stylesChangeHandle=this.after("stylesChange",this._updateHandler),this._visibleChangeHandle=this.after("visibleChange",this._handleVisibleChange)},validate:function(){this.draw(),this._renderered=!0},_categoryAxisChangeHandler:function(){var t=this.get("categoryAxis");t.after("dataReady",e.bind(this._categoryDataChangeHandler,this)),t.after("dataUpdate",e.bind(this._categoryDataChangeHandler,this))},_valueAxisChangeHandler:function(){var t=this.get("valueAxis");t.after("dataReady",e.bind(this._valueDataChangeHandler,this)),t.after("dataUpdate",e.bind(this._valueDataChangeHandler,this))},GUID:"pieseries",_categoryDataChangeHandler:function(){this._rendered&&this.get("categoryKey")&&this.get("valueKey")&&this.draw()},_valueDataChangeHandler:function(){this._rendered&&this.get("categoryKey")&&this.get("valueKey")&&this.draw()},getTotalValues:function(){var e=this.get("valueAxis").getTotalByKey(this.get("valueKey"));return e},draw:function(){var e=this.get("width"),t=this.get("height");if(isFinite(e)&&isFinite(t)&&e>0&&t>0){this._rendered=!0;if(this._drawing){this._callLater=!0;return}this._drawing=!0,this._callLater=!1,this.drawSeries(),this._drawing=!1,this._callLater?this.draw():this.fire("drawingComplete")}},drawPlots:function(){var t=this.get("valueAxis").getDataByKey(this.get("valueKey")).concat(),n=0,r=t.length,i=this.get("styles").marker,s=i.fill.colors,o=i.fill.alphas||["1"],u=i.border.colors,a=[i.border.weight],f=[i.border.alpha],l=a.concat(),c=u.concat(),h=f.concat(),p,d,v=i.padding,m=this.get("graphic"),g=Math.min(m.get("width"),m.get("height")),y=g-(v.left+v.right),b=g-(v.top+v.bottom),w=-90,E=y/2,S=b/2,x=Math.min(E,S),T=0,N,C=0,k,L,A,O,M,_=this.get("graphOrder")||0,D=e.Graphic.NAME==="canvasGraphic";for(;T<r;++T)N=parseFloat(t[T]),t.push(N),isNaN(N)||(n+=N);p=s?s.concat():null,d=o?o.concat():null,this._createMarkerCache(),D&&(this._setMap(),this._image.width=y,this._image.height=b);for(T=0;T<r;T++)N=t[T],n===0?C=360/t.length:C=360*(N/n),p&&p.length<1&&(p=s.concat()),d&&d.length<1&&(d=o.concat()),l&&l.length<1&&(l=a.concat()),l&&c.length<1&&(c=u.concat()),h&&h.length<1&&(h=f.concat()),A=l?l.shift():null,k=c?c.shift():null,L=h?h.shift():null,w+=C,O={border:{color:k,weight:A,alpha:L},fill:{color:p?p.shift():this._getDefaultColor(T,"slice"),alpha:d?d.shift():null},type:"pieslice",arc:C,radius:x,startAngle:w,cx:E,cy:S,width:y,height:b},M=this.getMarker(O,_,T),D&&this._addHotspot(O,_,T);this._clearMarkerCache()},_setStyles:function(t){return t.marker||(t={marker:t}),t=this._parseMarkerStyles(t),e.PieSeries.superclass._mergeStyles.apply(this,[t,this._getDefaultStyles()])},_addHotspot:function(e,t,n){var i=r.createElement("area"),o=1,u=e.cx,a=e.cy,f=e.arc,l=e.startAngle-f,c=e.startAngle,h=e.radius,p=u+Math.cos(l/180*Math.PI)*h,d=a+Math.sin(l/180*Math.PI)*h,v=u+Math.cos(c/180*Math.PI)*h,m=a+Math.sin(c/180*Math.PI)*h,g=Math.floor(f/10)-1,y=f/Math.floor(f/10)/180*Math.PI,b=Math.atan((d-a)/(p-u)),w=u+", "+a+", "+p+", "+d,E,S,x;for(o=1;o<=g;++o)x=y*o,E=Math.cos(b+x),S=Math.sin(b+x),l<=90?(w+=", "+(u+h*Math.cos(b+y*o)),w+=", "+(a+h*Math.sin(b+y*o))):(w+=", "+(u-h*Math.cos(b+y*o)),w+=", "+(a-h*Math.sin(b+y*o)));w+=", "+v+", "+m,w+=", "+u+", "+a,this._map.appendChild(i),i.setAttribute("class",s),i.setAttribute("id","hotSpot_"+t+"_"+n),i.setAttribute("shape","polygon"),i.setAttribute("coords",w),this._areaNodes.push(i)},updateMarkerState:function(e,t){if(this._markers[t]){var n=this._getState(e),r,i,s=this._markers[t],o=this.get("styles").marker;r=n==="off"||!o[n]?o:o[n],i=this._mergeStyles(r,{}),i.fill.color=i.fill.colors[t%i.fill.colors.length],i.fill.alpha=i.fill.alphas[t%i.fill.alphas.length],s.set(i)}},_createMarker:function(e){var t=this.get("graphic"),n,r=this._copyObject(e);return n=t.addShape(r),n.addClass(s),n},_clearMarkerCache:function(){var e=this._markerCache.length,t=0,n;for(;t<e;++t)n=this._markerCache[t],n&&n.destroy();this._markerCache=[]},_getPlotDefaults:function(){var e={padding:{top:0,left:0,right:0,bottom:0},fill:{alphas:["1"]},border:{weight:0,alpha:1}};return e.fill.colors=this._defaultSliceColors,e.border.colors=this._defaultBorderColors,e}},{ATTRS:{type:{value:"pie"},order:{},graph:{},categoryAxis:{value:null,validator:function(e){return e!==this.get("categoryAxis")}},valueAxis:{value:null,validator:function(e){return e!==
+this.get("valueAxis")}},categoryKey:{value:null,validator:function(e){return e!==this.get("categoryKey")}},valueKey:{value:null,validator:function(e){return e!==this.get("valueKey")}},categoryDisplayName:{setter:function(e){return this._categoryDisplayName=e,e},getter:function(){return this._categoryDisplayName||this.get("categoryKey")}},valueDisplayName:{setter:function(e){return this._valueDisplayName=e,e},getter:function(){return this._valueDisplayName||this.get("valueKey")}},slices:null}})},"3.17.2",{requires:["series-base","series-plot-util"]});
diff --git a/js/yui3/series-plot-util/series-plot-util-min.js b/js/yui3/series-plot-util/series-plot-util-min.js
new file mode 100644
index 000000000..770aaa07f
--- /dev/null
+++ b/js/yui3/series-plot-util/series-plot-util-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-plot-util",function(e,t){function s(e){var t={markers:{getter:function(){return this._markers}}};this.addAttrs(t,e)}var n=e.Lang,r=e.ClassNameManager.getClassName,i=r("seriesmarker");s.prototype={_plotDefaults:null,drawPlots:function(){if(!this.get("xcoords")||this.get("xcoords").length<1)return;var e=n.isNumber,t=this._copyObject(this.get("styles").marker),r=t.width,i=t.height,s=this.get("xcoords"),o=this.get("ycoords"),u=0,a=s.length,f=o[0],l,c,h=r/2,p=i/2,d,v,m=null,g=null,y=this.get("graphOrder"),b=this.get("groupMarkers");if(b){d=[],v=[];for(;u<a;++u)d.push(parseFloat(s[u]-h)),v.push(parseFloat(o[u]-p));this._createGroupMarker({xvalues:d,yvalues:v,fill:t.fill,border:t.border,dimensions:{width:r,height:i},graphOrder:y,shape:t.shape});return}n.isArray(t.fill.color)&&(m=t.fill.color.concat()),n.isArray(t.border.color)&&(g=t.border.color.concat()),this._createMarkerCache();for(;u<a;++u){f=parseFloat(o[u]-p),l=parseFloat(s[u]-h);if(!e(l)||!e(f)){this._markers.push(null);continue}m&&(t.fill.color=m[u%m.length]),g&&(t.border.color=g[u%g.length]),t.x=l,t.y=f,c=this.getMarker(t,y,u)}this._clearMarkerCache()},_groupShapes:{circle:e.CircleGroup,rect:e.RectGroup,ellipse:e.EllipseGroup,diamond:e.DiamondGroup},_getGroupShape:function(e){return n.isString(e)&&(e=this._groupShapes[e]),e},_getPlotDefaults:function(){var e={fill:{type:"solid",alpha:1,colors:null,alphas:null,ratios:null},border:{weight:1,alpha:1},width:10,height:10,shape:"circle"};return e.fill.color=this._getDefaultColor(this.get("graphOrder"),"fill"),e.border.color=this._getDefaultColor(this.get("graphOrder"),"border"),e},_markers:null,_markerCache:null,getMarker:function(e,t,n){var r,i=e.border;e.id=this._getChart().get("id")+"_"+t+"_"+n,i.opacity=i.alpha,e.stroke=i,e.fill.opacity=e.fill.alpha;if(this._markerCache.length>0){while(!r){if(this._markerCache.length<1){r=this._createMarker(e);break}r=this._markerCache.shift()}r.set(e)}else r=this._createMarker(e);return this._markers.push(r),r},_createMarker:function(e){var t=this.get("graphic"),n,r=this._copyObject(e);return r.type=r.shape,n=t.addShape(r),n.addClass(i),n},_createMarkerCache:function(){this._groupMarker&&(this._groupMarker.destroy(),this._groupMarker=null),this._markers&&this._markers.length>0?this._markerCache=this._markers.concat():this._markerCache=[],this._markers=[]},_createGroupMarker:function(e){var t,n=this.get("markers"),r=e.border,i,s,o;if(n&&n.length>0){while(n.length>0)t=n.shift(),t.destroy();this.set("markers",[])}r.opacity=r.alpha,s={id:this._getChart().get("id")+"_"+e.graphOrder,stroke:r,fill:e.fill,dimensions:e.dimensions,xvalues:e.xvalues,yvalues:e.yvalues},s.fill.opacity=e.fill.alpha,o=this._getGroupShape(e.shape),o&&(s.type=o),e.hasOwnProperty("radius")&&!isNaN(e.radius)&&(s.dimensions.radius=e.radius),this._groupMarker&&this._groupMarker.destroy(),i=this.get("graphic"),this._groupMarker=i.addShape(s),i._redraw()},_toggleVisible:function(e){var t,n=this.get("markers"),r=0,i;if(n){i=n.length;for(;r<i;++r)t=n[r],t&&t.set("visible",e)}},_clearMarkerCache:function(){var e;while(this._markerCache.length>0)e=this._markerCache.shift(),e&&e.destroy()},updateMarkerState:function(e,t){if(this._markers&&this._markers[t]){var n,r,i=this._copyObject(this.get("styles").marker),s=this._getState(e),o=this.get("xcoords"),u=this.get("ycoords"),a=this._markers[t],f=s==="off"||!i[s]?i:i[s];f.fill.color=this._getItemColor(f.fill.color,t),f.border.color=this._getItemColor(f.border.color,t),f.stroke=f.border,a.set(f),n=f.width,r=f.height,a.set("x",o[t]-n/2),a.set("y",u[t]-r/2),a.set("visible",this.get("visible"))}},_getItemColor:function(e,t){return n.isArray(e)?e[t%e.length]:e},_setStyles:function(t){return t=this._parseMarkerStyles(t),e.Renderer.prototype._setStyles.apply(this,[t])},_parseMarkerStyles:function(e){if(e.marker){var t=this._getPlotDefaults();e.marker=this._mergeStyles(e.marker,t),e.marker.over&&(e.marker.over=this._mergeStyles(e.marker.over,e.marker)),e.marker.down&&(e.marker.down=this._mergeStyles(e.marker.down,e.marker))}return e},_getState:function(e){var t;switch(e){case"mouseout":t="off";break;case"mouseover":t="over";break;case"mouseup":t="over";break;case"mousedown":t="down"}return t},_stateSyles:null,drawSeries:function(){this.drawPlots()},_getDefaultStyles:function(){var e=this._mergeStyles({marker:this._getPlotDefaults()},this.constructor.superclass._getDefaultStyles());return e}},e.augment(s,e.Attribute),e.Plots=s},"3.17.2");
diff --git a/js/yui3/series-range/series-range-min.js b/js/yui3/series-range/series-range-min.js
new file mode 100644
index 000000000..60d4b043f
--- /dev/null
+++ b/js/yui3/series-range/series-range-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-range",function(e,t){function n(){n.superclass.constructor.apply(this,arguments)}n.NAME="rangeSeries",n.ATTRS={type:{value:"range"},ohlckeys:{valueFn:function(){return{open:"open",high:"high",low:"low",close:"close"}}}},e.extend(n,e.CartesianSeries,{_calculateMarkerWidth:function(e,t,n){var r=0;while(r<3&&n>-1)n-=1,r=Math.round(e/t-n),r%2===0&&(r-=1);return Math.max(1,r)},drawSeries:function(){var e=this.get("xcoords"),t=this.get("ycoords"),n=this.get("styles"),r=n.padding,i=e.length,s=this.get("width")-(r.left+r.right),o=this.get("ohlckeys"),u=t[o.open],a=t[o.high],f=t[o.low],l=t[o.close],c=this._calculateMarkerWidth(s,i,n.spacing),h=c/2;this._drawMarkers(e,u,a,f,l,i,c,h,n)},_getDefaultStyles:function(){var e={spacing:3};return this._mergeStyles(e,n.superclass._getDefaultStyles())}}),e.RangeSeries=n},"3.17.2",{requires:["series-cartesian"]});
diff --git a/js/yui3/series-spline-stacked/series-spline-stacked-min.js b/js/yui3/series-spline-stacked/series-spline-stacked-min.js
new file mode 100644
index 000000000..42293a899
--- /dev/null
+++ b/js/yui3/series-spline-stacked/series-spline-stacked-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-spline-stacked",function(e,t){e.StackedSplineSeries=e.Base.create("stackedSplineSeries",e.SplineSeries,[e.StackingUtil],{setAreaData:function(){e.StackedSplineSeries.superclass.setAreaData.apply(this),this._stackCoordinates.apply(this)}},{ATTRS:{type:{value:"stackedSpline"}}})},"3.17.2",{requires:["series-stacked","series-spline"]});
diff --git a/js/yui3/series-spline/series-spline-min.js b/js/yui3/series-spline/series-spline-min.js
new file mode 100644
index 000000000..3c2cbfc4e
--- /dev/null
+++ b/js/yui3/series-spline/series-spline-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-spline",function(e,t){e.SplineSeries=e.Base.create("splineSeries",e.LineSeries,[e.CurveUtil,e.Lines],{drawSeries:function(){this.drawSpline()}},{ATTRS:{type:{value:"spline"}}})},"3.17.2",{requires:["series-line","series-curve-util"]});
diff --git a/js/yui3/series-stacked/series-stacked-min.js b/js/yui3/series-stacked/series-stacked-min.js
new file mode 100644
index 000000000..bd25fc2b1
--- /dev/null
+++ b/js/yui3/series-stacked/series-stacked-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("series-stacked",function(e,t){function r(){}var n=e.Lang;r.prototype={_stacked:!0,_stackCoordinates:function(){this.get("direction")==="vertical"?this._stackXCoords():this._stackYCoords()},_stackXCoords:function(){var e=this.get("order"),t=this.get("seriesTypeCollection"),r=0,i=this.get("xcoords"),s=this.get("ycoords"),o,u,a,f,l=i.concat(),c,h,p=[],d;e>0?(c=t[e-1].get("stackedXCoords"),h=t[e-1].get("stackedYCoords"),o=c.length):o=i.length;for(;r<o;r+=1)if(n.isNumber(i[r])){if(e>0){a=c[r];if(!n.isNumber(a)){f=e;while(f>-1&&!n.isNumber(a))f-=1,f>-1?a=t[f].get("stackedXCoords")[r]:a=this._leftOrigin}i[r]=i[r]+a}l[r]=i[r]}else p.push(r);this._cleanXNaN(l,s),o=p.length;if(o>0)for(r=0;r<o;r+=1)d=p[r],u=e>0?c[d]:this._leftOrigin,l[d]=Math.max(l[d],u);this.set("stackedXCoords",l),this.set("stackedYCoords",s)},_stackYCoords:function(){var e=this.get("order"),t=this.get("graphic"),r=t.get("height"),i=this.get("seriesTypeCollection"),s=0,o=this.get("xcoords"),u=this.get("ycoords"),a,f,l,c,h=u.concat(),p,d,v=[],m;e>0?(p=i[e-1].get("stackedXCoords"),d=i[e-1].get("stackedYCoords"),a=d.length):a=u.length;for(;s<a;s+=1)if(n.isNumber(u[s])){if(e>0){l=d[s];if(!n.isNumber(l)){c=e;while(c>-1&&!n.isNumber(l))c-=1,c>-1?l=i[c].get("stackedYCoords")[s]:l=this._bottomOrigin}u[s]=l-(r-u[s])}h[s]=u[s]}else v.push(s);this._cleanYNaN(o,h),a=v.length;if(a>0)for(s=0;s<a;s+=1)m=v[s],f=e>0?d[m]:r,h[m]=Math.min(h[m],f);this.set("stackedXCoords",o),this.set("stackedYCoords",h)},_cleanXNaN:function(e,t){var r,i,s,o,u,a,f,l,c=n.isNumber,h,p=0,d=t.length;for(;p<d;++p)u=e[p],a=t[p],!c(u)&&p>0&&p<d-1&&(o=t[p-1],s=this._getPreviousValidCoordValue(e,p),l=t[p+1],f=this._getNextValidCoordValue(e,p),c(s)&&c(f)&&(h=(l-o)/(f-s),e[p]=(a+h*s-o)/h),r=NaN,i=NaN)},_getPreviousValidCoordValue:function(e,t){var r,i=n.isNumber,s=-1;while(!i(r)&&t>s)t-=1,r=e[t];return r},_getNextValidCoordValue:function(e,t){var r,i=n.isNumber,s=e.length;while(!i(r)&&t<s)t+=1,r=e[t];return r},_cleanYNaN:function(e,t){var r,i,s,o,u,a,f,l,c=n.isNumber,h,p=0,d=e.length;for(;p<d;++p)u=e[p],a=t[p],!c(a)&&p>0&&p<d-1&&(s=e[p-1],o=this._getPreviousValidCoordValue(t,p),f=e[p+1],l=this._getNextValidCoordValue(t,p),c(o)&&c(l)&&(h=(l-o)/(f-s),t[p]=o+(h*u-h*s)),r=NaN,i=NaN)}},e.StackingUtil=r},"3.17.2",{requires:["axis-stacked"]});
diff --git a/js/yui3/shim-plugin/shim-plugin-min.js b/js/yui3/shim-plugin/shim-plugin-min.js
new file mode 100644
index 000000000..d01e7abf4
--- /dev/null
+++ b/js/yui3/shim-plugin/shim-plugin-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("shim-plugin",function(e,t){function n(e){this.init(e)}n.CLASS_NAME="yui-node-shim",n.TEMPLATE='<iframe class="'+n.CLASS_NAME+'" frameborder="0" title="Node Stacking Shim"'+'src="javascript:false" tabindex="-1" role="presentation"'+'style="position:absolute; z-index:-1;"></iframe>',n.prototype={init:function(e){this._host=e.host,this.initEvents(),this.insert(),this.sync()},initEvents:function(){this._resizeHandle=this._host.on("resize",this.sync,this)},getShim:function(){return this._shim||(this._shim=e.Node.create(n.TEMPLATE,this._host.get("ownerDocument")))},insert:function(){var e=this._host;this._shim=e.insertBefore(this.getShim(),e.get("firstChild"))},sync:function(){var e=this._shim,t=this._host;e&&e.setAttrs({width:t.getStyle("width"),height:t.getStyle("height")})},destroy:function(){var e=this._shim;e&&e.remove(!0),this._resizeHandle.detach()}},n.NAME="Shim",n.NS="shim",e.namespace("Plugin"),e.Plugin.Shim=n},"3.17.2",{requires:["node-style","node-pluginhost"]});
diff --git a/js/yui3/slider-base/assets/skins/audio-light/rail-x.png b/js/yui3/slider-base/assets/skins/audio-light/rail-x.png
new file mode 100644
index 000000000..4cd29b45b
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/audio-light/rail-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/audio-light/rail-y.png b/js/yui3/slider-base/assets/skins/audio-light/rail-y.png
new file mode 100644
index 000000000..9bcd2128b
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/audio-light/rail-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/audio-light/slider-base.css b/js/yui3/slider-base/assets/skins/audio-light/slider-base.css
new file mode 100644
index 000000000..52bd5c38a
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/audio-light/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail,.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x}.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail{height:35px;background-position:0 7px}.yui3-skin-audio-light .yui3-slider-x .yui3-slider-thumb{height:35px;width:19px}.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -20px;height:13px;left:-5px;width:5px;top:7px}.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -40px;height:13px;right:-5px;width:5px;top:7px}.yui3-skin-audio-light .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-3px}.yui3-skin-audio-light .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-43px}.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail,.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y}.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail{width:35px;background-position:7px 0}.yui3-skin-audio-light .yui3-slider-y .yui3-slider-thumb{width:35px;height:19px}.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-20px 0;width:13px;top:-5px;height:5px;left:7px}.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-40px 0;width:13px;bottom:-5px;height:5px;left:7px}.yui3-skin-audio-light .yui3-slider-y .yui3-slider-thumb-image{left:-3px;top:0}.yui3-skin-audio-light .yui3-slider-y .yui3-slider-thumb-shadow{left:-43px;opacity:.15;filter:alpha(opacity=15);top:0}#yui3-css-stamp.skin-audio-light-slider-base{display:none}
diff --git a/js/yui3/slider-base/assets/skins/audio-light/slider-skin.css b/js/yui3/slider-base/assets/skins/audio-light/slider-skin.css
new file mode 100644
index 000000000..2682ae7fc
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/audio-light/slider-skin.css
@@ -0,0 +1,98 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/audio/thumb-x.png */
+
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+}
+
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail {
+ height: 35px;
+ background-position: 0 7px;
+}
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-thumb {
+ height: 35px;
+ width: 19px;
+}
+
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 13px;
+ left: -5px;
+ width: 5px;
+ top: 7px;
+}
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 13px;
+ right: -5px;
+ width: 5px;
+ top: 7px;
+}
+
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -3px;
+}
+.yui3-skin-audio-light .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -43px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/audio/thumb-y.png */
+
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+}
+
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail {
+ width: 35px;
+ background-position: 7px 0;
+}
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-thumb {
+ width: 35px;
+ height: 19px;
+}
+
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 13px;
+ top: -5px;
+ height: 5px;
+ left: 7px;
+}
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 13px;
+ bottom: -5px;
+ height: 5px;
+ left: 7px;
+}
+
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-thumb-image {
+ left: -3px;
+ top: 0;
+}
+.yui3-skin-audio-light .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -43px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
diff --git a/js/yui3/slider-base/assets/skins/audio-light/thumb-x.png b/js/yui3/slider-base/assets/skins/audio-light/thumb-x.png
new file mode 100644
index 000000000..f9f69a8ca
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/audio-light/thumb-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/audio-light/thumb-y.png b/js/yui3/slider-base/assets/skins/audio-light/thumb-y.png
new file mode 100644
index 000000000..20120d34b
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/audio-light/thumb-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/audio/rail-x.png b/js/yui3/slider-base/assets/skins/audio/rail-x.png
new file mode 100644
index 000000000..dc37bb249
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/audio/rail-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/audio/rail-y.png b/js/yui3/slider-base/assets/skins/audio/rail-y.png
new file mode 100644
index 000000000..7bb0d90e3
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/audio/rail-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/audio/slider-base.css b/js/yui3/slider-base/assets/skins/audio/slider-base.css
new file mode 100644
index 000000000..81df7a805
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/audio/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-audio .yui3-slider-x .yui3-slider-rail,.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x}.yui3-skin-audio .yui3-slider-x .yui3-slider-rail{height:35px;background-position:0 7px}.yui3-skin-audio .yui3-slider-x .yui3-slider-thumb{height:35px;width:19px}.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -20px;height:13px;left:-5px;width:5px;top:7px}.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -40px;height:13px;right:-5px;width:5px;top:7px}.yui3-skin-audio .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-3px}.yui3-skin-audio .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-43px}.yui3-skin-audio .yui3-slider-y .yui3-slider-rail,.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y}.yui3-skin-audio .yui3-slider-y .yui3-slider-rail{width:35px;background-position:7px 0}.yui3-skin-audio .yui3-slider-y .yui3-slider-thumb{width:35px;height:19px}.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-20px 0;width:13px;top:-5px;height:5px;left:7px}.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-40px 0;width:13px;bottom:-5px;height:5px;left:7px}.yui3-skin-audio .yui3-slider-y .yui3-slider-thumb-image{left:-3px;top:0}.yui3-skin-audio .yui3-slider-y .yui3-slider-thumb-shadow{left:-43px;opacity:.15;filter:alpha(opacity=15);top:0}#yui3-css-stamp.skin-audio-slider-base{display:none}
diff --git a/js/yui3/slider-base/assets/skins/audio/slider-skin.css b/js/yui3/slider-base/assets/skins/audio/slider-skin.css
new file mode 100644
index 000000000..f9dd2d545
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/audio/slider-skin.css
@@ -0,0 +1,98 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/audio/thumb-x.png */
+
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+}
+
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail {
+ height: 35px;
+ background-position: 0 7px;
+}
+.yui3-skin-audio .yui3-slider-x .yui3-slider-thumb {
+ height: 35px;
+ width: 19px;
+}
+
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 13px;
+ left: -5px;
+ width: 5px;
+ top: 7px;
+}
+.yui3-skin-audio .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 13px;
+ right: -5px;
+ width: 5px;
+ top: 7px;
+}
+
+.yui3-skin-audio .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -3px;
+}
+.yui3-skin-audio .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -43px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/audio/thumb-y.png */
+
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+}
+
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail {
+ width: 35px;
+ background-position: 7px 0;
+}
+.yui3-skin-audio .yui3-slider-y .yui3-slider-thumb {
+ width: 35px;
+ height: 19px;
+}
+
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 13px;
+ top: -5px;
+ height: 5px;
+ left: 7px;
+}
+.yui3-skin-audio .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 13px;
+ bottom: -5px;
+ height: 5px;
+ left: 7px;
+}
+
+.yui3-skin-audio .yui3-slider-y .yui3-slider-thumb-image {
+ left: -3px;
+ top: 0;
+}
+.yui3-skin-audio .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -43px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
diff --git a/js/yui3/slider-base/assets/skins/audio/thumb-x.png b/js/yui3/slider-base/assets/skins/audio/thumb-x.png
new file mode 100644
index 000000000..e0fbfb2a3
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/audio/thumb-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/audio/thumb-y.png b/js/yui3/slider-base/assets/skins/audio/thumb-y.png
new file mode 100644
index 000000000..38dfc3f00
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/audio/thumb-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule-dark/rail-x-dots.png b/js/yui3/slider-base/assets/skins/capsule-dark/rail-x-dots.png
new file mode 100644
index 000000000..453fb64f3
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule-dark/rail-x-dots.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule-dark/rail-x-lines.png b/js/yui3/slider-base/assets/skins/capsule-dark/rail-x-lines.png
new file mode 100644
index 000000000..8a3c981bb
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule-dark/rail-x-lines.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule-dark/rail-x.png b/js/yui3/slider-base/assets/skins/capsule-dark/rail-x.png
new file mode 100644
index 000000000..f1aa290ee
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule-dark/rail-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule-dark/rail-y-dots.png b/js/yui3/slider-base/assets/skins/capsule-dark/rail-y-dots.png
new file mode 100644
index 000000000..0555b19d6
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule-dark/rail-y-dots.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule-dark/rail-y-lines.png b/js/yui3/slider-base/assets/skins/capsule-dark/rail-y-lines.png
new file mode 100644
index 000000000..178aa85e1
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule-dark/rail-y-lines.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule-dark/rail-y.png b/js/yui3/slider-base/assets/skins/capsule-dark/rail-y.png
new file mode 100644
index 000000000..a47997f25
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule-dark/rail-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule-dark/slider-base.css b/js/yui3/slider-base/assets/skins/capsule-dark/slider-base.css
new file mode 100644
index 000000000..803b3590f
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule-dark/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail,.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x}.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail{height:25px}.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-thumb{height:30px;width:14px}.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -20px;height:20px;left:-2px;width:5px}.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -40px;height:20px;right:-2px;width:5px}.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-10px}.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-50px}.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail,.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y}.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail{width:25px}.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-thumb{width:30px;height:14px}.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-20px 0;width:20px;top:-2px;height:5px}.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-40px 0;width:20px;bottom:-2px;height:5px}.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-thumb-image{left:-10px;top:0}.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-thumb-shadow{left:-50px;opacity:.15;filter:alpha(opacity=15);top:0}#yui3-css-stamp.skin-capsule-dark-slider-base{display:none}
diff --git a/js/yui3/slider-base/assets/skins/capsule-dark/slider-skin.css b/js/yui3/slider-base/assets/skins/capsule-dark/slider-skin.css
new file mode 100644
index 000000000..e297d8b41
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule-dark/slider-skin.css
@@ -0,0 +1,98 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/capsule-dark/thumb-x.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/capsule-dark/thumb-x-line.png */
+
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+ /* alternate: rail-x-dots.png */
+ /* alternate: rail-x-lines.png */
+}
+
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail {
+ height: 25px;
+}
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-thumb {
+ height: 30px;
+ width: 14px;
+}
+
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -10px;
+}
+.yui3-skin-capsule-dark .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -50px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/capsule-dark/thumb-y.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/capsule-dark/thumb-y-line.png */
+
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+ /* alternate: rail-y-dots.png */
+ /* alternate: rail-y-lines.png */
+}
+
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail {
+ width: 25px;
+}
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-thumb {
+ width: 30px;
+ height: 14px;
+}
+
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-thumb-image {
+ left: -10px;
+ top: 0;
+}
+.yui3-skin-capsule-dark .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -50px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
diff --git a/js/yui3/slider-base/assets/skins/capsule-dark/thumb-x-line.png b/js/yui3/slider-base/assets/skins/capsule-dark/thumb-x-line.png
new file mode 100644
index 000000000..bfdbe0db8
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule-dark/thumb-x-line.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule-dark/thumb-x.png b/js/yui3/slider-base/assets/skins/capsule-dark/thumb-x.png
new file mode 100644
index 000000000..abed02533
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule-dark/thumb-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule-dark/thumb-y-line.png b/js/yui3/slider-base/assets/skins/capsule-dark/thumb-y-line.png
new file mode 100644
index 000000000..037dca38d
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule-dark/thumb-y-line.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule-dark/thumb-y.png b/js/yui3/slider-base/assets/skins/capsule-dark/thumb-y.png
new file mode 100644
index 000000000..e968ed42e
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule-dark/thumb-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule/rail-x-dots.png b/js/yui3/slider-base/assets/skins/capsule/rail-x-dots.png
new file mode 100644
index 000000000..b3f65d499
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule/rail-x-dots.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule/rail-x-lines.png b/js/yui3/slider-base/assets/skins/capsule/rail-x-lines.png
new file mode 100644
index 000000000..26374dfad
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule/rail-x-lines.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule/rail-x.png b/js/yui3/slider-base/assets/skins/capsule/rail-x.png
new file mode 100644
index 000000000..f257ec000
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule/rail-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule/rail-y-dots.png b/js/yui3/slider-base/assets/skins/capsule/rail-y-dots.png
new file mode 100644
index 000000000..798c2ce3c
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule/rail-y-dots.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule/rail-y-lines.png b/js/yui3/slider-base/assets/skins/capsule/rail-y-lines.png
new file mode 100644
index 000000000..798c2ce3c
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule/rail-y-lines.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule/rail-y.png b/js/yui3/slider-base/assets/skins/capsule/rail-y.png
new file mode 100644
index 000000000..d20d3c3e9
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule/rail-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule/slider-base.css b/js/yui3/slider-base/assets/skins/capsule/slider-base.css
new file mode 100644
index 000000000..03b50f955
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail,.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x;background-repeat:repeat-x}.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail{height:25px}.yui3-skin-capsule .yui3-slider-x .yui3-slider-thumb{height:30px;width:14px}.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -20px;height:20px;left:-2px;width:5px}.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -40px;height:20px;right:-2px;width:5px}.yui3-skin-capsule .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-10px}.yui3-skin-capsule .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-50px}.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail,.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y;background-repeat:repeat-y}.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail{width:25px}.yui3-skin-capsule .yui3-slider-y .yui3-slider-thumb{width:30px;height:14px}.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-20px 0;width:20px;top:-2px;height:5px}.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-40px 0;width:20px;bottom:-2px;height:5px}.yui3-skin-capsule .yui3-slider-y .yui3-slider-thumb-image{left:-10px;top:0}.yui3-skin-capsule .yui3-slider-y .yui3-slider-thumb-shadow{left:-50px;opacity:.15;filter:alpha(opacity=15);top:0}#yui3-css-stamp.skin-capsule-slider-base{display:none}
diff --git a/js/yui3/slider-base/assets/skins/capsule/slider-skin.css b/js/yui3/slider-base/assets/skins/capsule/slider-skin.css
new file mode 100644
index 000000000..97f749022
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule/slider-skin.css
@@ -0,0 +1,100 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/capsule/thumb-x.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/capsule/thumb-x-line.png */
+
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+ background-repeat: repeat-x;
+ /* alternate: rail-x-dots.png */
+ /* alternate: rail-x-lines.png */
+}
+
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail {
+ height: 25px;
+}
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-thumb {
+ height: 30px;
+ width: 14px;
+}
+
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -10px;
+}
+.yui3-skin-capsule .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -50px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/capsule/thumb-y.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/capsule/thumb-y-line.png */
+
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+ background-repeat: repeat-y;
+ /* alternate: rail-y-dots.png */
+ /* alternate: rail-y-lines.png */
+}
+
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail {
+ width: 25px;
+}
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-thumb {
+ width: 30px;
+ height: 14px;
+}
+
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-thumb-image {
+ left: -10px;
+ top: 0;
+}
+.yui3-skin-capsule .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -50px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
diff --git a/js/yui3/slider-base/assets/skins/capsule/thumb-x-line.png b/js/yui3/slider-base/assets/skins/capsule/thumb-x-line.png
new file mode 100644
index 000000000..849a4548c
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule/thumb-x-line.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule/thumb-x.png b/js/yui3/slider-base/assets/skins/capsule/thumb-x.png
new file mode 100644
index 000000000..09baa964b
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule/thumb-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule/thumb-y-line.png b/js/yui3/slider-base/assets/skins/capsule/thumb-y-line.png
new file mode 100644
index 000000000..153c65b1c
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule/thumb-y-line.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule/thumb-y-lines.png b/js/yui3/slider-base/assets/skins/capsule/thumb-y-lines.png
new file mode 100644
index 000000000..4421d7027
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule/thumb-y-lines.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/capsule/thumb-y.png b/js/yui3/slider-base/assets/skins/capsule/thumb-y.png
new file mode 100644
index 000000000..4040ee352
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/capsule/thumb-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/night/rail-x-lines.png b/js/yui3/slider-base/assets/skins/night/rail-x-lines.png
new file mode 100644
index 000000000..4e5a44070
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/night/rail-x-lines.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/night/rail-x.png b/js/yui3/slider-base/assets/skins/night/rail-x.png
new file mode 100644
index 000000000..7079de67f
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/night/rail-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/night/rail-y-lines.png b/js/yui3/slider-base/assets/skins/night/rail-y-lines.png
new file mode 100644
index 000000000..af2bc2480
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/night/rail-y-lines.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/night/rail-y.png b/js/yui3/slider-base/assets/skins/night/rail-y.png
new file mode 100644
index 000000000..bc096bc77
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/night/rail-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/night/slider-base.css b/js/yui3/slider-base/assets/skins/night/slider-base.css
new file mode 100644
index 000000000..9aa9ebb90
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/night/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-night .yui3-slider-x .yui3-slider-rail,.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x}.yui3-skin-night .yui3-slider-x .yui3-slider-rail{height:25px}.yui3-skin-night .yui3-slider-x .yui3-slider-thumb{height:26px;width:21px}.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -20px;height:20px;left:-5px;width:5px}.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -40px;height:20px;right:-5px;width:5px}.yui3-skin-night .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-10px}.yui3-skin-night .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-50px}.yui3-skin-night .yui3-slider-y .yui3-slider-rail,.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y}.yui3-skin-night .yui3-slider-y .yui3-slider-rail{width:25px}.yui3-skin-night .yui3-slider-y .yui3-slider-thumb{width:26px;height:21px}.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-20px 0;width:20px;top:-5px;height:5px}.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-40px 0;width:20px;bottom:-5px;height:5px}.yui3-skin-night .yui3-slider-y .yui3-slider-thumb-image{left:-10px;top:0}.yui3-skin-night .yui3-slider-y .yui3-slider-thumb-shadow{left:-50px;opacity:.15;filter:alpha(opacity=15);top:0}#yui3-css-stamp.skin-night-slider-base{display:none}
diff --git a/js/yui3/slider-base/assets/skins/night/slider-skin.css b/js/yui3/slider-base/assets/skins/night/slider-skin.css
new file mode 100644
index 000000000..c13f1086a
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/night/slider-skin.css
@@ -0,0 +1,94 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/sam-dark/thumb-x.png */
+
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+ /* alternate: rail-x-lines.png */
+}
+
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail {
+ height: 26px;
+}
+.yui3-skin-night .yui3-slider-x .yui3-slider-thumb {
+ height: 26px;
+ width: 21px;
+}
+
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-night .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-night .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -10px;
+}
+.yui3-skin-night .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -50px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/sam-dark/thumb-y.png */
+
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+ /* alternate: rail-y-lines.png */
+}
+
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail {
+ width: 26px;
+}
+.yui3-skin-night .yui3-slider-y .yui3-slider-thumb {
+ width: 26px;
+ height: 15px;
+}
+
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-night .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-night .yui3-slider-y .yui3-slider-thumb-image {
+ left: -10px;
+ top: 0;
+}
+.yui3-skin-night .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -50px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
diff --git a/js/yui3/slider-base/assets/skins/night/thumb-x.png b/js/yui3/slider-base/assets/skins/night/thumb-x.png
new file mode 100644
index 000000000..2045257c2
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/night/thumb-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/night/thumb-y.png b/js/yui3/slider-base/assets/skins/night/thumb-y.png
new file mode 100644
index 000000000..5ea57f08e
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/night/thumb-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/round-dark/rail-x.png b/js/yui3/slider-base/assets/skins/round-dark/rail-x.png
new file mode 100644
index 000000000..ce0ee5024
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/round-dark/rail-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/round-dark/rail-y.png b/js/yui3/slider-base/assets/skins/round-dark/rail-y.png
new file mode 100644
index 000000000..7fedc219c
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/round-dark/rail-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/round-dark/slider-base.css b/js/yui3/slider-base/assets/skins/round-dark/slider-base.css
new file mode 100644
index 000000000..7bcf5e383
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/round-dark/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail,.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x}.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail{height:25px;background-position:0 3px}.yui3-skin-round-dark .yui3-slider-x .yui3-slider-thumb{height:26px;width:24px}.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -17px;height:20px;left:-2px;width:5px}.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -37px;height:20px;right:-2px;width:5px}.yui3-skin-round-dark .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-7px}.yui3-skin-round-dark .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-47px}.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail,.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y}.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail{width:25px;background-position:3px 0}.yui3-skin-round-dark .yui3-slider-y .yui3-slider-thumb{width:26px;height:24px}.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-17px 0;width:20px;top:-2px;height:5px}.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-37px 0;width:20px;bottom:-2px;height:5px}.yui3-skin-round-dark .yui3-slider-y .yui3-slider-thumb-image{top:0;left:-7px}.yui3-skin-round-dark .yui3-slider-y .yui3-slider-thumb-shadow{top:0;left:-47px;opacity:.15;filter:alpha(opacity=15)}#yui3-css-stamp.skin-round-dark-slider-base{display:none}
diff --git a/js/yui3/slider-base/assets/skins/round-dark/slider-skin.css b/js/yui3/slider-base/assets/skins/round-dark/slider-skin.css
new file mode 100644
index 000000000..215c6704a
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/round-dark/slider-skin.css
@@ -0,0 +1,96 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/round-dark/thumb-x.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/round-dark/thumb-x-grip.png */
+
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+}
+
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail {
+ height: 25px;
+ background-position: 0 3px;
+}
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-thumb {
+ height: 26px;
+ width: 24px;
+}
+
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -17px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -37px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -7px;
+}
+.yui3-skin-round-dark .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -47px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/round-dark/thumb-y.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/round-dark/thumb-y-grip.png */
+
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+}
+
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail {
+ width: 25px;
+ background-position: 3px 0;
+}
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-thumb {
+ width: 26px;
+ height: 24px;
+}
+
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -17px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -37px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-thumb-image {
+ top: 0;
+ left: -7px;
+}
+.yui3-skin-round-dark .yui3-slider-y .yui3-slider-thumb-shadow {
+ top: 0;
+ left: -47px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+}
diff --git a/js/yui3/slider-base/assets/skins/round-dark/thumb-x-grip.png b/js/yui3/slider-base/assets/skins/round-dark/thumb-x-grip.png
new file mode 100644
index 000000000..858964b82
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/round-dark/thumb-x-grip.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/round-dark/thumb-x.png b/js/yui3/slider-base/assets/skins/round-dark/thumb-x.png
new file mode 100644
index 000000000..df181708f
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/round-dark/thumb-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/round-dark/thumb-y-grip.png b/js/yui3/slider-base/assets/skins/round-dark/thumb-y-grip.png
new file mode 100644
index 000000000..2773a4a9f
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/round-dark/thumb-y-grip.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/round-dark/thumb-y.png b/js/yui3/slider-base/assets/skins/round-dark/thumb-y.png
new file mode 100644
index 000000000..8ed649cc7
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/round-dark/thumb-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/round/rail-x.png b/js/yui3/slider-base/assets/skins/round/rail-x.png
new file mode 100644
index 000000000..62a4e92d6
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/round/rail-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/round/rail-y.png b/js/yui3/slider-base/assets/skins/round/rail-y.png
new file mode 100644
index 000000000..86f0b8ca2
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/round/rail-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/round/slider-base.css b/js/yui3/slider-base/assets/skins/round/slider-base.css
new file mode 100644
index 000000000..a3cec7072
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/round/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-round .yui3-slider-x .yui3-slider-rail,.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x}.yui3-skin-round .yui3-slider-x .yui3-slider-rail{height:25px;background-position:0 3px}.yui3-skin-round .yui3-slider-x .yui3-slider-thumb{height:26px;width:24px}.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -17px;height:20px;left:-2px;width:5px}.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -37px;height:20px;right:-2px;width:5px}.yui3-skin-round .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-7px}.yui3-skin-round .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-47px}.yui3-skin-round .yui3-slider-y .yui3-slider-rail,.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y}.yui3-skin-round .yui3-slider-y .yui3-slider-rail{width:25px;background-position:3px 0}.yui3-skin-round .yui3-slider-y .yui3-slider-thumb{width:26px;height:24px}.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-17px 0;width:20px;top:-2px;height:5px}.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-37px 0;width:20px;bottom:-2px;height:5px}.yui3-skin-round .yui3-slider-y .yui3-slider-thumb-image{top:0;left:-8px}.yui3-skin-round .yui3-slider-y .yui3-slider-thumb-shadow{top:0;left:-48px;opacity:.15;filter:alpha(opacity=15)}#yui3-css-stamp.skin-round-slider-base{display:none}
diff --git a/js/yui3/slider-base/assets/skins/round/slider-skin.css b/js/yui3/slider-base/assets/skins/round/slider-skin.css
new file mode 100644
index 000000000..f60a22340
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/round/slider-skin.css
@@ -0,0 +1,96 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/round/thumb-x.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/round/thumb-x-grip.png */
+
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+}
+
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail {
+ height: 25px;
+ background-position: 0 3px;
+}
+.yui3-skin-round .yui3-slider-x .yui3-slider-thumb {
+ height: 26px;
+ width: 24px;
+}
+
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -17px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-round .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -37px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-round .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -7px;
+}
+.yui3-skin-round .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -47px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/round/thumb-y.png */
+/* Alternate thumbUrl /build/slider-base/assets/skins/round/thumb-y-grip.png */
+
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+}
+
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail {
+ width: 25px;
+ background-position: 3px 0;
+}
+.yui3-skin-round .yui3-slider-y .yui3-slider-thumb {
+ width: 26px;
+ height: 24px;
+}
+
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -17px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-round .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -37px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-round .yui3-slider-y .yui3-slider-thumb-image {
+ top: 0;
+ left: -8px;
+}
+.yui3-skin-round .yui3-slider-y .yui3-slider-thumb-shadow {
+ top: 0;
+ left: -48px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+}
diff --git a/js/yui3/slider-base/assets/skins/round/thumb-x-grip.png b/js/yui3/slider-base/assets/skins/round/thumb-x-grip.png
new file mode 100644
index 000000000..b80b8ac79
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/round/thumb-x-grip.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/round/thumb-x.png b/js/yui3/slider-base/assets/skins/round/thumb-x.png
new file mode 100644
index 000000000..22ac2a5b9
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/round/thumb-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/round/thumb-y-grip.png b/js/yui3/slider-base/assets/skins/round/thumb-y-grip.png
new file mode 100644
index 000000000..1bf7ff3e4
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/round/thumb-y-grip.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/round/thumb-y.png b/js/yui3/slider-base/assets/skins/round/thumb-y.png
new file mode 100644
index 000000000..a8a0c6202
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/round/thumb-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/sam-dark/rail-x-lines.png b/js/yui3/slider-base/assets/skins/sam-dark/rail-x-lines.png
new file mode 100644
index 000000000..39ac404cc
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/sam-dark/rail-x-lines.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/sam-dark/rail-x.png b/js/yui3/slider-base/assets/skins/sam-dark/rail-x.png
new file mode 100644
index 000000000..bdbc07cdd
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/sam-dark/rail-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/sam-dark/rail-y-lines.png b/js/yui3/slider-base/assets/skins/sam-dark/rail-y-lines.png
new file mode 100644
index 000000000..0553faa9a
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/sam-dark/rail-y-lines.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/sam-dark/rail-y.png b/js/yui3/slider-base/assets/skins/sam-dark/rail-y.png
new file mode 100644
index 000000000..a2913427e
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/sam-dark/rail-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/sam-dark/slider-base.css b/js/yui3/slider-base/assets/skins/sam-dark/slider-base.css
new file mode 100644
index 000000000..ea12b363d
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/sam-dark/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail,.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x}.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail{height:26px}.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-thumb{height:26px;width:15px}.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -20px;height:20px;left:-2px;width:5px}.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -40px;height:20px;right:-2px;width:5px}.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-10px}.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-50px}.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail,.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y}.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail{width:26px}.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-thumb{width:26px;height:15px}.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-20px 0;width:20px;top:-2px;height:5px}.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-40px 0;width:20px;bottom:-2px;height:5px}.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-thumb-image{left:-10px;top:0}.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-thumb-shadow{left:-50px;opacity:.15;filter:alpha(opacity=15);top:0}#yui3-css-stamp.skin-sam-dark-slider-base{display:none}
diff --git a/js/yui3/slider-base/assets/skins/sam-dark/slider-skin.css b/js/yui3/slider-base/assets/skins/sam-dark/slider-skin.css
new file mode 100644
index 000000000..60183f52a
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/sam-dark/slider-skin.css
@@ -0,0 +1,94 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/sam-dark/thumb-x.png */
+
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+ /* alternate: rail-x-lines.png */
+}
+
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail {
+ height: 26px;
+}
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-thumb {
+ height: 26px;
+ width: 15px;
+}
+
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -10px;
+}
+.yui3-skin-sam-dark .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -50px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/sam-dark/thumb-y.png */
+
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+ /* alternate: rail-y-lines.png */
+}
+
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail {
+ width: 26px;
+}
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-thumb {
+ width: 26px;
+ height: 15px;
+}
+
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-thumb-image {
+ left: -10px;
+ top: 0;
+}
+.yui3-skin-sam-dark .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -50px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
diff --git a/js/yui3/slider-base/assets/skins/sam-dark/thumb-x.png b/js/yui3/slider-base/assets/skins/sam-dark/thumb-x.png
new file mode 100644
index 000000000..3526ffc15
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/sam-dark/thumb-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/sam-dark/thumb-y.png b/js/yui3/slider-base/assets/skins/sam-dark/thumb-y.png
new file mode 100644
index 000000000..9aad18b57
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/sam-dark/thumb-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/sam/rail-x-lines.png b/js/yui3/slider-base/assets/skins/sam/rail-x-lines.png
new file mode 100644
index 000000000..45c84288f
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/sam/rail-x-lines.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/sam/rail-x.png b/js/yui3/slider-base/assets/skins/sam/rail-x.png
new file mode 100644
index 000000000..b99e1049e
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/sam/rail-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/sam/rail-y-lines.png b/js/yui3/slider-base/assets/skins/sam/rail-y-lines.png
new file mode 100644
index 000000000..841c97088
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/sam/rail-y-lines.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/sam/rail-y.png b/js/yui3/slider-base/assets/skins/sam/rail-y.png
new file mode 100644
index 000000000..2bec78ab6
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/sam/rail-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/sam/slider-base.css b/js/yui3/slider-base/assets/skins/sam/slider-base.css
new file mode 100644
index 000000000..9664d8892
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/sam/slider-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,.yui3-slider-rail{display:-moz-inline-stack;display:inline-block;*display:inline;zoom:1;vertical-align:middle}.yui3-slider-content{position:relative;display:block}.yui3-slider-rail{position:relative}.yui3-slider-rail-cap-top,.yui3-slider-rail-cap-left,.yui3-slider-rail-cap-bottom,.yui3-slider-rail-cap-right,.yui3-slider-thumb,.yui3-slider-thumb-image,.yui3-slider-thumb-shadow{position:absolute}.yui3-slider-thumb{overflow:hidden}.yui3-skin-sam .yui3-slider-x .yui3-slider-rail,.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-left,.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-right{background-image:url(rail-x.png);background-repeat:repeat-x}.yui3-skin-sam .yui3-slider-x .yui3-slider-rail{height:26px}.yui3-skin-sam .yui3-slider-x .yui3-slider-thumb{height:26px;width:15px}.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-left{background-position:0 -20px;height:20px;left:-2px;width:5px}.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-right{background-position:0 -40px;height:20px;right:-2px;width:5px}.yui3-skin-sam .yui3-slider-x .yui3-slider-thumb-image{left:0;top:-10px}.yui3-skin-sam .yui3-slider-x .yui3-slider-thumb-shadow{left:0;opacity:.15;filter:alpha(opacity=15);top:-50px}.yui3-skin-sam .yui3-slider-y .yui3-slider-rail,.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-top,.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-bottom{background-image:url(rail-y.png);background-repeat:repeat-y}.yui3-skin-sam .yui3-slider-y .yui3-slider-rail{width:26px}.yui3-skin-sam .yui3-slider-y .yui3-slider-thumb{width:26px;height:15px}.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-top{background-position:-20px 0;width:20px;top:-2px;height:5px}.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-bottom{background-position:-40px 0;width:20px;bottom:-2px;height:5px}.yui3-skin-sam .yui3-slider-y .yui3-slider-thumb-image{left:-10px;top:0}.yui3-skin-sam .yui3-slider-y .yui3-slider-thumb-shadow{left:-50px;opacity:.15;filter:alpha(opacity=15);top:0}#yui3-css-stamp.skin-sam-slider-base{display:none}
diff --git a/js/yui3/slider-base/assets/skins/sam/slider-skin.css b/js/yui3/slider-base/assets/skins/sam/slider-skin.css
new file mode 100644
index 000000000..db8b4ece5
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/sam/slider-skin.css
@@ -0,0 +1,94 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* Horizontal Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/sam/thumb-x.png */
+
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail,
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-left,
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-image: url(rail-x.png);
+ background-repeat: repeat-x;
+ /* alternate: rail-x-lines.png */
+}
+
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail {
+ height: 26px;
+}
+.yui3-skin-sam .yui3-slider-x .yui3-slider-thumb {
+ height: 26px;
+ width: 15px;
+}
+
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-left {
+ background-position: 0 -20px;
+ height: 20px;
+ left: -2px;
+ width: 5px;
+}
+.yui3-skin-sam .yui3-slider-x .yui3-slider-rail-cap-right {
+ background-position: 0 -40px;
+ height: 20px;
+ right: -2px;
+ width: 5px;
+}
+
+.yui3-skin-sam .yui3-slider-x .yui3-slider-thumb-image {
+ left: 0;
+ top: -10px;
+}
+.yui3-skin-sam .yui3-slider-x .yui3-slider-thumb-shadow {
+ left: 0;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: -50px;
+}
+
+/* Vertical Slider */
+
+/* Use thumbUrl /build/slider-base/assets/skins/sam/thumb-y.png */
+
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail,
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-top,
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-image: url(rail-y.png);
+ background-repeat: repeat-y;
+ /* alternate: rail-y-lines.png */
+}
+
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail {
+ width: 26px;
+}
+.yui3-skin-sam .yui3-slider-y .yui3-slider-thumb {
+ width: 26px;
+ height: 15px;
+}
+
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-top {
+ background-position: -20px 0;
+ width: 20px;
+ top: -2px;
+ height: 5px;
+}
+.yui3-skin-sam .yui3-slider-y .yui3-slider-rail-cap-bottom {
+ background-position: -40px 0;
+ width: 20px;
+ bottom: -2px;
+ height: 5px;
+}
+
+.yui3-skin-sam .yui3-slider-y .yui3-slider-thumb-image {
+ left: -10px;
+ top: 0;
+}
+.yui3-skin-sam .yui3-slider-y .yui3-slider-thumb-shadow {
+ left: -50px;
+ opacity: 0.15;
+ filter: alpha(opacity=15);
+ top: 0;
+}
diff --git a/js/yui3/slider-base/assets/skins/sam/thumb-x.png b/js/yui3/slider-base/assets/skins/sam/thumb-x.png
new file mode 100644
index 000000000..4d4bcbd48
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/sam/thumb-x.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/skins/sam/thumb-y.png b/js/yui3/slider-base/assets/skins/sam/thumb-y.png
new file mode 100644
index 000000000..0b17a0ed2
--- /dev/null
+++ b/js/yui3/slider-base/assets/skins/sam/thumb-y.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/slider-base-core.css b/js/yui3/slider-base/assets/slider-base-core.css
new file mode 100644
index 000000000..cab6ff331
--- /dev/null
+++ b/js/yui3/slider-base/assets/slider-base-core.css
@@ -0,0 +1,38 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,
+.yui3-slider-rail {
+ /* xbrowser inline-block styles */
+ display: -moz-inline-stack; /* FF2 */
+ display: inline-block;
+ *display: inline; /* IE 7- (with zoom) */
+ zoom: 1;
+ vertical-align: middle;
+}
+
+.yui3-slider-content {
+ position: relative;
+ display: block;
+}
+.yui3-slider-rail {
+ position: relative;
+}
+
+.yui3-slider-rail-cap-top,
+.yui3-slider-rail-cap-left,
+.yui3-slider-rail-cap-bottom,
+.yui3-slider-rail-cap-right,
+.yui3-slider-thumb,
+.yui3-slider-thumb-image,
+.yui3-slider-thumb-shadow {
+ position: absolute;
+}
+
+.yui3-slider-thumb {
+ overflow: hidden;
+}
diff --git a/js/yui3/slider-base/assets/slider-core.css b/js/yui3/slider-base/assets/slider-core.css
new file mode 100644
index 000000000..cab6ff331
--- /dev/null
+++ b/js/yui3/slider-base/assets/slider-core.css
@@ -0,0 +1,38 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-slider,
+.yui3-slider-rail {
+ /* xbrowser inline-block styles */
+ display: -moz-inline-stack; /* FF2 */
+ display: inline-block;
+ *display: inline; /* IE 7- (with zoom) */
+ zoom: 1;
+ vertical-align: middle;
+}
+
+.yui3-slider-content {
+ position: relative;
+ display: block;
+}
+.yui3-slider-rail {
+ position: relative;
+}
+
+.yui3-slider-rail-cap-top,
+.yui3-slider-rail-cap-left,
+.yui3-slider-rail-cap-bottom,
+.yui3-slider-rail-cap-right,
+.yui3-slider-thumb,
+.yui3-slider-thumb-image,
+.yui3-slider-thumb-shadow {
+ position: absolute;
+}
+
+.yui3-slider-thumb {
+ overflow: hidden;
+}
diff --git a/js/yui3/slider-base/assets/thumb-x-oblong-dark.png b/js/yui3/slider-base/assets/thumb-x-oblong-dark.png
new file mode 100644
index 000000000..bc0aa14ce
--- /dev/null
+++ b/js/yui3/slider-base/assets/thumb-x-oblong-dark.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/thumb-x-oblong.png b/js/yui3/slider-base/assets/thumb-x-oblong.png
new file mode 100644
index 000000000..670ba1ea1
--- /dev/null
+++ b/js/yui3/slider-base/assets/thumb-x-oblong.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/thumb-x-oblong2-dark.png b/js/yui3/slider-base/assets/thumb-x-oblong2-dark.png
new file mode 100644
index 000000000..20f126029
--- /dev/null
+++ b/js/yui3/slider-base/assets/thumb-x-oblong2-dark.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/thumb-x-oblong2.png b/js/yui3/slider-base/assets/thumb-x-oblong2.png
new file mode 100644
index 000000000..76e34e60a
--- /dev/null
+++ b/js/yui3/slider-base/assets/thumb-x-oblong2.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/thumb-y-oblong-dark.png b/js/yui3/slider-base/assets/thumb-y-oblong-dark.png
new file mode 100644
index 000000000..a0eed7087
--- /dev/null
+++ b/js/yui3/slider-base/assets/thumb-y-oblong-dark.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/thumb-y-oblong.png b/js/yui3/slider-base/assets/thumb-y-oblong.png
new file mode 100644
index 000000000..e63c8d7d8
--- /dev/null
+++ b/js/yui3/slider-base/assets/thumb-y-oblong.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/thumb-y-oblong2-dark.png b/js/yui3/slider-base/assets/thumb-y-oblong2-dark.png
new file mode 100644
index 000000000..e91ffb7b3
--- /dev/null
+++ b/js/yui3/slider-base/assets/thumb-y-oblong2-dark.png
Binary files differ
diff --git a/js/yui3/slider-base/assets/thumb-y-oblong2.png b/js/yui3/slider-base/assets/thumb-y-oblong2.png
new file mode 100644
index 000000000..89a466727
--- /dev/null
+++ b/js/yui3/slider-base/assets/thumb-y-oblong2.png
Binary files differ
diff --git a/js/yui3/slider-base/slider-base-min.js b/js/yui3/slider-base/slider-base-min.js
new file mode 100644
index 000000000..f5fcd3366
--- /dev/null
+++ b/js/yui3/slider-base/slider-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("slider-base",function(e,t){function r(){r.superclass.constructor.apply(this,arguments)}var n=e.Attribute.INVALID_VALUE;e.SliderBase=e.extend(r,e.Widget,{initializer:function(){this.axis=this.get("axis"),this._key={dim:this.axis==="y"?"height":"width",minEdge:this.axis==="y"?"top":"left",maxEdge:this.axis==="y"?"bottom":"right",xyIndex:this.axis==="y"?1:0},this.publish("thumbMove",{defaultFn:this._defThumbMoveFn,queuable:!0})},renderUI:function(){var e=this.get("contentBox");this.rail=this.renderRail(),this._uiSetRailLength(this.get("length")),this.thumb=this.renderThumb(),this.rail.appendChild(this.thumb),e.appendChild(this.rail),e.addClass(this.getClassName(this.axis))},renderRail:function(){var t=this.getClassName("rail","cap",this._key.minEdge),n=this.getClassName("rail","cap",this._key.maxEdge);return e.Node.create(e.Lang.sub(this.RAIL_TEMPLATE,{railClass:this.getClassName("rail"),railMinCapClass:t,railMaxCapClass:n}))},_uiSetRailLength:function(e){this.rail.setStyle(this._key.dim,e)},renderThumb:function(){this._initThumbUrl();var t=this.get("thumbUrl");return e.Node.create(e.Lang.sub(this.THUMB_TEMPLATE,{thumbClass:this.getClassName("thumb"),thumbShadowClass:this.getClassName("thumb","shadow"),thumbImageClass:this.getClassName("thumb","image"),thumbShadowUrl:t,thumbImageUrl:t,thumbAriaLabelId:this.getClassName("label",e.guid())}))},_onThumbClick:function(e){this.thumb.focus()},bindUI:function(){var t=this.get("boundingBox"),n=e.UA.opera?"press:":"down:",r=n+"38,40,33,34,35,36",i=n+"37,39",s=n+"37+meta,39+meta";t.on("key",this._onDirectionKey,r,this),t.on("key",this._onLeftRightKey,i,this),t.on("key",this._onLeftRightKeyMeta,s,this),this.thumb.on("click",this._onThumbClick,this),this._bindThumbDD(),this._bindValueLogic(),this.after("disabledChange",this._afterDisabledChange),this.after("lengthChange",this._afterLengthChange)},_incrMinor:function(){this.set("value",this.get("value")+this.get("minorStep"))},_decrMinor:function(){this.set("value",this.get("value")-this.get("minorStep"))},_incrMajor:function(){this.set("value",this.get("value")+this.get("majorStep"))},_decrMajor:function(){this.set("value",this.get("value")-this.get("majorStep"))},_setToMin:function(e){this.set("value",this.get("min"))},_setToMax:function(e){this.set("value",this.get("max"))},_onDirectionKey:function(e){e.preventDefault();if(this.get("disabled")===!1)switch(e.charCode){case 38:this._incrMinor();break;case 40:this._decrMinor();break;case 36:this._setToMin();break;case 35:this._setToMax();break;case 33:this._incrMajor();break;case 34:this._decrMajor()}},_onLeftRightKey:function(e){e.preventDefault();if(this.get("disabled")===!1)switch(e.charCode){case 37:this._decrMinor();break;case 39:this._incrMinor()}},_onLeftRightKeyMeta:function(e){e.preventDefault();if(this.get("disabled")===!1)switch(e.charCode){case 37:this._setToMin();break;case 39:this._setToMax()}},_bindThumbDD:function(){var t={constrain:this.rail};t["stick"+this.axis.toUpperCase()]=!0,this._dd=new e.DD.Drag({node:this.thumb,bubble:!1,on:{"drag:start":e.bind(this._onDragStart,this)},after:{"drag:drag":e.bind(this._afterDrag,this),"drag:end":e.bind(this._afterDragEnd,this)}}),this._dd.plug(e.Plugin.DDConstrained,t)},_bindValueLogic:function(){},_uiMoveThumb:function(e,t){this.thumb&&(this.thumb.setStyle(this._key.minEdge,e+"px"),t||(t={}),t.offset=e,this.fire("thumbMove",t))},_onDragStart:function(e){this.fire("slideStart",{ddEvent:e,originEvent:e})},_afterDrag:function(e){var t=e.info.xy[this._key.xyIndex],n=e.target.con._regionCache[this._key.minEdge];this.fire("thumbMove",{offset:t-n,ddEvent:e,originEvent:e})},_afterDragEnd:function(e){this.fire("slideEnd",{ddEvent:e,originEvent:e})},_afterDisabledChange:function(e){this._dd.set("lock",e.newVal)},_afterLengthChange:function(e){this.get("rendered")&&(this._uiSetRailLength(e.newVal),this.syncUI())},syncUI:function(){this._dd.con.resetCache(),this._syncThumbPosition(),this.thumb.set("aria-valuemin",this.get("min")),this.thumb.set("aria-valuemax",this.get("max")),this._dd.set("lock",this.get("disabled"))},_syncThumbPosition:function(){},_setAxis:function(e){return e=(e+"").toLowerCase(),e==="x"||e==="y"?e:n},_setLength:function(e){e=(e+"").toLowerCase();var t=parseFloat(e,10),r=e.replace(/[\d\.\-]/g,"")||this.DEF_UNIT;return t>0?t+r:n},_initThumbUrl:function(){if(!this.get("thumbUrl")){var t=this.getSkinName()||"sam",n=e.config.base;n.indexOf("http://yui.yahooapis.com/combo")===0&&(n="http://yui.yahooapis.com/"+e.version+"/build/"),this.set("thumbUrl",n+"slider-base/assets/skins/"+t+"/thumb-"+this.axis+".png")}},BOUNDING_TEMPLATE:"<span></span>",CONTENT_TEMPLATE:"<span></span>",RAIL_TEMPLATE:'<span class="{railClass}"><span class="{railMinCapClass}"></span><span class="{railMaxCapClass}"></span></span>',THUMB_TEMPLATE:'<span class="{thumbClass}" aria-labelledby="{thumbAriaLabelId}" aria-valuetext="" aria-valuemax="" aria-valuemin="" aria-valuenow="" role="slider" tabindex="0"><img src="{thumbShadowUrl}" alt="Slider thumb shadow" class="{thumbShadowClass}"><img src="{thumbImageUrl}" alt="Slider thumb" class="{thumbImageClass}"></span>'},{NAME:"sliderBase",ATTRS:{axis:{value:"x",writeOnce:!0,setter:"_setAxis",lazyAdd:!1},length:{value:"150px",setter:"_setLength"},thumbUrl:{value:null,validator:e.Lang.isString}}})},"3.17.2",{requires:["widget","dd-constrain","event-key"],skinnable:!0});
diff --git a/js/yui3/slider-value-range/assets/thumb-x-oblong-dark.png b/js/yui3/slider-value-range/assets/thumb-x-oblong-dark.png
new file mode 100644
index 000000000..bc0aa14ce
--- /dev/null
+++ b/js/yui3/slider-value-range/assets/thumb-x-oblong-dark.png
Binary files differ
diff --git a/js/yui3/slider-value-range/assets/thumb-x-oblong.png b/js/yui3/slider-value-range/assets/thumb-x-oblong.png
new file mode 100644
index 000000000..670ba1ea1
--- /dev/null
+++ b/js/yui3/slider-value-range/assets/thumb-x-oblong.png
Binary files differ
diff --git a/js/yui3/slider-value-range/assets/thumb-x-oblong2-dark.png b/js/yui3/slider-value-range/assets/thumb-x-oblong2-dark.png
new file mode 100644
index 000000000..20f126029
--- /dev/null
+++ b/js/yui3/slider-value-range/assets/thumb-x-oblong2-dark.png
Binary files differ
diff --git a/js/yui3/slider-value-range/assets/thumb-x-oblong2.png b/js/yui3/slider-value-range/assets/thumb-x-oblong2.png
new file mode 100644
index 000000000..76e34e60a
--- /dev/null
+++ b/js/yui3/slider-value-range/assets/thumb-x-oblong2.png
Binary files differ
diff --git a/js/yui3/slider-value-range/assets/thumb-y-oblong-dark.png b/js/yui3/slider-value-range/assets/thumb-y-oblong-dark.png
new file mode 100644
index 000000000..a0eed7087
--- /dev/null
+++ b/js/yui3/slider-value-range/assets/thumb-y-oblong-dark.png
Binary files differ
diff --git a/js/yui3/slider-value-range/assets/thumb-y-oblong.png b/js/yui3/slider-value-range/assets/thumb-y-oblong.png
new file mode 100644
index 000000000..e63c8d7d8
--- /dev/null
+++ b/js/yui3/slider-value-range/assets/thumb-y-oblong.png
Binary files differ
diff --git a/js/yui3/slider-value-range/assets/thumb-y-oblong2-dark.png b/js/yui3/slider-value-range/assets/thumb-y-oblong2-dark.png
new file mode 100644
index 000000000..e91ffb7b3
--- /dev/null
+++ b/js/yui3/slider-value-range/assets/thumb-y-oblong2-dark.png
Binary files differ
diff --git a/js/yui3/slider-value-range/assets/thumb-y-oblong2.png b/js/yui3/slider-value-range/assets/thumb-y-oblong2.png
new file mode 100644
index 000000000..89a466727
--- /dev/null
+++ b/js/yui3/slider-value-range/assets/thumb-y-oblong2.png
Binary files differ
diff --git a/js/yui3/slider-value-range/slider-value-range-min.js b/js/yui3/slider-value-range/slider-value-range-min.js
new file mode 100644
index 000000000..003b20e8b
--- /dev/null
+++ b/js/yui3/slider-value-range/slider-value-range-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("slider-value-range",function(e,t){function o(){this._initSliderValueRange()}var n="min",r="max",i="value",s=Math.round;e.SliderValueRange=e.mix(o,{prototype:{_factor:1,_initSliderValueRange:function(){},_bindValueLogic:function(){this.after({minChange:this._afterMinChange,maxChange:this._afterMaxChange,valueChange:this._afterValueChange})},_syncThumbPosition:function(){this._calculateFactor(),this._setPosition(this.get(i))},_calculateFactor:function(){var e=this.get("length"),t=this.thumb.getStyle(this._key.dim),i=this.get(n),s=this.get(r);e=parseFloat(e)||150,t=parseFloat(t)||15,this._factor=(s-i)/(e-t)},_defThumbMoveFn:function(e){e.source!=="set"&&this.set(i,this._offsetToValue(e.offset))},_offsetToValue:function(e){var t=s(e*this._factor)+this.get(n);return s(this._nearestValue(t))},_valueToOffset:function(e){var t=s((e-this.get(n))/this._factor);return t},getValue:function(){return this.get(i)},setValue:function(e){return this.set(i,e)},_afterMinChange:function(e){this._verifyValue(),this._syncThumbPosition()},_afterMaxChange:function(e){this._verifyValue(),this._syncThumbPosition()},_verifyValue:function(){var e=this.get(i),t=this._nearestValue(e);e!==t&&this.set(i,t)},_afterValueChange:function(e){var t=e.newVal;this._setPosition(t,{source:"set"})},_setPosition:function(e,t){this._uiMoveThumb(this._valueToOffset(e),t),this.thumb.set("aria-valuenow",e),this.thumb.set("aria-valuetext",e)},_validateNewMin:function(t){return e.Lang.isNumber(t)},_validateNewMax:function(t){return e.Lang.isNumber(t)},_setNewValue:function(t){return e.Lang.isNumber(t)?s(this._nearestValue(t)):e.Attribute.INVALID_VALUE},_nearestValue:function(e){var t=this.get(n),i=this.get(r),s;return s=i>t?i:t,t=i>t?t:i,i=s,e<t?t:e>i?i:e}},ATTRS:{min:{value:0,validator:"_validateNewMin"},max:{value:100,validator:"_validateNewMax"},minorStep:{value:1},majorStep:{value:10},value:{value:0,setter:"_setNewValue"}}},!0)},"3.17.2",{requires:["slider-base"]});
diff --git a/js/yui3/sortable-scroll/sortable-scroll-min.js b/js/yui3/sortable-scroll/sortable-scroll-min.js
new file mode 100644
index 000000000..424d76361
--- /dev/null
+++ b/js/yui3/sortable-scroll/sortable-scroll-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("sortable-scroll",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)};e.extend(n,e.Base,{initializer:function(){var t=this.get("host");t.plug(e.Plugin.DDNodeScroll,{node:t.get("container")}),t.delegate.on("drop:over",function(t){this.dd.nodescroll&&t.drag.nodescroll&&t.drag.nodescroll.set("parentScroll",e.one(this.get("container")))})}},{ATTRS:{host:{value:""}},NAME:"SortScroll",NS:"scroll"}),e.namespace("Y.Plugin"),e.Plugin.SortableScroll=n},"3.17.2",{requires:["dd-scroll","sortable"]});
diff --git a/js/yui3/sortable/sortable-min.js b/js/yui3/sortable/sortable-min.js
new file mode 100644
index 000000000..7845bc157
--- /dev/null
+++ b/js/yui3/sortable/sortable-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("sortable",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)},r="currentNode",i="opacityNode",s="container",o="id",u="zIndex",a="opacity",f="parentNode",l="nodes",c="node";e.extend(n,e.Base,{delegate:null,drop:null,initializer:function(){var t="sortable-"+e.guid(),r={container:this.get(s),nodes:this.get(l),target:!0,invalid:this.get("invalid"),dragConfig:{groups:[t]}},i;this.get("handles")&&(r.handles=this.get("handles")),i=new e.DD.Delegate(r),this.set(o,t),i.dd.plug(e.Plugin.DDProxy,{moveOnEnd:!1,cloneNode:!0}),this.drop=new e.DD.Drop({node:this.get(s),bubbleTarget:i,groups:i.dd.get("groups")}),this.drop.on("drop:enter",e.bind(this._onDropEnter,this)),i.on({"drag:start":e.bind(this._onDragStart,this),"drag:end":e.bind(this._onDragEnd,this),"drag:over":e.bind(this._onDragOver,this),"drag:drag":e.bind(this._onDrag,this)}),this.delegate=i,n.reg(this,t)},_up:null,_y:null,_onDrag:function(e){e.pageY<this._y?this._up=!0:e.pageY>this._y&&(this._up=!1),this._y=e.pageY},_onDropEnter:function(e){var t=e.drop.get(c),n=e.drag.get(c);!t.test(this.get(l))&&!n.get(f).compareTo(t)&&t.append(n)},_onDragOver:function(t){if(!t.drop.get(c).test(this.get(l)))return;if(t.drag.get(c)===t.drop.get(c))return;if(t.drag.get(c).contains(t.drop.get(c)))return;var n=!1,r,i,u,a,h,p=this.get("moveType").toLowerCase();t.drag.get(c).get(f).contains(t.drop.get(c))&&(n=!0),n&&p==="move"&&(p="insert");switch(p){case"insert":r=this._up?"before":"after",h=t.drop.get(c),e.Sortable._test(h,this.get(s))?h.append(t.drag.get(c)):h.insert(t.drag.get(c),r);break;case"swap":e.DD.DDM.swapNode(t.drag,t.drop);break;case"move":case"copy":a=e.Sortable.getSortable(t.drop.get(c).get(f));if(!a)return;e.DD.DDM.getDrop(t.drag.get(c)).addToGroup(a.get(o)),n?e.DD.DDM.swapNode(t.drag,t.drop):(this.get("moveType")==="copy"&&(i=t.drag.get(c),u=i.cloneNode(!0),u.set(o,""),t.drag.set(c,u),a.delegate.createDrop(u,[a.get(o)]),i.setStyles({top:"",left:""})),t.drop.get(c).insert(t.drag.get(c),"before"))}this.fire(p,{same:n,drag:t.drag,drop:t.drop}),this.fire("moved",{same:n,drag:t.drag,drop:t.drop})},_onDragStart:function(){var e=this.delegate,t=e.get("lastNode");t&&t.getDOMNode()&&t.setStyle(u,""),e.get(this.get(i)).setStyle(a,this.get(a)),e.get(r).setStyle(u,"999")},_onDragEnd:function(){this.delegate.get(this.get(i)).setStyle(a,1),this.delegate.get(r).setStyle(u,""),this.delegate.get(r).setStyles({top:"",left:""}),this.sync()},plug:function(e,t){return e&&e.NAME.substring(0,4).toLowerCase()==="sort"?this.constructor.superclass.plug.call(this,e,t):this.delegate.dd.plug(e,t),this},sync:function(){return this.delegate.syncTargets(),this},destructor:function(){this.drop.destroy(),this.delegate.destroy(),n.unreg(this,this.get(o))},join:function(t,n){if(t instanceof e.Sortable){n||(n="full"),n=n.toLowerCase();var r="_join_"+n;return this[r]&&this[r](t),this}return e.error("Sortable: join needs a Sortable Instance"),this},_join_none:function(e){this.delegate.dd.removeFromGroup(e.get(o)),e.delegate.dd.removeFromGroup(this.get(o))},_join_full:function(e){this.delegate.dd.addToGroup(e.get(o)),e.delegate.dd.addToGroup(this.get(o))},_join_outer:function(e){this.delegate.dd.addToGroup(e.get(o))},_join_inner:function(e){e.delegate.dd.addToGroup(this.get(o))},getOrdering:function(t){var n=[];return e.Lang.isFunction(t)||(t=function(e){return e}),e.one(this.get(s)).all(this.get(l)).each(function(e){n.push(t(e))}),n}},{NAME:"sortable",ATTRS:{handles:{value:!1},container:{value:"body"},nodes:{value:".dd-draggable"},opacity:{value:".75"},opacityNode:{value:"currentNode"},id:{value:null},moveType:{value:"insert"},invalid:{value:""}},_sortables:{},_test:function(t,n){var r;return n instanceof e.Node?r=n===t:r=t.test(n),r},getSortable:function(t){var n=null,r=null;return t=e.one(t),r=t.get(o),r&&e.Sortable._sortables[r]?e.Sortable._sortables[r]:(e.Object.each(e.Sortable._sortables,function(r){e.Sortable._test(t,r.get(s))&&(n=r)}),n)},reg:function(t,n){n||(n=t.get(o)),e.Sortable._sortables[n]=t},unreg:function(t,r){r||(r=t.get(o));if(r&&e.Sortable._sortables[r]){delete e.Sortable._sortables[r];return}e.Object.each(e.Sortable._sortables,function(e,r){e===t&&delete n._sortables[r]})}}),e.Sortable=n},"3.17.2",{requires:["dd-delegate","dd-drop-plugin","dd-proxy"]});
diff --git a/js/yui3/stylesheet/stylesheet-min.js b/js/yui3/stylesheet/stylesheet-min.js
new file mode 100644
index 000000000..28bdb84a1
--- /dev/null
+++ b/js/yui3/stylesheet/stylesheet-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("stylesheet",function(e,t){function v(t,r){var o,a,f,l={},h,p,m,g,y,b;if(!e.instanceOf(this,v))return new v(t,r);if(t){if(e.Node&&t instanceof e.Node)a=t._node;else if(t.nodeName)a=t;else if(s(t)){if(t&&u[t])return u[t];a=n.getElementById(t.replace(/^#/,d))}if(a&&u[e.stamp(a)])return u[e.stamp(a)]}if(!a||!/^(?:style|link)$/i.test(a.nodeName))a=n.createElement("style"),a.type="text/css";s(t)&&(t.indexOf("{")!=-1?a.styleSheet?a.styleSheet.cssText=t:a.appendChild(n.createTextNode(t)):r||(r=t));if(!a.parentNode||a.parentNode.nodeName.toLowerCase()!=="head")o=(a.ownerDocument||n).getElementsByTagName("head")[0],o.appendChild(a);f=a.sheet||a.styleSheet,h=f&&"cssRules"in f?"cssRules":"rules",m="deleteRule"in f?function(e){f.deleteRule(e)}:function(e){f.removeRule(e)},p="insertRule"in f?function(e,t,n){f.insertRule(e+" {"+t+"}",n)}:function(e,t,n){f.addRule(e,t,n)};for(g=f[h].length-1;g>=0;--g)y=f[h][g],b=y.selectorText,l[b]?(l[b].style.cssText+=";"+y.style.cssText,m(g)):l[b]=y;v.register(e.stamp(a),this),r&&v.register(r,this),e.mix(this,{getId:function(){return e.stamp(a)},enable:function(){return f.disabled=!1,this},disable:function(){return f.disabled=!0,this},isEnabled:function(){return!f.disabled},set:function(e,t){var n=l[e],r=e.split(/\s*,\s*/),i,s;if(r.length>1){for(i=r.length-1;i>=0;--i)this.set(r[i],t);return this}return v.isValidSelector(e)?(n?n.style.cssText=v.toCssText(t,n.style.cssText):(s=f[h].length,t=v.toCssText(t),t&&(p(e,t,s),l[e]=f[h][s])),this):this},unset:function(t,n){var r=l[t],s=t.split(/\s*,\s*/),o=!n,u,a;if(s.length>1){for(a=s.length-1;a>=0;--a)this.unset(s[a],n);return this}if(r){if(!o){n=e.Array(n),i.cssText=r.style.cssText;for(a=n.length-1;a>=0;--a)c(i,n[a]);i.cssText?r.style.cssText=i.cssText:o=!0}if(o){u=f[h];for(a=u.length-1;a>=0;--a)if(u[a]===r){delete l[t],m(a);break}}}return this},getCssText:function(e){var t,n,r;if(s(e))return t=l[e.split(/\s*,\s*/)[0]],t?t.style.cssText:null;n=[];for(r in l)l.hasOwnProperty(r)&&(t=l[r],n.push(t.selectorText+" {"+t.style.cssText+"}"));return n.join("\n")}})}var n=e.config.doc,r=n.createElement("p"),i=r.style,s=e.Lang.isString,o={},u={},a="cssFloat"in i?"cssFloat":"styleFloat",f,l,c,h="opacity",p="float",d="";l=h in i?function(e){e.opacity=d}:function(e){e.filter=d},i.border="1px solid red",i.border=d,c=i.borderLeft?function(e,t){var n;t!==a&&t.toLowerCase().indexOf(p)!=-1&&(t=a);if(s(e[t]))switch(t){case h:case"filter":l(e);break;case"font":e.font=e.fontStyle=e.fontVariant=e.fontWeight=e.fontSize=e.lineHeight=e.fontFamily=d;break;default:for(n in e)n.indexOf(t)===0&&(e[n]=d)}}:function(e,t){t!==a&&t.toLowerCase().indexOf(p)!=-1&&(t=a),s(e[t])&&(t===h?l(e):e[t]=d)},f=function(t,s){var o=t.styleFloat||t.cssFloat||t[p],u=e.Lang.trim,f;try{i.cssText=s||d}catch(l){r=n.createElement("p"),i=r.style,i.cssText=s||d}o&&!t[a]&&(t=e.merge(t),delete t.styleFloat,delete t.cssFloat,delete t[p],t[a]=o);for(f in t)if(t.hasOwnProperty(f))try{i[f]=u(t[f])}catch(c){}return i.cssText},e.mix(v,{toCssText:h in i?f:function(t,n){return h in t&&(t=e.merge(t,{filter:"alpha(opacity="+t.opacity*100+")"}),delete t.opacity),f(t,n)},register:function(e,t){return!!(e&&t instanceof v&&!u[e]&&(u[e]=t))},isValidSelector:function(e){var t=!1;return e&&s(e)&&(o.hasOwnProperty(e)||(o[e]=!/\S/.test(e.replace(/\s+|\s*[+~>]\s*/g," ").replace(/([^ ])\[.*?\]/g,"$1").replace(/([^ ])::?[a-z][a-z\-]+[a-z](?:\(.*?\))?/ig,"$1").replace(/(?:^| )[a-z0-6]+/ig," ").replace(/\\./g,d).replace(/[.#]\w[\w\-]*/g,d))),t=o[e]),t}},!0),e.StyleSheet=v},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/substitute/substitute-min.js b/js/yui3/substitute/substitute-min.js
new file mode 100644
index 000000000..7d557cb68
--- /dev/null
+++ b/js/yui3/substitute/substitute-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("substitute",function(e,t){var n=e.Lang,r="dump",i=" ",s="{",o="}",u=/(~-(\d+)-~)/g,a=/\{LBRACE\}/g,f=/\{RBRACE\}/g,l=function(t,l,c,h){var p,d,v,m,g,y,b=[],w,E,S=t.length;for(;;){p=t.lastIndexOf(s,S);if(p<0)break;d=t.indexOf(o,p);if(p+1>=d)break;w=t.substring(p+1,d),m=w,y=null,v=m.indexOf(i),v>-1&&(y=m.substring(v+1),m=m.substring(0,v)),g=l[m],c&&(g=c(m,g,y)),n.isObject(g)?e.dump?n.isArray(g)?g=e.dump(g,parseInt(y,10)):(y=y||"",E=y.indexOf(r),E>-1&&(y=y.substring(4)),g.toString===Object.prototype.toString||E>-1?g=e.dump(g,parseInt(y,10)):g=g.toString()):g=g.toString():n.isUndefined(g)&&(g="~-"+b.length+"-~",b.push(w)),t=t.substring(0,p)+g+t.substring(d+1),h||(S=p-1)}return t.replace(u,function(e,t,n){return s+b[parseInt(n,10)]+o}).replace(a,s).replace(f,o)};e.substitute=l,n.substitute=l},"3.17.2",{requires:["yui-base"],optional:["dump"]});
diff --git a/js/yui3/swf/swf-min.js b/js/yui3/swf/swf-min.js
new file mode 100644
index 000000000..9fb3151d2
--- /dev/null
+++ b/js/yui3/swf/swf-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("swf",function(e,t){function d(t,n,d){this._id=e.guid("yuiswf");var v=this._id,m=o.one(t),d=d||{},g=d.version||l,y=(g+"").split("."),b=r.isFlashVersionAtLeast(parseInt(y[0],10),parseInt(y[1],10),parseInt(y[2],10)),w=r.isFlashVersionAtLeast(8,0,0),E=w&&!b&&d.useExpressInstall,S=E?c:n,x="<object ",T,N,C="yId="+e.id+"&YUISwfId="+v+"&YUIBridgeCallback="+h+"&allowedDomain="+document.location.hostname;e.SWF._instances[v]=this;if(m&&(b||E)&&S){x+='id="'+v+'" ',s.ie?x+='classid="'+a+'" ':x+='type="'+f+'" data="'+u.html(S)+'" ',T="100%",N="100%",x+='width="'+T+'" height="'+N+'">',s.ie&&(x+='<param name="movie" value="'+u.html(S)+'"/>');for(var k in d.fixedAttributes)p.hasOwnProperty(k)&&(x+='<param name="'+u.html(k)+'" value="'+u.html(d.fixedAttributes[k])+'"/>');for(var L in d.flashVars){var A=d.flashVars[L];i.isString(A)&&(C+="&"+u.html(L)+"="+u.html(encodeURIComponent(A)))}C&&(x+='<param name="flashVars" value="'+C+'"/>'),x+="</object>",m.set("innerHTML",x),this._swf=o.one("#"+v)}else{var O={};O.type="wrongflashversion",this.publish("wrongflashversion",{fireOnce:!0}),this.fire("wrongflashversion",O)}}var n=e.Event,r=e.SWFDetect,i=e.Lang,s=e.UA,o=e.Node,u=e.Escape,a="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000",f="application/x-shockwave-flash",l="10.0.22",c="http://fpdownload.macromedia.com/pub/flashplayer/update/current/swf/autoUpdater.swf?"+Math.random(),h="SWF.eventHandler",p={align:"",allowFullScreen:"",allowNetworking:"",allowScriptAccess:"",base:"",bgcolor:"",loop:"",menu:"",name:"",play:"",quality:"",salign:"",scale:"",tabindex:"",wmode:""};d._instances=d._instances||{},d.eventHandler=function(e,t){d._instances[e]._eventHandler(t)},d.prototype={_eventHandler:function(e){e.type==="swfReady"?(this.publish("swfReady",{fireOnce:!0}),this.fire("swfReady",e)):e.type!=="log"&&this.fire(e.type,e)},callSWF:function(e,t){return t||(t=[]),this._swf._node[e]?this._swf._node[e].apply(this._swf._node,t):null},toString:function(){return"SWF "+this._id}},e.augment(d,e.EventTarget),e.SWF=d},"3.17.2",{requires:["event-custom","node","swfdetect","escape"]});
diff --git a/js/yui3/swfdetect/swfdetect-min.js b/js/yui3/swfdetect/swfdetect-min.js
new file mode 100644
index 000000000..4fefae541
--- /dev/null
+++ b/js/yui3/swfdetect/swfdetect-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("swfdetect",function(e,t){function c(e){return parseInt(e,10)}function h(e){i.isNumber(c(e[0]))&&(r.flashMajor=e[0]),i.isNumber(c(e[1]))&&(r.flashMinor=e[1]),i.isNumber(c(e[2]))&&(r.flashRev=e[2])}var n=0,r=e.UA,i=e.Lang,s="ShockwaveFlash",o,u,a,f,l;if(r.gecko||r.webkit||r.opera){if(o=navigator.mimeTypes["application/x-shockwave-flash"])if(u=o.enabledPlugin)a=u.description.replace(/\s[rd]/g,".").replace(/[A-Za-z\s]+/g,"").split("."),h(a)}else if(r.ie){try{f=new ActiveXObject(s+"."+s+".6"),f.AllowScriptAccess="always"}catch(p){f!==null&&(n=6)}if(n===0)try{l=new ActiveXObject(s+"."+s),a=l.GetVariable("$version").replace(/[A-Za-z\s]+/g,"").split(","),h(a)}catch(d){}}e.SWFDetect={getFlashVersion:function(){return String(r.flashMajor)+"."+String(r.flashMinor)+"."+String(r.flashRev)},isFlashVersionAtLeast:function(e,t,n){var i=c(r.flashMajor),s=c(r.flashMinor),o=c(r.flashRev);return e=c(e||0),t=c(t||0),n=c(n||0),e===i?t===s?n<=o:t<s:e<i}}},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/tabview-base/tabview-base-min.js b/js/yui3/tabview-base/tabview-base-min.js
new file mode 100644
index 000000000..79e3a1202
--- /dev/null
+++ b/js/yui3/tabview-base/tabview-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("tabview-base",function(e,t){var n=e.ClassNameManager.getClassName,r="tabview",i="tab",s="panel",o="selected",u={},a=".",f=function(){this.init.apply(this,arguments)};f.NAME="tabviewBase",f._classNames={tabview:n(r),tabviewPanel:n(r,s),tabviewList:n(r,"list"),tab:n(i),tabLabel:n(i,"label"),tabPanel:n(i,s),selectedTab:n(i,o),selectedPanel:n(i,s,o)},f._queries={tabview:a+f._classNames.tabview,tabviewList:"> ul",tab:"> ul > li",tabLabel:"> ul > li > a",tabviewPanel:"> div",tabPanel:"> div > div",selectedTab:"> ul > "+a+f._classNames.selectedTab,selectedPanel:"> div "+a+f._classNames.selectedPanel},e.mix(f.prototype,{init:function(t){t=t||u,this._node=t.host||e.one(t.node),this.refresh()},initClassNames:function(t){var n=e.TabviewBase._classNames;e.Object.each(e.TabviewBase._queries,function(e,r){if(n[r]){var i=this.all(e);t!==undefined&&(i=i.item(t)),i&&i.addClass(n[r])}},this._node),this._node.addClass(n.tabview)},_select:function(t){var n=e.TabviewBase._classNames,r=e.TabviewBase._queries,i=this._node,s=i.one(r.selectedTab),o=i.one(r.selectedPanel),u=i.all(r.tab).item(t),a=i.all(r.tabPanel).item(t);s&&s.removeClass(n.selectedTab),o&&o.removeClass(n.selectedPanel),u&&u.addClass(n.selectedTab),a&&a.addClass(n.selectedPanel)},initState:function(){var t=e.TabviewBase._queries,n=this._node,r=n.one(t.selectedTab),i=r?n.all(t.tab).indexOf(r):0;this._select(i)},_scrubTextNodes:function(){this._node.one(e.TabviewBase._queries.tabviewList).get("childNodes").each(function(e){e.get("nodeType")===3&&e.remove()})},refresh:function(){this._scrubTextNodes(),this.initClassNames(),this.initState(),this.initEvents()},tabEventName:"click",initEvents:function(){this._node.delegate(this.tabEventName,this.onTabEvent,e.TabviewBase._queries.tab,this)},onTabEvent:function(t){t.preventDefault(),this._select(this._node.all(e.TabviewBase._queries.tab).indexOf(t.currentTarget))},destroy:function(){this._node.detach(this.tabEventName)}}),e.TabviewBase=f},"3.17.2",{requires:["node-event-delegate","classnamemanager"]});
diff --git a/js/yui3/tabview-plugin/tabview-plugin-min.js b/js/yui3/tabview-plugin/tabview-plugin-min.js
new file mode 100644
index 000000000..b9abb0eb0
--- /dev/null
+++ b/js/yui3/tabview-plugin/tabview-plugin-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("tabview-plugin",function(e,t){function n(){n.superclass.constructor.apply(this,arguments)}n.NAME="tabviewPlugin",n.NS="tabs",e.extend(n,e.TabviewBase),e.namespace("Plugin"),e.Plugin.Tabview=n},"3.17.2",{requires:["tabview-base"]});
diff --git a/js/yui3/tabview/assets/skins/night/tabview.css b/js/yui3/tabview/assets/skins/night/tabview.css
new file mode 100644
index 000000000..82eafa1b1
--- /dev/null
+++ b/js/yui3/tabview/assets/skins/night/tabview.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-tab-panel{display:none}.yui3-tab-panel-selected{display:block}.yui3-tabview-list,.yui3-tab{margin:0;padding:0;list-style:none}.yui3-tabview{position:relative}.yui3-tabview,.yui3-tabview-list,.yui3-tabview-panel,.yui3-tab,.yui3-tab-panel{zoom:1}.yui3-tab{display:inline-block;*display:inline;vertical-align:bottom;cursor:pointer}.yui3-tab-label{display:block;display:inline-block;padding:6px 10px;position:relative;text-decoration:none;vertical-align:bottom}.yui3-skin-night .yui3-tabview-panel{background-color:#333;color:#808080;padding:1px}.yui3-skin-night .yui3-tab-panel p{margin:10px}.yui3-skin-night .yui3-tabview-list{background-color:#0f0f0f;border-top:1px solid #000;text-align:center;height:46px;background:-moz-linear-gradient(0% 100% 90deg,#0f0f0f 0,#1e1e1e 96%,#292929 100%);background:-webkit-gradient(linear,left bottom,left top,from(#0f0f0f),color-stop(0.96,#1e1e1e),to(#292929))}.yui3-skin-night .yui3-tabview-list li{margin-top:8px}.yui3-skin-night .yui3-tabview-list li a{border:solid 1px #0c0c0c;border-right-style:none;-moz-box-shadow:0 1px #222;-webkit-box-shadow:0 1px #222;box-shadow:0 1px #222;text-shadow:0 -1px 0 rgba(0,0,0,0.7);font-size:85%;text-align:center;color:#fff;padding:6px 28px;background-color:#555658;background:-moz-linear-gradient(0% 100% 90deg,#343536 0,#555658 96%,#3e3f41 100%);background:-webkit-gradient(linear,left bottom,left top,from(#343536),color-stop(0.96,#555658),to(#3e3f41))}.yui3-skin-night .yui3-tabview-list li.yui3-tab-selected a{background-color:#2b2d2d;background:-moz-linear-gradient(0% 100% 90deg,#242526 0,#3b3c3d 96%,#2c2d2f 100%);background:-webkit-gradient(linear,left bottom,left top,from(#242526),color-stop(0.96,#3b3c3d),to(#2c2d2f))}.yui3-skin-night .yui3-tabview-list li:first-child a{-moz-border-radius:6px 0 0 6px;-webkit-border-radius:6px 0 0 6px;border-radius:6px 0 0 6px}.yui3-skin-night .yui3-tabview-list li:last-child a{border-right-style:solid;-moz-border-radius:0 6px 6px 0;-webkit-border-radius:0 6px 6px 0;border-radius:0 6px 6px 0}#yui3-css-stamp.skin-night-tabview{display:none}
diff --git a/js/yui3/tabview/assets/skins/sam/tabview.css b/js/yui3/tabview/assets/skins/sam/tabview.css
new file mode 100644
index 000000000..7e45d2375
--- /dev/null
+++ b/js/yui3/tabview/assets/skins/sam/tabview.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-tab-panel{display:none}.yui3-tab-panel-selected{display:block}.yui3-tabview-list,.yui3-tab{margin:0;padding:0;list-style:none}.yui3-tabview{position:relative}.yui3-tabview,.yui3-tabview-list,.yui3-tabview-panel,.yui3-tab,.yui3-tab-panel{zoom:1}.yui3-tab{display:inline-block;*display:inline;vertical-align:bottom;cursor:pointer}.yui3-tab-label{display:block;display:inline-block;padding:6px 10px;position:relative;text-decoration:none;vertical-align:bottom}.yui3-skin-sam .yui3-tabview-list{border:solid #2647a0;border-width:0 0 5px;zoom:1}.yui3-skin-sam .yui3-tab{margin:0 .2em 0 0;padding:1px 0 0;zoom:1}.yui3-skin-sam .yui3-tab-selected{margin-bottom:-1px}.yui3-skin-sam .yui3-tab-label{background:#d8d8d8 url(../../../../assets/skins/sam/sprite.png) repeat-x;border:solid #a3a3a3;border-width:1px 1px 0 1px;color:#000;cursor:pointer;font-size:85%;padding:.3em .75em;text-decoration:none}.yui3-skin-sam .yui3-tab-label:hover,.yui3-skin-sam .yui3-tab-label:focus{background:#bfdaff url(../../../../assets/skins/sam/sprite.png) repeat-x left -1300px;outline:0}.yui3-skin-sam .yui3-tab-selected .yui3-tab-label,.yui3-skin-sam .yui3-tab-selected .yui3-tab-label:focus,.yui3-skin-sam .yui3-tab-selected .yui3-tab-label:hover{background:#2647a0 url(../../../../assets/skins/sam/sprite.png) repeat-x left -1400px;color:#fff}.yui3-skin-sam .yui3-tab-selected .yui3-tab-label{padding:.4em .75em}.yui3-skin-sam .yui3-tab-selected .yui3-tab-label{border-color:#243356}.yui3-skin-sam .yui3-tabview-panel{background:#edf5ff}.yui3-skin-sam .yui3-tabview-panel{border:1px solid #808080;border-top-color:#243356;padding:.25em .5em}#yui3-css-stamp.skin-sam-tabview{display:none}
diff --git a/js/yui3/tabview/assets/tabview-core.css b/js/yui3/tabview/assets/tabview-core.css
new file mode 100644
index 000000000..81573e7ea
--- /dev/null
+++ b/js/yui3/tabview/assets/tabview-core.css
@@ -0,0 +1,49 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-tab-panel {
+ display:none;
+}
+
+.yui3-tab-panel-selected {
+ display:block;
+}
+
+.yui3-tabview-list,
+.yui3-tab {
+ margin:0;
+ padding:0;
+ list-style:none;
+}
+
+.yui3-tabview {
+ position:relative; /* contain absolute positioned tabs (left/right) */
+}
+
+.yui3-tabview,
+.yui3-tabview-list,
+.yui3-tabview-panel,
+.yui3-tab,
+.yui3-tab-panel { /* IE: kill space between horizontal tabs */
+ zoom:1;
+}
+
+.yui3-tab {
+ display:inline-block;
+ *display:inline; /* IE */
+ vertical-align:bottom; /* safari: for overlap */
+ cursor:pointer;
+}
+
+.yui3-tab-label {
+ display:block;
+ display:inline-block;
+ padding: 6px 10px;
+ position:relative; /* IE: to allow overlap */
+ text-decoration: none;
+ vertical-align:bottom; /* safari: for overlap */
+}
diff --git a/js/yui3/tabview/tabview-min.js b/js/yui3/tabview/tabview-min.js
new file mode 100644
index 000000000..c501e04fb
--- /dev/null
+++ b/js/yui3/tabview/tabview-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("tabview",function(e,t){var n=".",r=e.Base.create("tabView",e.Widget,[e.WidgetParent],{_afterChildAdded:function(){this.get("contentBox").focusManager.refresh()},_defListNodeValueFn:function(){var t=e.Node.create(this.LIST_TEMPLATE);return t.addClass(e.TabviewBase._classNames.tabviewList),t},_defPanelNodeValueFn:function(){var t=e.Node.create(this.PANEL_TEMPLATE);return t.addClass(e.TabviewBase._classNames.tabviewPanel),t},_afterChildRemoved:function(e){var t=e.index,n=this.get("selection");n||(n=this.item(t-1)||this.item(0),n&&n.set("selected",1)),this.get("contentBox").focusManager.refresh()},_initAria:function(t){var n=t.one(e.TabviewBase._queries.tabviewList);n&&n.setAttrs({role:"tablist"})},bindUI:function(){this.get("contentBox").plug(e.Plugin.NodeFocusManager,{descendants:n+e.TabviewBase._classNames.tabLabel,keys:{next:"down:39",previous:"down:37"},circular:!0}),this.after("render",this._setDefSelection),this.after("addChild",this._afterChildAdded),this.after("removeChild",this._afterChildRemoved)},renderUI:function(){var e=this.get("contentBox");this._renderListBox(e),this._renderPanelBox(e),this._childrenContainer=this.get("listNode"),this._renderTabs(e),this._initAria(e)},_setDefSelection:function(){var e=this.get("selection")||this.item(0);this.some(function(t){if(t.get("selected"))return e=t,!0}),e&&(this.set("selection",e),e.set("selected",1))},_renderListBox:function(e){var t=this.get("listNode");t.inDoc()||e.append(t)},_renderPanelBox:function(e){var t=this.get("panelNode");t.inDoc()||e.append(t)},_renderTabs:function(t){var r=e.TabviewBase._classNames,i=e.TabviewBase._queries,s=t.all(i.tab),o=this.get("panelNode"),u=o?this.get("panelNode").get("children"):null,a=this;s&&(s.addClass(r.tab),t.all(i.tabLabel).addClass(r.tabLabel),t.all(i.tabPanel).addClass(r.tabPanel),s.each(function(e,t){var i=u?u.item(t):null;a.add({boundingBox:e,contentBox:e.one(n+r.tabLabel),panelNode:i})}))}},{ATTRS:{defaultChildType:{value:"Tab"},listNode:{setter:function(t){return t=e.one(t),t&&t.addClass(e.TabviewBase._classNames.tabviewList),t},valueFn:"_defListNodeValueFn"},panelNode:{setter:function(t){return t=e.one(t),t&&t.addClass(e.TabviewBase._classNames.tabviewPanel),t},valueFn:"_defPanelNodeValueFn"},tabIndex:{value:null}},HTML_PARSER:{listNode:function(t){return t.one(e.TabviewBase._queries.tabviewList)},panelNode:function(t){return t.one(e.TabviewBase._queries.tabviewPanel)}},LIST_TEMPLATE:"<ul></ul>",PANEL_TEMPLATE:"<div></div>"});r.prototype.LIST_TEMPLATE=r.LIST_TEMPLATE,r.prototype.PANEL_TEMPLATE=r.PANEL_TEMPLATE,e.TabView=r,e.Tab=e.Base.create("tab",e.Widget,[e.WidgetChild],{BOUNDING_TEMPLATE:"<li></li>",CONTENT_TEMPLATE:"<a></a>",PANEL_TEMPLATE:"<div></div>",_uiSetSelectedPanel:function(t){this.get("panelNode").toggleClass(e.TabviewBase._classNames.selectedPanel,t)},_afterTabSelectedChange:function(e){this._uiSetSelectedPanel(e.newVal)},_afterParentChange:function(e){e.newVal?this._add():this._remove()},_initAria:function(){var t=this.get("contentBox"),n=t.get("id"),r=this.get("panelNode");n||(n=e.guid(),t.set("id",n)),t.set("role","tab"),t.get("parentNode").set("role","presentation"),r.setAttrs({role:"tabpanel","aria-labelledby":n})},syncUI:function(){var t=e.TabviewBase._classNames;this.get("boundingBox").addClass(t.tab),this.get("contentBox").addClass(t.tabLabel),this.set("label",this.get("label")),this.set("content",this.get("content")),this._uiSetSelectedPanel(this.get("selected"))},bindUI:function(){this.after("selectedChange",this._afterTabSelectedChange),this.after("parentChange",this._afterParentChange)},renderUI:function(){this._renderPanel(),this._initAria()},_renderPanel:function(){this.get("parent").get("panelNode").appendChild(this.get("panelNode"))},_add:function(){var e=this.get("parent").get("contentBox"),t=e.get("listNode"),n=e.get("panelNode");t&&t.appendChild(this.get("boundingBox")),n&&n.appendChild(this.get("panelNode"))},_remove:function(){this.get("boundingBox").remove(),this.get("panelNode").remove()},_onActivate:function(e){e.target===this&&(e.domEvent.preventDefault(),e.target.set("selected",1))},initializer:function(){this.publish(this.get("triggerEvent"),{defaultFn:this._onActivate})},_defLabelGetter:function(){return this.get("contentBox").getHTML()},_defLabelSetter:function(e){var t=this.get("contentBox");return t.getHTML()!==e&&t.setHTML(e),e},_defContentSetter:function(e){var t=this.get("panelNode");return t.getHTML()!==e&&t.setHTML(e),e},_defContentGetter:function(){return this.get("panelNode").getHTML()},_defPanelNodeValueFn:function(){var t=e.TabviewBase._classNames,n=this.get("contentBox").get("href")||"",r=this.get("parent"),i=n.indexOf("#"),s;return n=n.substr(i),n.charAt(0)==="#"&&(s=e.one(n),s&&s.addClass(t.tabPanel)),!s&&r&&(s=r.get("panelNode").get("children").item(this.get("index"))),s||(s=e.Node.create(this.PANEL_TEMPLATE),s.addClass(t.tabPanel)),s}},{ATTRS:{triggerEvent:{value:"click"},label:{setter:"_defLabelSetter",getter:"_defLabelGetter"},content:{setter:"_defContentSetter",getter:"_defContentGetter"},panelNode:{setter:function(t){return t=e.one(t),t&&t.addClass(e.TabviewBase._classNames.tabPanel),t},valueFn:"_defPanelNodeValueFn"},tabIndex:{value:null,validator:"_validTabIndex"}},HTML_PARSER:{selected:function(){var t=this.get("boundingBox").hasClass(e.TabviewBase._classNames.selectedTab)?1:0;return t}}})},"3.17.2",{requires:["widget","widget-parent","widget-child","tabview-base","node-pluginhost","node-focusmanager"],skinnable:!0});
diff --git a/js/yui3/template-base/template-base-min.js b/js/yui3/template-base/template-base-min.js
new file mode 100644
index 000000000..5ba12d123
--- /dev/null
+++ b/js/yui3/template-base/template-base-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("template-base",function(e,t){function n(t,n){this.defaults=n,this.engine=t||e.Template.Micro,this.engine||e.error("No template engine loaded.")}n._registry={},n.register=function(e,t){return n._registry[e]=t,t},n.get=function(e){return n._registry[e]},n.render=function(t,r,i){var s=n._registry[t],o="";return s?o=s(r,i):e.error('Unregistered template: "'+t+'"'),o},n.prototype={compile:function(t,n){return n=n?e.merge(this.defaults,n):this.defaults,this.engine.compile(t,n)},precompile:function(t,n){return n=n?e.merge(this.defaults,n):this.defaults,this.engine.precompile(t,n)},render:function(t,n,r){return r=r?e.merge(this.defaults,r):this.defaults,this.engine.render?this.engine.render(t,n,r):this.engine.compile(t,r)(n,r)},revive:function(t,n){return n=n?e.merge(this.defaults,n):this.defaults,this.engine.revive?this.engine.revive(t,n):t}},e.Template=e.Template?e.mix(n,e.Template):n},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/template-micro/template-micro-min.js b/js/yui3/template-micro/template-micro-min.js
new file mode 100644
index 000000000..843bf1283
--- /dev/null
+++ b/js/yui3/template-micro/template-micro-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("template-micro",function(e,t){var n=e.namespace("Template.Micro");n.options={code:/<%([\s\S]+?)%>/g,escapedOutput:/<%=([\s\S]+?)%>/g,rawOutput:/<%==([\s\S]+?)%>/g,stringEscape:/\\|'|\r|\n|\t|\u2028|\u2029/g,stringReplace:{"\\":"\\\\","'":"\\'","\r":"\\r","\n":"\\n"," ":"\\t","\u2028":"\\u2028","\u2029":"\\u2029"}},n.compile=function(t,r){var i=[],s="\uffff",o="\ufffe",u;return r=e.merge(n.options,r),u="var $b='', $v=function (v){return v || v === 0 ? v : $b;}, $t='"+t.replace(/\ufffe|\uffff/g,"").replace(r.rawOutput,function(e,t){return o+(i.push("'+\n$v("+t+")+\n'")-1)+s}).replace(r.escapedOutput,function(e,t){return o+(i.push("'+\n$e($v("+t+"))+\n'")-1)+s}).replace(r.code,function(e,t){return o+(i.push("';\n"+t+"\n$t+='")-1)+s}).replace(r.stringEscape,function(e){return r.stringReplace[e]||""}).replace(/\ufffe(\d+)\uffff/g,function(e,t){return i[parseInt(t,10)]}).replace(/\n\$t\+='';\n/g,"\n")+"';\nreturn $t;",r.precompile?"function (Y, $e, data) {\n"+u+"\n}":this.revive(new Function("Y","$e","data",u))},n.precompile=function(e,t){return t||(t={}),t.precompile=!0,this.compile(e,t)},n.render=function(e,t,n){return this.compile(e,n)(t)},n.revive=function(t){return function(n){return n||(n={}),t.call(n,e,e.Escape.html,n)}}},"3.17.2",{requires:["escape"]});
diff --git a/js/yui3/test-console/assets/skins/sam/test-console.css b/js/yui3/test-console/assets/skins/sam/test-console.css
new file mode 100644
index 000000000..9a992bdbe
--- /dev/null
+++ b/js/yui3/test-console/assets/skins/sam/test-console.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-testconsole .yui3-console-entry{min-height:inherit;padding:5px}.yui3-testconsole .yui3-console-controls{display:none}.yui3-skin-sam .yui3-testconsole .yui3-console-content,.yui3-skin-sam .yui3-testconsole .yui3-console-bd,.yui3-skin-sam .yui3-testconsole .yui3-console-entry,.yui3-skin-sam .yui3-testconsole .yui3-console-ft,.yui3-skin-sam .yui3-testconsole .yui3-console-ft .yui3-console-filters-categories,.yui3-skin-sam .yui3-testconsole .yui3-console-ft .yui3-console-filters-sources,.yui3-skin-sam .yui3-testconsole .yui3-console-hd{background:0;border:0;-moz-border-radius:0;-webkit-border-radius:0;border-radius:0}.yui3-skin-sam .yui3-testconsole-content,.yui3-skin-sam .yui3-testconsole .yui3-console-bd{color:#333;font:13px/1.4 Helvetica,'DejaVu Sans','Bitstream Vera Sans',Arial,sans-serif}.yui3-skin-sam .yui3-testconsole-content{border:1px solid #afafaf}.yui3-skin-sam .yui3-testconsole .yui3-console-entry{border-bottom:1px solid #eaeaea;font-family:Menlo,Inconsolata,Consolas,'DejaVu Mono','Bitstream Vera Sans Mono',monospace;font-size:11px}.yui3-skin-sam .yui3-testconsole .yui3-console-ft{border-top:1px solid}.yui3-skin-sam .yui3-testconsole .yui3-console-hd{border-bottom:1px solid;*zoom:1}.yui3-skin-sam .yui3-testconsole.yui3-console-collapsed .yui3-console-hd{border:0}.yui3-skin-sam .yui3-testconsole .yui3-console-ft,.yui3-skin-sam .yui3-testconsole .yui3-console-hd{border-color:#cfcfcf}.yui3-skin-sam .yui3-testconsole .yui3-testconsole-entry-fail{background-color:#ffe0e0;border-bottom-color:#ffc5c4}.yui3-skin-sam .yui3-testconsole .yui3-testconsole-entry-pass{background-color:#ecffea;border-bottom-color:#d1ffcc}#yui3-css-stamp.skin-sam-test-console{display:none}
diff --git a/js/yui3/test-console/assets/test-console-core.css b/js/yui3/test-console/assets/test-console-core.css
new file mode 100644
index 000000000..118aa1811
--- /dev/null
+++ b/js/yui3/test-console/assets/test-console-core.css
@@ -0,0 +1,15 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-testconsole .yui3-console-entry {
+ min-height: inherit;
+ padding: 5px;
+}
+
+.yui3-testconsole .yui3-console-controls {
+ display: none;
+}
diff --git a/js/yui3/test-console/test-console-min.js b/js/yui3/test-console/test-console-min.js
new file mode 100644
index 000000000..bc1d7854c
--- /dev/null
+++ b/js/yui3/test-console/test-console-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("test-console",function(e,t){function n(){n.superclass.constructor.apply(this,arguments)}e.namespace("Test").Console=e.extend(n,e.Console,{initializer:function(t){this.on("entry",this._onEntry),this.plug(e.Plugin.ConsoleFilters,{category:e.merge({info:!0,pass:!1,fail:!0,status:!1},t&&t.filters||{}),defaultVisibility:!1,source:{TestRunner:!0}}),e.Test.Runner.on("complete",e.bind(this._parseCoverage,this))},_isIstanbul:function(t){var n=e.Object.keys(t)[0],r=!1;return t[n].s!==undefined&&t[n].fnMap!==undefined&&(r=!0),t.s!==undefined&&t.fnMap!==undefined&&(r=!0),r},parseYUITestCoverage:function(t){var n={lines:{hit:0,miss:0,total:0,percent:0},functions:{hit:0,miss:0,total:0,percent:0}},r;e.Object.each(t,function(e){n.lines.total+=e.coveredLines,n.lines.hit+=e.calledLines,n.lines.miss+=e.coveredLines-e.calledLines,n.lines.percent=Math.floor(n.lines.hit/n.lines.total*100),n.functions.total+=e.coveredFunctions,n.functions.hit+=e.calledFunctions,n.functions.miss+=e.coveredFunctions-e.calledFunctions,n.functions.percent=Math.floor(n.functions.hit/n.functions.total*100)}),r="Lines: Hit:"+n.lines.hit+" Missed:"+n.lines.miss+" Total:"+n.lines.total+" Percent:"+n.lines.percent+"%\n",r+="Functions: Hit:"+n.functions.hit+" Missed:"+n.functions.miss+" Total:"+n.functions.total+" Percent:"+n.functions.percent+"%",this.log("Coverage: "+r,"info","TestRunner")},_blankSummary:function(){return{lines:{total:0,covered:0,pct:"Unknown"},statements:{total:0,covered:0,pct:"Unknown"},functions:{total:0,covered:0,pct:"Unknown"},branches:{total:0,covered:0,pct:"Unknown"}}},_addDerivedInfoForFile:function(t){var n=t.statementMap,r=t.s,i;t.l||(t.l=i={},e.Object.each(r,function(e,t){var s=n[t].start.line,o=r[t],u=i[s];if(typeof u=="undefined"||u<o)i[s]=o}))},_percent:function(e,t){var n,r=100;return t>0&&(n=1e5*e/t+5,r=Math.floor(n/10)/100),r},_computeSimpleTotals:function(t,n){var r=t[n],i={total:0,covered:0};return e.Object.each(r,function(e){i.total+=1,e&&(i.covered+=1)}),i.pct=this._percent(i.covered,i.total),i},_computeBranchTotals:function(t){var n=t.b,r={total:0,covered:0};return e.Object.each(n,function(t){var n=e.Array.filter(t,function(e){return e>0});r.total+=t.length,r.covered+=n.length}),r.pct=this._percent(r.covered,r.total),r},parseIstanbul:function(t){var n=this,r="Coverage Report:\n";e.Object.each(t,function(t,i){var s=n._blankSummary();n._addDerivedInfoForFile(t),s.lines=n._computeSimpleTotals(t,"l"),s.functions=n._computeSimpleTotals(t,"f"),s.statements=n._computeSimpleTotals(t,"s"),s.branches=n._computeBranchTotals(t),r+=i+":\n",e.Array.each(["lines","functions","statements","branches"],function(e){r+=" "+e+": "+s[e].covered+"/"+s[e].total+" : "+s[e].pct+"%\n"})}),this.log(r,"info","TestRunner")},_parseCoverage:function(){var t=e.Test.Runner.getCoverage();if(!t)return;this._isIstanbul(t)?this.parseIstanbul(t):this.parseYUITestCoverage(t)},_onEntry:function(e){var t=e.message;t.category==="info"&&/\s(?:case|suite)\s|yuitests\d+|began/.test(t.message)?t.category="status":t.category==="fail"&&this.printBuffer()}},{NAME:"testConsole",ATTRS:{entryTemplate:{value:'<div class="{entry_class} {cat_class} {src_class}"><div class="{entry_content_class}">{message}</div></div>'},height:{value:"350px"},newestOnTop:{value:!1},style:{value:"block"},width:{value:e.UA.ie&&e.UA.ie<9?"100%":"inherit"}}})},"3.17.2",{requires:["console-filters","test","array-extras"],skinnable:!0});
diff --git a/js/yui3/test/test-min.js b/js/yui3/test/test-min.js
new file mode 100644
index 000000000..3b0f4000b
--- /dev/null
+++ b/js/yui3/test/test-min.js
@@ -0,0 +1,13 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("test",function(e,t){YUI.YUITest?e.Test=YUI.YUITest:(YUITest={version:"3.17.2",guid:function(t){return e.guid(t)}},e.namespace("Test"),YUITest.Object=e.Object,YUITest.Array=e.Array,YUITest.Util={mix:e.mix,JSON:e.JSON},YUITest.EventTarget=function(){this._handlers={}},YUITest.EventTarget.prototype={constructor:YUITest.EventTarget,attach:function(e,t){typeof this._handlers[e]=="undefined"&&(this._handlers[e]=[]),this._handlers[e].push(t)},subscribe:function(e,t){this.attach.apply(this,arguments)},fire:function(e){typeof e=="string"&&(e={type:e}),e.target||(e.target=this);if(!e.type)throw new Error("Event object missing 'type' property.");if(this._handlers[e.type]instanceof Array){var t=this._handlers[e.type];for(var n=0,r=t.length;n<r;n++)t[n].call(this,e)}},detach:function(e,t){if(this._handlers[e]instanceof Array){var n=this._handlers[e];for(var r=0,i=n.length;r<i;r++)if(n[r]===t){n.splice(r,1);break}}},unsubscribe:function(e,t){this.detach.apply(this,arguments)}},YUITest.TestSuite=function(e){this.name="",this.items=[];if(typeof e=="string")this.name=e;else if(e instanceof Object)for(var t in e)e.hasOwnProperty(t)&&(this[t]=e[t]);if(this.name===""||!this.name)this.name=YUITest.guid("testSuite_")},YUITest.TestSuite.prototype={constructor:YUITest.TestSuite,add:function(e){return(e instanceof YUITest.TestSuite||e instanceof YUITest.TestCase)&&this.items.push(e),this},setUp:function(){},tearDown:function(){}},YUITest.TestCase=function(e){this._should={};for(var t in e)this[t]=e[t];typeof this.name!="string"&&(this.name=YUITest.guid("testCase_"))},YUITest.TestCase.DEFAULT_WAIT=1e4,YUITest.TestCase._waitTimeout=function(){YUITest.Assert.fail("Timeout: wait() called but resume() never called.")},YUITest.TestCase.prototype={constructor:YUITest.TestCase,callback:function(){return YUITest.TestRunner.callback.apply(YUITest.TestRunner,arguments)},resume:function(e){YUITest.TestRunner.resume(e)},wait:function(e,t){throw t=typeof e=="number"?e:typeof t=="number"?t:YUITest.TestCase.DEFAULT_WAIT,typeof e!="function"&&(e=YUITest.TestCase._waitTimeout),new YUITest.Wait(e,t)},next:function(e,t){var n=this;return t=arguments.length>=2?arguments[1]:undefined,function(){var r=arguments;t===undefined&&(t=this),n.resume(function(){e.apply(t,r)})}},waitFor:function(e,t,n,r){var i=this,s;(typeof e!="function"||typeof t!="function")&&i.fail("waitFor() called with invalid parameters."),typeof n!="number"&&(n=YUITest.TestCase.DEFAULT_WAIT),s=+(new Date)+n,typeof r!="number"&&(r=100),i.wait(function(){var n;e.call(i)?t.call(i):(n=+(new Date),n>s?YUITest.TestCase._waitTimeout():i.waitFor(e,t,s-n,r))},r)},assert:function(e,t){YUITest.Assert._increment();if(!e)throw new YUITest.AssertionError(YUITest.Assert._formatMessage(t,"Assertion failed."))},fail:function(e){YUITest.Assert.fail(e)},init:function(){},destroy:function(){},setUp:function(){},tearDown:function(){}},YUITest.TestFormat=function(){function e(e){return e.replace(/[<>"'&]/g,function(e){switch(e){case"<":return"&lt;";case">":return"&gt;";case'"':return"&quot;";case"'":return"&apos;";case"&":return"&amp;"}})}return{JSON:function(e){return YUITest.Util.JSON.stringify(e)},XML:function(t){function n(t){var r="<"+t.type+' name="'+e(t.name)+'"';typeof t.duration=="number"&&(r+=' duration="'+t.duration+'"');if(t.type=="test")r+=' result="'+t.result+'" message="'+e(t.message)+'">';else{r+=' passed="'+t.passed+'" failed="'+t.failed+'" ignored="'+t.ignored+'" total="'+t.total+'">';for(var i in t)t.hasOwnProperty(i)&&t[i]&&typeof t[i]=="object"&&!(t[i]instanceof Array)&&(r+=n(t[i]))}return r+="</"+t.type+">",r}return'<?xml version="1.0" encoding="UTF-8"?>'+n(t)},JUnitXML:function(t){function n(t){var r="";switch(t.type){case"test":t.result!="ignore"&&(r='<testcase name="'+e(t.name)+'" time="'+t.duration/1e3+'">',t.result=="fail"&&(r+='<failure message="'+e(t.message)+'"><![CDATA['+t.message+"]]></failure>"),r+="</testcase>");break;case"testcase":r='<testsuite name="'+e(t.name)+'" tests="'+t.total+'" failures="'+t.failed+'" time="'+t.duration/1e3+'">';for(var i in t)t.hasOwnProperty(i)&&t[i]&&typeof t[i]=="object"&&!(t[i]instanceof Array)&&(r+=n(t[i]));r+="</testsuite>";break;case"testsuite":for(var i in t)t.hasOwnProperty(i)&&t[i]&&typeof t[i]=="object"&&!(t[i]instanceof Array)&&(r+=n(t[i]));break;case"report":r="<testsuites>";for(var i in t)t.hasOwnProperty(i)&&t[i]&&typeof t[i]=="object"&&!(t[i]instanceof Array)&&(r+=n(t[i]));r+="</testsuites>"}return r}return'<?xml version="1.0" encoding="UTF-8"?>'+n(t)},TAP:function(e){function n(e){var r="";switch(e.type){case"test":e.result!="ignore"?(r="ok "+t++ +" - "+e.name,e.result=="fail"&&(r="not "+r+" - "+e.message),r+="\n"):r="#Ignored test "+e.name+"\n";break;case"testcase":r="#Begin testcase "+e.name+"("+e.failed+" failed of "+e.total+")\n";for(var i in e)e.hasOwnProperty(i)&&e[i]&&typeof e[i]=="object"&&!(e[i]instanceof Array)&&(r+=n(e[i]));r+="#End testcase "+e.name+"\n";break;case"testsuite":r="#Begin testsuite "+e.name+"("+e.failed+" failed of "+e.total+")\n";for(var i in e)e.hasOwnProperty(i)&&e[i]&&typeof e[i]=="object"&&!(e[i]instanceof Array)&&(r+=n(e[i]));r+="#End testsuite "+e.name+"\n";break;case"report":for(var i in e)e.hasOwnProperty(i)&&e[i]&&typeof e[i]=="object"&&!(e[i]instanceof Array)&&(r+=n(e[i]))}return r}var t=1;return"1.."+e.total+"\n"+n(e)}}}(),YUITest.Reporter=function(e,t){this.url=e,this.format=t||YUITest.TestFormat.XML,this._fields=new Object,this._form=null,this._iframe=null},YUITest.Reporter.prototype={constructor:YUITest.Reporter,addField:function(e,t){this._fields[e]=t},clearFields:function(){this._fields=new Object},destroy:function(){this._form&&(this._form.parentNode.removeChild(this._form),this._form=null),this._iframe&&(this._iframe.parentNode.removeChild(this._iframe),this._iframe=null),this._fields=null},report:function(e){if(!this._form){this._form=document.createElement("form"),this._form.method="post",this._form.style.visibility="hidden",this._form.style.position="absolute"
+,this._form.style.top=0,document.body.appendChild(this._form);try{this._iframe=document.createElement('<iframe name="yuiTestTarget" />')}catch(t){this._iframe=document.createElement("iframe"),this._iframe.name="yuiTestTarget"}this._iframe.src="javascript:false",this._iframe.style.visibility="hidden",this._iframe.style.position="absolute",this._iframe.style.top=0,document.body.appendChild(this._iframe),this._form.target="yuiTestTarget"}this._form.action=this.url;while(this._form.hasChildNodes())this._form.removeChild(this._form.lastChild);this._fields.results=this.format(e),this._fields.useragent=navigator.userAgent,this._fields.timestamp=(new Date).toLocaleString();for(var n in this._fields){var r=this._fields[n];if(this._fields.hasOwnProperty(n)&&typeof r!="function"){var i=document.createElement("input");i.type="hidden",i.name=n,i.value=r,this._form.appendChild(i)}}delete this._fields.results,delete this._fields.useragent,delete this._fields.timestamp,arguments[1]!==!1&&this._form.submit()}},YUITest.TestRunner=function(){function e(e,t){if(!t.length)return!0;if(e)for(var n=0,r=e.length;n<r;n++)if(t.indexOf(","+e[n]+",")>-1)return!0;return!1}function t(e){this.testObject=e,this.firstChild=null,this.lastChild=null,this.parent=null,this.next=null,this.results=new YUITest.Results,e instanceof YUITest.TestSuite?(this.results.type="testsuite",this.results.name=e.name):e instanceof YUITest.TestCase&&(this.results.type="testcase",this.results.name=e.name)}function n(){YUITest.EventTarget.call(this),this.masterSuite=new YUITest.TestSuite(YUITest.guid("testSuite_")),this._cur=null,this._root=null,this._log=!0,this._waiting=!1,this._running=!1,this._lastResults=null,this._context=null,this._groups=""}return t.prototype={appendChild:function(e){var n=new t(e);return this.firstChild===null?this.firstChild=this.lastChild=n:(this.lastChild.next=n,this.lastChild=n),n.parent=this,n}},n.prototype=YUITest.Util.mix(new YUITest.EventTarget,{_ignoreEmpty:!1,constructor:YUITest.TestRunner,TEST_CASE_BEGIN_EVENT:"testcasebegin",TEST_CASE_COMPLETE_EVENT:"testcasecomplete",TEST_SUITE_BEGIN_EVENT:"testsuitebegin",TEST_SUITE_COMPLETE_EVENT:"testsuitecomplete",TEST_PASS_EVENT:"pass",TEST_FAIL_EVENT:"fail",ERROR_EVENT:"error",TEST_IGNORE_EVENT:"ignore",COMPLETE_EVENT:"complete",BEGIN_EVENT:"begin",_addTestCaseToTestTree:function(e,t){var n=e.appendChild(t),r,i;for(r in t)(r.indexOf("test")===0||r.indexOf(" ")>-1)&&typeof t[r]=="function"&&n.appendChild(r)},_addTestSuiteToTestTree:function(e,t){var n=e.appendChild(t);for(var r=0;r<t.items.length;r++)t.items[r]instanceof YUITest.TestSuite?this._addTestSuiteToTestTree(n,t.items[r]):t.items[r]instanceof YUITest.TestCase&&this._addTestCaseToTestTree(n,t.items[r])},_buildTestTree:function(){this._root=new t(this.masterSuite);for(var e=0;e<this.masterSuite.items.length;e++)this.masterSuite.items[e]instanceof YUITest.TestSuite?this._addTestSuiteToTestTree(this._root,this.masterSuite.items[e]):this.masterSuite.items[e]instanceof YUITest.TestCase&&this._addTestCaseToTestTree(this._root,this.masterSuite.items[e])},_handleTestObjectComplete:function(e){var t;e&&typeof e.testObject=="object"&&(t=e.parent,t&&(t.results.include(e.results),t.results[e.testObject.name]=e.results),e.testObject instanceof YUITest.TestSuite?(this._execNonTestMethod(e,"tearDown",!1),e.results.duration=new Date-e._start,this.fire({type:this.TEST_SUITE_COMPLETE_EVENT,testSuite:e.testObject,results:e.results})):e.testObject instanceof YUITest.TestCase&&(this._execNonTestMethod(e,"destroy",!1),e.results.duration=new Date-e._start,this.fire({type:this.TEST_CASE_COMPLETE_EVENT,testCase:e.testObject,results:e.results})))},_next:function(){if(this._cur===null)this._cur=this._root;else if(this._cur.firstChild)this._cur=this._cur.firstChild;else if(this._cur.next)this._cur=this._cur.next;else{while(this._cur&&!this._cur.next&&this._cur!==this._root)this._handleTestObjectComplete(this._cur),this._cur=this._cur.parent;this._handleTestObjectComplete(this._cur),this._cur==this._root?(this._cur.results.type="report",this._cur.results.timestamp=(new Date).toLocaleString(),this._cur.results.duration=new Date-this._cur._start,this._lastResults=this._cur.results,this._running=!1,this.fire({type:this.COMPLETE_EVENT,results:this._lastResults}),this._cur=null):this._cur&&(this._cur=this._cur.next)}return this._cur},_execNonTestMethod:function(e,t,n){var r=e.testObject,i={type:this.ERROR_EVENT};try{if(n&&r["async:"+t])return r["async:"+t](this._context),!0;r[t](this._context)}catch(s){e.results.errors++,i.error=s,i.methodName=t,r instanceof YUITest.TestCase?i.testCase=r:i.testSuite=testSuite,this.fire(i)}return!1},_run:function(){var e=!1,t=this._next();if(t!==null){this._running=!0,this._lastResult=null;var n=t.testObject;if(typeof n=="object"&&n!==null){if(n instanceof YUITest.TestSuite)this.fire({type:this.TEST_SUITE_BEGIN_EVENT,testSuite:n}),t._start=new Date,this._execNonTestMethod(t,"setUp",!1);else if(n instanceof YUITest.TestCase){this.fire({type:this.TEST_CASE_BEGIN_EVENT,testCase:n}),t._start=new Date;if(this._execNonTestMethod(t,"init",!0))return}typeof setTimeout!="undefined"?setTimeout(function(){YUITest.TestRunner._run()},0):this._run()}else this._runTest(t)}},_resumeTest:function(e){var t=this._cur;this._waiting=!1;if(!t)return;var n=t.testObject,r=t.parent.testObject;r.__yui_wait&&(clearTimeout(r.__yui_wait),delete r.__yui_wait);var i=n.indexOf("fail:")===0||(r._should.fail||{})[n],s=(r._should.error||{})[n],o=!1,u=null;try{e.call(r,this._context);if(YUITest.Assert._getCount()==0&&!this._ignoreEmpty)throw new YUITest.AssertionError("Test has no asserts.");i?(u=new YUITest.ShouldFail,o=!0):s&&(u=new YUITest.ShouldError,o=!0)}catch(a){r.__yui_wait&&(clearTimeout(r.__yui_wait),delete r.__yui_wait);if(a instanceof YUITest.AssertionError)i||(u=a,o=!0);else{if(a instanceof YUITest.Wait){if(typeof a.segment=="function"&&typeof a.delay=="number"){if(typeof setTimeout=="undefined")throw new Error("Asynchronous tests not supported in this environment."
+);r.__yui_wait=setTimeout(function(){YUITest.TestRunner._resumeTest(a.segment)},a.delay),this._waiting=!0}return}s?typeof s=="string"?a.message!=s&&(u=new YUITest.UnexpectedError(a),o=!0):typeof s=="function"?a instanceof s||(u=new YUITest.UnexpectedError(a),o=!0):typeof s=="object"&&s!==null&&(!(a instanceof s.constructor)||a.message!=s.message)&&(u=new YUITest.UnexpectedError(a),o=!0):(u=new YUITest.UnexpectedError(a),o=!0)}}o?this.fire({type:this.TEST_FAIL_EVENT,testCase:r,testName:n,error:u}):this.fire({type:this.TEST_PASS_EVENT,testCase:r,testName:n}),this._execNonTestMethod(t.parent,"tearDown",!1),YUITest.Assert._reset();var f=new Date-t._start;t.parent.results[n]={result:o?"fail":"pass",message:u?u.getMessage():"Test passed",type:"test",name:n,duration:f},o?t.parent.results.failed++:t.parent.results.passed++,t.parent.results.total++,typeof setTimeout!="undefined"?setTimeout(function(){YUITest.TestRunner._run()},0):this._run()},_handleError:function(e){if(!this._waiting)throw e;this._resumeTest(function(){throw e})},_runTest:function(t){var n=t.testObject,r=t.parent.testObject,i=r[n],s=n.indexOf("ignore:")===0||!e(r.groups,this._groups)||(r._should.ignore||{})[n];s?(t.parent.results[n]={result:"ignore",message:"Test ignored",type:"test",name:n.indexOf("ignore:")===0?n.substring(7):n},t.parent.results.ignored++,t.parent.results.total++,this.fire({type:this.TEST_IGNORE_EVENT,testCase:r,testName:n}),typeof setTimeout!="undefined"?setTimeout(function(){YUITest.TestRunner._run()},0):this._run()):(t._start=new Date,this._execNonTestMethod(t.parent,"setUp",!1),this._resumeTest(i))},getName:function(){return this.masterSuite.name},setName:function(e){this.masterSuite.name=e},add:function(e){return this.masterSuite.add(e),this},clear:function(){this.masterSuite=new YUITest.TestSuite(YUITest.guid("testSuite_"))},isWaiting:function(){return this._waiting},isRunning:function(){return this._running},getResults:function(e){return!this._running&&this._lastResults?typeof e=="function"?e(this._lastResults):this._lastResults:null},getCoverage:function(e){var t=null;return typeof _yuitest_coverage=="object"&&(t=_yuitest_coverage),typeof __coverage__=="object"&&(t=__coverage__),!this._running&&typeof t=="object"?typeof e=="function"?e(t):t:null},callback:function(){var e=arguments,t=this._context,n=this;return function(){for(var r=0;r<arguments.length;r++)t[e[r]]=arguments[r];n._run()}},resume:function(e){if(!this._waiting)throw new Error("resume() called without wait().");this._resumeTest(e||function(){})},run:function(e){e=e||{};var t=YUITest.TestRunner,n=e.oldMode;!n&&this.masterSuite.items.length==1&&this.masterSuite.items[0]instanceof YUITest.TestSuite&&(this.masterSuite=this.masterSuite.items[0]),t._groups=e.groups instanceof Array?","+e.groups.join(",")+",":"",t._buildTestTree(),t._context={},t._root._start=new Date,t.fire(t.BEGIN_EVENT),t._run()}}),new n}(),YUITest.ArrayAssert={_indexOf:function(e,t){if(e.indexOf)return e.indexOf(t);for(var n=0;n<e.length;n++)if(e[n]===t)return n;return-1},_some:function(e,t){if(e.some)return e.some(t);for(var n=0;n<e.length;n++)if(t(e[n]))return!0;return!1},contains:function(e,t,n){YUITest.Assert._increment(),this._indexOf(t,e)==-1&&YUITest.Assert.fail(YUITest.Assert._formatMessage(n,"Value "+e+" ("+typeof e+") not found in array ["+t+"]."))},containsItems:function(e,t,n){YUITest.Assert._increment();for(var r=0;r<e.length;r++)this._indexOf(t,e[r])==-1&&YUITest.Assert.fail(YUITest.Assert._formatMessage(n,"Value "+e[r]+" ("+typeof e[r]+") not found in array ["+t+"]."))},containsMatch:function(e,t,n){YUITest.Assert._increment();if(typeof e!="function")throw new TypeError("ArrayAssert.containsMatch(): First argument must be a function.");this._some(t,e)||YUITest.Assert.fail(YUITest.Assert._formatMessage(n,"No match found in array ["+t+"]."))},doesNotContain:function(e,t,n){YUITest.Assert._increment(),this._indexOf(t,e)>-1&&YUITest.Assert.fail(YUITest.Assert._formatMessage(n,"Value found in array ["+t+"]."))},doesNotContainItems:function(e,t,n){YUITest.Assert._increment();for(var r=0;r<e.length;r++)this._indexOf(t,e[r])>-1&&YUITest.Assert.fail(YUITest.Assert._formatMessage(n,"Value found in array ["+t+"]."))},doesNotContainMatch:function(e,t,n){YUITest.Assert._increment();if(typeof e!="function")throw new TypeError("ArrayAssert.doesNotContainMatch(): First argument must be a function.");this._some(t,e)&&YUITest.Assert.fail(YUITest.Assert._formatMessage(n,"Value found in array ["+t+"]."))},indexOf:function(e,t,n,r){YUITest.Assert._increment();for(var i=0;i<t.length;i++)if(t[i]===e){n!=i&&YUITest.Assert.fail(YUITest.Assert._formatMessage(r,"Value exists at index "+i+" but should be at index "+n+"."));return}YUITest.Assert.fail(YUITest.Assert._formatMessage(r,"Value doesn't exist in array ["+t+"]."))},itemsAreEqual:function(e,t,n){YUITest.Assert._increment(),(typeof e!="object"||typeof t!="object")&&YUITest.Assert.fail(YUITest.Assert._formatMessage(n,"Value should be an array.")),e.length!=t.length&&YUITest.Assert.fail(YUITest.Assert._formatMessage(n,"Array should have a length of "+e.length+" but has a length of "+t.length+"."));for(var r=0;r<e.length;r++)if(e[r]!=t[r])throw new YUITest.ComparisonFailure(YUITest.Assert._formatMessage(n,"Values in position "+r+" are not equal."),e[r],t[r])},itemsAreEquivalent:function(e,t,n,r){YUITest.Assert._increment();if(typeof n!="function")throw new TypeError("ArrayAssert.itemsAreEquivalent(): Third argument must be a function.");e.length!=t.length&&YUITest.Assert.fail(YUITest.Assert._formatMessage(r,"Array should have a length of "+e.length+" but has a length of "+t.length));for(var i=0;i<e.length;i++)if(!n(e[i],t[i]))throw new YUITest.ComparisonFailure(YUITest.Assert._formatMessage(r,"Values in position "+i+" are not equivalent."),e[i],t[i])},isEmpty:function(e,t){YUITest.Assert._increment(),e.length>0&&YUITest.Assert.fail(YUITest.Assert._formatMessage(t,"Array should be empty."))},isNotEmpty:function(e,t){YUITest.Assert._increment
+(),e.length===0&&YUITest.Assert.fail(YUITest.Assert._formatMessage(t,"Array should not be empty."))},itemsAreSame:function(e,t,n){YUITest.Assert._increment(),e.length!=t.length&&YUITest.Assert.fail(YUITest.Assert._formatMessage(n,"Array should have a length of "+e.length+" but has a length of "+t.length));for(var r=0;r<e.length;r++)if(e[r]!==t[r])throw new YUITest.ComparisonFailure(YUITest.Assert._formatMessage(n,"Values in position "+r+" are not the same."),e[r],t[r])},lastIndexOf:function(e,t,n,r){for(var i=t.length;i>=0;i--)if(t[i]===e){n!=i&&YUITest.Assert.fail(YUITest.Assert._formatMessage(r,"Value exists at index "+i+" but should be at index "+n+"."));return}YUITest.Assert.fail(YUITest.Assert._formatMessage(r,"Value doesn't exist in array."))},isUnique:function(t,n,r){YUITest.Assert._increment();if(!e.Lang.isArray(t))throw new TypeError("ArrayAssert.isUnique(): First argument must be an array");if(e.Lang.isValue(n)&&!e.Lang.isFunction(n))throw new TypeError("ArrayAssert.isUnique(): Second argument must be a function");e.Array.unique(t,n).length<t.length&&(r=YUITest.Assert._formatMessage(r,"Array contains duplicate(s)"),YUITest.Assert.fail(r))}},YUITest.Assert={_asserts:0,_formatMessage:function(e,t){return typeof e=="string"&&e.length>0?e.replace("{message}",t):t},_getCount:function(){return this._asserts},_increment:function(){this._asserts++},_reset:function(){this._asserts=0},fail:function(e){throw new YUITest.AssertionError(YUITest.Assert._formatMessage(e,"Test force-failed."))},pass:function(e){YUITest.Assert._increment()},areEqual:function(e,t,n){YUITest.Assert._increment();if(e!=t)throw new YUITest.ComparisonFailure(YUITest.Assert._formatMessage(n,"Values should be equal."),e,t)},areNotEqual:function(e,t,n){YUITest.Assert._increment();if(e==t)throw new YUITest.UnexpectedValue(YUITest.Assert._formatMessage(n,"Values should not be equal."),e)},areNotSame:function(e,t,n){YUITest.Assert._increment();if(e===t)throw new YUITest.UnexpectedValue(YUITest.Assert._formatMessage(n,"Values should not be the same."),e)},areSame:function(e,t,n){YUITest.Assert._increment();if(e!==t)throw new YUITest.ComparisonFailure(YUITest.Assert._formatMessage(n,"Values should be the same."),e,t)},isFalse:function(e,t){YUITest.Assert._increment();if(!1!==e)throw new YUITest.ComparisonFailure(YUITest.Assert._formatMessage(t,"Value should be false."),!1,e)},isTrue:function(e,t){YUITest.Assert._increment();if(!0!==e)throw new YUITest.ComparisonFailure(YUITest.Assert._formatMessage(t,"Value should be true."),!0,e)},isNaN:function(e,t){YUITest.Assert._increment();if(!isNaN(e))throw new YUITest.ComparisonFailure(YUITest.Assert._formatMessage(t,"Value should be NaN."),NaN,e)},isNotNaN:function(e,t){YUITest.Assert._increment();if(isNaN(e))throw new YUITest.UnexpectedValue(YUITest.Assert._formatMessage(t,"Values should not be NaN."),NaN)},isNotNull:function(e,t){YUITest.Assert._increment();if(e===null)throw new YUITest.UnexpectedValue(YUITest.Assert._formatMessage(t,"Values should not be null."),null)},isNotUndefined:function(e,t){YUITest.Assert._increment();if(typeof e=="undefined")throw new YUITest.UnexpectedValue(YUITest.Assert._formatMessage(t,"Value should not be undefined."),undefined)},isNull:function(e,t){YUITest.Assert._increment();if(e!==null)throw new YUITest.ComparisonFailure(YUITest.Assert._formatMessage(t,"Value should be null."),null,e)},isUndefined:function(e,t){YUITest.Assert._increment();if(typeof e!="undefined")throw new YUITest.ComparisonFailure(YUITest.Assert._formatMessage(t,"Value should be undefined."),undefined,e)},isArray:function(e,t){YUITest.Assert._increment();var n=!1;Array.isArray?n=!Array.isArray(e):n=Object.prototype.toString.call(e)!="[object Array]";if(n)throw new YUITest.UnexpectedValue(YUITest.Assert._formatMessage(t,"Value should be an array."),e)},isBoolean:function(e,t){YUITest.Assert._increment();if(typeof e!="boolean")throw new YUITest.UnexpectedValue(YUITest.Assert._formatMessage(t,"Value should be a Boolean."),e)},isFunction:function(e,t){YUITest.Assert._increment();if(!(e instanceof Function))throw new YUITest.UnexpectedValue(YUITest.Assert._formatMessage(t,"Value should be a function."),e)},isInstanceOf:function(e,t,n){YUITest.Assert._increment();if(!(t instanceof e))throw new YUITest.ComparisonFailure(YUITest.Assert._formatMessage(n,"Value isn't an instance of expected type."),e,t)},isNumber:function(e,t){YUITest.Assert._increment();if(typeof e!="number")throw new YUITest.UnexpectedValue(YUITest.Assert._formatMessage(t,"Value should be a number."),e)},isObject:function(e,t){YUITest.Assert._increment();if(!e||typeof e!="object"&&typeof e!="function")throw new YUITest.UnexpectedValue(YUITest.Assert._formatMessage(t,"Value should be an object."),e)},isString:function(e,t){YUITest.Assert._increment();if(typeof e!="string")throw new YUITest.UnexpectedValue(YUITest.Assert._formatMessage(t,"Value should be a string."),e)},isTypeOf:function(e,t,n){YUITest.Assert._increment();if(typeof t!=e)throw new YUITest.ComparisonFailure(YUITest.Assert._formatMessage(n,"Value should be of type "+e+"."),e,typeof t)},throwsError:function(e,t,n){YUITest.Assert._increment();var r=!1;try{t()}catch(i){if(typeof e=="string")i.message!=e&&(r=!0);else if(typeof e=="function")i instanceof e||(r=!0);else if(typeof e=="object"&&e!==null){if(!(i instanceof e.constructor)||i.message!=e.message)r=!0}else r=!0;if(r)throw new YUITest.UnexpectedError(i);return}throw new YUITest.AssertionError(YUITest.Assert._formatMessage(n,"Error should have been thrown."))}},YUITest.AssertionError=function(e){this.message=e,this.name="Assert Error"},YUITest.AssertionError.prototype={constructor:YUITest.AssertionError,getMessage:function(){return this.message},toString:function(){return this.name+": "+this.getMessage()}},YUITest.ComparisonFailure=function(e,t,n){YUITest.AssertionError.call(this,e),this.expected=t,this.actual=n,this.name="ComparisonFailure"},YUITest.ComparisonFailure.prototype=new YUITest.AssertionError,YUITest
+.ComparisonFailure.prototype.constructor=YUITest.ComparisonFailure,YUITest.ComparisonFailure.prototype.getMessage=function(){return this.message+"\nExpected: "+this.expected+" ("+typeof this.expected+")"+"\nActual: "+this.actual+" ("+typeof this.actual+")"},YUITest.CoverageFormat={JSON:function(e){return YUITest.Util.JSON.stringify(e)},XdebugJSON:function(e){var t={};for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n].lines);return YUITest.Util.JSON.stringify(e)}},YUITest.DateAssert={datesAreEqual:function(e,t,n){YUITest.Assert._increment();if(!(e instanceof Date&&t instanceof Date))throw new TypeError("YUITest.DateAssert.datesAreEqual(): Expected and actual values must be Date objects.");var r="";e.getFullYear()!=t.getFullYear()&&(r="Years should be equal."),e.getMonth()!=t.getMonth()&&(r="Months should be equal."),e.getDate()!=t.getDate()&&(r="Days of month should be equal.");if(r.length)throw new YUITest.ComparisonFailure(YUITest.Assert._formatMessage(n,r),e,t)},timesAreEqual:function(e,t,n){YUITest.Assert._increment();if(!(e instanceof Date&&t instanceof Date))throw new TypeError("YUITest.DateAssert.timesAreEqual(): Expected and actual values must be Date objects.");var r="";e.getHours()!=t.getHours()&&(r="Hours should be equal."),e.getMinutes()!=t.getMinutes()&&(r="Minutes should be equal."),e.getSeconds()!=t.getSeconds()&&(r="Seconds should be equal.");if(r.length)throw new YUITest.ComparisonFailure(YUITest.Assert._formatMessage(n,r),e,t)}},YUITest.Mock=function(e){e=e||{};var t,n;try{function r(){}r.prototype=e,t=new r}catch(i){t={}}for(n in e)e.hasOwnProperty(n)&&typeof e[n]=="function"&&(t[n]=function(e){return function(){YUITest.Assert.fail("Method "+e+"() was called but was not expected to be.")}}(n));return t},YUITest.Mock.expect=function(e,t){e.__expectations||(e.__expectations={});if(t.method){var n=t.method,r=t.args||[],i=t.returns,s=typeof t.callCount=="number"?t.callCount:1,o=t.error,u=t.run||function(){},a,f;e.__expectations[n]=t,t.callCount=s,t.actualCallCount=0;for(f=0;f<r.length;f++)r[f]instanceof YUITest.Mock.Value||(r[f]=YUITest.Mock.Value(YUITest.Assert.areSame,[r[f]],"Argument "+f+" of "+n+"() is incorrect."));s>0?e[n]=function(){try{t.actualCallCount++,YUITest.Assert.areEqual(r.length,arguments.length,"Method "+n+"() passed incorrect number of arguments.");for(var e=0,s=r.length;e<s;e++)r[e].verify(arguments[e]);a=u.apply(this,arguments);if(o)throw o}catch(f){YUITest.TestRunner._handleError(f)}return t.hasOwnProperty("returns")?i:a}:e[n]=function(){try{YUITest.Assert.fail("Method "+n+"() should not have been called.")}catch(e){YUITest.TestRunner._handleError(e)}}}else t.property&&(e.__expectations[t.property]=t)},YUITest.Mock.verify=function(e){try{for(var t in e.__expectations)if(e.__expectations.hasOwnProperty(t)){var n=e.__expectations[t];n.method?YUITest.Assert.areEqual(n.callCount,n.actualCallCount,"Method "+n.method+"() wasn't called the expected number of times."):n.property&&YUITest.Assert.areEqual(n.value,e[n.property],"Property "+n.property+" wasn't set to the correct value.")}}catch(r){YUITest.TestRunner._handleError(r)}},YUITest.Mock.Value=function(e,t,n){if(!(this instanceof YUITest.Mock.Value))return new YUITest.Mock.Value(e,t,n);this.verify=function(r){var i=[].concat(t||[]);i.push(r),i.push(n),e.apply(null,i)}},YUITest.Mock.Value.Any=YUITest.Mock.Value(function(){}),YUITest.Mock.Value.Boolean=YUITest.Mock.Value(YUITest.Assert.isBoolean),YUITest.Mock.Value.Number=YUITest.Mock.Value(YUITest.Assert.isNumber),YUITest.Mock.Value.String=YUITest.Mock.Value(YUITest.Assert.isString),YUITest.Mock.Value.Object=YUITest.Mock.Value(YUITest.Assert.isObject),YUITest.Mock.Value.Function=YUITest.Mock.Value(YUITest.Assert.isFunction),YUITest.ObjectAssert={areEqual:function(e,t,n){YUITest.Assert._increment();var r=YUITest.Object.keys(e),i=YUITest.Object.keys(t);r.length!=i.length&&YUITest.Assert.fail(YUITest.Assert._formatMessage(n,"Object should have "+r.length+" keys but has "+i.length));for(var s in e)if(e.hasOwnProperty(s)&&e[s]!=t[s])throw new YUITest.ComparisonFailure(YUITest.Assert._formatMessage(n,"Values should be equal for property "+s),e[s],t[s])},hasKey:function(e,t,n){YUITest.ObjectAssert.ownsOrInheritsKey(e,t,n)},hasKeys:function(e,t,n){YUITest.ObjectAssert.ownsOrInheritsKeys(e,t,n)},inheritsKey:function(e,t,n){YUITest.Assert._increment(),e in t&&!t.hasOwnProperty(e)||YUITest.Assert.fail(YUITest.Assert._formatMessage(n,"Property '"+e+"' not found on object instance."))},inheritsKeys:function(e,t,n){YUITest.Assert._increment();for(var r=0;r<e.length;r++)propertyName in t&&!t.hasOwnProperty(e[r])||YUITest.Assert.fail(YUITest.Assert._formatMessage(n,"Property '"+e[r]+"' not found on object instance."))},ownsKey:function(e,t,n){YUITest.Assert._increment(),t.hasOwnProperty(e)||YUITest.Assert.fail(YUITest.Assert._formatMessage(n,"Property '"+e+"' not found on object instance."))},ownsKeys:function(e,t,n){YUITest.Assert._increment();for(var r=0;r<e.length;r++)t.hasOwnProperty(e[r])||YUITest.Assert.fail(YUITest.Assert._formatMessage(n,"Property '"+e[r]+"' not found on object instance."))},ownsNoKeys:function(e,t){YUITest.Assert._increment();var n=YUITest.Object.keys(e).length;n!==0&&YUITest.Assert.fail(YUITest.Assert._formatMessage(t,"Object owns "+n+" properties but should own none."))},ownsOrInheritsKey:function(e,t,n){YUITest.Assert._increment(),e in t||YUITest.Assert.fail(YUITest.Assert._formatMessage(n,"Property '"+e+"' not found on object."))},ownsOrInheritsKeys:function(e,t,n){YUITest.Assert._increment();for(var r=0;r<e.length;r++)e[r]in t||YUITest.Assert.fail(YUITest.Assert._formatMessage(n,"Property '"+e[r]+"' not found on object."))}},YUITest.Results=function(e){this.name=e,this.passed=0,this.failed=0,this.errors=0,this.ignored=0,this.total=0,this.duration=0},YUITest.Results.prototype.include=function(e){this.passed+=e.passed,this.failed+=e.failed,this.ignored+=e.ignored,this.total+=e.total,this.errors+=e.errors},YUITest.ShouldError=function(e){YUITest.AssertionError
+.call(this,e||"This test should have thrown an error but didn't."),this.name="ShouldError"},YUITest.ShouldError.prototype=new YUITest.AssertionError,YUITest.ShouldError.prototype.constructor=YUITest.ShouldError,YUITest.ShouldFail=function(e){YUITest.AssertionError.call(this,e||"This test should fail but didn't."),this.name="ShouldFail"},YUITest.ShouldFail.prototype=new YUITest.AssertionError,YUITest.ShouldFail.prototype.constructor=YUITest.ShouldFail,YUITest.UnexpectedError=function(e){YUITest.AssertionError.call(this,"Unexpected error: "+e.message),this.cause=e,this.name="UnexpectedError",this.stack=e.stack},YUITest.UnexpectedError.prototype=new YUITest.AssertionError,YUITest.UnexpectedError.prototype.constructor=YUITest.UnexpectedError,YUITest.UnexpectedValue=function(e,t){YUITest.AssertionError.call(this,e),this.unexpected=t,this.name="UnexpectedValue"},YUITest.UnexpectedValue.prototype=new YUITest.AssertionError,YUITest.UnexpectedValue.prototype.constructor=YUITest.UnexpectedValue,YUITest.UnexpectedValue.prototype.getMessage=function(){return this.message+"\nUnexpected: "+this.unexpected+" ("+typeof this.unexpected+") "},YUITest.Wait=function(e,t){this.segment=typeof e=="function"?e:null,this.delay=typeof t=="number"?t:0},e.Test=YUITest,e.Object.each(YUITest,function(t,n){var n=n.replace("Test","");e.Test[n]=t})),e.Assert=YUITest.Assert,e.Assert.Error=e.Test.AssertionError,e.Assert.ComparisonFailure=e.Test.ComparisonFailure,e.Assert.UnexpectedValue=e.Test.UnexpectedValue,e.Mock=e.Test.Mock,e.ObjectAssert=e.Test.ObjectAssert,e.ArrayAssert=e.Test.ArrayAssert,e.DateAssert=e.Test.DateAssert,e.Test.ResultsFormat=e.Test.TestFormat;var n=e.Test.ArrayAssert.itemsAreEqual;e.Test.ArrayAssert.itemsAreEqual=function(t,r,i){return n.call(this,e.Array(t),e.Array(r),i)},e.assert=function(t,n){e.Assert._increment();if(!t)throw new e.Assert.Error(e.Assert._formatMessage(n,"Assertion failed."))},e.fail=e.Assert.fail,e.Test.Runner.once=e.Test.Runner.subscribe,e.Test.Runner.disableLogging=function(){e.Test.Runner._log=!1},e.Test.Runner.enableLogging=function(){e.Test.Runner._log=!0},e.Test.Runner._ignoreEmpty=!0,e.Test.Runner._log=!0,e.Test.Runner.on=e.Test.Runner.attach;if(!YUI.YUITest){e.config.win&&(e.config.win.YUITest=YUITest),YUI.YUITest=e.Test;var r=function(t){var n="",r="";switch(t.type){case this.BEGIN_EVENT:n="Testing began at "+(new Date).toString()+".",r="info";break;case this.COMPLETE_EVENT:n=e.Lang.sub("Testing completed at "+(new Date).toString()+".\n"+"Passed:{passed} Failed:{failed} "+"Total:{total} ({ignored} ignored)",t.results),r="info";break;case this.TEST_FAIL_EVENT:n=t.testName+": failed.\n"+t.error.getMessage(),r="fail";break;case this.TEST_IGNORE_EVENT:n=t.testName+": ignored.",r="ignore";break;case this.TEST_PASS_EVENT:n=t.testName+": passed.",r="pass";break;case this.TEST_SUITE_BEGIN_EVENT:n='Test suite "'+t.testSuite.name+'" started.',r="info";break;case this.TEST_SUITE_COMPLETE_EVENT:n=e.Lang.sub('Test suite "'+t.testSuite.name+'" completed'+".\n"+"Passed:{passed} Failed:{failed} "+"Total:{total} ({ignored} ignored)",t.results),r="info";break;case this.TEST_CASE_BEGIN_EVENT:n='Test case "'+t.testCase.name+'" started.',r="info";break;case this.TEST_CASE_COMPLETE_EVENT:n=e.Lang.sub('Test case "'+t.testCase.name+'" completed.\n'+"Passed:{passed} Failed:{failed} "+"Total:{total} ({ignored} ignored)",t.results),r="info";break;default:n="Unexpected event "+t.type,r="info"}e.Test.Runner._log&&e.log(n,r,"TestRunner")},i,s;for(i in e.Test.Runner)s=e.Test.Runner[i],i.indexOf("_EVENT")>-1&&e.Test.Runner.subscribe(s,r)}},"3.17.2",{requires:["event-simulate","event-custom","json-stringify"]});
diff --git a/js/yui3/text-accentfold/text-accentfold-min.js b/js/yui3/text-accentfold/text-accentfold-min.js
new file mode 100644
index 000000000..be4d670f4
--- /dev/null
+++ b/js/yui3/text-accentfold/text-accentfold-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("text-accentfold",function(e,t){var n=e.Array,r=e.Text,i=r.Data.AccentFold,s={canFold:function(e){var t;for(t in i)if(i.hasOwnProperty(t)&&e.search(i[t])!==-1)return!0;return!1},compare:function(e,t,n){var r=s.fold(e),i=s.fold(t);return n?!!n(r,i):r===i},filter:function(e,t){return n.filter(e,function(e){return t(s.fold(e))})},fold:function(t){return e.Lang.isArray(t)?n.map(t,s.fold):(t=t.toLowerCase(),e.Object.each(i,function(e,n){t=t.replace(e,n)}),t)}};r.AccentFold=s},"3.17.2",{requires:["array-extras","text-data-accentfold"]});
diff --git a/js/yui3/text-data-accentfold/text-data-accentfold-min.js b/js/yui3/text-data-accentfold/text-data-accentfold-min.js
new file mode 100644
index 000000000..6c5a2acfc
--- /dev/null
+++ b/js/yui3/text-data-accentfold/text-data-accentfold-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("text-data-accentfold",function(e,t){e.namespace("Text.Data").AccentFold={0:/[\u2070\u2080\u24EA\uFF10]/gi,1:/[\u00B9\u2081\u2460\uFF11]/gi,2:/[\u00B2\u2082\u2461\uFF12]/gi,3:/[\u00B3\u2083\u2462\uFF13]/gi,4:/[\u2074\u2084\u2463\uFF14]/gi,5:/[\u2075\u2085\u2464\uFF15]/gi,6:/[\u2076\u2086\u2465\uFF16]/gi,7:/[\u2077\u2087\u2466\uFF17]/gi,8:/[\u2078\u2088\u2467\uFF18]/gi,9:/[\u2079\u2089\u2468\uFF19]/gi,a:/[\u00AA\u00E0-\u00E5\u0101\u0103\u0105\u01CE\u01DF\u01E1\u01FB\u0201\u0203\u0227\u1D43\u1E01\u1E9A\u1EA1\u1EA3\u1EA5\u1EA7\u1EA9\u1EAB\u1EAD\u1EAF\u1EB1\u1EB3\u1EB5\u1EB7\u24D0\uFF41]/gi,b:/[\u1D47\u1E03\u1E05\u1E07\u24D1\uFF42]/gi,c:/[\u00E7\u0107\u0109\u010B\u010D\u1D9C\u1E09\u24D2\uFF43]/gi,d:/[\u010F\u1D48\u1E0B\u1E0D\u1E0F\u1E11\u1E13\u217E\u24D3\uFF44]/gi,e:/[\u00E8-\u00EB\u0113\u0115\u0117\u0119\u011B\u0205\u0207\u0229\u1D49\u1E15\u1E17\u1E19\u1E1B\u1E1D\u1EB9\u1EBB\u1EBD\u1EBF\u1EC1\u1EC3\u1EC5\u1EC7\u2091\u212F\u24D4\uFF45]/gi,f:/[\u1DA0\u1E1F\u24D5\uFF46]/gi,g:/[\u011D\u011F\u0121\u0123\u01E7\u01F5\u1D4D\u1E21\u210A\u24D6\uFF47]/gi,h:/[\u0125\u021F\u02B0\u1E23\u1E25\u1E27\u1E29\u1E2B\u1E96\u210E\u24D7\uFF48]/gi,i:/[\u00EC-\u00EF\u0129\u012B\u012D\u012F\u0133\u01D0\u0209\u020B\u1D62\u1E2D\u1E2F\u1EC9\u1ECB\u2071\u2139\u2170\u24D8\uFF49]/gi,j:/[\u0135\u01F0\u02B2\u24D9\u2C7C\uFF4A]/gi,k:/[\u0137\u01E9\u1D4F\u1E31\u1E33\u1E35\u24DA\uFF4B]/gi,l:/[\u013A\u013C\u013E\u0140\u01C9\u02E1\u1E37\u1E39\u1E3B\u1E3D\u2113\u217C\u24DB\uFF4C]/gi,m:/[\u1D50\u1E3F\u1E41\u1E43\u217F\u24DC\uFF4D]/gi,n:/[\u00F1\u0144\u0146\u0148\u01F9\u1E45\u1E47\u1E49\u1E4B\u207F\u24DD\uFF4E]/gi,o:/[\u00BA\u00F2-\u00F6\u014D\u014F\u0151\u01A1\u01D2\u01EB\u01ED\u020D\u020F\u022B\u022D\u022F\u0231\u1D52\u1E4D\u1E4F\u1E51\u1E53\u1ECD\u1ECF\u1ED1\u1ED3\u1ED5\u1ED7\u1ED9\u1EDB\u1EDD\u1EDF\u1EE1\u1EE3\u2092\u2134\u24DE\uFF4F]/gi,p:/[\u1D56\u1E55\u1E57\u24DF\uFF50]/gi,q:/[\u02A0\u24E0\uFF51]/gi,r:/[\u0155\u0157\u0159\u0211\u0213\u02B3\u1D63\u1E59\u1E5B\u1E5D\u1E5F\u24E1\uFF52]/gi,s:/[\u015B\u015D\u015F\u0161\u017F\u0219\u02E2\u1E61\u1E63\u1E65\u1E67\u1E69\u1E9B\u24E2\uFF53]/gi,t:/[\u0163\u0165\u021B\u1D57\u1E6B\u1E6D\u1E6F\u1E71\u1E97\u24E3\uFF54]/gi,u:/[\u00F9-\u00FC\u0169\u016B\u016D\u016F\u0171\u0173\u01B0\u01D4\u01D6\u01D8\u01DA\u01DC\u0215\u0217\u1D58\u1D64\u1E73\u1E75\u1E77\u1E79\u1E7B\u1EE5\u1EE7\u1EE9\u1EEB\u1EED\u1EEF\u1EF1\u24E4\uFF55]/gi,v:/[\u1D5B\u1D65\u1E7D\u1E7F\u2174\u24E5\uFF56]/gi,w:/[\u0175\u02B7\u1E81\u1E83\u1E85\u1E87\u1E89\u1E98\u24E6\uFF57]/gi,x:/[\u02E3\u1E8B\u1E8D\u2093\u2179\u24E7\uFF58]/gi,y:/[\u00FD\u00FF\u0177\u0233\u02B8\u1E8F\u1E99\u1EF3\u1EF5\u1EF7\u1EF9\u24E8\uFF59]/gi,z:/[\u017A\u017C\u017E\u1DBB\u1E91\u1E93\u1E95\u24E9\uFF5A]/gi}},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/text-data-wordbreak/text-data-wordbreak-min.js b/js/yui3/text-data-wordbreak/text-data-wordbreak-min.js
new file mode 100644
index 000000000..905e9dd2b
--- /dev/null
+++ b/js/yui3/text-data-wordbreak/text-data-wordbreak-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("text-data-wordbreak",function(e,t){e.namespace("Text.Data").WordBreak={aletter:"[A-Za-z\u00aa\u00b5\u00ba\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f3\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097f\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58\u0c59\u0c60\u0c61\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d60\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u10a0-\u10c5\u10d0-\u10fa\u10fc\u1100-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191c\u1a00-\u1a16\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bc0-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1ce9-\u1cec\u1cee-\u1cf1\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u24b6-\u24e9\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2d00-\u2d25\u2d30-\u2d65\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005\u303b\u303c\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua697\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790\ua791\ua7a0-\ua7a9\ua7fa-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uffa0-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]",midnumlet:"['\\.\u2018\u2019\u2024\ufe52\uff07\uff0e]",midletter:"[:\u00b7\u00b7\u05f4\u2027\ufe13\ufe55\uff1a]",midnum:"[,;;\u0589\u060c\u060d\u066c\u07f8\u2044\ufe10\ufe14\ufe50\ufe54\uff0c\uff1b]",numeric:"[0-9\u0660-\u0669\u066b\u06f0-\u06f9\u07c0-\u07c9\u0966-\u096f\u09e6-\u09ef\u0a66-\u0a6f\u0ae6-\u0aef\u0b66-\u0b6f\u0be6-\u0bef\u0c66-\u0c6f\u0ce6-\u0cef\u0d66-\u0d6f\u0e50-\u0e59\u0ed0-\u0ed9\u0f20-\u0f29\u1040-\u1049\u1090-\u1099\u17e0-\u17e9\u1810-\u1819\u1946-\u194f\u19d0-\u19d9\u1a80-\u1a89\u1a90-\u1a99\u1b50-\u1b59\u1bb0-\u1bb9\u1c40-\u1c49\u1c50-\u1c59\ua620-\ua629\ua8d0-\ua8d9\ua900-\ua909\ua9d0-\ua9d9\uaa50-\uaa59\uabf0-\uabf9]",cr:"\\r",lf:"\\n",newline:"[ \f\u0085\u2028\u2029]",extend:"[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065f\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u0900-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962\u0963\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09cb-\u09cd\u09d7\u09e2\u09e3\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2\u0ae3\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0c01-\u0c03\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0c82\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d02\u0d03\u0d3e-\u0d44\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0d62\u0d63\u0d82\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0df2\u0df3\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f3e\u0f3f\u0f71-\u0f84\u0f86\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102b-\u103e\u1056-\u1059\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f\u109a-\u109d\u135d-\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b6-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u192b\u1930-\u193b\u19b0-\u19c0\u19c8\u19c9\u1a17-\u1a1b\u1a55-\u1a5e\u1a60-\u1a7c\u1a7f\u1b00-\u1b04\u1b34-\u1b44\u1b6b-\u1b73\u1b80-\u1b82\u1ba1-\u1baa\u1be6-\u1bf3\u1c24-\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce8\u1ced\u1cf2\u1dc0-\u1de6\u1dfc-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua823-\ua827\ua880\ua881\ua8b4-\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua953\ua980-\ua983\ua9b3-\ua9c0\uaa29-\uaa36\uaa43\uaa4c\uaa4d\uaa7b\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe3-\uabea\uabec\uabed\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]"
+,format:"[\u00ad\u0600-\u0603\u06dd\u070f\u17b4\u17b5\u200e\u200f\u202a-\u202e\u2060-\u2064\u206a-\u206f\ufeff\ufff9-\ufffb]",katakana:"[\u3031-\u3035\u309b\u309c\u30a0-\u30fa\u30fc-\u30ff\u31f0-\u31ff\u32d0-\u32fe\u3300-\u3357\uff66-\uff9d]",extendnumlet:"[_\u203f\u2040\u2054\ufe33\ufe34\ufe4d-\ufe4f\uff3f]",punctuation:"[!-#%-*,-\\/:;?@\\[-\\]_{}\u00a1\u00ab\u00b7\u00bb\u00bf;\u00b7\u055a-\u055f\u0589\u058a\u05be\u05c0\u05c3\u05c6\u05f3\u05f4\u0609\u060a\u060c\u060d\u061b\u061e\u061f\u066a-\u066d\u06d4\u0700-\u070d\u07f7-\u07f9\u0830-\u083e\u085e\u0964\u0965\u0970\u0df4\u0e4f\u0e5a\u0e5b\u0f04-\u0f12\u0f3a-\u0f3d\u0f85\u0fd0-\u0fd4\u0fd9\u0fda\u104a-\u104f\u10fb\u1361-\u1368\u1400\u166d\u166e\u169b\u169c\u16eb-\u16ed\u1735\u1736\u17d4-\u17d6\u17d8-\u17da\u1800-\u180a\u1944\u1945\u1a1e\u1a1f\u1aa0-\u1aa6\u1aa8-\u1aad\u1b5a-\u1b60\u1bfc-\u1bff\u1c3b-\u1c3f\u1c7e\u1c7f\u1cd3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205e\u207d\u207e\u208d\u208e\u3008\u3009\u2768-\u2775\u27c5\u27c6\u27e6-\u27ef\u2983-\u2998\u29d8-\u29db\u29fc\u29fd\u2cf9-\u2cfc\u2cfe\u2cff\u2d70\u2e00-\u2e2e\u2e30\u2e31\u3001-\u3003\u3008-\u3011\u3014-\u301f\u3030\u303d\u30a0\u30fb\ua4fe\ua4ff\ua60d-\ua60f\ua673\ua67e\ua6f2-\ua6f7\ua874-\ua877\ua8ce\ua8cf\ua8f8-\ua8fa\ua92e\ua92f\ua95f\ua9c1-\ua9cd\ua9de\ua9df\uaa5c-\uaa5f\uaade\uaadf\uabeb\ufd3e\ufd3f\ufe10-\ufe19\ufe30-\ufe52\ufe54-\ufe61\ufe63\ufe68\ufe6a\ufe6b\uff01-\uff03\uff05-\uff0a\uff0c-\uff0f\uff1a\uff1b\uff1f\uff20\uff3b-\uff3d\uff3f\uff5b\uff5d\uff5f-\uff65]"}},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/text-wordbreak/text-wordbreak-min.js b/js/yui3/text-wordbreak/text-wordbreak-min.js
new file mode 100644
index 000000000..ab2d20a3e
--- /dev/null
+++ b/js/yui3/text-wordbreak/text-wordbreak-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("text-wordbreak",function(e,t){var n=e.Text,r=n.Data.WordBreak,i=0,s=1,o=2,u=3,a=4,f=5,l=6,c=7,h=8,p=9,d=10,v=11,m=12,g=[new RegExp(r.aletter),new RegExp(r.midnumlet),new RegExp(r.midletter),new RegExp(r.midnum),new RegExp(r.numeric),new RegExp(r.cr),new RegExp(r.lf),new RegExp(r.newline),new RegExp(r.extend),new RegExp(r.format),new RegExp(r.katakana),new RegExp(r.extendnumlet)],y="",b=new RegExp("^"+r.punctuation+"$"),w=/\s/,E={getWords:function(e,t){var n=0,r=E._classify(e),i=r.length,s=[],o=[],u,a,f;t||(t={}),t.ignoreCase&&(e=e.toLowerCase()),a=t.includePunctuation,f=t.includeWhitespace;for(;n<i;++n)u=e.charAt(n),s.push(u),E._isWordBoundary(r,n)&&(s=s.join(y),s&&(f||!w.test(s))&&(a||!b.test(s))&&o.push(s),s=[]);return o},getUniqueWords:function(t,n){return e.Array.unique(E.getWords(t,n))},isWordBoundary:function(e,t){return E._isWordBoundary(E._classify(e),t)},_classify:function(e){var t,n=[],r=0,i,s,o=e.length,u=g.length,a;for(;r<o;++r){t=e.charAt(r),a=m;for(i=0;i<u;++i){s=g[i];if(s&&s.test(t)){a=i;break}}n.push(a)}return n},_isWordBoundary:function(e,t){var n,r=e[t],m=e[t+1],g;return t<0||t>e.length-1&&t!==0?!1:r===i&&m===i?!1:(g=e[t+2],r!==i||m!==o&&m!==s||g!==i?(n=e[t-1],r!==o&&r!==s||m!==i||n!==i?r!==a&&r!==i||m!==a&&m!==i?r!==u&&r!==s||m!==a||n!==a?r!==a||m!==u&&m!==s||g!==a?r===h||r===p||n===h||n===p||m===h||m===p?!1:r===f&&m===l?!1:r===c||r===f||r===l?!0:m===c||m===f||m===l?!0:r===d&&m===d?!1:m!==v||r!==i&&r!==a&&r!==d&&r!==v?r!==v||m!==i&&m!==a&&m!==d?!0:!1:!1:!1:!1:!1:!1):!1)}};n.WordBreak=E},"3.17.2",{requires:["array-extras","text-data-wordbreak"]});
diff --git a/js/yui3/timers/timers-min.js b/js/yui3/timers/timers-min.js
new file mode 100644
index 000000000..379a49eb2
--- /dev/null
+++ b/js/yui3/timers/timers-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("timers",function(e,t){function i(e){return s}function s(e){this.capacity=this.snap(e),this.length=0,this.front=0,this.initialize()}function o(e,t,n,r,i){for(var s=0;s<i;++s)n[s+r]=e[s+t]}function u(e){return e>>>=0,e-=1,e|=e>>1,e|=e>>2,e|=e>>4,e|=e>>8,e|=e>>16,e+1}function v(){while(a.length>0){var e=a.shift();try{e.call()}catch(t){if(d)throw l(),t;setTimeout(function(){throw t},0)}}f=!1}function y(e){d&&p.domain&&(e=p.domain.bind(e)),a.push(e),f||(l(),f=!0)}function b(e){var t;return b._asynchronizer(function(){t||e()}),{cancel:function(){t=1}}}var n={},r=e.config.global;"use strict",n.exports=s,s.prototype.push=function(e){var t=this.length;this.capacity<=t&&this.grow(this.snap(this.capacity*this.growFactor));var n=this.front+t&this.capacity-1;this[n]=e,this.length=t+1},s.prototype.shift=function(){var e=this.front,t=this[e];return this[e]=void 0,this.front=e+1&this.capacity-1,this.length--,t},s.prototype.grow=function(e){var t=this.front,n=this.capacity,r=new Array(n),i=this.length;o(this,0,r,0,n),this.capacity=e,this.initialize(),this.front=0;if(t+i<=n)o(r,t,this,0,i);else{var s=i-(t+i&n-1);o(r,t,this,0,s),o(r,0,this,s,i-s)}},s.prototype.initialize=function(){var e=this.capacity;for(var t=0;t<e;++t)this[t]=void 0},s.prototype.snap=function(e){return typeof e!="number"?this.minCapacity:u(Math.min(this.maxCapacity,Math.max(this.minCapacity,e)))},s.prototype.maxCapacity=1<<30|0,s.prototype.minCapacity=16,s.prototype.growFactor=8,"use strict";var s=i("./queue"),a=new s(1024),f=!1,l=void 0,c=typeof setImmediate=="function",h,p=r.process,d=!!p&&{}.toString.call(p)==="[object process]";if(d)l=function(){var e=p.domain;e&&(h=h||(1,i)("domain"),h.active=p.domain=null),f&&c?setImmediate(v):p.nextTick(v),e&&(h.active=p.domain=e)};else if(c)l=function(){setImmediate(v)};else if(typeof MessageChannel!="undefined"){var m=new MessageChannel;m.port1.onmessage=function(){l=g,m.port1.onmessage=v,v()};var g=function(){m.port2.postMessage(0)};l=function(){setTimeout(v,0),g()}}else l=function(){setTimeout(v,0)};n.exports=y,b._asynchronizer=y,b._impl="asap",e.soon=b},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/transition-timer/transition-timer-min.js b/js/yui3/transition-timer/transition-timer-min.js
new file mode 100644
index 000000000..981000bf4
--- /dev/null
+++ b/js/yui3/transition-timer/transition-timer-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("transition-timer",function(e,t){var n=e.Transition;e.mix(n.prototype,{_start:function(){n.useNative?this._runNative():this._runTimer()},_runTimer:function(){var t=this;t._initAttrs(),n._running[e.stamp(t)]=t,t._startTime=new Date,n._startTimer()},_endTimer:function(){var t=this;delete n._running[e.stamp(t)],t._startTime=null},_runFrame:function(){var e=new Date-this._startTime;this._runAttrs(e)},_runAttrs:function(t){var r=this,i=r._node,s=r._config,o=e.stamp(i),u=n._nodeAttrs[o],a=n.behaviors,f=!1,l=!1,c,h,p,d,v,m,g,y,b;for(h in u)if((p=u[h])&&p.transition===r){g=p.duration,m=p.delay,v=(t-m)/1e3,y=t,c={type:"propertyEnd",propertyName:h,config:s,elapsedTime:v},d=b in a&&"set"in a[b]?a[b].set:n.DEFAULT_SETTER,f=y>=g,y>g&&(y=g);if(!m||t>=m)d(r,h,p.from,p.to,y-m,g-m,p.easing,p.unit),f&&(delete u[h],r._count--,s[h]&&s[h].on&&s[h].on.end&&s[h].on.end.call(e.one(i),c),!l&&r._count<=0&&(l=!0,r._end(v),r._endTimer()))}},_initAttrs:function(){var t=this,r=n.behaviors,i=e.stamp(t._node),s=n._nodeAttrs[i],o,u,a,f,l,c,h,p,d,v,m;for(c in s)(o=s[c])&&o.transition===t&&(u=o.duration*1e3,a=o.delay*1e3,f=o.easing,l=o.value,c in t._node.style||c in e.DOM.CUSTOM_STYLES?(v=c in r&&"get"in r[c]?r[c].get(t,c):n.DEFAULT_GETTER(t,c),p=n.RE_UNITS.exec(v),h=n.RE_UNITS.exec(l),v=p?p[1]:v,m=h?h[1]:l,d=h?h[2]:p?p[2]:"",!d&&n.RE_DEFAULT_UNIT.test(c)&&(d=n.DEFAULT_UNIT),typeof f=="string"&&(f.indexOf("cubic-bezier")>-1?f=f.substring(13,f.length-1).split(","):n.easings[f]&&(f=n.easings[f])),o.from=Number(v),o.to=Number(m),o.unit=d,o.easing=f,o.duration=u+a,o.delay=a):(delete s[c],t._count--))},destroy:function(){this.detachAll(),this._node=null}},!0),e.mix(e.Transition,{_runtimeAttrs:{},RE_DEFAULT_UNIT:/^width|height|top|right|bottom|left|margin.*|padding.*|border.*$/i,DEFAULT_UNIT:"px",intervalTime:20,behaviors:{left:{get:function(t,n){return e.DOM._getAttrOffset(t._node,n)}}},DEFAULT_SETTER:function(t,r,i,s,o,u,a,f){i=Number(i),s=Number(s);var l=t._node,c=n.cubicBezier(a,o/u);c=i+c[0]*(s-i);if(l){if(r in l.style||r in e.DOM.CUSTOM_STYLES)f=f||"",e.DOM.setStyle(l,r,c+f)}else t._end()},DEFAULT_GETTER:function(t,n){var r=t._node,i="";if(n in r.style||n in e.DOM.CUSTOM_STYLES)i=e.DOM.getComputedStyle(r,n);return i},_startTimer:function(){n._timer||(n._timer=setInterval(n._runFrame,n.intervalTime))},_stopTimer:function(){clearInterval(n._timer),n._timer=null},_runFrame:function(){var e=!0,t;for(t in n._running)n._running[t]._runFrame&&(e=!1,n._running[t]._runFrame());e&&n._stopTimer()},cubicBezier:function(e,t){var n=0,r=0,i=e[0],s=e[1],o=e[2],u=e[3],a=1,f=0,l=a-3*o+3*i-n,c=3*o-6*i+3*n,h=3*i-3*n,p=n,d=f-3*u+3*s-r,v=3*u-6*s+3*r,m=3*s-3*r,g=r,y=((l*t+c)*t+h)*t+p,b=((d*t+v)*t+m)*t+g;return[y,b]},easings:{ease:[.25,0,1,.25],linear:[0,0,1,1],"ease-in":[.42,0,1,1],"ease-out":[0,0,.58,1],"ease-in-out":[.42,0,.58,1]},_running:{},_timer:null,RE_UNITS:/^(-?\d*\.?\d*){1}(em|ex|px|in|cm|mm|pt|pc|%)*$/},!0),n.behaviors.top=n.behaviors.bottom=n.behaviors.right=n.behaviors.left,e.Transition=n},"3.17.2",{requires:["transition"]});
diff --git a/js/yui3/transition/transition-min.js b/js/yui3/transition/transition-min.js
new file mode 100644
index 000000000..70dba4a97
--- /dev/null
+++ b/js/yui3/transition/transition-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("transition",function(e,t){var n="",r="",i=e.config.doc,s="documentElement",o=i[s].style,u="transition",a="transitionProperty",f,l,c,h,p,d,v={},m=["Webkit","Moz"],g={Webkit:"webkitTransitionEnd"},y=function(){this.init.apply(this,arguments)};y._TRANSFORM="transform",y._toCamel=function(e){return e=e.replace(/-([a-z])/gi,function(e,t){return t.toUpperCase()}),e},y._toHyphen=function(e){return e=e.replace(/([A-Z]?)([a-z]+)([A-Z]?)/g,function(e,t,n,r){var i=(t?"-"+t.toLowerCase():"")+n;return r&&(i+="-"+r.toLowerCase()),i}),e},y.SHOW_TRANSITION="fadeIn",y.HIDE_TRANSITION="fadeOut",y.useNative=!1,"transition"in o&&"transitionProperty"in o&&"transitionDuration"in o&&"transitionTimingFunction"in o&&"transitionDelay"in o?(y.useNative=!0,y.supported=!0):e.Array.each(m,function(e){var t=e+"Transition";t in i[s].style&&(n=e,r=y._toHyphen(e)+"-",y.useNative=!0,y.supported=!0,y._VENDOR_PREFIX=e)}),typeof o.transform=="undefined"&&e.Array.each(m,function(e){var t=e+"Transform";typeof o[t]!="undefined"&&(y._TRANSFORM=t)}),n&&(u=n+"Transition",a=n+"TransitionProperty"),f=r+"transition-property",l=r+"transition-duration",c=r+"transition-timing-function",h=r+"transition-delay",p="transitionend",d="on"+n.toLowerCase()+"transitionend",p=g[n]||p,y.fx={},y.toggles={},y._hasEnd={},y._reKeywords=/^(?:node|duration|iterations|easing|delay|on|onstart|onend)$/i,e.Node.DOM_EVENTS[p]=1,y.NAME="transition",y.DEFAULT_EASING="ease",y.DEFAULT_DURATION=.5,y.DEFAULT_DELAY=0,y._nodeAttrs={},y.prototype={constructor:y,init:function(e,t){var n=this;return n._node=e,!n._running&&t&&(n._config=t,e._transition=n,n._duration="duration"in t?t.duration:n.constructor.DEFAULT_DURATION,n._delay="delay"in t?t.delay:n.constructor.DEFAULT_DELAY,n._easing=t.easing||n.constructor.DEFAULT_EASING,n._count=0,n._running=!1),n},addProperty:function(t,n){var r=this,i=this._node,s=e.stamp(i),o=e.one(i),u=y._nodeAttrs[s],a,f,l,c,h;u||(u=y._nodeAttrs[s]={}),c=u[t],n&&n.value!==undefined?h=n.value:n!==undefined&&(h=n,n=v),typeof h=="function"&&(h=h.call(o,o)),c&&c.transition&&c.transition!==r&&c.transition._count--,r._count++,l=(typeof n.duration!="undefined"?n.duration:r._duration)||1e-4,u[t]={value:h,duration:l,delay:typeof n.delay!="undefined"?n.delay:r._delay,easing:n.easing||r._easing,transition:r},a=e.DOM.getComputedStyle(i,t),f=typeof h=="string"?a:parseFloat(a),y.useNative&&f===h&&setTimeout(function(){r._onNativeEnd.call(i,{propertyName:t,elapsedTime:l})},l*1e3)},removeProperty:function(t){var n=this,r=y._nodeAttrs[e.stamp(n._node)];r&&r[t]&&(delete r[t],n._count--)},initAttrs:function(t){var n,r=this._node;t.transform&&!t[y._TRANSFORM]&&(t[y._TRANSFORM]=t.transform,delete t.transform);for(n in t)t.hasOwnProperty(n)&&!y._reKeywords.test(n)&&(this.addProperty(n,t[n]),r.style[n]===""&&e.DOM.setStyle(r,n,e.DOM.getComputedStyle(r,n)))},run:function(t){var n=this,r=n._node,i=n._config,s={type:"transition:start",config:i};return n._running||(n._running=!0,i.on&&i.on.start&&i.on.start.call(e.one(r),s),n.initAttrs(n._config),n._callback=t,n._start()),n},_start:function(){this._runNative()},_prepDur:function(e){return e=parseFloat(e)*1e3,e+"ms"},_runNative:function(){var t=this,n=t._node,r=e.stamp(n),i=n.style,s=n.ownerDocument.defaultView.getComputedStyle(n),o=y._nodeAttrs[r],u="",a=s[y._toCamel(f)],d=f+": ",v=l+": ",m=c+": ",g=h+": ",b,w,E;a!=="all"&&(d+=a+",",v+=s[y._toCamel(l)]+",",m+=s[y._toCamel(c)]+",",g+=s[y._toCamel(h)]+",");for(E in o)b=y._toHyphen(E),w=o[E],(w=o[E])&&w.transition===t&&(E in n.style?(v+=t._prepDur(w.duration)+",",g+=t._prepDur(w.delay)+",",m+=w.easing+",",d+=b+",",u+=b+": "+w.value+"; "):this.removeProperty(E));d=d.replace(/,$/,";"),v=v.replace(/,$/,";"),m=m.replace(/,$/,";"),g=g.replace(/,$/,";"),y._hasEnd[r]||(n.addEventListener(p,t._onNativeEnd,""),y._hasEnd[r]=!0),i.cssText+=d+v+m+g+u},_end:function(t){var n=this,r=n._node,i=n._callback,s=n._config,o={type:"transition:end",config:s,elapsedTime:t},u=e.one(r);n._running=!1,n._callback=null,r&&(s.on&&s.on.end?setTimeout(function(){s.on.end.call(u,o),i&&i.call(u,o)},1):i&&setTimeout(function(){i.call(u,o)},1))},_endNative:function(e){var t=this._node,n=t.ownerDocument.defaultView.getComputedStyle(t,"")[y._toCamel(f)];e=y._toHyphen(e),typeof n=="string"&&(n=n.replace(new RegExp("(?:^|,\\s)"+e+",?"),","),n=n.replace(/^,|,$/,""),t.style[u]=n)},_onNativeEnd:function(t){var n=this,r=e.stamp(n),i=t,s=y._toCamel(i.propertyName),o=i.elapsedTime,u=y._nodeAttrs[r],f=u[s],l=f?f.transition:null,c,h;l&&(l.removeProperty(s),l._endNative(s),h=l._config[s],c={type:"propertyEnd",propertyName:s,elapsedTime:o,config:h},h&&h.on&&h.on.end&&h.on.end.call(e.one(n),c),l._count<=0&&(l._end(o),n.style[a]=""))},destroy:function(){var e=this,t=e._node;t&&(t.removeEventListener(p,e._onNativeEnd,!1),e._node=null)}},e.Transition=y,e.TransitionNative=y,e.Node.prototype.transition=function(t,n,r){var i=y._nodeAttrs[e.stamp(this._node)],s=i?i.transition||null:null,o,u;if(typeof t=="string"){typeof n=="function"&&(r=n,n=null),o=y.fx[t];if(n&&typeof n=="object"){n=e.clone(n);for(u in o)o.hasOwnProperty(u)&&(u in n||(n[u]=o[u]))}else n=o}else r=n,n=t;return s&&!s._running?s.init(this,n):s=new y(this._node,n),s.run(r),this},e.Node.prototype.show=function(t,n,r){return this._show(),t&&e.Transition&&(typeof t!="string"&&!t.push&&(typeof n=="function"&&(r=n,n=t),t=y.SHOW_TRANSITION),this.transition(t,n,r)),this},e.NodeList.prototype.show=function(t,n,r){var i=this._nodes,s=0,o;while(o=i[s++])e.one(o).show(t,n,r);return this};var b=function(e,t,n){return function(){t&&t.call(e),n&&typeof n=="function"&&n.apply(e._node,arguments)}};e.Node.prototype.hide=function(t,n,r){return t&&e.Transition?(typeof n=="function"&&(r=n,n=null),r=b(this,this._hide,r),typeof t!="string"&&!t.push&&(typeof n=="function"&&(r=n,n=t),t=y.HIDE_TRANSITION),this.transition(t,n,r)):this._hide(),this},e.NodeList.prototype.hide=function(t,n,r){var i=this._nodes,s=0,o;while(o=i[s++])e.one(o).hide(t,n,r);return this},e.NodeList.prototype
+.transition=function(t,n,r){var i=this._nodes,s=this.size(),o=0,r=r===!0,u;while(u=i[o++])o<s&&r?e.one(u).transition(t):e.one(u).transition(t,n);return this},e.Node.prototype.toggleView=function(t,n,r){this._toggles=this._toggles||[],r=arguments[arguments.length-1];if(typeof t!="string"){n=t,this._toggleView(n,r);return}return typeof n=="function"&&(n=undefined),typeof n=="undefined"&&t in this._toggles&&(n=!this._toggles[t]),n=n?1:0,n?this._show():r=b(this,this._hide,r),this._toggles[t]=n,this.transition(e.Transition.toggles[t][n],r),this},e.NodeList.prototype.toggleView=function(t,n,r){var i=this._nodes,s=0,o;while(o=i[s++])o=e.one(o),o.toggleView.apply(o,arguments);return this},e.mix(y.fx,{fadeOut:{opacity:0,duration:.5,easing:"ease-out"},fadeIn:{opacity:1,duration:.5,easing:"ease-in"},sizeOut:{height:0,width:0,duration:.75,easing:"ease-out"},sizeIn:{height:function(e){return e.get("scrollHeight")+"px"},width:function(e){return e.get("scrollWidth")+"px"},duration:.5,easing:"ease-in",on:{start:function(){var e=this.getStyle("overflow");e!=="hidden"&&(this.setStyle("overflow","hidden"),this._transitionOverflow=e)},end:function(){this._transitionOverflow&&(this.setStyle("overflow",this._transitionOverflow),delete this._transitionOverflow)}}}}),e.mix(y.toggles,{size:["sizeOut","sizeIn"],fade:["fadeOut","fadeIn"]})},"3.17.2",{requires:["node-style"]});
diff --git a/js/yui3/tree-labelable/tree-labelable-min.js b/js/yui3/tree-labelable/tree-labelable-min.js
new file mode 100644
index 000000000..7502e854a
--- /dev/null
+++ b/js/yui3/tree-labelable/tree-labelable-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("tree-labelable",function(e,t){function n(){}function r(e,t){this._serializable=this._serializable.concat("label"),"label"in t&&(this.label=t.label)}n.prototype={initializer:function(){this.nodeExtensions=this.nodeExtensions.concat(e.Tree.Node.Labelable)}},e.Tree.Labelable=n,r.prototype={label:""},e.Tree.Node.Labelable=r},"3.17.2",{requires:["tree"]});
diff --git a/js/yui3/tree-lazy/tree-lazy-min.js b/js/yui3/tree-lazy/tree-lazy-min.js
new file mode 100644
index 000000000..0a1021a07
--- /dev/null
+++ b/js/yui3/tree-lazy/tree-lazy-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("tree-lazy",function(e,t){var n="beforeLoad",r="error",i="load";e.namespace("Plugin.Tree").Lazy=e.Base.create("lazyTreePlugin",e.Plugin.Base,[],{initializer:function(e){this._host=e.host,e.load&&(this.load=e.load),!this._host.openNode,this._published={},this._attachEvents()},load:function(e,t){t(new Error("Plugin.Tree.Lazy: Please provide a custom `load` method when instantiating this plugin."))},_attachEvents:function(){this.onHostEvent("open",this._onOpen)},_onOpen:function(e){var t=e.node;if(!t.canHaveChildren||t.state.loaded||t.state.loading)return;this._published[n]||(this._published[n]=this.publish(n,{defaultFn:this._defLoadingFn})),this.fire(n,{node:t})},_defLoadingFn:function(e){var t=e.node,n=this;t.state.loading=!0,this.load(t,function(e){delete t.state.loading;if(e){n.fire(r,{error:e,src:"load"});return}t.state.loaded=!0,n.fire(i,{node:t})})}},{NS:"lazy"})},"3.17.2",{requires:["base-pluginhost","plugin","tree"]});
diff --git a/js/yui3/tree-node/tree-node-min.js b/js/yui3/tree-node/tree-node-min.js
new file mode 100644
index 000000000..3c7132e64
--- /dev/null
+++ b/js/yui3/tree-node/tree-node-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("tree-node",function(e,t){function n(t,n){n||(n={}),this.id=this._yuid=n.id||this.id||e.guid("treeNode-"),this.tree=t,this.children=n.children||[],this.data=n.data||{},this.state=n.state||{},n.canHaveChildren?this.canHaveChildren=n.canHaveChildren:this.children.length&&(this.canHaveChildren=!0),e.mix(this,n);for(var r=0,i=this.children.length;r<i;r++)this.children[r].parent=this}n.prototype={_isIndexStale:!0,_isYUITreeNode:!0,_serializable:["canHaveChildren","data","id","state"],append:function(e,t){return this.tree.appendNode(this,e,t)},depth:function(){if(this.isRoot())return 0;var e=0,t=this.parent;while(t)e+=1,t=t.parent;return e},empty:function(e){return this.tree.emptyNode(this,e)},find:function(e,t,n){return this.tree.findNode(this,e,t,n)},hasChildren:function(){return!!this.children.length},index:function(){return this.parent?this.parent.indexOf(this):-1},indexOf:function(e){var t;return this._isIndexStale&&this._reindex(),t=this._indexMap[e.id],typeof t=="undefined"?-1:t},insert:function(e,t){return this.tree.insertNode(this,e,t)},isInTree:function(){return this.tree&&this.tree.rootNode===this?!0:!!this.parent&&!!this.parent.isInTree()},isRoot:function(){return!!this.tree&&this.tree.rootNode===this},next:function(){if(this.parent)return this.parent.children[this.index()+1]},prepend:function(e,t){return this.tree.prependNode(this,e,t)},previous:function(){if(this.parent)return this.parent.children[this.index()-1]},remove:function(e){return this.tree.removeNode(this,e)},size:function(){var e=this.children,t=e.length,n=t;for(var r=0;r<t;r++)n+=e[r].size();return n},toJSON:function(){var e={},t=this.state,n,r,i;if(t.destroyed)return null;for(n=0,i=this._serializable.length;n<i;n++)r=this._serializable[n],r in this&&(e[r]=this[r]);if(this.canHaveChildren){e.children=[];for(n=0,i=this.children.length;n<i;n++)e.children.push(this.children[n].toJSON())}return e},traverse:function(e,t,n){return this.tree.traverseNode(this,e,t,n)},_reindex:function(){var e=this.children,t={},n,r;for(n=0,r=e.length;n<r;n++)t[e[n].id]=n;this._indexMap=t,this._isIndexStale=!1}},e.namespace("Tree").Node=n},"3.17.2");
diff --git a/js/yui3/tree-openable/tree-openable-min.js b/js/yui3/tree-openable/tree-openable-min.js
new file mode 100644
index 000000000..0cdc24282
--- /dev/null
+++ b/js/yui3/tree-openable/tree-openable-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("tree-openable",function(e,t){function i(){}function s(){}var n="close",r="open";i.prototype={initializer:function(){this.nodeExtensions=this.nodeExtensions.concat(e.Tree.Node.Openable)},closeNode:function(e,t){return e.canHaveChildren&&e.isOpen()&&this._fireTreeEvent(n,{node:e,src:t&&t.src},{defaultFn:this._defCloseFn,silent:t&&t.silent}),this},openNode:function(e,t){return e.canHaveChildren&&!e.isOpen()&&this._fireTreeEvent(r,{node:e,src:t&&t.src},{defaultFn:this._defOpenFn,silent:t&&t.silent}),this},toggleOpenNode:function(e,t){return e.isOpen()?this.closeNode(e,t):this.openNode(e,t)},_defCloseFn:function(e){delete e.node.state.open},_defOpenFn:function(e){e.node.state.open=!0}},e.Tree.Openable=i,s.prototype={close:function(e){return this.tree.closeNode(this,e),this},isOpen:function(){return!!this.state.open||this.isRoot()},open:function(e){return this.tree.openNode(this,e),this},toggleOpen:function(e){return this.tree.toggleOpenNode(this,e),this}},e.Tree.Node.Openable=s},"3.17.2",{requires:["tree"]});
diff --git a/js/yui3/tree-selectable/tree-selectable-min.js b/js/yui3/tree-selectable/tree-selectable-min.js
new file mode 100644
index 000000000..53caa51c9
--- /dev/null
+++ b/js/yui3/tree-selectable/tree-selectable-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("tree-selectable",function(e,t){function s(){}function o(){}var n=e.Do,r="select",i="unselect";s.prototype={initializer:function(){this.nodeExtensions=this.nodeExtensions.concat(e.Tree.Node.Selectable),this._selectedMap={},n.after(this._selectableAfterDefAddFn,this,"_defAddFn"),n.after(this._selectableAfterDefClearFn,this,"_defClearFn"),n.after(this._selectableAfterDefRemoveFn,this,"_defRemoveFn"),this._selectableEvents=[this.after("multiSelectChange",this._afterMultiSelectChange)]},destructor:function(){(new e.EventHandle(this._selectableEvents)).detach(),this._selectableEvents=null,this._selectedMap=null},getSelectedNodes:function(){return e.Object.values(this._selectedMap)},selectNode:function(e,t){return this._selectedMap[e.id]||this._fireTreeEvent(r,{node:e,src:t&&t.src},{defaultFn:this._defSelectFn,silent:t&&t.silent}),this},unselect:function(e){for(var t in this._selectedMap)this._selectedMap.hasOwnProperty(t)&&this.unselectNode(this._selectedMap[t],e);return this},unselectNode:function(e,t){return(e.isSelected()||this._selectedMap[e.id])&&this._fireTreeEvent(i,{node:e,src:t&&t.src},{defaultFn:this._defUnselectFn,silent:t&&t.silent}),this},_selectableAfterDefAddFn:function(e){e.node.isSelected()&&this.selectNode(e.node)},_selectableAfterDefClearFn:function(){this._selectedMap={}},_selectableAfterDefRemoveFn:function(e){delete e.node.state.selected,delete this._selectedMap[e.node.id]},_afterMultiSelectChange:function(){this.unselect()},_defSelectFn:function(e){this.get("multiSelect")||this.unselect(),e.node.state.selected=!0,this._selectedMap[e.node.id]=e.node},_defUnselectFn:function(e){delete e.node.state.selected,delete this._selectedMap[e.node.id]}},s.ATTRS={multiSelect:{value:!1}},e.Tree.Selectable=s,o.prototype={isSelected:function(){return!!this.state.selected},select:function(e){return this.tree.selectNode(this,e),this},unselect:function(e){return this.tree.unselectNode(this,e),this}},e.Tree.Node.Selectable=o},"3.17.2",{requires:["tree"]});
diff --git a/js/yui3/tree-sortable/tree-sortable-min.js b/js/yui3/tree-sortable/tree-sortable-min.js
new file mode 100644
index 000000000..7a2207530
--- /dev/null
+++ b/js/yui3/tree-sortable/tree-sortable-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("tree-sortable",function(e,t){function r(){}function i(){}var n="sort";r.prototype={sortReverse:!1,initializer:function(t){this.nodeExtensions=this.nodeExtensions.concat(e.Tree.Node.Sortable),t&&(t.sortComparator&&(this.sortComparator=t.sortComparator),"sortReverse"in t&&(this.sortReverse=t.sortReverse))},sort:function(t){return this.sortNode(this.rootNode,e.merge(t,{deep:!0}))},sortComparator:function(e){return e.index()},sortNode:function(t,r){if(!t.children.length)return this;r||(r={});if(r.deep){r=e.merge(r,{deep:!1});var i=this;return this.traverseNode(t,function(e){i.sortNode(e,r)}),this}var s=this._getSortComparator(t,r),o;return"sortReverse"in r?o=t.sortReverse=r.sortReverse:"sortReverse"in t?o=t.sortReverse:o=this.sortReverse,t.children.sort(e.rbind(this._sort,this,s,o)),t._isIndexStale=!0,r.silent||this.fire(n,{node:t,reverse:!!o,src:r.src}),this},_compare:function(e,t){return e<t?-1:e>t?1:0},_compareReverse:function(e,t){return t<e?-1:t>e?1:0},_getDefaultNodeIndex:function(e,t){var n=e.children,i=this._getSortComparator(e),s=n.length,o=0,u="sortReverse"in e?e.sortReverse:this.sortReverse;if(!s)return s;if(i._unboundComparator===r.prototype.sortComparator)return u?0:s;var a=u?this._compareReverse:this._compare,f=i(t),l;while(o<s)l=o+s>>1,a(i(n[l]),f)<0?o=l+1:s=l;return o},_getSortComparator:function(e,t){var n,r,i;return t&&t.sortComparator?r=e.sortComparator=t.sortComparator:e.sortComparator?(r=e.sortComparator,i=e):(r=this.sortComparator,i=this),n=function(){return r.apply(i,arguments)},n._unboundComparator=r,n},_sort:function(e,t,n,r){return this[r?"_compareReverse":"_compare"](n(e),n(t))}},e.Tree.Sortable=r,i.prototype={sort:function(e){return this.tree.sortNode(this,e),this}},e.Tree.Node.Sortable=i},"3.17.2",{requires:["tree"]});
diff --git a/js/yui3/tree/tree-min.js b/js/yui3/tree/tree-min.js
new file mode 100644
index 000000000..41968ce9a
--- /dev/null
+++ b/js/yui3/tree/tree-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("tree",function(e,t){var n=e.Lang,r="add",i="clear",s="remove",o=e.Base.create("tree",e.Base,[],{nodeClass:e.Tree.Node,nodeExtensions:[],_isYUITree:!0,_rootNodeConfig:{canHaveChildren:!0},initializer:function(e){e||(e={}),e.nodeClass&&(this.nodeClass=e.nodeClass),e.nodeExtensions&&(this.nodeExtensions=this.nodeExtensions.concat(e.nodeExtensions)),this._published||(this._published={}),this._nodeMap={},this.onceAfter("initializedChange",function(){this._composeNodeClass(),this.clear(e.rootNode,{silent:!0}),e.nodes&&this.insertNode(this.rootNode,e.nodes,{silent:!0})})},destructor:function(){this.destroyNode(this.rootNode,{silent:!0}),this.children=null,this.rootNode=null,this._nodeClass=null,this._nodeMap=null,this._published=null},appendNode:function(t,n,r){return this.insertNode(t,n,e.merge(r,{index:t.children.length,src:"append"}))},clear:function(e,t){return this._fireTreeEvent(i,{rootNode:this.createNode(e||this._rootNodeConfig),src:t&&t.src},{defaultFn:this._defClearFn,silent:t&&t.silent})},createNode:function(t){t||(t={});if(t._isYUITreeNode)return t.state.destroyed?(e.error("Cannot insert a node that has already been destroyed.",null,"tree"),null):(this._adoptNode(t),t);if(t.children){var n=[];for(var r=0,i=t.children.length;r<i;r++)n.push(this.createNode(t.children[r]));t=e.merge(t,{children:n})}var s=new this._nodeClass(this,t);return this._nodeMap[s.id]=s},destroyNode:function(e,t){var n,r,i;t||(t={});for(r=0,i=e.children.length;r<i;r++)n=e.children[r],n.parent=null,this.destroyNode(n,t);return e.parent&&this.removeNode(e,t),e.children=[],e.data={},e.state={destroyed:!0},e.tree=null,e._indexMap={},delete this._nodeMap[e.id],this},emptyNode:function(e,t){var n=e.children,r=[];for(var i=n.length-1;i>-1;--i)r[i]=this.removeNode(n[i],t);return r},findNode:function(e,t,n,r){var i=null;return typeof t=="function"&&(r=n,n=t,t={}),this.traverseNode(e,t,function(e){if(n.call(r,e))return i=e,o.STOP_TRAVERSAL}),i},getNodeById:function(e){return this._nodeMap[e]},insertNode:function(e,t,i){i||(i={}),e||(e=this.rootNode);if("length"in t&&n.isArray(t)){var s="index"in i,o=[],u;for(var a=0,f=t.length;a<f;a++)u=this.insertNode(e,t[a],i),u&&(o.push(u),s&&(i.index+=1));return o}t=this.createNode(t);if(t){var l=i.index;typeof l=="undefined"&&(l=this._getDefaultNodeIndex(e,t,i)),this._fireTreeEvent(r,{index:l,node:t,parent:e,src:i.src||"insert"},{defaultFn:this._defAddFn,silent:i.silent})}return t},prependNode:function(t,n,r){return this.insertNode(t,n,e.merge(r,{index:0,src:"prepend"}))},removeNode:function(e,t){return t||(t={}),this._fireTreeEvent(s,{destroy:!!t.destroy,node:e,parent:e.parent,src:t.src||"remove"},{defaultFn:this._defRemoveFn,silent:t.silent}),e},size:function(){return this.rootNode.size()+1},toJSON:function(){return this.rootNode.toJSON()},traverseNode:function(t,n,r,i){if(t.state.destroyed){e.error("Cannot traverse a node that has been destroyed.",null,"tree");return}typeof n=="function"&&(i=r,r=n,n={}),n||(n={});var s=o.STOP_TRAVERSAL,u=typeof n.depth=="undefined";if(r.call(i,t)===s)return s;var a=t.children;if(u||n.depth>0){var f=u?n:{depth:n.depth-1};for(var l=0,c=a.length;l<c;l++)if(this.traverseNode(a[l],f,r,i)===s)return s}},_adoptNode:function(e,t){var n=e.tree,r;if(n===this)return;for(var i=0,s=e.children.length;i<s;i++)r=e.children[i],r.parent=null,this._adoptNode(r,{silent:!0}),r.parent=e;e.parent&&n.removeNode(e,t),delete n._nodeMap[e.id];if(!(e instanceof this._nodeClass)||n._nodeClass!==this._nodeClass)e=this.createNode(e.toJSON());e.tree=this,e._isIndexStale=!0,this._nodeMap[e.id]=e},_composeNodeClass:function(){var t=this.nodeClass,n=this.nodeExtensions,r;if(typeof t=="string"){t=e.Object.getValue(e,t.split("."));if(!t){e.error("Node class not found: "+t,null,"tree");return}this.nodeClass=t}if(!n.length){this._nodeClass=t;return}r=function(){var e=r._nodeExtensions;t.apply(this,arguments);for(var n=0,i=e.length;n<i;n++)e[n].apply(this,arguments)},e.extend(r,t);for(var i=0,s=n.length;i<s;i++)e.mix(r.prototype,n[i].prototype,!0);r._nodeExtensions=n,this._nodeClass=r},_fireTreeEvent:function(e,t,n){return n&&n.silent?n.defaultFn&&(t.silent=!0,n.defaultFn.call(this,t)):(n&&n.defaultFn&&!this._published[e]&&(this._published[e]=this.publish(e,{defaultFn:n.defaultFn})),this.fire(e,t)),this},_getDefaultNodeIndex:function(e){return e.children.length},_removeNodeFromParent:function(e){var t=e.parent,n;if(t){n=t.indexOf(e);if(n>-1){var r=t.children;n===r.length-1?r.pop():(r.splice(n,1),t._isIndexStale=!0),e.parent=null}}},_defAddFn:function(e){var t=e.index,n=e.node,r=e.parent,i;if(n.parent){if(n.parent===r){i=r.indexOf(n);if(i===t)return;i<t&&(t-=1)}this.removeNode(n,{silent:e.silent,src:"add"})}n.parent=r,r.children.splice(t,0,n),r.canHaveChildren=!0,r._isIndexStale=!0},_defClearFn:function(e){var t=e.rootNode;this.rootNode&&this.destroyNode(this.rootNode,{silent:!0}),this._nodeMap={},this._nodeMap[t.id]=t,this.rootNode=t,this.children=t.children},_defRemoveFn:function(e){var t=e.node;e.destroy?this.destroyNode(t,{silent:!0}):e.parent?this._removeNodeFromParent(t):this.rootNode===t&&(this.rootNode=this.createNode(this._rootNodeConfig),this.children=this.rootNode.children)}},{STOP_TRAVERSAL:{}});e.Tree=e.mix(o,e.Tree)},"3.17.2",{requires:["base-build","tree-node"]});
diff --git a/js/yui3/uploader-deprecated/assets/uploader.swf b/js/yui3/uploader-deprecated/assets/uploader.swf
new file mode 100644
index 000000000..95b853a7a
--- /dev/null
+++ b/js/yui3/uploader-deprecated/assets/uploader.swf
Binary files differ
diff --git a/js/yui3/uploader-flash/assets/uploader-flash-core.css b/js/yui3/uploader-flash/assets/uploader-flash-core.css
new file mode 100644
index 000000000..d2915569e
--- /dev/null
+++ b/js/yui3/uploader-flash/assets/uploader-flash-core.css
@@ -0,0 +1,11 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-uploader-selectfiles-button {
+ width: 100%;
+ height: 100%;
+} \ No newline at end of file
diff --git a/js/yui3/uploader-flash/uploader-flash-min.js b/js/yui3/uploader-flash/uploader-flash-min.js
new file mode 100644
index 000000000..59f860922
--- /dev/null
+++ b/js/yui3/uploader-flash/uploader-flash-min.js
@@ -0,0 +1,10 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("uploader-flash",function(e,t){function m(t,n,r){this._id=e.guid("yuiswf");var i=this._id,m=a.one(t),r=r||{},g=r.version||h,y=(g+"").split("."),b=s.isFlashVersionAtLeast(parseInt(y[0],10),parseInt(y[1],10),parseInt(y[2],10)),w=s.isFlashVersionAtLeast(8,0,0),E=w&&!b&&r.useExpressInstall,S=E?p:n,x="<object ",T,N,C="yId="+e.id+"&YUISwfId="+i+"&YUIBridgeCallback="+d+"&allowedDomain="+document.location.hostname;e.SWF._instances[i]=this;if(m&&(b||E)&&S){x+='id="'+i+'" ',u.ie?x+='classid="'+l+'" ':x+='type="'+c+'" data="'+f.html(S)+'" ',T="100%",N="100%",x+='width="'+T+'" height="'+N+'">',u.ie&&(x+='<param name="movie" value="'+f.html(S)+'"/>');for(var k in r.fixedAttributes)v.hasOwnProperty(k)&&(x+='<param name="'+f.html(k)+'" value="'+f.html(r.fixedAttributes[k])+'"/>');for(var L in r.flashVars){var A=r.flashVars[L];o.isString(A)&&(C+="&"+f.html(L)+"="+f.html(encodeURIComponent(A)))}C&&(x+='<param name="flashVars" value="'+C+'"/>'),x+="</object>",m.set("innerHTML",x),this._swf=a.one("#"+i)}else{var O={};O.type="wrongflashversion",this.publish("wrongflashversion",{fireOnce:!0}),this.fire("wrongflashversion",O)}}function y(){y.superclass.constructor.apply(this,arguments)}var n=e.Lang.sub,r=e.Uploader.Queue,i=e.Event,s=e.SWFDetect,o=e.Lang,u=e.UA,a=e.Node,f=e.Escape,l="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000",c="application/x-shockwave-flash",h="10.0.22",p="http://fpdownload.macromedia.com/pub/flashplayer/update/current/swf/autoUpdater.swf?"+Math.random(),d="SWF.eventHandler",v={align:"",allowFullScreen:"",allowNetworking:"",allowScriptAccess:"",base:"",bgcolor:"",loop:"",menu:"",name:"",play:"",quality:"",salign:"",scale:"",tabindex:"",wmode:""};m._instances=m._instances||{},m.eventHandler=function(e,t){m._instances[e]._eventHandler(t)},m.prototype={_eventHandler:function(e){e.type==="swfReady"?(this.publish("swfReady",{fireOnce:!0}),this.fire("swfReady",e)):e.type!=="log"&&this.fire(e.type,e)},callSWF:function(e,t){return t||(t=[]),this._swf._node[e]?this._swf._node[e].apply(this._swf._node,t):null},toString:function(){return"SWF "+this._id}},e.augment(m,e.EventTarget),e.SWF=m;var g=function(e){g.superclass.constructor.apply(this,arguments)};e.extend(g,e.Base,{initializer:function(t){this.get("id")||this._set("id",e.guid("file"))},_swfEventHandler:function(e){if(e.id===this.get("id"))switch(e.type){case"uploadstart":this.fire("uploadstart",{uploader:this.get("uploader")});break;case"uploadprogress":this.fire("uploadprogress",{originEvent:e,bytesLoaded:e.bytesLoaded,bytesTotal:e.bytesTotal,percentLoaded:Math.min(100,Math.round(1e4*e.bytesLoaded/e.bytesTotal)/100)}),this._set("bytesUploaded",e.bytesLoaded);break;case"uploadcomplete":this.fire("uploadfinished",{originEvent:e});break;case"uploadcompletedata":this.fire("uploadcomplete",{originEvent:e,data:e.data});break;case"uploadcancel":this.fire("uploadcancel",{originEvent:e});break;case"uploaderror":this.fire("uploaderror",{originEvent:e,status:e.status,statusText:e.message,source:e.source})}},startUpload:function(e,t,n){if(this.get("uploader")){var r=this.get("uploader"),i=n||"Filedata",s=this.get("id"),o=t||null;this._set("bytesUploaded",0),r.on("uploadstart",this._swfEventHandler,this),r.on("uploadprogress",this._swfEventHandler,this),r.on("uploadcomplete",this._swfEventHandler,this),r.on("uploadcompletedata",this._swfEventHandler,this),r.on("uploaderror",this._swfEventHandler,this),r.callSWF("upload",[s,e,o,i])}},cancelUpload:function(){this.get("uploader")&&(this.get("uploader").callSWF("cancel",[this.get("id")]),this.fire("uploadcancel"))}},{NAME:"file",TYPE:"flash",ATTRS:{id:{writeOnce:"initOnly",value:null},size:{writeOnce:"initOnly",value:0},name:{writeOnce:"initOnly",value:null},dateCreated:{writeOnce:"initOnly",value:null},dateModified:{writeOnce:"initOnly",value:null},bytesUploaded:{readOnly:!0,value:0},type:{writeOnce:"initOnly",value:null},uploader:{writeOnce:"initOnly",value:null}}}),e.FileFlash=g,e.UploaderFlash=e.extend(y,e.Widget,{_buttonState:"up",_buttonFocus:!1,_swfContainerId:null,_swfReference:null,queue:null,_tabElementBindings:null,initializer:function(){this._swfContainerId=e.guid("uploader"),this._swfReference=null,this.queue=null,this._buttonState="up",this._buttonFocus=null,this._tabElementBindings=null,this._fileList=[],this.publish("fileselect"),this.publish("uploadstart"),this.publish("fileuploadstart"),this.publish("uploadprogress"),this.publish("totaluploadprogress"),this.publish("uploadcomplete"),this.publish("alluploadscomplete"),this.publish("uploaderror"),this.publish("mouseenter"),this.publish("mouseleave"),this.publish("mousedown"),this.publish("mouseup"),this.publish("click")},renderUI:function(){var t=this.get("boundingBox"),r=this.get("contentBox"),i=this.get("selectFilesButton"),s=e.Node.create(n(y.FLASH_CONTAINER,{swfContainerId:this._swfContainerId})),o={version:"10.0.45",fixedAttributes:{wmode:"transparent",allowScriptAccess:"always",allowNetworking:"all",scale:"noscale"}};t.setStyle("position","relative"),i.setStyles({width:"100%",height:"100%"}),r.append(i),r.append(s),this._swfReference=new e.SWF(s,this.get("swfURL"),o)},bindUI:function(){this._swfReference.on("swfReady",function(){this._setMultipleFiles(),this._setFileFilters(),this._triggerEnabled(),this._attachTabElements(),this.after("multipleFilesChange",this._setMultipleFiles,this),this.after("fileFiltersChange",this._setFileFilters,this),this.after("enabledChange",this._triggerEnabled,this),this.after("tabElementsChange",this._attachTabElements)},this),this._swfReference.on("fileselect",this._updateFileList,this),this._swfReference.on("mouseenter",function(){this.fire("mouseenter"),this._setButtonClass("hover",!0),this._buttonState==="down"&&this._setButtonClass("active",!0)},this),this._swfReference.on("mouseleave",function(){this.fire("mouseleave"),this._setButtonClass("hover",!1),this._setButtonClass("active",!1)},this),this._swfReference.on("mousedown",function(){this.fire("mousedown"),this._buttonState="down",this._setButtonClass
+("active",!0)},this),this._swfReference.on("mouseup",function(){this.fire("mouseup"),this._buttonState="up",this._setButtonClass("active",!1)},this),this._swfReference.on("click",function(){this.fire("click"),this._buttonFocus=!0,this._setButtonClass("focus",!0),e.one("body").focus(),this._swfReference._swf.focus()},this)},_attachTabElements:function(){if(this.get("tabElements")!==null&&this.get("tabElements").from!==null&&this.get("tabElements").to!==null){this._tabElementBindings!==null?(this._tabElementBindings.from.detach(),this._tabElementBindings.to.detach(),this._tabElementBindings.tabback.detach(),this._tabElementBindings.tabforward.detach(),this._tabElementBindings.focus.detach(),this._tabElementBindings.blur.detach()):this._tabElementBindings={};var t=e.one(this.get("tabElements").from),n=e.one(this.get("tabElements").to);this._tabElementBindings.from=t.on("keydown",function(e){e.keyCode===9&&!e.shiftKey&&(e.preventDefault(),this._swfReference._swf.setAttribute("tabindex",0),this._swfReference._swf.setAttribute("role","button"),this._swfReference._swf.setAttribute("aria-label",this.get("selectButtonLabel")),this._swfReference._swf.focus())},this),this._tabElementBindings.to=n.on("keydown",function(e){e.keyCode===9&&e.shiftKey&&(e.preventDefault(),this._swfReference._swf.setAttribute("tabindex",0),this._swfReference._swf.setAttribute("role","button"),this._swfReference._swf.setAttribute("aria-label",this.get("selectButtonLabel")),this._swfReference._swf.focus())},this),this._tabElementBindings.tabback=this._swfReference.on("tabback",function(){this._swfReference._swf.blur(),setTimeout(function(){t.focus()},30)},this),this._tabElementBindings.tabforward=this._swfReference.on("tabforward",function(){this._swfReference._swf.blur(),setTimeout(function(){n.focus()},30)},this),this._tabElementBindings.focus=this._swfReference._swf.on("focus",function(){this._buttonFocus=!0,this._setButtonClass("focus",!0)},this),this._tabElementBindings.blur=this._swfReference._swf.on("blur",function(){this._buttonFocus=!1,this._setButtonClass("focus",!1)},this)}else this._tabElementBindings!==null&&(this._tabElementBindings.from.detach(),this._tabElementBindings.to.detach(),this._tabElementBindings.tabback.detach(),this._tabElementBindings.tabforward.detach(),this._tabElementBindings.focus.detach(),this._tabElementBindings.blur.detach())},_setButtonClass:function(e,t){t?this.get("selectFilesButton").addClass(this.get("buttonClassNames")[e]):this.get("selectFilesButton").removeClass(this.get("buttonClassNames")[e])},_setFileFilters:function(){this._swfReference&&this.get("fileFilters").length>0&&this._swfReference.callSWF("setFileFilters",[this.get("fileFilters")])},_setMultipleFiles:function(){this._swfReference&&this._swfReference.callSWF("setAllowMultipleFiles",[this.get("multipleFiles")])},_triggerEnabled:function(){this.get("enabled")?(this._swfReference.callSWF("enable"),this._swfReference._swf.setAttribute("aria-disabled","false"),this._setButtonClass("disabled",!1)):(this._swfReference.callSWF("disable"),this._swfReference._swf.setAttribute("aria-disabled","true"),this._setButtonClass("disabled",!0))},_getFileList:function(){return this._fileList.concat()},_setFileList:function(e){return this._fileList=e.concat(),this._fileList.concat()},_updateFileList:function(t){e.one("body").focus(),this._swfReference._swf.focus();var n=t.fileList,r=[],i=[],s=this._swfReference,o=this.get("fileFilterFunction"),u;e.each(n,function(e){var t={};t.id=e.fileId,t.name=e.fileReference.name,t.size=e.fileReference.size,t.type=e.fileReference.type,t.dateCreated=e.fileReference.creationDate,t.dateModified=e.fileReference.modificationDate,t.uploader=s,r.push(t)}),o?e.each(r,function(t){var n=new e.FileFlash(t);o(n)&&i.push(n)}):e.each(r,function(t){i.push(new e.FileFlash(t))}),i.length>0&&(u=this.get("fileList"),this.set("fileList",this.get("appendNewFiles")?u.concat(i):i),this.fire("fileselect",{fileList:i}))},_uploadEventHandler:function(e){switch(e.type){case"file:uploadstart":this.fire("fileuploadstart",e);break;case"file:uploadprogress":this.fire("uploadprogress",e);break;case"uploaderqueue:totaluploadprogress":this.fire("totaluploadprogress",e);break;case"file:uploadcomplete":this.fire("uploadcomplete",e);break;case"uploaderqueue:alluploadscomplete":this.queue=null,this.fire("alluploadscomplete",e);break;case"file:uploaderror":case"uploaderqueue:uploaderror":this.fire("uploaderror",e);break;case"file:uploadcancel":case"uploaderqueue:uploadcancel":this.fire("uploadcancel",e)}},upload:function(t,n,r){var i=n||this.get("uploadURL"),s=r||this.get("postVarsPerFile"),o=t.get("id");s=s.hasOwnProperty(o)?s[o]:s,t instanceof e.FileFlash&&(t.on("uploadstart",this._uploadEventHandler,this),t.on("uploadprogress",this._uploadEventHandler,this),t.on("uploadcomplete",this._uploadEventHandler,this),t.on("uploaderror",this._uploadEventHandler,this),t.on("uploadcancel",this._uploadEventHandler,this),t.startUpload(i,s,this.get("fileFieldName")))},uploadAll:function(e,t){this.uploadThese(this.get("fileList"),e,t)},uploadThese:function(e,t,n){if(!this.queue){var i=t||this.get("uploadURL"),s=n||this.get("postVarsPerFile");this.queue=new r({simUploads:this.get("simLimit"),errorAction:this.get("errorAction"),fileFieldName:this.get("fileFieldName"),fileList:e,uploadURL:i,perFileParameters:s,retryCount:this.get("retryCount")}),this.queue.on("uploadstart",this._uploadEventHandler,this),this.queue.on("uploadprogress",this._uploadEventHandler,this),this.queue.on("totaluploadprogress",this._uploadEventHandler,this),this.queue.on("uploadcomplete",this._uploadEventHandler,this),this.queue.on("alluploadscomplete",this._uploadEventHandler,this),this.queue.on("alluploadscancelled",function(){this.queue=null},this),this.queue.on("uploaderror",this._uploadEventHandler,this),this.queue.startUpload(),this.fire("uploadstart")}}},{FLASH_CONTAINER:'<div id="{swfContainerId}" style="position:absolute; top:0px; left: 0px; margin: 0; padding: 0; border: 0; width:100%; height:100%"></div>'
+,SELECT_FILES_BUTTON:"<button type='button' class='yui3-button' tabindex='-1'>{selectButtonLabel}</button>",TYPE:"flash",NAME:"uploader",ATTRS:{appendNewFiles:{value:!0},buttonClassNames:{value:{hover:"yui3-button-hover",active:"yui3-button-active",disabled:"yui3-button-disabled",focus:"yui3-button-selected"}},enabled:{value:!0},errorAction:{value:"continue",validator:function(e){return e===r.CONTINUE||e===r.STOP||e===r.RESTART_ASAP||e===r.RESTART_AFTER}},fileFilters:{value:[]},fileFilterFunction:{value:null},fileFieldName:{value:"Filedata"},fileList:{value:[],getter:"_getFileList",setter:"_setFileList"},multipleFiles:{value:!1},postVarsPerFile:{value:{}},selectButtonLabel:{value:"Select Files"},selectFilesButton:{valueFn:function(){return e.Node.create(n(e.UploaderFlash.SELECT_FILES_BUTTON,{selectButtonLabel:this.get("selectButtonLabel")}))}},simLimit:{value:2,validator:function(e){return e>=2&&e<=5}},swfURL:{valueFn:function(){var t="flashuploader.swf";return e.UA.ie>0?t+"?t="+e.guid("uploader"):t}},tabElements:{value:null},uploadURL:{value:""},retryCount:{value:3}}}),e.UploaderFlash.Queue=r},"3.17.2",{requires:["swfdetect","escape","widget","base","cssbutton","node","event-custom","uploader-queue"]});
diff --git a/js/yui3/uploader-html5/assets/uploader-flash-core.css b/js/yui3/uploader-html5/assets/uploader-flash-core.css
new file mode 100644
index 000000000..d2915569e
--- /dev/null
+++ b/js/yui3/uploader-html5/assets/uploader-flash-core.css
@@ -0,0 +1,11 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-uploader-selectfiles-button {
+ width: 100%;
+ height: 100%;
+} \ No newline at end of file
diff --git a/js/yui3/uploader-html5/uploader-html5-min.js b/js/yui3/uploader-html5/uploader-html5-min.js
new file mode 100644
index 000000000..ec480e4e3
--- /dev/null
+++ b/js/yui3/uploader-html5/uploader-html5-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("uploader-html5",function(e,t){function i(){i.superclass.constructor.apply(this,arguments)}var n=e.Lang.sub,r=e.Uploader.Queue;e.UploaderHTML5=e.extend(i,e.Widget,{_fileInputField:null,_buttonBinding:null,queue:null,initializer:function(){this._fileInputField=null,this.queue=null,this._buttonBinding=null,this._fileList=[],this.publish("fileselect"),this.publish("uploadstart"),this.publish("fileuploadstart"),this.publish("uploadprogress"),this.publish("totaluploadprogress"),this.publish("uploadcomplete"),this.publish("alluploadscomplete"),this.publish("uploaderror"),this.publish("dragenter"),this.publish("dragover"),this.publish("dragleave"),this.publish("drop")},renderUI:function(){var t=this.get("contentBox"),n=this.get("selectFilesButton");n.setStyles({width:"100%",height:"100%"}),t.append(n),this._fileInputField=e.Node.create(i.HTML5FILEFIELD_TEMPLATE),t.append(this._fileInputField)},bindUI:function(){this._bindSelectButton(),this._setMultipleFiles(),this._setFileFilters(),this._bindDropArea(),this._triggerEnabled(),this.after("multipleFilesChange",this._setMultipleFiles,this),this.after("fileFiltersChange",this._setFileFilters,this),this.after("enabledChange",this._triggerEnabled,this),this.after("selectFilesButtonChange",this._bindSelectButton,this),this.after("dragAndDropAreaChange",this._bindDropArea,this),this.after("tabIndexChange",function(){this.get("selectFilesButton").set("tabIndex",this.get("tabIndex"))},this),this._fileInputField.on("change",this._updateFileList,this),this._fileInputField.on("click",function(e){e.stopPropagation()},this),this.get("selectFilesButton").set("tabIndex",this.get("tabIndex"))},_rebindFileField:function(){this._fileInputField.remove(!0),this._fileInputField=e.Node.create(i.HTML5FILEFIELD_TEMPLATE),this.get("contentBox").append(this._fileInputField),this._fileInputField.on("change",this._updateFileList,this),this._setMultipleFiles(),this._setFileFilters()},_bindDropArea:function(e){var t=e||{prevVal:null},n=this.get("dragAndDropArea");t.prevVal!==null&&(t.prevVal.detach("drop",this._ddEventHandler),t.prevVal.detach("dragenter",this._ddEventHandler),t.prevVal.detach("dragover",this._ddEventHandler),t.prevVal.detach("dragleave",this._ddEventHandler)),n!==null&&(n.on("drop",this._ddEventHandler,this),n.on("dragenter",this._ddEventHandler,this),n.on("dragover",this._ddEventHandler,this),n.on("dragleave",this._ddEventHandler,this))},_bindSelectButton:function(){this._buttonBinding=this.get("selectFilesButton").on("click",this.openFileSelectDialog,this)},_ddEventHandler:function(t){t.stopPropagation(),t.preventDefault();if(e.Array.indexOf(t._event.dataTransfer.types,"Files")>-1)switch(t.type){case"dragenter":this.fire("dragenter");break;case"dragover":this.fire("dragover");break;case"dragleave":this.fire("dragleave");break;case"drop":var n=t._event.dataTransfer.files,r=[],i=this.get("fileFilterFunction"),s;i?e.each(n,function(t){var n=new e.FileHTML5(t);i(n)&&r.push(n)}):e.each(n,function(t){r.push(new e.FileHTML5(t))}),r.length>0&&(s=this.get("fileList"),this.set("fileList",this.get("appendNewFiles")?s.concat(r):r),this.fire("fileselect",{fileList:r})),this.fire("drop",{fileList:r})}},_setButtonClass:function(e,t){t?this.get("selectFilesButton").addClass(this.get("buttonClassNames")[e]):this.get("selectFilesButton").removeClass(this.get("buttonClassNames")[e])},_setMultipleFiles:function(){this.get("multipleFiles")===!0?this._fileInputField.set("multiple","multiple"):this._fileInputField.set("multiple","")},_setFileFilters:function(){this.get("fileFilters").length>0?this._fileInputField.set("accept",this.get("fileFilters").join(",")):this._fileInputField.set("accept","")},_triggerEnabled:function(){this.get("enabled")&&this._buttonBinding===null?(this._bindSelectButton(),this._setButtonClass("disabled",!1),this.get("selectFilesButton").setAttribute("aria-disabled","false")):!this.get("enabled")&&this._buttonBinding&&(this._buttonBinding.detach(),this._buttonBinding=null,this._setButtonClass("disabled",!0),this.get("selectFilesButton").setAttribute("aria-disabled","true"))},_getFileList:function(){return this._fileList.concat()},_setFileList:function(e){return this._fileList=e.concat(),this._fileList.concat()},_updateFileList:function(t){var n=t.target.getDOMNode().files,r=[],i=this.get("fileFilterFunction"),s;i?e.each(n,function(t){var n=new e.FileHTML5(t);i(n)&&r.push(n)}):e.each(n,function(t){r.push(new e.FileHTML5(t))}),r.length>0&&(s=this.get("fileList"),this.set("fileList",this.get("appendNewFiles")?s.concat(r):r),this.fire("fileselect",{fileList:r})),this._rebindFileField()},_uploadEventHandler:function(e){switch(e.type){case"file:uploadstart":this.fire("fileuploadstart",e);break;case"file:uploadprogress":this.fire("uploadprogress",e);break;case"uploaderqueue:totaluploadprogress":this.fire("totaluploadprogress",e);break;case"file:uploadcomplete":this.fire("uploadcomplete",e);break;case"uploaderqueue:alluploadscomplete":this.queue=null,this.fire("alluploadscomplete",e);break;case"file:uploaderror":case"uploaderqueue:uploaderror":this.fire("uploaderror",e);break;case"file:uploadcancel":case"uploaderqueue:uploadcancel":this.fire("uploadcancel",e)}},openFileSelectDialog:function(){var e=this._fileInputField.getDOMNode();e.click&&e.click()},upload:function(t,n,r){var i=n||this.get("uploadURL"),s=r||this.get("postVarsPerFile"),o=t.get("id");s=s.hasOwnProperty(o)?s[o]:s,t instanceof e.FileHTML5&&(t.on("uploadstart",this._uploadEventHandler,this),t.on("uploadprogress",this._uploadEventHandler,this),t.on("uploadcomplete",this._uploadEventHandler,this),t.on("uploaderror",this._uploadEventHandler,this),t.on("uploadcancel",this._uploadEventHandler,this),t.startUpload(i,s,this.get("fileFieldName")))},uploadAll:function(e,t){this.uploadThese(this.get("fileList"),e,t)},uploadThese:function(t,n,i){if(!this.queue){var s=n||this.get("uploadURL"),o=i||this.get("postVarsPerFile");this.queue=new r({simUploads:this.get("simLimit"),errorAction:this.get("errorAction"
+),fileFieldName:this.get("fileFieldName"),fileList:t,uploadURL:s,perFileParameters:o,retryCount:this.get("retryCount"),uploadHeaders:this.get("uploadHeaders"),withCredentials:this.get("withCredentials")}),this.queue.on("uploadstart",this._uploadEventHandler,this),this.queue.on("uploadprogress",this._uploadEventHandler,this),this.queue.on("totaluploadprogress",this._uploadEventHandler,this),this.queue.on("uploadcomplete",this._uploadEventHandler,this),this.queue.on("alluploadscomplete",this._uploadEventHandler,this),this.queue.on("uploadcancel",this._uploadEventHandler,this),this.queue.on("uploaderror",this._uploadEventHandler,this),this.queue.startUpload(),this.fire("uploadstart")}else this.queue._currentState===r.UPLOADING&&(this.queue.set("perFileParameters",this.get("postVarsPerFile")),e.each(t,function(e){this.queue.addToQueueBottom(e)},this))}},{HTML5FILEFIELD_TEMPLATE:"<input type='file' style='visibility:hidden; width:0px; height: 0px;'>",SELECT_FILES_BUTTON:'<button type="button" class="yui3-button" role="button" aria-label="{selectButtonLabel}" tabindex="{tabIndex}">{selectButtonLabel}</button>',TYPE:"html5",NAME:"uploader",ATTRS:{appendNewFiles:{value:!0},buttonClassNames:{value:{hover:"yui3-button-hover",active:"yui3-button-active",disabled:"yui3-button-disabled",focus:"yui3-button-selected"}},dragAndDropArea:{value:null,setter:function(t){return e.one(t)}},enabled:{value:!0},errorAction:{value:"continue",validator:function(e){return e===r.CONTINUE||e===r.STOP||e===r.RESTART_ASAP||e===r.RESTART_AFTER}},fileFilters:{value:[]},fileFilterFunction:{value:null},fileFieldName:{value:"Filedata"},fileList:{value:[],getter:"_getFileList",setter:"_setFileList"},multipleFiles:{value:!1},postVarsPerFile:{value:{}},selectButtonLabel:{value:"Select Files"},selectFilesButton:{valueFn:function(){return e.Node.create(n(e.UploaderHTML5.SELECT_FILES_BUTTON,{selectButtonLabel:this.get("selectButtonLabel"),tabIndex:this.get("tabIndex")}))}},simLimit:{value:2,validator:function(e){return e>=1&&e<=5}},uploadURL:{value:""},uploadHeaders:{value:{}},withCredentials:{value:!0},retryCount:{value:3}}}),e.UploaderHTML5.Queue=r},"3.17.2",{requires:["widget","node-event-simulate","file-html5","uploader-queue"]});
diff --git a/js/yui3/uploader-queue/assets/uploader-flash-core.css b/js/yui3/uploader-queue/assets/uploader-flash-core.css
new file mode 100644
index 000000000..d2915569e
--- /dev/null
+++ b/js/yui3/uploader-queue/assets/uploader-flash-core.css
@@ -0,0 +1,11 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-uploader-selectfiles-button {
+ width: 100%;
+ height: 100%;
+} \ No newline at end of file
diff --git a/js/yui3/uploader-queue/uploader-queue-min.js b/js/yui3/uploader-queue/uploader-queue-min.js
new file mode 100644
index 000000000..2e6667052
--- /dev/null
+++ b/js/yui3/uploader-queue/uploader-queue-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("uploader-queue",function(e,t){var n=function(){this.queuedFiles=[],this.uploadRetries={},this.numberOfUploads=0,this.currentUploadedByteValues={},this.currentFiles={},this.totalBytesUploaded=0,this.totalBytes=0,n.superclass.constructor.apply(this,arguments)};e.extend(n,e.Base,{_currentState:n.STOPPED,initializer:function(){},_uploadStartHandler:function(e){var t=e;t.file=e.target,t.originEvent=e,this.fire("uploadstart",t)},_uploadErrorHandler:function(e){var t=this.get("errorAction"),r=e,i,s;r.file=e.target,r.originEvent=e,this.numberOfUploads-=1,delete this.currentFiles[e.target.get("id")],this._detachFileEvents(e.target),e.target.cancelUpload(),t===n.STOP?this.pauseUpload():t===n.RESTART_ASAP?(i=e.target.get("id"),s=this.uploadRetries[i]||0,s<this.get("retryCount")&&(this.uploadRetries[i]=s+1,this.addToQueueTop(e.target)),this._startNextFile()):t===n.RESTART_AFTER&&(i=e.target.get("id"),s=this.uploadRetries[i]||0,s<this.get("retryCount")&&(this.uploadRetries[i]=s+1,this.addToQueueBottom(e.target)),this._startNextFile()),this.fire("uploaderror",r)},_startNextFile:function(){if(this.queuedFiles.length>0){var e=this.queuedFiles.shift(),t=e.get("id"),n=this.get("perFileParameters"),r=n.hasOwnProperty(t)?n[t]:n;this.currentUploadedByteValues[t]=0,e.on("uploadstart",this._uploadStartHandler,this),e.on("uploadprogress",this._uploadProgressHandler,this),e.on("uploadcomplete",this._uploadCompleteHandler,this),e.on("uploaderror",this._uploadErrorHandler,this),e.on("uploadcancel",this._uploadCancelHandler,this),e.set("xhrHeaders",this.get("uploadHeaders")),e.set("xhrWithCredentials",this.get("withCredentials")),e.startUpload(this.get("uploadURL"),r,this.get("fileFieldName")),this._registerUpload(e)}},_registerUpload:function(e){this.numberOfUploads+=1,this.currentFiles[e.get("id")]=e},_unregisterUpload:function(e){this.numberOfUploads>0&&(this.numberOfUploads-=1),delete this.currentFiles[e.get("id")],delete this.uploadRetries[e.get("id")],this._detachFileEvents(e)},_detachFileEvents:function(e){e.detach("uploadstart",this._uploadStartHandler),e.detach("uploadprogress",this._uploadProgressHandler),e.detach("uploadcomplete",this._uploadCompleteHandler),e.detach("uploaderror",this._uploadErrorHandler),e.detach("uploadcancel",this._uploadCancelHandler)},_uploadCompleteHandler:function(t){this._unregisterUpload(t.target),this.totalBytesUploaded+=t.target.get("size"),delete this.currentUploadedByteValues[t.target.get("id")],this.queuedFiles.length>0&&this._currentState===n.UPLOADING&&this._startNextFile();var r=t,i=this.totalBytesUploaded,s=Math.min(100,Math.round(1e4*i/this.totalBytes)/100);r.file=t.target,r.originEvent=t,e.each(this.currentUploadedByteValues,function(e){i+=e}),this.fire("totaluploadprogress",{bytesLoaded:i,bytesTotal:this.totalBytes,percentLoaded:s}),this.fire("uploadcomplete",r),this.queuedFiles.length===0&&this.numberOfUploads<=0&&(this.fire("alluploadscomplete"),this._currentState=n.STOPPED)},_uploadCancelHandler:function(e){var t=e;t.originEvent=e,t.file=e.target,this.fire("uploadcancel",t)},_uploadProgressHandler:function(t){this.currentUploadedByteValues[t.target.get("id")]=t.bytesLoaded;var n=t,r=this.totalBytesUploaded,i=Math.min(100,Math.round(1e4*r/this.totalBytes)/100);n.originEvent=t,n.file=t.target,this.fire("uploadprogress",n),e.each(this.currentUploadedByteValues,function(e){r+=e}),this.fire("totaluploadprogress",{bytesLoaded:r,bytesTotal:this.totalBytes,percentLoaded:i})},startUpload:function(){this.queuedFiles=this.get("fileList").slice(0),this.numberOfUploads=0,this.currentUploadedByteValues={},this.currentFiles={},this.totalBytesUploaded=0,this._currentState=n.UPLOADING;while(this.numberOfUploads<this.get("simUploads")&&this.queuedFiles.length>0)this._startNextFile()},pauseUpload:function(){this._currentState=n.STOPPED},restartUpload:function(){this._currentState=n.UPLOADING;while(this.numberOfUploads<this.get("simUploads"))this._startNextFile()},forceReupload:function(e){var t=e.get("id");this.currentFiles.hasOwnProperty(t)&&(e.cancelUpload(),this._unregisterUpload(e),this.addToQueueTop(e),this._startNextFile())},addToQueueTop:function(e){this.queuedFiles.unshift(e)},addToQueueBottom:function(e){this.queuedFiles.push(e)},cancelUpload:function(e){var t,r,i;if(e){t=e.get("id");if(this.currentFiles[t])this.currentFiles[t].cancelUpload(),this._unregisterUpload(this.currentFiles[t]),this._currentState===n.UPLOADING&&this._startNextFile();else for(r=0,len=this.queuedFiles.length;r<len;r++)if(this.queuedFiles[r].get("id")===t){this.queuedFiles.splice(r,1);break}}else{for(i in this.currentFiles)this.currentFiles[i].cancelUpload(),this._unregisterUpload(this.currentFiles[i]);this.currentUploadedByteValues={},this.currentFiles={},this.totalBytesUploaded=0,this.fire("alluploadscancelled"),this._currentState=n.STOPPED}}},{CONTINUE:"continue",STOP:"stop",RESTART_ASAP:"restartasap",RESTART_AFTER:"restartafter",STOPPED:"stopped",UPLOADING:"uploading",NAME:"uploaderqueue",ATTRS:{simUploads:{value:2,validator:function(e){return e>=1&&e<=5}},errorAction:{value:"continue",validator:function(e){return e===n.CONTINUE||e===n.STOP||e===n.RESTART_ASAP||e===n.RESTART_AFTER}},bytesUploaded:{readOnly:!0,value:0},bytesTotal:{readOnly:!0,value:0},fileList:{value:[],lazyAdd:!1,setter:function(t){var n=t;return e.Array.each(n,function(e){this.totalBytes+=e.get("size")},this),t}},fileFieldName:{value:"Filedata"},uploadURL:{value:""},uploadHeaders:{value:{}},withCredentials:{value:!0},perFileParameters:{value:{}},retryCount:{value:3}}}),e.namespace("Uploader"),e.Uploader.Queue=n},"3.17.2",{requires:["base"]});
diff --git a/js/yui3/uploader/assets/flashuploader.swf b/js/yui3/uploader/assets/flashuploader.swf
new file mode 100644
index 000000000..73026acb5
--- /dev/null
+++ b/js/yui3/uploader/assets/flashuploader.swf
Binary files differ
diff --git a/js/yui3/uploader/assets/uploader-flash-core.css b/js/yui3/uploader/assets/uploader-flash-core.css
new file mode 100644
index 000000000..d2915569e
--- /dev/null
+++ b/js/yui3/uploader/assets/uploader-flash-core.css
@@ -0,0 +1,11 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-uploader-selectfiles-button {
+ width: 100%;
+ height: 100%;
+} \ No newline at end of file
diff --git a/js/yui3/uploader/uploader-min.js b/js/yui3/uploader/uploader-min.js
new file mode 100644
index 000000000..ad8316368
--- /dev/null
+++ b/js/yui3/uploader/uploader-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("uploader",function(e,t){var n=e.config.win;n&&n.File&&n.FormData&&n.XMLHttpRequest?e.Uploader=e.UploaderHTML5:e.SWFDetect.isFlashVersionAtLeast(10,0,45)?e.Uploader=e.UploaderFlash:(e.namespace("Uploader"),e.Uploader.TYPE="none")},"3.17.2",{requires:["uploader-html5","uploader-flash"]});
diff --git a/js/yui3/view-node-map/view-node-map-min.js b/js/yui3/view-node-map/view-node-map-min.js
new file mode 100644
index 000000000..9d0f10b05
--- /dev/null
+++ b/js/yui3/view-node-map/view-node-map-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("view-node-map",function(e,t){function i(){}var n=e.namespace("View._buildCfg"),r={};n.aggregates||(n.aggregates=[]),n.aggregates.push("getByNode"),i.getByNode=function(t){var n;return e.one(t).ancestor(function(t){return(n=r[e.stamp(t,!0)])||!1},!0),n||null},i._instances=r,i.prototype={initializer:function(){r[e.stamp(this.get("container"))]=this},destructor:function(){var t=e.stamp(this.get("container"),!0);t in r&&delete r[t]}},e.View.NodeMap=i},"3.17.2",{requires:["view"]});
diff --git a/js/yui3/view/view-min.js b/js/yui3/view/view-min.js
new file mode 100644
index 000000000..ef4f25d68
--- /dev/null
+++ b/js/yui3/view/view-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("view",function(e,t){function n(){n.superclass.constructor.apply(this,arguments)}e.View=e.extend(n,e.Base,{containerTemplate:"<div/>",events:{},template:"",_allowAdHocAttrs:!0,initializer:function(t){t||(t={}),t.containerTemplate&&(this.containerTemplate=t.containerTemplate),t.template&&(this.template=t.template),this.events=t.events?e.merge(this.events,t.events):this.events,this.after("containerChange",this._afterContainerChange)},destroy:function(e){return e&&(e.remove||e["delete"])&&this.onceAfter("destroy",function(){this._destroyContainer()}),n.superclass.destroy.call(this)},destructor:function(){this.detachEvents(),delete this._container},attachEvents:function(t){var n=this.get("container"),r=e.Object.owns,i,s,o,u;this.detachEvents(),t||(t=this.events);for(u in t){if(!r(t,u))continue;s=t[u];for(o in s){if(!r(s,o))continue;i=s[o],typeof i=="string"&&(i=this[i]);if(!i)continue;this._attachedViewEvents.push(n.delegate(o,i,u,this))}}return this},create:function(t){return t?e.one(t):e.Node.create(this.containerTemplate)},detachEvents:function(){return e.Array.each(this._attachedViewEvents,function(e){e&&e.detach()}),this._attachedViewEvents=[],this},remove:function(){var e=this.get("container");return e&&e.remove(),this},render:function(){return this},_destroyContainer:function(){var e=this.get("container");e&&e.remove(!0)},_getContainer:function(e){return this._container||(e?(this._container=e,this.attachEvents()):(e=this._container=this.create(),this._set("container",e))),e},_afterContainerChange:function(){this.attachEvents(this.events)}},{NAME:"view",ATTRS:{container:{getter:"_getContainer",setter:e.one,writeOnce:!0}},_NON_ATTRS_CFG:["containerTemplate","events","template"]})},"3.17.2",{requires:["base-build","node-event-delegate"]});
diff --git a/js/yui3/widget-anim/widget-anim-min.js b/js/yui3/widget-anim/widget-anim-min.js
new file mode 100644
index 000000000..16daed85c
--- /dev/null
+++ b/js/yui3/widget-anim/widget-anim-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("widget-anim",function(e,t){function b(e){b.superclass.constructor.apply(this,arguments)}var n="boundingBox",r="host",i="node",s="opacity",o="",u="visible",a="destroy",f="hidden",l="rendered",c="start",h="end",p="duration",d="animShow",v="animHide",m="_uiSetVisible",g="animShowChange",y="animHideChange";b.NS="anim",b.NAME="pluginWidgetAnim",b.ANIMATIONS={fadeIn:function(){var t=this.get(r),f=t.get(n),l=new e.Anim({node:f,to:{opacity:1},duration:this.get(p)});return t.get(u)||f.setStyle(s,0),l.on(a,function(){this.get(i).setStyle(s,e.UA.ie?1:o)}),l},fadeOut:function(){return new e.Anim({node:this.get(r).get(n),to:{opacity:0},duration:this.get(p)})}},b.ATTRS={duration:{value:.2},animShow:{valueFn:b.ANIMATIONS.fadeIn},animHide:{valueFn:b.ANIMATIONS.fadeOut}},e.extend(b,e.Plugin.Base,{initializer:function(e){this._bindAnimShow(),this._bindAnimHide(),this.after(g,this._bindAnimShow),this.after(y,this._bindAnimHide),this.beforeHostMethod(m,this._uiAnimSetVisible)},destructor:function(){this.get(d).destroy(),this.get(v).destroy()},_uiAnimSetVisible:function(t){if(this.get(r).get(l))return t?(this.get(v).stop(),this.get(d).run()):(this.get(d).stop(),this.get(v).run()),new e.Do.Prevent},_uiSetVisible:function(e){var t=this.get(r),i=t.getClassName(f);t.get(n).toggleClass(i,!e)},_bindAnimShow:function(){this.get(d).on(c,e.bind(function(){this._uiSetVisible(!0)},this))},_bindAnimHide:function(){this.get(v).after(h,e.bind(function(){this._uiSetVisible(!1)},this))}}),e.namespace("Plugin").WidgetAnim=b},"3.17.2",{requires:["anim-base","plugin","widget"]});
diff --git a/js/yui3/widget-autohide/widget-autohide-min.js b/js/yui3/widget-autohide/widget-autohide-min.js
new file mode 100644
index 000000000..a840fb35e
--- /dev/null
+++ b/js/yui3/widget-autohide/widget-autohide-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("widget-autohide",function(e,t){function m(t){e.after(this._bindUIAutohide,this,f),e.after(this._syncUIAutohide,this,l),this.get(c)&&(this._bindUIAutohide(),this._syncUIAutohide())}var n="widgetAutohide",r="autohide",i="clickoutside",s="focusoutside",o="document",u="key",a="esc",f="bindUI",l="syncUI",c="rendered",h="boundingBox",p="visible",d="Change",v=e.ClassNameManager.getClassName;m.ATTRS={hideOn:{validator:e.Lang.isArray,valueFn:function(){return[{node:e.one(o),eventName:u,keyCode:a}]}}},m.prototype={_uiHandlesAutohide:null,destructor:function(){this._detachUIHandlesAutohide()},_bindUIAutohide:function(){this.after(p+d,this._afterHostVisibleChangeAutohide),this.after("hideOnChange",this._afterHideOnChange)},_syncUIAutohide:function(){this._uiSetHostVisibleAutohide(this.get(p))},_uiSetHostVisibleAutohide:function(t){t?e.later(1,this,"_attachUIHandlesAutohide"):this._detachUIHandlesAutohide()},_attachUIHandlesAutohide:function(){if(this._uiHandlesAutohide)return;var t=this.get(h),n=e.bind(this.hide,this),r=[],i=this,s=this.get("hideOn"),o=0,u={node:undefined,ev:undefined,keyCode:undefined};for(;o<s.length;o++)u.node=s[o].node,u.ev=s[o].eventName,u.keyCode=s[o].keyCode,!u.node&&!u.keyCode&&u.ev?r.push(t.on(u.ev,n)):u.node&&!u.keyCode&&u.ev?r.push(u.node.on(u.ev,n)):u.node&&u.keyCode&&u.ev&&r.push(u.node.on(u.ev,n,u.keyCode));this._uiHandlesAutohide=r},_detachUIHandlesAutohide:function(){e.each(this._uiHandlesAutohide,function(e){e.detach()}),this._uiHandlesAutohide=null},_afterHostVisibleChangeAutohide:function(e){this._uiSetHostVisibleAutohide(e.newVal)},_afterHideOnChange:function(e){this._detachUIHandlesAutohide(),this.get(p)&&this._attachUIHandlesAutohide()}},e.WidgetAutohide=m},"3.17.2",{requires:["base-build","event-key","event-outside","widget"]});
diff --git a/js/yui3/widget-base-ie/widget-base-ie-min.js b/js/yui3/widget-base-ie/widget-base-ie-min.js
new file mode 100644
index 000000000..9a3bb7bc9
--- /dev/null
+++ b/js/yui3/widget-base-ie/widget-base-ie-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("widget-base-ie",function(e,t){var n="boundingBox",r="contentBox",i="height",s="offsetHeight",o="",u=e.UA.ie,a=u<7,f=e.Widget.getClassName("tmp","forcesize"),l=e.Widget.getClassName("content","expanded");e.Widget.prototype._uiSizeCB=function(e){var t=this.get(n),c=this.get(r),h=this._bbs;h===undefined&&(this._bbs=h=!(u&&u<8&&t.get("ownerDocument").get("compatMode")!="BackCompat")),h?c.toggleClass(l,e):e?(a&&t.addClass(f),c.set(s,t.get(s)),a&&t.removeClass(f)):c.setStyle(i,o)}},"3.17.2",{requires:["widget-base"]});
diff --git a/js/yui3/widget-base/assets/skins/night/widget-base.css b/js/yui3/widget-base/assets/skins/night/widget-base.css
new file mode 100644
index 000000000..89eb3fd39
--- /dev/null
+++ b/js/yui3/widget-base/assets/skins/night/widget-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-widget-hidden{display:none}.yui3-widget-content{overflow:hidden}.yui3-widget-content-expanded{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;height:100%}.yui3-widget-tmp-forcesize{overflow:hidden!important}#yui3-css-stamp.skin-night-widget-base{display:none}
diff --git a/js/yui3/widget-base/assets/skins/sam/widget-base.css b/js/yui3/widget-base/assets/skins/sam/widget-base.css
new file mode 100644
index 000000000..388fd00aa
--- /dev/null
+++ b/js/yui3/widget-base/assets/skins/sam/widget-base.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-widget-hidden{display:none}.yui3-widget-content{overflow:hidden}.yui3-widget-content-expanded{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;height:100%}.yui3-widget-tmp-forcesize{overflow:hidden!important}#yui3-css-stamp.skin-sam-widget-base{display:none}
diff --git a/js/yui3/widget-base/assets/widget-base-core.css b/js/yui3/widget-base/assets/widget-base-core.css
new file mode 100644
index 000000000..3c36a40dc
--- /dev/null
+++ b/js/yui3/widget-base/assets/widget-base-core.css
@@ -0,0 +1,27 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-widget-hidden {
+ display:none;
+}
+
+.yui3-widget-content {
+ overflow:hidden;
+}
+
+.yui3-widget-content-expanded {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ box-sizing:border-box;
+ height:100%;
+}
+
+/* Only used for IE6, to go from a bigger size to a smaller size when using cb.sizeTo(bb) */
+.yui3-widget-tmp-forcesize {
+ overflow:hidden !important;
+} \ No newline at end of file
diff --git a/js/yui3/widget-base/widget-base-min.js b/js/yui3/widget-base/widget-base-min.js
new file mode 100644
index 000000000..dee0b4e63
--- /dev/null
+++ b/js/yui3/widget-base/widget-base-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("widget-base",function(e,t){function R(e){var t=this,n,r,i=t.constructor;t._strs={},t._cssPrefix=i.CSS_PREFIX||s(i.NAME.toLowerCase()),e=e||{},R.superclass.constructor.call(t,e),r=t.get(T),r&&(r!==P&&(n=r),t.render(n))}var n=e.Lang,r=e.Node,i=e.ClassNameManager,s=i.getClassName,o,u=e.cached(function(e){return e.substring(0,1).toUpperCase()+e.substring(1)}),a="content",f="visible",l="hidden",c="disabled",h="focused",p="width",d="height",v="boundingBox",m="contentBox",g="parentNode",y="ownerDocument",b="auto",w="srcNode",E="body",S="tabIndex",x="id",T="render",N="rendered",C="destroyed",k="strings",L="<div></div>",A="Change",O="loading",M="_uiSet",_="",D=function(){},P=!0,H=!1,B,j={},F=[f,c,d,p,h,S],I=e.UA.webkit,q={};R.NAME="widget",B=R.UI_SRC="ui",R.ATTRS=j,j[x]={valueFn:"_guid",writeOnce:P},j[N]={value:H,readOnly:P},j[v]={valueFn:"_defaultBB",setter:"_setBB",writeOnce:P},j[m]={valueFn:"_defaultCB",setter:"_setCB",writeOnce:P},j[S]={value:null,validator:"_validTabIndex"},j[h]={value:H,readOnly:P},j[c]={value:H},j[f]={value:P},j[d]={value:_},j[p]={value:_},j[k]={value:{},setter:"_strSetter",getter:"_strGetter"},j[T]={value:H,writeOnce:P},R.CSS_PREFIX=s(R.NAME.toLowerCase()),R.getClassName=function(){return s.apply(i,[R.CSS_PREFIX].concat(e.Array(arguments),!0))},o=R.getClassName,R.getByNode=function(t){var n,i=o();return t=r.one(t),t&&(t=t.ancestor("."+i,!0),t&&(n=q[e.stamp(t,!0)])),n||null},e.extend(R,e.Base,{getClassName:function(){return s.apply(i,[this._cssPrefix].concat(e.Array(arguments),!0))},initializer:function(t){var n=this.get(v);n instanceof r&&this._mapInstance(e.stamp(n))},_mapInstance:function(e){q[e]=this},destructor:function(){var t=this.get(v),n;t instanceof r&&(n=e.stamp(t,!0),n in q&&delete q[n],this._destroyBox())},destroy:function(e){return this._destroyAllNodes=e,R.superclass.destroy.apply(this)},_destroyBox:function(){var e=this.get(v),t=this.get(m),n=this._destroyAllNodes,r;r=e&&e.compareTo(t),this.UI_EVENTS&&this._destroyUIEvents(),this._unbindUI(e),t&&(n&&t.empty(),t.remove(P)),r||(n&&e.empty(),e.remove(P))},render:function(e){return!this.get(C)&&!this.get(N)&&(this.publish(T,{queuable:H,fireOnce:P,defaultTargetOnly:P,defaultFn:this._defRenderFn}),this.fire(T,{parentNode:e?r.one(e):null})),this},_defRenderFn:function(e){this._parentNode=e.parentNode,this.renderer(),this._set(N,P),this._removeLoadingClassNames()},renderer:function(){var e=this;e._renderUI(),e.renderUI(),e._bindUI(),e.bindUI(),e._syncUI(),e.syncUI()},bindUI:D,renderUI:D,syncUI:D,hide:function(){return this.set(f,H)},show:function(){return this.set(f,P)},focus:function(){return this._set(h,P)},blur:function(){return this._set(h,H)},enable:function(){return this.set(c,H)},disable:function(){return this.set(c,P)},_uiSizeCB:function(e){this.get(m).toggleClass(o(a,"expanded"),e)},_renderBox:function(e){var t=this,n=t.get(m),i=t.get(v),s=t.get(w),o=t.DEF_PARENT_NODE,u=s&&s.get(y)||i.get(y)||n.get(y);s&&!s.compareTo(n)&&!n.inDoc(u)&&s.replace(n),!i.compareTo(n.get(g))&&!i.compareTo(n)&&(n.inDoc(u)&&n.replace(i),i.appendChild(n)),e=e||o&&r.one(o),e?e.appendChild(i):i.inDoc(u)||r.one(E).insert(i,0)},_setBB:function(e){return this._setBox(this.get(x),e,this.BOUNDING_TEMPLATE,!0)},_setCB:function(e){return this.CONTENT_TEMPLATE===null?this.get(v):this._setBox(null,e,this.CONTENT_TEMPLATE,!1)},_defaultBB:function(){var e=this.get(w),t=this.CONTENT_TEMPLATE===null;return e&&t?e:null},_defaultCB:function(e){return this.get(w)||null},_setBox:function(t,n,i,s){return n=r.one(n),n||(n=r.create(i),s?this._bbFromTemplate=!0:this._cbFromTemplate=!0),n.get(x)||n.set(x,t||e.guid()),n},_renderUI:function(){this._renderBoxClassNames(),this._renderBox(this._parentNode)},_renderBoxClassNames:function(){var e=this._getClasses(),t,n=this.get(v),r;n.addClass(o());for(r=e.length-3;r>=0;r--)t=e[r],n.addClass(t.CSS_PREFIX||s(t.NAME.toLowerCase()));this.get(m).addClass(this.getClassName(a))},_removeLoadingClassNames:function(){var e=this.get(v),t=this.get(m),n=this.getClassName(O),r=o(O);e.removeClass(r).removeClass(n),t.removeClass(r).removeClass(n)},_bindUI:function(){this._bindAttrUI(this._UI_ATTRS.BIND),this._bindDOM()},_unbindUI:function(e){this._unbindDOM(e)},_bindDOM:function(){var t=this.get(v).get(y),n=R._hDocFocus;n||(n=R._hDocFocus=t.on("focus",this._onDocFocus,this),n.listeners={count:0}),n.listeners[e.stamp(this,!0)]=!0,n.listeners.count++,I&&(this._hDocMouseDown=t.on("mousedown",this._onDocMouseDown,this))},_unbindDOM:function(t){var n=R._hDocFocus,r=e.stamp(this,!0),i,s=this._hDocMouseDown;n&&(i=n.listeners,i[r]&&(delete i[r],i.count--),i.count===0&&(n.detach(),R._hDocFocus=null)),I&&s&&s.detach()},_syncUI:function(){this._syncAttrUI(this._UI_ATTRS.SYNC)},_uiSetHeight:function(e){this._uiSetDim(d,e),this._uiSizeCB(e!==_&&e!==b)},_uiSetWidth:function(e){this._uiSetDim(p,e)},_uiSetDim:function(e,t){this.get(v).setStyle(e,n.isNumber(t)?t+this.DEF_UNIT:t)},_uiSetVisible:function(e){this.get(v).toggleClass(this.getClassName(l),!e)},_uiSetDisabled:function(e){this.get(v).toggleClass(this.getClassName(c),e)},_uiSetFocused:function(e,t){var n=this.get(v);n.toggleClass(this.getClassName(h),e),t!==B&&(e?n.focus():n.blur())},_uiSetTabIndex:function(e){var t=this.get(v);n.isNumber(e)?t.set(S,e):t.removeAttribute(S)},_onDocMouseDown:function(e){this._domFocus&&this._onDocFocus(e)},_onDocFocus:function(e){var t=R.getByNode(e.target),n=R._active;n&&n!==t&&(n._domFocus=!1,n._set(h,!1,{src:B}),R._active=null),t&&(t._domFocus=!0,t._set(h,!0,{src:B}),R._active=t)},toString:function(){return this.name+"["+this.get(x)+"]"},DEF_UNIT:"px",DEF_PARENT_NODE:null,CONTENT_TEMPLATE:L,BOUNDING_TEMPLATE:L,_guid:function(){return e.guid()},_validTabIndex:function(e){return n.isNumber(e)||n.isNull(e)},_bindAttrUI:function(e){var t,n=e.length;for(t=0;t<n;t++)this.after(e[t]+A,this._setAttrUI)},_syncAttrUI:function(e){var t,n=e.length,r;for(t=0;t<n;t++)r=e[t],this[M+u(r)](this.get(r))},_setAttrUI:function(e){e.target===this&&this[M+u(e.attrName
+)](e.newVal,e.src)},_strSetter:function(t){return e.merge(this.get(k),t)},getString:function(e){return this.get(k)[e]},getStrings:function(){return this.get(k)},_UI_ATTRS:{BIND:F,SYNC:F}}),e.Widget=R},"3.17.2",{requires:["attribute","base-base","base-pluginhost","classnamemanager","event-focus","node-base","node-style"],skinnable:!0});
diff --git a/js/yui3/widget-buttons/assets/skins/night/sprite_icons.gif b/js/yui3/widget-buttons/assets/skins/night/sprite_icons.gif
new file mode 100644
index 000000000..fab7acb08
--- /dev/null
+++ b/js/yui3/widget-buttons/assets/skins/night/sprite_icons.gif
Binary files differ
diff --git a/js/yui3/widget-buttons/assets/skins/sam/sprite_icons.gif b/js/yui3/widget-buttons/assets/skins/sam/sprite_icons.gif
new file mode 100644
index 000000000..fa26094f6
--- /dev/null
+++ b/js/yui3/widget-buttons/assets/skins/sam/sprite_icons.gif
Binary files differ
diff --git a/js/yui3/widget-buttons/widget-buttons-min.js b/js/yui3/widget-buttons/widget-buttons-min.js
new file mode 100644
index 000000000..e11c7d636
--- /dev/null
+++ b/js/yui3/widget-buttons/widget-buttons-min.js
@@ -0,0 +1,9 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("widget-buttons",function(e,t){function p(e){return!!e.getDOMNode}function d(){this._buttonsHandles={}}var n=e.Array,r=e.Lang,i=e.Object,s=e.Plugin.Button,o=e.Widget,u=e.WidgetStdMod,a=e.ClassNameManager.getClassName,f=r.isArray,l=r.isNumber,c=r.isString,h=r.isValue;d.ATTRS={buttons:{getter:"_getButtons",setter:"_setButtons",value:{}},defaultButton:{readOnly:!0,value:null}},d.CLASS_NAMES={button:a("button"),buttons:o.getClassName("buttons"),primary:a("button","primary")},d.HTML_PARSER={buttons:function(e){return this._parseButtons(e)}},d.NON_BUTTON_NODE_CFG=["action","classNames","context","events","isDefault","section"],d.prototype={BUTTONS:{},BUTTONS_TEMPLATE:"<span />",DEFAULT_BUTTONS_SECTION:u.FOOTER,initializer:function(){this._stdModNode||e.error("WidgetStdMod must be added to a Widget before WidgetButtons."),this._mapButtons(this.get("buttons")),this._updateDefaultButton(),this.after({buttonsChange:e.bind("_afterButtonsChange",this),defaultButtonChange:e.bind("_afterDefaultButtonChange",this)}),e.after(this._bindUIButtons,this,"bindUI"),e.after(this._syncUIButtons,this,"syncUI")},destructor:function(){i.each(this._buttonsHandles,function(e){e.detach()}),delete this._buttonsHandles,delete this._buttonsMap,delete this._defaultButton},addButton:function(e,t,r){var i=this.get("buttons"),s,o;return p(e)||(e=this._mergeButtonConfig(e),t||(t=e.section)),t||(t=this.DEFAULT_BUTTONS_SECTION),s=i[t]||(i[t]=[]),l(r)||(r=s.length),s.splice(r,0,e),o=n.indexOf(s,e),this.set("buttons",i,{button:e,section:t,index:o,src:"add"}),this},getButton:function(e,t){if(!h(e))return;var n=this._buttonsMap,r;return t||(t=this.DEFAULT_BUTTONS_SECTION),l(e)?(r=this.get("buttons"),r[t]&&r[t][e]):arguments.length>1?n[t+":"+e]:n[e]},removeButton:function(e,t){if(!h(e))return this;var r=this.get("buttons"),s;return l(e)?(t||(t=this.DEFAULT_BUTTONS_SECTION),s=e,e=r[t][s]):(c(e)&&(e=this.getButton.apply(this,arguments)),i.some(r,function(r,i){s=n.indexOf(r,e);if(s>-1)return t=i,!0})),e&&s>-1&&(r[t].splice(s,1),this.set("buttons",r,{button:e,section:t,index:s,src:"remove"})),this},_bindUIButtons:function(){var t=e.bind("_afterContentChangeButtons",this);this.after({visibleChange:e.bind("_afterVisibleChangeButtons",this),headerContentChange:t,bodyContentChange:t,footerContentChange:t})},_createButton:function(t){var r,i,o,u,a,f,l,h;if(p(t))return e.one(t.getDOMNode()).plug(s);r=e.merge({context:this,events:"click",label:t.value},t),i=e.merge(r),o=d.NON_BUTTON_NODE_CFG;for(u=0,a=o.length;u<a;u+=1)delete i[o[u]];return t=s.createNode(i),l=r.context,f=r.action,c(f)&&(f=e.bind(f,l)),h=t.on(r.events,f,l),this._buttonsHandles[e.stamp(t,!0)]=h,t.setData("name",this._getButtonName(r)),t.setData("default",this._getButtonDefault(r)),n.each(n(r.classNames),t.addClass,t),t},_getButtonContainer:function(t,n){var r=u.SECTION_CLASS_NAMES[t],i=d.CLASS_NAMES.buttons,s=this.get("contentBox"),o,a;return o="."+r+" ."+i,a=s.one(o),!a&&n&&(a=e.Node.create(this.BUTTONS_TEMPLATE),a.addClass(i)),a},_getButtonDefault:function(e){var t=p(e)?e.getData("default"):e.isDefault;return c(t)?t.toLowerCase()==="true":!!t},_getButtonName:function(e){var t;return p(e)?t=e.getData("name")||e.get("name"):t=e&&(e.name||e.type),t},_getButtons:function(e){var t={};return i.each(e,function(e,n){t[n]=e.concat()}),t},_mapButton:function(e,t){var n=this._buttonsMap,r=this._getButtonName(e),i=this._getButtonDefault(e);r&&(n[r]=e,n[t+":"+r]=e),i&&(this._defaultButton=e)},_mapButtons:function(e){this._buttonsMap={},this._defaultButton=null,i.each(e,function(e,t){var n,r;for(n=0,r=e.length;n<r;n+=1)this._mapButton(e[n],t)},this)},_mergeButtonConfig:function(t){var n,r,i,s,o,u;return t=c(t)?{name:t}:e.merge(t),t.srcNode&&(s=t.srcNode,o=s.get("tagName").toLowerCase(),u=s.get(o==="input"?"value":"text"),n={disabled:!!s.get("disabled"),isDefault:this._getButtonDefault(s),name:this._getButtonName(s)},u&&(n.label=u),e.mix(t,n,!1,null,0,!0)),i=this._getButtonName(t),r=this.BUTTONS&&this.BUTTONS[i],r&&e.mix(t,r,!1,null,0,!0),t},_parseButtons:function(e){var t="."+d.CLASS_NAMES.button,r=["header","body","footer"],i=null;return n.each(r,function(e){var n=this._getButtonContainer(e),r=n&&n.all(t),s;if(!r||r.isEmpty())return;s=[],r.each(function(e){s.push({srcNode:e})}),i||(i={}),i[e]=s},this),i},_setButtons:function(e){function r(e,r){if(!f(e))return;var i,s,o,u;for(i=0,s=e.length;i<s;i+=1)o=e[i],u=r,p(o)||(o=this._mergeButtonConfig(o),u||(u=o.section)),o=this._createButton(o),u||(u=t),(n[u]||(n[u]=[])).push(o)}var t=this.DEFAULT_BUTTONS_SECTION,n={};return f(e)?r.call(this,e):i.each(e,r,this),n},_syncUIButtons:function(){this._uiSetButtons(this.get("buttons")),this._uiSetDefaultButton(this.get("defaultButton")),this._uiSetVisibleButtons(this.get("visible"))},_uiInsertButton:function(e,t,n){var r=d.CLASS_NAMES.button,i=this._getButtonContainer(t,!0),s=i.all("."+r);i.insertBefore(e,s.item(n)),this.setStdModContent(t,i,"after")},_uiRemoveButton:function(t,n,r){var i=e.stamp(t,this),s=this._buttonsHandles,o=s[i],u,a;o&&o.detach(),delete s[i],t.remove(),r||(r={}),r.preserveContent||(u=this._getButtonContainer(n),a=d.CLASS_NAMES.button,u&&u.all("."+a).isEmpty()&&(u.remove(),this._updateContentButtons(n)))},_uiSetButtons:function(e){var t=d.CLASS_NAMES.button,r=["header","body","footer"];n.each(r,function(n){var r=e[n]||[],i=r.length,s=this._getButtonContainer(n,i),o=!1,u,a,f,l;if(!s)return;u=s.all("."+t);for(a=0;a<i;a+=1)f=r[a],l=u.indexOf(f),l>-1?(u.splice(l,1),l!==a&&(s.insertBefore(f,a+1),o=!0)):(s.appendChild(f),o=!0);u.each(function(e){this._uiRemoveButton(e,n,{preserveContent:!0}),o=!0},this);if(i===0){s.remove(),this._updateContentButtons(n);return}o&&this.setStdModContent(n,s,"after")},this)},_uiSetDefaultButton:function(e,t){var n=d.CLASS_NAMES.primary;e&&e.addClass(n),t&&t.removeClass(n)},_uiSetVisibleButtons:function(e){if(!e)return;var t=this.get("defaultButton");t&&t.focus()},_unMapButton:function(e,t){var n=this._buttonsMap,r=this._getButtonName(e),i;r&&(n[r]===e&&delete
+n[r],i=t+":"+r,n[i]===e&&delete n[i]),this._defaultButton===e&&(this._defaultButton=null)},_updateDefaultButton:function(){var e=this._defaultButton;this.get("defaultButton")!==e&&this._set("defaultButton",e)},_updateContentButtons:function(e){var t=this.getStdModNode(e).get("childNodes");this.set(e+"Content",t.isEmpty()?null:t,{src:"buttons"})},_afterButtonsChange:function(e){var t=e.newVal,n=e.section,r=e.index,i=e.src,s;if(i==="add"){s=t[n][r],this._mapButton(s,n),this._updateDefaultButton(),this._uiInsertButton(s,n,r);return}if(i==="remove"){s=e.button,this._unMapButton(s,n),this._updateDefaultButton(),this._uiRemoveButton(s,n);return}this._mapButtons(t),this._updateDefaultButton(),this._uiSetButtons(t)},_afterContentChangeButtons:function(e){var t=e.src,n=e.stdModPosition,r=!n||n===u.REPLACE;r&&t!=="buttons"&&t!==o.UI_SRC&&this._uiSetButtons(this.get("buttons"))},_afterDefaultButtonChange:function(e){this._uiSetDefaultButton(e.newVal,e.prevVal)},_afterVisibleChangeButtons:function(e){this._uiSetVisibleButtons(e.newVal)}},e.WidgetButtons=d},"3.17.2",{requires:["button-plugin","cssbutton","widget-stdmod"]});
diff --git a/js/yui3/widget-child/widget-child-min.js b/js/yui3/widget-child/widget-child-min.js
new file mode 100644
index 000000000..efbe2c3e3
--- /dev/null
+++ b/js/yui3/widget-child/widget-child-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("widget-child",function(e,t){function r(){e.after(this._syncUIChild,this,"syncUI"),e.after(this._bindUIChild,this,"bindUI")}var n=e.Lang;r.ATTRS={selected:{value:0,validator:n.isNumber},index:{readOnly:!0,getter:function(){var e=this.get("parent"),t=-1;return e&&(t=e.indexOf(this)),t}},parent:{readOnly:!0},depth:{readOnly:!0,getter:function(){var e=this.get("parent"),t=this.get("root"),n=-1;while(e){n+=1;if(e==t)break;e=e.get("parent")}return n}},root:{readOnly:!0,getter:function(){var t=function(n){var r=n.get("parent"),i=n.ROOT_TYPE,s=r;return i&&(s=r&&e.instanceOf(r,i)),s?t(r):n};return t(this)}}},r.prototype={ROOT_TYPE:null,_getUIEventNode:function(){var e=this.get("root"),t;return e&&(t=e.get("boundingBox")),t},next:function(e){var t=this.get("parent"),n;return t&&(n=t.item(this.get("index")+1)),!n&&e&&(n=t.item(0)),n},previous:function(e){var t=this.get("parent"),n=this.get("index"),r;return t&&n>0&&(r=t.item([n-1])),!r&&e&&(r=t.item(t.size()-1)),r},remove:function(t){var r,i;return n.isNumber(t)?i=e.WidgetParent.prototype.remove.apply(this,arguments):(r=this.get("parent"),r&&(i=r.remove(this.get("index")))),i},isRoot:function(){return this==this.get("root")},ancestor:function(e){var t=this.get("root"),n;if(this.get("depth")>e){n=this.get("parent");while(n!=t&&n.get("depth")>e)n=n.get("parent")}return n},_uiSetChildSelected:function(e){var t=this.get("boundingBox"),n=this.getClassName("selected");e===0?t.removeClass(n):t.addClass(n)},_afterChildSelectedChange:function(e){this._uiSetChildSelected(e.newVal)},_syncUIChild:function(){this._uiSetChildSelected(this.get("selected"))},_bindUIChild:function(){this.after("selectedChange",this._afterChildSelectedChange)}},e.WidgetChild=r},"3.17.2",{requires:["base-build","widget"]});
diff --git a/js/yui3/widget-htmlparser/widget-htmlparser-min.js b/js/yui3/widget-htmlparser/widget-htmlparser-min.js
new file mode 100644
index 000000000..6fbcfc98f
--- /dev/null
+++ b/js/yui3/widget-htmlparser/widget-htmlparser-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("widget-htmlparser",function(e,t){var n=e.Widget,r=e.Node,i=e.Lang,s="srcNode",o="contentBox";n.HTML_PARSER={},n._buildCfg={aggregates:["HTML_PARSER"]},n.ATTRS[s]={value:null,setter:r.one,getter:"_getSrcNode",writeOnce:!0},e.mix(n.prototype,{_getSrcNode:function(e){return e||this.get(o)},_preAddAttrs:function(e,t,n){var r={id:e.id,boundingBox:e.boundingBox,contentBox:e.contentBox,srcNode:e.srcNode};this.addAttrs(r,t,n),delete e.boundingBox,delete e.contentBox,delete e.srcNode,delete e.id,this._applyParser&&this._applyParser(t)},_applyParsedConfig:function(t,n,r){return r?e.mix(n,r,!1):n},_applyParser:function(t){var n=this,r=this._getNodeToParse(),s=n._getHtmlParser(),o,u;s&&r&&e.Object.each(s,function(e,t,s){u=null,i.isFunction(e)?u=e.call(n,r):i.isArray(e)?(u=r.all(e[0]),u.isEmpty()&&(u=null)):u=r.one(e),u!==null&&u!==undefined&&(o=o||{},o[t]=u)}),t=n._applyParsedConfig(r,t,o)},_getNodeToParse:function(){var e=this.get("srcNode");return this._cbFromTemplate?null:e},_getHtmlParser:function(){var t=this._getClasses(),n={},r,i;for(r=t.length-1;r>=0;r--)i=t[r].HTML_PARSER,i&&e.mix(n,i,!0);return n}})},"3.17.2",{requires:["widget-base"]});
diff --git a/js/yui3/widget-modality/assets/skins/night/widget-modality.css b/js/yui3/widget-modality/assets/skins/night/widget-modality.css
new file mode 100644
index 000000000..1b9a6373d
--- /dev/null
+++ b/js/yui3/widget-modality/assets/skins/night/widget-modality.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-night .yui3-widget-mask{background-color:black;zoom:1;-ms-filter:"alpha(opacity=40)";filter:alpha(opacity=40);opacity:.4}#yui3-css-stamp.skin-night-widget-modality{display:none}
diff --git a/js/yui3/widget-modality/assets/skins/sam/widget-modality.css b/js/yui3/widget-modality/assets/skins/sam/widget-modality.css
new file mode 100644
index 000000000..28e44a10e
--- /dev/null
+++ b/js/yui3/widget-modality/assets/skins/sam/widget-modality.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-skin-sam .yui3-widget-mask{background-color:black;zoom:1;-ms-filter:"alpha(opacity=40)";filter:alpha(opacity=40);opacity:.4}#yui3-css-stamp.skin-sam-widget-modality{display:none}
diff --git a/js/yui3/widget-modality/assets/widget-modality-core.css b/js/yui3/widget-modality/assets/widget-modality-core.css
new file mode 100644
index 000000000..c11a608cd
--- /dev/null
+++ b/js/yui3/widget-modality/assets/widget-modality-core.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+/* WidgetModality core styles */
diff --git a/js/yui3/widget-modality/widget-modality-min.js b/js/yui3/widget-modality/widget-modality-min.js
new file mode 100644
index 000000000..f76d03ca8
--- /dev/null
+++ b/js/yui3/widget-modality/widget-modality-min.js
@@ -0,0 +1,10 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("widget-modality",function(e,t){function g(e){}var n="widget",r="renderUI",i="bindUI",s="syncUI",o="boundingBox",u="visible",a="zIndex",f="Change",l=e.Lang.isBoolean,c=e.ClassNameManager.getClassName,h="maskShow",p="maskHide",d="clickoutside",v="focusoutside",m=function(){
+/*! IS_POSITION_FIXED_SUPPORTED - Juriy Zaytsev (kangax) - http://yura.thinkweb2.com/cft/ */
+;var t=e.config.doc,n=null,r,i;return t.createElement&&(r=t.createElement("div"),r&&r.style&&(r.style.position="fixed",r.style.top="10px",i=t.body,i&&i.appendChild&&i.removeChild&&(i.appendChild(r),n=r.offsetTop===10,i.removeChild(r)))),n}(),y="modal",b="mask",w={modal:c(n,y),mask:c(n,b)};g.ATTRS={maskNode:{getter:"_getMaskNode",readOnly:!0},modal:{value:!1,validator:l},focusOn:{valueFn:function(){return[{eventName:d},{eventName:v}]},validator:e.Lang.isArray}},g.CLASSES=w,g._MASK=null,g._GET_MASK=function(){var t=g._MASK,n=e.one("win");return t&&t.getDOMNode()!==null&&t.inDoc()?t:(t=e.Node.create("<div></div>").addClass(w.mask),g._MASK=t,m?t.setStyles({position:"fixed",width:"100%",height:"100%",top:"0",left:"0",display:"block"}):t.setStyles({position:"absolute",width:n.get("winWidth")+"px",height:n.get("winHeight")+"px",top:"0",left:"0",display:"block"}),t)},g.STACK=[],g.prototype={initializer:function(){e.after(this._renderUIModal,this,r),e.after(this._syncUIModal,this,s),e.after(this._bindUIModal,this,i)},destructor:function(){this._uiSetHostVisibleModal(!1)},_uiHandlesModal:null,_renderUIModal:function(){var e=this.get(o);this._repositionMask(this),e.addClass(w.modal)},_bindUIModal:function(){this.after(u+f,this._afterHostVisibleChangeModal),this.after(a+f,this._afterHostZIndexChangeModal),this.after("focusOnChange",this._afterFocusOnChange),(!m||e.UA.ios&&e.UA.ios<5||e.UA.android&&e.UA.android<3)&&e.one("win").on("scroll",this._resyncMask,this)},_syncUIModal:function(){this._uiSetHostVisibleModal(this.get(u))},_focus:function(){var e=this.get(o),t=e.get("tabIndex");e.set("tabIndex",t>=0?t:0),this.focus()},_blur:function(){this.blur()},_getMaskNode:function(){return g._GET_MASK()},_uiSetHostVisibleModal:function(t){var n=g.STACK,r=this.get("maskNode"),i=this.get("modal"),s,o;t?(e.Array.each(n,function(e){e._detachUIHandlesModal(),e._blur()}),n.unshift(this),this._repositionMask(this),this._uiSetHostZIndexModal(this.get(a)),i&&(r.show(),e.later(1,this,"_attachUIHandlesModal"),this._focus())):(o=e.Array.indexOf(n,this),o>=0&&n.splice(o,1),this._detachUIHandlesModal(),this._blur(),n.length?(s=n[0],this._repositionMask(s),s._uiSetHostZIndexModal(s.get(a)),s.get("modal")&&(e.later(1,s,"_attachUIHandlesModal"),s._focus())):r.getStyle("display")==="block"&&r.hide())},_uiSetHostZIndexModal:function(e){this.get("modal")&&this.get("maskNode").setStyle(a,e||0)},_attachUIHandlesModal:function(){if(this._uiHandlesModal||g.STACK[0]!==this)return;var t=this.get(o),n=this.get("maskNode"),r=this.get("focusOn"),i=e.bind(this._focus,this),s=[],u,a,f;for(u=0,a=r.length;u<a;u++)f={},f.node=r[u].node,f.ev=r[u].eventName,f.keyCode=r[u].keyCode,!f.node&&!f.keyCode&&f.ev?s.push(t.on(f.ev,i)):f.node&&!f.keyCode&&f.ev?s.push(f.node.on(f.ev,i)):f.node&&f.keyCode&&f.ev?s.push(f.node.on(f.ev,i,f.keyCode)):e.Log('focusOn ATTR Error: The event with name "'+f.ev+'" could not be attached.');m||s.push(e.one("win").on("scroll",e.bind(function(){n.setStyle("top",n.get("docScrollY"))},this))),this._uiHandlesModal=s},_detachUIHandlesModal:function(){e.each(this._uiHandlesModal,function(e){e.detach()}),this._uiHandlesModal=null},_afterHostVisibleChangeModal:function(e){this._uiSetHostVisibleModal(e.newVal)},_afterHostZIndexChangeModal:function(e){this._uiSetHostZIndexModal(e.newVal)},isNested:function(){var e=g.STACK.length,t=e>1?!0:!1;return t},_repositionMask:function(t){var n=this.get("modal"),r=t.get("modal"),i=this.get("maskNode"),s,u;if(n&&!r)i.remove(),this.fire(p);else if(!n&&r||n&&r)i.remove(),this.fire(p),s=t.get(o),u=s.get("parentNode")||e.one("body"),u.insert(i,u.get("firstChild")),this.fire(h)},_resyncMask:function(e){var t=e.currentTarget,n=t.get("docScrollX"),r=t.get("docScrollY"),i=t.get("innerWidth")||t.get("winWidth"),s=t.get("innerHeight")||t.get("winHeight"),o=this.get("maskNode");o.setStyles({top:r+"px",left:n+"px",width:i+"px",height:s+"px"})},_afterFocusOnChange:function(){this._detachUIHandlesModal(),this.get(u)&&this._attachUIHandlesModal()}},e.WidgetModality=g},"3.17.2",{requires:["base-build","event-outside","widget"],skinnable:!0});
diff --git a/js/yui3/widget-parent/widget-parent-min.js b/js/yui3/widget-parent/widget-parent-min.js
new file mode 100644
index 000000000..520aa0a91
--- /dev/null
+++ b/js/yui3/widget-parent/widget-parent-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("widget-parent",function(e,t){function s(t){this.publish("addChild",{defaultTargetOnly:!0,defaultFn:this._defAddChildFn}),this.publish("removeChild",{defaultTargetOnly:!0,defaultFn:this._defRemoveChildFn}),this._items=[];var n,r;t&&t.children&&(n=t.children,r=this.after("initializedChange",function(e){this._add(n),r.detach()})),e.after(this._renderChildren,this,"renderUI"),e.after(this._bindUIParent,this,"bindUI"),this.after("selectionChange",this._afterSelectionChange),this.after("selectedChange",this._afterParentSelectedChange),this.after("activeDescendantChange",this._afterActiveDescendantChange),this._hDestroyChild=this.after("*:destroy",this._afterDestroyChild),this.after("*:focusedChange",this._updateActiveDescendant)}var n=e.Lang,r="rendered",i="boundingBox";s.ATTRS={defaultChildType:{setter:function(t){var r=e.Attribute.INVALID_VALUE,i=n.isString(t)?e[t]:t;return n.isFunction(i)&&(r=i),r}},activeDescendant:{readOnly:!0},multiple:{value:!1,validator:n.isBoolean,writeOnce:!0,getter:function(e){var t=this.get("root");return t&&t!=this?t.get("multiple"):e}},selection:{readOnly:!0,setter:"_setSelection",getter:function(t){var r=n.isArray(t)?new e.ArrayList(t):t;return r}},selected:{setter:function(t){var n=t;return t===1&&!this.get("multiple")&&(n=e.Attribute.INVALID_VALUE),n}}},s.prototype={destructor:function(){this._destroyChildren()},_afterDestroyChild:function(e){var t=e.target;t.get("parent")==this&&t.remove()},_afterSelectionChange:function(t){if(t.target==this&&t.src!=this){var n=t.newVal,r=0;n&&(r=2,e.instanceOf(n,e.ArrayList)&&n.size()===this.size()&&(r=1)),this.set("selected",r,{src:this})}},_afterActiveDescendantChange:function(e){var t=this.get("parent");t&&t._set("activeDescendant",e.newVal)},_afterParentSelectedChange:function(e){var t=e.newVal;this==e.target&&e.src!=this&&(t===0||t===1)&&this.each(function(e){e.set("selected",t,{src:this})},this)},_setSelection:function(e){var t=null,n;return this.get("multiple")&&!this.isEmpty()?(n=[],this.each(function(e){e.get("selected")>0&&n.push(e)}),n.length>0&&(t=n)):e.get("selected")>0&&(t=e),t},_updateSelection:function(e){var t=e.target,n;t.get("parent")==this&&(e.src!="_updateSelection"&&(n=this.get("selection"),!this.get("multiple")&&n&&e.newVal>0&&n.set("selected",0,{src:"_updateSelection"}),this._set("selection",t)),e.src==this&&this._set("selection",t,{src:this}))},_updateActiveDescendant:function(e){var t=e.newVal===!0?e.target:null;this._set("activeDescendant",t)},_createChild:function(t){var r=this.get("defaultChildType"),i=t.childType||t.type,s,o,u;return i&&(o=n.isString(i)?e[i]:i),n.isFunction(o)?u=o:r&&(u=r),u?s=new u(t):e.error("Could not create a child instance because its constructor is either undefined or invalid."),s},_defAddChildFn:function(t){var r=t.child,i=t.index,s=this._items;r.get("parent")&&r.remove(),n.isNumber(i)?s.splice(i,0,r):s.push(r),r._set("parent",this),r.addTarget(this),t.index=r.get("index"),r.after("selectedChange",e.bind(this._updateSelection,this))},_defRemoveChildFn:function(e){var t=e.child,n=e.index,r=this._items;t.get("focused")&&t.blur(),t.get("selected")&&t.set("selected",0),r.splice(n,1),t.removeTarget(this),t._oldParent=t.get("parent"),t._set("parent",null)},_add:function(t,r){var i,s,o;return n.isArray(t)?(i=[],e.each(t,function(e,t){s=this._add(e,r+t),s&&i.push(s)},this),i.length>0&&(o=i)):(e.instanceOf(t,e.Widget)?s=t:s=this._createChild(t),s&&this.fire("addChild",{child:s,index:r})&&(o=s)),o},add:function(){var t=this._add.apply(this,arguments),r=t?n.isArray(t)?t:[t]:[];return new e.ArrayList(r)},remove:function(e){var t=this._items[e],n;return t&&this.fire("removeChild",{child:t,index:e})&&(n=t),n},removeAll:function(){var t=[],n;return e.each(this._items.concat(),function(){n=this.remove(0),n&&t.push(n)},this),new e.ArrayList(t)},selectChild:function(e){this.item(e).set("selected",1)},selectAll:function(){this.set("selected",1)},deselectAll:function(){this.set("selected",0)},_uiAddChild:function(e,t){e.render(t);var n=e.get("boundingBox"),s,o=e.next(!1),u;o&&o.get(r)?(s=o.get(i),s.insert(n,"before")):(u=e.previous(!1),u&&u.get(r)?(s=u.get(i),s.insert(n,"after")):t.contains(n)||t.appendChild(n))},_uiRemoveChild:function(e){e.get("boundingBox").remove()},_afterAddChild:function(e){var t=e.child;t.get("parent")==this&&this._uiAddChild(t,this._childrenContainer)},_afterRemoveChild:function(e){var t=e.child;t._oldParent==this&&this._uiRemoveChild(t)},_bindUIParent:function(){this.after("addChild",this._afterAddChild),this.after("removeChild",this._afterRemoveChild)},_renderChildren:function(){var e=this._childrenContainer||this.get("contentBox");this._childrenContainer=e,this.each(function(t){t.render(e)})},_destroyChildren:function(){this._hDestroyChild.detach(),this.each(function(e){e.destroy()})}},e.augment(s,e.ArrayList),e.WidgetParent=s},"3.17.2",{requires:["arraylist","base-build","widget"]});
diff --git a/js/yui3/widget-position-align/widget-position-align-min.js b/js/yui3/widget-position-align/widget-position-align-min.js
new file mode 100644
index 000000000..b09a9fe2c
--- /dev/null
+++ b/js/yui3/widget-position-align/widget-position-align-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("widget-position-align",function(e,t){function c(e){}var n=e.Lang,r="align",i="alignOn",s="visible",o="boundingBox",u="offsetWidth",a="offsetHeight",f="region",l="viewportRegion";c.ATTRS={align:{value:null},centered:{setter:"_setAlignCenter",lazyAdd:!1,value:!1},alignOn:{value:[],validator:e.Lang.isArray}},c.TL="tl",c.TR="tr",c.BL="bl",c.BR="br",c.TC="tc",c.RC="rc",c.BC="bc",c.LC="lc",c.CC="cc",c.prototype={initializer:function(){this._posNode||e.error("WidgetPosition needs to be added to the Widget, before WidgetPositionAlign is added"),e.after(this._bindUIPosAlign,this,"bindUI"),e.after(this._syncUIPosAlign,this,"syncUI")},_posAlignUIHandles:null,destructor:function(){this._detachPosAlignUIHandles()},_bindUIPosAlign:function(){this.after("alignChange",this._afterAlignChange),this.after("alignOnChange",this._afterAlignOnChange),this.after("visibleChange",this._syncUIPosAlign)},_syncUIPosAlign:function(){var e=this.get(r);this._uiSetVisiblePosAlign(this.get(s)),e&&this._uiSetAlign(e.node,e.points)},align:function(e,t){return arguments.length?this.set(r,{node:e,points:t}):this._syncUIPosAlign(),this},centered:function(e){return this.align(e,[c.CC,c.CC])},_setAlignCenter:function(e){return e&&this.set(r,{node:e===!0?null:e,points:[c.CC,c.CC]}),e},_uiSetAlign:function(t,r){if(!n.isArray(r)||r.length!==2){e.error("align: Invalid Points Arguments");return}var i=this._getRegion(t),s,o,u;if(!i)return;s=r[0],o=r[1];switch(o){case c.TL:u=[i.left,i.top];break;case c.TR:u=[i.right,i.top];break;case c.BL:u=[i.left,i.bottom];break;case c.BR:u=[i.right,i.bottom];break;case c.TC:u=[i.left+Math.floor(i.width/2),i.top];break;case c.BC:u=[i.left+Math.floor(i.width/2),i.bottom];break;case c.LC:u=[i.left,i.top+Math.floor(i.height/2)];break;case c.RC:u=[i.right,i.top+Math.floor(i.height/2)];break;case c.CC:u=[i.left+Math.floor(i.width/2),i.top+Math.floor(i.height/2)];break;default:}u&&this._doAlign(s,u[0],u[1])},_uiSetVisiblePosAlign:function(e){e?this._attachPosAlignUIHandles():this._detachPosAlignUIHandles()},_attachPosAlignUIHandles:function(){if(this._posAlignUIHandles)return;var t=this.get(o),n=e.bind(this._syncUIPosAlign,this),r=[];e.Array.each(this.get(i),function(i){var s=i.eventName,o=e.one(i.node)||t;s&&r.push(o.on(s,n))}),this._posAlignUIHandles=r},_detachPosAlignUIHandles:function(){var t=this._posAlignUIHandles;t&&((new e.EventHandle(t)).detach(),this._posAlignUIHandles=null)},_doAlign:function(e,t,n){var r=this._posNode,i;switch(e){case c.TL:i=[t,n];break;case c.TR:i=[t-r.get(u),n];break;case c.BL:i=[t,n-r.get(a)];break;case c.BR:i=[t-r.get(u),n-r.get(a)];break;case c.TC:i=[t-r.get(u)/2,n];break;case c.BC:i=[t-r.get(u)/2,n-r.get(a)];break;case c.LC:i=[t,n-r.get(a)/2];break;case c.RC:i=[t-r.get(u),n-r.get(a)/2];break;case c.CC:i=[t-r.get(u)/2,n-r.get(a)/2];break;default:}i&&this.move(i)},_getRegion:function(t){var n;return t?(t=e.Node.one(t),t&&(n=t.get(f))):n=this._posNode.get(l),n},_afterAlignChange:function(e){var t=e.newVal;t&&this._uiSetAlign(t.node,t.points)},_afterAlignOnChange:function(e){this._detachPosAlignUIHandles(),this.get(s)&&this._attachPosAlignUIHandles()}},e.WidgetPositionAlign=c},"3.17.2",{requires:["widget-position"]});
diff --git a/js/yui3/widget-position-constrain/widget-position-constrain-min.js b/js/yui3/widget-position-constrain/widget-position-constrain-min.js
new file mode 100644
index 000000000..b2b4567cb
--- /dev/null
+++ b/js/yui3/widget-position-constrain/widget-position-constrain-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("widget-position-constrain",function(e,t){function m(e){}var n="constrain",r="constrain|xyChange",i="constrainChange",s="preventOverlap",o="align",u="",a="bindUI",f="xy",l="x",c="y",h=e.Node,p="viewportRegion",d="region",v;m.ATTRS={constrain:{value:null,setter:"_setConstrain"},preventOverlap:{value:!1}},v=m._PREVENT_OVERLAP={x:{tltr:1,blbr:1,brbl:1,trtl:1},y:{trbr:1,tlbl:1,bltl:1,brtr:1}},m.prototype={initializer:function(){this._posNode||e.error("WidgetPosition needs to be added to the Widget, before WidgetPositionConstrain is added"),e.after(this._bindUIPosConstrained,this,a)},getConstrainedXY:function(e,t){t=t||this.get(n);var r=this._getRegion(t===!0?null:t),i=this._posNode.get(d);return[this._constrain(e[0],l,i,r),this._constrain(e[1],c,i,r)]},constrain:function(e,t){var r,i,s=t||this.get(n);s&&(r=e||this.get(f),i=this.getConstrainedXY(r,s),(i[0]!==r[0]||i[1]!==r[1])&&this.set(f,i,{constrained:!0}))},_setConstrain:function(e){return e===!0?e:h.one(e)},_constrain:function(e,t,n,r){if(r){this.get(s)&&(e=this._preventOverlap(e,t,n,r));var i=t==l,o=i?r.width:r.height,u=i?n.width:n.height,a=i?r.left:r.top,f=i?r.right-u:r.bottom-u;if(e<a||e>f)u<o?e<a?e=a:e>f&&(e=f):e=a}return e},_preventOverlap:function(e,t,n,r){var i=this.get(o),s=t===l,a,f,c,h,p,d;return i&&i.points&&v[t][i.points.join(u)]&&(f=this._getRegion(i.node),f&&(a=s?n.width:n.height,c=s?f.left:f.top,h=s?f.right:f.bottom,p=s?f.left-r.left:f.top-r.top,d=s?r.right-f.right:r.bottom-f.bottom),e>c?d<a&&p>a&&(e=c-a):p<a&&d>a&&(e=h)),e},_bindUIPosConstrained:function(){this.after(i,this._afterConstrainChange),this._enableConstraints(this.get(n))},_afterConstrainChange:function(e){this._enableConstraints(e.newVal)},_enableConstraints:function(e){e?(this.constrain(),this._cxyHandle=this._cxyHandle||this.on(r,this._constrainOnXYChange)):this._cxyHandle&&(this._cxyHandle.detach(),this._cxyHandle=null)},_constrainOnXYChange:function(e){e.constrained||(e.newVal=this.getConstrainedXY(e.newVal))},_getRegion:function(e){var t;return e?(e=h.one(e),e&&(t=e.get(d))):t=this._posNode.get(p),t}},e.WidgetPositionConstrain=m},"3.17.2",{requires:["widget-position"]});
diff --git a/js/yui3/widget-position/widget-position-min.js b/js/yui3/widget-position/widget-position-min.js
new file mode 100644
index 000000000..3abed4e96
--- /dev/null
+++ b/js/yui3/widget-position/widget-position-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("widget-position",function(e,t){function d(e){}var n=e.Lang,r=e.Widget,i="xy",s="position",o="positioned",u="boundingBox",a="relative",f="renderUI",l="bindUI",c="syncUI",h=r.UI_SRC,p="xyChange";d.ATTRS={x:{setter:function(e){this._setX(e)},getter:function(){return this._getX()},lazyAdd:!1},y:{setter:function(e){this._setY(e)},getter:function(){return this._getY()},lazyAdd:!1},xy:{value:[0,0],validator:function(e){return this._validateXY(e)}}},d.POSITIONED_CLASS_NAME=r.getClassName(o),d.prototype={initializer:function(){this._posNode=this.get(u),e.after(this._renderUIPosition,this,f),e.after(this._syncUIPosition,this,c),e.after(this._bindUIPosition,this,l)},_renderUIPosition:function(){this._posNode.addClass(d.POSITIONED_CLASS_NAME)},_syncUIPosition:function(){var e=this._posNode;e.getStyle(s)===a&&this.syncXY(),this._uiSetXY(this.get(i))},_bindUIPosition:function(){this.after(p,this._afterXYChange)},move:function(){var e=arguments,t=n.isArray(e[0])?e[0]:[e[0],e[1]];this.set(i,t)},syncXY:function(){this.set(i,this._posNode.getXY(),{src:h})},_validateXY:function(e){return n.isArray(e)&&n.isNumber(e[0])&&n.isNumber(e[1])},_setX:function(e){this.set(i,[e,this.get(i)[1]])},_setY:function(e){this.set(i,[this.get(i)[0],e])},_getX:function(){return this.get(i)[0]},_getY:function(){return this.get(i)[1]},_afterXYChange:function(e){e.src!=h&&this._uiSetXY(e.newVal)},_uiSetXY:function(e){this._posNode.setXY(e)}},e.WidgetPosition=d},"3.17.2",{requires:["base-build","node-screen","widget"]});
diff --git a/js/yui3/widget-skin/widget-skin-min.js b/js/yui3/widget-skin/widget-skin-min.js
new file mode 100644
index 000000000..e9ba8ab40
--- /dev/null
+++ b/js/yui3/widget-skin/widget-skin-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("widget-skin",function(e,t){var n="boundingBox",r="contentBox",i="skin",s=e.ClassNameManager.getClassName;e.Widget.prototype.getSkinName=function(e){var t=this.get(r)||this.get(n),o,u;return e=e||s(i,""),u=new RegExp("\\b"+e+"(\\S+)"),t&&t.ancestor(function(e){return o=e.get("className").match(u),o}),o?o[1]:null}},"3.17.2",{requires:["widget-base"]});
diff --git a/js/yui3/widget-stack/assets/skins/night/widget-stack.css b/js/yui3/widget-stack/assets/skins/night/widget-stack.css
new file mode 100644
index 000000000..69c57e0dd
--- /dev/null
+++ b/js/yui3/widget-stack/assets/skins/night/widget-stack.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-widget-stacked .yui3-widget-shim{opacity:0;filter:alpha(opacity=0);position:absolute;border:0;top:0;left:0;padding:0;margin:0;z-index:-1;width:100%;height:100%;_width:0;_height:0}#yui3-css-stamp.skin-night-widget-stack{display:none}
diff --git a/js/yui3/widget-stack/assets/skins/sam/widget-stack.css b/js/yui3/widget-stack/assets/skins/sam/widget-stack.css
new file mode 100644
index 000000000..989ac5bcb
--- /dev/null
+++ b/js/yui3/widget-stack/assets/skins/sam/widget-stack.css
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-widget-stacked .yui3-widget-shim{opacity:0;filter:alpha(opacity=0);position:absolute;border:0;top:0;left:0;padding:0;margin:0;z-index:-1;width:100%;height:100%;_width:0;_height:0}#yui3-css-stamp.skin-sam-widget-stack{display:none}
diff --git a/js/yui3/widget-stack/assets/widget-stack-core.css b/js/yui3/widget-stack/assets/widget-stack-core.css
new file mode 100644
index 000000000..24ac03de9
--- /dev/null
+++ b/js/yui3/widget-stack/assets/widget-stack-core.css
@@ -0,0 +1,26 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-widget-stacked .yui3-widget-shim {
+ opacity:0;
+ filter:alpha(opacity=0);
+ position:absolute;
+ border:none;
+ top:0px;
+ left:0px;
+ padding:0;
+ margin:0;
+ z-index:-1;
+ width:100%;
+ height:100%;
+ /*
+ We'll be setting these programmatically for IE6,
+ to account for cases where height is not set
+ */
+ _width:0;
+ _height:0;
+} \ No newline at end of file
diff --git a/js/yui3/widget-stack/widget-stack-min.js b/js/yui3/widget-stack/widget-stack-min.js
new file mode 100644
index 000000000..125800642
--- /dev/null
+++ b/js/yui3/widget-stack/widget-stack-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("widget-stack",function(e,t){function O(e){}var n=e.Lang,r=e.UA,i=e.Node,s=e.Widget,o="zIndex",u="shim",a="visible",f="boundingBox",l="renderUI",c="bindUI",h="syncUI",p="offsetWidth",d="offsetHeight",v="parentNode",m="firstChild",g="ownerDocument",y="width",b="height",w="px",E="shimdeferred",S="shimresize",x="visibleChange",T="widthChange",N="heightChange",C="shimChange",k="zIndexChange",L="contentUpdate",A="stacked";O.ATTRS={shim:{value:r.ie==6},zIndex:{value:0,setter:"_setZIndex"}},O.HTML_PARSER={zIndex:function(e){return this._parseZIndex(e)}},O.SHIM_CLASS_NAME=s.getClassName(u),O.STACKED_CLASS_NAME=s.getClassName(A),O.SHIM_TEMPLATE='<iframe class="'+O.SHIM_CLASS_NAME+'" frameborder="0" title="Widget Stacking Shim" src="javascript:false" tabindex="-1" role="presentation"></iframe>',O.prototype={initializer:function(){this._stackNode=this.get(f),this._stackHandles={},e.after(this._renderUIStack,this,l),e.after(this._syncUIStack,this,h),e.after(this._bindUIStack,this,c)},_syncUIStack:function(){this._uiSetShim(this.get(u)),this._uiSetZIndex(this.get(o))},_bindUIStack:function(){this.after(C,this._afterShimChange),this.after(k,this._afterZIndexChange)},_renderUIStack:function(){this._stackNode.addClass(O.STACKED_CLASS_NAME)},_parseZIndex:function(e){var t;return!e.inDoc()||e.getStyle("position")==="static"?t="auto":t=e.getComputedStyle("zIndex"),t==="auto"?null:t},_setZIndex:function(e){return n.isString(e)&&(e=parseInt(e,10)),n.isNumber(e)||(e=0),e},_afterShimChange:function(e){this._uiSetShim(e.newVal)},_afterZIndexChange:function(e){this._uiSetZIndex(e.newVal)},_uiSetZIndex:function(e){this._stackNode.setStyle(o,e)},_uiSetShim:function(e){e?(this.get(a)?this._renderShim():this._renderShimDeferred(),r.ie==6&&this._addShimResizeHandlers()):this._destroyShim()},_renderShimDeferred:function(){this._stackHandles[E]=this._stackHandles[E]||[];var e=this._stackHandles[E],t=function(e){e.newVal&&this._renderShim()};e.push(this.on(x,t))},_addShimResizeHandlers:function(){this._stackHandles[S]=this._stackHandles[S]||[];var e=this.sizeShim,t=this._stackHandles[S];t.push(this.after(x,e)),t.push(this.after(T,e)),t.push(this.after(N,e)),t.push(this.after(L,e))},_detachStackHandles:function(e){var t=this._stackHandles[e],n;if(t&&t.length>0)while(n=t.pop())n.detach()},_renderShim:function(){var e=this._shimNode,t=this._stackNode;e||(e=this._shimNode=this._getShimTemplate(),t.insertBefore(e,t.get(m)),this._detachStackHandles(E),this.sizeShim())},_destroyShim:function(){this._shimNode&&(this._shimNode.get(v).removeChild(this._shimNode),this._shimNode=null,this._detachStackHandles(E),this._detachStackHandles(S))},sizeShim:function(){var e=this._shimNode,t=this._stackNode;e&&r.ie===6&&this.get(a)&&(e.setStyle(y,t.get(p)+w),e.setStyle(b,t.get(d)+w))},_getShimTemplate:function(){return i.create(O.SHIM_TEMPLATE,this._stackNode.get(g))}},e.WidgetStack=O},"3.17.2",{requires:["base-build","widget"],skinnable:!0});
diff --git a/js/yui3/widget-stdmod/widget-stdmod-min.js b/js/yui3/widget-stdmod/widget-stdmod-min.js
new file mode 100644
index 000000000..14d843135
--- /dev/null
+++ b/js/yui3/widget-stdmod/widget-stdmod-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("widget-stdmod",function(e,t){function H(e){}var n=e.Lang,r=e.Node,i=e.UA,s=e.Widget,o="",u="hd",a="bd",f="ft",l="header",c="body",h="footer",p="fillHeight",d="stdmod",v="Node",m="Content",g="firstChild",y="childNodes",b="ownerDocument",w="contentBox",E="height",S="offsetHeight",x="auto",T="headerContentChange",N="bodyContentChange",C="footerContentChange",k="fillHeightChange",L="heightChange",A="contentUpdate",O="renderUI",M="bindUI",_="syncUI",D="_applyParsedConfig",P=e.Widget.UI_SRC;H.HEADER=l,H.BODY=c,H.FOOTER=h,H.AFTER="after",H.BEFORE="before",H.REPLACE="replace";var B=H.HEADER,j=H.BODY,F=H.FOOTER,I=B+m,q=F+m,R=j+m;H.ATTRS={headerContent:{value:null},footerContent:{value:null},bodyContent:{value:null},fillHeight:{value:H.BODY,validator:function(e){return this._validateFillHeight(e)}}},H.HTML_PARSER={headerContent:function(e){return this._parseStdModHTML(B)},bodyContent:function(e){return this._parseStdModHTML(j)},footerContent:function(e){return this._parseStdModHTML(F)}},H.SECTION_CLASS_NAMES={header:s.getClassName(u),body:s.getClassName(a),footer:s.getClassName(f)},H.TEMPLATES={header:'<div class="'+H.SECTION_CLASS_NAMES[B]+'"></div>',body:'<div class="'+H.SECTION_CLASS_NAMES[j]+'"></div>',footer:'<div class="'+H.SECTION_CLASS_NAMES[F]+'"></div>'},H.prototype={initializer:function(){this._stdModNode=this.get(w),e.before(this._renderUIStdMod,this,O),e.before(this._bindUIStdMod,this,M),e.before(this._syncUIStdMod,this,_)},_syncUIStdMod:function(){var e=this._stdModParsed;(!e||!e[I])&&this._uiSetStdMod(B,this.get(I)),(!e||!e[R])&&this._uiSetStdMod(j,this.get(R)),(!e||!e[q])&&this._uiSetStdMod(F,this.get(q)),this._uiSetFillHeight(this.get(p))},_renderUIStdMod:function(){this._stdModNode.addClass(s.getClassName(d)),this._renderStdModSections(),this.after(T,this._afterHeaderChange),this.after(N,this._afterBodyChange),this.after(C,this._afterFooterChange)},_renderStdModSections:function(){n.isValue(this.get(I))&&this._renderStdMod(B),n.isValue(this.get(R))&&this._renderStdMod(j),n.isValue(this.get(q))&&this._renderStdMod(F)},_bindUIStdMod:function(){this.after(k,this._afterFillHeightChange),this.after(L,this._fillHeight),this.after(A,this._fillHeight)},_afterHeaderChange:function(e){e.src!==P&&this._uiSetStdMod(B,e.newVal,e.stdModPosition)},_afterBodyChange:function(e){e.src!==P&&this._uiSetStdMod(j,e.newVal,e.stdModPosition)},_afterFooterChange:function(e){e.src!==P&&this._uiSetStdMod(F,e.newVal,e.stdModPosition)},_afterFillHeightChange:function(e){this._uiSetFillHeight(e.newVal)},_validateFillHeight:function(e){return!e||e==H.BODY||e==H.HEADER||e==H.FOOTER},_uiSetFillHeight:function(e){var t=this.getStdModNode(e),n=this._currFillNode;n&&t!==n&&n.setStyle(E,o),t&&(this._currFillNode=t),this._fillHeight()},_fillHeight:function(){if(this.get(p)){var e=this.get(E);e!=o&&e!=x&&this.fillHeight(this.getStdModNode(this.get(p)))}},_uiSetStdMod:function(e,t,r){if(n.isValue(t)){var i=this.getStdModNode(e,!0);this._addStdModContent(i,t,r),this.set(e+m,this._getStdModContent(e),{src:P})}else this._eraseStdMod(e);this.fire(A)},_renderStdMod:function(e){var t=this.get(w),n=this._findStdModSection(e);return n||(n=this._getStdModTemplate(e)),this._insertStdModSection(t,e,n),this[e+v]=n,this[e+v]},_eraseStdMod:function(e){var t=this.getStdModNode(e);t&&(t.remove(!0),delete this[e+v])},_insertStdModSection:function(e,t,n){var r=e.get(g);if(t===F||!r)e.appendChild(n);else if(t===B)e.insertBefore(n,r);else{var i=this[F+v];i?e.insertBefore(n,i):e.appendChild(n)}},_getStdModTemplate:function(e){return r.create(H.TEMPLATES[e],this._stdModNode.get(b))},_addStdModContent:function(e,t,n){switch(n){case H.BEFORE:n=0;break;case H.AFTER:n=undefined;break;default:n=H.REPLACE}e.insert(t,n)},_getPreciseHeight:function(e){var t=e?e.get(S):0,n="getBoundingClientRect";if(e&&e.hasMethod(n)){var r=e.invoke(n);r&&(t=r.bottom-r.top)}return t},_findStdModSection:function(e){return this.get(w).one("> ."+H.SECTION_CLASS_NAMES[e])},_parseStdModHTML:function(t){var n=this._findStdModSection(t);return n?(this._stdModParsed||(this._stdModParsed={},e.before(this._applyStdModParsedConfig,this,D)),this._stdModParsed[t+m]=1,n.get("innerHTML")):null},_applyStdModParsedConfig:function(e,t,n){var r=this._stdModParsed;r&&(r[I]=!(I in t)&&I in r,r[R]=!(R in t)&&R in r,r[q]=!(q in t)&&q in r)},_getStdModContent:function(e){return this[e+v]?this[e+v].get(y):null},setStdModContent:function(e,t,n){this.set(e+m,t,{stdModPosition:n})},getStdModNode:function(e,t){var n=this[e+v]||null;return!n&&t&&(n=this._renderStdMod(e)),n},fillHeight:function(e){if(e){var t=this.get(w),r=[this.headerNode,this.bodyNode,this.footerNode],s,o,u=0,a=0,f=!1;for(var l=0,c=r.length;l<c;l++)s=r[l],s&&(s!==e?u+=this._getPreciseHeight(s):f=!0);f&&((i.ie||i.opera)&&e.set(S,0),o=t.get(S)-parseInt(t.getComputedStyle("paddingTop"),10)-parseInt(t.getComputedStyle("paddingBottom"),10)-parseInt(t.getComputedStyle("borderBottomWidth"),10)-parseInt(t.getComputedStyle("borderTopWidth"),10),n.isNumber(o)&&(a=o-u,a>=0&&e.set(S,a)))}}},e.WidgetStdMod=H},"3.17.2",{requires:["base-build","widget"]});
diff --git a/js/yui3/widget-uievents/widget-uievents-min.js b/js/yui3/widget-uievents/widget-uievents-min.js
new file mode 100644
index 000000000..d95cbc172
--- /dev/null
+++ b/js/yui3/widget-uievents/widget-uievents-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("widget-uievents",function(e,t){var n="boundingBox",r=e.Widget,i="render",s=e.Lang,o=":",u=e.Widget._uievts=e.Widget._uievts||{};e.mix(r.prototype,{_destroyUIEvents:function(){var t=e.stamp(this,!0);e.each(u,function(n,r){n.instances[t]&&(delete n.instances[t],e.Object.isEmpty(n.instances)&&(n.handle.detach(),u[r]&&delete u[r]))})},UI_EVENTS:e.Node.DOM_EVENTS,_getUIEventNode:function(){return this.get(n)},_createUIEvent:function(t){var n=this._getUIEventNode(),i=e.stamp(n)+t,s=u[i],o;s||(o=n.delegate(t,function(e){var t=r.getByNode(this);t&&t._filterUIEvent(e)&&t.fire(e.type,{domEvent:e})},"."+e.Widget.getClassName()),u[i]=s={instances:{},handle:o}),s.instances[e.stamp(this)]=1},_filterUIEvent:function(e){return e.currentTarget.compareTo(e.container)||e.container.compareTo(this._getUIEventNode())},_getUIEvent:function(e){if(s.isString(e)){var t=this.parseType(e)[1],n,r;return t&&(n=t.indexOf(o),n>-1&&(t=t.substring(n+o.length)),this.UI_EVENTS[t]&&(r=t)),r}},_initUIEvent:function(e){var t=this._getUIEvent(e),n=this._uiEvtsInitQueue||{};t&&!n[t]&&(this._uiEvtsInitQueue=n[t]=1,this.after(i,function(){this._createUIEvent(t),delete this._uiEvtsInitQueue[t]}))},on:function(e){return this._initUIEvent(e),r.superclass.on.apply(this,arguments)},publish:function(e,t){var n=this._getUIEvent(e);return n&&t&&t.defaultFn&&this._initUIEvent(n),r.superclass.publish.apply(this,arguments)}},!0)},"3.17.2",{requires:["node-event-delegate","widget-base"]});
diff --git a/js/yui3/yql-jsonp/yql-jsonp-min.js b/js/yui3/yql-jsonp/yql-jsonp-min.js
new file mode 100644
index 000000000..cd28e0d03
--- /dev/null
+++ b/js/yui3/yql-jsonp/yql-jsonp-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("yql-jsonp",function(e,t){e.YQLRequest.prototype._send=function(t,n){n.allowCache!==!1&&(n.allowCache=!0),this._jsonp?(this._jsonp.url=t,n.on&&n.on.success&&(this._jsonp._config.on.success=n.on.success),this._jsonp.send()):this._jsonp=e.jsonp(t,n)}},"3.17.2",{requires:["yql","jsonp","jsonp-url"]});
diff --git a/js/yui3/yql-nodejs/yql-nodejs-min.js b/js/yui3/yql-nodejs/yql-nodejs-min.js
new file mode 100644
index 000000000..fe4dec333
--- /dev/null
+++ b/js/yui3/yql-nodejs/yql-nodejs-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("yql-nodejs",function(e,t){var n=require("request");e.YQLRequest.prototype._send=function(e,t){n(e,{method:"GET",timeout:t.timeout||3e4},function(e,n){e?t.on.success({error:e}):t.on.success(JSON.parse(n.body))})}},"3.17.2",{requires:["yql"]});
diff --git a/js/yui3/yql-winjs/yql-winjs-min.js b/js/yui3/yql-winjs/yql-winjs-min.js
new file mode 100644
index 000000000..6f41d7a4c
--- /dev/null
+++ b/js/yui3/yql-winjs/yql-winjs-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("yql-winjs",function(e,t){e.YQLRequest.prototype._send=function(e,t){var n=new XMLHttpRequest,r;n.open("GET",e,!0),n.onreadystatechange=function(){n.readyState===4&&(clearTimeout(r),t.on.success(JSON.parse(n.responseText)))},n.send(),r=setTimeout(function(){n.abort(),t.on.timeout("script timeout")},t.timeout||3e4)}},"3.17.2",{requires:["yql"]});
diff --git a/js/yui3/yql/yql-min.js b/js/yui3/yql/yql-min.js
new file mode 100644
index 000000000..3647aecab
--- /dev/null
+++ b/js/yui3/yql/yql-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("yql",function(e,t){var n=function(t,n,r,i){r||(r={}),r.q=t,r.format||(r.format=e.YQLRequest.FORMAT),r.env||(r.env=e.YQLRequest.ENV),this._context=this,i&&i.context&&(this._context=i.context,delete i.context),r&&r.context&&(this._context=r.context,delete r.context),this._params=r,this._opts=i,this._callback=n};n.prototype={_jsonp:null,_opts:null,_callback:null,_params:null,_context:null,_internal:function(){this._callback.apply(this._context,arguments)},send:function(){var t=[],n=this._opts&&this._opts.proto?this._opts.proto:e.YQLRequest.PROTO,r;return e.Object.each(this._params,function(e,n){t.push(n+"="+encodeURIComponent(e))}),t=t.join("&"),n+=(this._opts&&this._opts.base?this._opts.base:e.YQLRequest.BASE_URL)+t,r=e.Lang.isFunction(this._callback)?{on:{success:this._callback}}:this._callback,r.on=r.on||{},this._callback=r.on.success,r.on.success=e.bind(this._internal,this),this._send(n,r),this},_send:function(){}},n.FORMAT="json",n.PROTO="http",n.BASE_URL="://query.yahooapis.com/v1/public/yql?",n.ENV="http://datatables.org/alltables.env",e.YQLRequest=n,e.YQL=function(t,n,r,i){return(new e.YQLRequest(t,n,r,i)).send()}},"3.17.2",{requires:["oop"]});
diff --git a/js/yui3/yui-base/yui-base-min.js b/js/yui3/yui-base/yui-base-min.js
new file mode 100644
index 000000000..8dc14ea86
--- /dev/null
+++ b/js/yui3/yui-base/yui-base-min.js
@@ -0,0 +1,14 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+typeof YUI!="undefined"&&(YUI._YUI=YUI);var YUI=function(){var e=0,t=this,n=arguments,r=n.length,i=function(e,t){return e&&e.hasOwnProperty&&e instanceof t},s=typeof YUI_config!="undefined"&&YUI_config;i(t,YUI)?(t._init(),YUI.GlobalConfig&&t.applyConfig(YUI.GlobalConfig),s&&t.applyConfig(s),r||t._setup()):t=new YUI;if(r){for(;e<r;e++)t.applyConfig(n[e]);t._setup()}return t.instanceOf=i,t};(function(){var e,t,n="3.17.2",r=".",i="http://yui.yahooapis.com/",s="yui3-js-enabled",o="yui3-css-stamp",u=function(){},a=Array.prototype.slice,f={"io.xdrReady":1,"io.xdrResponse":1,"SWF.eventHandler":1},l=typeof window!="undefined",c=l?window:null,h=l?c.document:null,p=h&&h.documentElement,d=p&&p.className,v={},m=(new Date).getTime(),g=function(e,t,n,r){e&&e.addEventListener?e.addEventListener(t,n,r):e&&e.attachEvent&&e.attachEvent("on"+t,n)},y=function(e,t,n,r){if(e&&e.removeEventListener)try{e.removeEventListener(t,n,r)}catch(i){}else e&&e.detachEvent&&e.detachEvent("on"+t,n)},b=function(){YUI.Env.DOMReady=!0,l&&y(h,"DOMContentLoaded",b)},w=function(){YUI.Env.windowLoaded=!0,YUI.Env.DOMReady=!0,l&&y(window,"load",w)},E=function(e,t){var n=e.Env._loader,r=["loader-base"],i=YUI.Env,s=i.mods;return n?(n.ignoreRegistered=!1,n.onEnd=null,n.data=null,n.required=[],n.loadType=null):(n=new e.Loader(e.config),e.Env._loader=n),s&&s.loader&&(r=[].concat(r,YUI.Env.loaderExtras)),YUI.Env.core=e.Array.dedupe([].concat(YUI.Env.core,r)),n},S=function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},x={success:!0};p&&d.indexOf(s)==-1&&(d&&(d+=" "),d+=s,p.className=d),n.indexOf("@")>-1&&(n="3.5.0"),e={applyConfig:function(e){e=e||u;var t,n,r=this.config,i=r.modules,s=r.groups,o=r.aliases,a=this.Env._loader;for(n in e)e.hasOwnProperty(n)&&(t=e[n],i&&n=="modules"?S(i,t):o&&n=="aliases"?S(o,t):s&&n=="groups"?S(s,t):n=="win"?(r[n]=t&&t.contentWindow||t,r.doc=r[n]?r[n].document:null):n!="_yuid"&&(r[n]=t));a&&a._config(e)},_config:function(e){this.applyConfig(e)},_init:function(){var e,t,r=this,s=YUI.Env,u=r.Env,a;r.version=n;if(!u){r.Env={core:["get","features","intl-base","yui-log","yui-later"],loaderExtras:["loader-rollup","loader-yui3"],mods:{},versions:{},base:i,cdn:i+n+"/build/",_idx:0,_used:{},_attached:{},_exported:{},_missed:[],_yidx:0,_uidx:0,_guidp:"y",_loaded:{},_BASE_RE:/(?:\?(?:[^&]*&)*([^&]*))?\b(yui(?:-\w+)?)\/\2(?:-(min|debug))?\.js/,parseBasePath:function(e,t){var n=e.match(t),r,i;return n&&(r=RegExp.leftContext||e.slice(0,e.indexOf(n[0])),i=n[3],n[1]&&(r+="?"+n[1]),r={filter:i,path:r}),r},getBase:s&&s.getBase||function(t){var n=h&&h.getElementsByTagName("script")||[],i=u.cdn,s,o,a,f;for(o=0,a=n.length;o<a;++o){f=n[o].src;if(f){s=r.Env.parseBasePath(f,t);if(s){e=s.filter,i=s.path;break}}}return i}},u=r.Env,u._loaded[n]={};if(s&&r!==YUI)u._yidx=++s._yidx,u._guidp=("yui_"+n+"_"+u._yidx+"_"+m).replace(/[^a-z0-9_]+/g,"_");else if(YUI._YUI){s=YUI._YUI.Env,u._yidx+=s._yidx,u._uidx+=s._uidx;for(a in s)a in u||(u[a]=s[a]);delete YUI._YUI}r.id=r.stamp(r),v[r.id]=r}r.constructor=YUI,r.config=r.config||{bootstrap:!0,cacheUse:!0,debug:!0,doc:h,fetchCSS:!0,throwFail:!0,useBrowserConsole:!0,useNativeES5:!0,win:c,global:Function("return this")()},h&&!h.getElementById(o)?(t=h.createElement("div"),t.innerHTML='<div id="'+o+'" style="position: absolute !important; visibility: hidden !important"></div>',YUI.Env.cssStampEl=t.firstChild,h.body?h.body.appendChild(YUI.Env.cssStampEl):p.insertBefore(YUI.Env.cssStampEl,p.firstChild)):h&&h.getElementById(o)&&!YUI.Env.cssStampEl&&(YUI.Env.cssStampEl=h.getElementById(o)),r.config.lang=r.config.lang||"en-US",r.config.base=YUI.config.base||r.Env.getBase(r.Env._BASE_RE);if(!e||!"mindebug".indexOf(e))e="min";e=e?"-"+e:e,r.config.loaderPath=YUI.config.loaderPath||"loader/loader"+e+".js"},_setup:function(){var e,t=this,n=[],r=YUI.Env.mods,i=t.config.core||[].concat(YUI.Env.core);for(e=0;e<i.length;e++)r[i[e]]&&n.push(i[e]);t._attach(["yui-base"]),t._attach(n),t.Loader&&E(t)},applyTo:function(e,t,n){if(t in f){var r=v[e],i,s,o;if(r){i=t.split("."),s=r;for(o=0;o<i.length;o+=1)s=s[i[o]],s||this.log("applyTo not found: "+t,"warn","yui");return s&&s.apply(r,n)}return null}return this.log(t+": applyTo not allowed","warn","yui"),null},add:function(e,t,n,r){r=r||{};var i=YUI.Env,s={name:e,fn:t,version:n,details:r},o={},u,a,f,l,c=i.versions;i.mods[e]=s,c[n]=c[n]||{},c[n][e]=s;for(l in v)v.hasOwnProperty(l)&&(a=v[l],o[a.id]||(o[a.id]=!0,u=a.Env._loader,u&&(f=u.getModuleInfo(e),(!f||f.temp)&&u.addModule(r,e))));return this},_attach:function(e,t){var n,r,i,s,o,u,a,f=YUI.Env.mods,l=YUI.Env.aliases,c=this,h,p=YUI.Env._renderedMods,d=c.Env._loader,v=c.Env._attached,m=c.Env._exported,g=e.length,d,y,b,w=[],E,S,x,T,N,C,k;for(n=0;n<g;n++){r=e[n],i=f[r],w.push(r);if(d&&d.conditions[r])for(h in d.conditions[r])d.conditions[r].hasOwnProperty(h)&&(y=d.conditions[r][h],b=y&&(y.ua&&c.UA[y.ua]||y.test&&y.test(c)),b&&w.push(y.name))}e=w,g=e.length;for(n=0;n<g;n++)if(!v[e[n]]){r=e[n],i=f[r];if(l&&l[r]&&!i){c._attach(l[r]);continue}if(!i)T=d&&d.getModuleInfo(r),T&&(i=T,t=!0),!t&&r&&r.indexOf("skin-")===-1&&r.indexOf("css")===-1&&(c.Env._missed.push(r),c.Env._missed=c.Array.dedupe(c.Env._missed),c.message("NOT loaded: "+r,"warn","yui"));else{v[r]=!0;for(h=0;h<c.Env._missed.length;h++)c.Env._missed[h]===r&&(c.message("Found: "+r+" (was reported as missing earlier)","warn","yui"),c.Env._missed.splice(h,1));if(d&&!d._canBeAttached(r))return!0;if(d&&p&&p[r]&&p[r].temp){d.getRequires(p[r]),o=[],T=d.getModuleInfo(r);for(h in T.expanded_map)T.expanded_map.hasOwnProperty(h)&&o.push(h);c._attach(o)}s=i.details,o=s.requires,S=s.es,u=s.use,a=s.after,s.lang&&(o=o||[],o.unshift("intl"));if(o){x=o.length;for(h=0;h<x;h++)if(!v[o[h]]){if(!c._attach(o))return!1;break}}if(a)for(h=0;h<a.length;h++)if(!v[a[h]]){if(!c._attach(a,!0))return!1;break}if(i.fn){E=[c,r];if(S){k={},C={},E.push(k,C);if(o){x=o.length;for(h=0;h<x;h++)k[o[h]]=m.hasOwnProperty(o[h])?m[o[h]]:c}}if(c.config.throwFail)C=i.fn.apply(S?undefined:i,E);else try{C=i.fn.apply
+(S?undefined:i,E)}catch(L){return c.error("Attach error: "+r,L,r),!1}S&&(m[r]=C,N=i.details.condition,N&&N.when==="instead"&&(m[N.trigger]=C))}if(u)for(h=0;h<u.length;h++)if(!v[u[h]]){if(!c._attach(u))return!1;break}}}return!0},_delayCallback:function(e,t){var n=this,r=["event-base"];return t=n.Lang.isObject(t)?t:{event:t},t.event==="load"&&r.push("event-synthetic"),function(){var i=arguments;n._use(r,function(){n.on(t.event,function(){i[1].delayUntil=t.event,e.apply(n,i)},t.args)})}},use:function(){var e=a.call(arguments,0),t=e[e.length-1],n=this,r=0,i,s=n.Env,o=!0;n.Lang.isFunction(t)?(e.pop(),n.config.delayUntil&&(t=n._delayCallback(t,n.config.delayUntil))):t=null,n.Lang.isArray(e[0])&&(e=e[0]);if(n.config.cacheUse){while(i=e[r++])if(!s._attached[i]){o=!1;break}if(o)return e.length,n._notify(t,x,e),n}return n._loading?(n._useQueue=n._useQueue||new n.Queue,n._useQueue.add([e,t])):n._use(e,function(n,r){n._notify(t,r,e)}),n},require:function(){var e=a.call(arguments),t;typeof e[e.length-1]=="function"&&(t=e.pop(),e.push(function(n){var r,i=e.length,s=n.Env._exported,o={};for(r=0;r<i;r++)s.hasOwnProperty(e[r])&&(o[e[r]]=s[e[r]]);t.call(undefined,n,o)})),this.use.apply(this,e)},_notify:function(e,t,n){if(!t.success&&this.config.loadErrorFn)this.config.loadErrorFn.call(this,this,e,t,n);else if(e){this.Env._missed&&this.Env._missed.length&&(t.msg="Missing modules: "+this.Env._missed.join(),t.success=!1);if(this.config.throwFail)e(this,t);else try{e(this,t)}catch(r){this.error("use callback error",r,n)}}},_use:function(e,t){this.Array||this._attach(["yui-base"]);var r,i,s,o=this,u=YUI.Env,a=u.mods,f=o.Env,l=f._used,c=u.aliases,h=u._loaderQueue,p=e[0],d=o.Array,v=o.config,m=v.bootstrap,g=[],y,b=[],w=!0,S=v.fetchCSS,x=function(e,t){var r=0,i=[],s,o,f,h,p;if(!e.length)return;if(c){o=e.length;for(r=0;r<o;r++)c[e[r]]&&!a[e[r]]?i=[].concat(i,c[e[r]]):i.push(e[r]);e=i}o=e.length;for(r=0;r<o;r++){s=e[r],t||b.push(s);if(l[s])continue;f=a[s],h=null,p=null,f?(l[s]=!0,h=f.details.requires,p=f.details.use):u._loaded[n][s]?l[s]=!0:g.push(s),h&&h.length&&x(h),p&&p.length&&x(p,1)}},T=function(n){var r=n||{success:!0,msg:"not dynamic"},i,s,u=!0,a=r.data;o._loading=!1,a&&(s=g,g=[],b=[],x(a),i=g.length,i&&[].concat(g).sort().join()==s.sort().join()&&(i=!1)),i&&a?(o._loading=!0,o._use(g,function(){o._attach(a)&&o._notify(t,r,a)})):(a&&(u=o._attach(a)),u&&o._notify(t,r,e)),o._useQueue&&o._useQueue.size()&&!o._loading&&o._use.apply(o,o._useQueue.next())};if(p==="*"){e=[];for(y in a)a.hasOwnProperty(y)&&e.push(y);return w=o._attach(e),w&&T(),o}return(a.loader||a["loader-base"])&&!o.Loader&&o._attach(["loader"+(a.loader?"":"-base")]),m&&o.Loader&&e.length&&(i=E(o),i.require(e),i.ignoreRegistered=!0,i._boot=!0,i.calculate(null,S?null:"js"),e=i.sorted,i._boot=!1),x(e),r=g.length,r&&(g=d.dedupe(g),r=g.length),m&&r&&o.Loader?(o._loading=!0,i=E(o),i.onEnd=T,i.context=o,i.data=e,i.ignoreRegistered=!1,i.require(g),i.insert(null,S?null:"js")):m&&r&&o.Get&&!f.bootstrapped?(o._loading=!0,s=function(){o._loading=!1,h.running=!1,f.bootstrapped=!0,u._bootstrapping=!1,o._attach(["loader"])&&o._use(e,t)},u._bootstrapping?h.add(s):(u._bootstrapping=!0,o.Get.script(v.base+v.loaderPath,{onEnd:s}))):(w=o._attach(e),w&&T()),o},namespace:function(){var e=arguments,t,n=0,i,s,o;for(;n<e.length;n++){t=this,o=e[n];if(o.indexOf(r)>-1){s=o.split(r);for(i=s[0]=="YAHOO"?1:0;i<s.length;i++)t[s[i]]=t[s[i]]||{},t=t[s[i]]}else t[o]=t[o]||{},t=t[o]}return t},log:u,message:u,dump:function(e){return""+e},error:function(e,t,n){var r=this,i;r.config.errorFn&&(i=r.config.errorFn.apply(r,arguments));if(!i)throw t||new Error(e);return r.message(e,"error",""+n),r},guid:function(e){var t=this.Env._guidp+"_"+ ++this.Env._uidx;return e?e+t:t},stamp:function(e,t){var n;if(!e)return e;e.uniqueID&&e.nodeType&&e.nodeType!==9?n=e.uniqueID:n=typeof e=="string"?e:e._yuid;if(!n){n=this.guid();if(!t)try{e._yuid=n}catch(r){n=null}}return n},destroy:function(){var e=this;e.Event&&e.Event._unload(),delete v[e.id],delete e.Env,delete e.config}},YUI.prototype=e;for(t in e)e.hasOwnProperty(t)&&(YUI[t]=e[t]);YUI.applyConfig=function(e){if(!e)return;YUI.GlobalConfig&&this.prototype.applyConfig.call(this,YUI.GlobalConfig),this.prototype.applyConfig.call(this,e),YUI.GlobalConfig=this.config},YUI._init(),l?(g(h,"DOMContentLoaded",b),g(window,"load",w)):(b(),w()),YUI.Env.add=g,YUI.Env.remove=y,typeof exports=="object"&&(exports.YUI=YUI,YUI.setLoadHook=function(e){YUI._getLoadHook=e},YUI._getLoadHook=null),YUI.Env[n]={}})(),YUI.add("yui-base",function(e,t){function m(e,t,n){var r,i;t||(t=0);if(n||m.test(e))try{return d.slice.call(e,t)}catch(s){i=[];for(r=e.length;t<r;++t)i.push(e[t]);return i}return[e]}function g(){this._init(),this.add.apply(this,arguments)}var n=e.Lang||(e.Lang={}),r=String.prototype,i=Object.prototype.toString,s={"undefined":"undefined",number:"number","boolean":"boolean",string:"string","[object Function]":"function","[object RegExp]":"regexp","[object Array]":"array","[object Date]":"date","[object Error]":"error"},o=/\{\s*([^|}]+?)\s*(?:\|([^}]*))?\s*\}/g,u=" \n \f\r \u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff",a="[ -\r \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+",f=new RegExp("^"+a),l=new RegExp(a+"$"),c=new RegExp(f.source+"|"+l.source,"g"),h=/\{\s*\[(?:native code|function)\]\s*\}/i;n._isNative=function(t){return!!(e.config.useNativeES5&&t&&h.test(t))},n.isArray=n._isNative(Array.isArray)?Array.isArray:function(e){return n.type(e)==="array"},n.isBoolean=function(e){return typeof e=="boolean"},n.isDate=function(e){return n.type(e)==="date"&&e.toString()!=="Invalid Date"&&!isNaN(e)},n.isFunction=function(e){return n.type(e)==="function"},n.isNull=function(e){return e===null},n.isNumber=function(e){return typeof e=="number"&&isFinite(e)},n.isObject=function(e,t){var r=typeof e;return e&&(r==="object"||!t&&(r==="function"||n.isFunction(e)))||!1},n.isRegExp=function(
+e){return n.type(e)==="regexp"},n.isString=function(e){return typeof e=="string"},n.isUndefined=function(e){return typeof e=="undefined"},n.isValue=function(e){var t=n.type(e);switch(t){case"number":return isFinite(e);case"null":case"undefined":return!1;default:return!!t}},n.now=Date.now||function(){return(new Date).getTime()},n.sub=function(e,t){return e.replace?e.replace(o,function(e,r){return n.isUndefined(t[r])?e:t[r]}):e},n.trim=n._isNative(r.trim)&&!u.trim()?function(e){return e&&e.trim?e.trim():e}:function(e){try{return e.replace(c,"")}catch(t){return e}},n.trimLeft=n._isNative(r.trimLeft)&&!u.trimLeft()?function(e){return e.trimLeft()}:function(e){return e.replace(f,"")},n.trimRight=n._isNative(r.trimRight)&&!u.trimRight()?function(e){return e.trimRight()}:function(e){return e.replace(l,"")},n.type=function(e){return s[typeof e]||s[i.call(e)]||(e?"object":"null")};var p=e.Lang,d=Array.prototype,v=Object.prototype.hasOwnProperty;e.Array=m,m.dedupe=p._isNative(Object.create)?function(e){var t=Object.create(null),n=[],r,i,s;for(r=0,s=e.length;r<s;++r)i=e[r],t[i]||(t[i]=1,n.push(i));return n}:function(e){var t={},n=[],r,i,s;for(r=0,s=e.length;r<s;++r)i=e[r],v.call(t,i)||(t[i]=1,n.push(i));return n},m.each=m.forEach=p._isNative(d.forEach)?function(t,n,r){return d.forEach.call(t||[],n,r||e),e}:function(t,n,r){for(var i=0,s=t&&t.length||0;i<s;++i)i in t&&n.call(r||e,t[i],i,t);return e},m.hash=function(e,t){var n={},r=t&&t.length||0,i,s;for(i=0,s=e.length;i<s;++i)i in e&&(n[e[i]]=r>i&&i in t?t[i]:!0);return n},m.indexOf=p._isNative(d.indexOf)?function(e,t,n){return d.indexOf.call(e,t,n)}:function(e,t,n){var r=e.length;n=+n||0,n=(n>0||-1)*Math.floor(Math.abs(n)),n<0&&(n+=r,n<0&&(n=0));for(;n<r;++n)if(n in e&&e[n]===t)return n;return-1},m.numericSort=function(e,t){return e-t},m.some=p._isNative(d.some)?function(e,t,n){return d.some.call(e,t,n)}:function(e,t,n){for(var r=0,i=e.length;r<i;++r)if(r in e&&t.call(n,e[r],r,e))return!0;return!1},m.test=function(e){var t=0;if(p.isArray(e))t=1;else if(p.isObject(e))try{"length"in e&&!e.tagName&&(!e.scrollTo||!e.document)&&!e.apply&&(t=2)}catch(n){}return t},g.prototype={_init:function(){this._q=[]},next:function(){return this._q.shift()},last:function(){return this._q.pop()},add:function(){return this._q.push.apply(this._q,arguments),this},size:function(){return this._q.length}},e.Queue=g,YUI.Env._loaderQueue=YUI.Env._loaderQueue||new g;var y="__",v=Object.prototype.hasOwnProperty,b=e.Lang.isObject;e.cached=function(e,t,n){return t||(t={}),function(r){var i=arguments.length>1?Array.prototype.join.call(arguments,y):String(r);if(!(i in t)||n&&t[i]==n)t[i]=e.apply(e,arguments);return t[i]}},e.getLocation=function(){var t=e.config.win;return t&&t.location},e.merge=function(){var e=0,t=arguments.length,n={},r,i;for(;e<t;++e){i=arguments[e];for(r in i)v.call(i,r)&&(n[r]=i[r])}return n},e.mix=function(t,n,r,i,s,o){var u,a,f,l,c,h,p;if(!t||!n)return t||e;if(s){s===2&&e.mix(t.prototype,n.prototype,r,i,0,o),f=s===1||s===3?n.prototype:n,p=s===1||s===4?t.prototype:t;if(!f||!p)return t}else f=n,p=t;u=r&&!o;if(i)for(l=0,h=i.length;l<h;++l){c=i[l];if(!v.call(f,c))continue;a=u?!1:c in p;if(o&&a&&b(p[c],!0)&&b(f[c],!0))e.mix(p[c],f[c],r,null,0,o);else if(r||!a)p[c]=f[c]}else{for(c in f){if(!v.call(f,c))continue;a=u?!1:c in p;if(o&&a&&b(p[c],!0)&&b(f[c],!0))e.mix(p[c],f[c],r,null,0,o);else if(r||!a)p[c]=f[c]}e.Object._hasEnumBug&&e.mix(p,f,r,e.Object._forceEnum,s,o)}return t};var p=e.Lang,v=Object.prototype.hasOwnProperty,w,E=e.Object=p._isNative(Object.create)?function(e){return Object.create(e)}:function(){function e(){}return function(t){return e.prototype=t,new e}}(),S=E._forceEnum=["hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toString","toLocaleString","valueOf"],x=E._hasEnumBug=!{valueOf:0}.propertyIsEnumerable("valueOf"),T=E._hasProtoEnumBug=function(){}.propertyIsEnumerable("prototype"),N=E.owns=function(e,t){return!!e&&v.call(e,t)};E.hasKey=N,E.keys=p._isNative(Object.keys)&&!T?Object.keys:function(e){if(!p.isObject(e))throw new TypeError("Object.keys called on a non-object");var t=[],n,r,i;if(T&&typeof e=="function")for(r in e)N(e,r)&&r!=="prototype"&&t.push(r);else for(r in e)N(e,r)&&t.push(r);if(x)for(n=0,i=S.length;n<i;++n)r=S[n],N(e,r)&&t.push(r);return t},E.values=function(e){var t=E.keys(e),n=0,r=t.length,i=[];for(;n<r;++n)i.push(e[t[n]]);return i},E.size=function(e){try{return E.keys(e).length}catch(t){return 0}},E.hasValue=function(t,n){return e.Array.indexOf(E.values(t),n)>-1},E.each=function(t,n,r,i){var s;for(s in t)(i||N(t,s))&&n.call(r||e,t[s],s,t);return e},E.some=function(t,n,r,i){var s;for(s in t)if(i||N(t,s))if(n.call(r||e,t[s],s,t))return!0;return!1},E.getValue=function(t,n){if(!p.isObject(t))return w;var r,i=e.Array(n),s=i.length;for(r=0;t!==w&&r<s;r++)t=t[i[r]];return t},E.setValue=function(t,n,r){var i,s=e.Array(n),o=s.length-1,u=t;if(o>=0){for(i=0;u!==w&&i<o;i++)u=u[s[i]];if(u===w)return w;u[s[i]]=r}return t},E.isEmpty=function(e){return!E.keys(Object(e)).length},YUI.Env.parseUA=function(t){var n=function(e){var t=0;return parseFloat(e.replace(/\./g,function(){return t++===1?"":"."}))},r=e.config.win,i=r&&r.navigator,s={ie:0,opera:0,gecko:0,webkit:0,safari:0,chrome:0,mobile:null,air:0,phantomjs:0,ipad:0,iphone:0,ipod:0,ios:null,android:0,silk:0,ubuntu:0,accel:!1,webos:0,caja:i&&i.cajaVersion,secure:!1,os:null,nodejs:0,winjs:typeof Windows!="undefined"&&!!Windows.System,touchEnabled:!1},o=t||i&&i.userAgent,u=r&&r.location,a=u&&u.href,f;return s.userAgent=o,s.secure=a&&a.toLowerCase().indexOf("https")===0,o&&(/windows|win32/i.test(o)?s.os="windows":/macintosh|mac_powerpc/i.test(o)?s.os="macintosh":/android/i.test(o)?s.os="android":/symbos/i.test(o)?s.os="symbos":/linux/i.test(o)?s.os="linux":/rhino/i.test(o)&&(s.os="rhino"),/KHTML/.test(o)&&(s.webkit=1),/IEMobile|XBLWP7/.test(o)&&(s.mobile="windows"),/Fennec/.test(o)&&(s.mobile="gecko"),f=o.match(/AppleWebKit\/([^\s]*)/),f&&f[1]&&(s.webkit=n(f[1]),s.safari=s.webkit,/PhantomJS/
+.test(o)&&(f=o.match(/PhantomJS\/([^\s]*)/),f&&f[1]&&(s.phantomjs=n(f[1]))),/ Mobile\//.test(o)||/iPad|iPod|iPhone/.test(o)?(s.mobile="Apple",f=o.match(/OS ([^\s]*)/),f&&f[1]&&(f=n(f[1].replace("_","."))),s.ios=f,s.os="ios",s.ipad=s.ipod=s.iphone=0,f=o.match(/iPad|iPod|iPhone/),f&&f[0]&&(s[f[0].toLowerCase()]=s.ios)):(f=o.match(/NokiaN[^\/]*|webOS\/\d\.\d/),f&&(s.mobile=f[0]),/webOS/.test(o)&&(s.mobile="WebOS",f=o.match(/webOS\/([^\s]*);/),f&&f[1]&&(s.webos=n(f[1]))),/ Android/.test(o)&&(/Mobile/.test(o)&&(s.mobile="Android"),f=o.match(/Android ([^\s]*);/),f&&f[1]&&(s.android=n(f[1]))),/Silk/.test(o)&&(f=o.match(/Silk\/([^\s]*)/),f&&f[1]&&(s.silk=n(f[1])),s.android||(s.android=2.34,s.os="Android"),/Accelerated=true/.test(o)&&(s.accel=!0))),f=o.match(/OPR\/(\d+\.\d+)/),f&&f[1]?s.opera=n(f[1]):(f=o.match(/(Chrome|CrMo|CriOS)\/([^\s]*)/),f&&f[1]&&f[2]?(s.chrome=n(f[2]),s.safari=0,f[1]==="CrMo"&&(s.mobile="chrome")):(f=o.match(/AdobeAIR\/([^\s]*)/),f&&(s.air=f[0])))),f=o.match(/Ubuntu\ (\d+\.\d+)/),f&&f[1]&&(s.os="linux",s.ubuntu=n(f[1]),f=o.match(/\ WebKit\/([^\s]*)/),f&&f[1]&&(s.webkit=n(f[1])),f=o.match(/\ Chromium\/([^\s]*)/),f&&f[1]&&(s.chrome=n(f[1])),/ Mobile$/.test(o)&&(s.mobile="Ubuntu")),s.webkit||(/Opera/.test(o)?(f=o.match(/Opera[\s\/]([^\s]*)/),f&&f[1]&&(s.opera=n(f[1])),f=o.match(/Version\/([^\s]*)/),f&&f[1]&&(s.opera=n(f[1])),/Opera Mobi/.test(o)&&(s.mobile="opera",f=o.replace("Opera Mobi","").match(/Opera ([^\s]*)/),f&&f[1]&&(s.opera=n(f[1]))),f=o.match(/Opera Mini[^;]*/),f&&(s.mobile=f[0])):(f=o.match(/MSIE ([^;]*)|Trident.*; rv:([0-9.]+)/),f&&(f[1]||f[2])?s.ie=n(f[1]||f[2]):(f=o.match(/Gecko\/([^\s]*)/),f&&(s.gecko=1,f=o.match(/rv:([^\s\)]*)/),f&&f[1]&&(s.gecko=n(f[1]),/Mobile|Tablet/.test(o)&&(s.mobile="ffos"))))))),r&&i&&!(s.chrome&&s.chrome<6)&&(s.touchEnabled="ontouchstart"in r||"msMaxTouchPoints"in i&&i.msMaxTouchPoints>0),t||(typeof process=="object"&&process.versions&&process.versions.node&&(s.os=process.platform,s.nodejs=n(process.versions.node)),YUI.Env.UA=s),s},e.UA=YUI.Env.UA||YUI.Env.parseUA(),e.UA.compareVersions=function(e,t){var n,r,i,s,o,u;if(e===t)return 0;r=(e+"").split("."),s=(t+"").split(".");for(o=0,u=Math.max(r.length,s.length);o<u;++o){n=parseInt(r[o],10),i=parseInt(s[o],10),isNaN(n)&&(n=0),isNaN(i)&&(i=0);if(n<i)return-1;if(n>i)return 1}return 0},YUI.Env.aliases={anim:["anim-base","anim-color","anim-curve","anim-easing","anim-node-plugin","anim-scroll","anim-xy"],"anim-shape-transform":["anim-shape"],app:["app-base","app-content","app-transitions","lazy-model-list","model","model-list","model-sync-rest","model-sync-local","router","view","view-node-map"],attribute:["attribute-base","attribute-complex"],"attribute-events":["attribute-observable"],autocomplete:["autocomplete-base","autocomplete-sources","autocomplete-list","autocomplete-plugin"],axes:["axis-numeric","axis-category","axis-time","axis-stacked"],"axes-base":["axis-numeric-base","axis-category-base","axis-time-base","axis-stacked-base"],base:["base-base","base-pluginhost","base-build"],cache:["cache-base","cache-offline","cache-plugin"],charts:["charts-base"],collection:["array-extras","arraylist","arraylist-add","arraylist-filter","array-invoke"],color:["color-base","color-hsl","color-harmony"],controller:["router"],dataschema:["dataschema-base","dataschema-json","dataschema-xml","dataschema-array","dataschema-text"],datasource:["datasource-local","datasource-io","datasource-get","datasource-function","datasource-cache","datasource-jsonschema","datasource-xmlschema","datasource-arrayschema","datasource-textschema","datasource-polling"],datatable:["datatable-core","datatable-table","datatable-head","datatable-body","datatable-base","datatable-column-widths","datatable-message","datatable-mutable","datatable-sort","datatable-datasource"],datatype:["datatype-date","datatype-number","datatype-xml"],"datatype-date":["datatype-date-parse","datatype-date-format","datatype-date-math"],"datatype-number":["datatype-number-parse","datatype-number-format"],"datatype-xml":["datatype-xml-parse","datatype-xml-format"],dd:["dd-ddm-base","dd-ddm","dd-ddm-drop","dd-drag","dd-proxy","dd-constrain","dd-drop","dd-scroll","dd-delegate"],dom:["dom-base","dom-screen","dom-style","selector-native","selector"],editor:["frame","editor-selection","exec-command","editor-base","editor-para","editor-br","editor-bidi","editor-tab","createlink-base"],event:["event-base","event-delegate","event-synthetic","event-mousewheel","event-mouseenter","event-key","event-focus","event-resize","event-hover","event-outside","event-touch","event-move","event-flick","event-valuechange","event-tap"],"event-custom":["event-custom-base","event-custom-complex"],"event-gestures":["event-flick","event-move"],handlebars:["handlebars-compiler"],highlight:["highlight-base","highlight-accentfold"],history:["history-base","history-hash","history-html5"],io:["io-base","io-xdr","io-form","io-upload-iframe","io-queue"],json:["json-parse","json-stringify"],loader:["loader-base","loader-rollup","loader-yui3"],node:["node-base","node-event-delegate","node-pluginhost","node-screen","node-style"],pluginhost:["pluginhost-base","pluginhost-config"],querystring:["querystring-parse","querystring-stringify"],recordset:["recordset-base","recordset-sort","recordset-filter","recordset-indexer"],resize:["resize-base","resize-proxy","resize-constrain"],slider:["slider-base","slider-value-range","clickable-rail","range-slider"],template:["template-base","template-micro"],text:["text-accentfold","text-wordbreak"],widget:["widget-base","widget-htmlparser","widget-skin","widget-uievents"]}},"3.17.2",{use:["get","features","intl-base","yui-log","yui-later"]}),YUI.add("get",function(e,t){var n=e.Lang,r,i,s;e.Get=i={cssOptions:{attributes:{rel:"stylesheet"},doc:e.config.linkDoc||e.config.doc,pollInterval:50},jsOptions:{autopurge:!0,doc:e.config.scriptDoc||e.config.doc},options:{attributes:{charset:"utf-8"},purgethreshold:20},REGEX_CSS:/\.css(?:[?;].*)?$/i,REGEX_JS:/\.js(?:[?;].*)?$/i
+,_insertCache:{},_pending:null,_purgeNodes:[],_queue:[],abort:function(e){var t,n,r,i,s;if(!e.abort){n=e,s=this._pending,e=null;if(s&&s.transaction.id===n)e=s.transaction,this._pending=null;else for(t=0,i=this._queue.length;t<i;++t){r=this._queue[t].transaction;if(r.id===n){e=r,this._queue.splice(t,1);break}}}e&&e.abort()},css:function(e,t,n){return this._load("css",e,t,n)},js:function(e,t,n){return this._load("js",e,t,n)},load:function(e,t,n){return this._load(null,e,t,n)},_autoPurge:function(e){e&&this._purgeNodes.length>=e&&this._purge(this._purgeNodes)},_getEnv:function(){var t=e.config.doc,n=e.UA;return this._env={async:t&&t.createElement("script").async===!0||n.ie>=10,cssFail:n.gecko>=9||n.compareVersions(n.webkit,535.24)>=0,cssLoad:(!n.gecko&&!n.webkit||n.gecko>=9||n.compareVersions(n.webkit,535.24)>=0)&&!(n.chrome&&n.chrome<=18),preservesScriptOrder:!!(n.gecko||n.opera||n.ie&&n.ie>=10)}},_getTransaction:function(t,r){var i=[],o,u,a,f;n.isArray(t)||(t=[t]),r=e.merge(this.options,r),r.attributes=e.merge(this.options.attributes,r.attributes);for(o=0,u=t.length;o<u;++o){f=t[o],a={attributes:{}};if(typeof f=="string")a.url=f;else{if(!f.url)continue;e.mix(a,f,!1,null,0,!0),f=f.url}e.mix(a,r,!1,null,0,!0),a.type||(this.REGEX_CSS.test(f)?a.type="css":(!this.REGEX_JS.test(f),a.type="js")),e.mix(a,a.type==="js"?this.jsOptions:this.cssOptions,!1,null,0,!0),a.attributes.id||(a.attributes.id=e.guid()),a.win?a.doc=a.win.document:a.win=a.doc.defaultView||a.doc.parentWindow,a.charset&&(a.attributes.charset=a.charset),i.push(a)}return new s(i,r)},_load:function(e,t,n,r){var s;return typeof n=="function"&&(r=n,n={}),n||(n={}),n.type=e,n._onFinish=i._onTransactionFinish,this._env||this._getEnv(),s=this._getTransaction(t,n),this._queue.push({callback:r,transaction:s}),this._next(),s},_onTransactionFinish:function(){i._pending=null,i._next()},_next:function(){var e;if(this._pending)return;e=this._queue.shift(),e&&(this._pending=e,e.transaction.execute(e.callback))},_purge:function(t){var n=this._purgeNodes,r=t!==n,i,s;while(s=t.pop()){if(!s._yuiget_finished)continue;s.parentNode&&s.parentNode.removeChild(s),r&&(i=e.Array.indexOf(n,s),i>-1&&n.splice(i,1))}}},i.script=i.js,i.Transaction=s=function(t,n){var r=this;r.id=s._lastId+=1,r.data=n.data,r.errors=[],r.nodes=[],r.options=n,r.requests=t,r._callbacks=[],r._queue=[],r._reqsWaiting=0,r.tId=r.id,r.win=n.win||e.config.win},s._lastId=0,s.prototype={_state:"new",abort:function(e){this._pending=null,this._pendingCSS=null,this._pollTimer=clearTimeout(this._pollTimer),this._queue=[],this._reqsWaiting=0,this.errors.push({error:e||"Aborted"}),this._finish()},execute:function(e){var t=this,n=t.requests,r=t._state,i,s,o,u;if(r==="done"){e&&e(t.errors.length?t.errors:null,t);return}e&&t._callbacks.push(e);if(r==="executing")return;t._state="executing",t._queue=o=[],t.options.timeout&&(t._timeout=setTimeout(function(){t.abort("Timeout")},t.options.timeout)),t._reqsWaiting=n.length;for(i=0,s=n.length;i<s;++i)u=n[i],u.async||u.type==="css"?t._insert(u):o.push(u);t._next()},purge:function(){i._purge(this.nodes)},_createNode:function(e,t,n){var i=n.createElement(e),s,o;r||(o=n.createElement("div"),o.setAttribute("class","a"),r=o.className==="a"?{}:{"for":"htmlFor","class":"className"});for(s in t)t.hasOwnProperty(s)&&i.setAttribute(r[s]||s,t[s]);return i},_finish:function(){var e=this.errors.length?this.errors:null,t=this.options,n=t.context||this,r,i,s;if(this._state==="done")return;this._state="done";for(i=0,s=this._callbacks.length;i<s;++i)this._callbacks[i].call(n,e,this);r=this._getEventData(),e?(t.onTimeout&&e[e.length-1].error==="Timeout"&&t.onTimeout.call(n,r),t.onFailure&&t.onFailure.call(n,r)):t.onSuccess&&t.onSuccess.call(n,r),t.onEnd&&t.onEnd.call(n,r),t._onFinish&&t._onFinish()},_getEventData:function(t){return t?e.merge(this,{abort:this.abort,purge:this.purge,request:t,url:t.url,win:t.win}):this},_getInsertBefore:function(t){var n=t.doc,r=t.insertBefore,s,o;return r?typeof r=="string"?n.getElementById(r):r:(s=i._insertCache,o=e.stamp(n),(r=s[o])?r:(r=n.getElementsByTagName("base")[0])?s[o]=r:(r=n.head||n.getElementsByTagName("head")[0],r?(r.appendChild(n.createTextNode("")),s[o]=r.lastChild):s[o]=n.getElementsByTagName("script")[0]))},_insert:function(t){function c(){u._progress("Failed to load "+t.url,t)}function h(){f&&clearTimeout(f),u._progress(null,t)}var n=i._env,r=this._getInsertBefore(t),s=t.type==="js",o=t.node,u=this,a=e.UA,f,l;o||(s?l="script":!n.cssLoad&&a.gecko?l="style":l="link",o=t.node=this._createNode(l,t.attributes,t.doc)),s?(o.setAttribute("src",t.url),t.async?o.async=!0:(n.async&&(o.async=!1),n.preservesScriptOrder||(this._pending=t))):!n.cssLoad&&a.gecko?o.innerHTML=(t.attributes.charset?'@charset "'+t.attributes.charset+'";':"")+'@import "'+t.url+'";':o.setAttribute("href",t.url),s&&a.ie&&(a.ie<9||document.documentMode&&document.documentMode<9)?o.onreadystatechange=function(){/loaded|complete/.test(o.readyState)&&(o.onreadystatechange=null,h())}:!s&&!n.cssLoad?this._poll(t):(a.ie>=10?(o.onerror=function(){setTimeout(c,0)},o.onload=function(){setTimeout(h,0)}):(o.onerror=c,o.onload=h),!n.cssFail&&!s&&(f=setTimeout(c,t.timeout||3e3))),this.nodes.push(o),r.parentNode.insertBefore(o,r)},_next:function(){if(this._pending)return;this._queue.length?this._insert(this._queue.shift()):this._reqsWaiting||this._finish()},_poll:function(t){var n=this,r=n._pendingCSS,i=e.UA.webkit,s,o,u,a,f,l;if(t){r||(r=n._pendingCSS=[]),r.push(t);if(n._pollTimer)return}n._pollTimer=null;for(s=0;s<r.length;++s){f=r[s];if(i){l=f.doc.styleSheets,u=l.length,a=f.node.href;while(--u>=0)if(l[u].href===a){r.splice(s,1),s-=1,n._progress(null,f);break}}else try{o=!!f.node.sheet.cssRules,r.splice(s,1),s-=1,n._progress(null,f)}catch(c){}}r.length&&(n._pollTimer=setTimeout(function(){n._poll.call(n)},n.options.pollInterval))},_progress:function(e,t){var n=this.options;e&&(t.error=e,this.errors.push({error:e,request:t})),t.node._yuiget_finished=t.finished=!0,n.onProgress&&n.
+onProgress.call(n.context||this,this._getEventData(t)),t.autopurge&&(i._autoPurge(this.options.purgethreshold),i._purgeNodes.push(t.node)),this._pending===t&&(this._pending=null),this._reqsWaiting-=1,this._next()}}},"3.17.2",{requires:["yui-base"]}),YUI.add("features",function(e,t){var n={};e.mix(e.namespace("Features"),{tests:n,add:function(e,t,r){n[e]=n[e]||{},n[e][t]=r},all:function(t,r){var i=n[t],s=[];return i&&e.Object.each(i,function(n,i){s.push(i+":"+(e.Features.test(t,i,r)?1:0))}),s.length?s.join(";"):""},test:function(t,r,i){i=i||[];var s,o,u,a=n[t],f=a&&a[r];return!f||(s=f.result,e.Lang.isUndefined(s)&&(o=f.ua,o&&(s=e.UA[o]),u=f.test,u&&(!o||s)&&(s=u.apply(e,i)),f.result=s)),s}});var r=e.Features.add;r("load","0",{name:"app-transitions-native",test:function(e){var t=e.config.doc,n=t?t.documentElement:null;return n&&n.style?"MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style:!1},trigger:"app-transitions"}),r("load","1",{name:"autocomplete-list-keys",test:function(e){return!e.UA.ios&&!e.UA.android},trigger:"autocomplete-list"}),r("load","2",{name:"dd-gestures",trigger:"dd-drag",ua:"touchEnabled"}),r("load","3",{name:"dom-style-ie",test:function(e){var t=e.Features.test,n=e.Features.add,r=e.config.win,i=e.config.doc,s="documentElement",o=!1;return n("style","computedStyle",{test:function(){return r&&"getComputedStyle"in r}}),n("style","opacity",{test:function(){return i&&"opacity"in i[s].style}}),o=!t("style","opacity")&&!t("style","computedStyle"),o},trigger:"dom-style"}),r("load","4",{name:"editor-para-ie",trigger:"editor-para",ua:"ie",when:"instead"}),r("load","5",{name:"event-base-ie",test:function(e){var t=e.config.doc&&e.config.doc.implementation;return t&&!t.hasFeature("Events","2.0")},trigger:"node-base"}),r("load","6",{name:"graphics-canvas",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"}),r("load","7",{name:"graphics-canvas-default",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"}),r("load","8",{name:"graphics-svg",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"}),r("load","9",{name:"graphics-svg-default",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"}),r("load","10",{name:"graphics-vml",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"}),r("load","11",{name:"graphics-vml-default",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"}),r("load","12",{name:"history-hash-ie",test:function(e){var t=e.config.doc&&e.config.doc.documentMode;return e.UA.ie&&(!("onhashchange"in e.config.win)||!t||t<8)},trigger:"history-hash"}),r("load","13",{name:"io-nodejs",trigger:"io-base",ua:"nodejs"}),r("load","14",{name:"json-parse-shim",test:function(e){function i(e,t){return e==="ok"?!0:t}var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONParse!==!1&&!!n;if(r)try{r=n.parse('{"ok":false}',i).ok}catch(s){r=!1}return!r},trigger:"json-parse"}),r("load","15",{name:"json-stringify-shim",test:function(e){var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONStringify!==!1&&!!n;if(r)try{r="0"===n.stringify(0)}catch(i){r=!1}return!r},trigger:"json-stringify"}),r("load","16",{name:"scrollview-base-ie",trigger:"scrollview-base",ua:"ie"}),r("load","17",{name:"selector-css2",test:function(e){var t=e.config.doc,n=t&&!("querySelectorAll"in t);return n},trigger:"selector"}),r("load","18",{name:"transition-timer",test:function(e){var t=e.config.doc,n=t?t.documentElement:null,r=!0;return n&&n.style&&(r=!("MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style)),r},trigger:"transition"}),r("load","19",{name:"widget-base-ie",trigger:"widget-base",ua:"ie"}),r("load","20",{name:"yql-jsonp",test:function(e){return!e.UA.nodejs&&!e.UA.winjs},trigger:"yql"}),r("load","21",{name:"yql-nodejs",trigger:"yql",ua:"nodejs"}),r("load","22",{name:"yql-winjs",trigger:"yql",ua:"winjs"})},"3.17.2",{requires:["yui-base"]}),YUI.add("intl-base",function(e,t){var n=/[, ]/;e.mix(e.namespace("Intl"),{lookupBestLang:function(t,r){function a(e){var t;for(t=0;t<r.length;t+=1)if(e.toLowerCase()===r[t].toLowerCase())return r[t]}var i,s,o,u;e.Lang.isString(t)&&(t=t.split(n));for(i=0;i<t.length;i+=1){s=t[i];if(!s||s==="*")continue;while(s.length>0){o=a(s);if(o)return o;u=s.lastIndexOf("-");if(!(u>=0))break;s=s.substring(0,u),u>=2&&s.charAt(u-2)==="-"&&(s=s.substring(0,u-2))}}return""}})},"3.17.2",{requires:["yui-base"]}),YUI.add("yui-log",function(e,t){var n=e,r="yui:log",i="undefined",s={debug:1,info:2,warn:4,error:8};n.log=function(e,t,o,u){var a,f,l,c,h,p,d=n,v=d.config,m=d.fire?d:YUI.Env.globalEvents;return v.debug&&(o=o||"",typeof o!="undefined"&&(f=v.logExclude,l=v.logInclude,!l||o in l?l&&
+o in l?a=!l[o]:f&&o in f&&(a=f[o]):a=1,typeof t=="undefined"&&(t="info"),d.config.logLevel=d.config.logLevel||"debug",p=s[d.config.logLevel.toLowerCase()],t in s&&s[t]<p&&(a=1)),a||(v.useBrowserConsole&&(c=o?o+": "+e:e,d.Lang.isFunction(v.logFn)?v.logFn.call(d,e,t,o):typeof console!==i&&console.log?(h=t&&console[t]&&t in s?t:"log",console[h](c)):typeof opera!==i&&opera.postError(c)),m&&!u&&(m===d&&!m.getEvent(r)&&m.publish(r,{broadcast:2}),m.fire(r,{msg:e,cat:t,src:o})))),d},n.message=function(){return n.log.apply(n,arguments)}},"3.17.2",{requires:["yui-base"]}),YUI.add("yui-later",function(e,t){var n=[];e.later=function(t,r,i,s,o){t=t||0,s=e.Lang.isUndefined(s)?n:e.Array(s),r=r||e.config.win||e;var u=!1,a=r&&e.Lang.isString(i)?r[i]:i,f=function(){u||(a.apply?a.apply(r,s||n):a(s[0],s[1],s[2],s[3]))},l=o?setInterval(f,t):setTimeout(f,t);return{id:l,interval:o,cancel:function(){u=!0,this.interval?clearInterval(l):clearTimeout(l)}}},e.Lang.later=e.later},"3.17.2",{requires:["yui-base"]}),YUI.add("yui",function(e,t){},"3.17.2",{use:["get","features","intl-base","yui-log","yui-later"]});
diff --git a/js/yui3/yui-core/yui-core-min.js b/js/yui3/yui-core/yui-core-min.js
new file mode 100644
index 000000000..d61d1d3c6
--- /dev/null
+++ b/js/yui3/yui-core/yui-core-min.js
@@ -0,0 +1,11 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+typeof YUI!="undefined"&&(YUI._YUI=YUI);var YUI=function(){var e=0,t=this,n=arguments,r=n.length,i=function(e,t){return e&&e.hasOwnProperty&&e instanceof t},s=typeof YUI_config!="undefined"&&YUI_config;i(t,YUI)?(t._init(),YUI.GlobalConfig&&t.applyConfig(YUI.GlobalConfig),s&&t.applyConfig(s),r||t._setup()):t=new YUI;if(r){for(;e<r;e++)t.applyConfig(n[e]);t._setup()}return t.instanceOf=i,t};(function(){var e,t,n="3.17.2",r=".",i="http://yui.yahooapis.com/",s="yui3-js-enabled",o="yui3-css-stamp",u=function(){},a=Array.prototype.slice,f={"io.xdrReady":1,"io.xdrResponse":1,"SWF.eventHandler":1},l=typeof window!="undefined",c=l?window:null,h=l?c.document:null,p=h&&h.documentElement,d=p&&p.className,v={},m=(new Date).getTime(),g=function(e,t,n,r){e&&e.addEventListener?e.addEventListener(t,n,r):e&&e.attachEvent&&e.attachEvent("on"+t,n)},y=function(e,t,n,r){if(e&&e.removeEventListener)try{e.removeEventListener(t,n,r)}catch(i){}else e&&e.detachEvent&&e.detachEvent("on"+t,n)},b=function(){YUI.Env.DOMReady=!0,l&&y(h,"DOMContentLoaded",b)},w=function(){YUI.Env.windowLoaded=!0,YUI.Env.DOMReady=!0,l&&y(window,"load",w)},E=function(e,t){var n=e.Env._loader,r=["loader-base"],i=YUI.Env,s=i.mods;return n?(n.ignoreRegistered=!1,n.onEnd=null,n.data=null,n.required=[],n.loadType=null):(n=new e.Loader(e.config),e.Env._loader=n),s&&s.loader&&(r=[].concat(r,YUI.Env.loaderExtras)),YUI.Env.core=e.Array.dedupe([].concat(YUI.Env.core,r)),n},S=function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},x={success:!0};p&&d.indexOf(s)==-1&&(d&&(d+=" "),d+=s,p.className=d),n.indexOf("@")>-1&&(n="3.5.0"),e={applyConfig:function(e){e=e||u;var t,n,r=this.config,i=r.modules,s=r.groups,o=r.aliases,a=this.Env._loader;for(n in e)e.hasOwnProperty(n)&&(t=e[n],i&&n=="modules"?S(i,t):o&&n=="aliases"?S(o,t):s&&n=="groups"?S(s,t):n=="win"?(r[n]=t&&t.contentWindow||t,r.doc=r[n]?r[n].document:null):n!="_yuid"&&(r[n]=t));a&&a._config(e)},_config:function(e){this.applyConfig(e)},_init:function(){var e,t,r=this,s=YUI.Env,u=r.Env,a;r.version=n;if(!u){r.Env={core:["intl-base"],loaderExtras:["loader-rollup","loader-yui3"],mods:{},versions:{},base:i,cdn:i+n+"/build/",_idx:0,_used:{},_attached:{},_exported:{},_missed:[],_yidx:0,_uidx:0,_guidp:"y",_loaded:{},_BASE_RE:/(?:\?(?:[^&]*&)*([^&]*))?\b(yui(?:-\w+)?)\/\2(?:-(min|debug))?\.js/,parseBasePath:function(e,t){var n=e.match(t),r,i;return n&&(r=RegExp.leftContext||e.slice(0,e.indexOf(n[0])),i=n[3],n[1]&&(r+="?"+n[1]),r={filter:i,path:r}),r},getBase:s&&s.getBase||function(t){var n=h&&h.getElementsByTagName("script")||[],i=u.cdn,s,o,a,f;for(o=0,a=n.length;o<a;++o){f=n[o].src;if(f){s=r.Env.parseBasePath(f,t);if(s){e=s.filter,i=s.path;break}}}return i}},u=r.Env,u._loaded[n]={};if(s&&r!==YUI)u._yidx=++s._yidx,u._guidp=("yui_"+n+"_"+u._yidx+"_"+m).replace(/[^a-z0-9_]+/g,"_");else if(YUI._YUI){s=YUI._YUI.Env,u._yidx+=s._yidx,u._uidx+=s._uidx;for(a in s)a in u||(u[a]=s[a]);delete YUI._YUI}r.id=r.stamp(r),v[r.id]=r}r.constructor=YUI,r.config=r.config||{bootstrap:!0,cacheUse:!0,debug:!0,doc:h,fetchCSS:!0,throwFail:!0,useBrowserConsole:!0,useNativeES5:!0,win:c,global:Function("return this")()},h&&!h.getElementById(o)?(t=h.createElement("div"),t.innerHTML='<div id="'+o+'" style="position: absolute !important; visibility: hidden !important"></div>',YUI.Env.cssStampEl=t.firstChild,h.body?h.body.appendChild(YUI.Env.cssStampEl):p.insertBefore(YUI.Env.cssStampEl,p.firstChild)):h&&h.getElementById(o)&&!YUI.Env.cssStampEl&&(YUI.Env.cssStampEl=h.getElementById(o)),r.config.lang=r.config.lang||"en-US",r.config.base=YUI.config.base||r.Env.getBase(r.Env._BASE_RE);if(!e||!"mindebug".indexOf(e))e="min";e=e?"-"+e:e,r.config.loaderPath=YUI.config.loaderPath||"loader/loader"+e+".js"},_setup:function(){var e,t=this,n=[],r=YUI.Env.mods,i=t.config.core||[].concat(YUI.Env.core);for(e=0;e<i.length;e++)r[i[e]]&&n.push(i[e]);t._attach(["yui-base"]),t._attach(n),t.Loader&&E(t)},applyTo:function(e,t,n){if(t in f){var r=v[e],i,s,o;if(r){i=t.split("."),s=r;for(o=0;o<i.length;o+=1)s=s[i[o]],s||this.log("applyTo not found: "+t,"warn","yui");return s&&s.apply(r,n)}return null}return this.log(t+": applyTo not allowed","warn","yui"),null},add:function(e,t,n,r){r=r||{};var i=YUI.Env,s={name:e,fn:t,version:n,details:r},o={},u,a,f,l,c=i.versions;i.mods[e]=s,c[n]=c[n]||{},c[n][e]=s;for(l in v)v.hasOwnProperty(l)&&(a=v[l],o[a.id]||(o[a.id]=!0,u=a.Env._loader,u&&(f=u.getModuleInfo(e),(!f||f.temp)&&u.addModule(r,e))));return this},_attach:function(e,t){var n,r,i,s,o,u,a,f=YUI.Env.mods,l=YUI.Env.aliases,c=this,h,p=YUI.Env._renderedMods,d=c.Env._loader,v=c.Env._attached,m=c.Env._exported,g=e.length,d,y,b,w=[],E,S,x,T,N,C,k;for(n=0;n<g;n++){r=e[n],i=f[r],w.push(r);if(d&&d.conditions[r])for(h in d.conditions[r])d.conditions[r].hasOwnProperty(h)&&(y=d.conditions[r][h],b=y&&(y.ua&&c.UA[y.ua]||y.test&&y.test(c)),b&&w.push(y.name))}e=w,g=e.length;for(n=0;n<g;n++)if(!v[e[n]]){r=e[n],i=f[r];if(l&&l[r]&&!i){c._attach(l[r]);continue}if(!i)T=d&&d.getModuleInfo(r),T&&(i=T,t=!0),!t&&r&&r.indexOf("skin-")===-1&&r.indexOf("css")===-1&&(c.Env._missed.push(r),c.Env._missed=c.Array.dedupe(c.Env._missed),c.message("NOT loaded: "+r,"warn","yui"));else{v[r]=!0;for(h=0;h<c.Env._missed.length;h++)c.Env._missed[h]===r&&(c.message("Found: "+r+" (was reported as missing earlier)","warn","yui"),c.Env._missed.splice(h,1));if(d&&!d._canBeAttached(r))return!0;if(d&&p&&p[r]&&p[r].temp){d.getRequires(p[r]),o=[],T=d.getModuleInfo(r);for(h in T.expanded_map)T.expanded_map.hasOwnProperty(h)&&o.push(h);c._attach(o)}s=i.details,o=s.requires,S=s.es,u=s.use,a=s.after,s.lang&&(o=o||[],o.unshift("intl"));if(o){x=o.length;for(h=0;h<x;h++)if(!v[o[h]]){if(!c._attach(o))return!1;break}}if(a)for(h=0;h<a.length;h++)if(!v[a[h]]){if(!c._attach(a,!0))return!1;break}if(i.fn){E=[c,r];if(S){k={},C={},E.push(k,C);if(o){x=o.length;for(h=0;h<x;h++)k[o[h]]=m.hasOwnProperty(o[h])?m[o[h]]:c}}if(c.config.throwFail)C=i.fn.apply(S?undefined:i,E);else try{C=i.fn.apply(S?undefined:i,E)}catch(L){return c.error
+("Attach error: "+r,L,r),!1}S&&(m[r]=C,N=i.details.condition,N&&N.when==="instead"&&(m[N.trigger]=C))}if(u)for(h=0;h<u.length;h++)if(!v[u[h]]){if(!c._attach(u))return!1;break}}}return!0},_delayCallback:function(e,t){var n=this,r=["event-base"];return t=n.Lang.isObject(t)?t:{event:t},t.event==="load"&&r.push("event-synthetic"),function(){var i=arguments;n._use(r,function(){n.on(t.event,function(){i[1].delayUntil=t.event,e.apply(n,i)},t.args)})}},use:function(){var e=a.call(arguments,0),t=e[e.length-1],n=this,r=0,i,s=n.Env,o=!0;n.Lang.isFunction(t)?(e.pop(),n.config.delayUntil&&(t=n._delayCallback(t,n.config.delayUntil))):t=null,n.Lang.isArray(e[0])&&(e=e[0]);if(n.config.cacheUse){while(i=e[r++])if(!s._attached[i]){o=!1;break}if(o)return e.length,n._notify(t,x,e),n}return n._loading?(n._useQueue=n._useQueue||new n.Queue,n._useQueue.add([e,t])):n._use(e,function(n,r){n._notify(t,r,e)}),n},require:function(){var e=a.call(arguments),t;typeof e[e.length-1]=="function"&&(t=e.pop(),e.push(function(n){var r,i=e.length,s=n.Env._exported,o={};for(r=0;r<i;r++)s.hasOwnProperty(e[r])&&(o[e[r]]=s[e[r]]);t.call(undefined,n,o)})),this.use.apply(this,e)},_notify:function(e,t,n){if(!t.success&&this.config.loadErrorFn)this.config.loadErrorFn.call(this,this,e,t,n);else if(e){this.Env._missed&&this.Env._missed.length&&(t.msg="Missing modules: "+this.Env._missed.join(),t.success=!1);if(this.config.throwFail)e(this,t);else try{e(this,t)}catch(r){this.error("use callback error",r,n)}}},_use:function(e,t){this.Array||this._attach(["yui-base"]);var r,i,s,o=this,u=YUI.Env,a=u.mods,f=o.Env,l=f._used,c=u.aliases,h=u._loaderQueue,p=e[0],d=o.Array,v=o.config,m=v.bootstrap,g=[],y,b=[],w=!0,S=v.fetchCSS,x=function(e,t){var r=0,i=[],s,o,f,h,p;if(!e.length)return;if(c){o=e.length;for(r=0;r<o;r++)c[e[r]]&&!a[e[r]]?i=[].concat(i,c[e[r]]):i.push(e[r]);e=i}o=e.length;for(r=0;r<o;r++){s=e[r],t||b.push(s);if(l[s])continue;f=a[s],h=null,p=null,f?(l[s]=!0,h=f.details.requires,p=f.details.use):u._loaded[n][s]?l[s]=!0:g.push(s),h&&h.length&&x(h),p&&p.length&&x(p,1)}},T=function(n){var r=n||{success:!0,msg:"not dynamic"},i,s,u=!0,a=r.data;o._loading=!1,a&&(s=g,g=[],b=[],x(a),i=g.length,i&&[].concat(g).sort().join()==s.sort().join()&&(i=!1)),i&&a?(o._loading=!0,o._use(g,function(){o._attach(a)&&o._notify(t,r,a)})):(a&&(u=o._attach(a)),u&&o._notify(t,r,e)),o._useQueue&&o._useQueue.size()&&!o._loading&&o._use.apply(o,o._useQueue.next())};if(p==="*"){e=[];for(y in a)a.hasOwnProperty(y)&&e.push(y);return w=o._attach(e),w&&T(),o}return(a.loader||a["loader-base"])&&!o.Loader&&o._attach(["loader"+(a.loader?"":"-base")]),m&&o.Loader&&e.length&&(i=E(o),i.require(e),i.ignoreRegistered=!0,i._boot=!0,i.calculate(null,S?null:"js"),e=i.sorted,i._boot=!1),x(e),r=g.length,r&&(g=d.dedupe(g),r=g.length),m&&r&&o.Loader?(o._loading=!0,i=E(o),i.onEnd=T,i.context=o,i.data=e,i.ignoreRegistered=!1,i.require(g),i.insert(null,S?null:"js")):m&&r&&o.Get&&!f.bootstrapped?(o._loading=!0,s=function(){o._loading=!1,h.running=!1,f.bootstrapped=!0,u._bootstrapping=!1,o._attach(["loader"])&&o._use(e,t)},u._bootstrapping?h.add(s):(u._bootstrapping=!0,o.Get.script(v.base+v.loaderPath,{onEnd:s}))):(w=o._attach(e),w&&T()),o},namespace:function(){var e=arguments,t,n=0,i,s,o;for(;n<e.length;n++){t=this,o=e[n];if(o.indexOf(r)>-1){s=o.split(r);for(i=s[0]=="YAHOO"?1:0;i<s.length;i++)t[s[i]]=t[s[i]]||{},t=t[s[i]]}else t[o]=t[o]||{},t=t[o]}return t},log:u,message:u,dump:function(e){return""+e},error:function(e,t,n){var r=this,i;r.config.errorFn&&(i=r.config.errorFn.apply(r,arguments));if(!i)throw t||new Error(e);return r.message(e,"error",""+n),r},guid:function(e){var t=this.Env._guidp+"_"+ ++this.Env._uidx;return e?e+t:t},stamp:function(e,t){var n;if(!e)return e;e.uniqueID&&e.nodeType&&e.nodeType!==9?n=e.uniqueID:n=typeof e=="string"?e:e._yuid;if(!n){n=this.guid();if(!t)try{e._yuid=n}catch(r){n=null}}return n},destroy:function(){var e=this;e.Event&&e.Event._unload(),delete v[e.id],delete e.Env,delete e.config}},YUI.prototype=e;for(t in e)e.hasOwnProperty(t)&&(YUI[t]=e[t]);YUI.applyConfig=function(e){if(!e)return;YUI.GlobalConfig&&this.prototype.applyConfig.call(this,YUI.GlobalConfig),this.prototype.applyConfig.call(this,e),YUI.GlobalConfig=this.config},YUI._init(),l?(g(h,"DOMContentLoaded",b),g(window,"load",w)):(b(),w()),YUI.Env.add=g,YUI.Env.remove=y,typeof exports=="object"&&(exports.YUI=YUI,YUI.setLoadHook=function(e){YUI._getLoadHook=e},YUI._getLoadHook=null),YUI.Env[n]={}})(),YUI.add("yui-base",function(e,t){function m(e,t,n){var r,i;t||(t=0);if(n||m.test(e))try{return d.slice.call(e,t)}catch(s){i=[];for(r=e.length;t<r;++t)i.push(e[t]);return i}return[e]}function g(){this._init(),this.add.apply(this,arguments)}var n=e.Lang||(e.Lang={}),r=String.prototype,i=Object.prototype.toString,s={"undefined":"undefined",number:"number","boolean":"boolean",string:"string","[object Function]":"function","[object RegExp]":"regexp","[object Array]":"array","[object Date]":"date","[object Error]":"error"},o=/\{\s*([^|}]+?)\s*(?:\|([^}]*))?\s*\}/g,u=" \n \f\r \u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff",a="[ -\r \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+",f=new RegExp("^"+a),l=new RegExp(a+"$"),c=new RegExp(f.source+"|"+l.source,"g"),h=/\{\s*\[(?:native code|function)\]\s*\}/i;n._isNative=function(t){return!!(e.config.useNativeES5&&t&&h.test(t))},n.isArray=n._isNative(Array.isArray)?Array.isArray:function(e){return n.type(e)==="array"},n.isBoolean=function(e){return typeof e=="boolean"},n.isDate=function(e){return n.type(e)==="date"&&e.toString()!=="Invalid Date"&&!isNaN(e)},n.isFunction=function(e){return n.type(e)==="function"},n.isNull=function(e){return e===null},n.isNumber=function(e){return typeof e=="number"&&isFinite(e)},n.isObject=function(e,t){var r=typeof e;return e&&(r==="object"||!t&&(r==="function"||n.isFunction(e)))||!1},n.isRegExp=function(e){return n.type(e)==="regexp"},n.
+isString=function(e){return typeof e=="string"},n.isUndefined=function(e){return typeof e=="undefined"},n.isValue=function(e){var t=n.type(e);switch(t){case"number":return isFinite(e);case"null":case"undefined":return!1;default:return!!t}},n.now=Date.now||function(){return(new Date).getTime()},n.sub=function(e,t){return e.replace?e.replace(o,function(e,r){return n.isUndefined(t[r])?e:t[r]}):e},n.trim=n._isNative(r.trim)&&!u.trim()?function(e){return e&&e.trim?e.trim():e}:function(e){try{return e.replace(c,"")}catch(t){return e}},n.trimLeft=n._isNative(r.trimLeft)&&!u.trimLeft()?function(e){return e.trimLeft()}:function(e){return e.replace(f,"")},n.trimRight=n._isNative(r.trimRight)&&!u.trimRight()?function(e){return e.trimRight()}:function(e){return e.replace(l,"")},n.type=function(e){return s[typeof e]||s[i.call(e)]||(e?"object":"null")};var p=e.Lang,d=Array.prototype,v=Object.prototype.hasOwnProperty;e.Array=m,m.dedupe=p._isNative(Object.create)?function(e){var t=Object.create(null),n=[],r,i,s;for(r=0,s=e.length;r<s;++r)i=e[r],t[i]||(t[i]=1,n.push(i));return n}:function(e){var t={},n=[],r,i,s;for(r=0,s=e.length;r<s;++r)i=e[r],v.call(t,i)||(t[i]=1,n.push(i));return n},m.each=m.forEach=p._isNative(d.forEach)?function(t,n,r){return d.forEach.call(t||[],n,r||e),e}:function(t,n,r){for(var i=0,s=t&&t.length||0;i<s;++i)i in t&&n.call(r||e,t[i],i,t);return e},m.hash=function(e,t){var n={},r=t&&t.length||0,i,s;for(i=0,s=e.length;i<s;++i)i in e&&(n[e[i]]=r>i&&i in t?t[i]:!0);return n},m.indexOf=p._isNative(d.indexOf)?function(e,t,n){return d.indexOf.call(e,t,n)}:function(e,t,n){var r=e.length;n=+n||0,n=(n>0||-1)*Math.floor(Math.abs(n)),n<0&&(n+=r,n<0&&(n=0));for(;n<r;++n)if(n in e&&e[n]===t)return n;return-1},m.numericSort=function(e,t){return e-t},m.some=p._isNative(d.some)?function(e,t,n){return d.some.call(e,t,n)}:function(e,t,n){for(var r=0,i=e.length;r<i;++r)if(r in e&&t.call(n,e[r],r,e))return!0;return!1},m.test=function(e){var t=0;if(p.isArray(e))t=1;else if(p.isObject(e))try{"length"in e&&!e.tagName&&(!e.scrollTo||!e.document)&&!e.apply&&(t=2)}catch(n){}return t},g.prototype={_init:function(){this._q=[]},next:function(){return this._q.shift()},last:function(){return this._q.pop()},add:function(){return this._q.push.apply(this._q,arguments),this},size:function(){return this._q.length}},e.Queue=g,YUI.Env._loaderQueue=YUI.Env._loaderQueue||new g;var y="__",v=Object.prototype.hasOwnProperty,b=e.Lang.isObject;e.cached=function(e,t,n){return t||(t={}),function(r){var i=arguments.length>1?Array.prototype.join.call(arguments,y):String(r);if(!(i in t)||n&&t[i]==n)t[i]=e.apply(e,arguments);return t[i]}},e.getLocation=function(){var t=e.config.win;return t&&t.location},e.merge=function(){var e=0,t=arguments.length,n={},r,i;for(;e<t;++e){i=arguments[e];for(r in i)v.call(i,r)&&(n[r]=i[r])}return n},e.mix=function(t,n,r,i,s,o){var u,a,f,l,c,h,p;if(!t||!n)return t||e;if(s){s===2&&e.mix(t.prototype,n.prototype,r,i,0,o),f=s===1||s===3?n.prototype:n,p=s===1||s===4?t.prototype:t;if(!f||!p)return t}else f=n,p=t;u=r&&!o;if(i)for(l=0,h=i.length;l<h;++l){c=i[l];if(!v.call(f,c))continue;a=u?!1:c in p;if(o&&a&&b(p[c],!0)&&b(f[c],!0))e.mix(p[c],f[c],r,null,0,o);else if(r||!a)p[c]=f[c]}else{for(c in f){if(!v.call(f,c))continue;a=u?!1:c in p;if(o&&a&&b(p[c],!0)&&b(f[c],!0))e.mix(p[c],f[c],r,null,0,o);else if(r||!a)p[c]=f[c]}e.Object._hasEnumBug&&e.mix(p,f,r,e.Object._forceEnum,s,o)}return t};var p=e.Lang,v=Object.prototype.hasOwnProperty,w,E=e.Object=p._isNative(Object.create)?function(e){return Object.create(e)}:function(){function e(){}return function(t){return e.prototype=t,new e}}(),S=E._forceEnum=["hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toString","toLocaleString","valueOf"],x=E._hasEnumBug=!{valueOf:0}.propertyIsEnumerable("valueOf"),T=E._hasProtoEnumBug=function(){}.propertyIsEnumerable("prototype"),N=E.owns=function(e,t){return!!e&&v.call(e,t)};E.hasKey=N,E.keys=p._isNative(Object.keys)&&!T?Object.keys:function(e){if(!p.isObject(e))throw new TypeError("Object.keys called on a non-object");var t=[],n,r,i;if(T&&typeof e=="function")for(r in e)N(e,r)&&r!=="prototype"&&t.push(r);else for(r in e)N(e,r)&&t.push(r);if(x)for(n=0,i=S.length;n<i;++n)r=S[n],N(e,r)&&t.push(r);return t},E.values=function(e){var t=E.keys(e),n=0,r=t.length,i=[];for(;n<r;++n)i.push(e[t[n]]);return i},E.size=function(e){try{return E.keys(e).length}catch(t){return 0}},E.hasValue=function(t,n){return e.Array.indexOf(E.values(t),n)>-1},E.each=function(t,n,r,i){var s;for(s in t)(i||N(t,s))&&n.call(r||e,t[s],s,t);return e},E.some=function(t,n,r,i){var s;for(s in t)if(i||N(t,s))if(n.call(r||e,t[s],s,t))return!0;return!1},E.getValue=function(t,n){if(!p.isObject(t))return w;var r,i=e.Array(n),s=i.length;for(r=0;t!==w&&r<s;r++)t=t[i[r]];return t},E.setValue=function(t,n,r){var i,s=e.Array(n),o=s.length-1,u=t;if(o>=0){for(i=0;u!==w&&i<o;i++)u=u[s[i]];if(u===w)return w;u[s[i]]=r}return t},E.isEmpty=function(e){return!E.keys(Object(e)).length},YUI.Env.parseUA=function(t){var n=function(e){var t=0;return parseFloat(e.replace(/\./g,function(){return t++===1?"":"."}))},r=e.config.win,i=r&&r.navigator,s={ie:0,opera:0,gecko:0,webkit:0,safari:0,chrome:0,mobile:null,air:0,phantomjs:0,ipad:0,iphone:0,ipod:0,ios:null,android:0,silk:0,ubuntu:0,accel:!1,webos:0,caja:i&&i.cajaVersion,secure:!1,os:null,nodejs:0,winjs:typeof Windows!="undefined"&&!!Windows.System,touchEnabled:!1},o=t||i&&i.userAgent,u=r&&r.location,a=u&&u.href,f;return s.userAgent=o,s.secure=a&&a.toLowerCase().indexOf("https")===0,o&&(/windows|win32/i.test(o)?s.os="windows":/macintosh|mac_powerpc/i.test(o)?s.os="macintosh":/android/i.test(o)?s.os="android":/symbos/i.test(o)?s.os="symbos":/linux/i.test(o)?s.os="linux":/rhino/i.test(o)&&(s.os="rhino"),/KHTML/.test(o)&&(s.webkit=1),/IEMobile|XBLWP7/.test(o)&&(s.mobile="windows"),/Fennec/.test(o)&&(s.mobile="gecko"),f=o.match(/AppleWebKit\/([^\s]*)/),f&&f[1]&&(s.webkit=n(f[1]),s.safari=s.webkit,/PhantomJS/.test(o)&&(f=o.match(/PhantomJS\/([^\s]*)/
+),f&&f[1]&&(s.phantomjs=n(f[1]))),/ Mobile\//.test(o)||/iPad|iPod|iPhone/.test(o)?(s.mobile="Apple",f=o.match(/OS ([^\s]*)/),f&&f[1]&&(f=n(f[1].replace("_","."))),s.ios=f,s.os="ios",s.ipad=s.ipod=s.iphone=0,f=o.match(/iPad|iPod|iPhone/),f&&f[0]&&(s[f[0].toLowerCase()]=s.ios)):(f=o.match(/NokiaN[^\/]*|webOS\/\d\.\d/),f&&(s.mobile=f[0]),/webOS/.test(o)&&(s.mobile="WebOS",f=o.match(/webOS\/([^\s]*);/),f&&f[1]&&(s.webos=n(f[1]))),/ Android/.test(o)&&(/Mobile/.test(o)&&(s.mobile="Android"),f=o.match(/Android ([^\s]*);/),f&&f[1]&&(s.android=n(f[1]))),/Silk/.test(o)&&(f=o.match(/Silk\/([^\s]*)/),f&&f[1]&&(s.silk=n(f[1])),s.android||(s.android=2.34,s.os="Android"),/Accelerated=true/.test(o)&&(s.accel=!0))),f=o.match(/OPR\/(\d+\.\d+)/),f&&f[1]?s.opera=n(f[1]):(f=o.match(/(Chrome|CrMo|CriOS)\/([^\s]*)/),f&&f[1]&&f[2]?(s.chrome=n(f[2]),s.safari=0,f[1]==="CrMo"&&(s.mobile="chrome")):(f=o.match(/AdobeAIR\/([^\s]*)/),f&&(s.air=f[0])))),f=o.match(/Ubuntu\ (\d+\.\d+)/),f&&f[1]&&(s.os="linux",s.ubuntu=n(f[1]),f=o.match(/\ WebKit\/([^\s]*)/),f&&f[1]&&(s.webkit=n(f[1])),f=o.match(/\ Chromium\/([^\s]*)/),f&&f[1]&&(s.chrome=n(f[1])),/ Mobile$/.test(o)&&(s.mobile="Ubuntu")),s.webkit||(/Opera/.test(o)?(f=o.match(/Opera[\s\/]([^\s]*)/),f&&f[1]&&(s.opera=n(f[1])),f=o.match(/Version\/([^\s]*)/),f&&f[1]&&(s.opera=n(f[1])),/Opera Mobi/.test(o)&&(s.mobile="opera",f=o.replace("Opera Mobi","").match(/Opera ([^\s]*)/),f&&f[1]&&(s.opera=n(f[1]))),f=o.match(/Opera Mini[^;]*/),f&&(s.mobile=f[0])):(f=o.match(/MSIE ([^;]*)|Trident.*; rv:([0-9.]+)/),f&&(f[1]||f[2])?s.ie=n(f[1]||f[2]):(f=o.match(/Gecko\/([^\s]*)/),f&&(s.gecko=1,f=o.match(/rv:([^\s\)]*)/),f&&f[1]&&(s.gecko=n(f[1]),/Mobile|Tablet/.test(o)&&(s.mobile="ffos"))))))),r&&i&&!(s.chrome&&s.chrome<6)&&(s.touchEnabled="ontouchstart"in r||"msMaxTouchPoints"in i&&i.msMaxTouchPoints>0),t||(typeof process=="object"&&process.versions&&process.versions.node&&(s.os=process.platform,s.nodejs=n(process.versions.node)),YUI.Env.UA=s),s},e.UA=YUI.Env.UA||YUI.Env.parseUA(),e.UA.compareVersions=function(e,t){var n,r,i,s,o,u;if(e===t)return 0;r=(e+"").split("."),s=(t+"").split(".");for(o=0,u=Math.max(r.length,s.length);o<u;++o){n=parseInt(r[o],10),i=parseInt(s[o],10),isNaN(n)&&(n=0),isNaN(i)&&(i=0);if(n<i)return-1;if(n>i)return 1}return 0},YUI.Env.aliases={anim:["anim-base","anim-color","anim-curve","anim-easing","anim-node-plugin","anim-scroll","anim-xy"],"anim-shape-transform":["anim-shape"],app:["app-base","app-content","app-transitions","lazy-model-list","model","model-list","model-sync-rest","model-sync-local","router","view","view-node-map"],attribute:["attribute-base","attribute-complex"],"attribute-events":["attribute-observable"],autocomplete:["autocomplete-base","autocomplete-sources","autocomplete-list","autocomplete-plugin"],axes:["axis-numeric","axis-category","axis-time","axis-stacked"],"axes-base":["axis-numeric-base","axis-category-base","axis-time-base","axis-stacked-base"],base:["base-base","base-pluginhost","base-build"],cache:["cache-base","cache-offline","cache-plugin"],charts:["charts-base"],collection:["array-extras","arraylist","arraylist-add","arraylist-filter","array-invoke"],color:["color-base","color-hsl","color-harmony"],controller:["router"],dataschema:["dataschema-base","dataschema-json","dataschema-xml","dataschema-array","dataschema-text"],datasource:["datasource-local","datasource-io","datasource-get","datasource-function","datasource-cache","datasource-jsonschema","datasource-xmlschema","datasource-arrayschema","datasource-textschema","datasource-polling"],datatable:["datatable-core","datatable-table","datatable-head","datatable-body","datatable-base","datatable-column-widths","datatable-message","datatable-mutable","datatable-sort","datatable-datasource"],datatype:["datatype-date","datatype-number","datatype-xml"],"datatype-date":["datatype-date-parse","datatype-date-format","datatype-date-math"],"datatype-number":["datatype-number-parse","datatype-number-format"],"datatype-xml":["datatype-xml-parse","datatype-xml-format"],dd:["dd-ddm-base","dd-ddm","dd-ddm-drop","dd-drag","dd-proxy","dd-constrain","dd-drop","dd-scroll","dd-delegate"],dom:["dom-base","dom-screen","dom-style","selector-native","selector"],editor:["frame","editor-selection","exec-command","editor-base","editor-para","editor-br","editor-bidi","editor-tab","createlink-base"],event:["event-base","event-delegate","event-synthetic","event-mousewheel","event-mouseenter","event-key","event-focus","event-resize","event-hover","event-outside","event-touch","event-move","event-flick","event-valuechange","event-tap"],"event-custom":["event-custom-base","event-custom-complex"],"event-gestures":["event-flick","event-move"],handlebars:["handlebars-compiler"],highlight:["highlight-base","highlight-accentfold"],history:["history-base","history-hash","history-html5"],io:["io-base","io-xdr","io-form","io-upload-iframe","io-queue"],json:["json-parse","json-stringify"],loader:["loader-base","loader-rollup","loader-yui3"],node:["node-base","node-event-delegate","node-pluginhost","node-screen","node-style"],pluginhost:["pluginhost-base","pluginhost-config"],querystring:["querystring-parse","querystring-stringify"],recordset:["recordset-base","recordset-sort","recordset-filter","recordset-indexer"],resize:["resize-base","resize-proxy","resize-constrain"],slider:["slider-base","slider-value-range","clickable-rail","range-slider"],template:["template-base","template-micro"],text:["text-accentfold","text-wordbreak"],widget:["widget-base","widget-htmlparser","widget-skin","widget-uievents"]}},"3.17.2");
diff --git a/js/yui3/yui-later/yui-later-min.js b/js/yui3/yui-later/yui-later-min.js
new file mode 100644
index 000000000..ceff3248a
--- /dev/null
+++ b/js/yui3/yui-later/yui-later-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("yui-later",function(e,t){var n=[];e.later=function(t,r,i,s,o){t=t||0,s=e.Lang.isUndefined(s)?n:e.Array(s),r=r||e.config.win||e;var u=!1,a=r&&e.Lang.isString(i)?r[i]:i,f=function(){u||(a.apply?a.apply(r,s||n):a(s[0],s[1],s[2],s[3]))},l=o?setInterval(f,t):setTimeout(f,t);return{id:l,interval:o,cancel:function(){u=!0,this.interval?clearInterval(l):clearTimeout(l)}}},e.Lang.later=e.later},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/yui-log-nodejs/yui-log-nodejs-min.js b/js/yui3/yui-log-nodejs/yui-log-nodejs-min.js
new file mode 100644
index 000000000..c7e132dba
--- /dev/null
+++ b/js/yui3/yui-log-nodejs/yui-log-nodejs-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("yui-log-nodejs",function(e,t){var n=require(process.binding("natives").util?"util":"sys"),r=!1;try{var i=require("stdio");r=i.isStderrATTY()}catch(s){r=!0}e.config.useColor=r,e.consoleColor=function(e,t){return this.config.useColor?(t||(t="32"),"["+t+"m"+e+""):e};var o=function(e,t,r){var i="",s,o;this.id&&(i="["+this.id+"]:"),t=t||"info",r=r?this.consoleColor(" ("+r.toLowerCase()+"):",35):"",e===null&&(e="null");if(typeof e=="object"||e instanceof Array)try{e.tagName||e._yuid||e._query?e=e.toString():e=n.inspect(e)}catch(u){}s="37;40",o=e?"":31,t+="";switch(t.toLowerCase()){case"error":s=o=31;break;case"warn":s=33;break;case"debug":s=34}typeof e=="string"&&e&&e.indexOf("\n")!==-1&&(e="\n"+e),n.error(this.consoleColor(t.toLowerCase()+":",s)+r+" "+this.consoleColor(e,o))};e.config.logFn||(e.config.logFn=o)},"3.17.2");
diff --git a/js/yui3/yui-log/yui-log-min.js b/js/yui3/yui-log/yui-log-min.js
new file mode 100644
index 000000000..b02c8e7cb
--- /dev/null
+++ b/js/yui3/yui-log/yui-log-min.js
@@ -0,0 +1,8 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("yui-log",function(e,t){var n=e,r="yui:log",i="undefined",s={debug:1,info:2,warn:4,error:8};n.log=function(e,t,o,u){var a,f,l,c,h,p,d=n,v=d.config,m=d.fire?d:YUI.Env.globalEvents;return v.debug&&(o=o||"",typeof o!="undefined"&&(f=v.logExclude,l=v.logInclude,!l||o in l?l&&o in l?a=!l[o]:f&&o in f&&(a=f[o]):a=1,typeof t=="undefined"&&(t="info"),d.config.logLevel=d.config.logLevel||"debug",p=s[d.config.logLevel.toLowerCase()],t in s&&s[t]<p&&(a=1)),a||(v.useBrowserConsole&&(c=o?o+": "+e:e,d.Lang.isFunction(v.logFn)?v.logFn.call(d,e,t,o):typeof console!==i&&console.log?(h=t&&console[t]&&t in s?t:"log",console[h](c)):typeof opera!==i&&opera.postError(c)),m&&!u&&(m===d&&!m.getEvent(r)&&m.publish(r,{broadcast:2}),m.fire(r,{msg:e,cat:t,src:o})))),d},n.message=function(){return n.log.apply(n,arguments)}},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/yui-nodejs/yui-nodejs-min.js b/js/yui3/yui-nodejs/yui-nodejs-min.js
new file mode 100644
index 000000000..e7198948e
--- /dev/null
+++ b/js/yui3/yui-nodejs/yui-nodejs-min.js
@@ -0,0 +1,22 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+typeof YUI!="undefined"&&(YUI._YUI=YUI);var YUI=function(){var e=0,t=this,n=arguments,r=n.length,i=function(e,t){return e&&e.hasOwnProperty&&e instanceof t},s=typeof YUI_config!="undefined"&&YUI_config;i(t,YUI)?(t._init(),YUI.GlobalConfig&&t.applyConfig(YUI.GlobalConfig),s&&t.applyConfig(s),r||t._setup()):t=new YUI;if(r){for(;e<r;e++)t.applyConfig(n[e]);t._setup()}return t.instanceOf=i,t};(function(){var e,t,n="3.17.2",r=".",i="http://yui.yahooapis.com/",s="yui3-js-enabled",o="yui3-css-stamp",u=function(){},a=Array.prototype.slice,f={"io.xdrReady":1,"io.xdrResponse":1,"SWF.eventHandler":1},l=typeof window!="undefined",c=l?window:null,h=l?c.document:null,p=h&&h.documentElement,d=p&&p.className,v={},m=(new Date).getTime(),g=function(e,t,n,r){e&&e.addEventListener?e.addEventListener(t,n,r):e&&e.attachEvent&&e.attachEvent("on"+t,n)},y=function(e,t,n,r){if(e&&e.removeEventListener)try{e.removeEventListener(t,n,r)}catch(i){}else e&&e.detachEvent&&e.detachEvent("on"+t,n)},b=function(){YUI.Env.DOMReady=!0,l&&y(h,"DOMContentLoaded",b)},w=function(){YUI.Env.windowLoaded=!0,YUI.Env.DOMReady=!0,l&&y(window,"load",w)},E=function(e,t){var n=e.Env._loader,r=["loader-base"],i=YUI.Env,s=i.mods;return n?(n.ignoreRegistered=!1,n.onEnd=null,n.data=null,n.required=[],n.loadType=null):(n=new e.Loader(e.config),e.Env._loader=n),s&&s.loader&&(r=[].concat(r,YUI.Env.loaderExtras)),YUI.Env.core=e.Array.dedupe([].concat(YUI.Env.core,r)),n},S=function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},x={success:!0};p&&d.indexOf(s)==-1&&(d&&(d+=" "),d+=s,p.className=d),n.indexOf("@")>-1&&(n="3.5.0"),e={applyConfig:function(e){e=e||u;var t,n,r=this.config,i=r.modules,s=r.groups,o=r.aliases,a=this.Env._loader;for(n in e)e.hasOwnProperty(n)&&(t=e[n],i&&n=="modules"?S(i,t):o&&n=="aliases"?S(o,t):s&&n=="groups"?S(s,t):n=="win"?(r[n]=t&&t.contentWindow||t,r.doc=r[n]?r[n].document:null):n!="_yuid"&&(r[n]=t));a&&a._config(e)},_config:function(e){this.applyConfig(e)},_init:function(){var e,t,r=this,s=YUI.Env,u=r.Env,a;r.version=n;if(!u){r.Env={core:["get","features","intl-base","yui-log","yui-log-nodejs","yui-later","loader-base","loader-rollup","loader-yui3"],loaderExtras:["loader-rollup","loader-yui3"],mods:{},versions:{},base:i,cdn:i+n+"/build/",_idx:0,_used:{},_attached:{},_exported:{},_missed:[],_yidx:0,_uidx:0,_guidp:"y",_loaded:{},_BASE_RE:/(?:\?(?:[^&]*&)*([^&]*))?\b(yui(?:-\w+)?)\/\2(?:-(min|debug))?\.js/,parseBasePath:function(e,t){var n=e.match(t),r,i;return n&&(r=RegExp.leftContext||e.slice(0,e.indexOf(n[0])),i=n[3],n[1]&&(r+="?"+n[1]),r={filter:i,path:r}),r},getBase:s&&s.getBase||function(t){var n=h&&h.getElementsByTagName("script")||[],i=u.cdn,s,o,a,f;for(o=0,a=n.length;o<a;++o){f=n[o].src;if(f){s=r.Env.parseBasePath(f,t);if(s){e=s.filter,i=s.path;break}}}return i}},u=r.Env,u._loaded[n]={};if(s&&r!==YUI)u._yidx=++s._yidx,u._guidp=("yui_"+n+"_"+u._yidx+"_"+m).replace(/[^a-z0-9_]+/g,"_");else if(YUI._YUI){s=YUI._YUI.Env,u._yidx+=s._yidx,u._uidx+=s._uidx;for(a in s)a in u||(u[a]=s[a]);delete YUI._YUI}r.id=r.stamp(r),v[r.id]=r}r.constructor=YUI,r.config=r.config||{bootstrap:!0,cacheUse:!0,debug:!0,doc:h,fetchCSS:!0,throwFail:!0,useBrowserConsole:!0,useNativeES5:!0,win:c,global:Function("return this")()},h&&!h.getElementById(o)?(t=h.createElement("div"),t.innerHTML='<div id="'+o+'" style="position: absolute !important; visibility: hidden !important"></div>',YUI.Env.cssStampEl=t.firstChild,h.body?h.body.appendChild(YUI.Env.cssStampEl):p.insertBefore(YUI.Env.cssStampEl,p.firstChild)):h&&h.getElementById(o)&&!YUI.Env.cssStampEl&&(YUI.Env.cssStampEl=h.getElementById(o)),r.config.lang=r.config.lang||"en-US",r.config.base=YUI.config.base||r.Env.getBase(r.Env._BASE_RE);if(!e||!"mindebug".indexOf(e))e="min";e=e?"-"+e:e,r.config.loaderPath=YUI.config.loaderPath||"loader/loader"+e+".js"},_setup:function(){var e,t=this,n=[],r=YUI.Env.mods,i=t.config.core||[].concat(YUI.Env.core);for(e=0;e<i.length;e++)r[i[e]]&&n.push(i[e]);t._attach(["yui-base"]),t._attach(n),t.Loader&&E(t)},applyTo:function(e,t,n){if(t in f){var r=v[e],i,s,o;if(r){i=t.split("."),s=r;for(o=0;o<i.length;o+=1)s=s[i[o]],s||this.log("applyTo not found: "+t,"warn","yui");return s&&s.apply(r,n)}return null}return this.log(t+": applyTo not allowed","warn","yui"),null},add:function(e,t,n,r){r=r||{};var i=YUI.Env,s={name:e,fn:t,version:n,details:r},o={},u,a,f,l,c=i.versions;i.mods[e]=s,c[n]=c[n]||{},c[n][e]=s;for(l in v)v.hasOwnProperty(l)&&(a=v[l],o[a.id]||(o[a.id]=!0,u=a.Env._loader,u&&(f=u.getModuleInfo(e),(!f||f.temp)&&u.addModule(r,e))));return this},_attach:function(e,t){var n,r,i,s,o,u,a,f=YUI.Env.mods,l=YUI.Env.aliases,c=this,h,p=YUI.Env._renderedMods,d=c.Env._loader,v=c.Env._attached,m=c.Env._exported,g=e.length,d,y,b,w=[],E,S,x,T,N,C,k;for(n=0;n<g;n++){r=e[n],i=f[r],w.push(r);if(d&&d.conditions[r])for(h in d.conditions[r])d.conditions[r].hasOwnProperty(h)&&(y=d.conditions[r][h],b=y&&(y.ua&&c.UA[y.ua]||y.test&&y.test(c)),b&&w.push(y.name))}e=w,g=e.length;for(n=0;n<g;n++)if(!v[e[n]]){r=e[n],i=f[r];if(l&&l[r]&&!i){c._attach(l[r]);continue}if(!i)T=d&&d.getModuleInfo(r),T&&(i=T,t=!0),!t&&r&&r.indexOf("skin-")===-1&&r.indexOf("css")===-1&&(c.Env._missed.push(r),c.Env._missed=c.Array.dedupe(c.Env._missed),c.message("NOT loaded: "+r,"warn","yui"));else{v[r]=!0;for(h=0;h<c.Env._missed.length;h++)c.Env._missed[h]===r&&(c.message("Found: "+r+" (was reported as missing earlier)","warn","yui"),c.Env._missed.splice(h,1));if(d&&!d._canBeAttached(r))return!0;if(d&&p&&p[r]&&p[r].temp){d.getRequires(p[r]),o=[],T=d.getModuleInfo(r);for(h in T.expanded_map)T.expanded_map.hasOwnProperty(h)&&o.push(h);c._attach(o)}s=i.details,o=s.requires,S=s.es,u=s.use,a=s.after,s.lang&&(o=o||[],o.unshift("intl"));if(o){x=o.length;for(h=0;h<x;h++)if(!v[o[h]]){if(!c._attach(o))return!1;break}}if(a)for(h=0;h<a.length;h++)if(!v[a[h]]){if(!c._attach(a,!0))return!1;break}if(i.fn){E=[c,r];if(S){k={},C={},E.push(k,C);if(o){x=o.length;for(h=0;h<x;h++)k[o[h]]=m.hasOwnProperty(o[h])?m[o[h]]:c}}if(c.config
+.throwFail)C=i.fn.apply(S?undefined:i,E);else try{C=i.fn.apply(S?undefined:i,E)}catch(L){return c.error("Attach error: "+r,L,r),!1}S&&(m[r]=C,N=i.details.condition,N&&N.when==="instead"&&(m[N.trigger]=C))}if(u)for(h=0;h<u.length;h++)if(!v[u[h]]){if(!c._attach(u))return!1;break}}}return!0},_delayCallback:function(e,t){var n=this,r=["event-base"];return t=n.Lang.isObject(t)?t:{event:t},t.event==="load"&&r.push("event-synthetic"),function(){var i=arguments;n._use(r,function(){n.on(t.event,function(){i[1].delayUntil=t.event,e.apply(n,i)},t.args)})}},use:function(){var e=a.call(arguments,0),t=e[e.length-1],n=this,r=0,i,s=n.Env,o=!0;n.Lang.isFunction(t)?(e.pop(),n.config.delayUntil&&(t=n._delayCallback(t,n.config.delayUntil))):t=null,n.Lang.isArray(e[0])&&(e=e[0]);if(n.config.cacheUse){while(i=e[r++])if(!s._attached[i]){o=!1;break}if(o)return e.length,n._notify(t,x,e),n}return n._loading?(n._useQueue=n._useQueue||new n.Queue,n._useQueue.add([e,t])):n._use(e,function(n,r){n._notify(t,r,e)}),n},require:function(){var e=a.call(arguments),t;typeof e[e.length-1]=="function"&&(t=e.pop(),e.push(function(n){var r,i=e.length,s=n.Env._exported,o={};for(r=0;r<i;r++)s.hasOwnProperty(e[r])&&(o[e[r]]=s[e[r]]);t.call(undefined,n,o)})),this.use.apply(this,e)},_notify:function(e,t,n){if(!t.success&&this.config.loadErrorFn)this.config.loadErrorFn.call(this,this,e,t,n);else if(e){this.Env._missed&&this.Env._missed.length&&(t.msg="Missing modules: "+this.Env._missed.join(),t.success=!1);if(this.config.throwFail)e(this,t);else try{e(this,t)}catch(r){this.error("use callback error",r,n)}}},_use:function(e,t){this.Array||this._attach(["yui-base"]);var r,i,s,o=this,u=YUI.Env,a=u.mods,f=o.Env,l=f._used,c=u.aliases,h=u._loaderQueue,p=e[0],d=o.Array,v=o.config,m=v.bootstrap,g=[],y,b=[],w=!0,S=v.fetchCSS,x=function(e,t){var r=0,i=[],s,o,f,h,p;if(!e.length)return;if(c){o=e.length;for(r=0;r<o;r++)c[e[r]]&&!a[e[r]]?i=[].concat(i,c[e[r]]):i.push(e[r]);e=i}o=e.length;for(r=0;r<o;r++){s=e[r],t||b.push(s);if(l[s])continue;f=a[s],h=null,p=null,f?(l[s]=!0,h=f.details.requires,p=f.details.use):u._loaded[n][s]?l[s]=!0:g.push(s),h&&h.length&&x(h),p&&p.length&&x(p,1)}},T=function(n){var r=n||{success:!0,msg:"not dynamic"},i,s,u=!0,a=r.data;o._loading=!1,a&&(s=g,g=[],b=[],x(a),i=g.length,i&&[].concat(g).sort().join()==s.sort().join()&&(i=!1)),i&&a?(o._loading=!0,o._use(g,function(){o._attach(a)&&o._notify(t,r,a)})):(a&&(u=o._attach(a)),u&&o._notify(t,r,e)),o._useQueue&&o._useQueue.size()&&!o._loading&&o._use.apply(o,o._useQueue.next())};if(p==="*"){e=[];for(y in a)a.hasOwnProperty(y)&&e.push(y);return w=o._attach(e),w&&T(),o}return(a.loader||a["loader-base"])&&!o.Loader&&o._attach(["loader"+(a.loader?"":"-base")]),m&&o.Loader&&e.length&&(i=E(o),i.require(e),i.ignoreRegistered=!0,i._boot=!0,i.calculate(null,S?null:"js"),e=i.sorted,i._boot=!1),x(e),r=g.length,r&&(g=d.dedupe(g),r=g.length),m&&r&&o.Loader?(o._loading=!0,i=E(o),i.onEnd=T,i.context=o,i.data=e,i.ignoreRegistered=!1,i.require(g),i.insert(null,S?null:"js")):m&&r&&o.Get&&!f.bootstrapped?(o._loading=!0,s=function(){o._loading=!1,h.running=!1,f.bootstrapped=!0,u._bootstrapping=!1,o._attach(["loader"])&&o._use(e,t)},u._bootstrapping?h.add(s):(u._bootstrapping=!0,o.Get.script(v.base+v.loaderPath,{onEnd:s}))):(w=o._attach(e),w&&T()),o},namespace:function(){var e=arguments,t,n=0,i,s,o;for(;n<e.length;n++){t=this,o=e[n];if(o.indexOf(r)>-1){s=o.split(r);for(i=s[0]=="YAHOO"?1:0;i<s.length;i++)t[s[i]]=t[s[i]]||{},t=t[s[i]]}else t[o]=t[o]||{},t=t[o]}return t},log:u,message:u,dump:function(e){return""+e},error:function(e,t,n){var r=this,i;r.config.errorFn&&(i=r.config.errorFn.apply(r,arguments));if(!i)throw t||new Error(e);return r.message(e,"error",""+n),r},guid:function(e){var t=this.Env._guidp+"_"+ ++this.Env._uidx;return e?e+t:t},stamp:function(e,t){var n;if(!e)return e;e.uniqueID&&e.nodeType&&e.nodeType!==9?n=e.uniqueID:n=typeof e=="string"?e:e._yuid;if(!n){n=this.guid();if(!t)try{e._yuid=n}catch(r){n=null}}return n},destroy:function(){var e=this;e.Event&&e.Event._unload(),delete v[e.id],delete e.Env,delete e.config}},YUI.prototype=e;for(t in e)e.hasOwnProperty(t)&&(YUI[t]=e[t]);YUI.applyConfig=function(e){if(!e)return;YUI.GlobalConfig&&this.prototype.applyConfig.call(this,YUI.GlobalConfig),this.prototype.applyConfig.call(this,e),YUI.GlobalConfig=this.config},YUI._init(),l?(g(h,"DOMContentLoaded",b),g(window,"load",w)):(b(),w()),YUI.Env.add=g,YUI.Env.remove=y,typeof exports=="object"&&(exports.YUI=YUI,YUI.setLoadHook=function(e){YUI._getLoadHook=e},YUI._getLoadHook=null),YUI.Env[n]={}})(),YUI.add("yui-base",function(e,t){function m(e,t,n){var r,i;t||(t=0);if(n||m.test(e))try{return d.slice.call(e,t)}catch(s){i=[];for(r=e.length;t<r;++t)i.push(e[t]);return i}return[e]}function g(){this._init(),this.add.apply(this,arguments)}var n=e.Lang||(e.Lang={}),r=String.prototype,i=Object.prototype.toString,s={"undefined":"undefined",number:"number","boolean":"boolean",string:"string","[object Function]":"function","[object RegExp]":"regexp","[object Array]":"array","[object Date]":"date","[object Error]":"error"},o=/\{\s*([^|}]+?)\s*(?:\|([^}]*))?\s*\}/g,u=" \n \f\r \u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff",a="[ -\r \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+",f=new RegExp("^"+a),l=new RegExp(a+"$"),c=new RegExp(f.source+"|"+l.source,"g"),h=/\{\s*\[(?:native code|function)\]\s*\}/i;n._isNative=function(t){return!!(e.config.useNativeES5&&t&&h.test(t))},n.isArray=n._isNative(Array.isArray)?Array.isArray:function(e){return n.type(e)==="array"},n.isBoolean=function(e){return typeof e=="boolean"},n.isDate=function(e){return n.type(e)==="date"&&e.toString()!=="Invalid Date"&&!isNaN(e)},n.isFunction=function(e){return n.type(e)==="function"},n.isNull=function(e){return e===null},n.isNumber=function(e){return typeof e=="number"&&isFinite(e)},n.isObject=function(e,t){var r=typeof e;return e&&(r==="object"||!
+t&&(r==="function"||n.isFunction(e)))||!1},n.isRegExp=function(e){return n.type(e)==="regexp"},n.isString=function(e){return typeof e=="string"},n.isUndefined=function(e){return typeof e=="undefined"},n.isValue=function(e){var t=n.type(e);switch(t){case"number":return isFinite(e);case"null":case"undefined":return!1;default:return!!t}},n.now=Date.now||function(){return(new Date).getTime()},n.sub=function(e,t){return e.replace?e.replace(o,function(e,r){return n.isUndefined(t[r])?e:t[r]}):e},n.trim=n._isNative(r.trim)&&!u.trim()?function(e){return e&&e.trim?e.trim():e}:function(e){try{return e.replace(c,"")}catch(t){return e}},n.trimLeft=n._isNative(r.trimLeft)&&!u.trimLeft()?function(e){return e.trimLeft()}:function(e){return e.replace(f,"")},n.trimRight=n._isNative(r.trimRight)&&!u.trimRight()?function(e){return e.trimRight()}:function(e){return e.replace(l,"")},n.type=function(e){return s[typeof e]||s[i.call(e)]||(e?"object":"null")};var p=e.Lang,d=Array.prototype,v=Object.prototype.hasOwnProperty;e.Array=m,m.dedupe=p._isNative(Object.create)?function(e){var t=Object.create(null),n=[],r,i,s;for(r=0,s=e.length;r<s;++r)i=e[r],t[i]||(t[i]=1,n.push(i));return n}:function(e){var t={},n=[],r,i,s;for(r=0,s=e.length;r<s;++r)i=e[r],v.call(t,i)||(t[i]=1,n.push(i));return n},m.each=m.forEach=p._isNative(d.forEach)?function(t,n,r){return d.forEach.call(t||[],n,r||e),e}:function(t,n,r){for(var i=0,s=t&&t.length||0;i<s;++i)i in t&&n.call(r||e,t[i],i,t);return e},m.hash=function(e,t){var n={},r=t&&t.length||0,i,s;for(i=0,s=e.length;i<s;++i)i in e&&(n[e[i]]=r>i&&i in t?t[i]:!0);return n},m.indexOf=p._isNative(d.indexOf)?function(e,t,n){return d.indexOf.call(e,t,n)}:function(e,t,n){var r=e.length;n=+n||0,n=(n>0||-1)*Math.floor(Math.abs(n)),n<0&&(n+=r,n<0&&(n=0));for(;n<r;++n)if(n in e&&e[n]===t)return n;return-1},m.numericSort=function(e,t){return e-t},m.some=p._isNative(d.some)?function(e,t,n){return d.some.call(e,t,n)}:function(e,t,n){for(var r=0,i=e.length;r<i;++r)if(r in e&&t.call(n,e[r],r,e))return!0;return!1},m.test=function(e){var t=0;if(p.isArray(e))t=1;else if(p.isObject(e))try{"length"in e&&!e.tagName&&(!e.scrollTo||!e.document)&&!e.apply&&(t=2)}catch(n){}return t},g.prototype={_init:function(){this._q=[]},next:function(){return this._q.shift()},last:function(){return this._q.pop()},add:function(){return this._q.push.apply(this._q,arguments),this},size:function(){return this._q.length}},e.Queue=g,YUI.Env._loaderQueue=YUI.Env._loaderQueue||new g;var y="__",v=Object.prototype.hasOwnProperty,b=e.Lang.isObject;e.cached=function(e,t,n){return t||(t={}),function(r){var i=arguments.length>1?Array.prototype.join.call(arguments,y):String(r);if(!(i in t)||n&&t[i]==n)t[i]=e.apply(e,arguments);return t[i]}},e.getLocation=function(){var t=e.config.win;return t&&t.location},e.merge=function(){var e=0,t=arguments.length,n={},r,i;for(;e<t;++e){i=arguments[e];for(r in i)v.call(i,r)&&(n[r]=i[r])}return n},e.mix=function(t,n,r,i,s,o){var u,a,f,l,c,h,p;if(!t||!n)return t||e;if(s){s===2&&e.mix(t.prototype,n.prototype,r,i,0,o),f=s===1||s===3?n.prototype:n,p=s===1||s===4?t.prototype:t;if(!f||!p)return t}else f=n,p=t;u=r&&!o;if(i)for(l=0,h=i.length;l<h;++l){c=i[l];if(!v.call(f,c))continue;a=u?!1:c in p;if(o&&a&&b(p[c],!0)&&b(f[c],!0))e.mix(p[c],f[c],r,null,0,o);else if(r||!a)p[c]=f[c]}else{for(c in f){if(!v.call(f,c))continue;a=u?!1:c in p;if(o&&a&&b(p[c],!0)&&b(f[c],!0))e.mix(p[c],f[c],r,null,0,o);else if(r||!a)p[c]=f[c]}e.Object._hasEnumBug&&e.mix(p,f,r,e.Object._forceEnum,s,o)}return t};var p=e.Lang,v=Object.prototype.hasOwnProperty,w,E=e.Object=p._isNative(Object.create)?function(e){return Object.create(e)}:function(){function e(){}return function(t){return e.prototype=t,new e}}(),S=E._forceEnum=["hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toString","toLocaleString","valueOf"],x=E._hasEnumBug=!{valueOf:0}.propertyIsEnumerable("valueOf"),T=E._hasProtoEnumBug=function(){}.propertyIsEnumerable("prototype"),N=E.owns=function(e,t){return!!e&&v.call(e,t)};E.hasKey=N,E.keys=p._isNative(Object.keys)&&!T?Object.keys:function(e){if(!p.isObject(e))throw new TypeError("Object.keys called on a non-object");var t=[],n,r,i;if(T&&typeof e=="function")for(r in e)N(e,r)&&r!=="prototype"&&t.push(r);else for(r in e)N(e,r)&&t.push(r);if(x)for(n=0,i=S.length;n<i;++n)r=S[n],N(e,r)&&t.push(r);return t},E.values=function(e){var t=E.keys(e),n=0,r=t.length,i=[];for(;n<r;++n)i.push(e[t[n]]);return i},E.size=function(e){try{return E.keys(e).length}catch(t){return 0}},E.hasValue=function(t,n){return e.Array.indexOf(E.values(t),n)>-1},E.each=function(t,n,r,i){var s;for(s in t)(i||N(t,s))&&n.call(r||e,t[s],s,t);return e},E.some=function(t,n,r,i){var s;for(s in t)if(i||N(t,s))if(n.call(r||e,t[s],s,t))return!0;return!1},E.getValue=function(t,n){if(!p.isObject(t))return w;var r,i=e.Array(n),s=i.length;for(r=0;t!==w&&r<s;r++)t=t[i[r]];return t},E.setValue=function(t,n,r){var i,s=e.Array(n),o=s.length-1,u=t;if(o>=0){for(i=0;u!==w&&i<o;i++)u=u[s[i]];if(u===w)return w;u[s[i]]=r}return t},E.isEmpty=function(e){return!E.keys(Object(e)).length},YUI.Env.parseUA=function(t){var n=function(e){var t=0;return parseFloat(e.replace(/\./g,function(){return t++===1?"":"."}))},r=e.config.win,i=r&&r.navigator,s={ie:0,opera:0,gecko:0,webkit:0,safari:0,chrome:0,mobile:null,air:0,phantomjs:0,ipad:0,iphone:0,ipod:0,ios:null,android:0,silk:0,ubuntu:0,accel:!1,webos:0,caja:i&&i.cajaVersion,secure:!1,os:null,nodejs:0,winjs:typeof Windows!="undefined"&&!!Windows.System,touchEnabled:!1},o=t||i&&i.userAgent,u=r&&r.location,a=u&&u.href,f;return s.userAgent=o,s.secure=a&&a.toLowerCase().indexOf("https")===0,o&&(/windows|win32/i.test(o)?s.os="windows":/macintosh|mac_powerpc/i.test(o)?s.os="macintosh":/android/i.test(o)?s.os="android":/symbos/i.test(o)?s.os="symbos":/linux/i.test(o)?s.os="linux":/rhino/i.test(o)&&(s.os="rhino"),/KHTML/.test(o)&&(s.webkit=1),/IEMobile|XBLWP7/.test(o)&&(s.mobile="windows"),/Fennec/.test(o)&&(s.mobile="gecko"),f=o.match(/AppleWebKit\/([^\s]*)/
+),f&&f[1]&&(s.webkit=n(f[1]),s.safari=s.webkit,/PhantomJS/.test(o)&&(f=o.match(/PhantomJS\/([^\s]*)/),f&&f[1]&&(s.phantomjs=n(f[1]))),/ Mobile\//.test(o)||/iPad|iPod|iPhone/.test(o)?(s.mobile="Apple",f=o.match(/OS ([^\s]*)/),f&&f[1]&&(f=n(f[1].replace("_","."))),s.ios=f,s.os="ios",s.ipad=s.ipod=s.iphone=0,f=o.match(/iPad|iPod|iPhone/),f&&f[0]&&(s[f[0].toLowerCase()]=s.ios)):(f=o.match(/NokiaN[^\/]*|webOS\/\d\.\d/),f&&(s.mobile=f[0]),/webOS/.test(o)&&(s.mobile="WebOS",f=o.match(/webOS\/([^\s]*);/),f&&f[1]&&(s.webos=n(f[1]))),/ Android/.test(o)&&(/Mobile/.test(o)&&(s.mobile="Android"),f=o.match(/Android ([^\s]*);/),f&&f[1]&&(s.android=n(f[1]))),/Silk/.test(o)&&(f=o.match(/Silk\/([^\s]*)/),f&&f[1]&&(s.silk=n(f[1])),s.android||(s.android=2.34,s.os="Android"),/Accelerated=true/.test(o)&&(s.accel=!0))),f=o.match(/OPR\/(\d+\.\d+)/),f&&f[1]?s.opera=n(f[1]):(f=o.match(/(Chrome|CrMo|CriOS)\/([^\s]*)/),f&&f[1]&&f[2]?(s.chrome=n(f[2]),s.safari=0,f[1]==="CrMo"&&(s.mobile="chrome")):(f=o.match(/AdobeAIR\/([^\s]*)/),f&&(s.air=f[0])))),f=o.match(/Ubuntu\ (\d+\.\d+)/),f&&f[1]&&(s.os="linux",s.ubuntu=n(f[1]),f=o.match(/\ WebKit\/([^\s]*)/),f&&f[1]&&(s.webkit=n(f[1])),f=o.match(/\ Chromium\/([^\s]*)/),f&&f[1]&&(s.chrome=n(f[1])),/ Mobile$/.test(o)&&(s.mobile="Ubuntu")),s.webkit||(/Opera/.test(o)?(f=o.match(/Opera[\s\/]([^\s]*)/),f&&f[1]&&(s.opera=n(f[1])),f=o.match(/Version\/([^\s]*)/),f&&f[1]&&(s.opera=n(f[1])),/Opera Mobi/.test(o)&&(s.mobile="opera",f=o.replace("Opera Mobi","").match(/Opera ([^\s]*)/),f&&f[1]&&(s.opera=n(f[1]))),f=o.match(/Opera Mini[^;]*/),f&&(s.mobile=f[0])):(f=o.match(/MSIE ([^;]*)|Trident.*; rv:([0-9.]+)/),f&&(f[1]||f[2])?s.ie=n(f[1]||f[2]):(f=o.match(/Gecko\/([^\s]*)/),f&&(s.gecko=1,f=o.match(/rv:([^\s\)]*)/),f&&f[1]&&(s.gecko=n(f[1]),/Mobile|Tablet/.test(o)&&(s.mobile="ffos"))))))),r&&i&&!(s.chrome&&s.chrome<6)&&(s.touchEnabled="ontouchstart"in r||"msMaxTouchPoints"in i&&i.msMaxTouchPoints>0),t||(typeof process=="object"&&process.versions&&process.versions.node&&(s.os=process.platform,s.nodejs=n(process.versions.node)),YUI.Env.UA=s),s},e.UA=YUI.Env.UA||YUI.Env.parseUA(),e.UA.compareVersions=function(e,t){var n,r,i,s,o,u;if(e===t)return 0;r=(e+"").split("."),s=(t+"").split(".");for(o=0,u=Math.max(r.length,s.length);o<u;++o){n=parseInt(r[o],10),i=parseInt(s[o],10),isNaN(n)&&(n=0),isNaN(i)&&(i=0);if(n<i)return-1;if(n>i)return 1}return 0},YUI.Env.aliases={anim:["anim-base","anim-color","anim-curve","anim-easing","anim-node-plugin","anim-scroll","anim-xy"],"anim-shape-transform":["anim-shape"],app:["app-base","app-content","app-transitions","lazy-model-list","model","model-list","model-sync-rest","model-sync-local","router","view","view-node-map"],attribute:["attribute-base","attribute-complex"],"attribute-events":["attribute-observable"],autocomplete:["autocomplete-base","autocomplete-sources","autocomplete-list","autocomplete-plugin"],axes:["axis-numeric","axis-category","axis-time","axis-stacked"],"axes-base":["axis-numeric-base","axis-category-base","axis-time-base","axis-stacked-base"],base:["base-base","base-pluginhost","base-build"],cache:["cache-base","cache-offline","cache-plugin"],charts:["charts-base"],collection:["array-extras","arraylist","arraylist-add","arraylist-filter","array-invoke"],color:["color-base","color-hsl","color-harmony"],controller:["router"],dataschema:["dataschema-base","dataschema-json","dataschema-xml","dataschema-array","dataschema-text"],datasource:["datasource-local","datasource-io","datasource-get","datasource-function","datasource-cache","datasource-jsonschema","datasource-xmlschema","datasource-arrayschema","datasource-textschema","datasource-polling"],datatable:["datatable-core","datatable-table","datatable-head","datatable-body","datatable-base","datatable-column-widths","datatable-message","datatable-mutable","datatable-sort","datatable-datasource"],datatype:["datatype-date","datatype-number","datatype-xml"],"datatype-date":["datatype-date-parse","datatype-date-format","datatype-date-math"],"datatype-number":["datatype-number-parse","datatype-number-format"],"datatype-xml":["datatype-xml-parse","datatype-xml-format"],dd:["dd-ddm-base","dd-ddm","dd-ddm-drop","dd-drag","dd-proxy","dd-constrain","dd-drop","dd-scroll","dd-delegate"],dom:["dom-base","dom-screen","dom-style","selector-native","selector"],editor:["frame","editor-selection","exec-command","editor-base","editor-para","editor-br","editor-bidi","editor-tab","createlink-base"],event:["event-base","event-delegate","event-synthetic","event-mousewheel","event-mouseenter","event-key","event-focus","event-resize","event-hover","event-outside","event-touch","event-move","event-flick","event-valuechange","event-tap"],"event-custom":["event-custom-base","event-custom-complex"],"event-gestures":["event-flick","event-move"],handlebars:["handlebars-compiler"],highlight:["highlight-base","highlight-accentfold"],history:["history-base","history-hash","history-html5"],io:["io-base","io-xdr","io-form","io-upload-iframe","io-queue"],json:["json-parse","json-stringify"],loader:["loader-base","loader-rollup","loader-yui3"],node:["node-base","node-event-delegate","node-pluginhost","node-screen","node-style"],pluginhost:["pluginhost-base","pluginhost-config"],querystring:["querystring-parse","querystring-stringify"],recordset:["recordset-base","recordset-sort","recordset-filter","recordset-indexer"],resize:["resize-base","resize-proxy","resize-constrain"],slider:["slider-base","slider-value-range","clickable-rail","range-slider"],template:["template-base","template-micro"],text:["text-accentfold","text-wordbreak"],widget:["widget-base","widget-htmlparser","widget-skin","widget-uievents"]}},"3.17.2",{use:["yui-base","get","features","intl-base","yui-log","yui-log-nodejs","yui-later","loader-base","loader-rollup","loader-yui3"]}),YUI.add("get",function(e,t){var n=require("module"),r=require("path"),i=require("fs"),s=require("request"),o=function(t,n,r){e.Lang.isFunction(t.onEnd)&&t.onEnd.call(e,n,r)},u=function(t
+){e.Lang.isFunction(t.onSuccess)&&t.onSuccess.call(e,t),o(t,"success","success")},a=function(t,n){n.errors=[n],e.Lang.isFunction(t.onFailure)&&t.onFailure.call(e,n,t),o(t,n,"fail")};e.Get=function(){},e.config.base=r.join(__dirname,"../"),YUI.require=require,YUI.process=process,e.Get._exec=function(e,t,i){e.charCodeAt(0)===65279&&(e=e.slice(1));var s=new n(t,module);s.filename=t,s.paths=n._nodeModulePaths(r.dirname(t)),typeof YUI._getLoadHook=="function"&&(e=YUI._getLoadHook(e,t)),s._compile("module.exports = function (YUI) {return (function () {"+e+"\n;return YUI;}).apply(global);"+"};",t),YUI=s.exports(YUI),s.loaded=!0,i(null,t)},e.Get._include=function(t,r){var o,u,a=this;if(t.match(/^https?:\/\//))o={url:t,timeout:a.timeout},s(o,function(n,i,s){n?r(n,t):e.Get._exec(s,t,r)});else{try{t=n._findPath(t,n._resolveLookupPaths(t,module.parent.parent)[1]);if(!e.config.useSync){i.readFile(t,"utf8",function(n,i){n?r(n,t):e.Get._exec(i,t,r)});return}u=i.readFileSync(t,"utf8")}catch(f){r(f,t);return}e.Get._exec(u,t,r)}},e.Get.js=function(t,n){var r=e.Array(t),i,s,o=r.length,f=0,l=function(){f===o&&u(n)};for(s=0;s<o;s++)i=r[s],e.Lang.isObject(i)&&(i=i.url),i=i.replace(/'/g,"%27"),e.Get._include(i,function(t,r){e.config||(e.config={debug:!0}),n.onProgress&&n.onProgress.call(n.context||e,r),t?a(n,t):(f++,l())});return{execute:function(){}}},e.Get.script=e.Get.js,e.Get.css=function(e,t){u(t)}},"3.17.2"),YUI.add("features",function(e,t){var n={};e.mix(e.namespace("Features"),{tests:n,add:function(e,t,r){n[e]=n[e]||{},n[e][t]=r},all:function(t,r){var i=n[t],s=[];return i&&e.Object.each(i,function(n,i){s.push(i+":"+(e.Features.test(t,i,r)?1:0))}),s.length?s.join(";"):""},test:function(t,r,i){i=i||[];var s,o,u,a=n[t],f=a&&a[r];return!f||(s=f.result,e.Lang.isUndefined(s)&&(o=f.ua,o&&(s=e.UA[o]),u=f.test,u&&(!o||s)&&(s=u.apply(e,i)),f.result=s)),s}});var r=e.Features.add;r("load","0",{name:"app-transitions-native",test:function(e){var t=e.config.doc,n=t?t.documentElement:null;return n&&n.style?"MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style:!1},trigger:"app-transitions"}),r("load","1",{name:"autocomplete-list-keys",test:function(e){return!e.UA.ios&&!e.UA.android},trigger:"autocomplete-list"}),r("load","2",{name:"dd-gestures",trigger:"dd-drag",ua:"touchEnabled"}),r("load","3",{name:"dom-style-ie",test:function(e){var t=e.Features.test,n=e.Features.add,r=e.config.win,i=e.config.doc,s="documentElement",o=!1;return n("style","computedStyle",{test:function(){return r&&"getComputedStyle"in r}}),n("style","opacity",{test:function(){return i&&"opacity"in i[s].style}}),o=!t("style","opacity")&&!t("style","computedStyle"),o},trigger:"dom-style"}),r("load","4",{name:"editor-para-ie",trigger:"editor-para",ua:"ie",when:"instead"}),r("load","5",{name:"event-base-ie",test:function(e){var t=e.config.doc&&e.config.doc.implementation;return t&&!t.hasFeature("Events","2.0")},trigger:"node-base"}),r("load","6",{name:"graphics-canvas",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"}),r("load","7",{name:"graphics-canvas-default",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"}),r("load","8",{name:"graphics-svg",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"}),r("load","9",{name:"graphics-svg-default",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"}),r("load","10",{name:"graphics-vml",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"}),r("load","11",{name:"graphics-vml-default",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"}),r("load","12",{name:"history-hash-ie",test:function(e){var t=e.config.doc&&e.config.doc.documentMode;return e.UA.ie&&(!("onhashchange"in e.config.win)||!t||t<8)},trigger:"history-hash"}),r("load","13",{name:"io-nodejs",trigger:"io-base",ua:"nodejs"}),r("load","14",{name:"json-parse-shim",test:function(e){function i(e,t){return e==="ok"?!0:t}var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONParse!==!1&&!!n;if(r)try{r=n.parse('{"ok":false}',i).ok}catch(s){r=!1}return!r},trigger:"json-parse"}),r("load","15",{name:"json-stringify-shim",test:function(e){var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONStringify!==!1&&!!n;if(r)try{r="0"===n.stringify(0)}catch(i){r=!1}return!r},trigger:"json-stringify"}),r("load","16",{name:"scrollview-base-ie",trigger:"scrollview-base",ua:"ie"}),r("load","17",{name:"selector-css2",test:function(e){var t=e.config.doc,n=t&&!("querySelectorAll"in t);return n},trigger:"selector"}),r("load","18",{name:"transition-timer",test:function(e){var t=e.config.doc,n=t?t.documentElement:null,r=!0;return n&&n.style&&(r=!("MozTransition"in n.style||"WebkitTransition"in
+n.style||"transition"in n.style)),r},trigger:"transition"}),r("load","19",{name:"widget-base-ie",trigger:"widget-base",ua:"ie"}),r("load","20",{name:"yql-jsonp",test:function(e){return!e.UA.nodejs&&!e.UA.winjs},trigger:"yql"}),r("load","21",{name:"yql-nodejs",trigger:"yql",ua:"nodejs"}),r("load","22",{name:"yql-winjs",trigger:"yql",ua:"winjs"})},"3.17.2",{requires:["yui-base"]}),YUI.add("intl-base",function(e,t){var n=/[, ]/;e.mix(e.namespace("Intl"),{lookupBestLang:function(t,r){function a(e){var t;for(t=0;t<r.length;t+=1)if(e.toLowerCase()===r[t].toLowerCase())return r[t]}var i,s,o,u;e.Lang.isString(t)&&(t=t.split(n));for(i=0;i<t.length;i+=1){s=t[i];if(!s||s==="*")continue;while(s.length>0){o=a(s);if(o)return o;u=s.lastIndexOf("-");if(!(u>=0))break;s=s.substring(0,u),u>=2&&s.charAt(u-2)==="-"&&(s=s.substring(0,u-2))}}return""}})},"3.17.2",{requires:["yui-base"]}),YUI.add("yui-log",function(e,t){var n=e,r="yui:log",i="undefined",s={debug:1,info:2,warn:4,error:8};n.log=function(e,t,o,u){var a,f,l,c,h,p,d=n,v=d.config,m=d.fire?d:YUI.Env.globalEvents;return v.debug&&(o=o||"",typeof o!="undefined"&&(f=v.logExclude,l=v.logInclude,!l||o in l?l&&o in l?a=!l[o]:f&&o in f&&(a=f[o]):a=1,typeof t=="undefined"&&(t="info"),d.config.logLevel=d.config.logLevel||"debug",p=s[d.config.logLevel.toLowerCase()],t in s&&s[t]<p&&(a=1)),a||(v.useBrowserConsole&&(c=o?o+": "+e:e,d.Lang.isFunction(v.logFn)?v.logFn.call(d,e,t,o):typeof console!==i&&console.log?(h=t&&console[t]&&t in s?t:"log",console[h](c)):typeof opera!==i&&opera.postError(c)),m&&!u&&(m===d&&!m.getEvent(r)&&m.publish(r,{broadcast:2}),m.fire(r,{msg:e,cat:t,src:o})))),d},n.message=function(){return n.log.apply(n,arguments)}},"3.17.2",{requires:["yui-base"]}),YUI.add("yui-log-nodejs",function(e,t){var n=require(process.binding("natives").util?"util":"sys"),r=!1;try{var i=require("stdio");r=i.isStderrATTY()}catch(s){r=!0}e.config.useColor=r,e.consoleColor=function(e,t){return this.config.useColor?(t||(t="32"),"["+t+"m"+e+""):e};var o=function(e,t,r){var i="",s,o;this.id&&(i="["+this.id+"]:"),t=t||"info",r=r?this.consoleColor(" ("+r.toLowerCase()+"):",35):"",e===null&&(e="null");if(typeof e=="object"||e instanceof Array)try{e.tagName||e._yuid||e._query?e=e.toString():e=n.inspect(e)}catch(u){}s="37;40",o=e?"":31,t+="";switch(t.toLowerCase()){case"error":s=o=31;break;case"warn":s=33;break;case"debug":s=34}typeof e=="string"&&e&&e.indexOf("\n")!==-1&&(e="\n"+e),n.error(this.consoleColor(t.toLowerCase()+":",s)+r+" "+this.consoleColor(e,o))};e.config.logFn||(e.config.logFn=o)},"3.17.2"),YUI.add("yui-later",function(e,t){var n=[];e.later=function(t,r,i,s,o){t=t||0,s=e.Lang.isUndefined(s)?n:e.Array(s),r=r||e.config.win||e;var u=!1,a=r&&e.Lang.isString(i)?r[i]:i,f=function(){u||(a.apply?a.apply(r,s||n):a(s[0],s[1],s[2],s[3]))},l=o?setInterval(f,t):setTimeout(f,t);return{id:l,interval:o,cancel:function(){u=!0,this.interval?clearInterval(l):clearTimeout(l)}}},e.Lang.later=e.later},"3.17.2",{requires:["yui-base"]}),YUI.add("loader-base",function(e,t){(function(){var t=e.version,n="/build/",r=t+"/",i=e.Env.base,s="gallery-2014.05.29-15-46",o="2in3",u="4",a="2.9.0",f=i+"combo?",l={version:t,root:r,base:e.Env.base,comboBase:f,skin:{defaultSkin:"sam",base:"assets/skins/",path:"skin.css",after:["cssreset","cssfonts","cssgrids","cssbase","cssreset-context","cssfonts-context"]},groups:{},patterns:{}},c=l.groups,h=function(e,t,r){var s=o+"."+(e||u)+"/"+(t||a)+n,l=r&&r.base?r.base:i,h=r&&r.comboBase?r.comboBase:f;c.yui2.base=l+s,c.yui2.root=s,c.yui2.comboBase=h},p=function(e,t){var r=(e||s)+n,o=t&&t.base?t.base:i,u=t&&t.comboBase?t.comboBase:f;c.gallery.base=o+r,c.gallery.root=r,c.gallery.comboBase=u};c[t]={},c.gallery={ext:!1,combine:!0,comboBase:f,update:p,patterns:{"gallery-":{},"lang/gallery-":{},"gallerycss-":{type:"css"}}},c.yui2={combine:!0,ext:!1,comboBase:f,update:h,patterns:{"yui2-":{configFn:function(e){/-skin|reset|fonts|grids|base/.test(e.name)&&(e.type="css",e.path=e.path.replace(/\.js/,".css"),e.path=e.path.replace(/\/yui2-skin/,"/assets/skins/sam/yui2-skin"))}}}},p(),h(),YUI.Env[t]&&e.mix(l,YUI.Env[t],!1,["modules","groups","skin"],0,!0),YUI.Env[t]=l})();var n={},r=[],i=1024,s=YUI.Env,o=s._loaded,u="css",a="js",f="intl",l="sam",c=e.version,h="",p=e.Object,d=p.each,v=e.Array,m=s._loaderQueue,g=s[c],y="skin-",b=e.Lang,w=s.mods,E,S=function(e,t,n,r){var i=e+"/"+t;return r||(i+="-min"),i+="."+(n||u),i};YUI.Env._cssLoaded||(YUI.Env._cssLoaded={}),e.Env.meta=g,e.Loader=function(t){var n=this;t=t||{},E=g.md5,n.context=e,t.doBeforeLoader&&t.doBeforeLoader.apply(n,arguments),n.base=e.Env.meta.base+e.Env.meta.root,n.comboBase=e.Env.meta.comboBase,n.combine=t.base&&t.base.indexOf(n.comboBase.substr(0,20))>-1,n.comboSep="&",n.maxURLLength=i,n.ignoreRegistered=t.ignoreRegistered,n.root=e.Env.meta.root,n.timeout=0,n.forceMap={},n.allowRollup=!1,n.filters={},n.required={},n.patterns={},n.moduleInfo={},n.groups=e.merge(e.Env.meta.groups),n.skin=e.merge(e.Env.meta.skin),n.conditions={},n.config=t,n._internal=!0,n._populateConditionsCache(),n.loaded=o[c],n.async=!0,n._inspectPage(),n._internal=!1,n._config(t),n.forceMap=n.force?e.Array.hash(n.force):{},n.testresults=null,e.config.tests&&(n.testresults=e.config.tests),n.sorted=[],n.dirty=!0,n.inserted={},n.skipped={},n.tested={},n.ignoreRegistered&&n._resetModules()},e.Loader.prototype={getModuleInfo:function(t){var n=this.moduleInfo[t],r,i,o,a;return n?n:(r=g.modules,i=s._renderedMods,o=this._internal,i&&i.hasOwnProperty(t)&&!this.ignoreRegistered?this.moduleInfo[t]=e.merge(i[t]):r.hasOwnProperty(t)&&(this._internal=!0,a=this.addModule(r[t],t),a&&a.type===u&&this.isCSSLoaded(a.name,!0)&&(this.loaded[a.name]=!0),this._internal=o),this.moduleInfo[t])},_expandAliases:function(t){var n=[],r=YUI.Env.aliases,i,s;t=e.Array(t);for(i=0;i<t.length;i+=1)s=t[i],n.push.apply(n,r[s]?r[s]:[s]);return n},_populateConditionsCache:function(){var t=g.modules,n=s._conditions,r,i,o,u;if(n&&!this.ignoreRegistered)for(r in n)n.hasOwnProperty
+(r)&&(this.conditions[r]=e.merge(n[r]));else{for(r in t)if(t.hasOwnProperty(r)&&t[r].condition){o=this._expandAliases(t[r].condition.trigger);for(i=0;i<o.length;i+=1)u=o[i],this.conditions[u]=this.conditions[u]||{},this.conditions[u][t[r].name||r]=t[r].condition}s._conditions=this.conditions}},_resetModules:function(){var e=this,t,n,r,i,s;for(t in e.moduleInfo)if(e.moduleInfo.hasOwnProperty(t)&&e.moduleInfo[t]){r=e.moduleInfo[t],i=r.name,s=YUI.Env.mods[i]?YUI.Env.mods[i].details:null,s&&(e.moduleInfo[i]._reset=!0,e.moduleInfo[i].requires=s.requires||[],e.moduleInfo[i].optional=s.optional||[],e.moduleInfo[i].supersedes=s.supercedes||[]);if(r.defaults)for(n in r.defaults)r.defaults.hasOwnProperty(n)&&r[n]&&(r[n]=r.defaults[n]);r.langCache=undefined,r.skinCache=undefined,r.skinnable&&e._addSkin(e.skin.defaultSkin,r.name)}},REGEX_CSS:/\.css(?:[?;].*)?$/i,FILTER_DEFS:{RAW:{searchExp:"-min\\.js",replaceStr:".js"},DEBUG:{searchExp:"-min\\.js",replaceStr:"-debug.js"},COVERAGE:{searchExp:"-min\\.js",replaceStr:"-coverage.js"}},_inspectPage:function(){var e=this,t,n,r,i,s;for(s in w)w.hasOwnProperty(s)&&(t=w[s],t.details&&(n=e.getModuleInfo(t.name),r=t.details.requires,i=n&&n.requires,n?!n._inspected&&r&&i.length!==r.length&&delete n.expanded:n=e.addModule(t.details,s),n._inspected=!0))},_requires:function(e,t){var n,r,i,s,o=this.getModuleInfo(e),a=this.getModuleInfo(t);if(!o||!a)return!1;r=o.expanded_map,i=o.after_map;if(i&&t in i)return!0;i=a.after_map;if(i&&e in i)return!1;s=a.supersedes;if(s)for(n=0;n<s.length;n++)if(this._requires(e,s[n]))return!0;s=o.supersedes;if(s)for(n=0;n<s.length;n++)if(this._requires(t,s[n]))return!1;return r&&t in r?!0:o.ext&&o.type===u&&!a.ext&&a.type===u?!0:!1},_config:function(t){var n,r,i,s,o,u,a,f=this,l=[],c,h;if(t)for(n in t)if(t.hasOwnProperty(n)){i=t[n];if(n==="require")f.require(i);else if(n==="skin")typeof i=="string"&&(f.skin.defaultSkin=t.skin,i={defaultSkin:i}),e.mix(f.skin,i,!0);else if(n==="groups"){for(r in i)if(i.hasOwnProperty(r)){a=r,u=i[r],f.addGroup(u,a);if(u.aliases)for(s in u.aliases)u.aliases.hasOwnProperty(s)&&f.addAlias(u.aliases[s],s)}}else if(n==="modules")for(r in i)i.hasOwnProperty(r)&&f.addModule(i[r],r);else if(n==="aliases")for(r in i)i.hasOwnProperty(r)&&f.addAlias(i[r],r);else n==="gallery"?this.groups.gallery.update&&this.groups.gallery.update(i,t):n==="yui2"||n==="2in3"?this.groups.yui2.update&&this.groups.yui2.update(t["2in3"],t.yui2,t):f[n]=i}o=f.filter,b.isString(o)&&(o=o.toUpperCase(),f.filterName=o,f.filter=f.FILTER_DEFS[o],o==="DEBUG"&&f.require("yui-log","dump"));if(f.filterName&&f.coverage&&f.filterName==="COVERAGE"&&b.isArray(f.coverage)&&f.coverage.length){for(n=0;n<f.coverage.length;n++)c=f.coverage[n],h=f.getModuleInfo(c),h&&h.use?l=l.concat(h.use):l.push(c);f.filters=f.filters||{},e.Array.each(l,function(e){f.filters[e]=f.FILTER_DEFS.COVERAGE}),f.filterName="RAW",f.filter=f.FILTER_DEFS[f.filterName]}},formatSkin:function(e,t){var n=y+e;return t&&(n=n+"-"+t),n},_addSkin:function(e,t,n){var r,i,s,o=this.skin,u=t&&this.getModuleInfo(t),a=u&&u.ext;return t&&(i=this.formatSkin(e,t),this.getModuleInfo(i)||(r=u.pkg||t,s={skin:!0,name:i,group:u.group,type:"css",after:o.after,path:(n||r)+"/"+o.base+e+"/"+t+".css",ext:a},u.base&&(s.base=u.base),u.configFn&&(s.configFn=u.configFn),this.addModule(s,i))),i},addAlias:function(e,t){YUI.Env.aliases[t]=e,this.addModule({name:t,use:e})},addGroup:function(e,t){var n=e.modules,r=this,i,s;t=t||e.name,e.name=t,r.groups[t]=e;if(e.patterns)for(i in e.patterns)e.patterns.hasOwnProperty(i)&&(e.patterns[i].group=t,r.patterns[i]=e.patterns[i]);if(n)for(i in n)n.hasOwnProperty(i)&&(s=n[i],typeof s=="string"&&(s={name:i,fullpath:s}),s.group=t,r.addModule(s,i))},addModule:function(t,n){n=n||t.name,typeof t=="string"&&(t={name:n,fullpath:t});var r,i,o,f,l,c,p,d,m,g,y,b,w,E,x,T,N,C,k,L,A,O,M=this.moduleInfo[n],_=this.conditions,D;M&&M.temp&&(t=e.merge(M,t)),t.name=n;if(!t||!t.name)return null;t.type||(t.type=a,O=t.path||t.fullpath,O&&this.REGEX_CSS.test(O)&&(t.type=u)),!t.path&&!t.fullpath&&(t.path=S(n,n,t.type)),t.supersedes=t.supersedes||t.use,t.ext="ext"in t?t.ext:this._internal?!1:!0,r=t.submodules,this.moduleInfo[n]=t,t.requires=t.requires||[];if(this.requires)for(i=0;i<this.requires.length;i++)t.requires.push(this.requires[i]);if(t.group&&this.groups&&this.groups[t.group]){A=this.groups[t.group];if(A.requires)for(i=0;i<A.requires.length;i++)t.requires.push(A.requires[i])}t.defaults||(t.defaults={requires:t.requires?[].concat(t.requires):null,supersedes:t.supersedes?[].concat(t.supersedes):null,optional:t.optional?[].concat(t.optional):null}),t.skinnable&&t.ext&&t.temp&&(k=this._addSkin(this.skin.defaultSkin,n),t.requires.unshift(k)),t.requires.length&&(t.requires=this.filterRequires(t.requires)||[]);if(!t.langPack&&t.lang){y=v(t.lang);for(g=0;g<y.length;g++)T=y[g],b=this.getLangPackName(T,n),p=this.getModuleInfo(b),p||(p=this._addLangPack(T,t,b))}if(r){l=t.supersedes||[],o=0;for(i in r)if(r.hasOwnProperty(i)){c=r[i],c.path=c.path||S(n,i,t.type),c.pkg=n,c.group=t.group,c.supersedes&&(l=l.concat(c.supersedes)),p=this.addModule(c,i),l.push(i);if(p.skinnable){t.skinnable=!0,C=this.skin.overrides;if(C&&C[i])for(g=0;g<C[i].length;g++)k=this._addSkin(C[i][g],i,n),l.push(k);k=this._addSkin(this.skin.defaultSkin,i,n),l.push(k)}if(c.lang&&c.lang.length){y=v(c.lang);for(g=0;g<y.length;g++)T=y[g],b=this.getLangPackName(T,n),w=this.getLangPackName(T,i),p=this.getModuleInfo(b),p||(p=this._addLangPack(T,t,b)),E=E||v.hash(p.supersedes),w in E||p.supersedes.push(w),t.lang=t.lang||[],x=x||v.hash(t.lang),T in x||t.lang.push(T),b=this.getLangPackName(h,n),w=this.getLangPackName(h,i),p=this.getModuleInfo(b),p||(p=this._addLangPack(T,t,b)),w in E||p.supersedes.push(w)}o++}t.supersedes=v.dedupe(l),this.allowRollup&&(t.rollup=o<4?o:Math.min(o-1,4))}d=t.plugins;if(d)for(i in d)d.hasOwnProperty(i)&&(m=d[i],m.pkg=n,m.path=m.path||S(n,i,t.type),m.requires=m.requires||[],m.group=t.group,this.addModule(m,i),t.skinnable&&this
+._addSkin(this.skin.defaultSkin,i,n));if(t.condition){f=this._expandAliases(t.condition.trigger);for(i=0;i<f.length;i++)D=f[i],L=t.condition.when,_[D]=_[D]||{},_[D][n]=t.condition,L&&L!=="after"?L==="instead"&&(t.supersedes=t.supersedes||[],t.supersedes.push(D)):(t.after=t.after||[],t.after.push(D))}return t.supersedes&&(t.supersedes=this.filterRequires(t.supersedes)),t.after&&(t.after=this.filterRequires(t.after),t.after_map=v.hash(t.after)),t.configFn&&(N=t.configFn(t),N===!1&&(delete this.moduleInfo[n],delete s._renderedMods[n],t=null)),t&&(s._renderedMods||(s._renderedMods={}),s._renderedMods[n]=e.mix(s._renderedMods[n]||{},t),s._conditions=_),t},require:function(t){var n=typeof t=="string"?v(arguments):t;this.dirty=!0,this.required=e.merge(this.required,v.hash(this.filterRequires(n))),this._explodeRollups()},_explodeRollups:function(){var e=this,t,n,r,i,s,o,u,a=e.required;if(!e.allowRollup){for(r in a)if(a.hasOwnProperty(r)){t=e.getModule(r);if(t&&t.use){o=t.use.length;for(i=0;i<o;i++){n=e.getModule(t.use[i]);if(n&&n.use){u=n.use.length;for(s=0;s<u;s++)a[n.use[s]]=!0}else a[t.use[i]]=!0}}}e.required=a}},filterRequires:function(t){if(t){e.Lang.isArray(t)||(t=[t]),t=e.Array(t);var n=[],r,i,s,o;for(r=0;r<t.length;r++){i=this.getModule(t[r]);if(i&&i.use)for(s=0;s<i.use.length;s++)o=this.getModule(i.use[s]),o&&o.use&&o.name!==i.name?n=e.Array.dedupe([].concat(n,this.filterRequires(o.use))):n.push(i.use[s]);else n.push(t[r])}t=n}return t},_canBeAttached:function(t){return t=this.getModule(t),t&&t.test?(t.hasOwnProperty("_testResult")||(t._testResult=t.test(e)),t._testResult):!0},getRequires:function(t){if(!t)return r;if(t._parsed)return t.expanded||r;var n,i,s,o,u,a,l,c=this.testresults,m=t.name,g,y=w[m]&&w[m].details,b=t.optionalRequires,E,S,x,T,N,C,k,L,A,O,M=t.lang||t.intl,_=e.Features&&e.Features.tests.load,D,P;t.temp&&y&&(N=t,t=this.addModule(y,m),t.group=N.group,t.pkg=N.pkg,delete t.expanded),P=!!this.lang&&t.langCache!==this.lang||t.skinCache!==this.skin.defaultSkin;if(t.expanded&&!P)return t.expanded;if(b)for(n=0,o=b.length;n<o;n++)this._canBeAttached(b[n])&&t.requires.push(b[n]);E=[],D={},T=this.filterRequires(t.requires),t.lang&&(E.unshift("intl"),T.unshift("intl"),M=!0),C=this.filterRequires(t.optional),t._parsed=!0,t.langCache=this.lang,t.skinCache=this.skin.defaultSkin;for(n=0;n<T.length;n++)if(!D[T[n]]){E.push(T[n]),D[T[n]]=!0,i=this.getModule(T[n]);if(i){u=this.getRequires(i),M=M||i.expanded_map&&f in i.expanded_map;for(s=0;s<u.length;s++)E.push(u[s])}}T=this.filterRequires(t.supersedes);if(T)for(n=0;n<T.length;n++)if(!D[T[n]]){t.submodules&&E.push(T[n]),D[T[n]]=!0,i=this.getModule(T[n]);if(i){u=this.getRequires(i),M=M||i.expanded_map&&f in i.expanded_map;for(s=0;s<u.length;s++)E.push(u[s])}}if(C&&this.loadOptional)for(n=0;n<C.length;n++)if(!D[C[n]]){E.push(C[n]),D[C[n]]=!0,i=this.getModuleInfo(C[n]);if(i){u=this.getRequires(i),M=M||i.expanded_map&&f in i.expanded_map;for(s=0;s<u.length;s++)E.push(u[s])}}g=this.conditions[m];if(g){t._parsed=!1;if(c&&_)d(c,function(e,t){var n=_[t].name;!D[n]&&_[t].trigger===m&&e&&_[t]&&(D[n]=!0,E.push(n))});else for(n in g)if(g.hasOwnProperty(n)&&!D[n]){x=g[n],S=x&&(!x.ua&&!x.test||x.ua&&e.UA[x.ua]||x.test&&x.test(e,T));if(S){D[n]=!0,E.push(n),i=this.getModule(n);if(i){u=this.getRequires(i);for(s=0;s<u.length;s++)E.push(u[s])}}}}if(t.skinnable){L=this.skin.overrides;for(n in YUI.Env.aliases)YUI.Env.aliases.hasOwnProperty(n)&&e.Array.indexOf(YUI.Env.aliases[n],m)>-1&&(A=n);if(L&&(L[m]||A&&L[A])){O=m,L[A]&&(O=A);for(n=0;n<L[O].length;n++)k=this._addSkin(L[O][n],m),this.isCSSLoaded(k,this._boot)||E.push(k)}else k=this._addSkin(this.skin.defaultSkin,m),this.isCSSLoaded(k,this._boot)||E.push(k)}return t._parsed=!1,M&&(t.lang&&!t.langPack&&e.Intl&&(l=e.Intl.lookupBestLang(this.lang||h,t.lang),a=this.getLangPackName(l,m),a&&E.unshift(a)),E.unshift(f)),t.expanded_map=v.hash(E),t.expanded=p.keys(t.expanded_map),t.expanded},isCSSLoaded:function(t,n){if(!t||!YUI.Env.cssStampEl||!n&&this.ignoreRegistered)return!1;var r=YUI.Env.cssStampEl,i=!1,s=YUI.Env._cssLoaded[t],o=r.currentStyle;return s!==undefined?s:(r.className=t,o||(o=e.config.doc.defaultView.getComputedStyle(r,null)),o&&o.display==="none"&&(i=!0),r.className="",YUI.Env._cssLoaded[t]=i,i)},getProvides:function(t){var r=this.getModule(t),i,s;return r?(r&&!r.provides&&(i={},s=r.supersedes,s&&v.each(s,function(t){e.mix(i,this.getProvides(t))},this),i[t]=!0,r.provides=i),r.provides):n},calculate:function(e,t){if(e||t||this.dirty)e&&this._config(e),this._init||this._setup(),this._explode(),this.allowRollup?this._rollup():this._explodeRollups(),this._reduce(),this._sort()},_addLangPack:function(t,n,r){var i=n.name,s,o,u=this.getModuleInfo(r);return u||(s=S(n.pkg||i,r,a,!0),o={path:s,intl:!0,langPack:!0,ext:n.ext,group:n.group,supersedes:[]},n.root&&(o.root=n.root),n.base&&(o.base=n.base),n.configFn&&(o.configFn=n.configFn),this.addModule(o,r),t&&(e.Env.lang=e.Env.lang||{},e.Env.lang[t]=e.Env.lang[t]||{},e.Env.lang[t][i]=!0)),this.getModuleInfo(r)},_setup:function(){var t=this.moduleInfo,n,r,i,o,u,a;for(n in t)t.hasOwnProperty(n)&&(o=t[n],o&&(o.requires=v.dedupe(o.requires),o.lang&&(a=this.getLangPackName(h,n),this._addLangPack(null,o,a))));u={},this.ignoreRegistered||e.mix(u,s.mods),this.ignore&&e.mix(u,v.hash(this.ignore));for(i in u)u.hasOwnProperty(i)&&e.mix(u,this.getProvides(i));if(this.force)for(r=0;r<this.force.length;r++)this.force[r]in u&&delete u[this.force[r]];e.mix(this.loaded,u),this._init=!0},getLangPackName:function(e,t){return"lang/"+t+(e?"_"+e:"")},_explode:function(){var t=this.required,n,r,i={},s=this,o,u;s.dirty=!1,s._explodeRollups(),t=s.required;for(o in t)t.hasOwnProperty(o)&&(i[o]||(i[o]=!0,n=s.getModule(o),n&&(u=n.expound,u&&(t[u]=s.getModule(u),r=s.getRequires(t[u]),e.mix(t,v.hash(r))),r=s.getRequires(n),e.mix(t,v.hash(r)))))},_patternTest:function(e,t){return e.indexOf(t)>-1},getModule:function(t){if(!t)return null;var n,r,i,s=this.getModuleInfo(t),o=this.patterns;if(!s||s&&s.
+ext)for(i in o)if(o.hasOwnProperty(i)){n=o[i],n.test||(n.test=this._patternTest);if(n.test(t,i)){r=n;break}}return s?r&&s&&r.configFn&&!s.configFn&&(s.configFn=r.configFn,s.configFn(s)):r&&(n.action?n.action.call(this,t,i):(s=this.addModule(e.merge(r,{test:void 0,temp:!0}),t),r.configFn&&(s.configFn=r.configFn))),s},_rollup:function(){},_reduce:function(e){e=e||this.required;var t,n,r,i,s=this.loadType,o=this.ignore?v.hash(this.ignore):!1;for(t in e)if(e.hasOwnProperty(t)){i=this.getModule(t),((this.loaded[t]||w[t])&&!this.forceMap[t]&&!this.ignoreRegistered||s&&i&&i.type!==s)&&delete e[t],o&&o[t]&&delete e[t],r=i&&i.supersedes;if(r)for(n=0;n<r.length;n++)r[n]in e&&delete e[r[n]]}return e},_finish:function(e,t){m.running=!1;var n=this.onEnd;n&&n.call(this.context,{msg:e,data:this.data,success:t}),this._continue()},_onSuccess:function(){var t=this,n=e.merge(t.skipped),r,i=[],s=t.requireRegistration,o,u,f,l;for(f in n)n.hasOwnProperty(f)&&delete t.inserted[f];t.skipped={};for(f in t.inserted)t.inserted.hasOwnProperty(f)&&(l=t.getModule(f),!l||!s||l.type!==a||f in YUI.Env.mods?e.mix(t.loaded,t.getProvides(f)):i.push(f));r=t.onSuccess,u=i.length?"notregistered":"success",o=!i.length,r&&r.call(t.context,{msg:u,data:t.data,success:o,failed:i,skipped:n}),t._finish(u,o)},_onProgress:function(e){var t=this,n;if(e.data&&e.data.length)for(n=0;n<e.data.length;n++)e.data[n]=t.getModule(e.data[n].name);t.onProgress&&t.onProgress.call(t.context,{name:e.url,data:e.data})},_onFailure:function(e){var t=this.onFailure,n=[],r=0,i=e.errors.length;for(r;r<i;r++)n.push(e.errors[r].error);n=n.join(","),t&&t.call(this.context,{msg:n,data:this.data,success:!1}),this._finish(n,!1)},_onTimeout:function(e){var t=this.onTimeout;t&&t.call(this.context,{msg:"timeout",data:this.data,success:!1,transaction:e})},_sort:function(){var e,t=this.required,n={};this.sorted=[];for(e in t)!n[e]&&t.hasOwnProperty(e)&&this._visit(e,n)},_visit:function(e,t){var n,r,i,s,o,u,a,f,l;t[e]=!0,n=this.required,i=this.moduleInfo[e],r=this.conditions[e]||{};if(i){o=i.expanded||i.requires;for(f=0,l=o.length;f<l;++f)s=o[f],u=r[s],a=u&&(!u.when||u.when==="after"),n[s]&&!t[s]&&!a&&this._visit(s,t)}this.sorted.push(e)},_insert:function(t,n,r,i){t&&this._config(t);var s=this.resolve(!i),o=this,f=0,l=0,c={},h,p;o._refetch=[],r&&(s[r===a?u:a]=[]),o.fetchCSS||(s.css=[]),s.js.length&&f++,s.css.length&&f++,p=function(t){l++;var n={},r=0,i=0,s="",u,a,p;if(t&&t.errors)for(r=0;r<t.errors.length;r++)t.errors[r].request?s=t.errors[r].request.url:s=t.errors[r],n[s]=s;if(t&&t.data&&t.data.length&&t.type==="success")for(r=0;r<t.data.length;r++){o.inserted[t.data[r].name]=!0;if(t.data[r].lang||t.data[r].skinnable)delete o.inserted[t.data[r].name],o._refetch.push(t.data[r].name)}if(l===f){o._loading=null;if(o._refetch.length){for(r=0;r<o._refetch.length;r++){h=o.getRequires(o.getModule(o._refetch[r]));for(i=0;i<h.length;i++)o.inserted[h[i]]||(c[h[i]]=h[i])}c=e.Object.keys(c);if(c.length){o.require(c),p=o.resolve(!0);if(p.cssMods.length){for(r=0;r<p.cssMods.length;r++)a=p.cssMods[r].name,delete YUI.Env._cssLoaded[a],o.isCSSLoaded(a)&&(o.inserted[a]=!0,delete o.required[a]);o.sorted=[],o._sort()}t=null,o._insert()}}t&&t.fn&&(u=t.fn,delete t.fn,u.call(o,t))}},this._loading=!0;if(!s.js.length&&!s.css.length){l=-1,p({fn:o._onSuccess});return}s.css.length&&e.Get.css(s.css,{data:s.cssMods,attributes:o.cssAttributes,insertBefore:o.insertBefore,charset:o.charset,timeout:o.timeout,context:o,onProgress:function(e){o._onProgress.call(o,e)},onTimeout:function(e){o._onTimeout.call(o,e)},onSuccess:function(e){e.type="success",e.fn=o._onSuccess,p.call(o,e)},onFailure:function(e){e.type="failure",e.fn=o._onFailure,p.call(o,e)}}),s.js.length&&e.Get.js(s.js,{data:s.jsMods,insertBefore:o.insertBefore,attributes:o.jsAttributes,charset:o.charset,timeout:o.timeout,autopurge:!1,context:o,async:o.async,onProgress:function(e){o._onProgress.call(o,e)},onTimeout:function(e){o._onTimeout.call(o,e)},onSuccess:function(e){e.type="success",e.fn=o._onSuccess,p.call(o,e)},onFailure:function(e){e.type="failure",e.fn=o._onFailure,p.call(o,e)}})},_continue:function(){!m.running&&m.size()>0&&(m.running=!0,m.next()())},insert:function(t,n,r){var i=this,s=e.merge(this);delete s.require,delete s.dirty,m.add(function(){i._insert(s,t,n,r)}),this._continue()},loadNext:function(){return},_filter:function(e,t,n){var r=this.filter,i=t&&t in this.filters,s=i&&this.filters[t],o=n||(this.getModuleInfo(t)||{}).group||null;return o&&this.groups[o]&&this.groups[o].filter&&(s=this.groups[o].filter,i=!0),e&&(i&&(r=b.isString(s)?this.FILTER_DEFS[s.toUpperCase()]||null:s),r&&(e=e.replace(new RegExp(r.searchExp,"g"),r.replaceStr))),e},_url:function(e,t,n){return this._filter((n||this.base||"")+e,t)},resolve:function(e,t){var r=this,s={js:[],jsMods:[],css:[],cssMods:[]},o;(r.skin.overrides||r.skin.defaultSkin!==l||r.ignoreRegistered)&&r._resetModules(),e&&r.calculate(),t=t||r.sorted,o=function(e){if(e){var t=e.group&&r.groups[e.group]||n,i;t.async===!1&&(e.async=t.async),i=e.fullpath?r._filter(e.fullpath,e.name):r._url(e.path,e.name,t.base||e.base);if(e.attributes||e.async===!1)i={url:i,async:e.async},e.attributes&&(i.attributes=e.attributes);s[e.type].push(i),s[e.type+"Mods"].push(e)}};var f=r.ignoreRegistered?{}:r.inserted,c={},h,p,d,v,m,g,y,b;for(b=0,y=t.length;b<y;b++){g=r.getModule(t[b]);if(!g||f[g.name])continue;m=r.groups[g.group],d=r.comboBase;if(m){if(!m.combine||g.fullpath){o(g);continue}g.combine=!0,typeof m.root=="string"&&(g.root=m.root),d=m.comboBase||d,v=m.comboSep,h=m.maxURLLength}else if(!r.combine){o(g);continue}if(!g.combine&&g.ext){o(g);continue}c[d]=c[d]||{js:[],jsMods:[],css:[],cssMods:[]},p=c[d],p.group=g.group,p.comboSep=v||r.comboSep,p.maxURLLength=h||r.maxURLLength,p[g.type+"Mods"].push(g)}var w,E,S,x,T,N,C;for(d in c)if(c.hasOwnProperty(d)){p=c[d],v=p.comboSep,h=p.maxURLLength;for(C in p)if(C===a||C===u){E=p[C+"Mods"],T=[];for(b=0,y=E.length;b<y;b+=1)g=E[b],N=(typeof g.root=="string"?g.root:r.root
+)+(g.path||g.fullpath),T.push(r._filter(N,g.name));S=d+T.join(v),x=S.length,h<=d.length&&(h=i);if(T.length)if(x>h){w=[];for(b=0,y=T.length;b<y;b++)w.push(T[b]),S=d+w.join(v),S.length>h&&(N=w.pop(),S=d+w.join(v),s[C].push(r._filter(S,null,p.group)),w=[],N&&w.push(N));w.length&&(S=d+w.join(v),s[C].push(r._filter(S,null,p.group)))}else s[C].push(r._filter(S,null,p.group));s[C+"Mods"]=s[C+"Mods"].concat(E)}}return s},load:function(e){if(!e)return;var t=this,n=t.resolve(!0);t.data=n,t.onEnd=function(){e.apply(t.context||t,arguments)},t.insert()}}},"3.17.2",{requires:["get","features"]}),YUI.add("loader-rollup",function(e,t){e.Loader.prototype._rollup=function(){var e,t,n,r,i=this.required,s,o=this.moduleInfo,u,a,f;if(this.dirty||!this.rollups){this.rollups={};for(e in o)o.hasOwnProperty(e)&&(n=this.getModule(e),n&&n.rollup&&(this.rollups[e]=n))}for(;;){u=!1;for(e in this.rollups)if(this.rollups.hasOwnProperty(e)&&!i[e]&&(!this.loaded[e]||this.forceMap[e])){n=this.getModule(e),r=n.supersedes||[],s=!1;if(!n.rollup)continue;a=0;for(t=0;t<r.length;t++){f=o[r[t]];if(this.loaded[r[t]]&&!this.forceMap[r[t]]){s=!1;break}if(i[r[t]]&&n.type===f.type){a++,s=a>=n.rollup;if(s)break}}s&&(i[e]=!0,u=!0,this.getRequires(n))}if(!u)break}}},"3.17.2",{requires:["loader-base"]}),YUI.add("loader-yui3",function(e,t){YUI.Env[e.version].modules=YUI.Env[e.version].modules||{},e.mix(YUI.Env[e.version].modules,{"align-plugin":{requires:["node-screen","node-pluginhost"]},anim:{use:["anim-base","anim-color","anim-curve","anim-easing","anim-node-plugin","anim-scroll","anim-xy"]},"anim-base":{requires:["base-base","node-style","color-base"]},"anim-color":{requires:["anim-base"]},"anim-curve":{requires:["anim-xy"]},"anim-easing":{requires:["anim-base"]},"anim-node-plugin":{requires:["node-pluginhost","anim-base"]},"anim-scroll":{requires:["anim-base"]},"anim-shape":{requires:["anim-base","anim-easing","anim-color","matrix"]},"anim-shape-transform":{use:["anim-shape"]},"anim-xy":{requires:["anim-base","node-screen"]},app:{use:["app-base","app-content","app-transitions","lazy-model-list","model","model-list","model-sync-rest","model-sync-local","router","view","view-node-map"]},"app-base":{requires:["classnamemanager","pjax-base","router","view"]},"app-content":{requires:["app-base","pjax-content"]},"app-transitions":{requires:["app-base"]},"app-transitions-css":{type:"css"},"app-transitions-native":{condition:{name:"app-transitions-native",test:function(e){var t=e.config.doc,n=t?t.documentElement:null;return n&&n.style?"MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style:!1},trigger:"app-transitions"},requires:["app-transitions","app-transitions-css","parallel","transition"]},"array-extras":{requires:["yui-base"]},"array-invoke":{requires:["yui-base"]},arraylist:{requires:["yui-base"]},"arraylist-add":{requires:["arraylist"]},"arraylist-filter":{requires:["arraylist"]},arraysort:{requires:["yui-base"]},"async-queue":{requires:["event-custom"]},attribute:{use:["attribute-base","attribute-complex"]},"attribute-base":{requires:["attribute-core","attribute-observable","attribute-extras"]},"attribute-complex":{requires:["attribute-base"]},"attribute-core":{requires:["oop"]},"attribute-events":{use:["attribute-observable"]},"attribute-extras":{requires:["oop"]},"attribute-observable":{requires:["event-custom"]},autocomplete:{use:["autocomplete-base","autocomplete-sources","autocomplete-list","autocomplete-plugin"]},"autocomplete-base":{optional:["autocomplete-sources"],requires:["array-extras","base-build","escape","event-valuechange","node-base"]},"autocomplete-filters":{requires:["array-extras","text-wordbreak"]},"autocomplete-filters-accentfold":{requires:["array-extras","text-accentfold","text-wordbreak"]},"autocomplete-highlighters":{requires:["array-extras","highlight-base"]},"autocomplete-highlighters-accentfold":{requires:["array-extras","highlight-accentfold"]},"autocomplete-list":{after:["autocomplete-sources"],lang:["en","es","hu","it"],requires:["autocomplete-base","event-resize","node-screen","selector-css3","shim-plugin","widget","widget-position","widget-position-align"],skinnable:!0},"autocomplete-list-keys":{condition:{name:"autocomplete-list-keys",test:function(e){return!e.UA.ios&&!e.UA.android},trigger:"autocomplete-list"},requires:["autocomplete-list","base-build"]},"autocomplete-plugin":{requires:["autocomplete-list","node-pluginhost"]},"autocomplete-sources":{optional:["io-base","json-parse","jsonp","yql"],requires:["autocomplete-base"]},axes:{use:["axis-numeric","axis-category","axis-time","axis-stacked"]},"axes-base":{use:["axis-numeric-base","axis-category-base","axis-time-base","axis-stacked-base"]},axis:{requires:["dom","widget","widget-position","widget-stack","graphics","axis-base"]},"axis-base":{requires:["classnamemanager","datatype-number","datatype-date","base","event-custom"]},"axis-category":{requires:["axis","axis-category-base"]},"axis-category-base":{requires:["axis-base"]},"axis-numeric":{requires:["axis","axis-numeric-base"]},"axis-numeric-base":{requires:["axis-base"]},"axis-stacked":{requires:["axis-numeric","axis-stacked-base"]},"axis-stacked-base":{requires:["axis-numeric-base"]},"axis-time":{requires:["axis","axis-time-base"]},"axis-time-base":{requires:["axis-base"]},base:{use:["base-base","base-pluginhost","base-build"]},"base-base":{requires:["attribute-base","base-core","base-observable"]},"base-build":{requires:["base-base"]},"base-core":{requires:["attribute-core"]},"base-observable":{requires:["attribute-observable","base-core"]},"base-pluginhost":{requires:["base-base","pluginhost"]},button:{requires:["button-core","cssbutton","widget"]},"button-core":{requires:["attribute-core","classnamemanager","node-base","escape"]},"button-group":{requires:["button-plugin","cssbutton","widget"]},"button-plugin":{requires:["button-core","cssbutton","node-pluginhost"]},cache:{use:["cache-base","cache-offline","cache-plugin"]},"cache-base":{requires:["base"]},"cache-offline"
+:{requires:["cache-base","json"]},"cache-plugin":{requires:["plugin","cache-base"]},calendar:{requires:["calendar-base","calendarnavigator"],skinnable:!0},"calendar-base":{lang:["de","en","es","es-AR","fr","hu","it","ja","nb-NO","nl","pt-BR","ru","zh-Hans","zh-Hans-CN","zh-Hant","zh-Hant-HK","zh-HANT-TW"],requires:["widget","datatype-date","datatype-date-math","cssgrids"],skinnable:!0},calendarnavigator:{requires:["plugin","classnamemanager","datatype-date","node"],skinnable:!0},charts:{use:["charts-base"]},"charts-base":{requires:["dom","event-mouseenter","event-touch","graphics-group","axes","series-pie","series-line","series-marker","series-area","series-spline","series-column","series-bar","series-areaspline","series-combo","series-combospline","series-line-stacked","series-marker-stacked","series-area-stacked","series-spline-stacked","series-column-stacked","series-bar-stacked","series-areaspline-stacked","series-combo-stacked","series-combospline-stacked"]},"charts-legend":{requires:["charts-base"]},classnamemanager:{requires:["yui-base"]},"clickable-rail":{requires:["slider-base"]},collection:{use:["array-extras","arraylist","arraylist-add","arraylist-filter","array-invoke"]},color:{use:["color-base","color-hsl","color-harmony"]},"color-base":{requires:["yui-base"]},"color-harmony":{requires:["color-hsl"]},"color-hsl":{requires:["color-base"]},"color-hsv":{requires:["color-base"]},console:{lang:["en","es","hu","it","ja"],requires:["yui-log","widget"],skinnable:!0},"console-filters":{requires:["plugin","console"],skinnable:!0},"content-editable":{requires:["node-base","editor-selection","stylesheet","plugin"]},controller:{use:["router"]},cookie:{requires:["yui-base"]},"createlink-base":{requires:["editor-base"]},cssbase:{after:["cssreset","cssfonts","cssgrids","cssreset-context","cssfonts-context","cssgrids-context"],type:"css"},"cssbase-context":{after:["cssreset","cssfonts","cssgrids","cssreset-context","cssfonts-context","cssgrids-context"],type:"css"},cssbutton:{type:"css"},cssfonts:{type:"css"},"cssfonts-context":{type:"css"},cssgrids:{optional:["cssnormalize"],type:"css"},"cssgrids-base":{optional:["cssnormalize"],type:"css"},"cssgrids-responsive":{optional:["cssnormalize"],requires:["cssgrids","cssgrids-responsive-base"],type:"css"},"cssgrids-units":{optional:["cssnormalize"],requires:["cssgrids-base"],type:"css"},cssnormalize:{type:"css"},"cssnormalize-context":{type:"css"},cssreset:{type:"css"},"cssreset-context":{type:"css"},dataschema:{use:["dataschema-base","dataschema-json","dataschema-xml","dataschema-array","dataschema-text"]},"dataschema-array":{requires:["dataschema-base"]},"dataschema-base":{requires:["base"]},"dataschema-json":{requires:["dataschema-base","json"]},"dataschema-text":{requires:["dataschema-base"]},"dataschema-xml":{requires:["dataschema-base"]},datasource:{use:["datasource-local","datasource-io","datasource-get","datasource-function","datasource-cache","datasource-jsonschema","datasource-xmlschema","datasource-arrayschema","datasource-textschema","datasource-polling"]},"datasource-arrayschema":{requires:["datasource-local","plugin","dataschema-array"]},"datasource-cache":{requires:["datasource-local","plugin","cache-base"]},"datasource-function":{requires:["datasource-local"]},"datasource-get":{requires:["datasource-local","get"]},"datasource-io":{requires:["datasource-local","io-base"]},"datasource-jsonschema":{requires:["datasource-local","plugin","dataschema-json"]},"datasource-local":{requires:["base"]},"datasource-polling":{requires:["datasource-local"]},"datasource-textschema":{requires:["datasource-local","plugin","dataschema-text"]},"datasource-xmlschema":{requires:["datasource-local","plugin","datatype-xml","dataschema-xml"]},datatable:{use:["datatable-core","datatable-table","datatable-head","datatable-body","datatable-base","datatable-column-widths","datatable-message","datatable-mutable","datatable-sort","datatable-datasource"]},"datatable-base":{requires:["datatable-core","datatable-table","datatable-head","datatable-body","base-build","widget"],skinnable:!0},"datatable-body":{requires:["datatable-core","view","classnamemanager"]},"datatable-column-widths":{requires:["datatable-base"]},"datatable-core":{requires:["escape","model-list","node-event-delegate"]},"datatable-datasource":{requires:["datatable-base","plugin","datasource-local"]},"datatable-foot":{requires:["datatable-core","view"]},"datatable-formatters":{requires:["datatable-body","datatype-number-format","datatype-date-format","escape"]},"datatable-head":{requires:["datatable-core","view","classnamemanager"]},"datatable-highlight":{requires:["datatable-base","event-hover"],skinnable:!0},"datatable-keynav":{requires:["datatable-base"]},"datatable-message":{lang:["en","fr","es","hu","it"],requires:["datatable-base"],skinnable:!0},"datatable-mutable":{requires:["datatable-base"]},"datatable-paginator":{lang:["en","fr"],requires:["model","view","paginator-core","datatable-foot","datatable-paginator-templates"],skinnable:!0},"datatable-paginator-templates":{requires:["template"]},"datatable-scroll":{requires:["datatable-base","datatable-column-widths","dom-screen"],skinnable:!0},"datatable-sort":{lang:["en","fr","es","hu"],requires:["datatable-base"],skinnable:!0},"datatable-table":{requires:["datatable-core","datatable-head","datatable-body","view","classnamemanager"]},datatype:{use:["datatype-date","datatype-number","datatype-xml"]},"datatype-date":{use:["datatype-date-parse","datatype-date-format","datatype-date-math"]},"datatype-date-format":{lang:["ar","ar-JO","ca","ca-ES","da","da-DK","de","de-AT","de-DE","el","el-GR","en","en-AU","en-CA","en-GB","en-IE","en-IN","en-JO","en-MY","en-NZ","en-PH","en-SG","en-US","es","es-AR","es-BO","es-CL","es-CO","es-EC","es-ES","es-MX","es-PE","es-PY","es-US","es-UY","es-VE","fi","fi-FI","fr","fr-BE","fr-CA","fr-FR","hi","hi-IN","hu","id","id-ID","it","it-IT","ja","ja-JP","ko","ko-KR","ms","ms-MY","nb","nb-NO","nl","nl-BE","nl-NL","pl","pl-PL"
+,"pt","pt-BR","ro","ro-RO","ru","ru-RU","sv","sv-SE","th","th-TH","tr","tr-TR","vi","vi-VN","zh-Hans","zh-Hans-CN","zh-Hant","zh-Hant-HK","zh-Hant-TW"]},"datatype-date-math":{requires:["yui-base"]},"datatype-date-parse":{},"datatype-number":{use:["datatype-number-parse","datatype-number-format"]},"datatype-number-format":{},"datatype-number-parse":{requires:["escape"]},"datatype-xml":{use:["datatype-xml-parse","datatype-xml-format"]},"datatype-xml-format":{},"datatype-xml-parse":{},dd:{use:["dd-ddm-base","dd-ddm","dd-ddm-drop","dd-drag","dd-proxy","dd-constrain","dd-drop","dd-scroll","dd-delegate"]},"dd-constrain":{requires:["dd-drag"]},"dd-ddm":{requires:["dd-ddm-base","event-resize"]},"dd-ddm-base":{requires:["node","base","yui-throttle","classnamemanager"]},"dd-ddm-drop":{requires:["dd-ddm"]},"dd-delegate":{requires:["dd-drag","dd-drop-plugin","event-mouseenter"]},"dd-drag":{requires:["dd-ddm-base"]},"dd-drop":{requires:["dd-drag","dd-ddm-drop"]},"dd-drop-plugin":{requires:["dd-drop"]},"dd-gestures":{condition:{name:"dd-gestures",trigger:"dd-drag",ua:"touchEnabled"},requires:["dd-drag","event-synthetic","event-gestures"]},"dd-plugin":{optional:["dd-constrain","dd-proxy"],requires:["dd-drag"]},"dd-proxy":{requires:["dd-drag"]},"dd-scroll":{requires:["dd-drag"]},dial:{lang:["en","es","hu"],requires:["widget","dd-drag","event-mouseenter","event-move","event-key","transition","intl"],skinnable:!0},dom:{use:["dom-base","dom-screen","dom-style","selector-native","selector"]},"dom-base":{requires:["dom-core"]},"dom-core":{requires:["oop","features"]},"dom-screen":{requires:["dom-base","dom-style"]},"dom-style":{requires:["dom-base"]},"dom-style-ie":{condition:{name:"dom-style-ie",test:function(e){var t=e.Features.test,n=e.Features.add,r=e.config.win,i=e.config.doc,s="documentElement",o=!1;return n("style","computedStyle",{test:function(){return r&&"getComputedStyle"in r}}),n("style","opacity",{test:function(){return i&&"opacity"in i[s].style}}),o=!t("style","opacity")&&!t("style","computedStyle"),o},trigger:"dom-style"},requires:["dom-style","color-base"]},dump:{requires:["yui-base"]},editor:{use:["frame","editor-selection","exec-command","editor-base","editor-para","editor-br","editor-bidi","editor-tab","createlink-base"]},"editor-base":{requires:["base","frame","node","exec-command","editor-selection"]},"editor-bidi":{requires:["editor-base"]},"editor-br":{requires:["editor-base"]},"editor-inline":{requires:["editor-base","content-editable"]},"editor-lists":{requires:["editor-base"]},"editor-para":{requires:["editor-para-base"]},"editor-para-base":{requires:["editor-base"]},"editor-para-ie":{condition:{name:"editor-para-ie",trigger:"editor-para",ua:"ie",when:"instead"},requires:["editor-para-base"]},"editor-selection":{requires:["node"]},"editor-tab":{requires:["editor-base"]},escape:{requires:["yui-base"]},event:{after:["node-base"],use:["event-base","event-delegate","event-synthetic","event-mousewheel","event-mouseenter","event-key","event-focus","event-resize","event-hover","event-outside","event-touch","event-move","event-flick","event-valuechange","event-tap"]},"event-base":{after:["node-base"],requires:["event-custom-base"]},"event-base-ie":{after:["event-base"],condition:{name:"event-base-ie",test:function(e){var t=e.config.doc&&e.config.doc.implementation;return t&&!t.hasFeature("Events","2.0")},trigger:"node-base"},requires:["node-base"]},"event-contextmenu":{requires:["event-synthetic","dom-screen"]},"event-custom":{use:["event-custom-base","event-custom-complex"]},"event-custom-base":{requires:["oop"]},"event-custom-complex":{requires:["event-custom-base"]},"event-delegate":{requires:["node-base"]},"event-flick":{requires:["node-base","event-touch","event-synthetic"]},"event-focus":{requires:["event-synthetic"]},"event-gestures":{use:["event-flick","event-move"]},"event-hover":{requires:["event-mouseenter"]},"event-key":{requires:["event-synthetic"]},"event-mouseenter":{requires:["event-synthetic"]},"event-mousewheel":{requires:["node-base"]},"event-move":{requires:["node-base","event-touch","event-synthetic"]},"event-outside":{requires:["event-synthetic"]},"event-resize":{requires:["node-base","event-synthetic"]},"event-simulate":{requires:["event-base"]},"event-synthetic":{requires:["node-base","event-custom-complex"]},"event-tap":{requires:["node-base","event-base","event-touch","event-synthetic"]},"event-touch":{requires:["node-base"]},"event-valuechange":{requires:["event-focus","event-synthetic"]},"exec-command":{requires:["frame"]},features:{requires:["yui-base"]},file:{requires:["file-flash","file-html5"]},"file-flash":{requires:["base"]},"file-html5":{requires:["base"]},frame:{requires:["base","node","plugin","selector-css3","yui-throttle"]},"gesture-simulate":{requires:["async-queue","event-simulate","node-screen"]},get:{requires:["yui-base"]},graphics:{requires:["node","event-custom","pluginhost","matrix","classnamemanager"]},"graphics-canvas":{condition:{name:"graphics-canvas",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"},requires:["graphics","color-base"]},"graphics-canvas-default":{condition:{name:"graphics-canvas-default",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"}},"graphics-group":{requires:["graphics"]},"graphics-svg":{condition:{name:"graphics-svg",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure"
+,"1.1");return i&&(n||!r)},trigger:"graphics"},requires:["graphics"]},"graphics-svg-default":{condition:{name:"graphics-svg-default",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"}},"graphics-vml":{condition:{name:"graphics-vml",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"},requires:["graphics","color-base"]},"graphics-vml-default":{condition:{name:"graphics-vml-default",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"}},handlebars:{use:["handlebars-compiler"]},"handlebars-base":{requires:[]},"handlebars-compiler":{requires:["handlebars-base"]},highlight:{use:["highlight-base","highlight-accentfold"]},"highlight-accentfold":{requires:["highlight-base","text-accentfold"]},"highlight-base":{requires:["array-extras","classnamemanager","escape","text-wordbreak"]},history:{use:["history-base","history-hash","history-html5"]},"history-base":{requires:["event-custom-complex"]},"history-hash":{after:["history-html5"],requires:["event-synthetic","history-base","yui-later"]},"history-hash-ie":{condition:{name:"history-hash-ie",test:function(e){var t=e.config.doc&&e.config.doc.documentMode;return e.UA.ie&&(!("onhashchange"in e.config.win)||!t||t<8)},trigger:"history-hash"},requires:["history-hash","node-base"]},"history-html5":{optional:["json"],requires:["event-base","history-base","node-base"]},imageloader:{requires:["base-base","node-style","node-screen"]},intl:{requires:["intl-base","event-custom"]},"intl-base":{requires:["yui-base"]},io:{use:["io-base","io-xdr","io-form","io-upload-iframe","io-queue"]},"io-base":{requires:["event-custom-base","querystring-stringify-simple"]},"io-form":{requires:["io-base","node-base"]},"io-nodejs":{condition:{name:"io-nodejs",trigger:"io-base",ua:"nodejs"},requires:["io-base"]},"io-queue":{requires:["io-base","queue-promote"]},"io-upload-iframe":{requires:["io-base","node-base"]},"io-xdr":{requires:["io-base","datatype-xml-parse"]},json:{use:["json-parse","json-stringify"]},"json-parse":{requires:["yui-base"]},"json-parse-shim":{condition:{name:"json-parse-shim",test:function(e){function i(e,t){return e==="ok"?!0:t}var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONParse!==!1&&!!n;if(r)try{r=n.parse('{"ok":false}',i).ok}catch(s){r=!1}return!r},trigger:"json-parse"},requires:["json-parse"]},"json-stringify":{requires:["yui-base"]},"json-stringify-shim":{condition:{name:"json-stringify-shim",test:function(e){var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONStringify!==!1&&!!n;if(r)try{r="0"===n.stringify(0)}catch(i){r=!1}return!r},trigger:"json-stringify"},requires:["json-stringify"]},jsonp:{requires:["get","oop"]},"jsonp-url":{requires:["jsonp"]},"lazy-model-list":{requires:["model-list"]},loader:{use:["loader-base","loader-rollup","loader-yui3"]},"loader-base":{requires:["get","features"]},"loader-rollup":{requires:["loader-base"]},"loader-yui3":{requires:["loader-base"]},matrix:{requires:["yui-base"]},model:{requires:["base-build","escape","json-parse"]},"model-list":{requires:["array-extras","array-invoke","arraylist","base-build","escape","json-parse","model"]},"model-sync-local":{requires:["model","json-stringify"]},"model-sync-rest":{requires:["model","io-base","json-stringify"]},node:{use:["node-base","node-event-delegate","node-pluginhost","node-screen","node-style"]},"node-base":{requires:["event-base","node-core","dom-base","dom-style"]},"node-core":{requires:["dom-core","selector"]},"node-event-delegate":{requires:["node-base","event-delegate"]},"node-event-html5":{requires:["node-base"]},"node-event-simulate":{requires:["node-base","event-simulate","gesture-simulate"]},"node-flick":{requires:["classnamemanager","transition","event-flick","plugin"],skinnable:!0},"node-focusmanager":{requires:["attribute","node","plugin","node-event-simulate","event-key","event-focus"]},"node-load":{requires:["node-base","io-base"]},"node-menunav":{requires:["node","classnamemanager","plugin","node-focusmanager"],skinnable:!0},"node-pluginhost":{requires:["node-base","pluginhost"]},"node-screen":{requires:["dom-screen","node-base"]},"node-scroll-info":{requires:["array-extras","base-build","event-resize","node-pluginhost","plugin","selector"]},"node-style":{requires:["dom-style","node-base"]},oop:{requires:["yui-base"]},overlay:{requires:["widget","widget-stdmod","widget-position","widget-position-align","widget-stack","widget-position-constrain"],skinnable:!0},paginator:{requires:["paginator-core"]},"paginator-core":{requires:["base"]},"paginator-url":{requires:["paginator"]},panel:{requires:["widget","widget-autohide","widget-buttons","widget-modality","widget-position","widget-position-align","widget-position-constrain","widget-stack","widget-stdmod"],skinnable:!0},parallel:{requires:["yui-base"]},pjax:{requires:["pjax-base","pjax-content"]},"pjax-base":{requires:["classnamemanager","node-event-delegate","router"]},"pjax-content":{requires:["io-base","node-base","router"]},"pjax-plugin":{requires:["node-pluginhost","pjax","plugin"]},plugin:{requires:["base-base"]},pluginhost:{use:["pluginhost-base","pluginhost-config"]},"pluginhost-base":{requires:["yui-base"]},"pluginhost-config":{requires:["pluginhost-base"]},promise:{requires:["timers"]},querystring:{use:["querystring-parse","querystring-stringify"]},"querystring-parse":{requires:["yui-base","array-extras"]},"querystring-parse-simple"
+:{requires:["yui-base"]},"querystring-stringify":{requires:["yui-base"]},"querystring-stringify-simple":{requires:["yui-base"]},"queue-promote":{requires:["yui-base"]},"range-slider":{requires:["slider-base","slider-value-range","clickable-rail"]},recordset:{use:["recordset-base","recordset-sort","recordset-filter","recordset-indexer"]},"recordset-base":{requires:["base","arraylist"]},"recordset-filter":{requires:["recordset-base","array-extras","plugin"]},"recordset-indexer":{requires:["recordset-base","plugin"]},"recordset-sort":{requires:["arraysort","recordset-base","plugin"]},resize:{use:["resize-base","resize-proxy","resize-constrain"]},"resize-base":{requires:["base","widget","event","oop","dd-drag","dd-delegate","dd-drop"],skinnable:!0},"resize-constrain":{requires:["plugin","resize-base"]},"resize-plugin":{optional:["resize-constrain"],requires:["resize-base","plugin"]},"resize-proxy":{requires:["plugin","resize-base"]},router:{optional:["querystring-parse"],requires:["array-extras","base-build","history"]},scrollview:{requires:["scrollview-base","scrollview-scrollbars"]},"scrollview-base":{requires:["widget","event-gestures","event-mousewheel","transition"],skinnable:!0},"scrollview-base-ie":{condition:{name:"scrollview-base-ie",trigger:"scrollview-base",ua:"ie"},requires:["scrollview-base"]},"scrollview-list":{requires:["plugin","classnamemanager"],skinnable:!0},"scrollview-paginator":{requires:["plugin","classnamemanager"]},"scrollview-scrollbars":{requires:["classnamemanager","transition","plugin"],skinnable:!0},selector:{requires:["selector-native"]},"selector-css2":{condition:{name:"selector-css2",test:function(e){var t=e.config.doc,n=t&&!("querySelectorAll"in t);return n},trigger:"selector"},requires:["selector-native"]},"selector-css3":{requires:["selector-native","selector-css2"]},"selector-native":{requires:["dom-base"]},"series-area":{requires:["series-cartesian","series-fill-util"]},"series-area-stacked":{requires:["series-stacked","series-area"]},"series-areaspline":{requires:["series-area","series-curve-util"]},"series-areaspline-stacked":{requires:["series-stacked","series-areaspline"]},"series-bar":{requires:["series-marker","series-histogram-base"]},"series-bar-stacked":{requires:["series-stacked","series-bar"]},"series-base":{requires:["graphics","axis-base"]},"series-candlestick":{requires:["series-range"]},"series-cartesian":{requires:["series-base"]},"series-column":{requires:["series-marker","series-histogram-base"]},"series-column-stacked":{requires:["series-stacked","series-column"]},"series-combo":{requires:["series-cartesian","series-line-util","series-plot-util","series-fill-util"]},"series-combo-stacked":{requires:["series-stacked","series-combo"]},"series-combospline":{requires:["series-combo","series-curve-util"]},"series-combospline-stacked":{requires:["series-combo-stacked","series-curve-util"]},"series-curve-util":{},"series-fill-util":{},"series-histogram-base":{requires:["series-cartesian","series-plot-util"]},"series-line":{requires:["series-cartesian","series-line-util"]},"series-line-stacked":{requires:["series-stacked","series-line"]},"series-line-util":{},"series-marker":{requires:["series-cartesian","series-plot-util"]},"series-marker-stacked":{requires:["series-stacked","series-marker"]},"series-ohlc":{requires:["series-range"]},"series-pie":{requires:["series-base","series-plot-util"]},"series-plot-util":{},"series-range":{requires:["series-cartesian"]},"series-spline":{requires:["series-line","series-curve-util"]},"series-spline-stacked":{requires:["series-stacked","series-spline"]},"series-stacked":{requires:["axis-stacked"]},"shim-plugin":{requires:["node-style","node-pluginhost"]},slider:{use:["slider-base","slider-value-range","clickable-rail","range-slider"]},"slider-base":{requires:["widget","dd-constrain","event-key"],skinnable:!0},"slider-value-range":{requires:["slider-base"]},sortable:{requires:["dd-delegate","dd-drop-plugin","dd-proxy"]},"sortable-scroll":{requires:["dd-scroll","sortable"]},stylesheet:{requires:["yui-base"]},substitute:{optional:["dump"],requires:["yui-base"]},swf:{requires:["event-custom","node","swfdetect","escape"]},swfdetect:{requires:["yui-base"]},tabview:{requires:["widget","widget-parent","widget-child","tabview-base","node-pluginhost","node-focusmanager"],skinnable:!0},"tabview-base":{requires:["node-event-delegate","classnamemanager"]},"tabview-plugin":{requires:["tabview-base"]},template:{use:["template-base","template-micro"]},"template-base":{requires:["yui-base"]},"template-micro":{requires:["escape"]},test:{requires:["event-simulate","event-custom","json-stringify"]},"test-console":{requires:["console-filters","test","array-extras"],skinnable:!0},text:{use:["text-accentfold","text-wordbreak"]},"text-accentfold":{requires:["array-extras","text-data-accentfold"]},"text-data-accentfold":{requires:["yui-base"]},"text-data-wordbreak":{requires:["yui-base"]},"text-wordbreak":{requires:["array-extras","text-data-wordbreak"]},timers:{requires:["yui-base"]},transition:{requires:["node-style"]},"transition-timer":{condition:{name:"transition-timer",test:function(e){var t=e.config.doc,n=t?t.documentElement:null,r=!0;return n&&n.style&&(r=!("MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style)),r},trigger:"transition"},requires:["transition"]},tree:{requires:["base-build","tree-node"]},"tree-labelable":{requires:["tree"]},"tree-lazy":{requires:["base-pluginhost","plugin","tree"]},"tree-node":{},"tree-openable":{requires:["tree"]},"tree-selectable":{requires:["tree"]},"tree-sortable":{requires:["tree"]},uploader:{requires:["uploader-html5","uploader-flash"]},"uploader-flash":{requires:["swfdetect","escape","widget","base","cssbutton","node","event-custom","uploader-queue"]},"uploader-html5":{requires:["widget","node-event-simulate","file-html5","uploader-queue"]},"uploader-queue":{requires:["base"]},view:{requires:["base-build","node-event-delegate"]},"view-node-map":{requires:["view"
+]},widget:{use:["widget-base","widget-htmlparser","widget-skin","widget-uievents"]},"widget-anim":{requires:["anim-base","plugin","widget"]},"widget-autohide":{requires:["base-build","event-key","event-outside","widget"]},"widget-base":{requires:["attribute","base-base","base-pluginhost","classnamemanager","event-focus","node-base","node-style"],skinnable:!0},"widget-base-ie":{condition:{name:"widget-base-ie",trigger:"widget-base",ua:"ie"},requires:["widget-base"]},"widget-buttons":{requires:["button-plugin","cssbutton","widget-stdmod"]},"widget-child":{requires:["base-build","widget"]},"widget-htmlparser":{requires:["widget-base"]},"widget-modality":{requires:["base-build","event-outside","widget"],skinnable:!0},"widget-parent":{requires:["arraylist","base-build","widget"]},"widget-position":{requires:["base-build","node-screen","widget"]},"widget-position-align":{requires:["widget-position"]},"widget-position-constrain":{requires:["widget-position"]},"widget-skin":{requires:["widget-base"]},"widget-stack":{requires:["base-build","widget"],skinnable:!0},"widget-stdmod":{requires:["base-build","widget"]},"widget-uievents":{requires:["node-event-delegate","widget-base"]},yql:{requires:["oop"]},"yql-jsonp":{condition:{name:"yql-jsonp",test:function(e){return!e.UA.nodejs&&!e.UA.winjs},trigger:"yql"},requires:["yql","jsonp","jsonp-url"]},"yql-nodejs":{condition:{name:"yql-nodejs",trigger:"yql",ua:"nodejs"},requires:["yql"]},"yql-winjs":{condition:{name:"yql-winjs",trigger:"yql",ua:"winjs"},requires:["yql"]},yui:{},"yui-base":{},"yui-later":{requires:["yui-base"]},"yui-log":{requires:["yui-base"]},"yui-throttle":{requires:["yui-base"]}}),YUI.Env[e.version].md5="45357bb11eddf7fd0a89c0b756599df2"},"3.17.2",{requires:["loader-base"]}),YUI.add("yui",function(e,t){},"3.17.2",{use:["get","features","intl-base","yui-log","yui-log-nodejs","yui-later","loader-base","loader-rollup","loader-yui3"]});
diff --git a/js/yui3/yui-throttle/yui-throttle-min.js b/js/yui3/yui-throttle/yui-throttle-min.js
new file mode 100644
index 000000000..3f58d9c42
--- /dev/null
+++ b/js/yui3/yui-throttle/yui-throttle-min.js
@@ -0,0 +1,10 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+YUI.add("yui-throttle",function(e,t){
+/*! Based on work by Simon Willison: http://gist.github.com/292562 */
+;e.throttle=function(t,n){n=n?n:e.config.throttleTime||150;if(n===-1)return function(){t.apply(this,arguments)};var r=e.Lang.now();return function(){var i=e.Lang.now();i-r>n&&(r=i,t.apply(this,arguments))}}},"3.17.2",{requires:["yui-base"]});
diff --git a/js/yui3/yui/yui-min.js b/js/yui3/yui/yui-min.js
new file mode 100644
index 000000000..4e1c4e525
--- /dev/null
+++ b/js/yui3/yui/yui-min.js
@@ -0,0 +1,23 @@
+/*
+YUI 3.17.2 (build 9c3c78e)
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+typeof YUI!="undefined"&&(YUI._YUI=YUI);var YUI=function(){var e=0,t=this,n=arguments,r=n.length,i=function(e,t){return e&&e.hasOwnProperty&&e instanceof t},s=typeof YUI_config!="undefined"&&YUI_config;i(t,YUI)?(t._init(),YUI.GlobalConfig&&t.applyConfig(YUI.GlobalConfig),s&&t.applyConfig(s),r||t._setup()):t=new YUI;if(r){for(;e<r;e++)t.applyConfig(n[e]);t._setup()}return t.instanceOf=i,t};(function(){var e,t,n="3.17.2",r=".",i="http://yui.yahooapis.com/",s="yui3-js-enabled",o="yui3-css-stamp",u=function(){},a=Array.prototype.slice,f={"io.xdrReady":1,"io.xdrResponse":1,"SWF.eventHandler":1},l=typeof window!="undefined",c=l?window:null,h=l?c.document:null,p=h&&h.documentElement,d=p&&p.className,v={},m=(new Date).getTime(),g=function(e,t,n,r){e&&e.addEventListener?e.addEventListener(t,n,r):e&&e.attachEvent&&e.attachEvent("on"+t,n)},y=function(e,t,n,r){if(e&&e.removeEventListener)try{e.removeEventListener(t,n,r)}catch(i){}else e&&e.detachEvent&&e.detachEvent("on"+t,n)},b=function(){YUI.Env.DOMReady=!0,l&&y(h,"DOMContentLoaded",b)},w=function(){YUI.Env.windowLoaded=!0,YUI.Env.DOMReady=!0,l&&y(window,"load",w)},E=function(e,t){var n=e.Env._loader,r=["loader-base"],i=YUI.Env,s=i.mods;return n?(n.ignoreRegistered=!1,n.onEnd=null,n.data=null,n.required=[],n.loadType=null):(n=new e.Loader(e.config),e.Env._loader=n),s&&s.loader&&(r=[].concat(r,YUI.Env.loaderExtras)),YUI.Env.core=e.Array.dedupe([].concat(YUI.Env.core,r)),n},S=function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},x={success:!0};p&&d.indexOf(s)==-1&&(d&&(d+=" "),d+=s,p.className=d),n.indexOf("@")>-1&&(n="3.5.0"),e={applyConfig:function(e){e=e||u;var t,n,r=this.config,i=r.modules,s=r.groups,o=r.aliases,a=this.Env._loader;for(n in e)e.hasOwnProperty(n)&&(t=e[n],i&&n=="modules"?S(i,t):o&&n=="aliases"?S(o,t):s&&n=="groups"?S(s,t):n=="win"?(r[n]=t&&t.contentWindow||t,r.doc=r[n]?r[n].document:null):n!="_yuid"&&(r[n]=t));a&&a._config(e)},_config:function(e){this.applyConfig(e)},_init:function(){var e,t,r=this,s=YUI.Env,u=r.Env,a;r.version=n;if(!u){r.Env={core:["get","features","intl-base","yui-log","yui-later","loader-base","loader-rollup","loader-yui3"],loaderExtras:["loader-rollup","loader-yui3"],mods:{},versions:{},base:i,cdn:i+n+"/build/",_idx:0,_used:{},_attached:{},_exported:{},_missed:[],_yidx:0,_uidx:0,_guidp:"y",_loaded:{},_BASE_RE:/(?:\?(?:[^&]*&)*([^&]*))?\b(yui(?:-\w+)?)\/\2(?:-(min|debug))?\.js/,parseBasePath:function(e,t){var n=e.match(t),r,i;return n&&(r=RegExp.leftContext||e.slice(0,e.indexOf(n[0])),i=n[3],n[1]&&(r+="?"+n[1]),r={filter:i,path:r}),r},getBase:s&&s.getBase||function(t){var n=h&&h.getElementsByTagName("script")||[],i=u.cdn,s,o,a,f;for(o=0,a=n.length;o<a;++o){f=n[o].src;if(f){s=r.Env.parseBasePath(f,t);if(s){e=s.filter,i=s.path;break}}}return i}},u=r.Env,u._loaded[n]={};if(s&&r!==YUI)u._yidx=++s._yidx,u._guidp=("yui_"+n+"_"+u._yidx+"_"+m).replace(/[^a-z0-9_]+/g,"_");else if(YUI._YUI){s=YUI._YUI.Env,u._yidx+=s._yidx,u._uidx+=s._uidx;for(a in s)a in u||(u[a]=s[a]);delete YUI._YUI}r.id=r.stamp(r),v[r.id]=r}r.constructor=YUI,r.config=r.config||{bootstrap:!0,cacheUse:!0,debug:!0,doc:h,fetchCSS:!0,throwFail:!0,useBrowserConsole:!0,useNativeES5:!0,win:c,global:Function("return this")()},h&&!h.getElementById(o)?(t=h.createElement("div"),t.innerHTML='<div id="'+o+'" style="position: absolute !important; visibility: hidden !important"></div>',YUI.Env.cssStampEl=t.firstChild,h.body?h.body.appendChild(YUI.Env.cssStampEl):p.insertBefore(YUI.Env.cssStampEl,p.firstChild)):h&&h.getElementById(o)&&!YUI.Env.cssStampEl&&(YUI.Env.cssStampEl=h.getElementById(o)),r.config.lang=r.config.lang||"en-US",r.config.base=YUI.config.base||r.Env.getBase(r.Env._BASE_RE);if(!e||!"mindebug".indexOf(e))e="min";e=e?"-"+e:e,r.config.loaderPath=YUI.config.loaderPath||"loader/loader"+e+".js"},_setup:function(){var e,t=this,n=[],r=YUI.Env.mods,i=t.config.core||[].concat(YUI.Env.core);for(e=0;e<i.length;e++)r[i[e]]&&n.push(i[e]);t._attach(["yui-base"]),t._attach(n),t.Loader&&E(t)},applyTo:function(e,t,n){if(t in f){var r=v[e],i,s,o;if(r){i=t.split("."),s=r;for(o=0;o<i.length;o+=1)s=s[i[o]],s||this.log("applyTo not found: "+t,"warn","yui");return s&&s.apply(r,n)}return null}return this.log(t+": applyTo not allowed","warn","yui"),null},add:function(e,t,n,r){r=r||{};var i=YUI.Env,s={name:e,fn:t,version:n,details:r},o={},u,a,f,l,c=i.versions;i.mods[e]=s,c[n]=c[n]||{},c[n][e]=s;for(l in v)v.hasOwnProperty(l)&&(a=v[l],o[a.id]||(o[a.id]=!0,u=a.Env._loader,u&&(f=u.getModuleInfo(e),(!f||f.temp)&&u.addModule(r,e))));return this},_attach:function(e,t){var n,r,i,s,o,u,a,f=YUI.Env.mods,l=YUI.Env.aliases,c=this,h,p=YUI.Env._renderedMods,d=c.Env._loader,v=c.Env._attached,m=c.Env._exported,g=e.length,d,y,b,w=[],E,S,x,T,N,C,k;for(n=0;n<g;n++){r=e[n],i=f[r],w.push(r);if(d&&d.conditions[r])for(h in d.conditions[r])d.conditions[r].hasOwnProperty(h)&&(y=d.conditions[r][h],b=y&&(y.ua&&c.UA[y.ua]||y.test&&y.test(c)),b&&w.push(y.name))}e=w,g=e.length;for(n=0;n<g;n++)if(!v[e[n]]){r=e[n],i=f[r];if(l&&l[r]&&!i){c._attach(l[r]);continue}if(!i)T=d&&d.getModuleInfo(r),T&&(i=T,t=!0),!t&&r&&r.indexOf("skin-")===-1&&r.indexOf("css")===-1&&(c.Env._missed.push(r),c.Env._missed=c.Array.dedupe(c.Env._missed),c.message("NOT loaded: "+r,"warn","yui"));else{v[r]=!0;for(h=0;h<c.Env._missed.length;h++)c.Env._missed[h]===r&&(c.message("Found: "+r+" (was reported as missing earlier)","warn","yui"),c.Env._missed.splice(h,1));if(d&&!d._canBeAttached(r))return!0;if(d&&p&&p[r]&&p[r].temp){d.getRequires(p[r]),o=[],T=d.getModuleInfo(r);for(h in T.expanded_map)T.expanded_map.hasOwnProperty(h)&&o.push(h);c._attach(o)}s=i.details,o=s.requires,S=s.es,u=s.use,a=s.after,s.lang&&(o=o||[],o.unshift("intl"));if(o){x=o.length;for(h=0;h<x;h++)if(!v[o[h]]){if(!c._attach(o))return!1;break}}if(a)for(h=0;h<a.length;h++)if(!v[a[h]]){if(!c._attach(a,!0))return!1;break}if(i.fn){E=[c,r];if(S){k={},C={},E.push(k,C);if(o){x=o.length;for(h=0;h<x;h++)k[o[h]]=m.hasOwnProperty(o[h])?m[o[h]]:c}}if(c.config.throwFail)C=i.fn
+.apply(S?undefined:i,E);else try{C=i.fn.apply(S?undefined:i,E)}catch(L){return c.error("Attach error: "+r,L,r),!1}S&&(m[r]=C,N=i.details.condition,N&&N.when==="instead"&&(m[N.trigger]=C))}if(u)for(h=0;h<u.length;h++)if(!v[u[h]]){if(!c._attach(u))return!1;break}}}return!0},_delayCallback:function(e,t){var n=this,r=["event-base"];return t=n.Lang.isObject(t)?t:{event:t},t.event==="load"&&r.push("event-synthetic"),function(){var i=arguments;n._use(r,function(){n.on(t.event,function(){i[1].delayUntil=t.event,e.apply(n,i)},t.args)})}},use:function(){var e=a.call(arguments,0),t=e[e.length-1],n=this,r=0,i,s=n.Env,o=!0;n.Lang.isFunction(t)?(e.pop(),n.config.delayUntil&&(t=n._delayCallback(t,n.config.delayUntil))):t=null,n.Lang.isArray(e[0])&&(e=e[0]);if(n.config.cacheUse){while(i=e[r++])if(!s._attached[i]){o=!1;break}if(o)return e.length,n._notify(t,x,e),n}return n._loading?(n._useQueue=n._useQueue||new n.Queue,n._useQueue.add([e,t])):n._use(e,function(n,r){n._notify(t,r,e)}),n},require:function(){var e=a.call(arguments),t;typeof e[e.length-1]=="function"&&(t=e.pop(),e.push(function(n){var r,i=e.length,s=n.Env._exported,o={};for(r=0;r<i;r++)s.hasOwnProperty(e[r])&&(o[e[r]]=s[e[r]]);t.call(undefined,n,o)})),this.use.apply(this,e)},_notify:function(e,t,n){if(!t.success&&this.config.loadErrorFn)this.config.loadErrorFn.call(this,this,e,t,n);else if(e){this.Env._missed&&this.Env._missed.length&&(t.msg="Missing modules: "+this.Env._missed.join(),t.success=!1);if(this.config.throwFail)e(this,t);else try{e(this,t)}catch(r){this.error("use callback error",r,n)}}},_use:function(e,t){this.Array||this._attach(["yui-base"]);var r,i,s,o=this,u=YUI.Env,a=u.mods,f=o.Env,l=f._used,c=u.aliases,h=u._loaderQueue,p=e[0],d=o.Array,v=o.config,m=v.bootstrap,g=[],y,b=[],w=!0,S=v.fetchCSS,x=function(e,t){var r=0,i=[],s,o,f,h,p;if(!e.length)return;if(c){o=e.length;for(r=0;r<o;r++)c[e[r]]&&!a[e[r]]?i=[].concat(i,c[e[r]]):i.push(e[r]);e=i}o=e.length;for(r=0;r<o;r++){s=e[r],t||b.push(s);if(l[s])continue;f=a[s],h=null,p=null,f?(l[s]=!0,h=f.details.requires,p=f.details.use):u._loaded[n][s]?l[s]=!0:g.push(s),h&&h.length&&x(h),p&&p.length&&x(p,1)}},T=function(n){var r=n||{success:!0,msg:"not dynamic"},i,s,u=!0,a=r.data;o._loading=!1,a&&(s=g,g=[],b=[],x(a),i=g.length,i&&[].concat(g).sort().join()==s.sort().join()&&(i=!1)),i&&a?(o._loading=!0,o._use(g,function(){o._attach(a)&&o._notify(t,r,a)})):(a&&(u=o._attach(a)),u&&o._notify(t,r,e)),o._useQueue&&o._useQueue.size()&&!o._loading&&o._use.apply(o,o._useQueue.next())};if(p==="*"){e=[];for(y in a)a.hasOwnProperty(y)&&e.push(y);return w=o._attach(e),w&&T(),o}return(a.loader||a["loader-base"])&&!o.Loader&&o._attach(["loader"+(a.loader?"":"-base")]),m&&o.Loader&&e.length&&(i=E(o),i.require(e),i.ignoreRegistered=!0,i._boot=!0,i.calculate(null,S?null:"js"),e=i.sorted,i._boot=!1),x(e),r=g.length,r&&(g=d.dedupe(g),r=g.length),m&&r&&o.Loader?(o._loading=!0,i=E(o),i.onEnd=T,i.context=o,i.data=e,i.ignoreRegistered=!1,i.require(g),i.insert(null,S?null:"js")):m&&r&&o.Get&&!f.bootstrapped?(o._loading=!0,s=function(){o._loading=!1,h.running=!1,f.bootstrapped=!0,u._bootstrapping=!1,o._attach(["loader"])&&o._use(e,t)},u._bootstrapping?h.add(s):(u._bootstrapping=!0,o.Get.script(v.base+v.loaderPath,{onEnd:s}))):(w=o._attach(e),w&&T()),o},namespace:function(){var e=arguments,t,n=0,i,s,o;for(;n<e.length;n++){t=this,o=e[n];if(o.indexOf(r)>-1){s=o.split(r);for(i=s[0]=="YAHOO"?1:0;i<s.length;i++)t[s[i]]=t[s[i]]||{},t=t[s[i]]}else t[o]=t[o]||{},t=t[o]}return t},log:u,message:u,dump:function(e){return""+e},error:function(e,t,n){var r=this,i;r.config.errorFn&&(i=r.config.errorFn.apply(r,arguments));if(!i)throw t||new Error(e);return r.message(e,"error",""+n),r},guid:function(e){var t=this.Env._guidp+"_"+ ++this.Env._uidx;return e?e+t:t},stamp:function(e,t){var n;if(!e)return e;e.uniqueID&&e.nodeType&&e.nodeType!==9?n=e.uniqueID:n=typeof e=="string"?e:e._yuid;if(!n){n=this.guid();if(!t)try{e._yuid=n}catch(r){n=null}}return n},destroy:function(){var e=this;e.Event&&e.Event._unload(),delete v[e.id],delete e.Env,delete e.config}},YUI.prototype=e;for(t in e)e.hasOwnProperty(t)&&(YUI[t]=e[t]);YUI.applyConfig=function(e){if(!e)return;YUI.GlobalConfig&&this.prototype.applyConfig.call(this,YUI.GlobalConfig),this.prototype.applyConfig.call(this,e),YUI.GlobalConfig=this.config},YUI._init(),l?(g(h,"DOMContentLoaded",b),g(window,"load",w)):(b(),w()),YUI.Env.add=g,YUI.Env.remove=y,typeof exports=="object"&&(exports.YUI=YUI,YUI.setLoadHook=function(e){YUI._getLoadHook=e},YUI._getLoadHook=null),YUI.Env[n]={}})(),YUI.add("yui-base",function(e,t){function m(e,t,n){var r,i;t||(t=0);if(n||m.test(e))try{return d.slice.call(e,t)}catch(s){i=[];for(r=e.length;t<r;++t)i.push(e[t]);return i}return[e]}function g(){this._init(),this.add.apply(this,arguments)}var n=e.Lang||(e.Lang={}),r=String.prototype,i=Object.prototype.toString,s={"undefined":"undefined",number:"number","boolean":"boolean",string:"string","[object Function]":"function","[object RegExp]":"regexp","[object Array]":"array","[object Date]":"date","[object Error]":"error"},o=/\{\s*([^|}]+?)\s*(?:\|([^}]*))?\s*\}/g,u=" \n \f\r \u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff",a="[ -\r \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+",f=new RegExp("^"+a),l=new RegExp(a+"$"),c=new RegExp(f.source+"|"+l.source,"g"),h=/\{\s*\[(?:native code|function)\]\s*\}/i;n._isNative=function(t){return!!(e.config.useNativeES5&&t&&h.test(t))},n.isArray=n._isNative(Array.isArray)?Array.isArray:function(e){return n.type(e)==="array"},n.isBoolean=function(e){return typeof e=="boolean"},n.isDate=function(e){return n.type(e)==="date"&&e.toString()!=="Invalid Date"&&!isNaN(e)},n.isFunction=function(e){return n.type(e)==="function"},n.isNull=function(e){return e===null},n.isNumber=function(e){return typeof e=="number"&&isFinite(e)},n.isObject=function(e,t){var r=typeof e;return e&&(r==="object"||!t&&(r==="function"||
+n.isFunction(e)))||!1},n.isRegExp=function(e){return n.type(e)==="regexp"},n.isString=function(e){return typeof e=="string"},n.isUndefined=function(e){return typeof e=="undefined"},n.isValue=function(e){var t=n.type(e);switch(t){case"number":return isFinite(e);case"null":case"undefined":return!1;default:return!!t}},n.now=Date.now||function(){return(new Date).getTime()},n.sub=function(e,t){return e.replace?e.replace(o,function(e,r){return n.isUndefined(t[r])?e:t[r]}):e},n.trim=n._isNative(r.trim)&&!u.trim()?function(e){return e&&e.trim?e.trim():e}:function(e){try{return e.replace(c,"")}catch(t){return e}},n.trimLeft=n._isNative(r.trimLeft)&&!u.trimLeft()?function(e){return e.trimLeft()}:function(e){return e.replace(f,"")},n.trimRight=n._isNative(r.trimRight)&&!u.trimRight()?function(e){return e.trimRight()}:function(e){return e.replace(l,"")},n.type=function(e){return s[typeof e]||s[i.call(e)]||(e?"object":"null")};var p=e.Lang,d=Array.prototype,v=Object.prototype.hasOwnProperty;e.Array=m,m.dedupe=p._isNative(Object.create)?function(e){var t=Object.create(null),n=[],r,i,s;for(r=0,s=e.length;r<s;++r)i=e[r],t[i]||(t[i]=1,n.push(i));return n}:function(e){var t={},n=[],r,i,s;for(r=0,s=e.length;r<s;++r)i=e[r],v.call(t,i)||(t[i]=1,n.push(i));return n},m.each=m.forEach=p._isNative(d.forEach)?function(t,n,r){return d.forEach.call(t||[],n,r||e),e}:function(t,n,r){for(var i=0,s=t&&t.length||0;i<s;++i)i in t&&n.call(r||e,t[i],i,t);return e},m.hash=function(e,t){var n={},r=t&&t.length||0,i,s;for(i=0,s=e.length;i<s;++i)i in e&&(n[e[i]]=r>i&&i in t?t[i]:!0);return n},m.indexOf=p._isNative(d.indexOf)?function(e,t,n){return d.indexOf.call(e,t,n)}:function(e,t,n){var r=e.length;n=+n||0,n=(n>0||-1)*Math.floor(Math.abs(n)),n<0&&(n+=r,n<0&&(n=0));for(;n<r;++n)if(n in e&&e[n]===t)return n;return-1},m.numericSort=function(e,t){return e-t},m.some=p._isNative(d.some)?function(e,t,n){return d.some.call(e,t,n)}:function(e,t,n){for(var r=0,i=e.length;r<i;++r)if(r in e&&t.call(n,e[r],r,e))return!0;return!1},m.test=function(e){var t=0;if(p.isArray(e))t=1;else if(p.isObject(e))try{"length"in e&&!e.tagName&&(!e.scrollTo||!e.document)&&!e.apply&&(t=2)}catch(n){}return t},g.prototype={_init:function(){this._q=[]},next:function(){return this._q.shift()},last:function(){return this._q.pop()},add:function(){return this._q.push.apply(this._q,arguments),this},size:function(){return this._q.length}},e.Queue=g,YUI.Env._loaderQueue=YUI.Env._loaderQueue||new g;var y="__",v=Object.prototype.hasOwnProperty,b=e.Lang.isObject;e.cached=function(e,t,n){return t||(t={}),function(r){var i=arguments.length>1?Array.prototype.join.call(arguments,y):String(r);if(!(i in t)||n&&t[i]==n)t[i]=e.apply(e,arguments);return t[i]}},e.getLocation=function(){var t=e.config.win;return t&&t.location},e.merge=function(){var e=0,t=arguments.length,n={},r,i;for(;e<t;++e){i=arguments[e];for(r in i)v.call(i,r)&&(n[r]=i[r])}return n},e.mix=function(t,n,r,i,s,o){var u,a,f,l,c,h,p;if(!t||!n)return t||e;if(s){s===2&&e.mix(t.prototype,n.prototype,r,i,0,o),f=s===1||s===3?n.prototype:n,p=s===1||s===4?t.prototype:t;if(!f||!p)return t}else f=n,p=t;u=r&&!o;if(i)for(l=0,h=i.length;l<h;++l){c=i[l];if(!v.call(f,c))continue;a=u?!1:c in p;if(o&&a&&b(p[c],!0)&&b(f[c],!0))e.mix(p[c],f[c],r,null,0,o);else if(r||!a)p[c]=f[c]}else{for(c in f){if(!v.call(f,c))continue;a=u?!1:c in p;if(o&&a&&b(p[c],!0)&&b(f[c],!0))e.mix(p[c],f[c],r,null,0,o);else if(r||!a)p[c]=f[c]}e.Object._hasEnumBug&&e.mix(p,f,r,e.Object._forceEnum,s,o)}return t};var p=e.Lang,v=Object.prototype.hasOwnProperty,w,E=e.Object=p._isNative(Object.create)?function(e){return Object.create(e)}:function(){function e(){}return function(t){return e.prototype=t,new e}}(),S=E._forceEnum=["hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toString","toLocaleString","valueOf"],x=E._hasEnumBug=!{valueOf:0}.propertyIsEnumerable("valueOf"),T=E._hasProtoEnumBug=function(){}.propertyIsEnumerable("prototype"),N=E.owns=function(e,t){return!!e&&v.call(e,t)};E.hasKey=N,E.keys=p._isNative(Object.keys)&&!T?Object.keys:function(e){if(!p.isObject(e))throw new TypeError("Object.keys called on a non-object");var t=[],n,r,i;if(T&&typeof e=="function")for(r in e)N(e,r)&&r!=="prototype"&&t.push(r);else for(r in e)N(e,r)&&t.push(r);if(x)for(n=0,i=S.length;n<i;++n)r=S[n],N(e,r)&&t.push(r);return t},E.values=function(e){var t=E.keys(e),n=0,r=t.length,i=[];for(;n<r;++n)i.push(e[t[n]]);return i},E.size=function(e){try{return E.keys(e).length}catch(t){return 0}},E.hasValue=function(t,n){return e.Array.indexOf(E.values(t),n)>-1},E.each=function(t,n,r,i){var s;for(s in t)(i||N(t,s))&&n.call(r||e,t[s],s,t);return e},E.some=function(t,n,r,i){var s;for(s in t)if(i||N(t,s))if(n.call(r||e,t[s],s,t))return!0;return!1},E.getValue=function(t,n){if(!p.isObject(t))return w;var r,i=e.Array(n),s=i.length;for(r=0;t!==w&&r<s;r++)t=t[i[r]];return t},E.setValue=function(t,n,r){var i,s=e.Array(n),o=s.length-1,u=t;if(o>=0){for(i=0;u!==w&&i<o;i++)u=u[s[i]];if(u===w)return w;u[s[i]]=r}return t},E.isEmpty=function(e){return!E.keys(Object(e)).length},YUI.Env.parseUA=function(t){var n=function(e){var t=0;return parseFloat(e.replace(/\./g,function(){return t++===1?"":"."}))},r=e.config.win,i=r&&r.navigator,s={ie:0,opera:0,gecko:0,webkit:0,safari:0,chrome:0,mobile:null,air:0,phantomjs:0,ipad:0,iphone:0,ipod:0,ios:null,android:0,silk:0,ubuntu:0,accel:!1,webos:0,caja:i&&i.cajaVersion,secure:!1,os:null,nodejs:0,winjs:typeof Windows!="undefined"&&!!Windows.System,touchEnabled:!1},o=t||i&&i.userAgent,u=r&&r.location,a=u&&u.href,f;return s.userAgent=o,s.secure=a&&a.toLowerCase().indexOf("https")===0,o&&(/windows|win32/i.test(o)?s.os="windows":/macintosh|mac_powerpc/i.test(o)?s.os="macintosh":/android/i.test(o)?s.os="android":/symbos/i.test(o)?s.os="symbos":/linux/i.test(o)?s.os="linux":/rhino/i.test(o)&&(s.os="rhino"),/KHTML/.test(o)&&(s.webkit=1),/IEMobile|XBLWP7/.test(o)&&(s.mobile="windows"),/Fennec/.test(o)&&(s.mobile="gecko"),f=o.match(/AppleWebKit\/([^\s]*)/),f&&f[1]&&(s.
+webkit=n(f[1]),s.safari=s.webkit,/PhantomJS/.test(o)&&(f=o.match(/PhantomJS\/([^\s]*)/),f&&f[1]&&(s.phantomjs=n(f[1]))),/ Mobile\//.test(o)||/iPad|iPod|iPhone/.test(o)?(s.mobile="Apple",f=o.match(/OS ([^\s]*)/),f&&f[1]&&(f=n(f[1].replace("_","."))),s.ios=f,s.os="ios",s.ipad=s.ipod=s.iphone=0,f=o.match(/iPad|iPod|iPhone/),f&&f[0]&&(s[f[0].toLowerCase()]=s.ios)):(f=o.match(/NokiaN[^\/]*|webOS\/\d\.\d/),f&&(s.mobile=f[0]),/webOS/.test(o)&&(s.mobile="WebOS",f=o.match(/webOS\/([^\s]*);/),f&&f[1]&&(s.webos=n(f[1]))),/ Android/.test(o)&&(/Mobile/.test(o)&&(s.mobile="Android"),f=o.match(/Android ([^\s]*);/),f&&f[1]&&(s.android=n(f[1]))),/Silk/.test(o)&&(f=o.match(/Silk\/([^\s]*)/),f&&f[1]&&(s.silk=n(f[1])),s.android||(s.android=2.34,s.os="Android"),/Accelerated=true/.test(o)&&(s.accel=!0))),f=o.match(/OPR\/(\d+\.\d+)/),f&&f[1]?s.opera=n(f[1]):(f=o.match(/(Chrome|CrMo|CriOS)\/([^\s]*)/),f&&f[1]&&f[2]?(s.chrome=n(f[2]),s.safari=0,f[1]==="CrMo"&&(s.mobile="chrome")):(f=o.match(/AdobeAIR\/([^\s]*)/),f&&(s.air=f[0])))),f=o.match(/Ubuntu\ (\d+\.\d+)/),f&&f[1]&&(s.os="linux",s.ubuntu=n(f[1]),f=o.match(/\ WebKit\/([^\s]*)/),f&&f[1]&&(s.webkit=n(f[1])),f=o.match(/\ Chromium\/([^\s]*)/),f&&f[1]&&(s.chrome=n(f[1])),/ Mobile$/.test(o)&&(s.mobile="Ubuntu")),s.webkit||(/Opera/.test(o)?(f=o.match(/Opera[\s\/]([^\s]*)/),f&&f[1]&&(s.opera=n(f[1])),f=o.match(/Version\/([^\s]*)/),f&&f[1]&&(s.opera=n(f[1])),/Opera Mobi/.test(o)&&(s.mobile="opera",f=o.replace("Opera Mobi","").match(/Opera ([^\s]*)/),f&&f[1]&&(s.opera=n(f[1]))),f=o.match(/Opera Mini[^;]*/),f&&(s.mobile=f[0])):(f=o.match(/MSIE ([^;]*)|Trident.*; rv:([0-9.]+)/),f&&(f[1]||f[2])?s.ie=n(f[1]||f[2]):(f=o.match(/Gecko\/([^\s]*)/),f&&(s.gecko=1,f=o.match(/rv:([^\s\)]*)/),f&&f[1]&&(s.gecko=n(f[1]),/Mobile|Tablet/.test(o)&&(s.mobile="ffos"))))))),r&&i&&!(s.chrome&&s.chrome<6)&&(s.touchEnabled="ontouchstart"in r||"msMaxTouchPoints"in i&&i.msMaxTouchPoints>0),t||(typeof process=="object"&&process.versions&&process.versions.node&&(s.os=process.platform,s.nodejs=n(process.versions.node)),YUI.Env.UA=s),s},e.UA=YUI.Env.UA||YUI.Env.parseUA(),e.UA.compareVersions=function(e,t){var n,r,i,s,o,u;if(e===t)return 0;r=(e+"").split("."),s=(t+"").split(".");for(o=0,u=Math.max(r.length,s.length);o<u;++o){n=parseInt(r[o],10),i=parseInt(s[o],10),isNaN(n)&&(n=0),isNaN(i)&&(i=0);if(n<i)return-1;if(n>i)return 1}return 0},YUI.Env.aliases={anim:["anim-base","anim-color","anim-curve","anim-easing","anim-node-plugin","anim-scroll","anim-xy"],"anim-shape-transform":["anim-shape"],app:["app-base","app-content","app-transitions","lazy-model-list","model","model-list","model-sync-rest","model-sync-local","router","view","view-node-map"],attribute:["attribute-base","attribute-complex"],"attribute-events":["attribute-observable"],autocomplete:["autocomplete-base","autocomplete-sources","autocomplete-list","autocomplete-plugin"],axes:["axis-numeric","axis-category","axis-time","axis-stacked"],"axes-base":["axis-numeric-base","axis-category-base","axis-time-base","axis-stacked-base"],base:["base-base","base-pluginhost","base-build"],cache:["cache-base","cache-offline","cache-plugin"],charts:["charts-base"],collection:["array-extras","arraylist","arraylist-add","arraylist-filter","array-invoke"],color:["color-base","color-hsl","color-harmony"],controller:["router"],dataschema:["dataschema-base","dataschema-json","dataschema-xml","dataschema-array","dataschema-text"],datasource:["datasource-local","datasource-io","datasource-get","datasource-function","datasource-cache","datasource-jsonschema","datasource-xmlschema","datasource-arrayschema","datasource-textschema","datasource-polling"],datatable:["datatable-core","datatable-table","datatable-head","datatable-body","datatable-base","datatable-column-widths","datatable-message","datatable-mutable","datatable-sort","datatable-datasource"],datatype:["datatype-date","datatype-number","datatype-xml"],"datatype-date":["datatype-date-parse","datatype-date-format","datatype-date-math"],"datatype-number":["datatype-number-parse","datatype-number-format"],"datatype-xml":["datatype-xml-parse","datatype-xml-format"],dd:["dd-ddm-base","dd-ddm","dd-ddm-drop","dd-drag","dd-proxy","dd-constrain","dd-drop","dd-scroll","dd-delegate"],dom:["dom-base","dom-screen","dom-style","selector-native","selector"],editor:["frame","editor-selection","exec-command","editor-base","editor-para","editor-br","editor-bidi","editor-tab","createlink-base"],event:["event-base","event-delegate","event-synthetic","event-mousewheel","event-mouseenter","event-key","event-focus","event-resize","event-hover","event-outside","event-touch","event-move","event-flick","event-valuechange","event-tap"],"event-custom":["event-custom-base","event-custom-complex"],"event-gestures":["event-flick","event-move"],handlebars:["handlebars-compiler"],highlight:["highlight-base","highlight-accentfold"],history:["history-base","history-hash","history-html5"],io:["io-base","io-xdr","io-form","io-upload-iframe","io-queue"],json:["json-parse","json-stringify"],loader:["loader-base","loader-rollup","loader-yui3"],node:["node-base","node-event-delegate","node-pluginhost","node-screen","node-style"],pluginhost:["pluginhost-base","pluginhost-config"],querystring:["querystring-parse","querystring-stringify"],recordset:["recordset-base","recordset-sort","recordset-filter","recordset-indexer"],resize:["resize-base","resize-proxy","resize-constrain"],slider:["slider-base","slider-value-range","clickable-rail","range-slider"],template:["template-base","template-micro"],text:["text-accentfold","text-wordbreak"],widget:["widget-base","widget-htmlparser","widget-skin","widget-uievents"]}},"3.17.2",{use:["yui-base","get","features","intl-base","yui-log","yui-later","loader-base","loader-rollup","loader-yui3"]}),YUI.add("get",function(e,t){var n=e.Lang,r,i,s;e.Get=i={cssOptions:{attributes:{rel:"stylesheet"},doc:e.config.linkDoc||e.config.doc,pollInterval:50},jsOptions:{autopurge:!0,doc:e.config.scriptDoc||e.config.doc},options
+:{attributes:{charset:"utf-8"},purgethreshold:20},REGEX_CSS:/\.css(?:[?;].*)?$/i,REGEX_JS:/\.js(?:[?;].*)?$/i,_insertCache:{},_pending:null,_purgeNodes:[],_queue:[],abort:function(e){var t,n,r,i,s;if(!e.abort){n=e,s=this._pending,e=null;if(s&&s.transaction.id===n)e=s.transaction,this._pending=null;else for(t=0,i=this._queue.length;t<i;++t){r=this._queue[t].transaction;if(r.id===n){e=r,this._queue.splice(t,1);break}}}e&&e.abort()},css:function(e,t,n){return this._load("css",e,t,n)},js:function(e,t,n){return this._load("js",e,t,n)},load:function(e,t,n){return this._load(null,e,t,n)},_autoPurge:function(e){e&&this._purgeNodes.length>=e&&this._purge(this._purgeNodes)},_getEnv:function(){var t=e.config.doc,n=e.UA;return this._env={async:t&&t.createElement("script").async===!0||n.ie>=10,cssFail:n.gecko>=9||n.compareVersions(n.webkit,535.24)>=0,cssLoad:(!n.gecko&&!n.webkit||n.gecko>=9||n.compareVersions(n.webkit,535.24)>=0)&&!(n.chrome&&n.chrome<=18),preservesScriptOrder:!!(n.gecko||n.opera||n.ie&&n.ie>=10)}},_getTransaction:function(t,r){var i=[],o,u,a,f;n.isArray(t)||(t=[t]),r=e.merge(this.options,r),r.attributes=e.merge(this.options.attributes,r.attributes);for(o=0,u=t.length;o<u;++o){f=t[o],a={attributes:{}};if(typeof f=="string")a.url=f;else{if(!f.url)continue;e.mix(a,f,!1,null,0,!0),f=f.url}e.mix(a,r,!1,null,0,!0),a.type||(this.REGEX_CSS.test(f)?a.type="css":(!this.REGEX_JS.test(f),a.type="js")),e.mix(a,a.type==="js"?this.jsOptions:this.cssOptions,!1,null,0,!0),a.attributes.id||(a.attributes.id=e.guid()),a.win?a.doc=a.win.document:a.win=a.doc.defaultView||a.doc.parentWindow,a.charset&&(a.attributes.charset=a.charset),i.push(a)}return new s(i,r)},_load:function(e,t,n,r){var s;return typeof n=="function"&&(r=n,n={}),n||(n={}),n.type=e,n._onFinish=i._onTransactionFinish,this._env||this._getEnv(),s=this._getTransaction(t,n),this._queue.push({callback:r,transaction:s}),this._next(),s},_onTransactionFinish:function(){i._pending=null,i._next()},_next:function(){var e;if(this._pending)return;e=this._queue.shift(),e&&(this._pending=e,e.transaction.execute(e.callback))},_purge:function(t){var n=this._purgeNodes,r=t!==n,i,s;while(s=t.pop()){if(!s._yuiget_finished)continue;s.parentNode&&s.parentNode.removeChild(s),r&&(i=e.Array.indexOf(n,s),i>-1&&n.splice(i,1))}}},i.script=i.js,i.Transaction=s=function(t,n){var r=this;r.id=s._lastId+=1,r.data=n.data,r.errors=[],r.nodes=[],r.options=n,r.requests=t,r._callbacks=[],r._queue=[],r._reqsWaiting=0,r.tId=r.id,r.win=n.win||e.config.win},s._lastId=0,s.prototype={_state:"new",abort:function(e){this._pending=null,this._pendingCSS=null,this._pollTimer=clearTimeout(this._pollTimer),this._queue=[],this._reqsWaiting=0,this.errors.push({error:e||"Aborted"}),this._finish()},execute:function(e){var t=this,n=t.requests,r=t._state,i,s,o,u;if(r==="done"){e&&e(t.errors.length?t.errors:null,t);return}e&&t._callbacks.push(e);if(r==="executing")return;t._state="executing",t._queue=o=[],t.options.timeout&&(t._timeout=setTimeout(function(){t.abort("Timeout")},t.options.timeout)),t._reqsWaiting=n.length;for(i=0,s=n.length;i<s;++i)u=n[i],u.async||u.type==="css"?t._insert(u):o.push(u);t._next()},purge:function(){i._purge(this.nodes)},_createNode:function(e,t,n){var i=n.createElement(e),s,o;r||(o=n.createElement("div"),o.setAttribute("class","a"),r=o.className==="a"?{}:{"for":"htmlFor","class":"className"});for(s in t)t.hasOwnProperty(s)&&i.setAttribute(r[s]||s,t[s]);return i},_finish:function(){var e=this.errors.length?this.errors:null,t=this.options,n=t.context||this,r,i,s;if(this._state==="done")return;this._state="done";for(i=0,s=this._callbacks.length;i<s;++i)this._callbacks[i].call(n,e,this);r=this._getEventData(),e?(t.onTimeout&&e[e.length-1].error==="Timeout"&&t.onTimeout.call(n,r),t.onFailure&&t.onFailure.call(n,r)):t.onSuccess&&t.onSuccess.call(n,r),t.onEnd&&t.onEnd.call(n,r),t._onFinish&&t._onFinish()},_getEventData:function(t){return t?e.merge(this,{abort:this.abort,purge:this.purge,request:t,url:t.url,win:t.win}):this},_getInsertBefore:function(t){var n=t.doc,r=t.insertBefore,s,o;return r?typeof r=="string"?n.getElementById(r):r:(s=i._insertCache,o=e.stamp(n),(r=s[o])?r:(r=n.getElementsByTagName("base")[0])?s[o]=r:(r=n.head||n.getElementsByTagName("head")[0],r?(r.appendChild(n.createTextNode("")),s[o]=r.lastChild):s[o]=n.getElementsByTagName("script")[0]))},_insert:function(t){function c(){u._progress("Failed to load "+t.url,t)}function h(){f&&clearTimeout(f),u._progress(null,t)}var n=i._env,r=this._getInsertBefore(t),s=t.type==="js",o=t.node,u=this,a=e.UA,f,l;o||(s?l="script":!n.cssLoad&&a.gecko?l="style":l="link",o=t.node=this._createNode(l,t.attributes,t.doc)),s?(o.setAttribute("src",t.url),t.async?o.async=!0:(n.async&&(o.async=!1),n.preservesScriptOrder||(this._pending=t))):!n.cssLoad&&a.gecko?o.innerHTML=(t.attributes.charset?'@charset "'+t.attributes.charset+'";':"")+'@import "'+t.url+'";':o.setAttribute("href",t.url),s&&a.ie&&(a.ie<9||document.documentMode&&document.documentMode<9)?o.onreadystatechange=function(){/loaded|complete/.test(o.readyState)&&(o.onreadystatechange=null,h())}:!s&&!n.cssLoad?this._poll(t):(a.ie>=10?(o.onerror=function(){setTimeout(c,0)},o.onload=function(){setTimeout(h,0)}):(o.onerror=c,o.onload=h),!n.cssFail&&!s&&(f=setTimeout(c,t.timeout||3e3))),this.nodes.push(o),r.parentNode.insertBefore(o,r)},_next:function(){if(this._pending)return;this._queue.length?this._insert(this._queue.shift()):this._reqsWaiting||this._finish()},_poll:function(t){var n=this,r=n._pendingCSS,i=e.UA.webkit,s,o,u,a,f,l;if(t){r||(r=n._pendingCSS=[]),r.push(t);if(n._pollTimer)return}n._pollTimer=null;for(s=0;s<r.length;++s){f=r[s];if(i){l=f.doc.styleSheets,u=l.length,a=f.node.href;while(--u>=0)if(l[u].href===a){r.splice(s,1),s-=1,n._progress(null,f);break}}else try{o=!!f.node.sheet.cssRules,r.splice(s,1),s-=1,n._progress(null,f)}catch(c){}}r.length&&(n._pollTimer=setTimeout(function(){n._poll.call(n)},n.options.pollInterval))},_progress:function(e,t){var n=this.options
+;e&&(t.error=e,this.errors.push({error:e,request:t})),t.node._yuiget_finished=t.finished=!0,n.onProgress&&n.onProgress.call(n.context||this,this._getEventData(t)),t.autopurge&&(i._autoPurge(this.options.purgethreshold),i._purgeNodes.push(t.node)),this._pending===t&&(this._pending=null),this._reqsWaiting-=1,this._next()}}},"3.17.2",{requires:["yui-base"]}),YUI.add("features",function(e,t){var n={};e.mix(e.namespace("Features"),{tests:n,add:function(e,t,r){n[e]=n[e]||{},n[e][t]=r},all:function(t,r){var i=n[t],s=[];return i&&e.Object.each(i,function(n,i){s.push(i+":"+(e.Features.test(t,i,r)?1:0))}),s.length?s.join(";"):""},test:function(t,r,i){i=i||[];var s,o,u,a=n[t],f=a&&a[r];return!f||(s=f.result,e.Lang.isUndefined(s)&&(o=f.ua,o&&(s=e.UA[o]),u=f.test,u&&(!o||s)&&(s=u.apply(e,i)),f.result=s)),s}});var r=e.Features.add;r("load","0",{name:"app-transitions-native",test:function(e){var t=e.config.doc,n=t?t.documentElement:null;return n&&n.style?"MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style:!1},trigger:"app-transitions"}),r("load","1",{name:"autocomplete-list-keys",test:function(e){return!e.UA.ios&&!e.UA.android},trigger:"autocomplete-list"}),r("load","2",{name:"dd-gestures",trigger:"dd-drag",ua:"touchEnabled"}),r("load","3",{name:"dom-style-ie",test:function(e){var t=e.Features.test,n=e.Features.add,r=e.config.win,i=e.config.doc,s="documentElement",o=!1;return n("style","computedStyle",{test:function(){return r&&"getComputedStyle"in r}}),n("style","opacity",{test:function(){return i&&"opacity"in i[s].style}}),o=!t("style","opacity")&&!t("style","computedStyle"),o},trigger:"dom-style"}),r("load","4",{name:"editor-para-ie",trigger:"editor-para",ua:"ie",when:"instead"}),r("load","5",{name:"event-base-ie",test:function(e){var t=e.config.doc&&e.config.doc.implementation;return t&&!t.hasFeature("Events","2.0")},trigger:"node-base"}),r("load","6",{name:"graphics-canvas",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"}),r("load","7",{name:"graphics-canvas-default",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"}),r("load","8",{name:"graphics-svg",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"}),r("load","9",{name:"graphics-svg-default",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"}),r("load","10",{name:"graphics-vml",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"}),r("load","11",{name:"graphics-vml-default",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"}),r("load","12",{name:"history-hash-ie",test:function(e){var t=e.config.doc&&e.config.doc.documentMode;return e.UA.ie&&(!("onhashchange"in e.config.win)||!t||t<8)},trigger:"history-hash"}),r("load","13",{name:"io-nodejs",trigger:"io-base",ua:"nodejs"}),r("load","14",{name:"json-parse-shim",test:function(e){function i(e,t){return e==="ok"?!0:t}var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONParse!==!1&&!!n;if(r)try{r=n.parse('{"ok":false}',i).ok}catch(s){r=!1}return!r},trigger:"json-parse"}),r("load","15",{name:"json-stringify-shim",test:function(e){var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONStringify!==!1&&!!n;if(r)try{r="0"===n.stringify(0)}catch(i){r=!1}return!r},trigger:"json-stringify"}),r("load","16",{name:"scrollview-base-ie",trigger:"scrollview-base",ua:"ie"}),r("load","17",{name:"selector-css2",test:function(e){var t=e.config.doc,n=t&&!("querySelectorAll"in t);return n},trigger:"selector"}),r("load","18",{name:"transition-timer",test:function(e){var t=e.config.doc,n=t?t.documentElement:null,r=!0;return n&&n.style&&(r=!("MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style)),r},trigger:"transition"}),r("load","19",{name:"widget-base-ie",trigger:"widget-base",ua:"ie"}),r("load","20",{name:"yql-jsonp",test:function(e){return!e.UA.nodejs&&!e.UA.winjs},trigger:"yql"}),r("load","21",{name:"yql-nodejs",trigger:"yql",ua:"nodejs"}),r("load","22",{name:"yql-winjs",trigger:"yql",ua:"winjs"})},"3.17.2",{requires:["yui-base"]}),YUI.add("intl-base",function(e,t){var n=/[, ]/;e.mix(e.namespace("Intl"),{lookupBestLang:function(t,r){function a(e){var t;for(t=0;t<r.length;t+=1)if(e.toLowerCase()===r[t].toLowerCase())return r[t]}var i,s,o,u;e.Lang.isString(t)&&(t=t.split(n));for(i=0;i<t.length;i+=1){s=t[i];if(!s||s==="*")continue;while(s.length>0){o=a(s);if(o)return o;u=s.lastIndexOf("-");if(!(u>=0))break;s=s.substring(0,u),u>=2&&s.charAt(u-2)==="-"&&(s=s.substring(0,u-2))}}return""}})},"3.17.2",{requires:["yui-base"]}),YUI.add("yui-log",function(e,t){var n=e,r="yui:log",i="undefined",s={debug:1,info:2,warn:4,error:8};n.log=function(e,t,o,u){var a,f,l,c,h,p,d=n,v=d.config,m=d.fire?d:YUI.Env
+.globalEvents;return v.debug&&(o=o||"",typeof o!="undefined"&&(f=v.logExclude,l=v.logInclude,!l||o in l?l&&o in l?a=!l[o]:f&&o in f&&(a=f[o]):a=1,typeof t=="undefined"&&(t="info"),d.config.logLevel=d.config.logLevel||"debug",p=s[d.config.logLevel.toLowerCase()],t in s&&s[t]<p&&(a=1)),a||(v.useBrowserConsole&&(c=o?o+": "+e:e,d.Lang.isFunction(v.logFn)?v.logFn.call(d,e,t,o):typeof console!==i&&console.log?(h=t&&console[t]&&t in s?t:"log",console[h](c)):typeof opera!==i&&opera.postError(c)),m&&!u&&(m===d&&!m.getEvent(r)&&m.publish(r,{broadcast:2}),m.fire(r,{msg:e,cat:t,src:o})))),d},n.message=function(){return n.log.apply(n,arguments)}},"3.17.2",{requires:["yui-base"]}),YUI.add("yui-later",function(e,t){var n=[];e.later=function(t,r,i,s,o){t=t||0,s=e.Lang.isUndefined(s)?n:e.Array(s),r=r||e.config.win||e;var u=!1,a=r&&e.Lang.isString(i)?r[i]:i,f=function(){u||(a.apply?a.apply(r,s||n):a(s[0],s[1],s[2],s[3]))},l=o?setInterval(f,t):setTimeout(f,t);return{id:l,interval:o,cancel:function(){u=!0,this.interval?clearInterval(l):clearTimeout(l)}}},e.Lang.later=e.later},"3.17.2",{requires:["yui-base"]}),YUI.add("loader-base",function(e,t){(function(){var t=e.version,n="/build/",r=t+"/",i=e.Env.base,s="gallery-2014.05.29-15-46",o="2in3",u="4",a="2.9.0",f=i+"combo?",l={version:t,root:r,base:e.Env.base,comboBase:f,skin:{defaultSkin:"sam",base:"assets/skins/",path:"skin.css",after:["cssreset","cssfonts","cssgrids","cssbase","cssreset-context","cssfonts-context"]},groups:{},patterns:{}},c=l.groups,h=function(e,t,r){var s=o+"."+(e||u)+"/"+(t||a)+n,l=r&&r.base?r.base:i,h=r&&r.comboBase?r.comboBase:f;c.yui2.base=l+s,c.yui2.root=s,c.yui2.comboBase=h},p=function(e,t){var r=(e||s)+n,o=t&&t.base?t.base:i,u=t&&t.comboBase?t.comboBase:f;c.gallery.base=o+r,c.gallery.root=r,c.gallery.comboBase=u};c[t]={},c.gallery={ext:!1,combine:!0,comboBase:f,update:p,patterns:{"gallery-":{},"lang/gallery-":{},"gallerycss-":{type:"css"}}},c.yui2={combine:!0,ext:!1,comboBase:f,update:h,patterns:{"yui2-":{configFn:function(e){/-skin|reset|fonts|grids|base/.test(e.name)&&(e.type="css",e.path=e.path.replace(/\.js/,".css"),e.path=e.path.replace(/\/yui2-skin/,"/assets/skins/sam/yui2-skin"))}}}},p(),h(),YUI.Env[t]&&e.mix(l,YUI.Env[t],!1,["modules","groups","skin"],0,!0),YUI.Env[t]=l})();var n={},r=[],i=1024,s=YUI.Env,o=s._loaded,u="css",a="js",f="intl",l="sam",c=e.version,h="",p=e.Object,d=p.each,v=e.Array,m=s._loaderQueue,g=s[c],y="skin-",b=e.Lang,w=s.mods,E,S=function(e,t,n,r){var i=e+"/"+t;return r||(i+="-min"),i+="."+(n||u),i};YUI.Env._cssLoaded||(YUI.Env._cssLoaded={}),e.Env.meta=g,e.Loader=function(t){var n=this;t=t||{},E=g.md5,n.context=e,t.doBeforeLoader&&t.doBeforeLoader.apply(n,arguments),n.base=e.Env.meta.base+e.Env.meta.root,n.comboBase=e.Env.meta.comboBase,n.combine=t.base&&t.base.indexOf(n.comboBase.substr(0,20))>-1,n.comboSep="&",n.maxURLLength=i,n.ignoreRegistered=t.ignoreRegistered,n.root=e.Env.meta.root,n.timeout=0,n.forceMap={},n.allowRollup=!1,n.filters={},n.required={},n.patterns={},n.moduleInfo={},n.groups=e.merge(e.Env.meta.groups),n.skin=e.merge(e.Env.meta.skin),n.conditions={},n.config=t,n._internal=!0,n._populateConditionsCache(),n.loaded=o[c],n.async=!0,n._inspectPage(),n._internal=!1,n._config(t),n.forceMap=n.force?e.Array.hash(n.force):{},n.testresults=null,e.config.tests&&(n.testresults=e.config.tests),n.sorted=[],n.dirty=!0,n.inserted={},n.skipped={},n.tested={},n.ignoreRegistered&&n._resetModules()},e.Loader.prototype={getModuleInfo:function(t){var n=this.moduleInfo[t],r,i,o,a;return n?n:(r=g.modules,i=s._renderedMods,o=this._internal,i&&i.hasOwnProperty(t)&&!this.ignoreRegistered?this.moduleInfo[t]=e.merge(i[t]):r.hasOwnProperty(t)&&(this._internal=!0,a=this.addModule(r[t],t),a&&a.type===u&&this.isCSSLoaded(a.name,!0)&&(this.loaded[a.name]=!0),this._internal=o),this.moduleInfo[t])},_expandAliases:function(t){var n=[],r=YUI.Env.aliases,i,s;t=e.Array(t);for(i=0;i<t.length;i+=1)s=t[i],n.push.apply(n,r[s]?r[s]:[s]);return n},_populateConditionsCache:function(){var t=g.modules,n=s._conditions,r,i,o,u;if(n&&!this.ignoreRegistered)for(r in n)n.hasOwnProperty(r)&&(this.conditions[r]=e.merge(n[r]));else{for(r in t)if(t.hasOwnProperty(r)&&t[r].condition){o=this._expandAliases(t[r].condition.trigger);for(i=0;i<o.length;i+=1)u=o[i],this.conditions[u]=this.conditions[u]||{},this.conditions[u][t[r].name||r]=t[r].condition}s._conditions=this.conditions}},_resetModules:function(){var e=this,t,n,r,i,s;for(t in e.moduleInfo)if(e.moduleInfo.hasOwnProperty(t)&&e.moduleInfo[t]){r=e.moduleInfo[t],i=r.name,s=YUI.Env.mods[i]?YUI.Env.mods[i].details:null,s&&(e.moduleInfo[i]._reset=!0,e.moduleInfo[i].requires=s.requires||[],e.moduleInfo[i].optional=s.optional||[],e.moduleInfo[i].supersedes=s.supercedes||[]);if(r.defaults)for(n in r.defaults)r.defaults.hasOwnProperty(n)&&r[n]&&(r[n]=r.defaults[n]);r.langCache=undefined,r.skinCache=undefined,r.skinnable&&e._addSkin(e.skin.defaultSkin,r.name)}},REGEX_CSS:/\.css(?:[?;].*)?$/i,FILTER_DEFS:{RAW:{searchExp:"-min\\.js",replaceStr:".js"},DEBUG:{searchExp:"-min\\.js",replaceStr:"-debug.js"},COVERAGE:{searchExp:"-min\\.js",replaceStr:"-coverage.js"}},_inspectPage:function(){var e=this,t,n,r,i,s;for(s in w)w.hasOwnProperty(s)&&(t=w[s],t.details&&(n=e.getModuleInfo(t.name),r=t.details.requires,i=n&&n.requires,n?!n._inspected&&r&&i.length!==r.length&&delete n.expanded:n=e.addModule(t.details,s),n._inspected=!0))},_requires:function(e,t){var n,r,i,s,o=this.getModuleInfo(e),a=this.getModuleInfo(t);if(!o||!a)return!1;r=o.expanded_map,i=o.after_map;if(i&&t in i)return!0;i=a.after_map;if(i&&e in i)return!1;s=a.supersedes;if(s)for(n=0;n<s.length;n++)if(this._requires(e,s[n]))return!0;s=o.supersedes;if(s)for(n=0;n<s.length;n++)if(this._requires(t,s[n]))return!1;return r&&t in r?!0:o.ext&&o.type===u&&!a.ext&&a.type===u?!0:!1},_config:function(t){var n,r,i,s,o,u,a,f=this,l=[],c,h;if(t)for(n in t)if(t.hasOwnProperty(n)){i=t[n];if(n==="require")f.require(i);else if(n==="skin")typeof i=="string"&&(f.skin.defaultSkin=
+t.skin,i={defaultSkin:i}),e.mix(f.skin,i,!0);else if(n==="groups"){for(r in i)if(i.hasOwnProperty(r)){a=r,u=i[r],f.addGroup(u,a);if(u.aliases)for(s in u.aliases)u.aliases.hasOwnProperty(s)&&f.addAlias(u.aliases[s],s)}}else if(n==="modules")for(r in i)i.hasOwnProperty(r)&&f.addModule(i[r],r);else if(n==="aliases")for(r in i)i.hasOwnProperty(r)&&f.addAlias(i[r],r);else n==="gallery"?this.groups.gallery.update&&this.groups.gallery.update(i,t):n==="yui2"||n==="2in3"?this.groups.yui2.update&&this.groups.yui2.update(t["2in3"],t.yui2,t):f[n]=i}o=f.filter,b.isString(o)&&(o=o.toUpperCase(),f.filterName=o,f.filter=f.FILTER_DEFS[o],o==="DEBUG"&&f.require("yui-log","dump"));if(f.filterName&&f.coverage&&f.filterName==="COVERAGE"&&b.isArray(f.coverage)&&f.coverage.length){for(n=0;n<f.coverage.length;n++)c=f.coverage[n],h=f.getModuleInfo(c),h&&h.use?l=l.concat(h.use):l.push(c);f.filters=f.filters||{},e.Array.each(l,function(e){f.filters[e]=f.FILTER_DEFS.COVERAGE}),f.filterName="RAW",f.filter=f.FILTER_DEFS[f.filterName]}},formatSkin:function(e,t){var n=y+e;return t&&(n=n+"-"+t),n},_addSkin:function(e,t,n){var r,i,s,o=this.skin,u=t&&this.getModuleInfo(t),a=u&&u.ext;return t&&(i=this.formatSkin(e,t),this.getModuleInfo(i)||(r=u.pkg||t,s={skin:!0,name:i,group:u.group,type:"css",after:o.after,path:(n||r)+"/"+o.base+e+"/"+t+".css",ext:a},u.base&&(s.base=u.base),u.configFn&&(s.configFn=u.configFn),this.addModule(s,i))),i},addAlias:function(e,t){YUI.Env.aliases[t]=e,this.addModule({name:t,use:e})},addGroup:function(e,t){var n=e.modules,r=this,i,s;t=t||e.name,e.name=t,r.groups[t]=e;if(e.patterns)for(i in e.patterns)e.patterns.hasOwnProperty(i)&&(e.patterns[i].group=t,r.patterns[i]=e.patterns[i]);if(n)for(i in n)n.hasOwnProperty(i)&&(s=n[i],typeof s=="string"&&(s={name:i,fullpath:s}),s.group=t,r.addModule(s,i))},addModule:function(t,n){n=n||t.name,typeof t=="string"&&(t={name:n,fullpath:t});var r,i,o,f,l,c,p,d,m,g,y,b,w,E,x,T,N,C,k,L,A,O,M=this.moduleInfo[n],_=this.conditions,D;M&&M.temp&&(t=e.merge(M,t)),t.name=n;if(!t||!t.name)return null;t.type||(t.type=a,O=t.path||t.fullpath,O&&this.REGEX_CSS.test(O)&&(t.type=u)),!t.path&&!t.fullpath&&(t.path=S(n,n,t.type)),t.supersedes=t.supersedes||t.use,t.ext="ext"in t?t.ext:this._internal?!1:!0,r=t.submodules,this.moduleInfo[n]=t,t.requires=t.requires||[];if(this.requires)for(i=0;i<this.requires.length;i++)t.requires.push(this.requires[i]);if(t.group&&this.groups&&this.groups[t.group]){A=this.groups[t.group];if(A.requires)for(i=0;i<A.requires.length;i++)t.requires.push(A.requires[i])}t.defaults||(t.defaults={requires:t.requires?[].concat(t.requires):null,supersedes:t.supersedes?[].concat(t.supersedes):null,optional:t.optional?[].concat(t.optional):null}),t.skinnable&&t.ext&&t.temp&&(k=this._addSkin(this.skin.defaultSkin,n),t.requires.unshift(k)),t.requires.length&&(t.requires=this.filterRequires(t.requires)||[]);if(!t.langPack&&t.lang){y=v(t.lang);for(g=0;g<y.length;g++)T=y[g],b=this.getLangPackName(T,n),p=this.getModuleInfo(b),p||(p=this._addLangPack(T,t,b))}if(r){l=t.supersedes||[],o=0;for(i in r)if(r.hasOwnProperty(i)){c=r[i],c.path=c.path||S(n,i,t.type),c.pkg=n,c.group=t.group,c.supersedes&&(l=l.concat(c.supersedes)),p=this.addModule(c,i),l.push(i);if(p.skinnable){t.skinnable=!0,C=this.skin.overrides;if(C&&C[i])for(g=0;g<C[i].length;g++)k=this._addSkin(C[i][g],i,n),l.push(k);k=this._addSkin(this.skin.defaultSkin,i,n),l.push(k)}if(c.lang&&c.lang.length){y=v(c.lang);for(g=0;g<y.length;g++)T=y[g],b=this.getLangPackName(T,n),w=this.getLangPackName(T,i),p=this.getModuleInfo(b),p||(p=this._addLangPack(T,t,b)),E=E||v.hash(p.supersedes),w in E||p.supersedes.push(w),t.lang=t.lang||[],x=x||v.hash(t.lang),T in x||t.lang.push(T),b=this.getLangPackName(h,n),w=this.getLangPackName(h,i),p=this.getModuleInfo(b),p||(p=this._addLangPack(T,t,b)),w in E||p.supersedes.push(w)}o++}t.supersedes=v.dedupe(l),this.allowRollup&&(t.rollup=o<4?o:Math.min(o-1,4))}d=t.plugins;if(d)for(i in d)d.hasOwnProperty(i)&&(m=d[i],m.pkg=n,m.path=m.path||S(n,i,t.type),m.requires=m.requires||[],m.group=t.group,this.addModule(m,i),t.skinnable&&this._addSkin(this.skin.defaultSkin,i,n));if(t.condition){f=this._expandAliases(t.condition.trigger);for(i=0;i<f.length;i++)D=f[i],L=t.condition.when,_[D]=_[D]||{},_[D][n]=t.condition,L&&L!=="after"?L==="instead"&&(t.supersedes=t.supersedes||[],t.supersedes.push(D)):(t.after=t.after||[],t.after.push(D))}return t.supersedes&&(t.supersedes=this.filterRequires(t.supersedes)),t.after&&(t.after=this.filterRequires(t.after),t.after_map=v.hash(t.after)),t.configFn&&(N=t.configFn(t),N===!1&&(delete this.moduleInfo[n],delete s._renderedMods[n],t=null)),t&&(s._renderedMods||(s._renderedMods={}),s._renderedMods[n]=e.mix(s._renderedMods[n]||{},t),s._conditions=_),t},require:function(t){var n=typeof t=="string"?v(arguments):t;this.dirty=!0,this.required=e.merge(this.required,v.hash(this.filterRequires(n))),this._explodeRollups()},_explodeRollups:function(){var e=this,t,n,r,i,s,o,u,a=e.required;if(!e.allowRollup){for(r in a)if(a.hasOwnProperty(r)){t=e.getModule(r);if(t&&t.use){o=t.use.length;for(i=0;i<o;i++){n=e.getModule(t.use[i]);if(n&&n.use){u=n.use.length;for(s=0;s<u;s++)a[n.use[s]]=!0}else a[t.use[i]]=!0}}}e.required=a}},filterRequires:function(t){if(t){e.Lang.isArray(t)||(t=[t]),t=e.Array(t);var n=[],r,i,s,o;for(r=0;r<t.length;r++){i=this.getModule(t[r]);if(i&&i.use)for(s=0;s<i.use.length;s++)o=this.getModule(i.use[s]),o&&o.use&&o.name!==i.name?n=e.Array.dedupe([].concat(n,this.filterRequires(o.use))):n.push(i.use[s]);else n.push(t[r])}t=n}return t},_canBeAttached:function(t){return t=this.getModule(t),t&&t.test?(t.hasOwnProperty("_testResult")||(t._testResult=t.test(e)),t._testResult):!0},getRequires:function(t){if(!t)return r;if(t._parsed)return t.expanded||r;var n,i,s,o,u,a,l,c=this.testresults,m=t.name,g,y=w[m]&&w[m].details,b=t.optionalRequires,E,S,x,T,N,C,k,L,A,O,M=t.lang||t.intl,_=e.Features&&e.Features.tests.load,D,P;t.temp&&y&&(N=t,t=this.addModule(y,m),t.group=N.group,t.pkg=
+N.pkg,delete t.expanded),P=!!this.lang&&t.langCache!==this.lang||t.skinCache!==this.skin.defaultSkin;if(t.expanded&&!P)return t.expanded;if(b)for(n=0,o=b.length;n<o;n++)this._canBeAttached(b[n])&&t.requires.push(b[n]);E=[],D={},T=this.filterRequires(t.requires),t.lang&&(E.unshift("intl"),T.unshift("intl"),M=!0),C=this.filterRequires(t.optional),t._parsed=!0,t.langCache=this.lang,t.skinCache=this.skin.defaultSkin;for(n=0;n<T.length;n++)if(!D[T[n]]){E.push(T[n]),D[T[n]]=!0,i=this.getModule(T[n]);if(i){u=this.getRequires(i),M=M||i.expanded_map&&f in i.expanded_map;for(s=0;s<u.length;s++)E.push(u[s])}}T=this.filterRequires(t.supersedes);if(T)for(n=0;n<T.length;n++)if(!D[T[n]]){t.submodules&&E.push(T[n]),D[T[n]]=!0,i=this.getModule(T[n]);if(i){u=this.getRequires(i),M=M||i.expanded_map&&f in i.expanded_map;for(s=0;s<u.length;s++)E.push(u[s])}}if(C&&this.loadOptional)for(n=0;n<C.length;n++)if(!D[C[n]]){E.push(C[n]),D[C[n]]=!0,i=this.getModuleInfo(C[n]);if(i){u=this.getRequires(i),M=M||i.expanded_map&&f in i.expanded_map;for(s=0;s<u.length;s++)E.push(u[s])}}g=this.conditions[m];if(g){t._parsed=!1;if(c&&_)d(c,function(e,t){var n=_[t].name;!D[n]&&_[t].trigger===m&&e&&_[t]&&(D[n]=!0,E.push(n))});else for(n in g)if(g.hasOwnProperty(n)&&!D[n]){x=g[n],S=x&&(!x.ua&&!x.test||x.ua&&e.UA[x.ua]||x.test&&x.test(e,T));if(S){D[n]=!0,E.push(n),i=this.getModule(n);if(i){u=this.getRequires(i);for(s=0;s<u.length;s++)E.push(u[s])}}}}if(t.skinnable){L=this.skin.overrides;for(n in YUI.Env.aliases)YUI.Env.aliases.hasOwnProperty(n)&&e.Array.indexOf(YUI.Env.aliases[n],m)>-1&&(A=n);if(L&&(L[m]||A&&L[A])){O=m,L[A]&&(O=A);for(n=0;n<L[O].length;n++)k=this._addSkin(L[O][n],m),this.isCSSLoaded(k,this._boot)||E.push(k)}else k=this._addSkin(this.skin.defaultSkin,m),this.isCSSLoaded(k,this._boot)||E.push(k)}return t._parsed=!1,M&&(t.lang&&!t.langPack&&e.Intl&&(l=e.Intl.lookupBestLang(this.lang||h,t.lang),a=this.getLangPackName(l,m),a&&E.unshift(a)),E.unshift(f)),t.expanded_map=v.hash(E),t.expanded=p.keys(t.expanded_map),t.expanded},isCSSLoaded:function(t,n){if(!t||!YUI.Env.cssStampEl||!n&&this.ignoreRegistered)return!1;var r=YUI.Env.cssStampEl,i=!1,s=YUI.Env._cssLoaded[t],o=r.currentStyle;return s!==undefined?s:(r.className=t,o||(o=e.config.doc.defaultView.getComputedStyle(r,null)),o&&o.display==="none"&&(i=!0),r.className="",YUI.Env._cssLoaded[t]=i,i)},getProvides:function(t){var r=this.getModule(t),i,s;return r?(r&&!r.provides&&(i={},s=r.supersedes,s&&v.each(s,function(t){e.mix(i,this.getProvides(t))},this),i[t]=!0,r.provides=i),r.provides):n},calculate:function(e,t){if(e||t||this.dirty)e&&this._config(e),this._init||this._setup(),this._explode(),this.allowRollup?this._rollup():this._explodeRollups(),this._reduce(),this._sort()},_addLangPack:function(t,n,r){var i=n.name,s,o,u=this.getModuleInfo(r);return u||(s=S(n.pkg||i,r,a,!0),o={path:s,intl:!0,langPack:!0,ext:n.ext,group:n.group,supersedes:[]},n.root&&(o.root=n.root),n.base&&(o.base=n.base),n.configFn&&(o.configFn=n.configFn),this.addModule(o,r),t&&(e.Env.lang=e.Env.lang||{},e.Env.lang[t]=e.Env.lang[t]||{},e.Env.lang[t][i]=!0)),this.getModuleInfo(r)},_setup:function(){var t=this.moduleInfo,n,r,i,o,u,a;for(n in t)t.hasOwnProperty(n)&&(o=t[n],o&&(o.requires=v.dedupe(o.requires),o.lang&&(a=this.getLangPackName(h,n),this._addLangPack(null,o,a))));u={},this.ignoreRegistered||e.mix(u,s.mods),this.ignore&&e.mix(u,v.hash(this.ignore));for(i in u)u.hasOwnProperty(i)&&e.mix(u,this.getProvides(i));if(this.force)for(r=0;r<this.force.length;r++)this.force[r]in u&&delete u[this.force[r]];e.mix(this.loaded,u),this._init=!0},getLangPackName:function(e,t){return"lang/"+t+(e?"_"+e:"")},_explode:function(){var t=this.required,n,r,i={},s=this,o,u;s.dirty=!1,s._explodeRollups(),t=s.required;for(o in t)t.hasOwnProperty(o)&&(i[o]||(i[o]=!0,n=s.getModule(o),n&&(u=n.expound,u&&(t[u]=s.getModule(u),r=s.getRequires(t[u]),e.mix(t,v.hash(r))),r=s.getRequires(n),e.mix(t,v.hash(r)))))},_patternTest:function(e,t){return e.indexOf(t)>-1},getModule:function(t){if(!t)return null;var n,r,i,s=this.getModuleInfo(t),o=this.patterns;if(!s||s&&s.ext)for(i in o)if(o.hasOwnProperty(i)){n=o[i],n.test||(n.test=this._patternTest);if(n.test(t,i)){r=n;break}}return s?r&&s&&r.configFn&&!s.configFn&&(s.configFn=r.configFn,s.configFn(s)):r&&(n.action?n.action.call(this,t,i):(s=this.addModule(e.merge(r,{test:void 0,temp:!0}),t),r.configFn&&(s.configFn=r.configFn))),s},_rollup:function(){},_reduce:function(e){e=e||this.required;var t,n,r,i,s=this.loadType,o=this.ignore?v.hash(this.ignore):!1;for(t in e)if(e.hasOwnProperty(t)){i=this.getModule(t),((this.loaded[t]||w[t])&&!this.forceMap[t]&&!this.ignoreRegistered||s&&i&&i.type!==s)&&delete e[t],o&&o[t]&&delete e[t],r=i&&i.supersedes;if(r)for(n=0;n<r.length;n++)r[n]in e&&delete e[r[n]]}return e},_finish:function(e,t){m.running=!1;var n=this.onEnd;n&&n.call(this.context,{msg:e,data:this.data,success:t}),this._continue()},_onSuccess:function(){var t=this,n=e.merge(t.skipped),r,i=[],s=t.requireRegistration,o,u,f,l;for(f in n)n.hasOwnProperty(f)&&delete t.inserted[f];t.skipped={};for(f in t.inserted)t.inserted.hasOwnProperty(f)&&(l=t.getModule(f),!l||!s||l.type!==a||f in YUI.Env.mods?e.mix(t.loaded,t.getProvides(f)):i.push(f));r=t.onSuccess,u=i.length?"notregistered":"success",o=!i.length,r&&r.call(t.context,{msg:u,data:t.data,success:o,failed:i,skipped:n}),t._finish(u,o)},_onProgress:function(e){var t=this,n;if(e.data&&e.data.length)for(n=0;n<e.data.length;n++)e.data[n]=t.getModule(e.data[n].name);t.onProgress&&t.onProgress.call(t.context,{name:e.url,data:e.data})},_onFailure:function(e){var t=this.onFailure,n=[],r=0,i=e.errors.length;for(r;r<i;r++)n.push(e.errors[r].error);n=n.join(","),t&&t.call(this.context,{msg:n,data:this.data,success:!1}),this._finish(n,!1)},_onTimeout:function(e){var t=this.onTimeout;t&&t.call(this.context,{msg:"timeout",data:this.data,success:!1,transaction:e})},_sort:function(){var e,t=this.required,n={};this.sorted=[];for(e in t)!n[e]&&t.hasOwnProperty(e)&&
+this._visit(e,n)},_visit:function(e,t){var n,r,i,s,o,u,a,f,l;t[e]=!0,n=this.required,i=this.moduleInfo[e],r=this.conditions[e]||{};if(i){o=i.expanded||i.requires;for(f=0,l=o.length;f<l;++f)s=o[f],u=r[s],a=u&&(!u.when||u.when==="after"),n[s]&&!t[s]&&!a&&this._visit(s,t)}this.sorted.push(e)},_insert:function(t,n,r,i){t&&this._config(t);var s=this.resolve(!i),o=this,f=0,l=0,c={},h,p;o._refetch=[],r&&(s[r===a?u:a]=[]),o.fetchCSS||(s.css=[]),s.js.length&&f++,s.css.length&&f++,p=function(t){l++;var n={},r=0,i=0,s="",u,a,p;if(t&&t.errors)for(r=0;r<t.errors.length;r++)t.errors[r].request?s=t.errors[r].request.url:s=t.errors[r],n[s]=s;if(t&&t.data&&t.data.length&&t.type==="success")for(r=0;r<t.data.length;r++){o.inserted[t.data[r].name]=!0;if(t.data[r].lang||t.data[r].skinnable)delete o.inserted[t.data[r].name],o._refetch.push(t.data[r].name)}if(l===f){o._loading=null;if(o._refetch.length){for(r=0;r<o._refetch.length;r++){h=o.getRequires(o.getModule(o._refetch[r]));for(i=0;i<h.length;i++)o.inserted[h[i]]||(c[h[i]]=h[i])}c=e.Object.keys(c);if(c.length){o.require(c),p=o.resolve(!0);if(p.cssMods.length){for(r=0;r<p.cssMods.length;r++)a=p.cssMods[r].name,delete YUI.Env._cssLoaded[a],o.isCSSLoaded(a)&&(o.inserted[a]=!0,delete o.required[a]);o.sorted=[],o._sort()}t=null,o._insert()}}t&&t.fn&&(u=t.fn,delete t.fn,u.call(o,t))}},this._loading=!0;if(!s.js.length&&!s.css.length){l=-1,p({fn:o._onSuccess});return}s.css.length&&e.Get.css(s.css,{data:s.cssMods,attributes:o.cssAttributes,insertBefore:o.insertBefore,charset:o.charset,timeout:o.timeout,context:o,onProgress:function(e){o._onProgress.call(o,e)},onTimeout:function(e){o._onTimeout.call(o,e)},onSuccess:function(e){e.type="success",e.fn=o._onSuccess,p.call(o,e)},onFailure:function(e){e.type="failure",e.fn=o._onFailure,p.call(o,e)}}),s.js.length&&e.Get.js(s.js,{data:s.jsMods,insertBefore:o.insertBefore,attributes:o.jsAttributes,charset:o.charset,timeout:o.timeout,autopurge:!1,context:o,async:o.async,onProgress:function(e){o._onProgress.call(o,e)},onTimeout:function(e){o._onTimeout.call(o,e)},onSuccess:function(e){e.type="success",e.fn=o._onSuccess,p.call(o,e)},onFailure:function(e){e.type="failure",e.fn=o._onFailure,p.call(o,e)}})},_continue:function(){!m.running&&m.size()>0&&(m.running=!0,m.next()())},insert:function(t,n,r){var i=this,s=e.merge(this);delete s.require,delete s.dirty,m.add(function(){i._insert(s,t,n,r)}),this._continue()},loadNext:function(){return},_filter:function(e,t,n){var r=this.filter,i=t&&t in this.filters,s=i&&this.filters[t],o=n||(this.getModuleInfo(t)||{}).group||null;return o&&this.groups[o]&&this.groups[o].filter&&(s=this.groups[o].filter,i=!0),e&&(i&&(r=b.isString(s)?this.FILTER_DEFS[s.toUpperCase()]||null:s),r&&(e=e.replace(new RegExp(r.searchExp,"g"),r.replaceStr))),e},_url:function(e,t,n){return this._filter((n||this.base||"")+e,t)},resolve:function(e,t){var r=this,s={js:[],jsMods:[],css:[],cssMods:[]},o;(r.skin.overrides||r.skin.defaultSkin!==l||r.ignoreRegistered)&&r._resetModules(),e&&r.calculate(),t=t||r.sorted,o=function(e){if(e){var t=e.group&&r.groups[e.group]||n,i;t.async===!1&&(e.async=t.async),i=e.fullpath?r._filter(e.fullpath,e.name):r._url(e.path,e.name,t.base||e.base);if(e.attributes||e.async===!1)i={url:i,async:e.async},e.attributes&&(i.attributes=e.attributes);s[e.type].push(i),s[e.type+"Mods"].push(e)}};var f=r.ignoreRegistered?{}:r.inserted,c={},h,p,d,v,m,g,y,b;for(b=0,y=t.length;b<y;b++){g=r.getModule(t[b]);if(!g||f[g.name])continue;m=r.groups[g.group],d=r.comboBase;if(m){if(!m.combine||g.fullpath){o(g);continue}g.combine=!0,typeof m.root=="string"&&(g.root=m.root),d=m.comboBase||d,v=m.comboSep,h=m.maxURLLength}else if(!r.combine){o(g);continue}if(!g.combine&&g.ext){o(g);continue}c[d]=c[d]||{js:[],jsMods:[],css:[],cssMods:[]},p=c[d],p.group=g.group,p.comboSep=v||r.comboSep,p.maxURLLength=h||r.maxURLLength,p[g.type+"Mods"].push(g)}var w,E,S,x,T,N,C;for(d in c)if(c.hasOwnProperty(d)){p=c[d],v=p.comboSep,h=p.maxURLLength;for(C in p)if(C===a||C===u){E=p[C+"Mods"],T=[];for(b=0,y=E.length;b<y;b+=1)g=E[b],N=(typeof g.root=="string"?g.root:r.root)+(g.path||g.fullpath),T.push(r._filter(N,g.name));S=d+T.join(v),x=S.length,h<=d.length&&(h=i);if(T.length)if(x>h){w=[];for(b=0,y=T.length;b<y;b++)w.push(T[b]),S=d+w.join(v),S.length>h&&(N=w.pop(),S=d+w.join(v),s[C].push(r._filter(S,null,p.group)),w=[],N&&w.push(N));w.length&&(S=d+w.join(v),s[C].push(r._filter(S,null,p.group)))}else s[C].push(r._filter(S,null,p.group));s[C+"Mods"]=s[C+"Mods"].concat(E)}}return s},load:function(e){if(!e)return;var t=this,n=t.resolve(!0);t.data=n,t.onEnd=function(){e.apply(t.context||t,arguments)},t.insert()}}},"3.17.2",{requires:["get","features"]}),YUI.add("loader-rollup",function(e,t){e.Loader.prototype._rollup=function(){var e,t,n,r,i=this.required,s,o=this.moduleInfo,u,a,f;if(this.dirty||!this.rollups){this.rollups={};for(e in o)o.hasOwnProperty(e)&&(n=this.getModule(e),n&&n.rollup&&(this.rollups[e]=n))}for(;;){u=!1;for(e in this.rollups)if(this.rollups.hasOwnProperty(e)&&!i[e]&&(!this.loaded[e]||this.forceMap[e])){n=this.getModule(e),r=n.supersedes||[],s=!1;if(!n.rollup)continue;a=0;for(t=0;t<r.length;t++){f=o[r[t]];if(this.loaded[r[t]]&&!this.forceMap[r[t]]){s=!1;break}if(i[r[t]]&&n.type===f.type){a++,s=a>=n.rollup;if(s)break}}s&&(i[e]=!0,u=!0,this.getRequires(n))}if(!u)break}}},"3.17.2",{requires:["loader-base"]}),YUI.add("loader-yui3",function(e,t){YUI.Env[e.version].modules=YUI.Env[e.version].modules||{},e.mix(YUI.Env[e.version].modules,{"align-plugin":{requires:["node-screen","node-pluginhost"]},anim:{use:["anim-base","anim-color","anim-curve","anim-easing","anim-node-plugin","anim-scroll","anim-xy"]},"anim-base":{requires:["base-base","node-style","color-base"]},"anim-color":{requires:["anim-base"]},"anim-curve":{requires:["anim-xy"]},"anim-easing":{requires:["anim-base"]},"anim-node-plugin":{requires:["node-pluginhost","anim-base"]},"anim-scroll":{requires:["anim-base"]},"anim-shape":{requires:["anim-base","anim-easing","anim-color"
+,"matrix"]},"anim-shape-transform":{use:["anim-shape"]},"anim-xy":{requires:["anim-base","node-screen"]},app:{use:["app-base","app-content","app-transitions","lazy-model-list","model","model-list","model-sync-rest","model-sync-local","router","view","view-node-map"]},"app-base":{requires:["classnamemanager","pjax-base","router","view"]},"app-content":{requires:["app-base","pjax-content"]},"app-transitions":{requires:["app-base"]},"app-transitions-css":{type:"css"},"app-transitions-native":{condition:{name:"app-transitions-native",test:function(e){var t=e.config.doc,n=t?t.documentElement:null;return n&&n.style?"MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style:!1},trigger:"app-transitions"},requires:["app-transitions","app-transitions-css","parallel","transition"]},"array-extras":{requires:["yui-base"]},"array-invoke":{requires:["yui-base"]},arraylist:{requires:["yui-base"]},"arraylist-add":{requires:["arraylist"]},"arraylist-filter":{requires:["arraylist"]},arraysort:{requires:["yui-base"]},"async-queue":{requires:["event-custom"]},attribute:{use:["attribute-base","attribute-complex"]},"attribute-base":{requires:["attribute-core","attribute-observable","attribute-extras"]},"attribute-complex":{requires:["attribute-base"]},"attribute-core":{requires:["oop"]},"attribute-events":{use:["attribute-observable"]},"attribute-extras":{requires:["oop"]},"attribute-observable":{requires:["event-custom"]},autocomplete:{use:["autocomplete-base","autocomplete-sources","autocomplete-list","autocomplete-plugin"]},"autocomplete-base":{optional:["autocomplete-sources"],requires:["array-extras","base-build","escape","event-valuechange","node-base"]},"autocomplete-filters":{requires:["array-extras","text-wordbreak"]},"autocomplete-filters-accentfold":{requires:["array-extras","text-accentfold","text-wordbreak"]},"autocomplete-highlighters":{requires:["array-extras","highlight-base"]},"autocomplete-highlighters-accentfold":{requires:["array-extras","highlight-accentfold"]},"autocomplete-list":{after:["autocomplete-sources"],lang:["en","es","hu","it"],requires:["autocomplete-base","event-resize","node-screen","selector-css3","shim-plugin","widget","widget-position","widget-position-align"],skinnable:!0},"autocomplete-list-keys":{condition:{name:"autocomplete-list-keys",test:function(e){return!e.UA.ios&&!e.UA.android},trigger:"autocomplete-list"},requires:["autocomplete-list","base-build"]},"autocomplete-plugin":{requires:["autocomplete-list","node-pluginhost"]},"autocomplete-sources":{optional:["io-base","json-parse","jsonp","yql"],requires:["autocomplete-base"]},axes:{use:["axis-numeric","axis-category","axis-time","axis-stacked"]},"axes-base":{use:["axis-numeric-base","axis-category-base","axis-time-base","axis-stacked-base"]},axis:{requires:["dom","widget","widget-position","widget-stack","graphics","axis-base"]},"axis-base":{requires:["classnamemanager","datatype-number","datatype-date","base","event-custom"]},"axis-category":{requires:["axis","axis-category-base"]},"axis-category-base":{requires:["axis-base"]},"axis-numeric":{requires:["axis","axis-numeric-base"]},"axis-numeric-base":{requires:["axis-base"]},"axis-stacked":{requires:["axis-numeric","axis-stacked-base"]},"axis-stacked-base":{requires:["axis-numeric-base"]},"axis-time":{requires:["axis","axis-time-base"]},"axis-time-base":{requires:["axis-base"]},base:{use:["base-base","base-pluginhost","base-build"]},"base-base":{requires:["attribute-base","base-core","base-observable"]},"base-build":{requires:["base-base"]},"base-core":{requires:["attribute-core"]},"base-observable":{requires:["attribute-observable","base-core"]},"base-pluginhost":{requires:["base-base","pluginhost"]},button:{requires:["button-core","cssbutton","widget"]},"button-core":{requires:["attribute-core","classnamemanager","node-base","escape"]},"button-group":{requires:["button-plugin","cssbutton","widget"]},"button-plugin":{requires:["button-core","cssbutton","node-pluginhost"]},cache:{use:["cache-base","cache-offline","cache-plugin"]},"cache-base":{requires:["base"]},"cache-offline":{requires:["cache-base","json"]},"cache-plugin":{requires:["plugin","cache-base"]},calendar:{requires:["calendar-base","calendarnavigator"],skinnable:!0},"calendar-base":{lang:["de","en","es","es-AR","fr","hu","it","ja","nb-NO","nl","pt-BR","ru","zh-Hans","zh-Hans-CN","zh-Hant","zh-Hant-HK","zh-HANT-TW"],requires:["widget","datatype-date","datatype-date-math","cssgrids"],skinnable:!0},calendarnavigator:{requires:["plugin","classnamemanager","datatype-date","node"],skinnable:!0},charts:{use:["charts-base"]},"charts-base":{requires:["dom","event-mouseenter","event-touch","graphics-group","axes","series-pie","series-line","series-marker","series-area","series-spline","series-column","series-bar","series-areaspline","series-combo","series-combospline","series-line-stacked","series-marker-stacked","series-area-stacked","series-spline-stacked","series-column-stacked","series-bar-stacked","series-areaspline-stacked","series-combo-stacked","series-combospline-stacked"]},"charts-legend":{requires:["charts-base"]},classnamemanager:{requires:["yui-base"]},"clickable-rail":{requires:["slider-base"]},collection:{use:["array-extras","arraylist","arraylist-add","arraylist-filter","array-invoke"]},color:{use:["color-base","color-hsl","color-harmony"]},"color-base":{requires:["yui-base"]},"color-harmony":{requires:["color-hsl"]},"color-hsl":{requires:["color-base"]},"color-hsv":{requires:["color-base"]},console:{lang:["en","es","hu","it","ja"],requires:["yui-log","widget"],skinnable:!0},"console-filters":{requires:["plugin","console"],skinnable:!0},"content-editable":{requires:["node-base","editor-selection","stylesheet","plugin"]},controller:{use:["router"]},cookie:{requires:["yui-base"]},"createlink-base":{requires:["editor-base"]},cssbase:{after:["cssreset","cssfonts","cssgrids","cssreset-context","cssfonts-context","cssgrids-context"],type:"css"},"cssbase-context":{after:["cssreset"
+,"cssfonts","cssgrids","cssreset-context","cssfonts-context","cssgrids-context"],type:"css"},cssbutton:{type:"css"},cssfonts:{type:"css"},"cssfonts-context":{type:"css"},cssgrids:{optional:["cssnormalize"],type:"css"},"cssgrids-base":{optional:["cssnormalize"],type:"css"},"cssgrids-responsive":{optional:["cssnormalize"],requires:["cssgrids","cssgrids-responsive-base"],type:"css"},"cssgrids-units":{optional:["cssnormalize"],requires:["cssgrids-base"],type:"css"},cssnormalize:{type:"css"},"cssnormalize-context":{type:"css"},cssreset:{type:"css"},"cssreset-context":{type:"css"},dataschema:{use:["dataschema-base","dataschema-json","dataschema-xml","dataschema-array","dataschema-text"]},"dataschema-array":{requires:["dataschema-base"]},"dataschema-base":{requires:["base"]},"dataschema-json":{requires:["dataschema-base","json"]},"dataschema-text":{requires:["dataschema-base"]},"dataschema-xml":{requires:["dataschema-base"]},datasource:{use:["datasource-local","datasource-io","datasource-get","datasource-function","datasource-cache","datasource-jsonschema","datasource-xmlschema","datasource-arrayschema","datasource-textschema","datasource-polling"]},"datasource-arrayschema":{requires:["datasource-local","plugin","dataschema-array"]},"datasource-cache":{requires:["datasource-local","plugin","cache-base"]},"datasource-function":{requires:["datasource-local"]},"datasource-get":{requires:["datasource-local","get"]},"datasource-io":{requires:["datasource-local","io-base"]},"datasource-jsonschema":{requires:["datasource-local","plugin","dataschema-json"]},"datasource-local":{requires:["base"]},"datasource-polling":{requires:["datasource-local"]},"datasource-textschema":{requires:["datasource-local","plugin","dataschema-text"]},"datasource-xmlschema":{requires:["datasource-local","plugin","datatype-xml","dataschema-xml"]},datatable:{use:["datatable-core","datatable-table","datatable-head","datatable-body","datatable-base","datatable-column-widths","datatable-message","datatable-mutable","datatable-sort","datatable-datasource"]},"datatable-base":{requires:["datatable-core","datatable-table","datatable-head","datatable-body","base-build","widget"],skinnable:!0},"datatable-body":{requires:["datatable-core","view","classnamemanager"]},"datatable-column-widths":{requires:["datatable-base"]},"datatable-core":{requires:["escape","model-list","node-event-delegate"]},"datatable-datasource":{requires:["datatable-base","plugin","datasource-local"]},"datatable-foot":{requires:["datatable-core","view"]},"datatable-formatters":{requires:["datatable-body","datatype-number-format","datatype-date-format","escape"]},"datatable-head":{requires:["datatable-core","view","classnamemanager"]},"datatable-highlight":{requires:["datatable-base","event-hover"],skinnable:!0},"datatable-keynav":{requires:["datatable-base"]},"datatable-message":{lang:["en","fr","es","hu","it"],requires:["datatable-base"],skinnable:!0},"datatable-mutable":{requires:["datatable-base"]},"datatable-paginator":{lang:["en","fr"],requires:["model","view","paginator-core","datatable-foot","datatable-paginator-templates"],skinnable:!0},"datatable-paginator-templates":{requires:["template"]},"datatable-scroll":{requires:["datatable-base","datatable-column-widths","dom-screen"],skinnable:!0},"datatable-sort":{lang:["en","fr","es","hu"],requires:["datatable-base"],skinnable:!0},"datatable-table":{requires:["datatable-core","datatable-head","datatable-body","view","classnamemanager"]},datatype:{use:["datatype-date","datatype-number","datatype-xml"]},"datatype-date":{use:["datatype-date-parse","datatype-date-format","datatype-date-math"]},"datatype-date-format":{lang:["ar","ar-JO","ca","ca-ES","da","da-DK","de","de-AT","de-DE","el","el-GR","en","en-AU","en-CA","en-GB","en-IE","en-IN","en-JO","en-MY","en-NZ","en-PH","en-SG","en-US","es","es-AR","es-BO","es-CL","es-CO","es-EC","es-ES","es-MX","es-PE","es-PY","es-US","es-UY","es-VE","fi","fi-FI","fr","fr-BE","fr-CA","fr-FR","hi","hi-IN","hu","id","id-ID","it","it-IT","ja","ja-JP","ko","ko-KR","ms","ms-MY","nb","nb-NO","nl","nl-BE","nl-NL","pl","pl-PL","pt","pt-BR","ro","ro-RO","ru","ru-RU","sv","sv-SE","th","th-TH","tr","tr-TR","vi","vi-VN","zh-Hans","zh-Hans-CN","zh-Hant","zh-Hant-HK","zh-Hant-TW"]},"datatype-date-math":{requires:["yui-base"]},"datatype-date-parse":{},"datatype-number":{use:["datatype-number-parse","datatype-number-format"]},"datatype-number-format":{},"datatype-number-parse":{requires:["escape"]},"datatype-xml":{use:["datatype-xml-parse","datatype-xml-format"]},"datatype-xml-format":{},"datatype-xml-parse":{},dd:{use:["dd-ddm-base","dd-ddm","dd-ddm-drop","dd-drag","dd-proxy","dd-constrain","dd-drop","dd-scroll","dd-delegate"]},"dd-constrain":{requires:["dd-drag"]},"dd-ddm":{requires:["dd-ddm-base","event-resize"]},"dd-ddm-base":{requires:["node","base","yui-throttle","classnamemanager"]},"dd-ddm-drop":{requires:["dd-ddm"]},"dd-delegate":{requires:["dd-drag","dd-drop-plugin","event-mouseenter"]},"dd-drag":{requires:["dd-ddm-base"]},"dd-drop":{requires:["dd-drag","dd-ddm-drop"]},"dd-drop-plugin":{requires:["dd-drop"]},"dd-gestures":{condition:{name:"dd-gestures",trigger:"dd-drag",ua:"touchEnabled"},requires:["dd-drag","event-synthetic","event-gestures"]},"dd-plugin":{optional:["dd-constrain","dd-proxy"],requires:["dd-drag"]},"dd-proxy":{requires:["dd-drag"]},"dd-scroll":{requires:["dd-drag"]},dial:{lang:["en","es","hu"],requires:["widget","dd-drag","event-mouseenter","event-move","event-key","transition","intl"],skinnable:!0},dom:{use:["dom-base","dom-screen","dom-style","selector-native","selector"]},"dom-base":{requires:["dom-core"]},"dom-core":{requires:["oop","features"]},"dom-screen":{requires:["dom-base","dom-style"]},"dom-style":{requires:["dom-base"]},"dom-style-ie":{condition:{name:"dom-style-ie",test:function(e){var t=e.Features.test,n=e.Features.add,r=e.config.win,i=e.config.doc,s="documentElement",o=!1;return n("style","computedStyle",{test:function(){return r&&"getComputedStyle"in r}})
+,n("style","opacity",{test:function(){return i&&"opacity"in i[s].style}}),o=!t("style","opacity")&&!t("style","computedStyle"),o},trigger:"dom-style"},requires:["dom-style","color-base"]},dump:{requires:["yui-base"]},editor:{use:["frame","editor-selection","exec-command","editor-base","editor-para","editor-br","editor-bidi","editor-tab","createlink-base"]},"editor-base":{requires:["base","frame","node","exec-command","editor-selection"]},"editor-bidi":{requires:["editor-base"]},"editor-br":{requires:["editor-base"]},"editor-inline":{requires:["editor-base","content-editable"]},"editor-lists":{requires:["editor-base"]},"editor-para":{requires:["editor-para-base"]},"editor-para-base":{requires:["editor-base"]},"editor-para-ie":{condition:{name:"editor-para-ie",trigger:"editor-para",ua:"ie",when:"instead"},requires:["editor-para-base"]},"editor-selection":{requires:["node"]},"editor-tab":{requires:["editor-base"]},escape:{requires:["yui-base"]},event:{after:["node-base"],use:["event-base","event-delegate","event-synthetic","event-mousewheel","event-mouseenter","event-key","event-focus","event-resize","event-hover","event-outside","event-touch","event-move","event-flick","event-valuechange","event-tap"]},"event-base":{after:["node-base"],requires:["event-custom-base"]},"event-base-ie":{after:["event-base"],condition:{name:"event-base-ie",test:function(e){var t=e.config.doc&&e.config.doc.implementation;return t&&!t.hasFeature("Events","2.0")},trigger:"node-base"},requires:["node-base"]},"event-contextmenu":{requires:["event-synthetic","dom-screen"]},"event-custom":{use:["event-custom-base","event-custom-complex"]},"event-custom-base":{requires:["oop"]},"event-custom-complex":{requires:["event-custom-base"]},"event-delegate":{requires:["node-base"]},"event-flick":{requires:["node-base","event-touch","event-synthetic"]},"event-focus":{requires:["event-synthetic"]},"event-gestures":{use:["event-flick","event-move"]},"event-hover":{requires:["event-mouseenter"]},"event-key":{requires:["event-synthetic"]},"event-mouseenter":{requires:["event-synthetic"]},"event-mousewheel":{requires:["node-base"]},"event-move":{requires:["node-base","event-touch","event-synthetic"]},"event-outside":{requires:["event-synthetic"]},"event-resize":{requires:["node-base","event-synthetic"]},"event-simulate":{requires:["event-base"]},"event-synthetic":{requires:["node-base","event-custom-complex"]},"event-tap":{requires:["node-base","event-base","event-touch","event-synthetic"]},"event-touch":{requires:["node-base"]},"event-valuechange":{requires:["event-focus","event-synthetic"]},"exec-command":{requires:["frame"]},features:{requires:["yui-base"]},file:{requires:["file-flash","file-html5"]},"file-flash":{requires:["base"]},"file-html5":{requires:["base"]},frame:{requires:["base","node","plugin","selector-css3","yui-throttle"]},"gesture-simulate":{requires:["async-queue","event-simulate","node-screen"]},get:{requires:["yui-base"]},graphics:{requires:["node","event-custom","pluginhost","matrix","classnamemanager"]},"graphics-canvas":{condition:{name:"graphics-canvas",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"},requires:["graphics","color-base"]},"graphics-canvas-default":{condition:{name:"graphics-canvas-default",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"}},"graphics-group":{requires:["graphics"]},"graphics-svg":{condition:{name:"graphics-svg",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"},requires:["graphics"]},"graphics-svg-default":{condition:{name:"graphics-svg-default",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"}},"graphics-vml":{condition:{name:"graphics-vml",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"},requires:["graphics","color-base"]},"graphics-vml-default":{condition:{name:"graphics-vml-default",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"}},handlebars:{use:["handlebars-compiler"]},"handlebars-base":{requires:[]},"handlebars-compiler":{requires:["handlebars-base"]},highlight:{use:["highlight-base","highlight-accentfold"]},"highlight-accentfold":{requires:["highlight-base","text-accentfold"]},"highlight-base":{requires:["array-extras","classnamemanager","escape","text-wordbreak"]},history:{use:["history-base","history-hash","history-html5"]},"history-base":{requires:["event-custom-complex"]},"history-hash":{after:["history-html5"],requires:["event-synthetic","history-base","yui-later"]},"history-hash-ie":{condition:{name:"history-hash-ie",test:function(e){var t=e.config.doc&&e.config.doc.documentMode;return e.UA.ie&&(!("onhashchange"in e.config.win)||!t||t<8)},trigger:"history-hash"},requires:["history-hash","node-base"]},"history-html5":{optional:["json"],requires:["event-base","history-base","node-base"
+]},imageloader:{requires:["base-base","node-style","node-screen"]},intl:{requires:["intl-base","event-custom"]},"intl-base":{requires:["yui-base"]},io:{use:["io-base","io-xdr","io-form","io-upload-iframe","io-queue"]},"io-base":{requires:["event-custom-base","querystring-stringify-simple"]},"io-form":{requires:["io-base","node-base"]},"io-nodejs":{condition:{name:"io-nodejs",trigger:"io-base",ua:"nodejs"},requires:["io-base"]},"io-queue":{requires:["io-base","queue-promote"]},"io-upload-iframe":{requires:["io-base","node-base"]},"io-xdr":{requires:["io-base","datatype-xml-parse"]},json:{use:["json-parse","json-stringify"]},"json-parse":{requires:["yui-base"]},"json-parse-shim":{condition:{name:"json-parse-shim",test:function(e){function i(e,t){return e==="ok"?!0:t}var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONParse!==!1&&!!n;if(r)try{r=n.parse('{"ok":false}',i).ok}catch(s){r=!1}return!r},trigger:"json-parse"},requires:["json-parse"]},"json-stringify":{requires:["yui-base"]},"json-stringify-shim":{condition:{name:"json-stringify-shim",test:function(e){var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONStringify!==!1&&!!n;if(r)try{r="0"===n.stringify(0)}catch(i){r=!1}return!r},trigger:"json-stringify"},requires:["json-stringify"]},jsonp:{requires:["get","oop"]},"jsonp-url":{requires:["jsonp"]},"lazy-model-list":{requires:["model-list"]},loader:{use:["loader-base","loader-rollup","loader-yui3"]},"loader-base":{requires:["get","features"]},"loader-rollup":{requires:["loader-base"]},"loader-yui3":{requires:["loader-base"]},matrix:{requires:["yui-base"]},model:{requires:["base-build","escape","json-parse"]},"model-list":{requires:["array-extras","array-invoke","arraylist","base-build","escape","json-parse","model"]},"model-sync-local":{requires:["model","json-stringify"]},"model-sync-rest":{requires:["model","io-base","json-stringify"]},node:{use:["node-base","node-event-delegate","node-pluginhost","node-screen","node-style"]},"node-base":{requires:["event-base","node-core","dom-base","dom-style"]},"node-core":{requires:["dom-core","selector"]},"node-event-delegate":{requires:["node-base","event-delegate"]},"node-event-html5":{requires:["node-base"]},"node-event-simulate":{requires:["node-base","event-simulate","gesture-simulate"]},"node-flick":{requires:["classnamemanager","transition","event-flick","plugin"],skinnable:!0},"node-focusmanager":{requires:["attribute","node","plugin","node-event-simulate","event-key","event-focus"]},"node-load":{requires:["node-base","io-base"]},"node-menunav":{requires:["node","classnamemanager","plugin","node-focusmanager"],skinnable:!0},"node-pluginhost":{requires:["node-base","pluginhost"]},"node-screen":{requires:["dom-screen","node-base"]},"node-scroll-info":{requires:["array-extras","base-build","event-resize","node-pluginhost","plugin","selector"]},"node-style":{requires:["dom-style","node-base"]},oop:{requires:["yui-base"]},overlay:{requires:["widget","widget-stdmod","widget-position","widget-position-align","widget-stack","widget-position-constrain"],skinnable:!0},paginator:{requires:["paginator-core"]},"paginator-core":{requires:["base"]},"paginator-url":{requires:["paginator"]},panel:{requires:["widget","widget-autohide","widget-buttons","widget-modality","widget-position","widget-position-align","widget-position-constrain","widget-stack","widget-stdmod"],skinnable:!0},parallel:{requires:["yui-base"]},pjax:{requires:["pjax-base","pjax-content"]},"pjax-base":{requires:["classnamemanager","node-event-delegate","router"]},"pjax-content":{requires:["io-base","node-base","router"]},"pjax-plugin":{requires:["node-pluginhost","pjax","plugin"]},plugin:{requires:["base-base"]},pluginhost:{use:["pluginhost-base","pluginhost-config"]},"pluginhost-base":{requires:["yui-base"]},"pluginhost-config":{requires:["pluginhost-base"]},promise:{requires:["timers"]},querystring:{use:["querystring-parse","querystring-stringify"]},"querystring-parse":{requires:["yui-base","array-extras"]},"querystring-parse-simple":{requires:["yui-base"]},"querystring-stringify":{requires:["yui-base"]},"querystring-stringify-simple":{requires:["yui-base"]},"queue-promote":{requires:["yui-base"]},"range-slider":{requires:["slider-base","slider-value-range","clickable-rail"]},recordset:{use:["recordset-base","recordset-sort","recordset-filter","recordset-indexer"]},"recordset-base":{requires:["base","arraylist"]},"recordset-filter":{requires:["recordset-base","array-extras","plugin"]},"recordset-indexer":{requires:["recordset-base","plugin"]},"recordset-sort":{requires:["arraysort","recordset-base","plugin"]},resize:{use:["resize-base","resize-proxy","resize-constrain"]},"resize-base":{requires:["base","widget","event","oop","dd-drag","dd-delegate","dd-drop"],skinnable:!0},"resize-constrain":{requires:["plugin","resize-base"]},"resize-plugin":{optional:["resize-constrain"],requires:["resize-base","plugin"]},"resize-proxy":{requires:["plugin","resize-base"]},router:{optional:["querystring-parse"],requires:["array-extras","base-build","history"]},scrollview:{requires:["scrollview-base","scrollview-scrollbars"]},"scrollview-base":{requires:["widget","event-gestures","event-mousewheel","transition"],skinnable:!0},"scrollview-base-ie":{condition:{name:"scrollview-base-ie",trigger:"scrollview-base",ua:"ie"},requires:["scrollview-base"]},"scrollview-list":{requires:["plugin","classnamemanager"],skinnable:!0},"scrollview-paginator":{requires:["plugin","classnamemanager"]},"scrollview-scrollbars":{requires:["classnamemanager","transition","plugin"],skinnable:!0},selector:{requires:["selector-native"]},"selector-css2":{condition:{name:"selector-css2",test:function(e){var t=e.config.doc,n=t&&!("querySelectorAll"in t);return n},trigger:"selector"},requires:["selector-native"]},"selector-css3":{requires:["selector-native","selector-css2"]},"selector-native":{requires:["dom-base"]},"series-area"
+:{requires:["series-cartesian","series-fill-util"]},"series-area-stacked":{requires:["series-stacked","series-area"]},"series-areaspline":{requires:["series-area","series-curve-util"]},"series-areaspline-stacked":{requires:["series-stacked","series-areaspline"]},"series-bar":{requires:["series-marker","series-histogram-base"]},"series-bar-stacked":{requires:["series-stacked","series-bar"]},"series-base":{requires:["graphics","axis-base"]},"series-candlestick":{requires:["series-range"]},"series-cartesian":{requires:["series-base"]},"series-column":{requires:["series-marker","series-histogram-base"]},"series-column-stacked":{requires:["series-stacked","series-column"]},"series-combo":{requires:["series-cartesian","series-line-util","series-plot-util","series-fill-util"]},"series-combo-stacked":{requires:["series-stacked","series-combo"]},"series-combospline":{requires:["series-combo","series-curve-util"]},"series-combospline-stacked":{requires:["series-combo-stacked","series-curve-util"]},"series-curve-util":{},"series-fill-util":{},"series-histogram-base":{requires:["series-cartesian","series-plot-util"]},"series-line":{requires:["series-cartesian","series-line-util"]},"series-line-stacked":{requires:["series-stacked","series-line"]},"series-line-util":{},"series-marker":{requires:["series-cartesian","series-plot-util"]},"series-marker-stacked":{requires:["series-stacked","series-marker"]},"series-ohlc":{requires:["series-range"]},"series-pie":{requires:["series-base","series-plot-util"]},"series-plot-util":{},"series-range":{requires:["series-cartesian"]},"series-spline":{requires:["series-line","series-curve-util"]},"series-spline-stacked":{requires:["series-stacked","series-spline"]},"series-stacked":{requires:["axis-stacked"]},"shim-plugin":{requires:["node-style","node-pluginhost"]},slider:{use:["slider-base","slider-value-range","clickable-rail","range-slider"]},"slider-base":{requires:["widget","dd-constrain","event-key"],skinnable:!0},"slider-value-range":{requires:["slider-base"]},sortable:{requires:["dd-delegate","dd-drop-plugin","dd-proxy"]},"sortable-scroll":{requires:["dd-scroll","sortable"]},stylesheet:{requires:["yui-base"]},substitute:{optional:["dump"],requires:["yui-base"]},swf:{requires:["event-custom","node","swfdetect","escape"]},swfdetect:{requires:["yui-base"]},tabview:{requires:["widget","widget-parent","widget-child","tabview-base","node-pluginhost","node-focusmanager"],skinnable:!0},"tabview-base":{requires:["node-event-delegate","classnamemanager"]},"tabview-plugin":{requires:["tabview-base"]},template:{use:["template-base","template-micro"]},"template-base":{requires:["yui-base"]},"template-micro":{requires:["escape"]},test:{requires:["event-simulate","event-custom","json-stringify"]},"test-console":{requires:["console-filters","test","array-extras"],skinnable:!0},text:{use:["text-accentfold","text-wordbreak"]},"text-accentfold":{requires:["array-extras","text-data-accentfold"]},"text-data-accentfold":{requires:["yui-base"]},"text-data-wordbreak":{requires:["yui-base"]},"text-wordbreak":{requires:["array-extras","text-data-wordbreak"]},timers:{requires:["yui-base"]},transition:{requires:["node-style"]},"transition-timer":{condition:{name:"transition-timer",test:function(e){var t=e.config.doc,n=t?t.documentElement:null,r=!0;return n&&n.style&&(r=!("MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style)),r},trigger:"transition"},requires:["transition"]},tree:{requires:["base-build","tree-node"]},"tree-labelable":{requires:["tree"]},"tree-lazy":{requires:["base-pluginhost","plugin","tree"]},"tree-node":{},"tree-openable":{requires:["tree"]},"tree-selectable":{requires:["tree"]},"tree-sortable":{requires:["tree"]},uploader:{requires:["uploader-html5","uploader-flash"]},"uploader-flash":{requires:["swfdetect","escape","widget","base","cssbutton","node","event-custom","uploader-queue"]},"uploader-html5":{requires:["widget","node-event-simulate","file-html5","uploader-queue"]},"uploader-queue":{requires:["base"]},view:{requires:["base-build","node-event-delegate"]},"view-node-map":{requires:["view"]},widget:{use:["widget-base","widget-htmlparser","widget-skin","widget-uievents"]},"widget-anim":{requires:["anim-base","plugin","widget"]},"widget-autohide":{requires:["base-build","event-key","event-outside","widget"]},"widget-base":{requires:["attribute","base-base","base-pluginhost","classnamemanager","event-focus","node-base","node-style"],skinnable:!0},"widget-base-ie":{condition:{name:"widget-base-ie",trigger:"widget-base",ua:"ie"},requires:["widget-base"]},"widget-buttons":{requires:["button-plugin","cssbutton","widget-stdmod"]},"widget-child":{requires:["base-build","widget"]},"widget-htmlparser":{requires:["widget-base"]},"widget-modality":{requires:["base-build","event-outside","widget"],skinnable:!0},"widget-parent":{requires:["arraylist","base-build","widget"]},"widget-position":{requires:["base-build","node-screen","widget"]},"widget-position-align":{requires:["widget-position"]},"widget-position-constrain":{requires:["widget-position"]},"widget-skin":{requires:["widget-base"]},"widget-stack":{requires:["base-build","widget"],skinnable:!0},"widget-stdmod":{requires:["base-build","widget"]},"widget-uievents":{requires:["node-event-delegate","widget-base"]},yql:{requires:["oop"]},"yql-jsonp":{condition:{name:"yql-jsonp",test:function(e){return!e.UA.nodejs&&!e.UA.winjs},trigger:"yql"},requires:["yql","jsonp","jsonp-url"]},"yql-nodejs":{condition:{name:"yql-nodejs",trigger:"yql",ua:"nodejs"},requires:["yql"]},"yql-winjs":{condition:{name:"yql-winjs",trigger:"yql",ua:"winjs"},requires:["yql"]},yui:{},"yui-base":{},"yui-later":{requires:["yui-base"]},"yui-log":{requires:["yui-base"]},"yui-throttle":{requires:["yui-base"]}}),YUI.Env[e.version].md5="45357bb11eddf7fd0a89c0b756599df2"},"3.17.2",{requires:["loader-base"]}),YUI.add("yui",function(e,t){},"3.17.2",{use:["yui-base","get","features","intl-base","yui-log","yui-later","loader-base","loader-rollup"
+,"loader-yui3"]});
diff --git a/long_list.cgi b/long_list.cgi
new file mode 100755
index 000000000..58bd255a3
--- /dev/null
+++ b/long_list.cgi
@@ -0,0 +1,36 @@
+#!/usr/bin/perl -wT
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Terry Weissman <terry@mozilla.org>
+# Gervase Markham <gerv@gerv.net>
+
+use strict;
+use lib qw(. lib);
+use Bugzilla;
+
+my $cgi = Bugzilla->cgi;
+
+# Convert comma/space separated elements into separate params
+my $buglist = $cgi->param('buglist') || $cgi->param('bug_id') || $cgi->param('id') || '';
+my @ids = split (/[\s,]+/, $buglist);
+
+my $ids = join('', map { $_ = "&id=" . $_ } @ids);
+
+print $cgi->redirect("show_bug.cgi?format=multiple$ids");
diff --git a/metrics.pl b/metrics.pl
new file mode 100755
index 000000000..f10499057
--- /dev/null
+++ b/metrics.pl
@@ -0,0 +1,46 @@
+#!/usr/bin/perl -w
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+
+BEGIN {
+ delete $ENV{SERVER_SOFTWARE};
+}
+
+use FindBin qw($Bin);
+use lib $Bin;
+use lib "$Bin/lib";
+
+use Bugzilla;
+use Bugzilla::Constants;
+use POSIX qw(setsid nice);
+
+Bugzilla->metrics_enabled(0);
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+nice(19);
+
+# grab reporter class and filename
+exit(1) unless my $reporter_class = shift;
+exit(1) unless my $filename = shift;
+
+# create reporter object and report
+eval "use $reporter_class";
+
+# detach
+if ($reporter_class->DETACH) {
+ open(STDIN, '</dev/null');
+ open(STDOUT, '>/dev/null');
+ open(STDERR, '>/dev/null');
+ setsid();
+}
+
+# report
+exit(1) unless my $reporter = $reporter_class->new({ json_filename => $filename });
+$reporter->report();
diff --git a/mod_perl.pl b/mod_perl.pl
index f3dae34c1..5840351f4 100644
--- a/mod_perl.pl
+++ b/mod_perl.pl
@@ -58,10 +58,14 @@ BEGIN { *CORE::GLOBAL::warn = \&Apache2::ServerRec::warn; }
Bugzilla::CGI->compile(qw(:cgi :push));
use Apache2::SizeLimit;
-# This means that every httpd child will die after processing
-# a CGI if it is taking up more than 45MB of RAM all by itself,
-# not counting RAM it is sharing with the other httpd processes.
-Apache2::SizeLimit->set_max_unshared_size(45_000);
+# This means that every httpd child will die after processing a request if it
+# is taking up more than 700MB of RAM all by itself, not counting RAM it is
+# sharing with the other httpd processes.
+if (Bugzilla->params->{'urlbase'} eq 'https://bugzilla.mozilla.org/') {
+ Apache2::SizeLimit->set_max_unshared_size(700_000);
+} else {
+ Apache2::SizeLimit->set_max_unshared_size(250_000);
+}
my $cgi_path = Bugzilla::Constants::bz_locations()->{'cgi_path'};
@@ -80,7 +84,7 @@ PerlChildInitHandler "sub { Bugzilla::RNG::srand(); srand(); }"
PerlResponseHandler Bugzilla::ModPerl::ResponseHandler
PerlCleanupHandler Apache2::SizeLimit Bugzilla::ModPerl::CleanupHandler
PerlOptions +ParseHeaders
- Options +ExecCGI
+ Options +ExecCGI +FollowSymLinks
AllowOverride Limit FileInfo Indexes
DirectoryIndex index.cgi index.html
</Directory>
@@ -118,6 +122,7 @@ package Bugzilla::ModPerl::ResponseHandler;
use strict;
use base qw(ModPerl::Registry);
use Bugzilla;
+use Bugzilla::Constants qw(USAGE_MODE_REST);
sub handler : method {
my $class = shift;
@@ -135,7 +140,14 @@ sub handler : method {
use warnings;
Bugzilla::init_page();
- return $class->SUPER::handler(@_);
+ my $result = $class->SUPER::handler(@_);
+
+ # When returning data from the REST api we must only return 200 or 304,
+ # which tells Apache not to append its error html documents to the
+ # response.
+ return Bugzilla->usage_mode == USAGE_MODE_REST && $result != 304
+ ? Apache2::Const::OK
+ : $result;
}
diff --git a/page.cgi b/page.cgi
index a6a198d8b..f4e5a9f6b 100755
--- a/page.cgi
+++ b/page.cgi
@@ -78,6 +78,11 @@ if ($id) {
ThrowCodeError("bad_page_cgi_id", { "page_id" => $id });
}
+ # BMO - append template filename to metrics data
+ if (Bugzilla->metrics_enabled) {
+ Bugzilla->metrics->name("page.cgi $id");
+ }
+
my %vars = (
quicksearch_field_names => \&quicksearch_field_names,
);
diff --git a/post_bug.cgi b/post_bug.cgi
index 6e1ecec81..006fd40ee 100755
--- a/post_bug.cgi
+++ b/post_bug.cgi
@@ -43,6 +43,8 @@ use Bugzilla::Keyword;
use Bugzilla::Token;
use Bugzilla::Flag;
+use List::MoreUtils qw(uniq);
+
my $user = Bugzilla->login(LOGIN_REQUIRED);
my $cgi = Bugzilla->cgi;
@@ -60,16 +62,25 @@ unless ($cgi->param()) {
exit;
}
+# BMO: Don't allow updating of bugs if disabled
+if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.');
+}
+
# Detect if the user already used the same form to submit a bug
my $token = trim($cgi->param('token'));
check_token_data($token, 'create_bug', 'index.cgi');
# do a match on the fields if applicable
-Bugzilla::User::match_field ({
+# BMO: allow extensions to define custom user fields
+my $user_match_fields = {
'cc' => { 'type' => 'multi' },
'assigned_to' => { 'type' => 'single' },
'qa_contact' => { 'type' => 'single' },
-});
+};
+Bugzilla::Hook::process('bug_user_match_fields', { fields => $user_match_fields });
+Bugzilla::User::match_field($user_match_fields);
if (defined $cgi->param('maketemplate')) {
$vars->{'url'} = $cgi->canonicalise_query('token');
@@ -122,7 +133,7 @@ push(@bug_fields, qw(
version
target_milestone
status_whiteboard
-
+ see_also
estimated_time
deadline
));
@@ -144,6 +155,14 @@ foreach my $field (@multi_selects) {
$bug_params{$field->name} = [$cgi->param($field->name)];
}
+# BMO - add user_match_fields. it's important to source from input_params
+# instead of $cgi->param to ensure we get the correct value.
+foreach my $field (keys %$user_match_fields) {
+ next if exists $bug_params{$field};
+ next unless $cgi->should_set($field);
+ $bug_params{$field} = Bugzilla->input_params->{$field} // [];
+}
+
my $bug = Bugzilla::Bug->create(\%bug_params);
# Get the bug ID back and delete the token used to create this bug.
@@ -198,6 +217,7 @@ if ($data_fh || $attach_text) {
if ($attachment) {
# Set attachment flags.
+ Bugzilla::Hook::process('post_bug_attachment_flags', { bug => $bug });
my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi(
$bug, $attachment, $vars, SKIP_REQUESTEE_ON_ERROR);
$attachment->set_flags($flags, $new_flags);
@@ -212,6 +232,11 @@ if ($data_fh || $attach_text) {
}
}
+# Set bug_ignored from the hidden field
+if (scalar $cgi->param('bug_ignored')) {
+ $bug->set_bug_ignored(1);
+}
+
# Set bug flags.
my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi($bug, undef, $vars,
SKIP_REQUESTEE_ON_ERROR);
@@ -230,12 +255,21 @@ my $bug_sent = Bugzilla::BugMail::Send($id, $recipients);
$bug_sent->{type} = 'created';
$bug_sent->{id} = $id;
my @all_mail_results = ($bug_sent);
+
foreach my $dep (@{$bug->dependson || []}, @{$bug->blocked || []}) {
my $dep_sent = Bugzilla::BugMail::Send($dep, $recipients);
$dep_sent->{type} = 'dep';
$dep_sent->{id} = $dep;
push(@all_mail_results, $dep_sent);
}
+
+# Sending emails for any referenced bugs.
+foreach my $ref_bug_id (uniq @{ $bug->{see_also_changes} || [] }) {
+ my $ref_sent = Bugzilla::BugMail::Send($ref_bug_id, $recipients);
+ $ref_sent->{id} = $ref_bug_id;
+ push(@all_mail_results, $ref_sent);
+}
+
$vars->{sentmail} = \@all_mail_results;
$format = $template->get_format("bug/create/created",
diff --git a/process_bug.cgi b/process_bug.cgi
index 9272dec60..75c59b084 100755
--- a/process_bug.cgi
+++ b/process_bug.cgi
@@ -58,6 +58,7 @@ use Bugzilla::Keyword;
use Bugzilla::Flag;
use Bugzilla::Status;
use Bugzilla::Token;
+use Bugzilla::Hook;
use List::MoreUtils qw(firstidx);
use Storable qw(dclone);
@@ -93,6 +94,12 @@ sub should_set {
# Begin Data/Security Validation
######################################################################
+# BMO: Don't allow updating of bugs if disabled
+if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.');
+}
+
# Create a list of objects for all bugs being modified in this request.
my @bug_objects;
if (defined $cgi->param('id')) {
@@ -127,43 +134,67 @@ if (defined $cgi->param('dontchange')) {
}
# do a match on the fields if applicable
-Bugzilla::User::match_field({
- 'qa_contact' => { 'type' => 'single' },
- 'newcc' => { 'type' => 'multi' },
- 'masscc' => { 'type' => 'multi' },
- 'assigned_to' => { 'type' => 'single' },
-});
+# BMO: allow extensions to define custom user fields
+my $user_match_fields = {
+ 'qa_contact' => { 'type' => 'single' },
+ 'newcc' => { 'type' => 'multi' },
+ 'masscc' => { 'type' => 'multi' },
+ 'assigned_to' => { 'type' => 'single' },
+};
+Bugzilla::Hook::process('bug_user_match_fields', { fields => $user_match_fields });
+Bugzilla::User::match_field($user_match_fields);
print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_EMAIL;
# Check for a mid-air collision. Currently this only works when updating
# an individual bug.
-if (defined $cgi->param('delta_ts'))
-{
- my $delta_ts_z = datetime_from($cgi->param('delta_ts'));
+my $delta_ts = $cgi->param('delta_ts') || '';
+
+if ($delta_ts) {
+ my $delta_ts_z = datetime_from($delta_ts)
+ or ThrowCodeError('invalid_timestamp', { timestamp => $delta_ts });
+
my $first_delta_tz_z = datetime_from($first_bug->delta_ts);
+
if ($first_delta_tz_z ne $delta_ts_z) {
- ($vars->{'operations'}) =
- Bugzilla::Bug::GetBugActivity($first_bug->id, undef,
- scalar $cgi->param('delta_ts'));
+ ($vars->{'operations'}) = Bugzilla::Bug::GetBugActivity($first_bug->id, undef, $delta_ts);
- $vars->{'title_tag'} = "mid_air";
-
- ThrowCodeError('undefined_field', { field => 'longdesclength' })
- if !defined $cgi->param('longdesclength');
+ my $start_at = $cgi->param('longdesclength')
+ or ThrowCodeError('undefined_field', { field => 'longdesclength' });
- $vars->{'start_at'} = $cgi->param('longdesclength');
# Always sort midair collision comments oldest to newest,
# regardless of the user's personal preference.
- $vars->{'comments'} = $first_bug->comments({ order => "oldest_to_newest" });
- $vars->{'bug'} = $first_bug;
+ my $comments = $first_bug->comments({ order => "oldest_to_newest" });
+
+ # Show midair if previous changes made other than CC
+ # and/or one or more comments were made
+ my $do_midair = scalar @$comments > $start_at ? 1 : 0;
+
+ if (!$do_midair) {
+ foreach my $operation (@{ $vars->{'operations'} }) {
+ foreach my $change (@{ $operation->{'changes'} }) {
+ if ($change->{'fieldname'} ne 'cc') {
+ $do_midair = 1;
+ last;
+ }
+ }
+ last if $do_midair;
+ }
+ }
- # The token contains the old delta_ts. We need a new one.
- $cgi->param('token', issue_hash_token([$first_bug->id, $first_bug->delta_ts]));
- # Warn the user about the mid-air collision and ask them what to do.
- $template->process("bug/process/midair.html.tmpl", $vars)
- || ThrowTemplateError($template->error());
- exit;
+ if ($do_midair) {
+ $vars->{'title_tag'} = "mid_air";
+ $vars->{'start_at'} = $start_at;
+ $vars->{'comments'} = $comments;
+ $vars->{'bug'} = $first_bug;
+ # The token contains the old delta_ts. We need a new one.
+ $cgi->param('token', issue_hash_token([$first_bug->id, $first_bug->delta_ts]));
+
+ # Warn the user about the mid-air collision and ask them what to do.
+ $template->process("bug/process/midair.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+ exit;
+ }
}
}
@@ -173,7 +204,7 @@ if (defined $cgi->param('delta_ts'))
my $token = $cgi->param('token');
if ($cgi->param('id')) {
- check_hash_token($token, [$first_bug->id, $first_bug->delta_ts]);
+ check_hash_token($token, [$first_bug->id, $delta_ts || $first_bug->delta_ts]);
}
else {
check_token_data($token, 'buglist_mass_change', 'query.cgi');
@@ -206,6 +237,7 @@ if (defined $cgi->param('id')) {
# Include both action = 'same_bug' and 'nothing'.
else {
$vars->{'bug'} = $first_bug;
+ $vars->{'bugids'} = $first_bug->id;
}
}
else {
@@ -230,9 +262,9 @@ my @set_fields = qw(op_sys rep_platform priority bug_severity
bug_file_loc status_whiteboard short_desc
deadline remaining_time estimated_time
work_time set_default_assignee set_default_qa_contact
- cclist_accessible reporter_accessible
+ cclist_accessible reporter_accessible
product confirm_product_change
- bug_status resolution dup_id);
+ bug_status resolution dup_id bug_ignored);
push(@set_fields, 'assigned_to') if !$cgi->param('set_default_assignee');
push(@set_fields, 'qa_contact') if !$cgi->param('set_default_qa_contact');
my %field_translation = (
@@ -343,6 +375,14 @@ foreach my $field (@custom_fields) {
}
}
+# BMO - add user_match_fields. it's important to source from input_params
+# instead of $cgi->param to ensure we get the correct value.
+foreach my $field (keys %$user_match_fields) {
+ next if exists $set_all_fields{$field};
+ next unless should_set($field, 1);
+ $set_all_fields{$field} = Bugzilla->input_params->{$field} // [];
+}
+
# We are going to alter the list of removed groups, so we keep a copy here.
my @unchecked_groups = @$removed_groups;
foreach my $b (@bug_objects) {
@@ -382,7 +422,7 @@ foreach my $bug (@bug_objects) {
}
}
- $bug->send_changes($changes, $vars);
+ my $recipient_count = $bug->send_changes($changes, $vars);
}
# Delete the session token used for the mass-change.
diff --git a/query.cgi b/query.cgi
index 26c26049d..a05d4034b 100755
--- a/query.cgi
+++ b/query.cgi
@@ -32,6 +32,7 @@ use Bugzilla;
use Bugzilla::Bug;
use Bugzilla::Constants;
use Bugzilla::Search;
+use Bugzilla::Search::Saved;
use Bugzilla::User;
use Bugzilla::Util;
use Bugzilla::Error;
@@ -54,9 +55,11 @@ if ($cgi->param('nukedefaultquery')) {
if ($userid) {
my $token = $cgi->param('token');
check_hash_token($token, ['nukedefaultquery']);
- $dbh->do("DELETE FROM namedqueries" .
- " WHERE userid = ? AND name = ?",
- undef, ($userid, DEFAULT_QUERY_NAME));
+ my $named_queries = Bugzilla::Search::Saved->match(
+ { userid => $userid, name => DEFAULT_QUERY_NAME });
+ if (@$named_queries) {
+ $named_queries->[0]->remove_from_db();
+ }
}
$buffer = "";
}
diff --git a/quips.cgi b/quips.cgi
index 74c0047a1..14126e43c 100755
--- a/quips.cgi
+++ b/quips.cgi
@@ -78,6 +78,7 @@ if ($action eq "add") {
check_hash_token($token, ['create-quips']);
# Add the quip
+ # Upstreaming: https://bugzilla.mozilla.org/show_bug.cgi?id=621879
my $approved = (Bugzilla->params->{'quip_list_entry_control'} eq "open")
|| $user->in_group('bz_quip_moderators') || 0;
my $comment = $cgi->param("quip");
diff --git a/relogin.cgi b/relogin.cgi
index dc8f7f422..6eb798205 100755
--- a/relogin.cgi
+++ b/relogin.cgi
@@ -146,11 +146,18 @@ elsif ($action eq 'begin-sudo') {
# For future sessions, store the unique ID of the target user
my $token = Bugzilla::Token::_create_token($user->id, 'sudo', $target_user->id);
+
+ my %args;
+ if (Bugzilla->params->{ssl_redirect}) {
+ $args{'-secure'} = 1;
+ }
+
$cgi->send_cookie('-name' => 'sudo',
'-expires' => $time_string,
- '-value' => $token
- );
-
+ '-value' => $token,
+ '-httponly' => 1,
+ %args);
+
# For the present, change the values of Bugzilla::user & Bugzilla::sudoer
Bugzilla->sudo_request($target_user, $user);
diff --git a/report.cgi b/report.cgi
index c73f52b44..7a6093c8c 100755
--- a/report.cgi
+++ b/report.cgi
@@ -131,13 +131,12 @@ my $search = new Bugzilla::Search(
params => scalar $params->Vars,
allow_unlimited => 1,
);
-my $query = $search->sql;
$::SIG{TERM} = 'DEFAULT';
$::SIG{PIPE} = 'DEFAULT';
-my $dbh = Bugzilla->switch_to_shadow_db();
-my $results = $dbh->selectall_arrayref($query);
+Bugzilla->switch_to_shadow_db();
+my ($results, $extra_data) = $search->data;
# We have a hash of hashes for the data itself, and a hash to hold the
# row/col/table names.
@@ -224,8 +223,7 @@ if ($width && $formatparam eq "bar") {
$vars->{'width'} = $width if $width;
$vars->{'height'} = $height if $height;
-
-$vars->{'query'} = $query;
+$vars->{'queries'} = $extra_data;
if ($cgi->param('debug')
&& Bugzilla->params->{debug_group}
@@ -282,11 +280,8 @@ my $format = $template->get_format("reports/report", $formatparam,
# set debug=1 to always get an HTML content-type, and view the error.
$format->{'ctype'} = "text/html" if $cgi->param('debug');
-my @time = localtime(time());
-my $date = sprintf "%04d-%02d-%02d", 1900+$time[5],$time[4]+1,$time[3];
-my $filename = "report-$date.$format->{extension}";
-print $cgi->header(-type => $format->{'ctype'},
- -content_disposition => "inline; filename=$filename");
+$cgi->set_dated_content_disp("inline", "report", $format->{extension});
+print $cgi->header($format->{'ctype'});
# Problems with this CGI are often due to malformed data. Setting debug=1
# prints out both data structures.
diff --git a/request.cgi b/request.cgi
index 16d7662e8..589d773fb 100755
--- a/request.cgi
+++ b/request.cgi
@@ -46,8 +46,12 @@ my $cgi = Bugzilla->cgi;
Bugzilla->switch_to_shadow_db;
my $template = Bugzilla->template;
my $action = $cgi->param('action') || '';
+my $format = $template->get_format('request/queue',
+ scalar($cgi->param('format')),
+ scalar($cgi->param('ctype')));
-print $cgi->header();
+$cgi->set_dated_content_disp("inline", "requests", $format->{extension});
+print $cgi->header($format->{'ctype'});
################################################################################
# Main Body Execution
@@ -66,7 +70,7 @@ unless (defined $cgi->param('requestee')
Bugzilla::User::match_field($fields);
if ($action eq 'queue') {
- queue();
+ queue($format);
}
else {
my $flagtypes = get_flag_types();
@@ -84,8 +88,8 @@ else {
}
$vars->{'components'} = [ sort { $a cmp $b } keys %components ];
- $template->process('request/queue.html.tmpl', $vars)
- || ThrowTemplateError($template->error());
+ $template->process($format->{'template'}, $vars)
+ || ThrowTemplateError($template->error());
}
exit;
@@ -94,6 +98,7 @@ exit;
################################################################################
sub queue {
+ my $format = shift;
my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh;
my $template = Bugzilla->template;
@@ -114,7 +119,11 @@ sub queue {
flags.attach_id, attachments.description,
requesters.realname, requesters.login_name,
requestees.realname, requestees.login_name, COUNT(privs.group_id),
- " . $dbh->sql_date_format('flags.modification_date', '%Y.%m.%d %H:%i') .
+ " . $dbh->sql_date_format('flags.modification_date', '%Y.%m.%d %H:%i') . ",
+ attachments.ispatch,
+ bugs.bug_status,
+ bugs.priority,
+ bugs.bug_severity " .
# Use the flags and flagtypes tables for information about the flags,
# the bugs and attachments tables for target info, the profiles tables
# for setter and requestee info, the products/components tables
@@ -174,48 +183,57 @@ sub queue {
# need to display a "status" column in the report because the value for that
# column will always be the same.
my @excluded_columns = ();
-
- # Filter requests by status: "pending", "granted", "denied", "all"
- # (which means any), or "fulfilled" (which means "granted" or "denied").
- if ($status) {
- if ($status eq "+-") {
- push(@criteria, "flags.status IN ('+', '-')");
- push(@excluded_columns, 'status') unless $cgi->param('do_union');
- }
- elsif ($status ne "all") {
- push(@criteria, "flags.status = '$status'");
- push(@excluded_columns, 'status') unless $cgi->param('do_union');
- }
- }
-
+ my $do_union = $cgi->param('do_union');
+
# Filter results by exact email address of requester or requestee.
if (defined $cgi->param('requester') && $cgi->param('requester') ne "") {
my $requester = $dbh->quote($cgi->param('requester'));
trick_taint($requester); # Quoted above
push(@criteria, $dbh->sql_istrcmp('requesters.login_name', $requester));
- push(@excluded_columns, 'requester') unless $cgi->param('do_union');
+ push(@excluded_columns, 'requester') unless $do_union;
}
if (defined $cgi->param('requestee') && $cgi->param('requestee') ne "") {
if ($cgi->param('requestee') ne "-") {
my $requestee = $dbh->quote($cgi->param('requestee'));
trick_taint($requestee); # Quoted above
- push(@criteria, $dbh->sql_istrcmp('requestees.login_name',
- $requestee));
+ push(@criteria, $dbh->sql_istrcmp('requestees.login_name', $requestee));
+ }
+ else {
+ push(@criteria, "flags.requestee_id IS NULL");
+ }
+ push(@excluded_columns, 'requestee') unless $do_union;
+ }
+
+ # If the user wants requester = foo OR requestee = bar, we have to join
+ # these criteria separately as all other criteria use AND.
+ if (@criteria == 2 && $do_union) {
+ my $union = join(' OR ', @criteria);
+ @criteria = ("($union)");
+ }
+
+ # Filter requests by status: "pending", "granted", "denied", "all"
+ # (which means any), or "fulfilled" (which means "granted" or "denied").
+ if ($status) {
+ if ($status eq "+-") {
+ push(@criteria, "flags.status IN ('+', '-')");
+ push(@excluded_columns, 'status');
+ }
+ elsif ($status ne "all") {
+ push(@criteria, "flags.status = '$status'");
+ push(@excluded_columns, 'status');
}
- else { push(@criteria, "flags.requestee_id IS NULL") }
- push(@excluded_columns, 'requestee') unless $cgi->param('do_union');
}
-
+
# Filter results by exact product or component.
if (defined $cgi->param('product') && $cgi->param('product') ne "") {
my $product = Bugzilla::Product->check(scalar $cgi->param('product'));
push(@criteria, "bugs.product_id = " . $product->id);
- push(@excluded_columns, 'product') unless $cgi->param('do_union');
+ push(@excluded_columns, 'product');
if (defined $cgi->param('component') && $cgi->param('component') ne "") {
my $component = Bugzilla::Component->check({ product => $product,
name => scalar $cgi->param('component') });
push(@criteria, "bugs.component_id = " . $component->id);
- push(@excluded_columns, 'component') unless $cgi->param('do_union');
+ push(@excluded_columns, 'component');
}
}
@@ -233,15 +251,11 @@ sub queue {
my $quoted_form_type = $dbh->quote($form_type);
trick_taint($quoted_form_type); # Already SQL quoted
push(@criteria, "flagtypes.name = " . $quoted_form_type);
- push(@excluded_columns, 'type') unless $cgi->param('do_union');
+ push(@excluded_columns, 'type');
}
-
- # Add the criteria to the query. We do an intersection by default
- # but do a union if the "do_union" URL parameter (for which there is no UI
- # because it's an advanced feature that people won't usually want) is true.
- my $and_or = $cgi->param('do_union') ? " OR " : " AND ";
- $query .= " AND (" . join($and_or, @criteria) . ") " if scalar(@criteria);
-
+
+ $query .= ' AND ' . join(' AND ', @criteria) if scalar(@criteria);
+
# Group the records by flag ID so we don't get multiple rows of data
# for each flag. This is only necessary because of the code that
# removes flags on bugs the user is unauthorized to access.
@@ -250,9 +264,9 @@ sub queue {
products.name, components.name, flags.attach_id,
attachments.description, requesters.realname,
requesters.login_name, requestees.realname,
- requestees.login_name, flags.modification_date,
+ requestees.login_name, flags.modification_date, attachments.ispatch
cclist_accessible, bugs.reporter, bugs.reporter_accessible,
- bugs.assigned_to');
+ bugs.assigned_to, attachments.ispatch');
# Group the records, in other words order them by the group column
# so the loop in the display template can break them up into separate
@@ -278,7 +292,7 @@ sub queue {
# Pass the query to the template for use when debugging this script.
$vars->{'query'} = $query;
$vars->{'debug'} = $cgi->param('debug') ? 1 : 0;
-
+
my $results = $dbh->selectall_arrayref($query);
my @requests = ();
foreach my $result (@$results) {
@@ -295,7 +309,11 @@ sub queue {
'requester' => ($data[9] ? "$data[9] <$data[10]>" : $data[10]) ,
'requestee' => ($data[11] ? "$data[11] <$data[12]>" : $data[12]) ,
'restricted' => $data[13] ? 1 : 0,
- 'created' => $data[14]
+ 'created' => $data[14],
+ 'ispatch' => $data[15],
+ 'bug_status' => $data[16],
+ 'priority' => $data[17],
+ 'bug_severity' => $data[18],
};
push(@requests, $request);
}
@@ -318,9 +336,11 @@ sub queue {
}
$vars->{'components'} = [ sort { $a cmp $b } keys %components ];
+ $vars->{'urlquerypart'} = $cgi->canonicalise_query('ctype');
+
# Generate and return the UI (HTML page) from the appropriate template.
- $template->process("request/queue.html.tmpl", $vars)
- || ThrowTemplateError($template->error());
+ $template->process($format->{'template'}, $vars)
+ || ThrowTemplateError($template->error());
}
################################################################################
diff --git a/rest.cgi b/rest.cgi
new file mode 100755
index 000000000..928eb9ce4
--- /dev/null
+++ b/rest.cgi
@@ -0,0 +1,29 @@
+#!/usr/bin/perl -wT
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::WebService::Constants;
+BEGIN {
+ if (!Bugzilla->feature('rest')
+ || !Bugzilla->feature('jsonrpc'))
+ {
+ ThrowUserError('feature_disabled', { feature => 'rest' });
+ }
+}
+use Bugzilla::WebService::Server::REST;
+Bugzilla->usage_mode(USAGE_MODE_REST);
+local @INC = (bz_locations()->{extensionsdir}, @INC);
+my $server = new Bugzilla::WebService::Server::REST;
+$server->version('1.1');
+$server->handle();
diff --git a/robots.txt b/robots.txt
index 0f823cb24..9a64b62d8 100644
--- a/robots.txt
+++ b/robots.txt
@@ -1,3 +1,19 @@
User-agent: *
-Allow: /index.cgi
Disallow: /
+
+Allow: /$
+Allow: /index.cgi
+
+Allow: /page.cgi
+Disallow: /page.cgi*id=voting*
+Disallow: /page.cgi*id=productdashboard*
+
+Allow: /show_bug.cgi
+Disallow: /show_bug.cgi*ctype=*
+Disallow: /show_bug.cgi*format=multiple*
+
+Allow: /describecomponents.cgi
+Allow: /describekeywords.cgi
+
+Allow: /data/SiteMapIndex/sitemap*.xml.gz
+Sitemap: http://bugzilla.mozilla.org/page.cgi?id=sitemap/sitemap.xml
diff --git a/sanitycheck.cgi b/sanitycheck.cgi
index 7d530ea4b..f718312f4 100755
--- a/sanitycheck.cgi
+++ b/sanitycheck.cgi
@@ -91,6 +91,7 @@ else {
}
}
my $vars = {};
+my $clear_memcached = 0;
print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
@@ -167,6 +168,7 @@ if ($cgi->param('createmissinggroupcontrolmapentries')) {
}
Status('group_control_map_entries_repaired', {counter => $counter});
+ $clear_memcached = 1 if $counter;
}
###########################################################################
@@ -193,6 +195,7 @@ if ($cgi->param('repair_creation_date')) {
$sth_UpdateDate->execute($date, $bugid);
}
Status('bug_creation_date_fixed', {bug_count => scalar(@$bug_ids)});
+ $clear_memcached = 1 if @$bug_ids;
}
###########################################################################
@@ -209,6 +212,7 @@ if ($cgi->param('repair_everconfirmed')) {
$dbh->do("UPDATE bugs SET everconfirmed = 1 WHERE bug_status IN ($confirmed_open_states)");
Status('everconfirmed_end');
+ $clear_memcached = 1;
}
###########################################################################
@@ -224,11 +228,12 @@ if ($cgi->param('repair_bugs_fulltext')) {
ON bugs_fulltext.bug_id = bugs.bug_id
WHERE bugs_fulltext.bug_id IS NULL');
- foreach my $bugid (@$bug_ids) {
- Bugzilla::Bug->new($bugid)->_sync_fulltext('new_bug');
- }
+ foreach my $bugid (@$bug_ids) {
+ Bugzilla::Bug->new($bugid)->_sync_fulltext( new_bug => 1 );
+ }
- Status('bugs_fulltext_fixed', {bug_count => scalar(@$bug_ids)});
+ Status('bugs_fulltext_fixed', {bug_count => scalar(@$bug_ids)});
+ $clear_memcached = 1 if @$bug_ids;
}
###########################################################################
@@ -264,7 +269,10 @@ if ($cgi->param('rescanallBugMail')) {
Bugzilla::BugMail::Send($bugid, $vars);
}
- Status('send_bugmail_end') if scalar(@$list);
+ if (@$list) {
+ Status('send_bugmail_end');
+ Bugzilla->memcached->clear_all();
+ }
unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
$template->process('global/footer.html.tmpl', $vars)
@@ -298,6 +306,7 @@ if ($cgi->param('remove_invalid_bug_references')) {
if (scalar(@$bug_ids)) {
$dbh->do("DELETE FROM $table WHERE $field IN (" . join(',', @$bug_ids) . ")");
+ $clear_memcached = 1;
}
}
@@ -328,6 +337,7 @@ if ($cgi->param('remove_invalid_attach_references')) {
$dbh->bz_commit_transaction();
Status('attachment_reference_deletion_end');
+ $clear_memcached = 1 if @$attach_ids;
}
###########################################################################
@@ -354,12 +364,17 @@ if ($cgi->param('remove_old_whine_targets')) {
$dbh->do("DELETE FROM whine_schedules
WHERE mailto_type = $type AND mailto IN (" .
join(',', @$old_ids) . ")");
+ $clear_memcached = 1;
}
}
$dbh->bz_commit_transaction();
Status('whines_obsolete_target_deletion_end');
}
+# If any repairs were attempted or made, we need to clear memcached to ensure
+# state is consistent.
+Bugzilla->memcached->clear_all() if $clear_memcached;
+
###########################################################################
# Repair hook
###########################################################################
@@ -735,6 +750,7 @@ if (scalar(@invalid_flags)) {
# Silently delete these flags, with no notification to requesters/setters.
$dbh->do('DELETE FROM flags WHERE id IN (' . join(',', @flag_ids) .')');
Status('flag_deletion_end');
+ Bugzilla->memcached->clear_all();
}
else {
foreach my $flag (@$invalid_flags) {
diff --git a/search_plugin.cgi b/search_plugin.cgi
index 4dfe8fa9f..b50467911 100755
--- a/search_plugin.cgi
+++ b/search_plugin.cgi
@@ -31,13 +31,5 @@ my $vars = {};
# Return the appropriate HTTP response headers.
print $cgi->header('application/xml');
-# Get the contents of favicon.ico
-my $filename = bz_locations()->{'libpath'} . "/images/favicon.ico";
-if (open(IN, $filename)) {
- local $/;
- binmode IN;
- $vars->{'favicon'} = <IN>;
- close IN;
-}
$template->process("search/search-plugin.xml.tmpl", $vars)
|| ThrowTemplateError($template->error());
diff --git a/sentry.pl b/sentry.pl
new file mode 100755
index 000000000..5c93e6ee3
--- /dev/null
+++ b/sentry.pl
@@ -0,0 +1,93 @@
+#!/usr/bin/perl -w
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+#
+# report errors to sentry
+# expects a filename with a Data::Dumper serialised parameters
+# called by Bugzilla::Sentry
+#
+
+use strict;
+use warnings;
+
+BEGIN {
+ delete $ENV{SERVER_SOFTWARE};
+}
+
+use FindBin qw($Bin);
+use lib $Bin;
+use lib "$Bin/lib";
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::RNG qw(irand);
+use Fcntl qw(:flock);
+use File::Slurp;
+use HTTP::Request::Common;
+use JSON ();
+use LWP::UserAgent;
+use POSIX qw(setsid nice);
+use Safe;
+use URI;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+nice(19);
+
+# detach
+open(STDIN, '</dev/null');
+open(STDOUT, '>/dev/null');
+open(STDERR, '>/dev/null');
+setsid();
+
+# grab sentry server url
+my $sentry_uri = Bugzilla->params->{sentry_uri} || '';
+exit(1) unless $sentry_uri;
+
+# read data dump
+exit(1) unless my $filename = shift;
+my $dump = read_file($filename);
+unlink($filename);
+
+# deserialise
+my $cpt = new Safe;
+$cpt->reval($dump) || exit(1);
+my $data = ${$cpt->varglob('VAR1')};
+
+# split the sentry uri
+my $uri = URI->new($sentry_uri);
+my ($public_key, $secret_key) = split(/:/, $uri->userinfo);
+$uri->userinfo(undef);
+my $project_id = $uri->path;
+$project_id =~ s/^\///;
+$uri->path("/api/$project_id/store/");
+
+# build the message
+my $message = JSON->new->utf8(1)->pretty(0)->allow_nonref(1)->encode($data);
+my %header = (
+ 'X-Sentry-Auth' => sprintf(
+ "Sentry sentry_version=%s, sentry_timestamp=%s, sentry_key=%s, sentry_client=%s, sentry_secret=%s",
+ '2.0',
+ (time),
+ $public_key,
+ 'bugzilla/4.2',
+ $secret_key,
+ ),
+ 'Content-Type' => 'application/json'
+);
+
+# ensure we send warnings one at a time per webhead
+flock(DATA, LOCK_EX);
+
+# and post to sentry
+my $request = POST $uri->canonical, %header, Content => $message;
+my $response = LWP::UserAgent->new(timeout => 10)->request($request);
+
+__DATA__
+this exists so the flock() code works.
+do not remove this data section.
diff --git a/show_activity.cgi b/show_activity.cgi
index 27096018f..b05c1c95a 100755
--- a/show_activity.cgi
+++ b/show_activity.cgi
@@ -54,8 +54,8 @@ my $bug = Bugzilla::Bug->check($id);
# visible immediately due to replication lag.
Bugzilla->switch_to_shadow_db;
-($vars->{'operations'}, $vars->{'incomplete_data'}) =
- Bugzilla::Bug::GetBugActivity($bug->id);
+($vars->{'operations'}, $vars->{'incomplete_data'}) =
+ Bugzilla::Bug::GetBugActivity($bug->id, undef, undef, 1);
$vars->{'bug'} = $bug;
diff --git a/show_bug.cgi b/show_bug.cgi
index a2bf57ada..332d5fcbd 100755
--- a/show_bug.cgi
+++ b/show_bug.cgi
@@ -61,7 +61,7 @@ Bugzilla->switch_to_shadow_db unless $user->id;
if ($single) {
my $id = $cgi->param('id');
- push @bugs, Bugzilla::Bug->check($id);
+ push @bugs, Bugzilla::Bug->check({ id => $id, cache => 1 });
if (defined $cgi->param('mark')) {
foreach my $range (split ',', $cgi->param('mark')) {
if ($range =~ /^(\d+)-(\d+)$/) {
@@ -77,8 +77,8 @@ if ($single) {
foreach my $id ($cgi->param('id')) {
# Be kind enough and accept URLs of the form: id=1,2,3.
my @ids = split(/,/, $id);
- foreach (@ids) {
- my $bug = new Bugzilla::Bug($_);
+ foreach my $bug_id (@ids) {
+ my $bug = new Bugzilla::Bug({ id => $bug_id, cache => 1 });
# This is basically a backwards-compatibility hack from when
# Bugzilla::Bug->new used to set 'NotPermitted' if you couldn't
# see the bug.
diff --git a/showattachment.cgi b/showattachment.cgi
new file mode 100755
index 000000000..e90a01533
--- /dev/null
+++ b/showattachment.cgi
@@ -0,0 +1,40 @@
+#!/usr/bin/perl -wT
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Terry Weissman <terry@mozilla.org>
+# Jacob Steenhagen <jake@bugzilla.org>
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Util;
+
+my $cgi = Bugzilla->cgi;
+
+my $id = $cgi->param('attach_id');
+detaint_natural($id) if defined $id;
+$id ||= "";
+
+print $cgi->redirect(-location=>"attachment.cgi?id=$id",
+ -status=>'301 Permanent Redirect');
+
+exit;
diff --git a/showdependencygraph.cgi b/showdependencygraph.cgi
index 71eaa9183..28b113460 100755
--- a/showdependencygraph.cgi
+++ b/showdependencygraph.cgi
@@ -44,7 +44,8 @@ my $vars = {};
# performance.
my $dbh = Bugzilla->switch_to_shadow_db();
-local our (%seen, %edgesdone, %bugtitles);
+our (%seen, %edgesdone, %bugtitles);
+our $bug_count = 0;
# CreateImagemap: This sub grabs a local filename as a parameter, reads the
# dot-generated image map datafile residing in that file and turns it into
@@ -91,6 +92,7 @@ sub AddLink {
if (!exists $edgesdone{$key}) {
$edgesdone{$key} = 1;
print $fh "$dependson -> $blocked\n";
+ $bug_count++;
$seen{$blocked} = 1;
$seen{$dependson} = 1;
}
@@ -123,10 +125,10 @@ chmod Bugzilla::Install::Filesystem::CGI_WRITE, $filename
my $urlbase = correct_urlbase();
print $fh "digraph G {";
-print $fh qq{
+print $fh qq(
graph [URL="${urlbase}query.cgi", rankdir=$rankdir]
node [URL="${urlbase}show_bug.cgi?id=\\N", style=filled, color=lightgrey]
-};
+);
my %baselist;
@@ -236,6 +238,11 @@ foreach my $k (keys(%seen)) {
print $fh "}\n";
close $fh;
+if ($bug_count > MAX_WEBDOT_BUGS) {
+ unlink($filename);
+ ThrowUserError("webdot_too_large");
+}
+
my $webdotbase = Bugzilla->params->{'webdotbase'};
if ($webdotbase =~ /^https?:/) {
@@ -311,7 +318,8 @@ foreach my $f (@files)
# symlinks), this can't escape to delete anything it shouldn't
# (unless someone moves the location of $webdotdir, of course)
trick_taint($f);
- if (file_mod_time($f) < $since) {
+ my $mtime = file_mod_time($f);
+ if ($mtime && $mtime < $since) {
unlink $f;
}
}
diff --git a/showdependencytree.cgi b/showdependencytree.cgi
index 8683c190c..b50b38ce2 100755
--- a/showdependencytree.cgi
+++ b/showdependencytree.cgi
@@ -53,9 +53,10 @@ my $bug = Bugzilla::Bug->check(scalar $cgi->param('id'));
my $id = $bug->id;
local our $hide_resolved = $cgi->param('hide_resolved') ? 1 : 0;
-
local our $maxdepth = $cgi->param('maxdepth') || 0;
-if ($maxdepth !~ /^\d+$/) { $maxdepth = 0 };
+if ($maxdepth !~ /^\d+$/) {
+ $maxdepth = 0;
+}
################################################################################
# Main Section #
@@ -70,7 +71,7 @@ my $dependson_tree = { $id => $bug };
my $dependson_ids = {};
GenerateTree($id, "dependson", 1, $dependson_tree, $dependson_ids);
$vars->{'dependson_tree'} = $dependson_tree;
-$vars->{'dependson_ids'} = [keys(%$dependson_ids)];
+$vars->{'dependson_ids'} = [keys(%$dependson_ids)];
# Generate the tree of bugs that this bug blocks and a list of IDs
# appearing in the tree.
@@ -78,64 +79,84 @@ my $blocked_tree = { $id => $bug };
my $blocked_ids = {};
GenerateTree($id, "blocked", 1, $blocked_tree, $blocked_ids);
$vars->{'blocked_tree'} = $blocked_tree;
-$vars->{'blocked_ids'} = [keys(%$blocked_ids)];
-
-$vars->{'realdepth'} = $realdepth;
+$vars->{'blocked_ids'} = [keys(%$blocked_ids)];
-$vars->{'bugid'} = $id;
-$vars->{'maxdepth'} = $maxdepth;
-$vars->{'hide_resolved'} = $hide_resolved;
+$vars->{'bugid'} = $id;
+$vars->{'realdepth'} = $realdepth;
+$vars->{'maxdepth'} = $maxdepth;
+$vars->{'hide_resolved'} = $hide_resolved;
print $cgi->header();
$template->process("bug/dependency-tree.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
-################################################################################
-# Recursive Tree Generation Function #
-################################################################################
+# Tree Generation Functions
sub GenerateTree {
- # Generates a dependency tree for a given bug. Calls itself recursively
- # to generate sub-trees for the bug's dependencies.
my ($bug_id, $relationship, $depth, $bugs, $ids) = @_;
- my @dependencies;
- if ($relationship eq 'dependson') {
- @dependencies = @{$bugs->{$bug_id}->dependson};
- }
- else {
- @dependencies = @{$bugs->{$bug_id}->blocked};
+ # determine just the list of bug ids
+ _generate_bug_ids($bug_id, $relationship, $depth, $ids);
+ my $bug_ids = [ keys %$ids ];
+ return unless @$bug_ids;
+
+ # load all the bugs at once
+ foreach my $bug (@{ Bugzilla::Bug->new_from_list($bug_ids) }) {
+ if (!$bug->{error}) {
+ $bugs->{$bug->id} = $bug;
+ }
}
- # Don't do anything if this bug doesn't have any dependencies.
- return unless scalar(@dependencies);
+ # preload bug visibility
+ Bugzilla->user->visible_bugs($bug_ids);
+
+ # and generate the tree
+ _generate_tree($bug_id, $relationship, $depth, $bugs, $ids);
+}
+
+sub _generate_bug_ids {
+ my ($bug_id, $relationship, $depth, $ids) = @_;
- # Record this depth in the global $realdepth variable if it's farther
+ # Record this depth in the global $realdepth variable if it's farther
# than we've gone before.
$realdepth = max($realdepth, $depth);
- foreach my $dep_id (@dependencies) {
- # Get this dependency's record from the database and generate
- # its sub-tree if we haven't already done so (which happens
- # when bugs appear in dependency trees multiple times).
- if (!$bugs->{$dep_id}) {
- $bugs->{$dep_id} = new Bugzilla::Bug($dep_id);
- GenerateTree($dep_id, $relationship, $depth+1, $bugs, $ids);
+ my $dependencies = _get_dependencies($bug_id, $relationship);
+ foreach my $dep_id (@$dependencies) {
+ if (!$maxdepth || $depth <= $maxdepth) {
+ $ids->{$dep_id} = 1;
+ _generate_bug_ids($dep_id, $relationship, $depth + 1, $ids);
+ }
+ }
+}
+
+sub _generate_tree {
+ my ($bug_id, $relationship, $depth, $bugs, $ids) = @_;
+
+ my $dependencies = _get_dependencies($bug_id, $relationship);
+
+ foreach my $dep_id (@$dependencies) {
+ # recurse
+ if (!$maxdepth || $depth < $maxdepth) {
+ _generate_tree($dep_id, $relationship, $depth + 1, $bugs, $ids);
}
- # Add this dependency to the list of this bug's dependencies
- # if it exists, if we haven't exceeded the maximum depth the user
- # wants the tree to go, and if the dependency isn't resolved
- # (if we're ignoring resolved dependencies).
- if (!$bugs->{$dep_id}->{'error'}
- && Bugzilla->user->can_see_bug($dep_id)
- && (!$maxdepth || $depth <= $maxdepth)
- && ($bugs->{$dep_id}->isopened || !$hide_resolved))
- {
- # Due to AUTOLOAD in Bug.pm, we cannot add 'dependencies'
- # as a bug object attribute from here.
- push(@{$bugs->{'dependencies'}->{$bug_id}}, $dep_id);
- $ids->{$dep_id} = 1;
+ # remove bugs according to visiblity
+ if (!Bugzilla->user->can_see_bug($dep_id)) {
+ delete $ids->{$dep_id};
+ }
+ elsif (!grep { $_ == $dep_id } @{ $bugs->{dependencies}->{$bug_id} }) {
+ push @{ $bugs->{dependencies}->{$bug_id} }, $dep_id;
}
}
}
+
+sub _get_dependencies {
+ my ($bug_id, $relationship) = @_;
+ my $cache = Bugzilla->request_cache->{dependency_cache} ||= {};
+ return $cache->{$bug_id}->{$relationship} ||=
+ $relationship eq 'dependson'
+ ? Bugzilla::Bug::EmitDependList('blocked', 'dependson', $bug_id, $hide_resolved)
+ : Bugzilla::Bug::EmitDependList('dependson', 'blocked', $bug_id, $hide_resolved);
+}
+
diff --git a/skins/README b/skins/README
index 111c00f03..d649350b9 100644
--- a/skins/README
+++ b/skins/README
@@ -1,20 +1,18 @@
There are three directories here, standard/, custom/, and contrib/.
-standard/ holds the standard stylesheets. These are used no matter
-what skin the user selects. If the user selects the "Classic" skin,
-then *only* the standard/ stylesheets are used.
+standard/ holds the standard stylesheets. These are used no matter what skin
+the user selects. If the user selects the "Classic" skin, then *only* the
+standard/ stylesheets are used.
-contrib/ holds "skins" that the user can select in their preferences.
-skins are in directories, and they contain files with the same names
-as the files in skins/standard/. Simply putting a new directory
-into the contrib/ directory adds a new skin as an option in users'
-preferences.
+contrib/ holds "skins" that the user can select in their preferences. skins
+are in directories, and they contain files with the same names as the files in
+skins/standard/. Simply putting a new directory into the contrib/ directory
+adds a new skin as an option in users' preferences.
-custom/ allows you to locally override the standard/ and contrib/ CSS.
-If you put files into the custom/ directory with the same names as the CSS
-files in skins/standard/, you can override the standard/ and contrib/
-CSS. For example, if you want to override some CSS in
-skins/standard/global.css, then you should create a file called "global.css"
-in custom/ and put some CSS in it. The CSS you put into files in custom/ will
-be used *in addition* to the CSS in skins/standard/ or the CSS in
-skins/contrib/. It will apply to every skin.
+custom/ allows you to locally override the standard/ and contrib/ CSS. If you
+put files into the custom/ directory with the same names as the CSS files in
+skins/standard/, you can override the standard/ and contrib/ CSS. For example,
+if you want to override some CSS in skins/standard/global.css, then you should
+create a file called "global.css" in custom/ and put some CSS in it. The CSS
+you put into files in custom/ will be used *in addition* to the CSS in
+skins/standard/ or the CSS in skins/contrib/. It will apply to every skin.
diff --git a/skins/contrib/Dusk-Helvetica/buglist.css b/skins/contrib/Dusk-Helvetica/buglist.css
new file mode 100644
index 000000000..2e14368b1
--- /dev/null
+++ b/skins/contrib/Dusk-Helvetica/buglist.css
@@ -0,0 +1,24 @@
+/* The contents of this file are subject to the Mozilla Public
+ * License Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS
+ * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * rights and limitations under the License.
+ *
+ * The Original Code is the Bugzilla Bug Tracking System.
+ *
+ * The Initial Developer of the Original Code is Mike Schrag.
+ * Portions created by Marc Schumann are Copyright (c) 2007 Mike Schrag.
+ * All rights reserved.
+ *
+ * Contributor(s): Mike Schrag <mschrag@pobox.com>
+ * Byron Jones <bugzilla@glob.com.au>
+ * Marc Schumann <wurblzap@gmail.com>
+ */
+
+tr.bz_bugitem:hover {
+ background-color: #ccccff;
+}
diff --git a/skins/contrib/Dusk-Helvetica/global.css b/skins/contrib/Dusk-Helvetica/global.css
new file mode 100644
index 000000000..566bf3cf7
--- /dev/null
+++ b/skins/contrib/Dusk-Helvetica/global.css
@@ -0,0 +1,267 @@
+/* The contents of this file are subject to the Mozilla Public
+ * License Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS
+ * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * rights and limitations under the License.
+ *
+ * The Original Code is the Bugzilla Bug Tracking System.
+ *
+ * The Initial Developer of the Original Code is Mike Schrag.
+ * Portions created by Marc Schumann are Copyright (c) 2007 Mike Schrag.
+ * All rights reserved.
+ *
+ * Contributor(s): Mike Schrag <mschrag@pobox.com>
+ * Byron Jones <bugzilla@glob.com.au>
+ * Marc Schumann <wurblzap@gmail.com>
+ * Frédéric Buclin <LpSolit@gmail.com>
+ */
+
+body {
+ background: #c8c8c8;
+ font-family: "Helvetica Neue", "Nimbus Sans L", Arial, sans-serif;
+ padding-left: 1em;
+ padding-right: 1em;
+}
+
+body, td, th, input {
+ font-family: "Helvetica Neue", "Nimbus Sans L", Arial, sans-serif;
+}
+
+/* page title */
+
+#titles {
+ -moz-border-radius-topleft: 5px;
+ -moz-border-radius-topright: 5px;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+}
+
+#header .links, #footer {
+ background-color: #929bb1;
+ color: #ddd;
+}
+
+#header {
+ -moz-border-radius-bottomleft: 5px;
+ -moz-border-radius-bottomright: 5px;
+ border-bottom-left-radius: 5px;
+ border-bottom-right-radius: 5px;
+ border: none;
+}
+
+#header a, #footer a {
+ color: white;
+ text-decoration: none;
+}
+#header a:hover, #footer a:hover {
+ text-decoration: underline;
+}
+
+/* body */
+
+#bugzilla-body {
+ background: #f0f0f0;
+ color: black;
+ border: 1px solid #747e93;
+ padding: 10px;
+ font-size: 10pt;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+a {
+ color: #6070cf;
+}
+a:hover {
+ color: #8090ef;
+}
+
+hr {
+ border-color: #969696;
+ border-style: dashed;
+ border-width: 1px;
+ margin-top: 10px;
+}
+
+/* edit */
+
+#bugzilla-body th {
+ font-weight: bold;
+ vertical-align: top;
+ white-space: nowrap;
+}
+
+#bug-form td {
+ padding-top: 2px;
+}
+
+/* attachments */
+
+#attachment-list {
+ border: 2px solid #c8c8ba;
+ font-size: 9pt;
+}
+
+#attachment-list th {
+ background-color: #e6e6d8;
+ border: none;
+ border-bottom: 1px solid #c8c8ba;
+ text-align: left;
+}
+
+#attachment-list th a {
+ color: #646456;
+}
+
+#attachment-list td {
+ border: none;
+}
+
+#attachment-list-actions td {
+ border-top: 1px solid #c8c8ba;
+}
+
+/************/
+/* Comments */
+/************/
+
+#comments th {
+ font-size: 9pt;
+ font-weight: bold;
+ padding-top: 5px;
+ padding-right: 5px;
+ padding-bottom: 10px;
+ text-align: right;
+ vertical-align: top;
+ white-space: nowrap;
+}
+
+#comments td {
+ padding-top: 2px;
+}
+
+.reply-button a {
+ padding-left: 2px;
+ padding-right: 2px;
+}
+
+.bz_comment {
+ background-color: #e8e8e8;
+ margin: 1px 1px 10px 1px;
+ border-width: 1px;
+ border-style: solid;
+ border-color: #c8c8ba;
+ padding: 5px;
+ font-size: 9pt;
+}
+
+.bz_comment_head, .bz_first_comment_head {
+ margin: 0; padding: 0;
+ background-color: transparent;
+ font-weight: bold;
+}
+
+.bz_comment_user {
+ margin-left: 0;
+}
+
+.bz_comment.bz_private {
+ background-color: #f0e8e8;
+ border-color: #f8c8ba;
+}
+
+.comment_rule {
+ display: none;
+}
+
+/* footer */
+
+#footer {
+ border: 1px solid #747e93;
+ width: 100%;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+#footer #links-actions,
+#footer #links-edit,
+#footer #links-saved,
+#footer #links-special {
+ margin-top: 2ex;
+}
+
+#footer .links {
+ border-spacing: 30px;
+ margin-bottom: 2ex;
+}
+
+.separator {
+ color: #cccccc;
+}
+
+/* tabs */
+
+.tabbed .tabbody {
+ background: #f8f8f8;
+ padding: 1em;
+ border-style: solid;
+ border-color: #000000;
+ border-width: 0 3px 3px 1px;
+}
+
+.tabs {
+ margin: 0;
+ padding: 0;
+ border-collapse: collapse;
+}
+
+.tabs td {
+ background: #c8c8c8;
+ border-width: 1px;
+}
+
+.tabs td.selected {
+ background: #f8f8f8;
+ border-width: 1px 3px 0 1px;
+}
+
+.tabs td.spacer {
+ background: transparent;
+ border-top: none;
+ border-left: none;
+ border-right: none;
+}
+
+/* other */
+
+.bz_row_odd {
+ background-color: #f0f0f0;
+}
+
+.arrow_down {
+ border-top-color: #6070cf;
+}
+
+/* Rules specific for printing */
+@media print {
+ #header,
+ #footer,
+ .navigation {
+ display: none;
+ }
+
+ body {
+ background-image: none;
+ background-color: #ffffff;
+ }
+
+ #bugzilla-body {
+ border: none;
+ margin: 0;
+ padding: 0;
+ }
+}
diff --git a/skins/contrib/Dusk-Helvetica/index.css b/skins/contrib/Dusk-Helvetica/index.css
new file mode 100644
index 000000000..c9c2d1705
--- /dev/null
+++ b/skins/contrib/Dusk-Helvetica/index.css
@@ -0,0 +1,9 @@
+/*
+ * Custom rules for index.css.
+ * The rules you put here override rules in that stylesheet.
+ */
+
+ div#page-index .outro
+ {
+ clear:both;
+ }
diff --git a/skins/contrib/Dusk-Segoe/buglist.css b/skins/contrib/Dusk-Segoe/buglist.css
new file mode 100644
index 000000000..2e14368b1
--- /dev/null
+++ b/skins/contrib/Dusk-Segoe/buglist.css
@@ -0,0 +1,24 @@
+/* The contents of this file are subject to the Mozilla Public
+ * License Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS
+ * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * rights and limitations under the License.
+ *
+ * The Original Code is the Bugzilla Bug Tracking System.
+ *
+ * The Initial Developer of the Original Code is Mike Schrag.
+ * Portions created by Marc Schumann are Copyright (c) 2007 Mike Schrag.
+ * All rights reserved.
+ *
+ * Contributor(s): Mike Schrag <mschrag@pobox.com>
+ * Byron Jones <bugzilla@glob.com.au>
+ * Marc Schumann <wurblzap@gmail.com>
+ */
+
+tr.bz_bugitem:hover {
+ background-color: #ccccff;
+}
diff --git a/skins/contrib/Dusk-Segoe/global.css b/skins/contrib/Dusk-Segoe/global.css
new file mode 100644
index 000000000..e9fb55e08
--- /dev/null
+++ b/skins/contrib/Dusk-Segoe/global.css
@@ -0,0 +1,267 @@
+/* The contents of this file are subject to the Mozilla Public
+ * License Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS
+ * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * rights and limitations under the License.
+ *
+ * The Original Code is the Bugzilla Bug Tracking System.
+ *
+ * The Initial Developer of the Original Code is Mike Schrag.
+ * Portions created by Marc Schumann are Copyright (c) 2007 Mike Schrag.
+ * All rights reserved.
+ *
+ * Contributor(s): Mike Schrag <mschrag@pobox.com>
+ * Byron Jones <bugzilla@glob.com.au>
+ * Marc Schumann <wurblzap@gmail.com>
+ * Frédéric Buclin <LpSolit@gmail.com>
+ */
+
+body {
+ background: #c8c8c8;
+ font-family: Segoe, "Segoe UI", "Helvetica Neue", Verdana, sans-serif;
+ padding-left: 1em;
+ padding-right: 1em;
+}
+
+body, td, th, input {
+ font-family: Segoe, "Segoe UI", "Helvetica Neue", Verdana, sans-serif;
+}
+
+/* page title */
+
+#titles {
+ -moz-border-radius-topleft: 5px;
+ -moz-border-radius-topright: 5px;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+}
+
+#header .links, #footer {
+ background-color: #929bb1;
+ color: #ddd;
+}
+
+#header {
+ -moz-border-radius-bottomleft: 5px;
+ -moz-border-radius-bottomright: 5px;
+ border-bottom-left-radius: 5px;
+ border-bottom-right-radius: 5px;
+ border: none;
+}
+
+#header a, #footer a {
+ color: white;
+ text-decoration: none;
+}
+#header a:hover, #footer a:hover {
+ text-decoration: underline;
+}
+
+/* body */
+
+#bugzilla-body {
+ background: #f0f0f0;
+ color: black;
+ border: 1px solid #747e93;
+ padding: 10px;
+ font-size: 10pt;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+a {
+ color: #6070cf;
+}
+a:hover {
+ color: #8090ef;
+}
+
+hr {
+ border-color: #969696;
+ border-style: dashed;
+ border-width: 1px;
+ margin-top: 10px;
+}
+
+/* edit */
+
+#bugzilla-body th {
+ font-weight: bold;
+ vertical-align: top;
+ white-space: nowrap;
+}
+
+#bug-form td {
+ padding-top: 2px;
+}
+
+/* attachments */
+
+#attachment-list {
+ border: 2px solid #c8c8ba;
+ font-size: 9pt;
+}
+
+#attachment-list th {
+ background-color: #e6e6d8;
+ border: none;
+ border-bottom: 1px solid #c8c8ba;
+ text-align: left;
+}
+
+#attachment-list th a {
+ color: #646456;
+}
+
+#attachment-list td {
+ border: none;
+}
+
+#attachment-list-actions td {
+ border-top: 1px solid #c8c8ba;
+}
+
+/************/
+/* Comments */
+/************/
+
+#comments th {
+ font-size: 9pt;
+ font-weight: bold;
+ padding-top: 5px;
+ padding-right: 5px;
+ padding-bottom: 10px;
+ text-align: right;
+ vertical-align: top;
+ white-space: nowrap;
+}
+
+#comments td {
+ padding-top: 2px;
+}
+
+.reply-button a {
+ padding-left: 2px;
+ padding-right: 2px;
+}
+
+.bz_comment {
+ background-color: #e8e8e8;
+ margin: 1px 1px 10px 1px;
+ border-width: 1px;
+ border-style: solid;
+ border-color: #c8c8ba;
+ padding: 5px;
+ font-size: 9pt;
+}
+
+.bz_comment_head, .bz_first_comment_head {
+ margin: 0; padding: 0;
+ background-color: transparent;
+ font-weight: bold;
+}
+
+.bz_comment_user {
+ margin-left: 0;
+}
+
+.bz_comment.bz_private {
+ background-color: #f0e8e8;
+ border-color: #f8c8ba;
+}
+
+.comment_rule {
+ display: none;
+}
+
+/* footer */
+
+#footer {
+ border: 1px solid #747e93;
+ width: 100%;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+#footer #links-actions,
+#footer #links-edit,
+#footer #links-saved,
+#footer #links-special {
+ margin-top: 2ex;
+}
+
+#footer .links {
+ border-spacing: 30px;
+ margin-bottom: 2ex;
+}
+
+.separator {
+ color: #cccccc;
+}
+
+/* tabs */
+
+.tabbed .tabbody {
+ background: #f8f8f8;
+ padding: 1em;
+ border-style: solid;
+ border-color: #000000;
+ border-width: 0 3px 3px 1px;
+}
+
+.tabs {
+ margin: 0;
+ padding: 0;
+ border-collapse: collapse;
+}
+
+.tabs td {
+ background: #c8c8c8;
+ border-width: 1px;
+}
+
+.tabs td.selected {
+ background: #f8f8f8;
+ border-width: 1px 3px 0 1px;
+}
+
+.tabs td.spacer {
+ background: transparent;
+ border-top: none;
+ border-left: none;
+ border-right: none;
+}
+
+/* other */
+
+.bz_row_odd {
+ background-color: #f0f0f0;
+}
+
+.arrow_down {
+ border-top-color: #6070cf;
+}
+
+/* Rules specific for printing */
+@media print {
+ #header,
+ #footer,
+ .navigation {
+ display: none;
+ }
+
+ body {
+ background-image: none;
+ background-color: #ffffff;
+ }
+
+ #bugzilla-body {
+ border: none;
+ margin: 0;
+ padding: 0;
+ }
+}
diff --git a/skins/contrib/Dusk-Segoe/index.css b/skins/contrib/Dusk-Segoe/index.css
new file mode 100644
index 000000000..c9c2d1705
--- /dev/null
+++ b/skins/contrib/Dusk-Segoe/index.css
@@ -0,0 +1,9 @@
+/*
+ * Custom rules for index.css.
+ * The rules you put here override rules in that stylesheet.
+ */
+
+ div#page-index .outro
+ {
+ clear:both;
+ }
diff --git a/skins/contrib/Dusk-Segoe/show_bug.css b/skins/contrib/Dusk-Segoe/show_bug.css
new file mode 100644
index 000000000..92e52d02e
--- /dev/null
+++ b/skins/contrib/Dusk-Segoe/show_bug.css
@@ -0,0 +1,3 @@
+.bz_comment {
+ font-size: small;
+}
diff --git a/skins/contrib/Dusk/global.css b/skins/contrib/Dusk/global.css
index 63375672b..b3c8aea4d 100644
--- a/skins/contrib/Dusk/global.css
+++ b/skins/contrib/Dusk/global.css
@@ -22,11 +22,15 @@
body {
background: #c8c8c8;
- font-family: Helvetica, Arial, Geneva;
+ font-family: Verdana, sans-serif;
padding-left: 1em;
padding-right: 1em;
}
+body, td, th, input {
+ font-family: Verdana, sans-serif;
+}
+
/* page title */
#titles {
@@ -232,6 +236,10 @@ hr {
background-color: #f0f0f0;
}
+.arrow_down {
+ border-top-color: #6070cf;
+}
+
/* Rules specific for printing */
@media print {
#header,
diff --git a/skins/contrib/Mozilla-OpenSans/bugzilla-magnifier.png b/skins/contrib/Mozilla-OpenSans/bugzilla-magnifier.png
new file mode 100644
index 000000000..b859b1668
--- /dev/null
+++ b/skins/contrib/Mozilla-OpenSans/bugzilla-magnifier.png
Binary files differ
diff --git a/skins/contrib/Mozilla-OpenSans/bugzilla-papericon.png b/skins/contrib/Mozilla-OpenSans/bugzilla-papericon.png
new file mode 100644
index 000000000..677567929
--- /dev/null
+++ b/skins/contrib/Mozilla-OpenSans/bugzilla-papericon.png
Binary files differ
diff --git a/skins/contrib/Mozilla-OpenSans/bugzilla-person-alternate.png b/skins/contrib/Mozilla-OpenSans/bugzilla-person-alternate.png
new file mode 100644
index 000000000..a9e9ff213
--- /dev/null
+++ b/skins/contrib/Mozilla-OpenSans/bugzilla-person-alternate.png
Binary files differ
diff --git a/skins/contrib/Mozilla-OpenSans/bugzilla-person.png b/skins/contrib/Mozilla-OpenSans/bugzilla-person.png
new file mode 100644
index 000000000..62351c265
--- /dev/null
+++ b/skins/contrib/Mozilla-OpenSans/bugzilla-person.png
Binary files differ
diff --git a/skins/contrib/Mozilla-OpenSans/bugzilla-questionmark2.png b/skins/contrib/Mozilla-OpenSans/bugzilla-questionmark2.png
new file mode 100644
index 000000000..441d07f93
--- /dev/null
+++ b/skins/contrib/Mozilla-OpenSans/bugzilla-questionmark2.png
Binary files differ
diff --git a/skins/contrib/Mozilla-OpenSans/dropdown.png b/skins/contrib/Mozilla-OpenSans/dropdown.png
new file mode 100644
index 000000000..e01e5e51d
--- /dev/null
+++ b/skins/contrib/Mozilla-OpenSans/dropdown.png
Binary files differ
diff --git a/skins/contrib/Mozilla-OpenSans/footer-mozilla.png b/skins/contrib/Mozilla-OpenSans/footer-mozilla.png
new file mode 100644
index 000000000..593c10308
--- /dev/null
+++ b/skins/contrib/Mozilla-OpenSans/footer-mozilla.png
Binary files differ
diff --git a/skins/contrib/Mozilla-OpenSans/global.css b/skins/contrib/Mozilla-OpenSans/global.css
new file mode 100644
index 000000000..89d1f81af
--- /dev/null
+++ b/skins/contrib/Mozilla-OpenSans/global.css
@@ -0,0 +1,878 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Open Sans'), local('OpenSans'), url(opensans.woff) format('woff');
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url(opensans-semibold.woff) format('woff');
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 700;
+ src: local('Open Sans Bold'), local('OpenSans-Bold'), url(opensans-bold.woff) format('woff');
+}
+
+body {
+ background: #f6f4ec;
+ background-image: url(noise.png);
+ background-image: url(noise.png), -moz-linear-gradient(#d7d3c8, #f6f4ec 400px);
+ background-image: url(noise.png), -webkit-linear-gradient(#d7d3c8, #f6f4ec 400px);
+ background-image: url(noise.png), linear-gradient(#d7d3c8, #f6f4ec 400px);
+ background-repeat: repeat, repeat-x;
+ color: #404040;
+}
+
+body, td, th, input, select, option, optgroup, .text_input {
+ font-family: "Open Sans", "Helvetica Neue", Arial, Helvetica, sans-serif;
+}
+
+/* security group colouring */
+/* these are also defined in skins/custom/bug_groups.css */
+
+body[class*=bz_group_] {
+ background-color: inherit;
+ background-image: url(noise.png), -moz-linear-gradient(#d7d7ff, #f0f0ff 400px);
+ background-image: url(noise.png), -webkit-linear-gradient(#d7d7ff, #f0f0ff 400px);
+ background-image: url(noise.png), linear-gradient(#d7d7ff, #f0f0ff 400px);
+}
+
+body[class*=core-security],
+body.bz_group_infrasec {
+ background-image: url(noise.png), -moz-linear-gradient(#ffe0b0, #fff4e3 400px);
+ background-image: url(noise.png), -webkit-linear-gradient(#ffe0b0, #fff4e3 400px);
+ background-image: url(noise.png), linear-gradient(#ffe0b0, #fff4e3 400px);
+}
+
+body.bz_group_webtools-security,
+body.bz_group_websites-security,
+body.bz_group_bugzilla-security {
+ background-image: url(noise.png), -moz-linear-gradient(#ffcccc, #fff0f0 400px);
+ background-image: url(noise.png), -webkit-linear-gradient(#ffcccc, #fff0f0 400px);
+ background-image: url(noise.png), linear-gradient(#ffcccc, #fff0f0 400px);
+}
+
+body.bz_group_client-services-security,
+body.bz_group_mozilla-services-security {
+ background-image: url(noise.png), -moz-linear-gradient(#ffffa3, #ffffe3 400px);
+ background-image: url(noise.png), -webkit-linear-gradient(#ffffa3, #ffffe3 400px);
+ background-image: url(noise.png), linear-gradient(#ffffa3, #ffffe3 400px);
+}
+
+a, #header a, #header a:visited, #footer a, #footer a:visited {
+ color: #0095dd;
+}
+
+a:hover, #header a:hover, #footer a:hover {
+ color: #00539f;
+}
+
+select[multiple], textarea, input[type=text], input[type=password], input:not([type]), .text_input, .yui-ac-input {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ border: 1px solid #b2b2b2;
+ border-radius: .25em;
+ box-shadow: inset 0 1px rgba(0, 0, 0, 0.1);
+ background: white;
+ padding: 4px 3px 5px;
+ color: #404040;
+ vertical-align: top;
+}
+
+select[multiple], .text_input, .yui-ac-input, input {
+ font-size: 1em;
+}
+
+select[multiple]:focus, textarea:focus, .text-input:focus, -yui-ac-input:focus, input:focus {
+ border-color: #42a4e0;
+ -webkit-box-shadow: 0 0 0 2px rgba(73,173,227,0.4);
+ -moz-box-shadow: 0 0 0 2px rgba(73,173,227,0.4);
+ box-shadow: 0 0 0 2px rgba(73,173,227,0.4);
+}
+
+select, select[multiple] {
+ font-size: 12px;
+}
+
+hr {
+ border: none;
+ height: 1px;
+ color: #ccc;
+ background-color: #ccc;
+ margin: 1em 0;
+}
+
+#changeform hr {
+ display: none;
+}
+
+#header {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ background: #e5e3dc;
+ background: -moz-linear-gradient(#e5e3dc, #ecebe5);
+ background: -webkit-linear-gradient(#e5e3dc, #ecebe5);
+ background: linear-gradient(#e5e3dc, #ecebe5);
+ border-radius: 0;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ border-top: 2px solid rgb(255, 255, 255);
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ margin: -15px -15px 0 -15px;
+ color: transparent;
+}
+
+#header .subheader {
+ text-align: left;
+ padding-left: 10px;
+}
+
+#header .wrapper {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ margin: -1px auto 0px;
+ width: 99%;
+}
+
+#header .wrapper:after {
+ clear: both;
+ content: ".";
+ display: block;
+ height: 0;
+ visibility: hidden;
+}
+
+#bugzilla_version {
+ float: right;
+ color: #888;
+ padding: 5px 20px;
+}
+
+#titles {
+ width: 100%;
+ background-color: transparent;
+ padding: 0 1em 0 1em;
+}
+
+#information {
+ text-align: left;
+ padding-left: 2em;
+}
+
+#title {
+ width: 150px;
+ font-size: 120%;
+}
+
+#moz_tab {
+ width: 100px;
+ vertical-align: top;
+}
+
+#moz_login {
+ text-align: right;
+ padding-right: 2em;
+ color: #404040;
+}
+
+#header .links {
+ background: transparent;
+ border: none;
+ border-radius: 0;
+ color: #404040;
+ position: relative;
+ width: 50%;
+}
+
+#header .links {
+ width: auto;
+}
+
+.login-links ul {
+}
+
+.login-links li {
+ display: inline;
+}
+
+.links a {
+ margin: 0 10px 0 10px;
+}
+
+.links .home {
+ font-weight: bold;
+}
+
+.links .separator {
+ display: none;
+}
+
+#quicksearch_top, #quicksearch_main {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ background: url(search.png) 5px center no-repeat, #fafafa;
+ background: url(search.png) 5px center no-repeat, -moz-linear-gradient(#fafafa, #fff);
+ background: url(search.png) 5px center no-repeat, -webkit-linear-gradient(#fafafa, #fff);
+ background: url(search.png) 5px center no-repeat, linear-gradient(#fafafa, #fff);
+ padding: .4em 1em .45em 26px;
+ width: 200px;
+}
+
+#footer .links .quicksearch_form {
+ display: none;
+}
+
+#header .form a {
+ margin: 0;
+}
+
+.links .dropdown {
+ background: rgba(0, 0, 0, 0.05);
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ border-radius: .25em;
+ display: inline-block;
+ padding: 4px 8px;
+ position: relative;
+ cursor: default;
+}
+
+.links .dropdown .anchor {
+ background-image: url(dropdown.png);
+ background-position: right center;
+ background-repeat: no-repeat;
+ display: inline-block;
+ min-width: 110px;
+ padding-right: 15px;
+}
+
+.links .dropdown ul {
+ background: #fafafa;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 0 0 .25em .25em;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ display: none;
+ padding: 4px;
+ position: absolute;
+ right: -1px;
+ margin-top: 4px;
+ z-index: 2;
+ text-align: left;
+}
+
+.links .dropdown:hover ul {
+ display: block;
+}
+
+.links .dropdown li {
+ display: block;
+}
+
+.links .dropdown:hover {
+ border-bottom-right-radius: 0;
+}
+
+.links .dropdown li {
+ display: block;
+}
+
+#bugzilla-body {
+ background: none;
+ border: none;
+ color: #404040;
+ margin: 10px auto 15px;
+ padding: 0;
+ width: 99%;
+}
+
+/* Home */
+
+#page-index {
+ max-width: none;
+}
+
+#page-index td:first-child {
+ text-align: center;
+}
+
+#quicksearch_links {
+ margin-top: 10px;
+}
+
+/* Bugs */
+
+.navigation {
+ background: rgba(255, 255, 255, 0.3);
+ padding: 5px 10px;
+}
+
+u {
+ border-bottom: 1px solid #aaa;
+ text-decoration: none;
+}
+
+#field_container_see_also br {
+ margin-bottom: 10px;
+}
+
+.bz_alias_short_desc_container {
+ background: none;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 30px;
+ padding: 5px 0;
+ text-shadow: 0 1px rgba(255, 255, 255, 0.2);
+}
+
+.bz_alias_short_desc_container b {
+ font-weight: normal;
+}
+
+.bz_alias_short_desc_container .editme {
+ font-weight: normal;
+}
+
+.last_comment_link {
+ font-size: 18px;
+}
+
+.last_comment_link b {
+ border-bottom: 1px solid #aaa;
+ font-weight: normal;
+}
+
+table.edit_form {
+ background: #fff;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ margin-bottom: 20px;
+ padding: 10px 10px 80px;
+ position: relative;
+}
+
+table.edit_form tbody {
+ width: 100%;
+}
+
+table.edit_form hr {
+ display: none;
+}
+
+.field_label {
+ font-weight: bold !important;
+ padding-right: 10px;
+ vertical-align: baseline;
+ white-space: nowrap;
+}
+
+.field_label a, .field_label b {
+ color: #404040;
+ font-weight: bold;
+}
+
+.field_value .text_input {
+ min-width: 0;
+}
+
+#product, #component {
+ width: 235px;
+}
+
+#bz_show_bug_column_1 tr:last-child span {
+ position: absolute;
+ left: 20px;
+ bottom: 20px;
+}
+
+#commit_top {
+ position: absolute;
+ bottom: 20px;
+ right: 10px;
+}
+
+.cc_list_display {
+ background: #fff;
+ float: none;
+ font-size: 11px;
+ margin-top: 3px;
+ max-width: none;
+ padding: 5px;
+}
+
+#project-flags, #custom-flags {
+ border-collapse: collapse;
+}
+
+#project-flags label, #custom-flags label {
+ margin-right: 10px;
+}
+
+#cf_crash_signature {
+ width: 100%;
+}
+
+#attachment_table {
+ background: #fff;
+ border: none;
+ border-collapse: collapse;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ margin-bottom: 40px;
+}
+
+#attachment_table td {
+ border: none;
+}
+
+#attachment_table th, .bz_attach_footer, .bz_time_tracking_table th {
+ background: #eee;
+ color: #404040;
+}
+
+#attachment_table .bz_attach_actions {
+ white-space: nowrap;
+}
+
+/* background for diff views */
+.file_table, .file-table {
+ background: #ffffff;
+}
+
+.bz_comment {
+ width: 65em !important;
+ margin: 0 0 20px;
+}
+
+.bz_comment pre, #comment {
+ font: 13px/1.2 "Droid Sans Mono", Menlo, Monaco, "Courier New", Courier, monospace;
+}
+
+.bz_first_comment_head, .bz_comment_head {
+ font-weight: normal;
+ line-height: 32px;
+ padding-bottom: 2px;
+ padding-left: 0px;
+ margin-left: -5px;
+ white-space: nowrap;
+ background-color: transparent;
+}
+
+.bz_comment_head img, .bz_first_comment_head img {
+ vertical-align: middle;
+}
+
+.bz_comment_user a {
+ -moz-transition: all 0.25s linear 0s;
+ -webkit-transition: all 0.25s linear 0s;
+ transition: all 0.25s linear 0s;
+ transition: all 0.25s linear 0s;
+ color: #0095dd;
+ padding: 0px;
+ margin: 0px;
+}
+
+.bz_comment_user a:hover {
+ -moz-transition: all 0.25s linear 0s;
+ -webkit-transition: all 0.25s linear 0s;
+ transition: all 0.25s linear 0s;
+ background: #fff;
+ border: none;
+ text-decoration: none;
+}
+
+.bz_comment_user .vcard {
+ font-weight: bold;
+}
+
+.bz_comment_actions {
+ margin: 0px 0px;
+}
+
+.new_user {
+ margin-left: 10px;
+}
+
+.ih_history {
+ padding: 0 !important;
+}
+
+.ih_history .bz_comment_head {
+ padding-bottom: 3px;
+}
+
+.ih_history_item:not(.ih_hidden) ~ .ih_history_item:not(.ih_hidden) {
+ margin-top: 20px;
+}
+
+.ih_history_change {
+ background: #eee;
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ padding: 10px;
+ position: relative;
+}
+
+.bz_comment_text {
+ background: #fff;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ margin: 1px 0 0 0;
+ overflow: auto;
+ padding: 10px;
+ position: relative;
+}
+
+.bz_comment_text:after, .bz_comment_text:before {
+ bottom: 100%;
+ border: solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+}
+
+.bz_comment_text:after {
+ border-bottom-color: #fff;
+ border-width: 8px;
+ left: 16px;
+}
+
+.bz_comment_text span.quote, .bz_comment_text span.quote_wrapped {
+ background: #eee !important;
+ color: #444 !important;
+ display: block !important;
+ margin-top: 5px !important;
+ margin-bottom: -10px !important;
+ overflow: auto;
+ padding: 5px !important;
+}
+
+.bz_comment_tags {
+ background: #eee;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+ padding: 5px;
+}
+
+.bz_comment_tag {
+ background: #fff;
+ color: #444;
+ border: none;
+ padding: 2px 6px;
+}
+
+.bz_comment_tag a {
+ color: #0095DD;
+}
+
+#bz_ctag_error {
+ border: none;
+ background-color: #faa;
+ color: #444;
+ padding: 2px 6px;
+}
+
+#bz_ctag_error a {
+ color: #0095DD;
+}
+
+.ih_inlinehistory {
+ background: #eee;
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ padding: 10px;
+ position: relative;
+ top: -1px;
+}
+
+.bz_collapse_expand_comments li {
+ white-space: nowrap;
+}
+
+#add_comment {
+ border: 1px solid #ccc;
+ border-width: 1px 0;
+ margin-bottom: 20px;
+ padding: 10px 0;
+}
+
+#add_comment > table {
+ border-collapse: collapse;
+ width: 661px;
+}
+
+#comment {
+ -moz-box-sizing: content-box;
+ -webkit-box-sizing: content-box;
+ box-sizing: content-box;
+}
+
+#comment_tabs {
+ margin-top: 2px;
+}
+
+#comment_preview {
+ background: white;
+ display: block;
+ clear: both;
+}
+
+#comment_preview_text {
+ background: transparent;
+ border: none;
+ box-shadow: none;
+ padding: 2px 2px 2px 1px;
+}
+
+#footer {
+ background: #fff;
+ border: none;
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+ border-radius: 0;
+ color: #bbb;
+ width: auto;
+ margin-bottom: 1em;
+}
+
+#privacy-policy {
+ margin-bottom: 1em;
+}
+
+button, input[type=submit], input[type=button], #commit, #commit_top, #header .btn, #header input[type=submit] {
+ background-color: #43a6e2;
+ background-image: -moz-linear-gradient(#43a6e2,#277ac1);
+ background-image: -webkit-linear-gradient(#43a6e2,#277ac1);
+ background-image: linear-gradient(#43a6e2,#277ac1);
+ -moz-transition: all linear 0.25s;
+ -webkit-transition: all linear 0.25s;
+ transition: all linear 0.25s;
+ border-radius: .25em;
+ border: 0px none;
+ box-shadow: 0 1px 0 0 rgba(0,0,0,0.2),inset 0 -1px 0 0 rgba(0,0,0,0.3);
+ color: rgb(255, 255, 255);
+ cursor: pointer;
+ display: inline-block;
+ font-size: 12px;
+ font-weight: 600;
+ text-align: center;
+ text-decoration: none;
+ text-shadow: 0px 1px 0px rgba(0, 0, 0, 0.25);
+ padding: .425em 1em .5em;
+}
+
+button:hover, input[type=submit]:hover, input[type=button]:hover, #commit:hover, #commit_top:hover, #header .btn:hover, #header input[type=submit]:hover {
+ -webkit-box-shadow: 0 1px 0 0 rgba(0,0,0,0.2),inset 0 -1px 0 0 rgba(0,0,0,0.3),inset 0 12px 24px 2px #38a9ed;
+ -moz-box-shadow: 0 1px 0 0 rgba(0,0,0,0.2),inset 0 -1px 0 0 rgba(0,0,0,0.3),inset 0 12px 24px 2px #38a9ed;
+ box-shadow: 0 1px 0 0 rgba(0,0,0,0.2),inset 0 -1px 0 0 rgba(0,0,0,0.3),inset 0 12px 24px 2px #38a9ed;
+ -moz-transition: all linear 0.25s;
+ -webkit-transition: all linear 0.25s;
+ transition: all linear 0.25s;
+}
+
+button:active, input[type=submit]:active, input[type=button]:active, #commit:active, #commit_top:active, #header .btn:active, #header input[type=submit]:active {
+ -webkit-box-shadow: inset 0 2px 0 0 rgba(0,0,0,0.2),inset 0 12px 24px 6px rgba(0,0,0,0.2),inset 0 0 2px 2px rgba(0,0,0,0.2);
+ -moz-box-shadow: inset 0 2px 0 0 rgba(0,0,0,0.2),inset 0 12px 24px 6px rgba(0,0,0,0.2),inset 0 0 2px 2px rgba(0,0,0,0.2);
+ box-shadow: inset 0 2px 0 0 rgba(0,0,0,0.2),inset 0 12px 24px 6px rgba(0,0,0,0.2),inset 0 0 2px 2px rgba(0,0,0,0.2);
+ -moz-transition: all linear 0.25s;
+ -webkit-transition: all linear 0.25s;
+ transition: all linear 0.25s;
+}
+
+button[disabled], input[type=submit][disabled], input[type=button][disabled], button[disabled]:hover, input[type=submit][disabled]:hover, input[type=button][disabled]:hover, button[disabled]:active, input[type=submit][disabled]:active, input[type=button][disabled]:active {
+ background-color: #bfc7cd;
+ background-image: -moz-linear-gradient(#bfc7cd,#9ca3aa);
+ background-image: -webkit-linear-gradient(#bfc7cd,#9ca3aa);
+ background-image: linear-gradient(#bfc7cd,#9ca3aa);
+ box-shadow: 0 1px 0 0 rgba(0,0,0,0.2),inset 0 -1px 0 0 rgba(0,0,0,0.3);
+ cursor: pointer;
+}
+
+.notransition {
+ -webkit-transition: none !important;
+ -moz-transition: none !important;
+ -o-transition: none !important;
+ -ms-transition: none !important;
+ transition: none !important;
+}
+
+.calendar_button, .calendar_button:hover {
+ box-shadow: none;
+ padding: 0;
+}
+
+.related_actions {
+ line-height: 19px;
+ padding: 5px 10px;
+}
+
+.arrow_down {
+ border-top-color: #0095dd;
+}
+
+/* Attachments */
+
+#viewFrame {
+ border: 2px solid #222;
+ margin-bottom: 10px;
+}
+
+#editFrame, #viewDiffFrame, #viewFrame {
+ margin-left: 0;
+}
+
+#flags label {
+ font-weight: normal;
+}
+
+/* tabs */
+
+table.tabs {
+ border-collapse: separate;
+ border-spacing: 1em 0;
+}
+
+.tabs td {
+ background: rgba(255,255,255,0.5);
+ padding: 1em;
+ text-align: center;
+ border-style: none;
+ font-size: 12px;
+ text-transform: uppercase;
+}
+
+.tabs td.selected {
+ background: white;
+ font-weight: 700;
+}
+
+.tabs td.spacer {
+ background: transparent;
+}
+
+.tabs a {
+ color: #333;
+}
+
+.tabbody {
+ background: white;
+ padding: 1em 2em;
+}
+
+/* splinter */
+
+#splinter-files .new-line, #splinter-files .old-line {
+ font-size: 90%;
+}
+
+/* search */
+
+#summary_field.search_field_row input {
+ padding-bottom: 6px;
+}
+
+/* Smaller than standard 990 (devices and browsers) */
+@media only screen and (max-width: 989px) {
+ #header .links {
+ float: none;
+ }
+}
+
+/* Tablet Portrait size to standard 990 */
+@media only screen and (min-width: 768px) and (max-width: 989px) {
+ body {
+ min-width: 768px;
+ }
+
+ #header .wrapper, #bugzilla-body {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ min-width: 768px;
+ }
+
+ #footer > * {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ min-width: 768px;
+ }
+}
+
+/* All Mobile Sizes */
+@media only screen and (max-width: 767px) {
+ table.edit_form, table.edit_form > tbody > tr > td {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ display: block;
+ width: 100% !important;
+ }
+
+ .bz_column_spacer {
+ width: auto;
+ height: 20px;
+ }
+}
+
+
+
+/* Mobile Landscape Size to Tablet Portrait */
+@media only screen and (min-width: 480px) and (max-width: 767px) {
+ body {
+ min-width: 480px;
+ }
+
+ #header .wrapper, #bugzilla-body, #attachment_table,
+ .bz_comment, #add_comment > table, #comment {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 480px !important;
+ }
+
+ #footer > * {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 480px;
+ }
+}
+
+/* Mobile Portrait Size to Mobile Landscape Size */
+@media only screen and (max-width: 479px) {
+ body {
+ min-width: 100%;
+ }
+
+ #header .wrapper, #bugzilla-body, #attachment_table,
+ .bz_comment, #add_comment > table, #comment {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 100% !important;
+ }
+
+ #footer > * {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 100%;
+ }
+}
diff --git a/skins/contrib/Mozilla-OpenSans/grain.png b/skins/contrib/Mozilla-OpenSans/grain.png
new file mode 100644
index 000000000..2980ee90e
--- /dev/null
+++ b/skins/contrib/Mozilla-OpenSans/grain.png
Binary files differ
diff --git a/skins/contrib/Mozilla-OpenSans/index.css b/skins/contrib/Mozilla-OpenSans/index.css
new file mode 100644
index 000000000..3ca1fba51
--- /dev/null
+++ b/skins/contrib/Mozilla-OpenSans/index.css
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#enter_bug {
+ background: url(bugzilla-papericon.png) no-repeat;
+}
+#query {
+ background: url(bugzilla-magnifier.png) no-repeat;
+}
+#account {
+ background: url(bugzilla-person-alternate.png) no-repeat;
+ margin-right: 0;
+}
+#get_help {
+ background: url(bugzilla-questionmark2.png) no-repeat !important;
+}
diff --git a/skins/contrib/Mozilla-OpenSans/noise.png b/skins/contrib/Mozilla-OpenSans/noise.png
new file mode 100644
index 000000000..97407ffd2
--- /dev/null
+++ b/skins/contrib/Mozilla-OpenSans/noise.png
Binary files differ
diff --git a/skins/contrib/Mozilla-OpenSans/opensans-bold.woff b/skins/contrib/Mozilla-OpenSans/opensans-bold.woff
new file mode 100644
index 000000000..27619e7ce
--- /dev/null
+++ b/skins/contrib/Mozilla-OpenSans/opensans-bold.woff
Binary files differ
diff --git a/skins/contrib/Mozilla-OpenSans/opensans-semibold.woff b/skins/contrib/Mozilla-OpenSans/opensans-semibold.woff
new file mode 100644
index 000000000..e83bb333d
--- /dev/null
+++ b/skins/contrib/Mozilla-OpenSans/opensans-semibold.woff
Binary files differ
diff --git a/skins/contrib/Mozilla-OpenSans/opensans.woff b/skins/contrib/Mozilla-OpenSans/opensans.woff
new file mode 100644
index 000000000..55b25f867
--- /dev/null
+++ b/skins/contrib/Mozilla-OpenSans/opensans.woff
Binary files differ
diff --git a/skins/contrib/Mozilla-OpenSans/search.png b/skins/contrib/Mozilla-OpenSans/search.png
new file mode 100644
index 000000000..a56b5e2cd
--- /dev/null
+++ b/skins/contrib/Mozilla-OpenSans/search.png
Binary files differ
diff --git a/skins/contrib/Mozilla-OpenSans/tabzilla.png b/skins/contrib/Mozilla-OpenSans/tabzilla.png
new file mode 100644
index 000000000..7619610c3
--- /dev/null
+++ b/skins/contrib/Mozilla-OpenSans/tabzilla.png
Binary files differ
diff --git a/skins/contrib/Mozilla/bugzilla-magnifier.png b/skins/contrib/Mozilla/bugzilla-magnifier.png
new file mode 100644
index 000000000..b859b1668
--- /dev/null
+++ b/skins/contrib/Mozilla/bugzilla-magnifier.png
Binary files differ
diff --git a/skins/contrib/Mozilla/bugzilla-papericon.png b/skins/contrib/Mozilla/bugzilla-papericon.png
new file mode 100644
index 000000000..677567929
--- /dev/null
+++ b/skins/contrib/Mozilla/bugzilla-papericon.png
Binary files differ
diff --git a/skins/contrib/Mozilla/bugzilla-person-alternate.png b/skins/contrib/Mozilla/bugzilla-person-alternate.png
new file mode 100644
index 000000000..a9e9ff213
--- /dev/null
+++ b/skins/contrib/Mozilla/bugzilla-person-alternate.png
Binary files differ
diff --git a/skins/contrib/Mozilla/bugzilla-person.png b/skins/contrib/Mozilla/bugzilla-person.png
new file mode 100644
index 000000000..62351c265
--- /dev/null
+++ b/skins/contrib/Mozilla/bugzilla-person.png
Binary files differ
diff --git a/skins/contrib/Mozilla/bugzilla-questionmark2.png b/skins/contrib/Mozilla/bugzilla-questionmark2.png
new file mode 100644
index 000000000..441d07f93
--- /dev/null
+++ b/skins/contrib/Mozilla/bugzilla-questionmark2.png
Binary files differ
diff --git a/skins/contrib/Mozilla/dropdown.png b/skins/contrib/Mozilla/dropdown.png
new file mode 100644
index 000000000..e01e5e51d
--- /dev/null
+++ b/skins/contrib/Mozilla/dropdown.png
Binary files differ
diff --git a/skins/contrib/Mozilla/fira/FiraMono-Bold.woff b/skins/contrib/Mozilla/fira/FiraMono-Bold.woff
new file mode 100644
index 000000000..0155c4800
--- /dev/null
+++ b/skins/contrib/Mozilla/fira/FiraMono-Bold.woff
Binary files differ
diff --git a/skins/contrib/Mozilla/fira/FiraMono-Regular.woff b/skins/contrib/Mozilla/fira/FiraMono-Regular.woff
new file mode 100644
index 000000000..29db168f4
--- /dev/null
+++ b/skins/contrib/Mozilla/fira/FiraMono-Regular.woff
Binary files differ
diff --git a/skins/contrib/Mozilla/fira/FiraSans-Bold.woff b/skins/contrib/Mozilla/fira/FiraSans-Bold.woff
new file mode 100644
index 000000000..404f7cfd2
--- /dev/null
+++ b/skins/contrib/Mozilla/fira/FiraSans-Bold.woff
Binary files differ
diff --git a/skins/contrib/Mozilla/fira/FiraSans-BoldItalic.woff b/skins/contrib/Mozilla/fira/FiraSans-BoldItalic.woff
new file mode 100644
index 000000000..d919d014e
--- /dev/null
+++ b/skins/contrib/Mozilla/fira/FiraSans-BoldItalic.woff
Binary files differ
diff --git a/skins/contrib/Mozilla/fira/FiraSans-Italic.woff b/skins/contrib/Mozilla/fira/FiraSans-Italic.woff
new file mode 100644
index 000000000..1ef173c3d
--- /dev/null
+++ b/skins/contrib/Mozilla/fira/FiraSans-Italic.woff
Binary files differ
diff --git a/skins/contrib/Mozilla/fira/FiraSans-Regular.woff b/skins/contrib/Mozilla/fira/FiraSans-Regular.woff
new file mode 100644
index 000000000..7fc569b99
--- /dev/null
+++ b/skins/contrib/Mozilla/fira/FiraSans-Regular.woff
Binary files differ
diff --git a/skins/contrib/Mozilla/fira/FiraSans-SemiBold.woff b/skins/contrib/Mozilla/fira/FiraSans-SemiBold.woff
new file mode 100644
index 000000000..a4253cb11
--- /dev/null
+++ b/skins/contrib/Mozilla/fira/FiraSans-SemiBold.woff
Binary files differ
diff --git a/skins/contrib/Mozilla/fira/FiraSans-SemiBoldItalic.woff b/skins/contrib/Mozilla/fira/FiraSans-SemiBoldItalic.woff
new file mode 100644
index 000000000..3a98281f2
--- /dev/null
+++ b/skins/contrib/Mozilla/fira/FiraSans-SemiBoldItalic.woff
Binary files differ
diff --git a/skins/contrib/Mozilla/footer-mozilla.png b/skins/contrib/Mozilla/footer-mozilla.png
new file mode 100644
index 000000000..593c10308
--- /dev/null
+++ b/skins/contrib/Mozilla/footer-mozilla.png
Binary files differ
diff --git a/skins/contrib/Mozilla/global.css b/skins/contrib/Mozilla/global.css
new file mode 100644
index 000000000..adea5a275
--- /dev/null
+++ b/skins/contrib/Mozilla/global.css
@@ -0,0 +1,921 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+@font-face{
+ font-family: 'Fira Sans';
+ src: local('Fira Sans'), local('FiraSans'),
+ url('fira/FiraSans-Regular.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+}
+
+@font-face{
+ font-family: 'Fira Sans';
+ src: local('Fira Sans Italic'), local('FiraSansItalic'),
+ url('fira/FiraSans-Italic.woff') format('woff');
+ font-weight: 400;
+ font-style: italic;
+}
+
+@font-face{
+ font-family: 'Fira Sans';
+ src: local('Fira Sans SemiBold'), local('FiraSansSemiBold'),
+ url('fira/FiraSans-SemiBold.woff') format('woff');
+ font-weight: 600;
+ font-style: normal;
+}
+
+@font-face{
+ font-family: 'Fira Sans';
+ src: local('Fira Sans SemiBold Italic'), local('FiraSansSemiBoldItalic'),
+ url('fira/FiraSans-SemiBoldItalic.woff') format('woff');
+ font-weight: 600;
+ font-style: italic;
+}
+
+@font-face{
+ font-family: 'Fira Sans';
+ src: local('Fira Sans Bold'), local('FiraSansBold'),
+ url('fira/FiraSans-Bold.woff') format('woff');
+ font-weight: 700;
+ font-style: normal;
+}
+
+@font-face{
+ font-family: 'Fira Sans';
+ src: local('Fira Sans Bold Italic'), local('FiraSansBoldItalic'),
+ url('fira/FiraSans-BoldItalic.woff') format('woff');
+ font-weight: 700;
+ font-style: italic;
+}
+
+@font-face{
+ font-family: 'Fira Mono';
+ src: local('Fira Mono'), local('FiraMono'),
+ url('fira/FiraMono-Regular.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+}
+
+@font-face{
+ font-family: 'Fira Mono';
+ src: local('Fira Mono Bold'), local('FiraMonoBold'),
+ url('fira/FiraMono-Bold.woff') format('woff');
+ font-weight: 600;
+ font-style: normal;
+}
+
+body {
+ background: #f6f4ec;
+ background-image: url(noise.png);
+ background-image: url(noise.png), -moz-linear-gradient(#d7d3c8, #f6f4ec 400px);
+ background-image: url(noise.png), -webkit-linear-gradient(#d7d3c8, #f6f4ec 400px);
+ background-image: url(noise.png), linear-gradient(#d7d3c8, #f6f4ec 400px);
+ background-repeat: repeat, repeat-x;
+ color: #404040;
+}
+
+body, td, th, input, select, option, optgroup, .text_input {
+ font-family: "Fira Sans", "Open Sans", "Helvetica Neue", Arial, Helvetica, sans-serif;
+}
+
+/* security group colouring */
+/* these are also defined in skins/custom/bug_groups.css */
+
+body[class*=bz_group_] {
+ background-color: inherit;
+ background-image: url(noise.png), -moz-linear-gradient(#d7d7ff, #f0f0ff 400px);
+ background-image: url(noise.png), -webkit-linear-gradient(#d7d7ff, #f0f0ff 400px);
+ background-image: url(noise.png), linear-gradient(#d7d7ff, #f0f0ff 400px);
+}
+
+body[class*=core-security],
+body.bz_group_infrasec {
+ background-image: url(noise.png), -moz-linear-gradient(#ffe0b0, #fff4e3 400px);
+ background-image: url(noise.png), -webkit-linear-gradient(#ffe0b0, #fff4e3 400px);
+ background-image: url(noise.png), linear-gradient(#ffe0b0, #fff4e3 400px);
+}
+
+body.bz_group_webtools-security,
+body.bz_group_websites-security,
+body.bz_group_bugzilla-security {
+ background-image: url(noise.png), -moz-linear-gradient(#ffcccc, #fff0f0 400px);
+ background-image: url(noise.png), -webkit-linear-gradient(#ffcccc, #fff0f0 400px);
+ background-image: url(noise.png), linear-gradient(#ffcccc, #fff0f0 400px);
+}
+
+body.bz_group_client-services-security,
+body.bz_group_mozilla-services-security {
+ background-image: url(noise.png), -moz-linear-gradient(#ffffa3, #ffffe3 400px);
+ background-image: url(noise.png), -webkit-linear-gradient(#ffffa3, #ffffe3 400px);
+ background-image: url(noise.png), linear-gradient(#ffffa3, #ffffe3 400px);
+}
+
+a, #header a, #header a:visited, #footer a, #footer a:visited {
+ color: #0095dd;
+}
+
+a:hover, #header a:hover, #footer a:hover {
+ color: #00539f;
+}
+
+select[multiple], textarea, input[type=text], input[type=password], input:not([type]), .text_input, .yui-ac-input {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ border: 1px solid #b2b2b2;
+ border-radius: .25em;
+ box-shadow: inset 0 1px rgba(0, 0, 0, 0.1);
+ background: white;
+ padding: 4px 3px 5px;
+ color: #404040;
+ vertical-align: top;
+}
+
+select[multiple], .text_input, .yui-ac-input, input {
+ font-size: 1em;
+}
+
+select[multiple]:focus, textarea:focus, .text-input:focus, -yui-ac-input:focus, input:focus {
+ border-color: #42a4e0;
+ -webkit-box-shadow: 0 0 0 2px rgba(73,173,227,0.4);
+ -moz-box-shadow: 0 0 0 2px rgba(73,173,227,0.4);
+ box-shadow: 0 0 0 2px rgba(73,173,227,0.4);
+}
+
+select, select[multiple] {
+ font-size: 12px;
+}
+
+hr {
+ border: none;
+ height: 1px;
+ color: #ccc;
+ background-color: #ccc;
+ margin: 1em 0;
+}
+
+#changeform hr {
+ display: none;
+}
+
+#header {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ background: #e5e3dc;
+ background: -moz-linear-gradient(#e5e3dc, #ecebe5);
+ background: -webkit-linear-gradient(#e5e3dc, #ecebe5);
+ background: linear-gradient(#e5e3dc, #ecebe5);
+ border-radius: 0;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ border-top: 2px solid rgb(255, 255, 255);
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ margin: -15px -15px 0 -15px;
+ color: transparent;
+}
+
+#header .subheader {
+ text-align: left;
+ padding-left: 10px;
+}
+
+#header .wrapper {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ margin: -1px auto 0px;
+ width: 99%;
+}
+
+#header .wrapper:after {
+ clear: both;
+ content: ".";
+ display: block;
+ height: 0;
+ visibility: hidden;
+}
+
+#bugzilla_version {
+ float: right;
+ color: #888;
+ padding: 5px 20px;
+}
+
+#titles {
+ width: 100%;
+ background-color: transparent;
+ padding: 0 1em 0 1em;
+}
+
+#information {
+ text-align: left;
+ padding-left: 2em;
+}
+
+#title {
+ width: 150px;
+ font-size: 120%;
+}
+
+#moz_tab {
+ width: 100px;
+ vertical-align: top;
+}
+
+#moz_login {
+ text-align: right;
+ padding-right: 2em;
+ color: #404040;
+}
+
+#header .links {
+ background: transparent;
+ border: none;
+ border-radius: 0;
+ color: #404040;
+ position: relative;
+ width: 50%;
+}
+
+#header .links {
+ width: auto;
+}
+
+.login-links ul {
+}
+
+.login-links li {
+ display: inline;
+}
+
+.links a {
+ margin: 0 10px 0 10px;
+}
+
+.links .home {
+ font-weight: bold;
+}
+
+.links .separator {
+ display: none;
+}
+
+#quicksearch_top, #quicksearch_main {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ background: url(search.png) 5px center no-repeat, #fafafa;
+ background: url(search.png) 5px center no-repeat, -moz-linear-gradient(#fafafa, #fff);
+ background: url(search.png) 5px center no-repeat, -webkit-linear-gradient(#fafafa, #fff);
+ background: url(search.png) 5px center no-repeat, linear-gradient(#fafafa, #fff);
+ padding: .4em 1em .45em 26px;
+ width: 200px;
+}
+
+#footer .links .quicksearch_form {
+ display: none;
+}
+
+#header .form a {
+ margin: 0;
+}
+
+.links .dropdown {
+ background: rgba(0, 0, 0, 0.05);
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ border-radius: .25em;
+ display: inline-block;
+ padding: 4px 8px;
+ position: relative;
+ cursor: default;
+}
+
+.links .dropdown .anchor {
+ background-image: url(dropdown.png);
+ background-position: right center;
+ background-repeat: no-repeat;
+ display: inline-block;
+ min-width: 110px;
+ padding-right: 15px;
+}
+
+.links .dropdown ul {
+ background: #fafafa;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 0 0 .25em .25em;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ display: none;
+ padding: 4px;
+ position: absolute;
+ right: -1px;
+ margin-top: 4px;
+ z-index: 2;
+ text-align: left;
+}
+
+.links .dropdown:hover ul {
+ display: block;
+}
+
+.links .dropdown li {
+ display: block;
+}
+
+.links .dropdown:hover {
+ border-bottom-right-radius: 0;
+}
+
+.links .dropdown li {
+ display: block;
+}
+
+#bugzilla-body {
+ background: none;
+ border: none;
+ color: #404040;
+ margin: 10px auto 15px;
+ padding: 0;
+ width: 99%;
+}
+
+/* Home */
+
+#page-index {
+ max-width: none;
+}
+
+#page-index td:first-child {
+ text-align: center;
+}
+
+#quicksearch_links {
+ margin-top: 10px;
+}
+
+/* Bugs */
+
+.navigation {
+ background: rgba(255, 255, 255, 0.3);
+ padding: 5px 10px;
+}
+
+u {
+ border-bottom: 1px solid #aaa;
+ text-decoration: none;
+}
+
+#field_container_see_also br {
+ margin-bottom: 10px;
+}
+
+.bz_alias_short_desc_container {
+ background: none;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 30px;
+ padding: 5px 0;
+ text-shadow: 0 1px rgba(255, 255, 255, 0.2);
+}
+
+.bz_alias_short_desc_container b {
+ font-weight: normal;
+}
+
+.bz_alias_short_desc_container .editme {
+ font-weight: normal;
+}
+
+.last_comment_link {
+ font-size: 18px;
+}
+
+.last_comment_link b {
+ border-bottom: 1px solid #aaa;
+ font-weight: normal;
+}
+
+table.edit_form {
+ background: #fff;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ margin-bottom: 20px;
+ padding: 10px 10px 80px;
+ position: relative;
+}
+
+table.edit_form tbody {
+ width: 100%;
+}
+
+table.edit_form hr {
+ display: none;
+}
+
+.field_label {
+ font-weight: bold !important;
+ padding-right: 10px;
+ vertical-align: baseline;
+ white-space: nowrap;
+}
+
+.field_label a, .field_label b {
+ color: #404040;
+ font-weight: bold;
+}
+
+.field_value .text_input {
+ min-width: 0;
+}
+
+#product, #component {
+ width: 235px;
+}
+
+#bz_show_bug_column_1 tr:last-child span {
+ position: absolute;
+ left: 20px;
+ bottom: 20px;
+}
+
+#commit_top {
+ position: absolute;
+ bottom: 20px;
+ right: 10px;
+}
+
+.cc_list_display {
+ background: #fff;
+ float: none;
+ font-size: 11px;
+ margin-top: 3px;
+ max-width: none;
+ padding: 5px;
+}
+
+#project-flags, #custom-flags {
+ border-collapse: collapse;
+}
+
+#project-flags label, #custom-flags label {
+ margin-right: 10px;
+}
+
+#cf_crash_signature {
+ width: 100%;
+}
+
+#attachment_table {
+ background: #fff;
+ border: none;
+ border-collapse: collapse;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ margin-bottom: 40px;
+}
+
+#attachment_table td {
+ border: none;
+}
+
+#attachment_table th, .bz_attach_footer, .bz_time_tracking_table th {
+ background: #eee;
+ color: #404040;
+}
+
+#attachment_table .bz_attach_actions {
+ white-space: nowrap;
+}
+
+/* background for diff views */
+.file_table, .file-table {
+ background: #ffffff;
+}
+
+.bz_comment {
+ width: 65em !important;
+ margin: 0 0 20px;
+}
+
+.bz_comment pre, #comment {
+ font: 13px/1.2 "Droid Sans Mono", Menlo, Monaco, "Courier New", Courier, monospace;
+}
+
+.bz_first_comment_head, .bz_comment_head {
+ font-weight: normal;
+ line-height: 32px;
+ padding-bottom: 2px;
+ padding-left: 0px;
+ margin-left: -5px;
+ white-space: nowrap;
+ background-color: transparent;
+}
+
+.bz_comment_head img, .bz_first_comment_head img {
+ vertical-align: middle;
+}
+
+.bz_comment_user a {
+ -moz-transition: all 0.25s linear 0s;
+ -webkit-transition: all 0.25s linear 0s;
+ transition: all 0.25s linear 0s;
+ transition: all 0.25s linear 0s;
+ color: #0095dd;
+ padding: 0px;
+ margin: 0px;
+}
+
+.bz_comment_user a:hover {
+ -moz-transition: all 0.25s linear 0s;
+ -webkit-transition: all 0.25s linear 0s;
+ transition: all 0.25s linear 0s;
+ background: #fff;
+ border: none;
+ text-decoration: none;
+}
+
+.bz_comment_user .vcard {
+ font-weight: bold;
+}
+
+.bz_comment_actions {
+ margin: 0px 0px;
+}
+
+.new_user {
+ margin-left: 10px;
+}
+
+.ih_history {
+ padding: 0 !important;
+}
+
+.ih_history .bz_comment_head {
+ padding-bottom: 3px;
+}
+
+.ih_history_item:not(.ih_hidden) ~ .ih_history_item:not(.ih_hidden) {
+ margin-top: 20px;
+}
+
+.ih_history_change {
+ background: #eee;
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ padding: 10px;
+ position: relative;
+}
+
+.bz_comment_text {
+ background: #fff;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ margin: 1px 0 0 0;
+ overflow: auto;
+ padding: 10px;
+ position: relative;
+}
+
+.bz_comment_text:after, .bz_comment_text:before {
+ bottom: 100%;
+ border: solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+}
+
+.bz_comment_text:after {
+ border-bottom-color: #fff;
+ border-width: 8px;
+ left: 16px;
+}
+
+.bz_comment_text span.quote, .bz_comment_text span.quote_wrapped {
+ background: #eee !important;
+ color: #444 !important;
+ display: block !important;
+ margin-top: 5px !important;
+ margin-bottom: -10px !important;
+ overflow: auto;
+ padding: 5px !important;
+}
+
+.bz_comment_tags {
+ background: #eee;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+ padding: 5px;
+}
+
+.bz_comment_tag {
+ background: #fff;
+ color: #444;
+ border: none;
+ padding: 2px 6px;
+}
+
+.bz_comment_tag a {
+ color: #0095DD;
+}
+
+#bz_ctag_error {
+ border: none;
+ background-color: #faa;
+ color: #444;
+ padding: 2px 6px;
+}
+
+#bz_ctag_error a {
+ color: #0095DD;
+}
+
+.ih_inlinehistory {
+ background: #eee;
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ padding: 10px;
+ position: relative;
+ top: -1px;
+}
+
+.bz_collapse_expand_comments li {
+ white-space: nowrap;
+}
+
+#add_comment {
+ border: 1px solid #ccc;
+ border-width: 1px 0;
+ margin-bottom: 20px;
+ padding: 10px 0;
+}
+
+#add_comment > table {
+ border-collapse: collapse;
+ width: 661px;
+}
+
+#comment {
+ -moz-box-sizing: content-box;
+ -webkit-box-sizing: content-box;
+ box-sizing: content-box;
+}
+
+#comment_tabs {
+ margin-top: 2px;
+}
+
+#comment_preview {
+ background: white;
+ display: block;
+ clear: both;
+}
+
+#comment_preview_text {
+ background: transparent;
+ border: none;
+ box-shadow: none;
+ padding: 2px 2px 2px 1px;
+}
+
+#footer {
+ background: #fff;
+ border: none;
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+ border-radius: 0;
+ color: #bbb;
+ width: auto;
+ margin-bottom: 1em;
+}
+
+#privacy-policy {
+ margin-bottom: 1em;
+}
+
+button, input[type=submit], input[type=button], #commit, #commit_top, #header .btn, #header input[type=submit] {
+ background-color: #43a6e2;
+ background-image: -moz-linear-gradient(#43a6e2,#277ac1);
+ background-image: -webkit-linear-gradient(#43a6e2,#277ac1);
+ background-image: linear-gradient(#43a6e2,#277ac1);
+ -moz-transition: all linear 0.25s;
+ -webkit-transition: all linear 0.25s;
+ transition: all linear 0.25s;
+ border-radius: .25em;
+ border: 0px none;
+ box-shadow: 0 1px 0 0 rgba(0,0,0,0.2),inset 0 -1px 0 0 rgba(0,0,0,0.3);
+ color: rgb(255, 255, 255);
+ cursor: pointer;
+ display: inline-block;
+ font-size: 12px;
+ font-weight: 600;
+ text-align: center;
+ text-decoration: none;
+ text-shadow: 0px 1px 0px rgba(0, 0, 0, 0.25);
+ padding: .425em 1em .5em;
+}
+
+button:hover, input[type=submit]:hover, input[type=button]:hover, #commit:hover, #commit_top:hover, #header .btn:hover, #header input[type=submit]:hover {
+ -webkit-box-shadow: 0 1px 0 0 rgba(0,0,0,0.2),inset 0 -1px 0 0 rgba(0,0,0,0.3),inset 0 12px 24px 2px #38a9ed;
+ -moz-box-shadow: 0 1px 0 0 rgba(0,0,0,0.2),inset 0 -1px 0 0 rgba(0,0,0,0.3),inset 0 12px 24px 2px #38a9ed;
+ box-shadow: 0 1px 0 0 rgba(0,0,0,0.2),inset 0 -1px 0 0 rgba(0,0,0,0.3),inset 0 12px 24px 2px #38a9ed;
+ -moz-transition: all linear 0.25s;
+ -webkit-transition: all linear 0.25s;
+ transition: all linear 0.25s;
+}
+
+button:active, input[type=submit]:active, input[type=button]:active, #commit:active, #commit_top:active, #header .btn:active, #header input[type=submit]:active {
+ -webkit-box-shadow: inset 0 2px 0 0 rgba(0,0,0,0.2),inset 0 12px 24px 6px rgba(0,0,0,0.2),inset 0 0 2px 2px rgba(0,0,0,0.2);
+ -moz-box-shadow: inset 0 2px 0 0 rgba(0,0,0,0.2),inset 0 12px 24px 6px rgba(0,0,0,0.2),inset 0 0 2px 2px rgba(0,0,0,0.2);
+ box-shadow: inset 0 2px 0 0 rgba(0,0,0,0.2),inset 0 12px 24px 6px rgba(0,0,0,0.2),inset 0 0 2px 2px rgba(0,0,0,0.2);
+ -moz-transition: all linear 0.25s;
+ -webkit-transition: all linear 0.25s;
+ transition: all linear 0.25s;
+}
+
+button[disabled], input[type=submit][disabled], input[type=button][disabled], button[disabled]:hover, input[type=submit][disabled]:hover, input[type=button][disabled]:hover, button[disabled]:active, input[type=submit][disabled]:active, input[type=button][disabled]:active {
+ background-color: #bfc7cd;
+ background-image: -moz-linear-gradient(#bfc7cd,#9ca3aa);
+ background-image: -webkit-linear-gradient(#bfc7cd,#9ca3aa);
+ background-image: linear-gradient(#bfc7cd,#9ca3aa);
+ box-shadow: 0 1px 0 0 rgba(0,0,0,0.2),inset 0 -1px 0 0 rgba(0,0,0,0.3);
+ cursor: pointer;
+}
+
+.notransition {
+ -webkit-transition: none !important;
+ -moz-transition: none !important;
+ -o-transition: none !important;
+ -ms-transition: none !important;
+ transition: none !important;
+}
+
+.calendar_button, .calendar_button:hover {
+ box-shadow: none;
+ padding: 0;
+}
+
+.related_actions {
+ line-height: 19px;
+ padding: 5px 10px;
+}
+
+.arrow_down {
+ border-top-color: #0095dd;
+}
+
+/* Attachments */
+
+#viewFrame {
+ border: 2px solid #222;
+ margin-bottom: 10px;
+}
+
+#editFrame, #viewDiffFrame, #viewFrame {
+ margin-left: 0;
+}
+
+#flags label {
+ font-weight: normal;
+}
+
+/* tabs */
+
+table.tabs {
+ border-collapse: separate;
+ border-spacing: 1em 0;
+}
+
+.tabs td {
+ background: rgba(255,255,255,0.5);
+ padding: 1em;
+ text-align: center;
+ border-style: none;
+ font-size: 12px;
+ text-transform: uppercase;
+}
+
+.tabs td.selected {
+ background: white;
+ font-weight: 700;
+}
+
+.tabs td.spacer {
+ background: transparent;
+}
+
+.tabs a {
+ color: #333;
+}
+
+.tabbody {
+ background: white;
+ padding: 1em 2em;
+}
+
+/* splinter */
+
+#splinter-files .new-line, #splinter-files .old-line {
+ font-size: 90%;
+}
+
+/* search */
+
+#summary_field.search_field_row input {
+ padding-bottom: 6px;
+}
+
+/* Smaller than standard 990 (devices and browsers) */
+@media only screen and (max-width: 989px) {
+ #header .links {
+ float: none;
+ }
+}
+
+/* Tablet Portrait size to standard 990 */
+@media only screen and (min-width: 768px) and (max-width: 989px) {
+ body {
+ min-width: 768px;
+ }
+
+ #header .wrapper, #bugzilla-body {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ min-width: 768px;
+ }
+
+ #footer > * {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ min-width: 768px;
+ }
+}
+
+/* All Mobile Sizes */
+@media only screen and (max-width: 767px) {
+ table.edit_form, table.edit_form > tbody > tr > td {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ display: block;
+ width: 100% !important;
+ }
+
+ .bz_column_spacer {
+ width: auto;
+ height: 20px;
+ }
+}
+
+
+
+/* Mobile Landscape Size to Tablet Portrait */
+@media only screen and (min-width: 480px) and (max-width: 767px) {
+ body {
+ min-width: 480px;
+ }
+
+ #header .wrapper, #bugzilla-body, #attachment_table,
+ .bz_comment, #add_comment > table, #comment {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 480px !important;
+ }
+
+ #footer > * {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 480px;
+ }
+}
+
+/* Mobile Portrait Size to Mobile Landscape Size */
+@media only screen and (max-width: 479px) {
+ body {
+ min-width: 100%;
+ }
+
+ #header .wrapper, #bugzilla-body, #attachment_table,
+ .bz_comment, #add_comment > table, #comment {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 100% !important;
+ }
+
+ #footer > * {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 100%;
+ }
+}
diff --git a/skins/contrib/Mozilla/grain.png b/skins/contrib/Mozilla/grain.png
new file mode 100644
index 000000000..2980ee90e
--- /dev/null
+++ b/skins/contrib/Mozilla/grain.png
Binary files differ
diff --git a/skins/contrib/Mozilla/index.css b/skins/contrib/Mozilla/index.css
new file mode 100644
index 000000000..3ca1fba51
--- /dev/null
+++ b/skins/contrib/Mozilla/index.css
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#enter_bug {
+ background: url(bugzilla-papericon.png) no-repeat;
+}
+#query {
+ background: url(bugzilla-magnifier.png) no-repeat;
+}
+#account {
+ background: url(bugzilla-person-alternate.png) no-repeat;
+ margin-right: 0;
+}
+#get_help {
+ background: url(bugzilla-questionmark2.png) no-repeat !important;
+}
diff --git a/skins/contrib/Mozilla/noise.png b/skins/contrib/Mozilla/noise.png
new file mode 100644
index 000000000..97407ffd2
--- /dev/null
+++ b/skins/contrib/Mozilla/noise.png
Binary files differ
diff --git a/skins/contrib/Mozilla/search.png b/skins/contrib/Mozilla/search.png
new file mode 100644
index 000000000..a56b5e2cd
--- /dev/null
+++ b/skins/contrib/Mozilla/search.png
Binary files differ
diff --git a/skins/contrib/Mozilla/tabzilla.png b/skins/contrib/Mozilla/tabzilla.png
new file mode 100644
index 000000000..7619610c3
--- /dev/null
+++ b/skins/contrib/Mozilla/tabzilla.png
Binary files differ
diff --git a/skins/custom/IE-fixes.css b/skins/custom/IE-fixes.css
new file mode 100644
index 000000000..0d5c47630
--- /dev/null
+++ b/skins/custom/IE-fixes.css
@@ -0,0 +1,4 @@
+.bz_short_desc_column a, .bz_short_short_desc_column a {
+ /* color:inherit */
+ color: expression(this.parentNode.currentStyle['color']);
+}
diff --git a/skins/custom/bug_groups.css b/skins/custom/bug_groups.css
new file mode 100644
index 000000000..282757a3b
--- /dev/null
+++ b/skins/custom/bug_groups.css
@@ -0,0 +1,31 @@
+/* colorize bugs in various groups */
+/* these are also defined in skins/contrib/Mozilla/global.css */
+
+body[class*=bz_group_] {
+ background-color: #e0e0ff;
+ border-left: solid red 2px;
+ padding-left: 13px;
+}
+
+body[class*=bz_group_] #bugzilla-body {
+ background-color: inherit;
+}
+
+body.bz_group_infrasec {
+ background-color: #ffcc99;
+}
+
+body.bz_group_webtools-security,
+body.bz_group_websites-security,
+body.bz_group_bugzilla-security {
+ background-color: #ffeeee;
+}
+
+body.bz_group_client-services-security,
+body.bz_group_mozilla-services-security {
+ background-color: #ffff80;
+}
+
+body[class*=core-security] {
+ background-color: #ffe0b0;
+}
diff --git a/skins/custom/buglist.css b/skins/custom/buglist.css
new file mode 100644
index 000000000..d3097aedd
--- /dev/null
+++ b/skins/custom/buglist.css
@@ -0,0 +1,41 @@
+/* For the JS-sorting buglist. */
+
+th.sorttable_sorted,
+th.sorttable_sorted_reverse,
+th.sorted_0 {
+ background-color: #aaa;
+}
+
+th.sorted_1 {
+ background-color: #bbb;
+}
+
+th.sorted_2 {
+ background-color: #ccc;
+}
+
+th.sorted_3 {
+ background-color: #ddd;
+}
+
+th.sorted_4 {
+ background-color: #eee;
+}
+
+th.sorted_5 {
+ background-color: #fff;
+}
+
+.bz_short_desc_column a, .bz_short_short_desc_column a {
+ text-decoration: none;
+ color: inherit;
+}
+
+.bz_short_desc_column a:hover, .bz_short_short_desc_column a:hover {
+ text-decoration: underline;
+}
+
+#request_form #filtering th {
+ padding-left: 0.5em;
+}
+
diff --git a/skins/custom/create_bug.css b/skins/custom/create_bug.css
new file mode 100644
index 000000000..333aff48f
--- /dev/null
+++ b/skins/custom/create_bug.css
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+
+.tracking_flags .field_label a {
+ font-weight: normal !important;
+ color: #000;
+}
+
+#guided {
+ margin-top: 30px;
+}
+
+#component {
+ width: 25em;
+}
+
+.hidden_text {
+ opacity: 0;
+ filter: alpha(opacity=0);
+}
+
+#bug_create_warning {
+ border: 1px solid #dddddd;
+ background: #fff9db;
+ color: #666458;
+ padding: 5px;
+}
+
+#bug_create_warning_image {
+ float: left;
+ padding: 5px;
+}
+
+#bug_create_warning_text {
+ margin-left: 42px;
+}
+
+#keyword_container,
+#bug_mentors_autocomplete,
+#container_see_also {
+ width: 25em
+}
+
+#custom_form_list {
+ font-weight: bold;
+ border: 1px solid #dddddd;
+ background: #fff9db;
+ color: #666458;
+ padding: 5px 5px 10px 5px;
+ min-height: 48px;
+ border-radius: 5px;
+}
+
+#custom_form_list_image {
+ float: left;
+}
+
+#custom_form_list_text {
+ margin-left: 55px;
+}
+
+#custom_form_list ul {
+ margin-top: 0.5em;
+ margin-bottom: 0;
+ padding-left: 20px;
+}
diff --git a/skins/custom/global.css b/skins/custom/global.css
new file mode 100644
index 000000000..cc155124a
--- /dev/null
+++ b/skins/custom/global.css
@@ -0,0 +1,77 @@
+/*
+ * Custom rules for skins/standard/global.css.
+ * The rules you put here override rules in that stylesheet.
+ */
+
+body {
+ margin: 0;
+ padding: 15px 15px 2px 15px;
+}
+
+#header .btn, #header .txt {
+ font-size: 100%;
+}
+
+#header #information {
+ color: #dddddd;
+ font-size: small;
+}
+
+pre {
+ font-size: medium;
+}
+
+#attachment_table {
+ width: 50em;
+}
+
+#page-index #quicksearchForm {
+ padding-top: 20px;
+}
+
+/* createaccount styling */
+.support_div {
+ width: 40%;
+ font-size: 80%;
+}
+
+.support_div > img {
+ padding: 5px 20px;
+}
+
+a {
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+a.controller {
+ font-size: 100%;
+ border: 1px solid #c0c0c0;
+ padding: 3px;
+}
+
+#footer .outro {
+ text-align:left;
+ padding-left:1ex;
+ padding-bottom:1ex;
+}
+
+.group_secure > th > a {
+ background-image: url("../../images/padlock.png");
+ background-position: center left;
+ background-repeat: no-repeat;
+ padding-left: 18px;
+}
+
+#privacy-policy {
+ font-size: x-small;
+ width: 100%;
+ text-align: right;
+}
+
+.highlighted {
+ background: lightyellow;
+}
diff --git a/skins/custom/index.css b/skins/custom/index.css
new file mode 100644
index 000000000..0c6884124
--- /dev/null
+++ b/skins/custom/index.css
@@ -0,0 +1,31 @@
+/*
+ * Custom rules for index.css.
+ * The rules you put here override rules in that stylesheet.
+ */
+
+/* index.html.tmpl puts intro hook contents inside a div which causes
+ * the icons to display over two rows when adding the Help icon.
+ * So we change to inline to make it display a single row. */
+#page-index .intro { display: inline; }
+
+#get_help { background: url(../standard/index/help.png) no-repeat; }
+
+.bz_common_actions {
+ display: block;
+ height: 170px;
+ width: 145px;
+ float: left;
+ margin: 0 2ex 2em 0;
+ text-align: center;
+}
+.bz_common_actions span {
+ position: relative;
+ top: 95%;
+ font-weight: bold;
+}
+.bz_common_actions,
+.bz_common_actions:visited,
+.bz_common_actions:hover
+{
+ text-decoration: none;
+}
diff --git a/skins/custom/search_form.css b/skins/custom/search_form.css
new file mode 100644
index 000000000..1855eb445
--- /dev/null
+++ b/skins/custom/search_form.css
@@ -0,0 +1,6 @@
+
+/* let the browser choose the select height from the "size" param */
+.search_field_grid select {
+ height: auto;
+}
+
diff --git a/skins/custom/show_bug.css b/skins/custom/show_bug.css
new file mode 100644
index 000000000..e98e32fb4
--- /dev/null
+++ b/skins/custom/show_bug.css
@@ -0,0 +1,85 @@
+/*
+ * Custom rules for show_bug.css.
+ * The rules you put here override rules in that stylesheet.
+ */
+
+.last_comment_link {
+ float: right;
+ font-size: 80%;
+ font-weight: normal;
+ margin-left: 1em;
+}
+
+#legal_disclaimer {
+ width: 40em;
+ padding: 1em;
+ margin: 0 1em 1em 1em;
+ font-weight: bold;
+ border: 1px red solid;
+ background-color: lightyellow;
+}
+
+.bz_patch {
+ background: #ffffcc;
+}
+
+.cc_list_display {
+ list-style: none;
+ margin:0px;
+ padding:5px;
+ padding-right:20px;
+ overflow:auto;
+ float:left;
+ max-width:465px;
+ max-height:100px;
+ border:1px solid #CCC;
+}
+
+.cc_list_display li {
+ margin:0px;
+ padding:0px;
+ white-space:nowrap;
+}
+
+#wave_wand {
+ margin-top: 0px;
+}
+
+/* put the width on the TD rather than the PRE to stop the col resizing
+ when comments are hidden */
+.bz_comment {
+ width: 55em;
+}
+.bz_comment_text {
+ width: auto;
+}
+
+.bz_comment_number {
+ float: right;
+}
+
+/* style all field labels the same */
+
+.field_label, .field_label a {
+ color: #000;
+ font-weight: bold;
+}
+
+.field_label a {
+ cursor: help;
+}
+
+.edit_form table th:first-child {
+ width: 0px;
+}
+
+#bz_show_bug_column_1, #bz_show_bug_column_2 {
+ width: 50%;
+}
+
+/* fix flag table's vertical alignment */
+
+table#flags {
+ border-collapse: collapse;
+ border-spacing: 0px;
+}
diff --git a/skins/standard/attachment.css b/skins/standard/attachment.css
index 55e62f2b0..01c4311d4 100644
--- a/skins/standard/attachment.css
+++ b/skins/standard/attachment.css
@@ -30,7 +30,9 @@ table.attachment_entry td {
}
table#flags th,
-table#flags td {
+table#flags td,
+table#attachment_flags th,
+table#attachment_flags td {
text-align: left;
vertical-align: baseline;
font-size: small;
diff --git a/skins/standard/buglist.css b/skins/standard/buglist.css
index ebebfb3ef..e6460a48b 100644
--- a/skins/standard/buglist.css
+++ b/skins/standard/buglist.css
@@ -25,6 +25,15 @@
font-weight: bold;
}
+.bz_query_buttons form {
+ float: left;
+ margin-right: 2px;
+}
+
+.bz_query_edit {
+ padding-left: 2em;
+}
+
.search_description {
margin: .5em 0;
padding: 0;
@@ -119,7 +128,7 @@ td.bz_total {
margin-top: .25em;
}
-.bz_query_explain {
+.bz_query_debug {
text-align: left;
}
@@ -127,3 +136,17 @@ td.bz_total {
color: inherit;
}
+/* The "filtering" table is specific to request.cgi.
+ * Same for the "requests" class used for tables. */
+
+#filtering #requester, #filtering #requestee {
+ min-width: 8em;
+}
+
+#filtering th {
+ text-align: right;
+}
+
+table.requests th {
+ text-align: left;
+}
diff --git a/skins/standard/enter_bug.css b/skins/standard/enter_bug.css
index 88d9e9e85..34be42f7a 100644
--- a/skins/standard/enter_bug.css
+++ b/skins/standard/enter_bug.css
@@ -69,4 +69,4 @@
/* Make the Add Me to CC button never wrap. */
#possible_duplicates .yui-dt-col-update_token { white-space: nowrap; }
-form#Create #possible_duplicates td { vertical-align: middle; } \ No newline at end of file
+form#Create #possible_duplicates td { vertical-align: middle; }
diff --git a/skins/standard/global.css b/skins/standard/global.css
index 4d4b02153..3a61dae83 100644
--- a/skins/standard/global.css
+++ b/skins/standard/global.css
@@ -350,6 +350,11 @@ div#docslinks {
padding: 1em 0;
}
+.bz_comment_collapse_reason,
+.bz_default_collapsed .bz_comment_number {
+ font-weight: normal;
+}
+
/** End Comments **/
.bz_default_hidden, .bz_tui_hidden, .bz_hidden_field, .bz_hidden_option {
@@ -365,6 +370,10 @@ div#docslinks {
white-space: pre;
}
+.bz_comment_text span.quote_wrapped {
+ color: #65379c;
+}
+
table#flags th,
table#flags td {
vertical-align: middle;
@@ -380,7 +389,7 @@ input.requestee {
}
#error_msg {
- font-size: x-large;
+ font-size: large;
}
.warning {
@@ -388,9 +397,9 @@ input.requestee {
}
.throw_error {
- background-color: #ff0000;
+ background-color: #ff6666;
color: black;
- font-size: 120%;
+ font-size: large;
margin: 1em;
padding: 0.5em 1em;
}
@@ -455,7 +464,31 @@ div.user_match {
padding: 0.5em 1em;
}
-.collapsed {
+.arrow_down {
+ width: 0;
+ height: 0;
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 4px solid #003399;
+ position: relative;
+ top: 1em;
+ margin: 4px 0;
+}
+
+.arrow_container {
+ margin: 0 2px;
+ display: inline-block;
+}
+
+.collapsed,
+.bz_default_collapsed .bz_private_checkbox,
+.bz_default_collapsed .bz_comment_user,
+.bz_default_collapsed .bz_comment_user_images,
+.bz_default_collapsed .bz_comment_time,
+.bz_default_collapsed .bz_comment_tags,
+.bz_default_collapsed .bz_comment_text,
+.bz_default_collapsed .bz_collapsed_actions
+{
display: none;
}
@@ -527,13 +560,28 @@ input.required, select.required, span.required_explanation {
}
.bug_urls {
- margin: 0 0 1em 0;
+ margin: 0;
padding: 0;
list-style-type: none;
}
+.field_textarea_readonly {
+ margin: 2px;
+ padding: 4px;
+ overflow: auto;
+ float: left;
+ max-width: 30em;
+ max-height: 7em;
+ border: 1px solid #CCC;
+}
+
+.field_textarea_readonly pre {
+ font-family: monospace;
+ white-space: pre-wrap;
+}
+
/* custom styles for inline instances of autocomplete input fields */
-.yui-skin-sam .yui-ac-input { position:static !important;
+.yui-skin-sam .yui-ac-input { position:static !important;
vertical-align:middle !important; }
.yui-skin-sam .yui-ac-container { left:0px !important; }
.yui-skin-sam .yui-ac { display: inline-block; }
@@ -552,6 +600,43 @@ input.required, select.required, span.required_explanation {
margin-left: -1px;
}
+#comment_tabs {
+ border-spacing: 0;
+}
+
+.comment_tab {
+ display: table-cell;
+ border: 1px solid silver;
+ padding: 2px 1em;
+ cursor: pointer;
+ background: transparent;
+}
+
+.active_comment_tab {
+ background: #fff;
+ font-weight: bold;
+}
+
+#comment_preview {
+ border: 1px solid silver;
+ padding: 1px;
+ overflow: auto;
+ margin: 0px;
+}
+
+#comment_preview_text {
+ margin: 0px;
+ width: auto;
+}
+
+#comment_preview_loading {
+ font-style: italic;
+}
+
+#comment {
+ margin: 0px 0px 1em 0px;
+}
+
/*******************/
/* Form Validation */
/*******************/
diff --git a/skins/standard/guided.css b/skins/standard/guided.css
new file mode 100644
index 000000000..efecfe3ce
--- /dev/null
+++ b/skins/standard/guided.css
@@ -0,0 +1,4 @@
+#somebugs {
+ width: 100%;
+ height: 500px;
+}
diff --git a/skins/standard/reports.css b/skins/standard/reports.css
index 00272fdba..205946550 100644
--- a/skins/standard/reports.css
+++ b/skins/standard/reports.css
@@ -90,3 +90,8 @@
color: #333;
}
+.component_hilite {
+ background-color: lightgreen;
+ margin: 0;
+ padding: 1em 0;
+}
diff --git a/skins/standard/show_bug.css b/skins/standard/show_bug.css
index 8214ce5f4..656e130a0 100644
--- a/skins/standard/show_bug.css
+++ b/skins/standard/show_bug.css
@@ -116,3 +116,42 @@ table#flags {
.bz_bug .bz_alias_short_desc_container {
width: inherit;
}
+
+.bz_comment_tags {
+ margin-top: 3px;
+}
+
+.bz_comment_tag {
+ border: 1px solid #c8c8ba;
+ padding: 1px 3px;
+ margin-right: 2px;
+ border-radius: 0.5em;
+ background-color: #eee;
+ color: #000;
+}
+
+#bz_ctag_div {
+ display: inline-block;
+}
+
+#bz_ctag_error {
+ border: 1px solid #ff6666;
+ padding: 0px 2px;
+ border-radius: 0.5em;
+ margin: 2px;
+ display: inline-block;
+}
+
+#comment_tags_collapse_expand_container {
+ padding-top: 1em;
+}
+
+#comment_tags_collapse_expand {
+ list-style-type: none;
+ padding-left: 1em;
+}
+
+#comment_tags_collapse_expand li {
+ margin-bottom: 0px;
+}
+
diff --git a/t/001compile.t b/t/001compile.t
index 97a339b2d..a2176babd 100644
--- a/t/001compile.t
+++ b/t/001compile.t
@@ -45,6 +45,11 @@ sub compile_file {
# Bugzilla::Install::CPAN.)
local @INC = @INC;
+ if ($file =~ /extensions/) {
+ skip "$file: extensions not tested", 1;
+ return;
+ }
+
if ($file =~ s/\.pm$//) {
$file =~ s{/}{::}g;
use_ok($file);
diff --git a/t/004template.t b/t/004template.t
index 3b858c0b3..666ce5fa4 100644
--- a/t/004template.t
+++ b/t/004template.t
@@ -38,7 +38,7 @@ use CGI qw(-no_debug);
use File::Spec;
use Template;
-use Test::More tests => ( scalar(@referenced_files) + $num_actual_files );
+use Test::More tests => ( scalar(@referenced_files) + 2 * $num_actual_files );
# Capture the TESTOUT from Test::More or Test::Builder for printing errors.
# This will handle verbosity for us automatically.
@@ -60,11 +60,16 @@ my $fh;
# fall back to English if necessary.
foreach my $file (@referenced_files) {
- my $path = File::Spec->catfile($english_default_include_path, $file);
- if (-e $path) {
- ok(1, "$path exists");
+ my @path = map(File::Spec->catfile($_, $file), @include_paths);
+ push(@path, File::Spec->catfile($english_default_include_path, $file));
+ my $found;
+ foreach my $path (@path) {
+ $found = $path if -e $path;
+ }
+ if ($found) {
+ ok(1, "$file exists");
} else {
- ok(0, "$path cannot be located --ERROR");
+ ok(0, "$file cannot be located --ERROR");
}
}
@@ -118,6 +123,20 @@ foreach my $include_path (@include_paths) {
ok(0, "$path has bad syntax --ERROR");
print $fh $data . "\n";
}
+
+ # Make sure no forbidden constructs are present.
+ local $/;
+ open(FILE, '<', $path) or die "Can't open $file: $!\n";
+ $data = <FILE>;
+ close (FILE);
+
+ # Forbid single quotes to delimit URLs, see bug 926085.
+ if ($data =~ /href=\\?'/) {
+ ok(0, "$path contains blacklisted constructs: href='...'");
+ }
+ else {
+ ok(1, "$path contains no blacklisted constructs");
+ }
}
}
diff --git a/t/008filter.t b/t/008filter.t
index e73d23835..d0c0311f6 100644
--- a/t/008filter.t
+++ b/t/008filter.t
@@ -175,7 +175,8 @@ sub directive_ok {
return 1 if $directive =~ /^(IF|END|UNLESS|FOREACH|PROCESS|INCLUDE|
BLOCK|USE|ELSE|NEXT|LAST|DEFAULT|FLUSH|
ELSIF|SET|SWITCH|CASE|WHILE|RETURN|STOP|
- TRY|CATCH|FINAL|THROW|CLEAR|MACRO|FILTER)/x;
+ TRY|CATCH|FINAL|THROW|CLEAR|MACRO|FILTER|
+ CALL)/x;
# ? :
if ($directive =~ /.+\?(.+):(.+)/) {
@@ -224,7 +225,7 @@ sub directive_ok {
return 1 if $directive =~ /FILTER\ (html|csv|js|base64|css_class_quote|ics|
quoteUrls|time|uri|xml|lower|html_light|
obsolete|inactive|closed|unitconvert|
- txt|html_linebreak|none)\b/x;
+ txt|html_linebreak|none|json)\b/x;
return 0;
}
diff --git a/t/012throwables.t b/t/012throwables.t
index 3738ad524..590fb8aa5 100644
--- a/t/012throwables.t
+++ b/t/012throwables.t
@@ -62,7 +62,7 @@ foreach my $include_path (@include_paths) {
$file =~ s/\s.*$//; # nuke everything after the first space
$file =~ s|\\|/|g if ON_WINDOWS; # convert \ to / in path if on windows
$test_templates{$file} = ()
- if $file =~ m#global/(code|user)-error\.html\.tmpl#;
+ if $file =~ m#global/(code|user)-error(?:-errors)?\.html\.tmpl#;
}
}
@@ -75,7 +75,7 @@ plan tests => $tests;
# Collect all errors defined in templates
foreach my $file (keys %test_templates) {
- $file =~ m|template/([^/]+).*/global/([^/]+)-error\.html\.tmpl|;
+ $file =~ m|template/([^/]+).*/global/([^/]+)-error(?:-errors)?\.html\.tmpl|;
my $lang = $1;
my $errtype = $2;
diff --git a/t/Support/Files.pm b/t/Support/Files.pm
index 6c6e0ee57..2898fdd3f 100644
--- a/t/Support/Files.pm
+++ b/t/Support/Files.pm
@@ -23,14 +23,25 @@
package Support::Files;
+use Bugzilla;
+
use File::Find;
@additional_files = ();
@files = glob('*');
-find(sub { push(@files, $File::Find::name) if $_ =~ /\.pm$/;}, 'Bugzilla');
+my @extension_paths = map { $_->package_dir } @{ Bugzilla->extensions };
+find(sub { push(@files, $File::Find::name) if $_ =~ /\.pm$/;}, 'Bugzilla', @extension_paths);
push(@files, 'extensions/create.pl');
+my @extensions = glob('extensions/*');
+foreach my $extension (@extensions) {
+ # Skip disabled extensions
+ next if -e "$extension/disabled";
+
+ find(sub { push(@files, $File::Find::name) if $_ =~ /\.pm$/;}, $extension);
+}
+
sub isTestingFile {
my ($file) = @_;
my $exclude;
diff --git a/template/en/default/account/auth/login-small.html.tmpl b/template/en/default/account/auth/login-small.html.tmpl
index a9d86036a..220eb5f21 100644
--- a/template/en/default/account/auth/login-small.html.tmpl
+++ b/template/en/default/account/auth/login-small.html.tmpl
@@ -47,13 +47,14 @@
id="mini_login[% qs_suffix FILTER html %]"
onsubmit="return check_mini_login_fields( '[% qs_suffix FILTER html %]' );"
>
+
<input id="Bugzilla_login[% qs_suffix FILTER html %]"
class="bz_login"
name="Bugzilla_login"
title="Login"
- onfocus="mini_login_on_focus('[% qs_suffix FILTER js %]')"
+ placeholder="email address"
>
- <input class="bz_password"
+ <input class="bz_password"
id="Bugzilla_password[% qs_suffix FILTER html %]"
name="Bugzilla_password"
type="password"
@@ -62,7 +63,6 @@
<input class="bz_password bz_default_hidden bz_mini_login_help" type="text"
id="Bugzilla_password_dummy[% qs_suffix %]" value="password"
title="Password"
- onfocus="mini_login_on_focus('[% qs_suffix FILTER js %]')"
>
[% IF Param('rememberlogin') == 'defaulton' ||
Param('rememberlogin') == 'defaultoff'
@@ -74,42 +74,8 @@
[% END %]
<input type="submit" name="GoAheadAndLogIn" value="Log in"
id="log_in[% qs_suffix %]">
- <script type="text/javascript">
- mini_login_constants = {
- "login" : "login",
- "warning" : "You must set the login and password before logging in."
- };
- [%# We need this event to fire after autocomplete, because it does
- # something different depending on whether or not there's already
- # data in the login and password box.
- # However, autocomplete happens at all sorts of different times in
- # different browsers (before or after onDOMReady, before or after
- # window.onload, in almost all combinations you can imagine).
- # The only good solution I found is to time the event 200
- # milliseconds after window.onload for WebKit (doing it immediately
- # at onload works in Chrome but not in Safari, but I can't detect
- # them separately using YUI), and right after onDOMReady in Gecko.
- # The WebKit solution is also fairly guaranteed to work on any
- # browser (it's just strange, since the fields only populate 200 ms
- # after the page loads), so it's the default. IE doesn't even
- # recognize our forms as login forms, so I made it use the Gecko
- # method also (since it's nicer visually). Opera never autocompletes
- # forms without user interaction, so it also uses the Gecko method.
- #%]
- if (YAHOO.env.ua.gecko || YAHOO.env.ua.ie || YAHOO.env.ua.opera) {
- YAHOO.util.Event.onDOMReady(function() {
- init_mini_login_form('[% qs_suffix FILTER html %]');
- });
- }
- else {
- YAHOO.util.Event.on(window, 'load', function () {
- window.setTimeout(function() {
- init_mini_login_form('[% qs_suffix FILTER html %]');
- }, 200);
- });
- }
- </script>
- <a href="#" onclick="return hide_mini_login_form('[% qs_suffix %]')">[x]</a>
+ <a href="#" id="hide_mini_login[% qs_suffix FILTER html %]"
+ onclick="return hide_mini_login_form('[% qs_suffix %]')">[x]</a>
</form>
</li>
<li id="forgot_container[% qs_suffix %]">
diff --git a/template/en/default/account/auth/login.html.tmpl b/template/en/default/account/auth/login.html.tmpl
index 3de52b6a0..0aac403a5 100644
--- a/template/en/default/account/auth/login.html.tmpl
+++ b/template/en/default/account/auth/login.html.tmpl
@@ -37,14 +37,14 @@
[% USE Bugzilla %]
<p>
- I need a legitimate login and password to continue.
+ I need an email address and password to continue.
</p>
<form name="login" action="[% target FILTER html %]" method="POST"
[%- IF Bugzilla.cgi.param("data") %] enctype="multipart/form-data"[% END %]>
<table>
<tr>
- <th align="right"><label for="Bugzilla_login">Login:</label></th>
+ <th align="right"><label for="Bugzilla_login">Email Address:</label></th>
<td>
<input size="35" id="Bugzilla_login" name="Bugzilla_login">
[% Param('emailsuffix') FILTER html %]
@@ -64,7 +64,7 @@
<td>
<input type="checkbox" id="Bugzilla_remember" name="Bugzilla_remember" value="on"
[%+ "checked" IF Param('rememberlogin') == "defaulton" %]>
- <label for="Bugzilla_remember">Remember my Login</label>
+ <label for="Bugzilla_remember">Remember my email address</label>
</td>
</tr>
[% END %]
@@ -112,7 +112,7 @@
<form id="forgot" method="get" action="token.cgi">
<input type="hidden" name="a" value="reqpw">
If you have an account, but have forgotten your password,
- enter your login name below and submit a request
+ enter your email address below and submit a request
to change your password.<br>
<input size="35" name="loginname">
<input type="hidden" id="token" name="token" value="[% issue_hash_token(['reqpw']) FILTER html %]">
diff --git a/template/en/default/account/create.html.tmpl b/template/en/default/account/create.html.tmpl
index 5acd9f541..985a54841 100644
--- a/template/en/default/account/create.html.tmpl
+++ b/template/en/default/account/create.html.tmpl
@@ -77,4 +77,6 @@
<input type="submit" id="send" value="Send">
</form>
+[% Hook.process('additional_methods') %]
+
[% PROCESS global/footer.html.tmpl %]
diff --git a/template/en/default/account/prefs/email.html.tmpl b/template/en/default/account/prefs/email.html.tmpl
index 96a111bae..ffb153785 100644
--- a/template/en/default/account/prefs/email.html.tmpl
+++ b/template/en/default/account/prefs/email.html.tmpl
@@ -46,9 +46,12 @@
function SetCheckboxes(setting) {
for (var count = 0; count < document.userprefsform.elements.length; count++) {
var theinput = document.userprefsform.elements[count];
- if (theinput.type == "checkbox" && !theinput.disabled) {
+ if (theinput.type == "checkbox"
+ && !theinput.disabled
+ && !theinput.name.match("remove_ignored_bug"))
+ {
if (theinput.name.match("neg")) {
- theinput.checked = false;
+ theinput.checked = !setting;
}
else {
theinput.checked = setting;
@@ -119,6 +122,8 @@ document.write('<input type="button" value="Disable All Mail" onclick="SetCheckb
description = "A new $terms.bug is created" },
{ id = constants.EVT_OPENED_CLOSED,
description = "The $terms.bug is resolved or reopened" },
+ { id = constants.EVT_COMPONENT,
+ description = "The product or component changes" },
{ id = constants.EVT_PROJ_MANAGEMENT,
description = "The priority, status, severity, or milestone changes" },
{ id = constants.EVT_COMMENT,
@@ -284,6 +289,40 @@ You are currently not watching any users.
[% END %]
</p>
-<hr>
+<b>Ignore [% terms.Bugs %]</b>
-<br>
+<p>
+ You can specify a list of [% terms.bugs %] from which you never want to get
+ any email notification of any kind by adding their ID(s) as a comma-separated
+ list. Removing [% terms.abug %] by selecting it from the current ignored list
+ will re-enable email notifications for the [% terms.bug %].
+</p>
+[% IF user.bugs_ignored.size %]
+ <p>
+ You are currently ignoring:
+ <table>
+ [% FOREACH bug = user.bugs_ignored %]
+ <tr>
+ <td>
+ <input type="checkbox" name="remove_ignored_bug_[% bug.id FILTER html %]" value="1">
+ </td>
+ <td><a href="[% urlbase FILTER html %]show_bug.cgi?id=[% bug.id FILTER uri %]">
+ [% bug.id FILTER html %]</a>
+ </td>
+ <td>[% bug.status FILTER html %]</td>
+ <td>
+ [% IF user.can_see_bug(bug.id) %]
+ - [% bug.summary FILTER html %]
+ [% ELSE %]
+ (private)
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ </table>
+ </p>
+[% END %]
+
+<p>Add [% terms.bugs %]:<br>
+ <input type="text" id="add_ignored_bugs"
+ name="add_ignored_bugs" size="60"></p>
diff --git a/template/en/default/account/prefs/permissions.html.tmpl b/template/en/default/account/prefs/permissions.html.tmpl
index 5e8dc9ca2..d3c787b07 100644
--- a/template/en/default/account/prefs/permissions.html.tmpl
+++ b/template/en/default/account/prefs/permissions.html.tmpl
@@ -65,9 +65,9 @@
There are no permission bits set on your account.
[% END %]
- [% IF user.in_group('editusers') %]
+ [% IF user.in_group('admin') %]
<br>
- You have editusers privileges. You can turn on and off
+ You have admin privileges. You can turn on and off
all permissions for all users.
[% ELSIF set_bits.size %]
<br>
diff --git a/template/en/default/account/prefs/saved-searches.html.tmpl b/template/en/default/account/prefs/saved-searches.html.tmpl
index 1b78592ca..ce9623372 100644
--- a/template/en/default/account/prefs/saved-searches.html.tmpl
+++ b/template/en/default/account/prefs/saved-searches.html.tmpl
@@ -67,6 +67,7 @@
Share With a Group
</th>
[% END %]
+ [% Hook.process('saved-header') %]
</tr>
<tr>
<td>My [% terms.Bugs %]</td>
@@ -145,6 +146,7 @@
[% END %]
</td>
[% END %]
+ [% Hook.process('saved-row') %]
</tr>
[% END %]
</table>
diff --git a/template/en/default/account/prefs/settings.html.tmpl b/template/en/default/account/prefs/settings.html.tmpl
index f8b6ba487..65e31359b 100644
--- a/template/en/default/account/prefs/settings.html.tmpl
+++ b/template/en/default/account/prefs/settings.html.tmpl
@@ -42,7 +42,7 @@
[% FOREACH name = setting_names %]
[% default_name = name _ '-isdefault' %]
[% default_val = settings.${name}.default_value %]
- <tr>
+ <tr id="[% name FILTER html %]_row">
<td align="right">
[% setting_descs.$name OR name FILTER html %]
</td>
@@ -75,3 +75,10 @@
</table>
[% END %]
<br>
+
+<script>
+YAHOO.util.Event.onDOMReady(function() {
+ var id = document.location.hash.substring(1) + '_row';
+ YAHOO.util.Dom.addClass(id, 'highlighted');
+});
+</script>
diff --git a/template/en/default/account/profile-activity.html.tmpl b/template/en/default/account/profile-activity.html.tmpl
index ee00875fe..aa6a63e85 100644
--- a/template/en/default/account/profile-activity.html.tmpl
+++ b/template/en/default/account/profile-activity.html.tmpl
@@ -35,7 +35,7 @@
#%]
[% title = BLOCK %]
- Account History for '[% otheruser.login FILTER html %]'
+ [% IF action == 'admin_activity' %]Admin[% ELSE %]Account[% END %] History for '[% otheruser.login FILTER html %]'
[% END %]
diff --git a/template/en/default/admin/flag-type/edit.html.tmpl b/template/en/default/admin/flag-type/edit.html.tmpl
index de0476e19..69dc05bd3 100644
--- a/template/en/default/admin/flag-type/edit.html.tmpl
+++ b/template/en/default/admin/flag-type/edit.html.tmpl
@@ -231,6 +231,8 @@
</td>
</tr>
+ [% Hook.process('rows') %]
+
<tr>
<th>&nbsp;</th>
<td>
diff --git a/template/en/default/admin/params/admin.html.tmpl b/template/en/default/admin/params/admin.html.tmpl
index dd83ebb2e..f84dbc701 100644
--- a/template/en/default/admin/params/admin.html.tmpl
+++ b/template/en/default/admin/params/admin.html.tmpl
@@ -37,5 +37,8 @@
"$terms.Bugzilla will issue a warning in case you'd run into inconsistencies " _
"when you're about to do so, but such deletions remain kinda scary. " _
"So, you have to turn on this option before any such deletions " _
- "will ever happen." }
-%] \ No newline at end of file
+ "will ever happen."
+
+ last_visit_keep_days => "This option controls how many days $terms.Bugzilla will " _
+ "remember when users visit specific ${terms.bugs}."}
+%]
diff --git a/template/en/default/admin/params/advanced.html.tmpl b/template/en/default/admin/params/advanced.html.tmpl
index a8e8a297b..a2103c652 100644
--- a/template/en/default/admin/params/advanced.html.tmpl
+++ b/template/en/default/admin/params/advanced.html.tmpl
@@ -78,4 +78,26 @@
_ " use the <code>http://user:pass@proxy_url/</code> syntax.",
strict_transport_security => sts_desc,
+
+ disable_bug_updates =>
+ "When enabled, all updates to $terms.bugs will be blocked.",
+
+ sentry_uri =>
+ "When set, important errors and warnings will be sent to the"
+ _ " specified Sentry server. Enter the full API KEY URL."
+ _ " eg <kbd>https://01234567890123456780123456780123:01234567890123456780123456780123@errormill.mozilla.org/10</kbd>.",
+
+ metrics_enabled =>
+ "Collect metrics for reporting to ElasticSearch",
+ metrics_user_ids =>
+ "Comma separated list of user_id's which trigger data collection and reporting."
+ _ " eg <kbd>3881,5038,5898,13647,20209,251051,373476,409787</kbd>.",
+ metrics_elasticsearch_server =>
+ "Metrics ElasticSearch server and port. eg <kbd>127.0.0.1:9200</kbd>",
+ metrics_elasticsearch_index =>
+ "Metrics ElasticSearch index. eg <kbd>bmo-metrics</kbd>",
+ metrics_elasticsearch_type =>
+ "Metrics ElasticSearch type. eg <kbd>timings</kbd>",
+ metrics_elasticsearch_ttl =>
+ "The time to live for data in the ElasticSearch cluster, in milliseconds. eg <kbd>1210000000</kbd>",
} %]
diff --git a/template/en/default/admin/params/attachment.html.tmpl b/template/en/default/admin/params/attachment.html.tmpl
index 69f62e9be..4075374bc 100644
--- a/template/en/default/admin/params/attachment.html.tmpl
+++ b/template/en/default/admin/params/attachment.html.tmpl
@@ -63,13 +63,13 @@
maxattachmentsize => "The maximum size (in kilobytes) of attachments to be stored " _
"in the database. If a file larger than this size is attached " _
"to ${terms.abug}, $terms.Bugzilla will look at the " _
- "<a href='#maxlocalattachment'><tt>maxlocalattachment</tt> parameter</a> " _
+ "<a href=\"#maxlocalattachment\"><tt>maxlocalattachment</tt> parameter</a> " _
"to determine if the file can be stored locally on the web server. " _
"If the file size exceeds both limits, then the attachment is rejected. " _
"Settings both parameters to 0 will prevent attaching files to ${terms.bugs}.",
maxlocalattachment => "The maximum size (in megabytes) of attachments to be stored " _
"locally on the web server. If set to a value lower than the " _
- "<a href='#maxattachmentsize'><tt>maxattachmentsize</tt> parameter</a>, " _
+ "<a href=\"#maxattachmentsize\"><tt>maxattachmentsize</tt> parameter</a>, " _
"attachments will never be kept on the local filesystem." }
%]
diff --git a/template/en/default/admin/params/auth.html.tmpl b/template/en/default/admin/params/auth.html.tmpl
index 2e11dffbc..7a8d34791 100644
--- a/template/en/default/admin/params/auth.html.tmpl
+++ b/template/en/default/admin/params/auth.html.tmpl
@@ -107,6 +107,12 @@
"front page will require a login. No anonymous users will " _
"be permitted.",
+ webservice_email_filter => "Filter email addresses returned by the WebService API depending on " _
+ "if the user is logged in or not. This works similarly to how the " _
+ "web UI currently filters email addresses. If <tt>requirelogin</tt> " _
+ "is enabled, then this parameter has no effect as users must be logged " _
+ "in to use Bugzilla.",
+
emailregexp => "This defines the regexp to use for legal email addresses. The " _
"default tries to match fully qualified email addresses. Another " _
"popular value to put here is <tt>^[^@]+$</tt>, which means " _
diff --git a/template/en/default/admin/params/bugfields.html.tmpl b/template/en/default/admin/params/bugfields.html.tmpl
index 58b08f615..a0d9664ad 100644
--- a/template/en/default/admin/params/bugfields.html.tmpl
+++ b/template/en/default/admin/params/bugfields.html.tmpl
@@ -57,5 +57,9 @@
"entry form.<br> " _
"You can leave this empty: " _
"$terms.Bugzilla will then use the operating system that the browser " _
- "reports to be running on as the default." }
+ "reports to be running on as the default.",
+
+ collapsed_comment_tags => "A comma separated list of tags which, when applied " _
+ "to comments, will cause them to be collapsed by default",
+ }
%]
diff --git a/template/en/default/admin/params/groupsecurity.html.tmpl b/template/en/default/admin/params/groupsecurity.html.tmpl
index 783099a11..041af6833 100644
--- a/template/en/default/admin/params/groupsecurity.html.tmpl
+++ b/template/en/default/admin/params/groupsecurity.html.tmpl
@@ -42,6 +42,9 @@
querysharegroup => "The name of the group of users who can share their " _
"saved searches with others.",
+ comment_taggers_group => "The name of the group of users who can tag comment." _
+ " Setting this to empty disables comment tagging.",
+
debug_group => "The name of the group of users who can view the actual " _
"SQL query generated when viewing $terms.bug lists and reports.",
diff --git a/template/en/default/admin/params/memcached.html.tmpl b/template/en/default/admin/params/memcached.html.tmpl
new file mode 100644
index 000000000..eef39860a
--- /dev/null
+++ b/template/en/default/admin/params/memcached.html.tmpl
@@ -0,0 +1,22 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+[%
+ title = "Memcached"
+ desc = "Set up Memcached integration"
+%]
+
+[% param_descs = {
+ memcached_servers =>
+ "If this option is set, $terms.Bugzilla will integrate with Memcached. " _
+ "Specify one of more server, separated by spaces, using hostname:port " _
+ "notation (for example: 127.0.0.1:11211).",
+
+ memcached_namespace =>
+ "Specify a string to prefix to each key on Memcached.",
+ }
+%]
diff --git a/template/en/default/admin/users/edit.html.tmpl b/template/en/default/admin/users/edit.html.tmpl
index 3efa4b8bf..8eced20f7 100644
--- a/template/en/default/admin/users/edit.html.tmpl
+++ b/template/en/default/admin/users/edit.html.tmpl
@@ -107,6 +107,17 @@
[% END %]
</td>
</tr>
+
+ <tr>
+ <th>Last Login:</th>
+ <td>
+ [% IF otheruser.last_seen_date %]
+ [% otheruser.last_seen_date FILTER html %]
+ [% ELSE %]
+ <em>never</em>
+ [% END %]
+ </td>
+ </tr>
</table>
<p>
@@ -116,9 +127,15 @@
<input type="hidden" name="token" value="[% token FILTER html %]">
[% INCLUDE listselectionhiddenfields %]
- or <a href="editusers.cgi?action=activity&amp;userid=[% otheruser.id %]"
- title="View Account History for '
- [%- otheruser.login FILTER html %]'">View Account History</a>
+ [% IF editusers %], [% ELSE %] or [% END %]
+ <a href="editusers.cgi?action=activity&amp;userid=[% otheruser.id %]"
+ title="View Account History for '
+ [%- otheruser.login FILTER html %]'">View Account History</a>
+ [% IF editusers %]
+ or <a href="editusers.cgi?action=admin_activity&amp;userid=[% otheruser.id %]"
+ title="View Account History for '
+ [%- otheruser.login FILTER html %]'">View Admin History</a>
+ [% END %]
</p>
</form>
<p>
diff --git a/template/en/default/admin/users/list.html.tmpl b/template/en/default/admin/users/list.html.tmpl
index 3f745a458..3ebfc2970 100644
--- a/template/en/default/admin/users/list.html.tmpl
+++ b/template/en/default/admin/users/list.html.tmpl
@@ -42,6 +42,9 @@
{name => 'realname'
heading => 'Real name'
}
+ {name => 'last_seen_date'
+ heading => 'Last Login'
+ }
{heading => 'Account History'
content => 'View'
contentlink => 'editusers.cgi?action=activity' _
@@ -51,6 +54,17 @@
]
%]
+[% IF editusers %]
+ [% columns.push({
+ heading => 'Admin History'
+ content => 'View'
+ contentlink => 'editusers.cgi?action=admin_activity' _
+ '&amp;userid=%%userid%%' _
+ listselectionurlparams
+ })
+ %]
+[% END %]
+
[% IF Param('allowuserdeletion') && editusers %]
[% columns.push({heading => 'Action'
content => 'Delete'
diff --git a/template/en/default/attachment/create.html.tmpl b/template/en/default/attachment/create.html.tmpl
index 863d83ad0..45c61d5a1 100644
--- a/template/en/default/attachment/create.html.tmpl
+++ b/template/en/default/attachment/create.html.tmpl
@@ -105,13 +105,11 @@ TUI_hide_default('attachment_text_field');
<th><label for="comment">Comment:</label></th>
<td>
<em>(optional) Add a comment about this attachment to the [% terms.bug %].</em><br>
- [% INCLUDE global/textarea.html.tmpl
- name = 'comment'
- id = 'comment'
- minrows = 6
- maxrows = 15
- cols = constants.COMMENT_COLS
- wrap = 'soft'
+ [% INCLUDE bug/comment.html.tmpl
+ minrows = 6
+ maxrows = 15
+ cols = constants.COMMENT_COLS
+ wrap = 'soft'
%]
</td>
</tr>
diff --git a/template/en/default/attachment/createformcontents.html.tmpl b/template/en/default/attachment/createformcontents.html.tmpl
index 5b04382b6..7f738c07f 100644
--- a/template/en/default/attachment/createformcontents.html.tmpl
+++ b/template/en/default/attachment/createformcontents.html.tmpl
@@ -54,6 +54,7 @@
<th>Content Type:</th>
<td>
<em>If the attachment is a patch, check the box below.</em><br>
+ [% Hook.process("patch_notes") %]
<input type="checkbox" id="ispatch" name="ispatch" value="1"
onchange="setContentTypeDisabledState(this.form);">
<label for="ispatch">patch</label><br><br>
@@ -99,6 +100,7 @@
{type => "image/gif", desc => "GIF image"},
{type => "image/jpeg", desc => "JPEG image"},
{type => "image/png", desc => "PNG image"},
+ {type => "application/pdf", desc => "PDF document"},
{type => "application/octet-stream", desc => "binary file"}]
%]
[% Hook.process("mimetypes", "attachment/createformcontents.html.tmpl") %]
diff --git a/template/en/default/attachment/delete_reason.txt.tmpl b/template/en/default/attachment/delete_reason.txt.tmpl
index e4a1fc41f..87175c1a3 100644
--- a/template/en/default/attachment/delete_reason.txt.tmpl
+++ b/template/en/default/attachment/delete_reason.txt.tmpl
@@ -16,17 +16,10 @@
[%# INTERFACE:
# attachment: object of the attachment the user wants to delete.
# reason: string; The reason provided by the user.
- # date: the date when the request to delete the attachment was made.
#%]
-The content of attachment [% attachment.id %] has been deleted by
- [%+ user.identity %]
-[% IF reason %]
-who provided the following reason:
+The content of attachment [% attachment.id %] has been deleted
+[%~ IF reason %] for the following reason:
[%+ reason %]
-[% ELSE %]
-without providing any reason.
[% END %]
-
-The token used to delete this attachment was generated at [% date FILTER time %].
diff --git a/template/en/default/attachment/diff-footer.html.tmpl b/template/en/default/attachment/diff-footer.html.tmpl
index 49c662a98..e9965a9a8 100644
--- a/template/en/default/attachment/diff-footer.html.tmpl
+++ b/template/en/default/attachment/diff-footer.html.tmpl
@@ -20,6 +20,12 @@
</form>
+[% IF !file_count %]
+<div id="error_msg" class="throw_error">
+ No valid patch files were found in the attachment.
+</div>
+[% END %]
+
[% IF headers %]
<br>
diff --git a/template/en/default/attachment/diff-header.html.tmpl b/template/en/default/attachment/diff-header.html.tmpl
index c13b2e7ba..b407b4f3a 100644
--- a/template/en/default/attachment/diff-header.html.tmpl
+++ b/template/en/default/attachment/diff-header.html.tmpl
@@ -52,7 +52,7 @@ Interdiff of #[% oldid %] and #[% newid %] for [% terms.bug %] #[% bugid %]
[% bugsummary FILTER html %]
[% END %]
[% PROCESS global/header.html.tmpl doc_section = "attachments.html#patchviewer"
- javascript_urls = "js/attachment.js"
+ javascript_urls = [ "js/attachment.js" ]
style_urls = ['skins/standard/attachment.css'] %]
[% ELSE %]
<html>
@@ -133,15 +133,18 @@ Interdiff of #[% oldid %] and #[% newid %] for [% terms.bug %] #[% bugid %]
[% END %]
[% IF warning %]
-<h2 class="warning">Warning:
+<h2 class="warning">
+ Warning:
[% IF warning == "interdiff1" %]
- this difference between two patches may show things in the wrong places due
- to a limitation in [% terms.Bugzilla %] when comparing patches with different
- sets of files.
- [% END %]
- [% IF warning == "interdiff2" %]
- this difference between two patches may be inaccurate due to a limitation in
- [%+ terms.Bugzilla %] when comparing patches made against different revisions.
+ this difference between two patches may show things in the wrong places due
+ to a limitation in [% terms.Bugzilla %] when comparing patches with
+ different sets of files.
+ [% ELSIF warning == "interdiff2" %]
+ this difference between two patches may be inaccurate due to a limitation
+ in [%+ terms.Bugzilla %] when comparing patches made against different
+ revisions.
+ [% ELSIF warning == "interdiff3" %]
+ interdiff encountered errors while comparing these patches.
[% END %]
</h2>
[% ELSE %]
diff --git a/template/en/default/attachment/edit.html.tmpl b/template/en/default/attachment/edit.html.tmpl
index 95ad4d335..e29c45c94 100644
--- a/template/en/default/attachment/edit.html.tmpl
+++ b/template/en/default/attachment/edit.html.tmpl
@@ -25,6 +25,7 @@
[%# Define strings that will serve as the title and header of this page %]
[% title = BLOCK %]
Attachment [% attachment.id %] Details for [% terms.Bug %] [%+ attachment.bug_id %]
+ &ndash; [% attachment.description FILTER html %]
[% END %]
[% header = BLOCK %]
Attachment [% attachment.id %] Details for
@@ -208,7 +209,8 @@
readonly = 'readonly'
%]
[% ELSE %]
- <iframe id="viewFrame" src="attachment.cgi?id=[% attachment.id %]">
+ <iframe id="viewFrame" src="attachment.cgi?id=[% attachment.id %]
+ [%- "&amp;content_type=text/plain" IF attachment.contenttype.match('^text/x-') %]">
<b>You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
<a href="attachment.cgi?id=[% attachment.id %]">View the attachment on a separate page</a>.</b>
</iframe>
@@ -258,22 +260,21 @@
<label for="comment">Comment (on the [% terms.bug %]):</label>
[% classNames = 'block' %]
[% classNames = "$classes bz_private" IF attachment.isprivate %]
- [% INCLUDE global/textarea.html.tmpl
- id = 'comment'
- name = 'comment'
- minrows = 10
- cols = 80
- wrap = 'soft'
- classes = classNames
+ [% INCLUDE bug/comment.html.tmpl
+ minrows = 10
+ cols = 80
+ wrap = 'soft'
+ classes = classNames
%]
+ [% Hook.process('after_comment_textarea') %]
</div>
- [% END %]
+ [% END %]
<div id="attachment_flags">
[% IF attachment.flag_types.size > 0 %]
[% PROCESS "flag/list.html.tmpl" flag_types = attachment.flag_types
read_only_flags = !can_edit
%]
-
+
[% END %]
</div>
@@ -306,10 +307,17 @@
<div id="attachment_list">
Attachments on [% "$terms.bug ${attachment.bug_id}" FILTER bug_link(attachment.bug_id) FILTER none %]:
[% FOREACH a = attachments %]
- [% IF a == attachment.id %]
- [%+ a %]
+ [% IF a.isobsolete %]
+ <span class="bz_obsolete">
+ [% END %]
+ [% IF a.id == attachment.id %]
+ [%+ a.id FILTER html %]
[% ELSE %]
- <a href="attachment.cgi?id=[% a %]&amp;action=edit">[% a %]</a>
+ <a href="attachment.cgi?id=[% a.id FILTER uri %]&amp;action=edit"
+ title="[% a.description FILTER html %]">[% a.id FILTER html %]</a>
+ [% END %]
+ [% IF a.isobsolete %]
+ </span>
[% END %]
[% " |" UNLESS loop.last() %]
[% END %]
diff --git a/template/en/default/attachment/list.html.tmpl b/template/en/default/attachment/list.html.tmpl
index fa8e4774e..c89c7bb78 100644
--- a/template/en/default/attachment/list.html.tmpl
+++ b/template/en/default/attachment/list.html.tmpl
@@ -64,6 +64,7 @@ function toggle_display(link) {
[% count = 0 %]
[% obsolete_attachments = 0 %]
+ [% user_cache = template_cache.users %]
[% FOREACH attachment = attachments %]
[% count = count + 1 %]
@@ -100,9 +101,16 @@ function toggle_display(link) {
<br>
<a href="#attach_[% attachment.id %]"
title="Go to the comment associated with the attachment">
- [%- attachment.attached FILTER time %]</a>,
+ [%- attachment.attached FILTER time("%Y-%m-%d %H:%M %Z") %]</a>,
- [% INCLUDE global/user.html.tmpl who = attachment.attacher %]
+ [%# No need to recreate the exact same template if we already have it. %]
+ [% attacher_id = attachment.attacher.id %]
+ [% UNLESS user_cache.$attacher_id %]
+ [% user_cache.$attacher_id = BLOCK %]
+ [% INCLUDE global/user.html.tmpl who = attachment.attacher %]
+ [% END %]
+ [% END %]
+ [% user_cache.$attacher_id FILTER none %]
</span>
</td>
@@ -134,7 +142,7 @@ function toggle_display(link) {
</td>
[% END %]
- <td valign="top">
+ <td class="bz_attach_actions" valign="top">
<a href="attachment.cgi?id=[% attachment.id %]&amp;action=edit">Details</a>
[% IF attachment.ispatch && feature_enabled('patch_viewer') %]
| <a href="attachment.cgi?id=[% attachment.id %]&amp;action=diff">Diff</a>
diff --git a/template/en/default/bug/activity/table.html.tmpl b/template/en/default/bug/activity/table.html.tmpl
index a9aca0a64..8098d89b2 100644
--- a/template/en/default/bug/activity/table.html.tmpl
+++ b/template/en/default/bug/activity/table.html.tmpl
@@ -110,7 +110,7 @@
change.fieldname == 'flagtypes.name' %]
[% display_value(change.fieldname, change_type) FILTER email FILTER html %]
[% ELSE %]
- [% display_value(change.fieldname, change_type) FILTER html %]
+ [% display_value(change.fieldname, change_type) FILTER html FILTER html_line_break %]
[% END %]
[% ELSE %]
&nbsp;
diff --git a/template/en/default/bug/comment.html.tmpl b/template/en/default/bug/comment.html.tmpl
new file mode 100644
index 000000000..96cbb63ed
--- /dev/null
+++ b/template/en/default/bug/comment.html.tmpl
@@ -0,0 +1,37 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%# INTERFACE:
+ #
+ # This template supports the same parameters as global/textarea.html.tmpl
+ # with the exception of "name" and "id", which will always be "comment".
+ #%]
+
+[% IF feature_enabled('jsonrpc') %]
+ <div id="comment_tabs" role="tablist">
+ <div id="comment_tab" class="comment_tab active_comment_tab"
+ role="tab" aria-selected="true"
+ onclick="show_comment_edit()">Comment</div>
+ <div id="comment_preview_tab" class="comment_tab"
+ role="tab" aria-selected="false"
+ onclick="show_comment_preview([% bug.id FILTER none %])">Preview</div>
+ </div>
+[% END %]
+
+[% INCLUDE global/textarea.html.tmpl
+ name = "comment"
+ id = "comment"
+%]
+
+[% IF feature_enabled('jsonrpc') %]
+ <div id="comment_preview" class="bz_default_hidden bz_comment">
+ <div id="comment_preview_loading" class="bz_default_hidden">Generating Preview...</div>
+ <div id="comment_preview_error" class="bz_default_hidden"></div>
+ <pre id="comment_preview_text" class="bz_comment_text"></pre>
+ </div>
+[% END %]
diff --git a/template/en/default/bug/comments.html.tmpl b/template/en/default/bug/comments.html.tmpl
index e3099d94a..8effb89da 100644
--- a/template/en/default/bug/comments.html.tmpl
+++ b/template/en/default/bug/comments.html.tmpl
@@ -25,8 +25,65 @@
<script src="[% 'js/comments.js' FILTER mtime %]" type="text/javascript">
</script>
+<script type="text/javascript">
+<!--
+ /* Adds the reply text to the 'comment' textarea */
+ function replyToComment(id, real_id, name) {
+ var prefix = "(In reply to " + name + " from comment #" + id + ")\n";
+ var replytext = "";
+ [% IF user.settings.quote_replies.value == 'quoted_reply' %]
+ /* pre id="comment_name_N" */
+ var text_elem = document.getElementById('comment_text_'+id);
+ var text = getText(text_elem);
+ replytext = prefix + wrapReplyText(text);
+ [% ELSIF user.settings.quote_replies.value == 'simple_reply' %]
+ replytext = prefix;
+ [% END %]
+
+ [% IF user.is_insider %]
+ if (document.getElementById('isprivate_' + real_id).checked) {
+ document.getElementById('newcommentprivacy').checked = 'checked';
+ updateCommentTagControl(document.getElementById('newcommentprivacy'), 'comment');
+ }
+ [% END %]
+
+ /* Remove embedded links to attachment details */
+ replytext = replytext.replace(/(attachment\s+\d+)(\s+\[[^\[\n]+\])+/gi, '$1');
+
+ /* <textarea id="comment"> */
+ var textarea = document.getElementById('comment');
+ if (textarea.value != replytext) {
+ textarea.value += replytext;
+ }
+
+ textarea.focus();
+ }
+
+ function toggleCommentWrap(a, id) {
+ var spans = document.getElementById('comment_text_' + id).getElementsByTagName('span');
+ var old_class;
+ var new_class;
+ if (a.innerHTML == 'wrap') {
+ a.innerHTML = 'unwrap';
+ old_class = 'quote';
+ new_class = 'quote_wrapped';
+ } else {
+ a.innerHTML = 'wrap';
+ old_class = 'quote_wrapped';
+ new_class = 'quote';
+ }
+ for (var i = 0, l = spans.length; i < l; i++) {
+ if (spans[i].className == old_class)
+ spans[i].className = new_class;
+ }
+ return false;
+ }
+//-->
+</script>
+
[% DEFAULT start_at = 0 mode = "show" %]
[% sort_order = user.settings.comment_sort_order.value %]
+[% user_cache = template_cache.users %]
[%# NOTE: (start_at > 0) means we came here from a midair collision,
# in which case we don't care what the user's preference is.
@@ -35,23 +92,36 @@
[% sort_order = "oldest_to_newest" %]
[% END %]
+
+[%# Set up the variables as needed, depending on the sort order %]
+[% IF sort_order == "oldest_to_newest" %]
+ [% count = 0 %]
+ [% description = 0 %]
+ [% increment = 1 %]
+[% ELSE %]
+ [% increment = -1 %]
+ [% IF sort_order == "newest_to_oldest" %]
+ [% count = comments.size - 1 %]
+ [% description = 0 %]
+ [% ELSIF sort_order == "newest_to_oldest_desc_first" %]
+ [% count = comments.size %]
+ [% description = comments.size %]
+ [% END %]
+[% END %]
+
+[% Hook.process("comment_banner") %]
+
<!-- This auto-sizes the comments and positions the collapse/expand links
to the right. -->
<table class="bz_comment_table" cellpadding="0" cellspacing="0"><tr>
<td>
[% FOREACH comment = comments %]
- [% IF comment.count >= start_at %]
+ [% IF count >= start_at %]
[% PROCESS a_comment %]
[% END %]
-[% END %]
-
-[% IF user.settings.comment_box_position.value == "before_comments" && user.id %]
- <div class="bz_add_comment">
- <a href="#"
- onclick="return goto_add_comments();">
- Add Comment</a>
- </div>
+
+ [% count = count + increment %]
[% END %]
[%# Note: this template is used in multiple places; if you use this hook,
@@ -63,15 +133,13 @@
<td>
[% IF mode == "edit" %]
<ul class="bz_collapse_expand_comments">
- <li><a href="#" onclick="toggle_all_comments('collapse');
+ <li><a href="#" onclick="toggle_all_comments('collapse');
return false;">Collapse All Comments</a></li>
<li><a href="#" onclick="toggle_all_comments('expand');
return false;">Expand All Comments</a></li>
- [% IF user.settings.comment_box_position.value == "after_comments" && user.id %]
- <li class="bz_add_comment"><a href="#"
- onclick="return goto_add_comments('bug_status_bottom');">
- Add Comment</a></li>
- [% END %]
+ [% IF Param('comment_taggers_group') %]
+ <li><div id="comment_tags_collapse_expand_container"></div></li>
+ [% END %]
</ul>
[% END %]
</td>
@@ -82,13 +150,14 @@
[%############################################################################%]
[% BLOCK a_comment %]
- [% RETURN IF comment.is_private AND ! user.is_insider %]
+ [% RETURN IF comment.is_private AND NOT (user.is_insider || user.id == comment.author.id) %]
[% comment_text = comment.body_full %]
[% RETURN IF comment_text == '' AND (comment.work_time - 0) != 0 AND !user.is_timetracker %]
<div id="c[% comment.count %]" class="bz_comment[% " bz_private" IF comment.is_private %]
+ [% " bz_default_collapsed" IF comment.collapsed %]
[% " bz_comment_hilite" IF marks.${comment.count} %]
- [% " bz_first_comment" IF comment.count == 0 %]">
+ [% " bz_first_comment" IF comment.count == description %]">
[% IF comment.count == 0 %]
[% class_name = "bz_first_comment_head" %]
[% comment_label = "Description" %]
@@ -101,14 +170,40 @@
[% IF mode == "edit" %]
<span class="bz_comment_actions">
- <script type="text/javascript"><!--
- addReplyLink([% comment.count %], [% comment.id %]);
- addCollapseLink([% comment.count %], 'Toggle comment display'); // -->
+ [% IF comment.collapsed %]
+ <span class="bz_collapsed_actions">
+ [% END %]
+ [% IF comment_text.search("(?:^>|\n>)") %]
+ [<a class="bz_wrap_link" href="#"
+ onclick="return toggleCommentWrap(this, [% comment.count %])">wrap</a>]
+ [% END %]
+ [% IF bug.check_can_change_field('longdesc', 1, 0) %]
+ [% IF user.can_tag_comments %]
+ [<a href="#"
+ onclick="YAHOO.bugzilla.commentTagging.toggle([% comment.id %], [% comment.count %]);return false">tag</a>]
+ [% END %]
+ [<a class="bz_reply_link" href="#add_comment"
+ [% IF user.settings.quote_replies.value != 'off' %]
+ onclick="replyToComment('[% comment.count %]', '[% comment.id %]', '[% comment.author.name || comment.author.nick FILTER html FILTER js %]'); return false;"
+ [% END %]
+ >reply</a>]
+ [% END %]
+ [% IF comment.collapsed %]
+ </span>
+ [% END %]
+ <script type="text/javascript">
+ addCollapseLink([% comment.count %], [% comment.collapsed FILTER js %], 'Toggle comment display');
+ </script>
+ </span>
+ [% ELSIF comment.collapsed %]
+ <span class="bz_comment_actions">
+ <script type="text/javascript">
+ addCollapseLink([% comment.count %], [% comment.collapsed FILTER js %], 'Toggle comment display');
</script>
</span>
[% END %]
- [% IF mode == "edit" && user.is_insider %]
+ [% IF mode == "edit" && user.is_insider && bug.check_can_change_field('longdesc', 0, 1) %]
<div class="bz_private_checkbox">
<input type="hidden" value="1"
name="defined_isprivate_[% comment.id %]">
@@ -128,12 +223,21 @@
</span>
<span class="bz_comment_user">
- [% INCLUDE global/user.html.tmpl who = comment.author %]
- </span>
+ [% who = comment.author %]
+ [% Hook.process('user-image', 'bug/comments.html.tmpl') %]
+ [%# No need to recreate the exact same template if we already have it. %]
+ [% commenter_id = comment.author.id %]
+ [% UNLESS user_cache.$commenter_id %]
+ [% user_cache.$commenter_id = BLOCK %]
+ [% INCLUDE global/user.html.tmpl who = comment.author %]
+ [% END %]
+ [% END %]
+ [% user_cache.$commenter_id FILTER none %]
+ [% Hook.process('user', 'bug/comments.html.tmpl') %]
+ </span>
<span class="bz_comment_user_images">
- [% FOREACH group = comment.author.direct_group_membership %]
- [% NEXT UNLESS group.icon_url %]
+ [% FOREACH group = comment.author.groups_with_icon %]
<img src="[% group.icon_url FILTER html %]"
alt="[% group.name FILTER html %]"
title="[% group.name FILTER html %] - [% group.description FILTER html %]">
@@ -143,6 +247,14 @@
<span class="bz_comment_time">
[%+ comment.creation_ts FILTER time %]
</span>
+
+ [% IF comment.collapsed %]
+ <span id="cr[% comment.count %]" class="bz_comment_collapse_reason"
+ title="[% comment.author.name || comment.author.login FILTER html %]
+ [%~ %] [[% comment.creation_ts FILTER time %]]">
+ Comment hidden ([% comment.tags.join(', ') FILTER html %])
+ </span>
+ [% END %]
</div>
[% IF user.is_timetracker &&
@@ -152,12 +264,34 @@
[% PROCESS formattimeunit time_unit=comment.work_time %]
[% END %]
+ [% IF user.id && Param('comment_taggers_group') %]
+ <div id="comment_tag_[% comment.count FILTER html %]"
+ class="bz_comment_tags
+ [% " bz_default_hidden" UNLESS comment.tags.size %]">
+ <span id="ct_[% comment.count %]">
+ [% IF comment.tags.size %]
+ <script>
+ YAHOO.bugzilla.commentTagging.showTags([% comment.id FILTER none %],
+ [% comment.count FILTER none %], [
+ [% FOREACH tag = comment.tags %]
+ [%~%]'[% tag FILTER js %]'[% "," UNLESS loop.last %]
+ [% END %]
+ [%~%]]);
+ </script>
+ [% END %]
+ </span>
+ </div>
+ [% END %]
+
[%# Don't indent the <pre> block, since then the spaces are displayed in the
# generated HTML
#%]
-<pre class="bz_comment_text"
- [% ' id="comment_text_' _ comment.count _ '"' IF mode == "edit" %]>
+<pre class="bz_comment_text[% " collapsed" IF comment.collapsed %]"
+ [% IF mode == "edit" || comment.collapsed %]
+ id="comment_text_[% comment.count FILTER none %]"
+ [% END %]>
[%- comment_text FILTER quoteUrls(bug, comment) -%]
</pre>
+ [% Hook.process('a_comment-end', 'bug/comments.html.tmpl') %]
</div>
[% END %]
diff --git a/template/en/default/bug/create/comment-guided.txt.tmpl b/template/en/default/bug/create/comment-guided.txt.tmpl
index df04d8fb5..67748e594 100644
--- a/template/en/default/bug/create/comment-guided.txt.tmpl
+++ b/template/en/default/bug/create/comment-guided.txt.tmpl
@@ -41,7 +41,7 @@ Steps to Reproduce:
[%+ cgi.param("reproduce_steps") %]
[% END %]
-[% IF cgi.param("actual_results") -%]
+[% IF cgi.param("actual_results") %]
Actual Results:
[%+ cgi.param("actual_results") %]
[% END %]
diff --git a/template/en/default/bug/create/create-guided.html.tmpl b/template/en/default/bug/create/create-guided.html.tmpl
index d10314628..43437bcd7 100644
--- a/template/en/default/bug/create/create-guided.html.tmpl
+++ b/template/en/default/bug/create/create-guided.html.tmpl
@@ -31,22 +31,12 @@
[% PROCESS global/header.html.tmpl
title = "Enter $terms.ABug"
onload = "PutDescription()"
- style = "#somebugs { width: 100%; height: 500px }"
+ style_urls = [ "skins/standard/guided.css" ]
%]
[% style = "" %]
-<p>
- <font color="red">
- This is a template used on mozilla.org. This template, and the
- comment-guided.txt.tmpl template that formats the data submitted via
- the form in this template, are included as a demo of what it's
- possible to do with custom templates in general, and custom [% terms.bug %]
- entry templates in particular. As much of the text will not apply,
- you should alter it
- if you want to use this form on your [% terms.Bugzilla %] installation.
- </font>
-</p>
+[% INCLUDE 'bug/create/user-message.html.tmpl' %]
[% tablecolour = "#FFFFCC" %]
@@ -80,15 +70,15 @@ function PutDescription() {
[%# Include other products if sensible %]
[% IF product.name == "Firefox" %]
- [% productstring = "product=Mozilla%20Application%20Suite&amp;product=Firefox" %]
+ [% productstring = "product=Toolkit&amp;product=Core&amp;product=Firefox" %]
[% ELSIF product.name == "Thunderbird" %]
- [% productstring = "product=Mozilla%20Application%20Suite&amp;product=Thunderbird" %]
+ [% productstring = "product=MailNews%20Core&amp;product=Thunderbird" %]
[% ELSE %]
[% productstring = BLOCK %]product=[% product.name FILTER uri %][% END %]
[% END %]
<p>
- <a href="duplicates.cgi?[% productstring %]&amp;format=simple" target="somebugs">All-time Top 100</a> (loaded initially) |
+ <a href="duplicates.cgi?[% productstring %]&amp;format=simple" target="somebugs">All-time Top 20</a> (loaded initially) |
<a href="duplicates.cgi?[% productstring %]&amp;format=simple&amp;sortby=delta&amp;reverse=1&amp;maxrows=100&amp;changedsince=14" target="somebugs">Hot in the last two weeks</a>
</p>
@@ -112,14 +102,14 @@ function PutDescription() {
<input type="hidden" name="product" value="[% product.name FILTER html %]">
[% IF product.name == "Firefox" OR
product.name == "Thunderbird" OR
- product.name == "Mozilla Application Suite" OR
+ product.name == "SeaMonkey" OR
product.name == "Camino" %]
<input type="hidden" name="product" value="Core">
<input type="hidden" name="product" value="Toolkit">
- <input type="hidden" name="product" value="PSM">
<input type="hidden" name="product" value="NSPR">
<input type="hidden" name="product" value="NSS">
- [% END %]
+ <input type="hidden" name="product" value="MailNews Core">
+ [% END %]
<input type="hidden" name="chfieldfrom" value="-6m">
<input type="hidden" name="chfieldto" value="Now">
<input type="hidden" name="chfield" value="[Bug creation]">
@@ -215,7 +205,7 @@ function PutDescription() {
[%# We override rep_platform and op_sys for simplicity. The values chosen
are based on which are most common in the b.m.o database %]
- [% rep_platform = [ "PC", "Macintosh", "All", "Other" ] %]
+ [% rep_platform = [ "x86", "x86_64", "PowerPC", "All", "Other" ] %]
<tr bgcolor="[% tablecolour %]">
<td align="right" valign="top">
@@ -238,7 +228,7 @@ function PutDescription() {
</td>
</tr>
- [% IF product.name.match("Firefox|Camino|Mozilla Application Suite") %]
+ [% IF product.name.match("Firefox|Camino|SeaMonkey") %]
[% matches = cgi.user_agent('Gecko/(\d+)') %]
[% buildid = cgi.user_agent() IF matches %]
[% END %]
@@ -257,8 +247,8 @@ function PutDescription() {
<p>
This should identify the exact version of the product you were using.
If the above field is blank or you know it is incorrect, copy the
- version text from the product's Help |
- About menu (for browsers this will begin with "Mozilla/5.0...").
+ user agent text from the product's Help | Troubleshooting Information menu
+ (for browsers this will begin with "Mozilla/5.0...").
If the product won't start, instead paste the complete URL you downloaded
it from.
</p>
@@ -275,7 +265,7 @@ function PutDescription() {
URL that demonstrates the problem you are seeing (optional).<br>
<b>IMPORTANT</b>: if the problem is with a broken web page, you need
to report it
- <a href="https://bugzilla.mozilla.org/page.cgi?id=broken-website.html">a different way</a>.
+ <a href="http://input.mozilla.com/feedback">a different way</a>.
</p>
</td>
</tr>
@@ -418,10 +408,7 @@ function PutDescription() {
%]
<p>
Add any additional information you feel may be
- relevant to this [% terms.bug %], such as the <b>theme</b> you were
- using (does the [% terms.bug %] still occur
- with the default theme?), a
- <b><a href="http://kb.mozillazine.org/Quality_Feedback_Agent">Talkback crash ID</a></b>, or special
+ relevant to this [% terms.bug %], such as special
information about <b>your computer's configuration</b>. Any information
longer than a few lines, such as a <b>stack trace</b> or <b>HTML
testcase</b>, should be added
@@ -431,13 +418,12 @@ function PutDescription() {
into your URL bar.
<br>
<br>
- If you are reporting a crash, note the module in
- which the software crashed (e.g., <tt>Application Violation in
- gkhtml.dll</tt>).
+ If you are reporting a crash, please <a href="https://developer.mozilla.org/En/How_to_get_a_stacktrace_for_a_bug_report
+">try and get a stack trace</a>, which tells us exactly where things went wrong.
</p>
</td>
</tr>
-
+
<tr>
<td valign="top" align="right">
<b>Severity</b>
diff --git a/template/en/default/bug/create/create.html.tmpl b/template/en/default/bug/create/create.html.tmpl
index 634bcf326..fa344b1ca 100644
--- a/template/en/default/bug/create/create.html.tmpl
+++ b/template/en/default/bug/create/create.html.tmpl
@@ -32,16 +32,37 @@
title = title
yui = [ 'autocomplete', 'calendar', 'datatable', 'button' ]
style_urls = [ 'skins/standard/attachment.css',
- 'skins/standard/enter_bug.css' ]
+ 'skins/standard/enter_bug.css',
+ 'skins/custom/create_bug.css' ]
javascript_urls = [ "js/attachment.js", "js/util.js",
- "js/field.js", "js/TUI.js", "js/bug.js" ]
- onload = "set_assign_to(); hideElementById('attachment_true');
- showElementById('attachment_false'); showElementById('btn_no_attachment');"
+ "js/field.js", "js/TUI.js", "js/bug.js",
+ "js/create_bug.js" ]
+ onload = "init();"
%]
<script type="text/javascript">
<!--
+function init() {
+ set_assign_to();
+ hideElementById('attachment_true');
+ showElementById('attachment_false');
+ showElementById('btn_no_attachment');
+ initCrashSignatureField();
+ init_take_handler('[% user.login FILTER js %]');
+}
+
+function initCrashSignatureField() {
+ var el = document.getElementById('cf_crash_signature');
+ if (!el) return;
+ [% IF cf_crash_signature.length %]
+ YAHOO.util.Dom.addClass('cf_crash_signature_container', 'bz_default_hidden');
+ [% ELSE %]
+ hideEditableField('cf_crash_signature_container','cf_crash_signature_input',
+ 'cf_crash_signature_action', 'cf_crash_signature');
+ [% END %]
+}
+
var initialowners = new Array([% product.components.size %]);
var last_initialowner;
var initialccs = new Array([% product.components.size %]);
@@ -60,11 +81,9 @@ var flags = new Array([% product.components.size %]);
initialowners[[% count %]] = "[% c.default_assignee.login FILTER js %]";
[% flag_list = [] %]
[% FOREACH f = c.flag_types.bug %]
- [% NEXT UNLESS f.is_active %]
[% flag_list.push(f.id) %]
[% END %]
[% FOREACH f = c.flag_types.attachment %]
- [% NEXT UNLESS f.is_active %]
[% flag_list.push(f.id) %]
[% END %]
flags[[% count %]] = [[% flag_list.join(",") FILTER js %]];
@@ -112,6 +131,14 @@ function set_assign_to() {
document.getElementById('initial_cc').innerHTML = initialccs[index];
document.getElementById('comp_desc').innerHTML = comp_desc[index];
+ if (initialccs[index]) {
+ showElementById('initial_cc_label');
+ showElementById('initial_cc');
+ } else {
+ hideElementById('initial_cc_label');
+ hideElementById('initial_cc');
+ }
+
[% IF Param("useqacontact") %]
var contact = initialqacontacts[index];
if (qa_contact == last_initialqacontact
@@ -122,30 +149,31 @@ function set_assign_to() {
}
[% END %]
- // First, we disable all flags. Then we re-enable those
- // which are available for the selected component.
- var inputElements = document.getElementsByTagName("select");
- var inputElement, flagField;
- for ( var i=0 ; i<inputElements.length ; i++ ) {
- inputElement = inputElements.item(i);
- if (inputElement.name.search(/^flag_type-(\d+)$/) != -1) {
- var id = inputElement.name.replace(/^flag_type-(\d+)$/, "$1");
- inputElement.disabled = true;
- // Also hide the requestee field, if it exists.
- inputElement = document.getElementById("requestee_type-" + id);
- if (inputElement)
- YAHOO.util.Dom.addClass(inputElement.parentNode, 'bz_default_hidden');
+ // We show or hide the available flags depending on the selected component.
+ var flag_rows = YAHOO.util.Dom.getElementsByClassName('bz_flag_type', 'tbody');
+ for (var i = 0; i < flag_rows.length; i++) {
+ // Each flag table row should have one flag form select element
+ // We get the flag type id from the id attribute of the select.
+ var flag_select = YAHOO.util.Dom.getElementsByClassName('flag_select',
+ 'select',
+ flag_rows[i])[0];
+ var type_id = flag_select.id.split('-')[1];
+ var can_set = flag_select.options.length > 1 ? 1 : 0;
+ var show = 0;
+ // Loop through the allowed flag ids for the selected component
+ // and if we match, then show the row, otherwise hide the row.
+ for (var j = 0; j < flags[index].length; j++) {
+ if (flags[index][j] == type_id) {
+ show = 1;
+ break;
+ }
}
- }
- // Now enable flags available for the selected component.
- for (var i = 0; i < flags[index].length; i++) {
- flagField = document.getElementById("flag_type-" + flags[index][i]);
- // Do not enable flags the user cannot set nor request.
- if (flagField && flagField.options.length > 1) {
- flagField.disabled = false;
- // Re-enabling the requestee field depends on the status
- // of the flag.
- toggleRequesteeField(flagField, 1);
+ if (show && can_set) {
+ flag_select.disabled = false;
+ YAHOO.util.Dom.removeClass(flag_rows[i], 'bz_default_hidden');
+ } else {
+ flag_select.disabled = true;
+ YAHOO.util.Dom.addClass(flag_rows[i], 'bz_default_hidden');
}
}
}
@@ -171,6 +199,7 @@ TUI_hide_default('attachment_text_field');
onsubmit="return validateEnterBug(this)">
<input type="hidden" name="product" value="[% product.name FILTER html %]">
<input type="hidden" name="token" value="[% token FILTER html %]">
+<input type="hidden" name="bug_ignored" value="[% bug_ignored ? "1" : "0" %]">
<table>
<tbody>
@@ -185,9 +214,8 @@ TUI_hide_default('attachment_text_field');
<tr>
<td colspan="2">
- <a id="expert_fields_controller" class="controller bz_default_hidden"
- href="javascript:TUI_toggle_class('expert_fields')">Hide
- Advanced Fields</a>
+ <input type="button" id="expert_fields_controller"
+ value="Hide Advanced Fields" onClick="toggleAdvancedFields()">
[%# Show the link if the browser supports JS %]
<script type="text/javascript">
YAHOO.util.Dom.removeClass('expert_fields_controller',
@@ -349,120 +377,77 @@ TUI_hide_default('attachment_text_field');
bug = default, field = bug_fields.bug_status,
editable = (bug_status.size > 1), value = default.bug_status
override_legal_values = bug_status %]
-
- <td>&nbsp;</td>
- [%# Calculate the number of rows we can use for flags %]
- [% num_rows = 6 + (Param("useqacontact") ? 1 : 0) +
- (user.is_timetracker ? 3 : 0) +
- (Param("usebugaliases") ? 1 : 0)
- %]
-
- <td rowspan="[% num_rows FILTER html %]">
- [% IF product.flag_types.bug.size > 0 %]
- [% display_flag_headers = 0 %]
- [% any_flags_requesteeble = 0 %]
-
- [% FOREACH flag_type = product.flag_types.bug %]
- [% NEXT UNLESS flag_type.is_active %]
- [% display_flag_headers = 1 %]
- [% SET any_flags_requesteeble = 1 IF flag_type.is_requestable && flag_type.is_requesteeble %]
- [% END %]
-
- [% IF display_flag_headers %]
- [% PROCESS "flag/list.html.tmpl" flag_types = product.flag_types.bug
- any_flags_requesteeble = any_flags_requesteeble
- flag_table_id = "bug_flags"
- %]
- [% END %]
- [% END %]
- </td>
</tr>
<tr>
[% INCLUDE "bug/field-label.html.tmpl"
field = bug_fields.assigned_to editable = 1
%]
- <td colspan="2">
+ <td>
[% INCLUDE global/userselect.html.tmpl
- id => "assigned_to"
- name => "assigned_to"
- value => assigned_to
+ id => "assigned_to"
+ name => "assigned_to"
+ value => assigned_to
disabled => assigned_to_disabled
- size => 30
- emptyok => 1
+ size => 30
+ emptyok => 1
custom_userlist => assignees_list
- %]
+ %]
+ [% UNLESS assigned_to_disabled %]
+ <span id="take_bug">
+ &nbsp;(<a title="Assign to yourself" href="#"
+ onclick="return take_bug('[% user.login FILTER js %]')">take</a>)
+ </span>
+ [% END %]
<noscript>(Leave blank to assign to component's default assignee)</noscript>
</td>
- </tr>
[% IF Param("useqacontact") %]
- <tr>
- [% INCLUDE "bug/field-label.html.tmpl"
- field = bug_fields.qa_contact editable = 1
- %]
- <td colspan="2">
- [% INCLUDE global/userselect.html.tmpl
- id => "qa_contact"
- name => "qa_contact"
- value => qa_contact
- disabled => qa_contact_disabled
- size => 30
- emptyok => 1
- custom_userlist => qa_contacts_list
- %]
- <noscript>(Leave blank to assign to default qa contact)</noscript>
- </td>
- </tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.qa_contact editable = 1
+ %]
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "qa_contact"
+ name => "qa_contact"
+ value => qa_contact
+ disabled => qa_contact_disabled
+ size => 30
+ emptyok => 1
+ custom_userlist => qa_contacts_list
+ %]
+ <noscript>(Leave blank to assign to default qa contact)</noscript>
+ </td>
+ </tr>
[% END %]
<tr>
[% INCLUDE "bug/field-label.html.tmpl"
field = bug_fields.cc editable = 1
%]
- <td colspan="2">
+ <td>
[% INCLUDE global/userselect.html.tmpl
- id => "cc"
- name => "cc"
- value => cc
+ id => "cc"
+ name => "cc"
+ value => cc
disabled => cc_disabled
- size => 30
+ size => 30
multiple => 5
%]
+ </td>
+ <th>
+ <span id="initial_cc_label" class="bz_default_hidden">
+ Default [% field_descs.cc FILTER html %]:
+ </span>
+ </th>
+ <td>
+ <span id="initial_cc"></span>
</td>
</tr>
<tr>
- <th>Default [% field_descs.cc FILTER html %]:</th>
- <td colspan="2">
- <div id="initial_cc">
- </div>
- </td>
- </tr>
-
- <tr>
- <td colspan="3">&nbsp;</td>
- </tr>
-
-[% IF user.is_timetracker %]
- <tr>
- [% INCLUDE "bug/field-label.html.tmpl"
- field = bug_fields.estimated_time editable = 1
- %]
- <td colspan="2">
- <input name="estimated_time" size="6" maxlength="6" value="[% estimated_time FILTER html %]">
- </td>
- </tr>
- <tr>
- [% INCLUDE bug/field.html.tmpl
- bug = default, field = bug_fields.deadline, value = deadline,
- editable = 1, value_span = 2 %]
- </tr>
-
- <tr>
<td colspan="3">&nbsp;</td>
</tr>
-[% END %]
[% IF Param("usebugaliases") %]
<tr>
@@ -474,34 +459,9 @@ TUI_hide_default('attachment_text_field');
</td>
</tr>
[% END %]
-
- <tr>
- [% INCLUDE "bug/field-label.html.tmpl"
- field = bug_fields.bug_file_loc editable = 1
- %]
- <td colspan="2" class="field_value">
- <input name="bug_file_loc" id="bug_file_loc" class="text_input"
- size="40" value="[% bug_file_loc FILTER html %]">
- </td>
- </tr>
-</tbody>
-
-<tbody>
- [% USE Bugzilla %]
-
- [% FOREACH field = Bugzilla.active_custom_fields %]
- [% NEXT UNLESS field.enter_bug %]
- [% SET value = ${field.name}.defined ? ${field.name} : "" %]
- <tr [% 'class="expert_fields"' IF !field.is_mandatory %]>
- [% INCLUDE bug/field.html.tmpl
- bug = default, field = field, value = value, editable = 1,
- value_span = 3 %]
- </tr>
- [% END %]
</tbody>
<tbody>
-
<tr>
[% INCLUDE "bug/field-label.html.tmpl"
field = bug_fields.short_desc editable = 1
@@ -513,7 +473,8 @@ TUI_hide_default('attachment_text_field');
</td>
</tr>
- [% IF feature_enabled('jsonrpc') AND !cloned_bug_id %]
+ [% IF feature_enabled('jsonrpc') AND !cloned_bug_id
+ AND user.settings.possible_duplicates.value == 'on' %]
<tr id="possible_duplicates_container" class="bz_default_hidden">
<th>Possible<br>Duplicates:</th>
<td colspan="3">
@@ -562,9 +523,7 @@ TUI_hide_default('attachment_text_field');
# by global/textarea.html.tmpl. So we must not escape the comment here. %]
[% comment FILTER none %]
[%- END %]
- [% INCLUDE global/textarea.html.tmpl
- name = 'comment'
- id = 'comment'
+ [% INCLUDE bug/comment.html.tmpl
minrows = 10
maxrows = 25
cols = constants.COMMENT_COLS
@@ -574,21 +533,17 @@ TUI_hide_default('attachment_text_field');
</td>
</tr>
- [% IF user.is_insider %]
- <tr class="expert_fields">
- <th>&nbsp;</th>
- <td colspan="3">
- &nbsp;&nbsp;
- <input type="checkbox" id="comment_is_private" name="comment_is_private"
- [% ' checked="checked"' IF comment_is_private %]
- onClick="updateCommentTagControl(this, 'comment')">
- <label for="comment_is_private">
- Make description and any new attachment private (visible only to members
- of the <strong>[% Param('insidergroup') FILTER html %]</strong> group)
- </label>
- </td>
- </tr>
- [% END %]
+<tbody class="expert_fields">
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.bug_file_loc editable = 1
+ %]
+ <td colspan="3" class="field_value">
+ <input name="bug_file_loc" id="bug_file_loc" class="text_input"
+ size="40" value="[% bug_file_loc FILTER html %]">
+ </td>
+ </tr>
+</tbody>
[% IF Param("maxattachmentsize") || Param("maxlocalattachment") %]
<tr>
@@ -609,6 +564,16 @@ TUI_hide_default('attachment_text_field');
any_flags_requesteeble = 1
flag_table_id ="attachment_flags" %]
</table>
+
+ [% IF user.is_insider %]
+ <input type="checkbox" id="comment_is_private" name="comment_is_private"
+ [% ' checked="checked"' IF comment_is_private %]
+ onClick="updateCommentTagControl(this, 'comment')">
+ <label for="comment_is_private">
+ Make this attachment and [% terms.bug %] description private (visible only
+ to members of the <strong>[% Param('insidergroup') FILTER html %]</strong> group)
+ </label>
+ [% END %]
</fieldset>
</div>
</td>
@@ -618,41 +583,139 @@ TUI_hide_default('attachment_text_field');
<tbody class="expert_fields">
[% IF user.in_group('editbugs', product.id) %]
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.dependson editable = 1
+ %]
+ <td>
+ <input name="dependson" accesskey="d" value="[% dependson FILTER html %]" size="30">
+ </td>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.blocked editable = 1
+ %]
+ <td>
+ <input name="blocked" accesskey="b" value="[% blocked FILTER html %]" size="30">
+ </td>
+ </tr>
+
[% IF use_keywords %]
<tr>
[% INCLUDE bug/field.html.tmpl
bug = default, field = bug_fields.keywords, editable = 1,
value = keywords, desc_url = "describekeywords.cgi",
- value_span = 2
+ value_span = 3
%]
</tr>
[% END %]
<tr>
- [% INCLUDE "bug/field-label.html.tmpl"
- field = bug_fields.dependson editable = 1
- %]
- <td colspan="3">
- <input name="dependson" accesskey="d" value="[% dependson FILTER html %]">
+ <th>Status Whiteboard:</th>
+ <td colspan="3" class="field_value">
+ <input id="status_whiteboard" name="status_whiteboard" size="70"
+ value="[% status_whiteboard FILTER html %]" class="text_input">
</td>
</tr>
+ [% END %]
+
+ [% IF user.is_timetracker %]
<tr>
[% INCLUDE "bug/field-label.html.tmpl"
- field = bug_fields.blocked editable = 1
+ field = bug_fields.estimated_time editable = 1
%]
- <td colspan="3">
- <input name="blocked" accesskey="b" value="[% blocked FILTER html %]">
+ <td>
+ <input name="estimated_time" size="6" maxlength="6" value="[% estimated_time FILTER html %]">
</td>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.deadline, value = deadline, editable = 1
+ %]
</tr>
[% END %]
+
+ [% IF Param('use_see_also') %]
+ <tr>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default
+ field = bug_fields.see_also
+ editable = 1
+ value = see_also
+ value_span = 3
+ %]
+ </tr>
+ [% END %]
+</tbody>
+
+<tbody>
+[%# non-tracking flags custom fields %]
+[% FOREACH field = Bugzilla.active_custom_fields(product=>product) %]
+ [% NEXT IF field.type == constants.FIELD_TYPE_EXTENSION %]
+ [% NEXT UNLESS field.enter_bug %]
+ [% Hook.process('custom_field', 'bug/create/create.html.tmpl') %]
+ [% NEXT IF field.hidden %]
+ [% SET value = ${field.name}.defined ? ${field.name} : "" %]
+ <tr [% 'class="expert_fields"' IF !field.is_mandatory %]>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = field, value = value, editable = 1,
+ value_span = 3 %]
+ </tr>
+[% END %]
+[% Hook.process('after_custom_fields') %]
</tbody>
+[% display_flags = 0 %]
+[% any_flags_requesteeble = 0 %]
+[% FOREACH flag_type = product.flag_types.bug %]
+ [% display_flags = 1 %]
+ [% SET any_flags_requesteeble = 1 IF flag_type.is_requestable && flag_type.is_requesteeble %]
+ [% LAST IF display_flags && any_flags_requesteeable %]
+[% END %]
+
+[% IF tracking_flags.size || display_flags %]
+ <tbody class="expert_fields">
+ <tr>
+ <th>Flags:</th>
+ <td colspan="3">
+ <div id="bug_flags_false" class="bz_default_hidden">
+ <input type="button" value="Set [% terms.bug FILTER html %] flags" onClick="handleWantsBugFlags(true)">
+ </div>
+
+ <div id="bug_flags_true">
+ <input type="button" id="btn_no_bug_flags" value="Don't set [% terms.bug %] flags"
+ class="bz_default_hidden" onClick="handleWantsBugFlags(false)">
+
+ <fieldset>
+ <legend>Set [% terms.bug %] flags</legend>
+ <table>
+ <tr>
+ [% Hook.process('bug_flags') %]
+ [% IF display_flags %]
+ <td>
+ [% PROCESS "flag/list.html.tmpl" flag_types = product.flag_types.bug
+ any_flags_requesteeble = any_flags_requesteeble
+ flag_table_id = "bug_flags"
+ %]
+ </td>
+ [% END %]
+ </tr>
+ </table>
+ </fieldset>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+[% END %]
+
<tbody class="expert_fields">
- [% IF product.groups_available.size %]
+ [%# BMO - exclude the default security from from the groups_available %]
+ [%# list, as it will be added by the BMO extension %]
+ [% groups_available = [] %]
+ [% FOREACH group = product.groups_available %]
+ [% NEXT IF group.name == product.default_security_group %]
+ [% groups_available.push(group) %]
+ [% END %]
+ [% IF groups_available.size %]
<tr>
<th>&nbsp;</th>
<td colspan="3">
- <br>
<strong>
Only users in all of the selected groups can view this
[%+ terms.bug %]:
@@ -662,11 +725,10 @@ TUI_hide_default('attachment_text_field');
(Leave all boxes unchecked to make this a public [% terms.bug %].)
</font>
<br>
- <br>
<!-- Checkboxes -->
<input type="hidden" name="defined_groups" value="1">
- [% FOREACH group = product.groups_available %]
+ [% FOREACH group = groups_available %]
<input type="checkbox" id="group_[% group.id FILTER html %]"
name="groups" value="[% group.name FILTER html %]"
[% ' checked="checked"' IF default.groups.contains(group.name)
@@ -694,6 +756,13 @@ TUI_hide_default('attachment_text_field');
</td>
</tr>
</tbody>
+ [%# "status whiteboard" and "qa contact" are the longest labels
+ # add them here to avoid shifting the page when toggling advanced fields %]
+ <tr>
+ <th class="hidden_text">Status Whiteboard:</th>
+ <td>&nbsp;</td>
+ <th class="hidden_text">QA Contact:</th>
+ </tr>
</table>
<input type="hidden" name="form_name" value="enter_bug">
</form>
@@ -701,6 +770,14 @@ TUI_hide_default('attachment_text_field');
[%# Links or content with more information about the bug being created. %]
[% Hook.process("end") %]
+<div id="guided">
+ <a id="guided_img" href="enter_bug.cgi?format=guided&amp;product=[% product.name FILTER uri %]"><img
+ src="extensions/BMO/web/images/guided.png" width="16" height="16" border="0" align="absmiddle"></a>
+ <a id="guided_link" href="enter_bug.cgi?format=guided&amp;product=[% product.name FILTER uri %]"
+ >Switch to the [% terms.Bugzilla %] Helper</a>
+ | <a href="page.cgi?id=custom_forms.html">Custom [% terms.bug %] entry forms</a>
+</div>
+
[% PROCESS global/footer.html.tmpl %]
[%############################################################################%]
diff --git a/template/en/default/bug/edit.html.tmpl b/template/en/default/bug/edit.html.tmpl
index fbc6e4a96..4f7b8add4 100644
--- a/template/en/default/bug/edit.html.tmpl
+++ b/template/en/default/bug/edit.html.tmpl
@@ -30,73 +30,42 @@
[% PROCESS bug/time.html.tmpl %]
+[% IF Param('comment_taggers_group') %]
+ [% IF user.can_tag_comments %]
+ <div id="bz_ctag_div" class="bz_default_hidden">
+ <a href="javascript:void(0)" onclick="YAHOO.bugzilla.commentTagging.hideInput()">x</a>
+ <div>
+ <input id="bz_ctag_add" size="10" placeholder="add tag"
+ maxlength="[% constants.MAX_COMMENT_TAG_LENGTH FILTER html %]">
+ <span id="bz_ctag_autocomp"></span>
+ </div>
+ [<a href="https://wiki.mozilla.org/BMO/comment_tagging"
+ target="_blank" title="About Comment Tagging">help</a>]
+ &nbsp;
+ </div>
+ <div id="bz_ctag_error" class="bz_default_hidden">
+ <a href="javascript:void(0)" onclick="YAHOO.bugzilla.commentTagging.hideError()">x</a>
+ <span id="bz_ctag_error_msg"></span>
+ </div>
+ [% END %]
+ [% IF user.id %]
+ <script type="text/javascript">
+ YAHOO.bugzilla.commentTagging.init([% user.can_tag_comments ? 'true' : 'false' %]);
+ YAHOO.bugzilla.commentTagging.min_len = [% constants.MIN_COMMENT_TAG_LENGTH FILTER js %];
+ YAHOO.bugzilla.commentTagging.max_len = [% constants.MAX_COMMENT_TAG_LENGTH FILTER js %];
+ YAHOO.bugzilla.commentTagging.label = 'Comment Tags:';
+ YAHOO.bugzilla.commentTagging.min_len_error =
+ 'Comment tags must be at least
+ [%~ " " _ constants.MIN_COMMENT_TAG_LENGTH FILTER js %] characters.';
+ YAHOO.bugzilla.commentTagging.max_len_error =
+ 'Comment tags cannot be longer than
+ [%~ " " _ constants.MAX_COMMENT_TAG_LENGTH FILTER js %] characters.';
+ </script>
+ [% END %]
+[% END %]
+
<script type="text/javascript">
<!--
- /* Outputs a link to call replyToComment(); used to reduce HTML output */
- function addReplyLink(id, real_id) {
- /* XXX this should really be updated to use the DOM Core's
- * createElement, but finding a container isn't trivial.
- */
- [% IF user.settings.quote_replies.value != 'off' %]
- document.write('[<a href="#add_comment" onclick="replyToComment(' +
- id + ',' + real_id + '); return false;">reply<' + '/a>]');
- [% END %]
- }
-
- /* Adds the reply text to the `comment' textarea */
- function replyToComment(id, real_id) {
- var prefix = "(In reply to comment #" + id + ")\n";
- var replytext = "";
- [% IF user.settings.quote_replies.value == 'quoted_reply' %]
- /* pre id="comment_name_N" */
- var text_elem = document.getElementById('comment_text_'+id);
- var text = getText(text_elem);
- replytext = prefix + wrapReplyText(text);
- [% ELSIF user.settings.quote_replies.value == 'simple_reply' %]
- replytext = prefix;
- [% END %]
-
- [% IF user.is_insider %]
- if (document.getElementById('isprivate_' + real_id).checked) {
- document.getElementById('newcommentprivacy').checked = 'checked';
- updateCommentTagControl(document.getElementById('newcommentprivacy'), 'comment');
- }
- [% END %]
-
- /* <textarea id="comment"> */
- var textarea = document.getElementById('comment');
- textarea.value += replytext;
-
- textarea.focus();
- }
-
- if (typeof Node == 'undefined') {
- /* MSIE doesn't define Node, so provide a compatibility object */
- window.Node = {
- TEXT_NODE: 3,
- ENTITY_REFERENCE_NODE: 5
- };
- }
-
- /* Concatenates all text from element's childNodes. This is used
- * instead of innerHTML because we want the actual text (and
- * innerText is non-standard).
- */
- function getText(element) {
- var child, text = "";
- for (var i=0; i < element.childNodes.length; i++) {
- child = element.childNodes[i];
- var type = child.nodeType;
- if (type == Node.TEXT_NODE || type == Node.ENTITY_REFERENCE_NODE) {
- text += child.nodeValue;
- } else {
- /* recurse into nodes of other types */
- text += getText(child);
- }
- }
- return text;
- }
-
[% IF user.is_timetracker %]
var fRemainingTime = [% bug.remaining_time %]; // holds the original value
function adjustRemainingTime() {
@@ -115,7 +84,6 @@
// if the remaining time is changed manually, update fRemainingTime
fRemainingTime = document.changeform.remaining_time.value;
}
-
[% END %]
[% IF user.id %]
@@ -164,34 +132,48 @@
[% PROCESS section_url_keyword_whiteboard %]
[% PROCESS section_spacer %]
-
- [%# *** Dependencies *** %]
+
+ [%# *** Dependencies and duplicates *** %]
+ [% PROCESS section_duplicates %]
+
[% PROCESS section_dependson_blocks %]
-
+
+ [% IF user.id %]
+ <tr>
+ <td colspan="2">
+ <span style="float:left">
+ <a href="page.cgi?id=fields.html">What do these fields mean?</a>
+ </span>
+ [% PROCESS commit_button id="_top"%]
+ </td>
+ </tr>
+ [% END %]
</table>
</td>
<td>
<div class="bz_column_spacer">&nbsp;</div>
</td>
[%# 2nd Column %]
- <td id="bz_show_bug_column_2" class="bz_show_bug_column">
+ <td id="bz_show_bug_column_2" class="bz_show_bug_column_table" valign="top">
<table cellpadding="3" cellspacing="1">
[%# *** Reported and modified dates *** %]
[% PROCESS section_dates %]
-
+
[% PROCESS section_cclist %]
-
+
+ [% PROCESS section_bug_ignored %]
+
[% PROCESS section_spacer %]
- [% PROCESS section_see_also %]
-
- [% PROCESS section_customfields %]
-
+ [% PROCESS section_flags %]
+
+ [% PROCESS section_see_also %]
+
[% PROCESS section_spacer %]
-
+
+ [% PROCESS section_customfields %]
+
[% Hook.process("after_custom_fields") %]
-
- [% PROCESS section_flags %]
</table>
</td>
@@ -220,9 +202,13 @@
[% IF user.settings.comment_box_position.value == 'before_comments' %]
[% PROCESS comment_box %]
+ [% ELSIF user.id %]
+ [% PROCESS summon_comment_box %]
[% END %]
</td>
<td>
+ [%# BMO hook for adding custom group visibility %]
+ [% Hook.process('before_restrict_visibility', 'bug/edit.html.tmpl') %]
[% PROCESS section_restrict_visibility %]
</td>
</tr></table>
@@ -238,7 +224,10 @@
[% IF user.settings.comment_box_position.value == 'after_comments' %]
<hr>
[% PROCESS comment_box %]
- [% END %]
+ [% ELSE %]
+ [% PROCESS summon_comment_box %]
+ [% END %]
+
</form>
@@ -249,7 +238,10 @@
[% BLOCK section_title %]
[%# That's the main table, which contains all editable fields. %]
<div class="bz_alias_short_desc_container edit_form">
- [% PROCESS commit_button id="_top"%]
+ <span class="last_comment_link">
+ <a href="#c[% bug.comments.size - 1 %]"
+ accesskey="l"><b>L</b>ast Comment</a>
+ </span>
<a href="show_bug.cgi?id=[% bug.bug_id %]">
[%-# %]<b>[% terms.Bug %]&nbsp;[% bug.bug_id FILTER html %]</b>
[%-# %]</a> -<span id="summary_alias_container" class="bz_default_hidden">
@@ -351,9 +343,9 @@
%]
</tr>
<tr>
- <td class="field_label">
- <label for="version"><b>Version</b></label>:
- </td>
+ <th class="field_label">
+ <label for="version">Version</label>:
+ </th>
[% PROCESS select selname => "version" %]
</tr>
@@ -361,9 +353,9 @@
[%# PLATFORM #%]
[%############%]
<tr>
- <td class="field_label">
- <label for="rep_platform" accesskey="h"><b>Platform</b></label>:
- </td>
+ <th class="field_label">
+ <label for="rep_platform" accesskey="h">Platform</label>:
+ </th>
<td class="field_value">
[% INCLUDE bug/field.html.tmpl
bug = bug, field = bug_fields.rep_platform,
@@ -373,9 +365,6 @@
bug = bug, field = bug_fields.op_sys,
no_tds = 1, value = bug.op_sys
editable = bug.check_can_change_field('op_sys', 0, 1) %]
- <script type="text/javascript">
- assignToDefaultOnChange(['product', 'component']);
- </script>
</td>
</tr>
@@ -389,9 +378,9 @@
[% BLOCK section_status %]
<tr>
- <td class="field_label">
- <b><a href="page.cgi?id=fields.html#status">Status</a></b>:
- </td>
+ <th class="field_label">
+ <a href="page.cgi?id=fields.html#status">Status</a>:
+ </th>
<td id="bz_field_status">
<span id="static_bug_status">
[% display_value("bug_status", bug.bug_status) FILTER html %]
@@ -408,6 +397,33 @@
</span>
</td>
</tr>
+ [% IF Param('usestatuswhiteboard') %]
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.status_whiteboard
+ editable = 1
+ accesskey = "w"
+ desc_url = "https://wiki.mozilla.org/BMO/Whiteboard"
+ %]
+ [% PROCESS input inputname => "status_whiteboard" size => "40" colspan => 2 %]
+ </tr>
+ [% END %]
+
+ [% IF use_keywords %]
+ <tr>
+ <th class="field_label">
+ <label for="keywords" accesskey="k">
+ <a href="describekeywords.cgi"><u>K</u>eywords</a></label>:
+ </th>
+ <td class="field_value" colspan="2">
+ [% INCLUDE bug/field.html.tmpl
+ bug = bug, field = bug_fields.keywords, value = bug.keywords
+ editable = bug.check_can_change_field("keywords", 0, 1),
+ no_tds = 1
+ %]
+ </td>
+ </tr>
+ [% END %]
[% END %]
[%############################################################################%]
@@ -420,10 +436,10 @@
[%# Importance (priority and severity) #%]
[%###############################################################%]
<tr>
- <td class="field_label">
+ <th class="field_label">
<label for="priority" accesskey="i">
- <b><a href="page.cgi?id=fields.html#importance"><u>I</u>mportance</a></b></label>:
- </td>
+ <a href="page.cgi?id=fields.html#importance"><u>I</u>mportance</a></label>:
+ </th>
<td>
[% INCLUDE bug/field.html.tmpl
bug = bug, field = bug_fields.priority,
@@ -439,11 +455,11 @@
[% IF Param("usetargetmilestone") && bug.target_milestone %]
<tr>
- <td class="field_label">
+ <th class="field_label">
<label for="target_milestone">
- <a href="page.cgi?id=fields.html#target_milestone">
+ <a href="page.cgi?id=fields.html#target_milestone">
Target&nbsp;Milestone</a></label>:
- </td>
+ </th>
[% PROCESS select selname = "target_milestone" %]
</tr>
[% END %]
@@ -457,9 +473,9 @@
[% BLOCK section_people %]
<tr>
- <td class="field_label">
- <b><a href="page.cgi?id=fields.html#assigned_to">Assigned To</a></b>:
- </td>
+ <th class="field_label">
+ <a href="page.cgi?id=fields.html#assigned_to">Assigned To</a>:
+ </th>
<td>
[% IF bug.check_can_change_field("assigned_to", 0, 1) %]
<div id="bz_assignee_edit_container" class="bz_default_hidden">
@@ -506,41 +522,46 @@
[% IF Param('useqacontact') %]
<tr>
- <td class="field_label">
- <label for="qa_contact" accesskey="q"><b><u>Q</u>A Contact</b></label>:
- </td>
+ <th class="field_label">
+ <label for="qa_contact" accesskey="q"><u>Q</u>A Contact</label>:
+ </th>
<td>
[% IF bug.check_can_change_field("qa_contact", 0, 1) %]
- [% IF bug.qa_contact != "" %]
- <div id="bz_qa_contact_edit_container" class="bz_default_hidden">
+ <div id="bz_qa_contact_edit_container" class="bz_default_hidden">
<span>
- <span id="bz_qa_contact_edit_display">
- [% INCLUDE global/user.html.tmpl who = bug.qa_contact %]</span>
+ [% INCLUDE global/user.html.tmpl who = bug.qa_contact %]
(<a href="#" id="bz_qa_contact_edit_action">edit</a>)
+ [% IF bug.qa_contact.id != user.id %]
+ (<a title="Change QA contact to yourself"
+ href="#" id="bz_qa_contact_take_action">take</a>)
+ [% END %]
</span>
</div>
- [% END %]
<div id="bz_qa_contact_input">
[% INCLUDE global/userselect.html.tmpl
- id => "qa_contact"
- name => "qa_contact"
- value => bug.qa_contact.login
- size => 30
- classes => ["bz_userfield"]
- emptyok => 1
+ id => "qa_contact"
+ name => "qa_contact"
+ value => bug.qa_contact.login
+ size => 30
+ classes => ["bz_userfield"]
+ emptyok => 1
%]
<br>
<input type="checkbox" id="set_default_qa_contact" name="set_default_qa_contact" value="1">
<label for="set_default_qa_contact" id="set_default_qa_contact_label">Reset QA Contact to default</label>
</div>
<script type="text/javascript">
- [% IF bug.qa_contact != "" %]
- hideEditableField('bz_qa_contact_edit_container',
- 'bz_qa_contact_input',
- 'bz_qa_contact_edit_action',
- 'qa_contact',
- '[% bug.qa_contact.login FILTER js %]');
- [% END %]
+ hideEditableField('bz_qa_contact_edit_container',
+ 'bz_qa_contact_input',
+ 'bz_qa_contact_edit_action',
+ 'qa_contact',
+ '[% bug.qa_contact.login FILTER js %]');
+ hideEditableField('bz_qa_contact_edit_container',
+ 'bz_qa_contact_input',
+ 'bz_qa_contact_take_action',
+ 'qa_contact',
+ '[% bug.qa_contact.login FILTER js %]',
+ '[% user.login FILTER js %]');
initDefaultCheckbox('qa_contact');
</script>
[% ELSE %]
@@ -549,6 +570,15 @@
</td>
</tr>
[% END %]
+
+ [%# BMO - hook for adding mentors %]
+ [% Hook.process("after_people", "bug/edit.html.tmpl") %]
+
+ <script type="text/javascript">
+ assignToDefaultOnChange(['product', 'component'],
+ '[% bug.component_obj.default_assignee.login FILTER js %]',
+ '[% bug.component_obj.default_qa_contact.login FILTER js %]');
+ </script>
[% END %]
[%############################################################################%]
@@ -564,14 +594,17 @@
<td>
[% IF bug.check_can_change_field("bug_file_loc", 0, 1) %]
<span id="bz_url_edit_container" class="bz_default_hidden">
- [% IF is_safe_url(bug.bug_file_loc) %]
- <a href="[% bug.bug_file_loc FILTER html %]" target="_blank"
- title="[% bug.bug_file_loc FILTER html %]">
- [% bug.bug_file_loc FILTER truncate(40) FILTER html %]</a>
- [% ELSE %]
- [% bug.bug_file_loc FILTER html %]
- [% END %]
- (<a href="#" id="bz_url_edit_action">edit</a>)</span>
+ <a href="[% bug.bug_file_loc FILTER html %]" target="_blank"
+ title="[% bug.bug_file_loc FILTER html %]"
+ [% IF NOT is_safe_url(bug.bug_file_loc) %]
+ onclick="return confirm(
+ 'This is considered an unsafe URL and could possibly be harmful. '
+ + 'The full URL is:\n\n[% bug.bug_file_loc FILTER js FILTER html %]\n\n'
+ + 'Continue?')"
+ [% END %]>
+ [% bug.bug_file_loc FILTER truncate(40) FILTER html %]</a>
+ (<a href="#" id="bz_url_edit_action">edit</a>)
+ </span>
[% END %]
<span id="bz_url_input_area">
[% url_output = PROCESS input no_td=1 inputname => "bug_file_loc" size => "40" colspan => 2 %]
@@ -593,36 +626,34 @@
[% END %]
</td>
</tr>
-
- [% IF Param('usestatuswhiteboard') %]
- <tr>
- <td class="field_label">
- <label for="status_whiteboard" accesskey="w"><b><u>W</u>hiteboard</b></label>:
- </td>
- [% PROCESS input inputname => "status_whiteboard" size => "40" colspan => 2 %]
- </tr>
- [% END %]
-
- [% IF use_keywords %]
- <tr>
- <td class="field_label">
- <label for="keywords" accesskey="k">
- <b><a href="describekeywords.cgi"><u>K</u>eywords</a></b></label>:
- </td>
- <td class="field_value" colspan="2">
- [% INCLUDE bug/field.html.tmpl
- bug = bug, field = bug_fields.keywords, value = bug.keywords
- editable = bug.check_can_change_field("keywords", 0, 1),
- no_tds = 1
- %]
- </td>
- </tr>
- [% END %]
[% END %]
[%############################################################################%]
-[%# Block for Depends On / Blocks #%]
+[%# Block for Duplicates #%]
+[%############################################################################%]
+
+[% BLOCK section_duplicates %]
+ [% RETURN UNLESS bug.duplicates.size %]
+ <tr>
+ <th class="field_label">
+ <label for="duplicates">Duplicates</label>:
+ </th>
+ <td class="field_value" colspan="2">
+ <span id="duplicates">
+ [% FOREACH dupe = bug.duplicates %]
+ [% dupe.id FILTER bug_link(dupe, use_alias => 1) FILTER none %][% " " %]
+ [% END %]
+ </span>
+ (<a href="buglist.cgi?bug_id=[% bug.duplicate_ids.join(",") FILTER html %]">
+ [%-%]view as [% terms.bug %] list</a>)
+ </td>
+ </tr>
+[% END %]
+
+[%############################################################################%]
+[%# Block for Depends On / Blocks #%]
[%############################################################################%]
+
[% BLOCK section_dependson_blocks %]
<tr>
[% INCLUDE dependencies
@@ -749,20 +780,20 @@
[% BLOCK section_dates %]
<tr>
- <td class="field_label">
- <b>Reported</b>:
- </td>
+ <th class="field_label">
+ Reported:
+ </th>
<td>
- [% bug.creation_ts FILTER time %] by [% INCLUDE global/user.html.tmpl who = bug.reporter %]
+ [% bug.creation_ts FILTER time("%Y-%m-%d %H:%M %Z") %] by [% INCLUDE global/user.html.tmpl who = bug.reporter %]
</td>
</tr>
<tr>
- <td class="field_label">
- <b> Modified</b>:
- </td>
+ <th class="field_label">
+ Modified:
+ </th>
<td>
- [% bug.delta_ts FILTER time FILTER replace(':\d\d$', '') FILTER replace(':\d\d ', ' ')%]
+ [% bug.delta_ts FILTER time("%Y-%m-%d %H:%M %Z") %]
(<a href="show_activity.cgi?id=[% bug.bug_id %]">[%# terms.Bug %]History</a>)
</td>
@@ -774,9 +805,9 @@
[%############################################################################%]
[% BLOCK section_cclist %]
<tr>
- <td class="field_label">
- <label for="newcc" accesskey="a"><b>CC List</b>:</label>
- </td>
+ <th class="field_label">
+ <label for="newcc" accesskey="a">CC List:</label>
+ </th>
<td>
[% IF user.id %]
[% IF NOT bug.cc || NOT bug.cc.contains(user.login) %]
@@ -808,10 +839,17 @@
[% IF user.id || bug.cc.size %]
<span id="cc_edit_area_showhide_container" class="bz_default_hidden">
(<a href="#" id="cc_edit_area_showhide">[% IF user.id %]edit[% ELSE %]show[% END %]</a>)
- </span>
+ [% IF user.id && bug.cc.size %]
+ <br>
+ <ul class="cc_list_display">
+ [% FOREACH c = bug.cc %]
+ <li>[% c FILTER email FILTER html %]</li>
+ [% END %]
+ </ul>
+ [% END %]
+ </span>
[% END %]
<div id="cc_edit_area">
- <br>
[% IF user.id %]
<div>
<div><label for="cc"><b>Add</b></label></div>
@@ -864,6 +902,29 @@
[% END %]
[%############################################################################%]
+[%# Block for Bug Ignored #%]
+[%############################################################################%]
+[% BLOCK section_bug_ignored %]
+ [% IF user.id %]
+ <tr>
+ <th class="field_label">
+ <label for="bug_ignored" title="Ignore all email for this [% terms.bug %]">
+ Ignore [% terms.Bug %] Mail:
+ </label>
+ </th>
+ <td>
+ <input type="hidden" name="defined_bug_ignored" value="1">
+ <span title="You will still receive emails for flag requests directed at you.">
+ <input type="checkbox" name="bug_ignored" id="bug_ignored" value="1"
+ [% ' checked="checked"' IF user.is_bug_ignored(bug.id) %]>
+ (never email me about this [% terms.bug %])
+ </span>
+ </td>
+ </tr>
+ [% END %]
+[% END %]
+
+[%############################################################################%]
[%# Block for See Also #%]
[%############################################################################%]
[% BLOCK section_see_also %]
@@ -885,26 +946,52 @@
[% BLOCK section_flags %]
[%# *** Flags *** %]
[% show_bug_flags = 0 %]
+ [% bug_flags_set = 0 %]
+ [% show_more_flags = 0 %]
[% FOREACH type = bug.flag_types %]
[% IF (type.flags && type.flags.size > 0) || (user.id && type.is_active) %]
[% show_bug_flags = 1 %]
- [% LAST %]
[% END %]
+ [% IF user.id && type.is_active && (type.flags.size == 0 || type.is_multiplicable) %]
+ [% show_more_flags = 1 %]
+ [% END %]
+ [% IF type.flags && type.flags.size > 0 %]
+ [% bug_flags_set = 1 %]
+ [% END %]
+ [% LAST IF show_bug_flags && show_more_flags && bug_flags_set %]
[% END %]
[% IF show_bug_flags %]
<tr>
- <td class="field_label flags_label">
- <label><b>Flags:</b></label>
- </td>
- <td></td>
- </tr>
- <tr>
- <td colspan="2">
+ <th class="field_label">
+ <label>Flags:</label>
+ </th>
+ <td>
[% IF bug.flag_types.size > 0 %]
[% PROCESS "flag/list.html.tmpl" flag_no_header = 1
flag_types = bug.flag_types
any_flags_requesteeble = bug.any_flags_requesteeble %]
[% END %]
+ [% IF show_more_flags && bug.check_can_change_field('flagtypes.name', 0, 1) %]
+ <span id="bz_flags_more_container" class="bz_default_hidden">
+ [% IF !bug_flags_set %]<em>None yet set</em>[% END %]
+ (<a href="#" id="bz_flags_more_action">[% IF !bug_flags_set %]set[% ELSE %]more[% END %] flags</a>)
+ </span>
+ <script type="text/javascript">
+ YAHOO.util.Dom.removeClass('bz_flags_more_container', 'bz_default_hidden');
+ var table = YAHOO.util.Dom.get("flags");
+ var rows = YAHOO.util.Dom.getElementsByClassName('bz_flag_type', 'tbody', table);
+ for (var i = 0; i < rows.length; i++) {
+ YAHOO.util.Dom.addClass(rows[i], 'bz_default_hidden');
+ }
+ YAHOO.util.Event.addListener('bz_flags_more_action', 'click', function (e) {
+ YAHOO.util.Dom.addClass('bz_flags_more_container', 'bz_default_hidden');
+ for (var i = 0; i < rows.length; i++) {
+ YAHOO.util.Dom.removeClass(rows[i], 'bz_default_hidden');
+ }
+ YAHOO.util.Event.preventDefault(e);
+ });
+ </script>
+ [% END %]
</td>
</tr>
[% END %]
@@ -917,7 +1004,12 @@
[% BLOCK section_customfields %]
[%# *** Custom Fields *** %]
[% USE Bugzilla %]
- [% FOREACH field = Bugzilla.active_custom_fields %]
+ [% FOREACH field = Bugzilla.active_custom_fields(product=>bug.product_obj,component=>bug.component_obj) %]
+ [% NEXT IF field.type == constants.FIELD_TYPE_EXTENSION %]
+ [% NEXT IF NOT user.id AND field.value == "---" %]
+ [%# BMO hook for controlling field visibility %]
+ [% Hook.process('custom_field', 'bug/edit.html.tmpl') %]
+ [% NEXT IF field.hidden %]
<tr>
[% PROCESS bug/field.html.tmpl value = bug.${field.name}
editable = bug.check_can_change_field(field.name, 0, 1)
@@ -1068,7 +1160,7 @@
<label for="comment" accesskey="c"><b>Additional
<u>C</u>omments</b></label>:
- [% IF user.is_insider %]
+ [% IF user.is_insider && bug.check_can_change_field('longdesc', 0, 1) %]
<input type="checkbox" name="comment_is_private" value="1"
id="newcommentprivacy"
onClick="updateCommentTagControl(this, 'comment')">
@@ -1080,23 +1172,32 @@
<!-- This table keeps the submit button aligned with the box. -->
<table><tr><td>
- [% INCLUDE global/textarea.html.tmpl
- name = 'comment'
- id = 'comment'
- minrows = 10
- maxrows = 25
- cols = constants.COMMENT_COLS
- %]
- [% Hook.process("after_comment_textarea", 'bug/edit.html.tmpl') %]
+ [% IF bug.check_can_change_field('longdesc', 0, 1) %]
+ [% INCLUDE bug/comment.html.tmpl
+ minrows = 10
+ maxrows = 25
+ cols = constants.COMMENT_COLS
+ %]
+ [% Hook.process("after_comment_textarea", 'bug/edit.html.tmpl') %]
+ [% ELSE %]
+ <div id="comment">
+ <fieldset>
+ <legend>Note</legend>
+ You are not allowed to make an additional comment on this [% terms.bug %].
+ </fieldset>
+ </div>
+ [% END %]
<br>
[% PROCESS commit_button id=""%]
+ [% Hook.process("after_comment_commit_button", 'bug/edit.html.tmpl') %]
+
<table id="bug_status_bottom"
class="status" cellspacing="0" cellpadding="0">
<tr>
- <td class="field_label">
- <b><a href="page.cgi?id=fields.html#status">Status</a></b>:
- </td>
+ <th class="field_label">
+ <a href="page.cgi?id=fields.html#status">Status</a>:
+ </th>
<td>
[% PROCESS bug/knob.html.tmpl %]
</td>
@@ -1123,6 +1224,21 @@
</div>
[% END %]
+[% BLOCK summon_comment_box %]
+<div id="comment_top_hat">
+ <script type="text/javascript">
+ function summonCommentBox() {
+ var commentbox = document.getElementById('add_comment');
+ document.getElementById('comment_top_hat').appendChild(commentbox);
+ document.getElementById('wave_wand').style.display = 'none';
+ }
+ </script>
+ <p id="wave_wand">
+ <a href="javascript:summonCommentBox()"><i>Summon comment box</i></a>
+ </p>
+</div>
+[% END %]
+
[%############################################################################%]
[%# Block for SELECT fields #%]
[%############################################################################%]
@@ -1131,6 +1247,7 @@
<td>
[% IF bug.check_can_change_field(selname, 0, 1)
AND bug.choices.${selname}.size > 1 %]
+ <input type="hidden" id="[% selname %]_dirty">
<select id="[% selname %]" name="[% selname %]">
[% FOREACH x = bug.choices.${selname} %]
[% NEXT IF NOT x.is_active AND x.name != bug.${selname} %]
diff --git a/template/en/default/bug/field-help.none.tmpl b/template/en/default/bug/field-help.none.tmpl
index a9449fbcd..9985e4172 100644
--- a/template/en/default/bug/field-help.none.tmpl
+++ b/template/en/default/bug/field-help.none.tmpl
@@ -89,6 +89,10 @@ estimated_time =>
"The amount of time that has been estimated it will take to resolve
this ${terms.bug}.",
+importance =>
+ "The importance of $terms.abug is described as the combination of
+ its $vars.field_descs.priority and ${vars.field_descs.bug_severity}.",
+
keywords =>
"You can add keywords from a defined list to $terms.bugs, in order"
_ " to easily identify and group them.",
diff --git a/template/en/default/bug/field.html.tmpl b/template/en/default/bug/field.html.tmpl
index 58f1b0ccc..e9eefd419 100644
--- a/template/en/default/bug/field.html.tmpl
+++ b/template/en/default/bug/field.html.tmpl
@@ -57,8 +57,9 @@
value="[% value FILTER html %]" size="40"
maxlength="[% constants.MAX_FREETEXT_LENGTH FILTER none %]"
[% ' aria-required="true"' IF field.is_mandatory %]>
- [% CASE constants.FIELD_TYPE_DATETIME %]
- <input name="[% field.name FILTER html %]" size="20"
+ [% CASE [constants.FIELD_TYPE_DATETIME, constants.FIELD_TYPE_DATE] %]
+ [% size = (field.type == constants.FIELD_TYPE_DATE) ? 10 : 20 %]
+ <input name="[% field.name FILTER html %]" size="[% size FILTER none %]"
id="[% field.name FILTER html %]"
value="[% value FILTER html %]"
[% ' aria-required="true"' IF field.is_mandatory %]
@@ -97,6 +98,7 @@
</script>
[% CASE [ constants.FIELD_TYPE_SINGLE_SELECT
constants.FIELD_TYPE_MULTI_SELECT ] %]
+ <input type="hidden" id="[% field.name FILTER html %]_dirty">
<select id="[% field.name FILTER html %]"
name="[% field.name FILTER html %]"
[% IF field.type == constants.FIELD_TYPE_MULTI_SELECT %]
@@ -121,6 +123,30 @@
[% END %]
[% FOREACH legal_value = legal_values %]
[% NEXT IF NOT legal_value.is_active AND NOT value.contains(legal_value.name).size %]
+
+ [%# Purpose: hide field values from those who can't change them %]
+ [% IF field.name.match("^cf_blocking_") OR
+ field.name.match("^cf_status_") OR
+ field.name.match("^cf_tracking_") OR
+ field.name == "resolution" %]
+ [% NEXT UNLESS bug.check_can_change_field(field.name, '---', legal_value.name) OR
+ value.contains(legal_value.name).size %]
+ [% END %]
+
+ [% IF field.name == "resolution" &&
+ legal_value.name != bug.resolution %]
+ [% r = legal_value.name %]
+ [% IF bug.user.canconfirm &&
+ !(bug.user.canedit || bug.user.isreporter) %]
+ [% NEXT IF r != "WORKSFORME" && r != "INCOMPLETE" %]
+ [% END %]
+ [% IF bug.user.isreporter &&
+ !(bug.user.canconfirm || bug.user.canedit) %]
+ [% NEXT IF r == "INCOMPLETE" %]
+ [% END %]
+ [% NEXT IF r == "EXPIRED" %]
+ [% END %]
+
<option value="[% legal_value.name FILTER html %]"
id="v[% legal_value.id FILTER html %]_
[%- field.name FILTER html %]"
@@ -155,36 +181,56 @@
</script>
[% CASE constants.FIELD_TYPE_TEXTAREA %]
- [% INCLUDE global/textarea.html.tmpl
- id = field.name name = field.name minrows = 4 maxrows = 8
- cols = 60 defaultcontent = value mandatory = field.is_mandatory %]
+ <div id="[% field.name FILTER html %]_edit_container" class="bz_default_hidden">
+ <div>
+ (<a href="#" id="[% field.name FILTER html %]_edit_action">edit</a>)
+ </div>
+ [% IF value %]
+ <pre class="field_textarea_readonly">[% value FILTER html %]</pre>
+ [% END %]
+ </div>
+ <div id="[% field.name FILTER html %]_input">
+ [% INCLUDE global/textarea.html.tmpl
+ id = field.name name = field.name minrows = 4 maxrows = 8
+ cols = 60 defaultcontent = value mandatory = field.is_mandatory %]
+ </div>
+ <script type="text/javascript">
+ hideEditableField('[% field.name FILTER js %]_edit_container',
+ '[% field.name FILTER js %]_input',
+ '[% field.name FILTER js %]_edit_action',
+ '[% field.name FILTER js %]',
+ '[% value FILTER js %]',
+ '',
+ true);
+ </script>
[% CASE constants.FIELD_TYPE_BUG_URLS %]
- [% '<ul class="bug_urls">' IF value.size %]
- [% FOREACH bug_url = value %]
- <li>
- [% PROCESS bug_url_link bug_url = bug_url %]
- <label><input type="checkbox" value="[% bug_url.name FILTER html %]"
- name="remove_[% field.name FILTER html %]">
- Remove</label>
- </li>
+ [% IF bug.id && value.size %]
+ <ul class="bug_urls">
+ [% FOREACH bug_url = value %]
+ <li>
+ [% PROCESS bug_url_link bug_url = bug_url %]
+ <label><input type="checkbox" value="[% bug_url.name FILTER html %]"
+ name="remove_[% field.name FILTER html %]">
+ Remove</label>
+ </li>
+ [% END %]
+ </ul>
[% END %]
- [% '</ul>' IF value.size %]
-
[% IF Param('use_see_also') %]
<span id="container_showhide_[% field.name FILTER html %]"
class="bz_default_hidden">
- <a href="#" id="showhide_[% field.name FILTER html %]">(add)</a>
+ (<a href="#" id="showhide_[% field.name FILTER html %]">add</a>)
</span>
<div id="container_[% field.name FILTER html %]">
- <label for="[% field.name FILTER html %]">
- <strong>Add [% terms.Bug %] URLs:</strong>
- </label><br>
<input type="text" id="[% field.name FILTER html %]" size="40"
- class="text_input" name="[% field.name FILTER html %]">
+ class="text_input" name="[% field.name FILTER html %]"
+ [% IF !bug.id %]value="[% value FILTER html %]"[% END %]>
</div>
- <script type="text/javascript">
+ [% IF bug.id %]
+ <script type="text/javascript">
setupEditLink('[% field.name FILTER js %]');
- </script>
+ </script>
+ [% END %]
[% END %]
[% CASE constants.FIELD_TYPE_KEYWORDS %]
<div id="keyword_container">
@@ -201,6 +247,8 @@
YAHOO.bugzilla.keywordAutocomplete.init('[% field.name FILTER js %]',
'keyword_autocomplete');
</script>
+ [% CASE constants.FIELD_TYPE_EXTENSION %]
+ [% Hook.process('editable') %]
[% END %]
[% ELSE %]
[% SWITCH field.type %]
@@ -224,10 +272,18 @@
</li>
[% END %]
[% '</ul>' IF value.size %]
+ [% CASE constants.FIELD_TYPE_EXTENSION %]
+ [% Hook.process('non_editable') %]
[% CASE %]
[% value.join(', ') FILTER html %]
[% END %]
[% END %]
+
+[% IF bug && field.name == 'component' %]
+ (<a href="buglist.cgi?component=[% bug.component FILTER uri %]&amp;product=[% bug.product FILTER uri %]&amp;bug_status=__open__"
+ target="_blank">show other [% terms.bugs %]</a>)
+[% END %]
+
[% Hook.process('end_field_column') %]
[% '</td>' IF NOT no_tds %]
diff --git a/template/en/default/bug/navigate.html.tmpl b/template/en/default/bug/navigate.html.tmpl
index 46b92aec4..95fe5a411 100644
--- a/template/en/default/bug/navigate.html.tmpl
+++ b/template/en/default/bug/navigate.html.tmpl
@@ -28,19 +28,28 @@
[% bug.bug_id FILTER uri %]">Format For Printing</a></li>
<li>&nbsp;-&nbsp;<a href="show_bug.cgi?ctype=xml&amp;id=
[% bug.bug_id FILTER uri %]">XML</a></li>
- <li>&nbsp;-&nbsp;<a href="enter_bug.cgi?cloned_bug_id=
- [% bug.bug_id FILTER uri %]">Clone This
+ <li>&nbsp;-&nbsp;<a href="enter_bug.cgi?format=__default__&amp;cloned_bug_id=
+ [% bug.bug_id FILTER uri %]"
+ id="clone_bug">Clone This
[% terms.Bug %]</a></li>
[%# Links to more things users can do with this bug. %]
[% Hook.process("links") %]
<li>&nbsp;-&nbsp;<a href="#">Top of page </a></li>
- </ul>
-[% END %]
-
+ </ul>
+ <script type="text/javascript">
+ YAHOO.util.Event.onDOMReady(function() {
+ init_clone_bug_menu(
+ YAHOO.util.Dom.get('clone_bug'),
+ '[% bug.bug_id FILTER js %]',
+ '[% bug.product FILTER js %]',
+ '[% bug.component FILTER js %]');
+ });
+ </script>
+[% END %]
-<div class="navigation">
[% SET my_search = user.recent_search_for(bug) %]
[% IF my_search %]
+ <div class="navigation">
[% SET last_bug_list = my_search.bug_list %]
[% SET this_bug_idx = lsearch(last_bug_list, bug.id) %]
<b>[% terms.Bug %] List:</b>
@@ -74,14 +83,5 @@
&nbsp;&nbsp;<a href="buglist.cgi?regetlastlist=
[%- my_search.id FILTER uri %]">Show last search results</a>
-[% ELSE %]
- [%# With no list, don't show link to search results %]
- <i><font color="#777777">First</font></i>
- <i><font color="#777777">Last</font></i>
- <i><font color="#777777">Prev</font></i>
- <i><font color="#777777">Next</font></i>
- &nbsp;&nbsp;
- <i><font color="#777777">This [% terms.bug %] is not in your last
- search results.</font></i>
+ </div>
[% END %]
-</div>
diff --git a/template/en/default/bug/process/bugmail.html.tmpl b/template/en/default/bug/process/bugmail.html.tmpl
index b0132a2fe..21e4ff7b7 100644
--- a/template/en/default/bug/process/bugmail.html.tmpl
+++ b/template/en/default/bug/process/bugmail.html.tmpl
@@ -24,37 +24,59 @@
# sent_bugmail: The results of Bugzilla::BugMail::Send().
#%]
+[% USE CGI %]
[% PROCESS global/variables.none.tmpl %]
-<dl>
-[% PROCESS emails
- description = "Email sent to"
- names = sent_bugmail.sent
+[%# hide the recipient list by default from new users %]
+[% show_recipients =
+ user.settings.post_bug_submit_action.value == 'nothing'
+ || CGI.cookie('show_bugmail_recipients')
+ || !user.can_see_bug(mailing_bugid)
%]
+[% recipient_count = sent_bugmail.sent.size %]
-[% PROCESS emails
- description = "Excluding"
- names = sent_bugmail.excluded
-%]
-</dl>
+<script>
+function toggleBugmailRecipients(bug_id, show) {
+ if (show) {
+ YAHOO.util.Dom.removeClass('bugmail_summary_' + bug_id, 'bz_default_hidden');
+ YAHOO.util.Dom.addClass('bugmail_summary_' + bug_id + '_short', 'bz_default_hidden');
+ } else {
+ YAHOO.util.Dom.addClass('bugmail_summary_' + bug_id, 'bz_default_hidden');
+ YAHOO.util.Dom.removeClass('bugmail_summary_' + bug_id + '_short', 'bz_default_hidden');
+ }
+ YAHOO.util.Cookie.set('show_bugmail_recipients', (show ? 1 : 0), {
+ expires: new Date("January 12, 2025")
+ });
+ return false;
+}
+</script>
-[%############################################################################%]
-[%# Block for a set of email addresses #%]
-[%############################################################################%]
-
-[% BLOCK emails %]
- <dt>[% description FILTER html %]:</dt>
+<dl id="bugmail_summary_[% mailing_bugid FILTER none %]"
+ class="[% show_recipients ? "" : "bz_default_hidden" %]">
+ <dt>Email sent to:</dt>
<dd>
[% IF user.can_see_bug(mailing_bugid) %]
- [% IF names.size > 0 %]
- [%+ FOREACH name = names %]
+ [% IF sent_bugmail.sent.size > 0 %]
+ [%+ FOREACH name = sent_bugmail.sent %]
<code>[% name FILTER html %]</code>[% ", " UNLESS loop.last() %]
[% END %]
[% ELSE %]
no one
[% END %]
+ (<a href="#" onclick="return toggleBugmailRecipients([% mailing_bugid FILTER none %], false)">hide</a>)
[% ELSE %]
(list of e-mails not available)
[% END %]
</dd>
-[% END %]
+</dl>
+
+<div id="bugmail_summary_[% mailing_bugid FILTER none %]_short"
+ class="[% show_recipients ? "bz_default_hidden" : "" %]">
+ [% IF recipient_count > 0 %]
+ Email sent to [% recipient_count FILTER html %] recipient[% 's' UNLESS recipient_count == 1 %].
+ (<a href="#" onclick="return toggleBugmailRecipients([% mailing_bugid FILTER none %], true)">show</a>)
+ [% ELSE %]
+ No emails were sent.
+ [% END %]
+</div>
+
diff --git a/template/en/default/bug/process/updates-disabled.html.tmpl b/template/en/default/bug/process/updates-disabled.html.tmpl
new file mode 100644
index 000000000..5ea84d476
--- /dev/null
+++ b/template/en/default/bug/process/updates-disabled.html.tmpl
@@ -0,0 +1,73 @@
+[%# The contents of this file are subject to the Mozilla Public License Version
+ # 1.1 (the "License"); you may not use this file except in compliance with
+ # the License. You may obtain a copy of the License at
+ # http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS IS" basis,
+ # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ # for the specific language governing rights and limitations under the
+ # License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is
+ # the Mozilla Foundation.
+ # Portions created by the Initial Developer are Copyright (C) 2011
+ # the Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s): Byron Jones <glob@mozilla.com>
+ #
+ #%]
+[% PROCESS global/variables.none.tmpl %]
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+<title>[% terms.Bugzilla %] - [% terms.Bug %] Updates Temporarily Suspended</title>
+<style type="text/css">
+body {
+ margin: 2em;
+ background-color: #455372;
+ color: #fff;
+ font-family: verdana, sans-serif;
+ font-size: small;
+}
+a {
+ color: #fff;
+ text-decoration: underline;
+}
+#buggie {
+ float: left;
+}
+#content {
+ margin-left: 100px;
+ max-width: 600px;
+}
+</style>
+</head>
+<body>
+<img src="images/buggie.png" id="buggie" alt="buggie">
+<div id="content">
+<h1>[% terms.Bug %] Updates Temporarily Suspended</h1>
+
+<p>
+We are currently adding a field to [% terms.Bugzilla %]. This requires us to
+prevent updates to [% terms.bugs %] for the duration of the database schema
+change to add the field (usually 3 to 5 minutes).
+</p>
+
+<p>
+<b>You should be able to leave this page open, wait a minute or two, then hit
+reload or refresh in your browser</b> (and OK any request to re-send the form
+data) to complete your [% terms.bug %] change. Once this maintenance is
+complete, your change will succeed and you won't get this page any more.
+</p>
+
+<p>
+Only updates to [% terms.bugs %] are being blocked by this page, any other
+activities in [% terms.Bugzilla %] are still fair game. <a href="index.cgi"
+target="_blank">Open [% terms.Bugzilla %] in a new tab/window</a> if you'd
+like, to continue working on other things while waiting.
+</p>
+</div>
+</body>
+</html>
diff --git a/template/en/default/bug/process/verify-new-product.html.tmpl b/template/en/default/bug/process/verify-new-product.html.tmpl
index c02c26470..1d2e8689f 100644
--- a/template/en/default/bug/process/verify-new-product.html.tmpl
+++ b/template/en/default/bug/process/verify-new-product.html.tmpl
@@ -120,9 +120,9 @@
[% IF old_groups.size %]
<p>These groups are not legal for the '[% product.name FILTER html %]'
- product or you are not allowed to restrict [% terms.bugs %] to these groups.
- [%+ terms.Bugs %] will no longer be restricted to these groups and may become
- public if no other group applies:<br>
+ product or you are not allowed to restrict [% terms.bugs %] to these groups.<br>
+ <b>[%+ terms.Bugs %] will no longer be restricted to these groups and may become
+ public if no other group applies:</b><br>
[% FOREACH group = old_groups %]
<input type="checkbox" id="group_[% group.id FILTER html %]"
name="groups" disabled="disabled" value="[% group.name FILTER html %]">
@@ -150,6 +150,9 @@
[% END %]
[% END %]
+ [%# BMO - check the default product sec-group to avoid accidental removal of all groups %]
+ [% CALL Bugzilla.check_default_product_security_group(product, old_groups, optional_groups) %]
+
[% IF optional_groups.size %]
<p>These groups are optional. You can decide to restrict [% terms.bugs %] to
one or more of the following groups:<br>
diff --git a/template/en/default/bug/show-header.html.tmpl b/template/en/default/bug/show-header.html.tmpl
index 54570911d..062e1aa82 100644
--- a/template/en/default/bug/show-header.html.tmpl
+++ b/template/en/default/bug/show-header.html.tmpl
@@ -31,24 +31,51 @@
[% filtered_desc = bug.short_desc FILTER html %]
[% subheader = filtered_desc %]
[% filtered_timestamp = bug.delta_ts FILTER time %]
-[% title = "$terms.Bug $bug.bug_id &ndash; $filtered_desc" %]
+[% title = "$terms.Bug $bug.bug_id &ndash; " %]
+[% IF bug.alias != '' %]
+ [% title = title _ "($bug.alias) " %]
+[% END %]
+[% title = title _ filtered_desc %]
[% header = "$terms.Bug&nbsp;$bug.bug_id" %]
[% header_addl_info = "Last modified: $filtered_timestamp" %]
[% yui = ['autocomplete', 'calendar'] %]
+[% yui.push('container') IF user.can_tag_comments %]
[% javascript_urls = [ "js/util.js", "js/field.js" ] %]
+[% javascript_urls.push("js/bug.js") IF user.id %]
+[% javascript_urls.push('js/comment-tagging.js')
+ IF user.id && Param('comment_taggers_group') %]
[% IF bug.defined %]
- [% unfiltered_title = "$terms.Bug $bug.bug_id – $bug.short_desc" %]
+ [% unfiltered_title = "$terms.Bug $bug.bug_id – " %]
+ [% IF bug.alias != '' %]
+ [% unfiltered_title = unfiltered_title _ "($bug.alias) " %]
+ [% END %]
+ [% unfiltered_title = unfiltered_title _ bug.short_desc %]
[% javascript = BLOCK %]
- if( !document.location.href.match(/show_bug\.cgi/) && history && history.replaceState ) {
- history.replaceState( null,
- "[% unfiltered_title FILTER js %]",
- "show_bug.cgi?id=[% bug.bug_id FILTER js %]" );
- document.title = "[% unfiltered_title FILTER js %]";
+ if (history && history.replaceState) {
+ if(!document.location.href.match(/show_bug\.cgi/)) {
+ history.replaceState( null,
+ "[% unfiltered_title FILTER js %]",
+ "show_bug.cgi?id=[% bug.bug_id FILTER js %]" );
+ document.title = "[% unfiltered_title FILTER js %]";
+ }
+ if (document.location.href.match(/show_bug\.cgi\?.*list_id=/)) {
+ var href = document.location.href;
+ href = href.replace(/[\?&]+list_id=(\d+|cookie)/, '');
+ history.replaceState(null, "[% unfiltered_title FILTER js %]", href);
+ }
}
+ YAHOO.util.Event.onDOMReady(function() {
+ initDirtyFieldTracking();
+
+ [% IF user.id AND user.is_involved_in_bug(bug) %]
+ YAHOO.bugzilla.bugUserLastVisit.update([ [% bug.bug_id FILTER none %] ]);
+ [% END %]
+ });
[% javascript FILTER none %]
[% END %]
[% END %]
-[% style_urls = [ "skins/standard/show_bug.css" ] %]
+[% style_urls = [ "skins/standard/show_bug.css",
+ "skins/custom/bug_groups.css" ] %]
[% doc_section = "bug_page.html" %]
[% bodyclasses = ['bz_bug',
"bz_status_$bug.bug_status",
diff --git a/template/en/default/bug/show-multiple.html.tmpl b/template/en/default/bug/show-multiple.html.tmpl
index 7c2b5345e..cfd0d8e20 100644
--- a/template/en/default/bug/show-multiple.html.tmpl
+++ b/template/en/default/bug/show-multiple.html.tmpl
@@ -191,7 +191,7 @@
[% USE Bugzilla %]
[% field_counter = 0 %]
- [% FOREACH field = Bugzilla.active_custom_fields %]
+ [% FOREACH field = Bugzilla.active_custom_fields(product=>bug.product_obj,component=>bug.component_obj,bug_id=>bug.id) %]
[% field_counter = field_counter + 1 %]
[%# Odd-numbered fields get an opening <tr> %]
[% '<tr>' IF field_counter % 2 %]
diff --git a/template/en/default/bug/show.xml.tmpl b/template/en/default/bug/show.xml.tmpl
index dae207f26..c0f32d69e 100644
--- a/template/en/default/bug/show.xml.tmpl
+++ b/template/en/default/bug/show.xml.tmpl
@@ -20,8 +20,10 @@
#
#%]
[% PROCESS bug/time.html.tmpl %]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
<?xml version="1.0" [% IF Param('utf8') %]encoding="UTF-8" [% END %]standalone="yes" ?>
-<!DOCTYPE bugzilla SYSTEM "[% urlbase FILTER html %]bugzilla.dtd">
+<!DOCTYPE bugzilla [% IF cgi.param('dtd') %][[% PROCESS pages/bugzilla.dtd.tmpl %]][% ELSE %]SYSTEM "[% urlbase FILTER xml %]page.cgi?id=bugzilla.dtd"[% END %]>
<bugzilla version="[% constants.BUGZILLA_VERSION %]"
urlbase="[% urlbase FILTER xml %]"
diff --git a/template/en/default/bug/time.html.tmpl b/template/en/default/bug/time.html.tmpl
index e070e7de0..c58675b96 100644
--- a/template/en/default/bug/time.html.tmpl
+++ b/template/en/default/bug/time.html.tmpl
@@ -18,7 +18,7 @@
# Contributor(s): Jeff Hedlund <jeff.hedlund@matrixsi.com>
#
#%]
-
+
[% BLOCK formattimeunit %]
[%# INTERFACE:
# time_unit: the number converting, converts to 2 decimal places
@@ -26,11 +26,7 @@
# 1 decimal place
#%]
[% time_unit = time_unit FILTER format('%.2f') %]
- [% IF time_unit.match('0\Z') %]
- [% time_unit FILTER format('%.1f') %]
- [% ELSE %]
- [% time_unit FILTER format('%.2f') %]
- [% END %]
+ [% time_unit.replace('0\Z', '') %]
[% END %]
[% BLOCK calculatepercentage %]
diff --git a/template/en/default/config.rdf.tmpl b/template/en/default/config.rdf.tmpl
index 15f784ce8..5686d138b 100644
--- a/template/en/default/config.rdf.tmpl
+++ b/template/en/default/config.rdf.tmpl
@@ -168,12 +168,12 @@
<bz:component rdf:about="[% escaped_urlbase %]component.cgi?name=[% component.name FILTER uri
%]&amp;product=[% product.name FILTER uri %]">
<bz:name>[% component.name FILTER html %]</bz:name>
+ <bz:is_active>[% component.is_active FILTER html %]</bz:is_active>
[% IF show_flags %]
<bz:flag_types>
<Seq>
[% flag_types = component.flag_types.bug.merge(component.flag_types.attachment) %]
[% FOREACH flag_type = flag_types %]
- [% NEXT UNLESS flag_type.is_active %]
[% all_visible_flag_types.${flag_type.id} = flag_type %]
<li resource="[% escaped_urlbase %]flag.cgi?id=[% flag_type.id FILTER uri
%]&amp;name=[% flag_type.name FILTER uri %]" />
@@ -195,6 +195,7 @@
<li>
<bz:version rdf:about="[% escaped_urlbase %]version.cgi?name=[% version.name FILTER uri %]">
<bz:name>[% version.name FILTER html %]</bz:name>
+ <bz:is_active>[% version.is_active FILTER html %]</bz:is_active>
</bz:version>
</li>
[% END %]
@@ -210,6 +211,7 @@
<li>
<bz:target_milestone rdf:about="[% escaped_urlbase %]milestone.cgi?name=[% milestone.name FILTER uri %]">
<bz:name>[% milestone.name FILTER html %]</bz:name>
+ <bz:is_active>[% milestone.is_active FILTER html %]</bz:is_active>
</bz:target_milestone>
</li>
[% END %]
diff --git a/template/en/default/email/bugmail-header.txt.tmpl b/template/en/default/email/bugmail-header.txt.tmpl
index 94559a942..679e705cd 100644
--- a/template/en/default/email/bugmail-header.txt.tmpl
+++ b/template/en/default/email/bugmail-header.txt.tmpl
@@ -23,25 +23,17 @@
[% PROCESS "global/field-descs.none.tmpl" %]
[% PROCESS "global/reason-descs.none.tmpl" %]
[% isnew = bug.lastdiffed ? 0 : 1 %]
+[% show_new = isnew
+ && (to_user.settings.bugmail_new_prefix.value == 'on') %]
From: [% Param('mailfrom') %]
To: [% to_user.email %]
-Subject: [[% terms.Bug %] [%+ bug.id %]] [% 'New: ' IF isnew %][%+ bug.short_desc %]
+Subject: [[% terms.Bug %] [%+ bug.id %]] [% 'New: ' IF show_new %][%+ bug.short_desc %]
Date: [% date %]
X-Bugzilla-Reason: [% reasonsheader %]
-X-Bugzilla-Type: [% isnew ? 'new' : 'changed' %]
+X-Bugzilla-Type: [% bugmailtype %]
X-Bugzilla-Watch-Reason: [% reasonswatchheader %]
-[% IF Param('useclassification') %]
-X-Bugzilla-Classification: [% bug.classification %]
-[% END %]
-X-Bugzilla-Product: [% bug.product %]
-X-Bugzilla-Component: [% bug.component %]
-X-Bugzilla-Keywords: [% bug.keywords %]
-X-Bugzilla-Severity: [% bug.bug_severity %]
-X-Bugzilla-Who: [% changer.login %]
-X-Bugzilla-Status: [% bug.bug_status %]
-X-Bugzilla-Priority: [% bug.priority %]
-X-Bugzilla-Assigned-To: [% bug.assigned_to.login %]
-X-Bugzilla-Target-Milestone: [% bug.target_milestone %]
+[%+ INCLUDE "email/header-common.txt.tmpl" %]
X-Bugzilla-Changed-Fields: [% changedfields.join(" ") %]
+X-Bugzilla-Changed-Field-Names: [% changedfieldnames.join(" ") %]
[%+ threadingmarker %]
diff --git a/template/en/default/email/bugmail.html.tmpl b/template/en/default/email/bugmail.html.tmpl
index d52fe6306..2cd9318c1 100644
--- a/template/en/default/email/bugmail.html.tmpl
+++ b/template/en/default/email/bugmail.html.tmpl
@@ -34,15 +34,30 @@
<b>[% "Comment # ${comment.count}" FILTER bug_link(bug,
{comment_num => comment.count, full_url => 1, user => to_user}) FILTER none %]
on [% "$terms.bug $bug.id" FILTER bug_link(bug, { full_url => 1, user => to_user }) FILTER none %]
- from [% INCLUDE global/user.html.tmpl who = comment.author %]</b>
+ from [% INCLUDE global/user.html.tmpl user = to_user, who = comment.author %]</b>
[% END %]
<pre>[% comment.body_full({ wrap => 1 }) FILTER quoteUrls(bug, comment, to_user) %]</pre>
</div>
[% END %]
</p>
+
+ [% IF referenced_bugs.size %]
+ <hr>
+ <span>Referenced [% terms.Bugs %]:</span>
+
+ <ul>
+ [% FOREACH ref = referenced_bugs %]
+ <li>
+ [<a href="[% urlbase FILTER html %]show_bug.cgi?id=[% ref.id FILTER none %]">
+ [% terms.Bug %]&nbsp;[% ref.id FILTER none %]</a>] [% ref.short_desc FILTER html %]
+ </li>
+ [% END %]
+ </ul>
+ [% END %]
+
<hr>
<span>You are receiving this mail because:</span>
-
+
<ul>
[% FOREACH reason = reasons %]
[% IF reason_descs.$reason %]
@@ -75,7 +90,7 @@
FILTER bug_link(change.blocker, {full_url => 1, user => to_user}) FILTER none %],
which changed state.
[% ELSE %]
- [% INCLUDE global/user.html.tmpl who = change.who %] changed
+ [% INCLUDE global/user.html.tmpl user = to_user, who = change.who %] changed
[%+ "${terms.bug} ${bug.id}" FILTER bug_link(bug, {full_url => 1, user => to_user}) FILTER none %]
[% END %]
<br>
diff --git a/template/en/default/email/bugmail.txt.tmpl b/template/en/default/email/bugmail.txt.tmpl
index a3a0b873c..525070d99 100644
--- a/template/en/default/email/bugmail.txt.tmpl
+++ b/template/en/default/email/bugmail.txt.tmpl
@@ -34,6 +34,15 @@
[% END %]
[%+ comment.body_full({ is_bugmail => 1, wrap => 1 }) FILTER strip_control_chars %]
[% END %]
+[% IF referenced_bugs.size %]
+
+Referenced [% terms.Bugs %]:
+
+[% FOREACH ref = referenced_bugs %]
+[%+ urlbase %]show_bug.cgi?id=[% ref.id %]
+[%+ "[" _ terms.Bug _ " " _ ref.id _ "] " _ ref.short_desc FILTER wrap_comment(76) %]
+[% END %]
+[% END %]
-- [%# Protect the trailing space of the signature marker %]
You are receiving this mail because:
diff --git a/template/en/default/email/header-common.txt.tmpl b/template/en/default/email/header-common.txt.tmpl
new file mode 100644
index 000000000..3f3b7d373
--- /dev/null
+++ b/template/en/default/email/header-common.txt.tmpl
@@ -0,0 +1,24 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+[% IF Param('useclassification') %]
+X-Bugzilla-Classification: [% bug.classification %]
+[% END %]
+X-Bugzilla-ID: [% bug.id %]
+X-Bugzilla-Product: [% bug.product %]
+X-Bugzilla-Component: [% bug.component %]
+X-Bugzilla-Version: [% bug.version %]
+X-Bugzilla-Keywords: [% bug.keywords %]
+X-Bugzilla-Severity: [% bug.bug_severity %]
+X-Bugzilla-Who: [% changer.login %]
+X-Bugzilla-Status: [% bug.bug_status %]
+X-Bugzilla-Resolution: [% bug.resolution %]
+X-Bugzilla-Priority: [% bug.priority %]
+X-Bugzilla-Assigned-To: [% bug.assigned_to.login %]
+X-Bugzilla-Target-Milestone: [% bug.target_milestone %]
+X-Bugzilla-Flags:[% FOREACH flag = bug.flags %] [%+ flag.name %][% flag.status %][% END %]
+X-Bugzilla-OS: [% bug.op_sys %]
diff --git a/template/en/default/email/lockout.txt.tmpl b/template/en/default/email/lockout.txt.tmpl
index ac6525779..94e9c74cb 100644
--- a/template/en/default/email/lockout.txt.tmpl
+++ b/template/en/default/email/lockout.txt.tmpl
@@ -22,10 +22,10 @@
From: [% Param('mailfrom') %]
To: [% Param('maintainer') %]
-Subject: [[% terms.Bugzilla %]] Account Lock-Out: [% locked_user.login %] ([% attempts.0.ip_addr %])
+Subject: [[% terms.Bugzilla %]] Account Lock-Out: [% locked_user.login %] ([% address %])
X-Bugzilla-Type: admin
-The IP address [% attempts.0.ip_addr %] failed too many login attempts (
+The address [% address %] failed too many login attempts (
[%- constants.MAX_LOGIN_ATTEMPTS +%]) for
the account [% locked_user.login %].
diff --git a/template/en/default/extensions/config.pm.tmpl b/template/en/default/extensions/config.pm.tmpl
index 6997ec178..07ac83a41 100644
--- a/template/en/default/extensions/config.pm.tmpl
+++ b/template/en/default/extensions/config.pm.tmpl
@@ -1,33 +1,20 @@
-[%# -*- mode: perl -*- %]
-[%# The contents of this file are subject to the Mozilla Public
- # License Version 1.1 (the "License"); you may not use this file
- # except in compliance with the License. You may obtain a copy of
- # the License at http://www.mozilla.org/MPL/
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
- # Software distributed under the License is distributed on an "AS
- # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
- # implied. See the License for the specific language governing
- # rights and limitations under the License.
- #
- # The Original Code is the Bugzilla Bug Tracking System.
- #
- # The Initial Developer of the Original Code is Everything Solved, Inc.
- # Portions created by the Initial Developer are Copyright (C) 2009 the
- # Initial Developer. All Rights Reserved.
- #
- # Contributor(s):
- # Max Kanat-Alexander <mkanat@bugzilla.org>
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
#%]
[%# INTERFACE:
# name: string; The name of the extension.
#%]
-[% PROCESS global/variables.none.tmpl %]
-
[% PROCESS extensions/license.txt.tmpl %]
package B[% %]ugzilla::Extension::[% name %];
+
+use 5.10.1;
use strict;
use constant NAME => '[% name %]';
diff --git a/template/en/default/extensions/extension.pm.tmpl b/template/en/default/extensions/extension.pm.tmpl
index 249227103..ebeb73719 100644
--- a/template/en/default/extensions/extension.pm.tmpl
+++ b/template/en/default/extensions/extension.pm.tmpl
@@ -1,35 +1,22 @@
-[%# -*- mode: perl -*- %]
-[%# The contents of this file are subject to the Mozilla Public
- # License Version 1.1 (the "License"); you may not use this file
- # except in compliance with the License. You may obtain a copy of
- # the License at http://www.mozilla.org/MPL/
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
- # Software distributed under the License is distributed on an "AS
- # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
- # implied. See the License for the specific language governing
- # rights and limitations under the License.
- #
- # The Original Code is the Bugzilla Bug Tracking System.
- #
- # The Initial Developer of the Original Code is Everything Solved, Inc.
- # Portions created by the Initial Developer are Copyright (C) 2009 the
- # Initial Developer. All Rights Reserved.
- #
- # Contributor(s):
- # Max Kanat-Alexander <mkanat@bugzilla.org>
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
#%]
[%# INTERFACE:
# name: string; The name of the extension.
#%]
-[% PROCESS global/variables.none.tmpl %]
-
[% PROCESS extensions/license.txt.tmpl %]
package B[% %]ugzilla::Extension::[% name %];
+
+use 5.10.1;
use strict;
-use base qw(B[% %]ugzilla::Extension);
+use parent qw(B[% %]ugzilla::Extension);
# This code for this is in [% path %]/lib/Util.pm
use B[% %]ugzilla::Extension::[% name %]::Util;
diff --git a/template/en/default/extensions/hook-readme.txt.tmpl b/template/en/default/extensions/hook-readme.txt.tmpl
index efceec136..63e09e419 100644
--- a/template/en/default/extensions/hook-readme.txt.tmpl
+++ b/template/en/default/extensions/hook-readme.txt.tmpl
@@ -1,25 +1,11 @@
-[%# The contents of this file are subject to the Mozilla Public
- # License Version 1.1 (the "License"); you may not use this file
- # except in compliance with the License. You may obtain a copy of
- # the License at http://www.mozilla.org/MPL/
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
- # Software distributed under the License is distributed on an "AS
- # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
- # implied. See the License for the specific language governing
- # rights and limitations under the License.
- #
- # The Original Code is the Bugzilla Bug Tracking System.
- #
- # The Initial Developer of the Original Code is Everything Solved, Inc.
- # Portions created by the Initial Developer are Copyright (C) 2009 the
- # Initial Developer. All Rights Reserved.
- #
- # Contributor(s):
- # Max Kanat-Alexander <mkanat@bugzilla.org>
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
#%]
-[% PROCESS global/variables.none.tmpl %]
-
Template hooks go in this directory. Template hooks are called in normal
[%+ terms.Bugzilla %] templates like [[% '%' %] Hook.process('some-hook') %].
More information about them can be found in the documentation of
diff --git a/template/en/default/extensions/license.txt.tmpl b/template/en/default/extensions/license.txt.tmpl
index 964e07505..6acde01e0 100644
--- a/template/en/default/extensions/license.txt.tmpl
+++ b/template/en/default/extensions/license.txt.tmpl
@@ -1,47 +1,18 @@
-[%# -*- mode: perl -*- %]
-[%# The contents of this file are subject to the Mozilla Public
- # License Version 1.1 (the "License"); you may not use this file
- # except in compliance with the License. You may obtain a copy of
- # the License at http://www.mozilla.org/MPL/
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
- # Software distributed under the License is distributed on an "AS
- # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
- # implied. See the License for the specific language governing
- # rights and limitations under the License.
- #
- # The Original Code is the Bugzilla Bug Tracking System.
- #
- # The Initial Developer of the Original Code is Everything Solved, Inc.
- # Portions created by the Initial Developer are Copyright (C) 2009 the
- # Initial Developer. All Rights Reserved.
- #
- # Contributor(s):
- # Max Kanat-Alexander <mkanat@bugzilla.org>
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
#%]
[%# INTERFACE:
# name: string; The name of the extension.
#%]
-[% PROCESS global/variables.none.tmpl %]
-
-# -*- Mode: perl; indent-tabs-mode: nil -*-
-#
-# The contents of this file are subject to the Mozilla Public
-# License Version 1.1 (the "License"); you may not use this file
-# except in compliance with the License. You may obtain a copy of
-# the License at http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS
-# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
-# implied. See the License for the specific language governing
-# rights and limitations under the License.
-#
-# The Original Code is the [% name %] [%+ terms.Bugzilla %] Extension.
-#
-# The Initial Developer of the Original Code is YOUR NAME
-# Portions created by the Initial Developer are Copyright (C) [% year %] the
-# Initial Developer. All Rights Reserved.
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
-# Contributor(s):
-# YOUR NAME <YOUR EMAIL ADDRESS>
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
diff --git a/template/en/default/extensions/name-readme.txt.tmpl b/template/en/default/extensions/name-readme.txt.tmpl
index 6d25c839e..5403bab7f 100644
--- a/template/en/default/extensions/name-readme.txt.tmpl
+++ b/template/en/default/extensions/name-readme.txt.tmpl
@@ -1,25 +1,11 @@
-[%# The contents of this file are subject to the Mozilla Public
- # License Version 1.1 (the "License"); you may not use this file
- # except in compliance with the License. You may obtain a copy of
- # the License at http://www.mozilla.org/MPL/
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
- # Software distributed under the License is distributed on an "AS
- # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
- # implied. See the License for the specific language governing
- # rights and limitations under the License.
- #
- # The Original Code is the Bugzilla Bug Tracking System.
- #
- # The Initial Developer of the Original Code is Everything Solved, Inc.
- # Portions created by the Initial Developer are Copyright (C) 2009 the
- # Initial Developer. All Rights Reserved.
- #
- # Contributor(s):
- # Max Kanat-Alexander <mkanat@bugzilla.org>
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
#%]
-[% PROCESS global/variables.none.tmpl %]
-
Normal templates go in this directory. You can load them in your
code like this:
diff --git a/template/en/default/extensions/util.pm.tmpl b/template/en/default/extensions/util.pm.tmpl
index 32076a665..3493007f4 100644
--- a/template/en/default/extensions/util.pm.tmpl
+++ b/template/en/default/extensions/util.pm.tmpl
@@ -1,35 +1,22 @@
-[%# -*- mode: perl -*- %]
-[%# The contents of this file are subject to the Mozilla Public
- # License Version 1.1 (the "License"); you may not use this file
- # except in compliance with the License. You may obtain a copy of
- # the License at http://www.mozilla.org/MPL/
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
- # Software distributed under the License is distributed on an "AS
- # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
- # implied. See the License for the specific language governing
- # rights and limitations under the License.
- #
- # The Original Code is the Bugzilla Bug Tracking System.
- #
- # The Initial Developer of the Original Code is Everything Solved, Inc.
- # Portions created by the Initial Developer are Copyright (C) 2009 the
- # Initial Developer. All Rights Reserved.
- #
- # Contributor(s):
- # Max Kanat-Alexander <mkanat@bugzilla.org>
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
#%]
[%# INTERFACE:
# name: string; The name of the extension.
#%]
-[% PROCESS global/variables.none.tmpl %]
-
[% PROCESS extensions/license.txt.tmpl %]
package B[% %]ugzilla::Extension::[% name %]::Util;
+
+use 5.10.1;
use strict;
-use base qw(Exporter);
+use parent qw(Exporter);
our @EXPORT = qw(
);
diff --git a/template/en/default/extensions/web-readme.txt.tmpl b/template/en/default/extensions/web-readme.txt.tmpl
index 55e593914..41dcd8edf 100644
--- a/template/en/default/extensions/web-readme.txt.tmpl
+++ b/template/en/default/extensions/web-readme.txt.tmpl
@@ -1,25 +1,11 @@
-[%# The contents of this file are subject to the Mozilla Public
- # License Version 1.1 (the "License"); you may not use this file
- # except in compliance with the License. You may obtain a copy of
- # the License at http://www.mozilla.org/MPL/
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
- # Software distributed under the License is distributed on an "AS
- # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
- # implied. See the License for the specific language governing
- # rights and limitations under the License.
- #
- # The Original Code is the Bugzilla Bug Tracking System.
- #
- # The Initial Developer of the Original Code is Everything Solved, Inc.
- # Portions created by the Initial Developer are Copyright (C) 2010 the
- # Initial Developer. All Rights Reserved.
- #
- # Contributor(s):
- # Max Kanat-Alexander <mkanat@bugzilla.org>
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
#%]
-[% PROCESS global/variables.none.tmpl %]
-
Web-accessible files, like JavaScript, CSS, and images go in this
directory. You can reference them directly in your HTML. For example,
if you have a file called "style.css" and your extension is called
diff --git a/template/en/default/filterexceptions.pl b/template/en/default/filterexceptions.pl
index 402862734..a02230950 100644
--- a/template/en/default/filterexceptions.pl
+++ b/template/en/default/filterexceptions.pl
@@ -52,7 +52,6 @@
],
'flag/list.html.tmpl' => [
- 'flag.id',
'flag.status',
'type.id',
],
@@ -281,8 +280,7 @@
'bug/time.html.tmpl' => [
- 'time_unit FILTER format(\'%.1f\')',
- 'time_unit FILTER format(\'%.2f\')',
+ "time_unit.replace('0\\Z', '')",
'(act / (act + rem)) * 100
FILTER format("%d")',
],
@@ -320,7 +318,6 @@
'attachment/edit.html.tmpl' => [
'attachment.id',
'attachment.bug_id',
- 'a',
'editable_or_hide',
],
diff --git a/template/en/default/flag/list.html.tmpl b/template/en/default/flag/list.html.tmpl
index 4467e81ce..0d84e9bff 100644
--- a/template/en/default/flag/list.html.tmpl
+++ b/template/en/default/flag/list.html.tmpl
@@ -18,7 +18,7 @@
# Contributor(s): Myk Melez <myk@mozilla.org>
#%]
-[% IF user.id AND !read_only_flags %]
+[% IF user.id && (!bug || bug.check_can_change_field('flagtypes.name', 0, 1)) %]
[%# We list flags by looping twice over the flag types relevant for the bug.
# In the first loop, we display existing flags and then, for active types,
@@ -51,85 +51,30 @@
[%-# Step 1a: Display existing flag(s). %]
[% FOREACH flag = type.flags %]
- <tr>
- <td>
- <span title="[% flag.setter.identity FILTER html %]">[% flag.setter.nick FILTER html %]</span>:
- </td>
- <td>
- <label title="[% type.description FILTER html %]"
- for="flag-[% flag.id %]">
- [%- type.name FILTER html FILTER no_break -%]</label>
- </td>
- <td>
- <select id="flag-[% flag.id %]" name="flag-[% flag.id %]"
- title="[% type.description FILTER html %]"
- onchange="toggleRequesteeField(this);"
- class="flag_select flag_type-[% type.id %]">
- [%# Only display statuses the user is allowed to set. %]
- [% IF user.can_request_flag(type) || flag.setter_id == user.id %]
- <option value="X"></option>
- [% END %]
- [% IF type.is_active %]
- [% IF (type.is_requestable && user.can_request_flag(type)) || flag.status == "?" %]
- <option value="?" [% "selected" IF flag.status == "?" %]>?</option>
- [% END %]
- [% IF user.can_set_flag(type) || flag.status == "+" %]
- <option value="+" [% "selected" IF flag.status == "+" %]>+</option>
- [% END %]
- [% IF user.can_set_flag(type) || flag.status == "-" %]
- <option value="-" [% "selected" IF flag.status == "-" %]>-</option>
- [% END %]
- [% ELSE %]
- <option value="[% flag.status %]" selected="selected">[% flag.status %]</option>
- [% END %]
- </select>
- </td>
- [% IF any_flags_requesteeble %]
- <td>
- [% IF (type.is_active && type.is_requestable && type.is_requesteeble) || flag.requestee %]
- <span style="white-space: nowrap;">
- [% SET flag_custom_list = [] %]
- [% IF Param('usemenuforusers') %]
- [% flag_custom_list = flag.type.grant_list %]
- [% IF !(type.is_active && type.is_requestable && type.is_requesteeble) %]
- [%# We are here only because there was already a requestee. In this case,
- the only valid action is to remove the requestee or leave it alone;
- nothing else. %]
- [% flag_custom_list = [flag.requestee] %]
- [% END %]
- [% END %]
- [% INCLUDE global/userselect.html.tmpl
- name => "requestee-$flag.id"
- id => "requestee-$flag.id"
- value => flag.requestee.login
- multiple => 0
- emptyok => 1
- classes => ["requestee"]
- custom_userlist => flag_custom_list
- %]
- </span>
- [% END %]
- </td>
- [% END %]
- </tr>
+ [% PROCESS flag_row flag = flag type = type %]
[% END -%]
+ [% SET flag = "" %]
+ [% NEXT IF read_only_flags %]
+
[%-# Step 1b: Display UI for setting flag. %]
[% IF (!type.flags || type.flags.size == 0) && type.is_active %]
-
- [% PROCESS flag_row first_cell_empty = 1 addl_text = "" %]
+ [% PROCESS flag_row type = type %]
[% END %]
[% END %]
- [%# Step 2: Display flag type again (if type is multiplicable). %]
- [% FOREACH type = flag_types %]
- [% NEXT UNLESS type.flags && type.flags.size > 0 && type.is_multiplicable && type.is_active %]
- [% IF !separator_displayed %]
- <tr><td colspan="3"><hr></td></tr>
+ [% IF !read_only_flags %]
+ [%# Step 2: Display flag type again (if type is multiplicable). %]
+ [% FOREACH type = flag_types %]
+ [% NEXT UNLESS type.flags && type.flags.size > 0 && type.is_multiplicable && type.is_active %]
+ [% IF !separator_displayed %]
+ <tbody class="bz_flag_type">
+ <tr><td colspan="3"><hr></td></tr>
+ </tbody>
[% separator_displayed = 1 %]
+ [% END %]
+ [% PROCESS flag_row type = type addl_text = "addl." %]
[% END %]
-
- [% PROCESS flag_row first_cell_empty = 0 addl_text = "addl." %]
[% END %]
</table>
@@ -159,58 +104,87 @@
[% END %]
[% END %]
-[%# Display a table row for unset flags %]
+[%# Display a table row for flags %]
[% BLOCK flag_row %]
- <tr>
- [% IF first_cell_empty %]
- <td>&nbsp;</td>
- <td>
- [% ELSE %]
- <td colspan="2">
- [% END %]
-
- [% addl_text FILTER html %]
- <label title="[% type.description FILTER html %]" for="flag_type-[% type.id %]">
- [%- type.name FILTER html FILTER no_break %]</label>
- </td>
- <td>
- <select id="flag_type-[% type.id %]" name="flag_type-[% type.id %]"
- title="[% type.description FILTER html %]"
- [% " disabled=\"disabled\"" UNLESS (type.is_requestable && user.can_request_flag(type)) || user.can_set_flag(type) %]
- onchange="toggleRequesteeField(this);"
- class="flag_select flag_type-[% type.id %]">
- <option value="X"></option>
- [% IF type.is_requestable && user.can_request_flag(type) %]
- <option value="?">?</option>
- [% END %]
- [% IF user.can_set_flag(type) %]
- <option value="+">+</option>
- <option value="-">-</option>
+ [% SET fid = flag ? "flag-$flag.id" : "flag_type-$type.id" %]
+ [% can_edit_flag = (!read_only_flags || (flag && (flag.setter_id == user.id || (flag.requestee_id && flag.requestee_id == user.id)))) ? 1 : 0 %]
+ <tbody[% ' class="bz_flag_type"' IF !flag %]>
+ <tr>
+ <td>
+ [% IF flag %]
+ <span title="[% flag.setter.identity FILTER html %]">[% flag.setter.nick FILTER html %]</span>:
+ [% ELSE %]
+ [% addl_text FILTER html %]
[% END %]
- </select>
- </td>
- [% IF any_flags_requesteeble %]
+ </td>
+ <td>
+ <label title="[% type.description FILTER html %]" for="[% fid FILTER html %]">
+ [%- type.name FILTER html FILTER no_break -%]</label>
+ </td>
<td>
- [% IF type.is_requestable && type.is_requesteeble %]
- <span style="white-space: nowrap;">
- [% SET grant_list = [] %]
- [% IF Param('usemenuforusers') %]
- [% grant_list = type.grant_list %]
- [% END %]
- [% INCLUDE global/userselect.html.tmpl
- name => "requestee_type-$type.id"
- id => "requestee_type-$type.id"
- multiple => type.is_multiplicable * 3
- emptyok => !type.is_multiplicable
- value => ""
- custom_userlist => grant_list
- classes => ["requestee"]
- %]
-
- </span>
+ <input type="hidden" id="[% fid FILTER html %]_dirty">
+ <select id="[% fid FILTER html %]" name="[% fid FILTER html %]"
+ [% IF !flag && !((type.is_requestable && user.can_request_flag(type)) || user.can_set_flag(type)) %]
+ disabled="disabled"
+ [% END %]
+ title="[% type.description FILTER html %]"
+ onchange="toggleRequesteeField(this);"
+ class="flag_select flag_type-[% type.id %]"
+ [% IF !can_edit_flag %] disabled="disabled"[% END %]>
+ [%# Only display statuses the user is allowed to set. %]
+ [% IF !flag || (can_edit_flag && user.can_request_flag(type)) || flag.setter_id == user.id %]
+ <option value="X"></option>
[% END %]
+ [% IF type.is_active && can_edit_flag %]
+ [% IF (type.is_requestable && user.can_request_flag(type)) || (flag && flag.status == "?") %]
+ <option value="?" [% "selected" IF flag && flag.status == "?" %]>?</option>
+ [% END %]
+ [% IF user.can_set_flag(type) || (flag && flag.status == "+") %]
+ <option value="+" [% "selected" IF flag && flag.status == "+" %]>+</option>
+ [% END %]
+ [% IF user.can_set_flag(type) || (flag && flag.status == "-") %]
+ <option value="-" [% "selected" IF flag && flag.status == "-" %]>-</option>
+ [% END %]
+ [% ELSE %]
+ <option value="[% flag.status %]" selected="selected">[% flag.status %]</option>
+ [% END %]
+ </select>
</td>
- [% END %]
- </tr>
+ [% IF any_flags_requesteeble %]
+ <td>
+ [% IF (type.is_active && type.is_requestable && type.is_requesteeble) || (flag && flag.requestee) %]
+ <span style="white-space: nowrap;">
+ [% SET grant_list = [] %]
+ [% IF Param('usemenuforusers') %]
+ [% IF !can_edit_flag || (flag && !(type.is_active && type.is_requestable && type.is_requesteeble)) %]
+ [%# We are here only because there was already a requestee. In this case,
+ the only valid action is to remove the requestee or leave it alone;
+ nothing else. %]
+ [% grant_list = [flag.requestee] %]
+ [% ELSE %]
+ [% grant_list = type.grant_list %]
+ [% END %]
+ [% END %]
+ [% SET flag_name = flag ? "requestee-$flag.id" : "requestee_type-$type.id" %]
+ [% SET flag_requestee = (flag && flag.requestee) ? flag.requestee.login : '' %]
+ [% SET flag_multiple = flag ? 0 : type.is_multiplicable * 3 %]
+ [% SET flag_empty_ok = flag ? 1 : !type.is_multiplicable %]
+ [% INCLUDE global/userselect.html.tmpl
+ name => flag_name
+ id => flag_name
+ value => flag_requestee
+ multiple => flag_multiple
+ emptyok => flag_empty_ok
+ classes => ["requestee"]
+ custom_userlist => grant_list
+ disabled => !can_edit_flag
+ %]
+ [% Hook.process("requestee", "flag/list.html.tmpl") %]
+ </span>
+ [% END %]
+ </td>
+ [% END %]
+ </tr>
+ </tbody>
[% END %]
diff --git a/template/en/default/global/code-error.html.tmpl b/template/en/default/global/code-error.html.tmpl
index 877fe8d66..62442c268 100644
--- a/template/en/default/global/code-error.html.tmpl
+++ b/template/en/default/global/code-error.html.tmpl
@@ -262,7 +262,7 @@
Flags cannot be set for objects of type [% caller FILTER html %].
They can only be set for [% terms.bugs %] and attachments.
- [% ELSIF error == "flag_requestee_disabled" %]
+ [% ELSIF error == "flag_type_requestee_disabled" %]
[% title = "Flag not Requestable from Specific Person" %]
You can't ask a specific person for
<em>[% type.name FILTER html %]</em>.
@@ -509,31 +509,23 @@
admindocslinks = admindocslinks
%]
-<tt>
- <p>
- [% terms.Bugzilla %] has suffered an internal error. Please save this page and send
- it to [% Param("maintainer") %] with details of what you were doing at
- the time this message appeared.
- </p>
- <script type="text/javascript"> <!--
- document.write("<p>URL: " +
- document.location.href.replace(/&/g,"&amp;")
- .replace(/</g,"&lt;")
- .replace(/>/g,"&gt;") + "</p>");
- // -->
- </script>
-</tt>
-
-<table cellpadding="20">
- <tr>
- <td id="error_msg" class="throw_error">
- [% error_message FILTER none %]
- </td>
- </tr>
-</table>
+[%# return the generated error_message for sentry %]
+[% processed.error_message = error_message %]
+
+<p>
+ [% terms.Bugzilla %] has suffered an internal error:
+</p>
-<p>Traceback:</p>
-<pre>[% traceback FILTER html %]</pre>
+<p class="throw_error">
+ [% error_message FILTER none %]
+</p>
+
+[% IF maintainers_notified %]
+<p>
+ The [% terms.Bugzilla %] maintainers have been notified of this error
+ [#[% uid FILTER html %]].
+</p>
+[% END %]
[% IF variables %]
<pre>
diff --git a/template/en/default/global/common-links.html.tmpl b/template/en/default/global/common-links.html.tmpl
index 769d41e7e..50cfa020c 100644
--- a/template/en/default/global/common-links.html.tmpl
+++ b/template/en/default/global/common-links.html.tmpl
@@ -28,7 +28,7 @@
<li><span class="separator">| </span><a href="describecomponents.cgi">Browse</a></li>
<li><span class="separator">| </span><a href="query.cgi">Search</a></li>
- <li class="form">
+ <li class="form quicksearch_form">
<span class="separator">| </span>
<form action="buglist.cgi" method="get"
onsubmit="if (this.quicksearch.value == '')
@@ -39,10 +39,12 @@
<input class="btn" type="submit" value="Search"
id="find[% qs_suffix FILTER html %]">
[%-# Work around FF bug: keep this on one line %]</form>
- <a href="page.cgi?id=quicksearch.html" title="Quicksearch Help">[?]</a></li>
+ [<a href="page.cgi?id=quicksearch.html" title="Quicksearch Help">help</a>]
+ </li>
<li><span class="separator">| </span><a href="report.cgi">Reports</a></li>
+ [% IF user.settings.skin.value != 'Mozilla' && user.settings.skin.value != 'Mozilla-OpenSans' %]
<li>
[% IF Param('shutdownhtml') || Bugzilla.has_flags %]
<span class="separator">| </span>
@@ -54,7 +56,11 @@
[% END %]
[% END %]
[%-# Work around FF bug: keep this on one line %]</li>
+ [% END %]
+
+ [% Hook.process('action-links') %]
+ [% IF user.settings.skin.value != 'Mozilla' && user.settings.skin.value != 'Mozilla-OpenSans' %]
[% IF user.login %]
<li><span class="separator">| </span><a href="userprefs.cgi">Preferences</a></li>
[% IF user.in_group('tweakparams') || user.in_group('editusers') || user.can_bless
@@ -104,6 +110,7 @@
[% PROCESS "account/auth/login-small.html.tmpl" %]
[% END %]
[% END %]
+ [% END %]
</ul>
[% Hook.process("link-row") %]
diff --git a/template/en/default/global/field-descs.none.tmpl b/template/en/default/global/field-descs.none.tmpl
index 3e86e9bad..6b365b35f 100644
--- a/template/en/default/global/field-descs.none.tmpl
+++ b/template/en/default/global/field-descs.none.tmpl
@@ -49,7 +49,9 @@
"changedto" => "changed to",
"changedby" => "changed by",
"matches" => "matches",
- "notmatches" => "does not match",
+ "notmatches" => "does not match",
+ "isempty" => "is empty",
+ "isnotempty" => "is not empty",
} %]
[% field_types = { ${constants.FIELD_TYPE_UNKNOWN} => "Unknown Type",
@@ -58,7 +60,9 @@
${constants.FIELD_TYPE_MULTI_SELECT} => "Multiple-Selection Box",
${constants.FIELD_TYPE_TEXTAREA} => "Large Text Box",
${constants.FIELD_TYPE_DATETIME} => "Date/Time",
+ ${constants.FIELD_TYPE_DATE} => "Date",
${constants.FIELD_TYPE_BUG_ID} => "$terms.Bug ID",
+ ${constants.FIELD_TYPE_EXTENSION} => "Extension",
} %]
[% IF in_template_var %]
@@ -75,6 +79,7 @@
"alias" => "Alias",
"assigned_to" => "Assignee",
"assigned_to_realname" => "Assignee Real Name",
+ "assignee_last_login" => "Assignee Last Login Date",
"attach_data.thedata" => "Attachment data",
"attachments.description" => "Attachment description",
"attachments.filename" => "Attachment filename",
@@ -107,6 +112,7 @@
"everconfirmed" => "Ever confirmed",
"flagtypes.name" => "Flags",
"keywords" => "Keywords",
+ "last_visit_ts" => "Last Visit",
"longdesc" => "Comment",
"longdescs.count" => "Number of Comments",
"longdescs.isprivate" => "Comment is private",
diff --git a/template/en/default/global/footer.html.tmpl b/template/en/default/global/footer.html.tmpl
index 661f8afe6..29d17bccd 100644
--- a/template/en/default/global/footer.html.tmpl
+++ b/template/en/default/global/footer.html.tmpl
@@ -24,8 +24,6 @@
# global/useful-links.html.tmpl.
#%]
-[% INCLUDE "global/help.html.tmpl" %]
-
</div>
[%# Migration note: below this point, this file corresponds to the old Param
diff --git a/template/en/default/global/header.html.tmpl b/template/en/default/global/header.html.tmpl
index 0dffcb5de..a558d28ed 100644
--- a/template/en/default/global/header.html.tmpl
+++ b/template/en/default/global/header.html.tmpl
@@ -108,49 +108,33 @@
[% PROCESS 'global/setting-descs.none.tmpl' %]
[% SET yui = yui_resolve_deps(yui, yui_deps) %]
- [% SET css_sets = css_files(style_urls, yui, yui_css) %]
-
- [%# CSS cascade, part 1: Standard Bugzilla stylesheet set (persistent).
- # Always present.
- #%]
- [%# This allows people to switch back to the "Classic" skin if they
- # are in another skin.
- #%]
- <link href="[% 'skins/standard/global.css' FILTER mtime FILTER html %]"
- rel="alternate stylesheet"
- title="[% setting_descs.standard FILTER html %]">
- [% FOREACH style_url = css_sets.standard %]
- [% PROCESS format_css_link css_set_name = 'standard' %]
- [% END %]
-
- [%# CSS cascade, part 2 & 3: Third-party stylesheet set (selected and
- # selectable). All third-party skins are present as alternate
- # stylesheets, even if they are not currently in use.
- #%]
- [% FOREACH style_url = css_sets.skin %]
- [% PROCESS format_css_link css_set_name = user.settings.skin.value %]
- [% END %]
- [% FOREACH alternate_skin = css_sets.alternate.keys %]
- [% FOREACH style_url = css_sets.alternate.$alternate_skin %]
- [% PROCESS format_css_link css_set_name = alternate_skin %]
+ [% SET css_sets = css_files(style_urls, yui, yui_css) %]
+ [% IF constants.CONCATENATE_ASSETS %]
+ [% PROCESS format_css_link asset_url = css_sets.unified_standard_skin %]
+ [% ELSE %]
+ [% FOREACH asset_url = css_sets.standard %]
+ [% PROCESS format_css_link %]
+ [% END %]
+ [% FOREACH asset_url = css_sets.skin %]
+ [% PROCESS format_css_link %]
[% END %]
[% END %]
- [%# CSS cascade, part 4: page-specific styles.
- #%]
[% IF style %]
<style type="text/css">
[% style %]
</style>
[% END %]
- [%# CSS cascade, part 5: Custom Bugzilla stylesheet set (persistent).
- # Always present. Site administrators may override all other style
- # definitions, including skins, using custom stylesheets.
- #%]
- [% FOREACH style_url = css_sets.custom %]
- [% PROCESS format_css_link css_set_name = 'standard' %]
+ [% IF css_sets.unified_custom %]
+ [% IF constants.CONCATENATE_ASSETS %]
+ [% PROCESS format_css_link asset_url = css_sets.unified_custom %]
+ [% ELSE %]
+ [% FOREACH asset_rul = css_sets.custom %]
+ [% PROCESS format_css_link %]
+ [% END %]
+ [% END %]
[% END %]
[%# YUI Scripts %]
@@ -159,7 +143,7 @@
[% END %]
[% starting_js_urls.push('js/global.js') %]
- [% FOREACH javascript_url = starting_js_urls %]
+ [% FOREACH asset_url = concatenate_js(starting_js_urls) %]
[% PROCESS format_js_link %]
[% END %]
@@ -226,7 +210,7 @@
// -->
</script>
- [% FOREACH javascript_url = javascript_urls %]
+ [% FOREACH asset_url = concatenate_js(javascript_urls) %]
[% PROCESS format_js_link %]
[% END %]
@@ -239,8 +223,7 @@
[%# Required for the 'Autodiscovery' feature in Firefox 2 and IE 7. %]
<link rel="search" type="application/opensearchdescription+xml"
- title="[% terms.Bugzilla %]" href="./search_plugin.cgi">
- <link rel="shortcut icon" href="images/favicon.ico" >
+ title="[% terms.BugzillaTitle %]" href="./search_plugin.cgi">
[% Hook.process("additional_header") %]
</head>
@@ -250,6 +233,7 @@
<body onload="[% onload %]"
class="[% urlbase.replace('^https?://','').replace('/$','').replace('[-~@:/.]+','-') FILTER css_class_quote %]
+ skin-[% user.settings.skin.value FILTER css_class_quote %]
[% FOREACH class = bodyclasses %]
[% ' ' %][% class FILTER css_class_quote %]
[% END %] yui-skin-sam">
@@ -260,12 +244,95 @@
<div id="header">
+[% IF user.settings.skin.value == 'Mozilla' || user.settings.skin.value == 'Mozilla-OpenSans' %]
+ <div class="wrapper">
+ <table border="0" cellspacing="0" cellpadding="0" id="titles">
+ <tr>
+ <td id="title">
+ <a href="./" title="Home">[% terms.BugzillaTitle %]</a>
+ </td>
+ <td>
+ [% Hook.process("message") %]
+ </td>
+ <td id="moz_login">
+ [% IF user.id %]
+ <ul class="links">
+ <li class="dropdown">
+ <span class="anchor">[% user.login FILTER html %]</span>
+ <ul>
+ [% IF user.showmybugslink %]
+ [% filtered_username = user.login FILTER uri %]
+ <li><a href="[% Param('mybugstemplate').replace('%userid%', filtered_username) %]">My [% terms.Bugs %]</a></li>
+ [% END %]
+ <li><a href="page.cgi?id=mydashboard.html">My Dashboard</a></li>
+ <li><a href="user_profile">My Profile</a></li>
+ <li><a href="page.cgi?id=user_activity.html&amp;action=run&amp;who=[% user.login FILTER uri %]">My Activity</a></li>
+ <li><a href="request.cgi?requester=[% user.login FILTER uri %]&amp;requestee=[% user.login FILTER uri %]&amp;do_union=1&amp;group=type&amp;action=queue">My Requests</a></li>
+ <li><a href="userprefs.cgi">Preferences</a></li>
+ [% IF user.in_group('tweakparams') || user.in_group('editusers') || user.can_bless
+ || (Param('useclassification') && user.in_group('editclassifications'))
+ || user.in_group('editcomponents') || user.in_group('admin') || user.in_group('creategroups')
+ || user.in_group('editkeywords') || user.in_group('bz_canusewhines')
+ || user.get_products_by_permission("editcomponents").size %]
+ <li><a href="admin.cgi">Administration</a></li>
+ [% END %]
+ [% IF user.authorizer.can_logout %]
+ <li><a href="index.cgi?logout=1">Log&nbsp;out</a></li>
+ [% END %]
+ [% IF sudoer %]
+ <li>
+ <a href="relogin.cgi?action=end-sudo">End sudo session impersonating [% user.login FILTER html %]</a>
+ </li>
+ [% END %]
+ </ul>
+ </li>
+ </ul>
+ [% ELSE %]
+ <ul class="login-links">
+ [% IF Param('createemailregexp')
+ && user.authorizer.user_can_create_account %]
+ <li id="moz_new_account_container_top"><a href="createaccount.cgi">New&nbsp;Account</a></li>
+ [% END %]
+
+ [%# Only display one login form when we're on a LOGIN_REQUIRED page. That
+ # way, we're guaranteed that the user will use the form that has
+ # hidden_fields in it (the center form) instead of this one. Also, it's
+ # less confusing to have one form (as opposed to three) when you're
+ # required to log in.
+ #%]
+ [% IF user.authorizer.can_login && !Bugzilla.page_requires_login %]
+ [% PROCESS "account/auth/login-small.html.tmpl" qs_suffix = "_top" %]
+ [% END %]
+ </ul>
+ [% END %]
+ </td>
+ <td id="moz_tab">
+ <a href="https://www.mozilla.org/" title="Mozilla - Home of the Mozilla Project">
+ <img src="skins/contrib/Mozilla/tabzilla.png" border="0" height="42" width="154"></a>
+ </td>
+ </tr>
+ </table>
+
+ [%# display the version number on the index page %]
+ [% IF title == "$terms.Bugzilla Main Page" %]
+ <div id="bugzilla_version">
+ [% header_addl_info FILTER html %]
+ </div>
+ [% END %]
+
+ [% PROCESS "global/common-links.html.tmpl" qs_suffix = "_top" %]
+
+ </div>
+
+[% ELSE %]
+
[% INCLUDE global/banner.html.tmpl %]
<table border="0" cellspacing="0" cellpadding="0" id="titles">
<tr>
<td id="title">
- <p>[% terms.Bugzilla %]
+ <p>[% terms.BugzillaTitle %]
+ [% Hook.process("message") %]
[% " &ndash; $header" IF header %]</p>
</td>
@@ -302,52 +369,34 @@
</td></tr></table>
[% PROCESS "global/common-links.html.tmpl" qs_suffix = "_top" %]
+
+[% END %]
+
</div> [%# header %]
<div id="bugzilla-body">
+[%# in most cases the "header" variable provides redundant information, however
+ # there are exceptions where not displaying this text is problematic. %]
+[% IF (user.settings.skin.value == 'Mozilla' || user.settings.skin.value == 'Mozilla-OpenSans')
+ && template.name.match('^attachment/')
+ && !header.match('^Bug&nbsp;\d+$')
+%]
+ <h2>[% header FILTER none %]</h2>
+[% END %]
+
[% IF Param('announcehtml') %]
[% Param('announcehtml') FILTER none %]
[% END %]
[% IF message %]
-<div id="message">[% message %]</div>
+ <div id="message">[% message %]</div>
[% END %]
[% BLOCK format_css_link %]
- [% IF style_url.match('/IE-fixes\.css') %]
- <!--[if lte IE 7]>
- [%# Internet Explorer treats [if IE] HTML comments as uncommented.
- # We use it to import CSS fixes so that Bugzilla looks decent on IE 7
- # and below.
- #%]
- [% END %]
-
- [% IF css_set_name == 'standard'
- OR css_set_name == user.settings.skin.value
- %]
- [% SET css_rel = 'stylesheet' %]
- [% SET css_set_display_name = setting_descs.${user.settings.skin.value}
- || user.settings.skin.value %]
- [% ELSE %]
- [% SET css_rel = 'alternate stylesheet' %]
- [% SET css_set_display_name = setting_descs.$css_set_name || css_set_name %]
- [% END %]
-
- [% IF css_set_name == 'standard' %]
- [% SET css_title_link = '' %]
- [% ELSE %]
- [% css_title_link = BLOCK ~%]
- title="[% css_set_display_name FILTER html %]"
- [% END %]
- [% END %]
-
- <link href="[% style_url FILTER html %]" rel="[% css_rel FILTER none %]"
- type="text/css" [% css_title_link FILTER none %]>
-
- [% '<![endif]-->' IF style_url.match('/IE-fixes\.css') %]
+ <link href="[% asset_url FILTER html %]" rel="stylesheet" type="text/css">
[% END %]
[% BLOCK format_js_link %]
- <script type="text/javascript" src="[% javascript_url FILTER mtime FILTER html %]"></script>
+ <script type="text/javascript" src="[% asset_url FILTER mtime FILTER html %]"></script>
[% END %]
diff --git a/template/en/default/global/messages.html.tmpl b/template/en/default/global/messages.html.tmpl
index 6cc15ccd8..0f408842f 100644
--- a/template/en/default/global/messages.html.tmpl
+++ b/template/en/default/global/messages.html.tmpl
@@ -652,6 +652,7 @@
[% IF changes.inclusions.defined || changes.exclusions.defined %]
<li>The inclusions and exclusions lists have been updated</li>
[% END %]
+ [% Hook.process('flag_type_updated_fields') %]
</ul>
[% ELSE %]
No changes made to flag type <em>[% flagtype.name FILTER html %]</em>.
diff --git a/template/en/default/global/setting-descs.none.tmpl b/template/en/default/global/setting-descs.none.tmpl
index a0b11f048..5ba100183 100644
--- a/template/en/default/global/setting-descs.none.tmpl
+++ b/template/en/default/global/setting-descs.none.tmpl
@@ -52,7 +52,10 @@
"email_format" => "Preferred email format",
"html" => "HTML",
"text_only" => "Text Only",
- }
+ "bugmail_new_prefix" => "Add 'New:' to subject line of email sent when a new $terms.bug is filed",
+ "possible_duplicates" => "Display possible duplicates when reporting a new $terms.bug",
+ "requestee_cc" => "Automatically add me to the CC list of $terms.bugs I am requested to review",
+ }
%]
[% Hook.process('settings') %]
diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl
index 8de412413..b04deb4d6 100644
--- a/template/en/default/global/user-error.html.tmpl
+++ b/template/en/default/global/user-error.html.tmpl
@@ -160,6 +160,8 @@
use
[% ELSIF action == "approve" %]
approve
+ [% ELSIF action == "admin_activity" %]
+ view admin activity for
[% ELSE %]
[%+ Hook.process('auth_failure_action') %]
[% END %]
@@ -182,6 +184,8 @@
classifications
[% ELSIF object == "components" %]
components
+ [% ELSIF object == "comment_tags" %]
+ comment tags
[% ELSIF object == "custom_fields" %]
custom fields
[% ELSIF object == "field_values" %]
@@ -270,6 +274,8 @@
<li>A ticket in a Trac installation.</li>
<li>A b[% %]ug in a MantisBT installation.</li>
<li>A b[% %]ug on sourceforge.net.</li>
+ <li>An issue on github.com.</li>
+ <li>A question on support.mozilla.org</li>
</ul>
[% ELSIF reason == 'id' %]
There is no valid [% terms.bug %] id in that URL.
@@ -322,6 +328,25 @@
Comments cannot be longer than
[%+ constants.MAX_COMMENT_LENGTH FILTER html %] characters.
+ [% ELSIF error == "comment_tag_disabled" %]
+ [% title = "Comment Tagging Disabled" %]
+ The comment tagging is not enabled.
+
+ [% ELSIF error == "comment_tag_invalid" %]
+ [% title = "Invalid Comment Tag" %]
+ The comment tag "[% tag FILTER html %]" contains invalid characters or
+ words.
+
+ [% ELSIF error == "comment_tag_too_long" %]
+ [% title = "Comment Tag Too Long" %]
+ Comment tags cannot be longer than
+ [%+ constants.MAX_COMMENT_TAG_LENGTH FILTER html %] characters.
+
+ [% ELSIF error == "comment_tag_too_short" %]
+ [% title = "Comment Tag Too Short" %]
+ Comment tags must be at least
+ [%+ constants.MIN_COMMENT_TAG_LENGTH FILTER html %] characters.
+
[% ELSIF error == "auth_classification_not_enabled" %]
[% title = "Classification Not Enabled" %]
Sorry, classification is not enabled.
@@ -469,6 +494,15 @@
The first letter of your extension's name must be a capital letter.
(You specified '[% name FILTER html %]'.)
+ [% ELSIF error == "feature_disabled" %]
+ The [% install_string("feature_$feature") FILTER html %] feature is not
+ available in this [% terms.Bugzilla %].
+ [% IF user.in_group('admin') %]
+ If you would like to enable this feature, please run
+ <kbd>checksetup.pl</kbd> to see how to install the necessary
+ requirements for this feature.
+ [% END %]
+
[% ELSIF error == "field_already_exists" %]
[% title = "Field Already Exists" %]
The field '[% field.name FILTER html %]'
@@ -618,6 +652,11 @@
<br>Alternately, if your attachment is an image, you could convert
it to a compressible format like JPG or PNG and try again.
+ [% ELSIF error == "flag_requestee_disabled" %]
+ [% title = "Flag Requestee Disabled" %]
+ You can't ask <em>[% requestee.identity FILTER html %]</em> because that
+ account is disabled.
+
[% ELSIF error == "flag_requestee_needs_privs" %]
[% title = "Flag Requestee Needs Privileges" %]
[% requestee.identity FILTER html %] does not have permission to set the
@@ -699,6 +738,16 @@
You are not allowed to edit properties of the '[% flagtype.name FILTER html %]'
flag type, because this flag type is not available for the products you can administer.
+ [% ELSIF error == "flag_not_unique" %]
+ [% title = "Flag not Unique" %]
+ The flag '[% value FILTER html %]' has been set multiple times.
+ You must specify the id value to update the flag.
+
+ [% ELSIF error == "flag_type_not_unique" %]
+ [% title = "Flag Type not Unique" %]
+ The flag type '[% value FILTER html %]' matches several flag types.
+ You must specify the type id value to update or add a flag.
+
[% ELSIF error == "flag_type_not_multiplicable" %]
[% docslinks = {'flags-overview.html' => 'An overview on Flags',
'flags.html' => 'Using Flags'} %]
@@ -943,6 +992,10 @@
Invalid datasets <em>[% datasets.join(":") FILTER html %]</em>. Only digits,
letters and colons are allowed.
+ [% ELSIF error == "invalid_flag_id" %]
+ [% title = "Invalid Flag ID" %]
+ The flag id [% flag_id FILTER html %] is invalid.
+
[% ELSIF error == "invalid_format" %]
[% title = "Invalid Format" %]
The format "[% format FILTER html %]" is invalid (must be one of
@@ -1013,6 +1066,11 @@
[%+ constants.LOGIN_LOCKOUT_INTERVAL FILTER html %] minutes.
[% END %]
+ [% ELSIF error == "invalid_cookies_or_token" %]
+ [% title = "Invalid Cookies or Token" %]
+ The cookies or token provide were not valid or have expired.
+ You may login again to get new cookies or a new token.
+
[% ELSIF error == "json_rpc_get_method_required" %]
When using JSON-RPC over GET, you must specify a 'method'
parameter. See the documentation at
@@ -1037,6 +1095,13 @@
For security reasons, you must use HTTP POST to call the
'[% method FILTER html %]' method.
+ [% ELSIF error == "rest_invalid_resource" %]
+ A REST API resource was not found for '[% method FILTER html +%] [%+ path FILTER html %]'.
+
+ [% ELSIF error == "get_products_invalid_type" %]
+ The product type '[% type FILTER html %]' is invalid. Valid choices
+ are 'accessible', 'selectable', and 'enterable'.
+
[% ELSIF error == "keyword_already_exists" %]
[% title = "Keyword Already Exists" %]
A keyword with the name [% name FILTER html %] already exists.
@@ -1350,6 +1415,40 @@
[% END %]
</ul>
+ [% ELSIF error == "password_not_complex" %]
+ [% title = "Password Fails Requirements" %]
+ [% passregex = Param('password_complexity') %]
+ Password must contain at least one:
+ <ul>
+ [% IF passregex.search('letters') %]
+ <li>UPPERCASE letter</li>
+ <li>lowercase letter</li>
+ [% END %]
+ [% IF passregex.search('numbers') %]
+ <li>digit</li>
+ [% END %]
+ [% IF passregex.search('specialchars') %]
+ <li>special character</li>
+ [% END %]
+ </ul>
+
+ [% ELSIF error == "password_not_complex" %]
+ [% title = "Password Fails Requirements" %]
+ [% passregex = Param('password_complexity') %]
+ Password must contain at least one:
+ <ul>
+ [% IF passregex.search('letters') %]
+ <li>UPPERCASE letter</li>
+ <li>lowercase letter</li>
+ [% END %]
+ [% IF passregex.search('numbers') %]
+ <li>digit</li>
+ [% END %]
+ [% IF passregex.search('specialchars') %]
+ <li>special character</li>
+ [% END %]
+ </ul>
+
[% ELSIF error == "product_access_denied" %]
[% title = "Product Access Denied" %]
Either the product
@@ -1538,6 +1637,17 @@
and the "matches" search can only be used with the "content"
field.
+ [% ELSIF error == "search_grouped_field_invalid" %]
+ [% terms.Bugzilla %] does not support using the
+ "[%+ field_descs.$field FILTER html %]" ([% field FILTER html %])
+ field with grouped search conditions.
+
+ [% ELSIF error == "search_grouped_invalid_nesting" %]
+ You cannot nest clauses within grouped search conditions.
+
+ [% ELSIF error == "search_grouped_field_mismatch" %]
+ All conditions under a groups search must use the same field.
+
[% ELSIF error == "search_field_operator_invalid" %]
[% terms.Bugzilla %] does not support using the
"[%+ field_descs.$field FILTER html %]" ([% field FILTER html %])
@@ -1610,7 +1720,7 @@
[% ELSIF error == "tag_name_too_long" %]
[% title = "Tag Name Too Long" %]
- The tag name must be less than [% constants.MAX_LEN_QUERY_NAME FILTER html %]
+ The tag must be less than [% constants.MAX_LEN_QUERY_NAME FILTER html %]
characters long.
[% ELSIF error == "token_does_not_exist" %]
@@ -1707,6 +1817,16 @@
Sorry, but you are not allowed to (un)mark comments or attachments
as private.
+ [% ELSIF error == "user_not_involved" %]
+ [% title = "User Not Involved with $terms.Bug" %]
+ Sorry, but you are not involved with [% terms.Bug %] [%+
+ bug_id FILTER bug_link(bug_id) FILTER none %].
+
+ [% ELSIF error == "webdot_too_large" %]
+ [% title = "Dependency Graph Too Large" %]
+ The dependency graph contains too many [% terms.bugs %] to display (more
+ than [% constants.MAX_WEBDOT_BUGS FILTER html %] [%+ terms.bugs %]).
+
[% ELSIF error == "wrong_token_for_cancelling_email_change" %]
[% title = "Wrong Token" %]
That token cannot be used to cancel an email address change.
@@ -1764,6 +1884,8 @@
[% error_message FILTER none %]
[% END %]
[% END %]
+
+ [% Hook.process('error_message') %]
[% END %]
[%# We only want HTML error messages for ERROR_MODE_WEBPAGE %]
diff --git a/template/en/default/global/user.html.tmpl b/template/en/default/global/user.html.tmpl
index df902b451..38c1da113 100644
--- a/template/en/default/global/user.html.tmpl
+++ b/template/en/default/global/user.html.tmpl
@@ -27,12 +27,19 @@
[% FILTER collapse %]
[% IF user.id %]
<a class="email" href="mailto:[% who.email FILTER html %]"
- title="[% who.identity FILTER html %]">
+ onclick="return show_usermenu(event, [% who.id FILTER none %], '[% who.email FILTER js %]',
+ [% user.in_group('editusers') || user.bless_groups.size > 0 ? "true" : "false" %]);"
+ title="[% who.identity FILTER html %]">
[%- END -%]
- [% IF who.name %]
- <span class="fn">[% who.name FILTER html %]</span>
- [% ELSE %]
- [% who.login FILTER email FILTER html %]
+ [% IF who %]
+ [% IF who.name %]
+ <span class="fn">[% who.name FILTER html %]</span>
+ [% ELSE %]
+ <span class="ln">[% who.login FILTER email FILTER html %]</span>
+ [% END %]
+ [% IF user.id && who.id %]
+ <span class="arrow_container"><span class="arrow_down"></span></span>
+ [% END %]
[% END %]
[% '</a>' IF user.id %]
[% END %]
diff --git a/template/en/default/global/userselect.html.tmpl b/template/en/default/global/userselect.html.tmpl
index 1d0395043..d7b4786f9 100644
--- a/template/en/default/global/userselect.html.tmpl
+++ b/template/en/default/global/userselect.html.tmpl
@@ -30,6 +30,7 @@
# multiple: optional, do multiselect box, value is size (height) of box
# custom_userlist: optional, specify a limited list of users to use
# field_title: optional, extra information to display as a tooltip
+ # placeholder: optional, input only; placeholder attribute value
#%]
[% IF Param("usemenuforusers") %]
@@ -92,6 +93,7 @@
[% IF accesskey %] accesskey="[% accesskey FILTER html %]" [% END %]
[% IF field_title %] title="[% field_title FILTER html %]" [% END %]
[% IF size %] size="[% size FILTER html %]" [% END %]
+ [% IF placeholder %] placeholder="[% placeholder FILTER html %]" [% END %]
[% IF id %] id="[% id FILTER html %]" [% END %]
>
[% IF feature_enabled('jsonrpc') && Param('ajax_user_autocompletion') && id %]
diff --git a/template/en/default/index.html.tmpl b/template/en/default/index.html.tmpl
index 5b9237aa1..fa2a4d126 100644
--- a/template/en/default/index.html.tmpl
+++ b/template/en/default/index.html.tmpl
@@ -38,40 +38,13 @@
<script type="text/javascript">
-<!--
-function onLoadActions() {
- quicksearchHelpText('quicksearch_main', 'show');
- if( window.external.AddSearchProvider ){
- YAHOO.util.Dom.removeClass('quicksearch_plugin', 'bz_default_hidden');
- }
- document.getElementById('quicksearch_top').focus();
-}
-var quicksearch_message = "Enter [% terms.abug %] # or some search terms";
-
function checkQuicksearch( form ) {
- if (form.quicksearch.value == '' || form.quicksearch.value == quicksearch_message ) {
+ if (form.quicksearch.value == '') {
alert('Please enter one or more search terms first.');
- return false;
+ return false;
}
- return true;
+ return true;
}
-
-function quicksearchHelpText(el_id, action){
- var el = document.getElementById(el_id);
- if ( action == "show") {
- if( el.value == "" ) {
- el.value = quicksearch_message
- YAHOO.util.Dom.addClass(el, "quicksearch_help_text");
- }
- } else {
- if( el.value == quicksearch_message ) {
- el.value = "";
- YAHOO.util.Dom.removeClass(el, "quicksearch_help_text");
- }
- }
-}
-YAHOO.util.Event.onDOMReady(onLoadActions);
-//-->
</script>
[% IF release %]
@@ -125,39 +98,27 @@ YAHOO.util.Event.onDOMReady(onLoadActions);
<td>
<h1 id="welcome"> Welcome to [% terms.Bugzilla %]</h1>
<div class="intro">[% Hook.process('intro') %]</div>
-
- <div class="bz_common_actions">
- <ul>
- <li>
- <a id="enter_bug" href="enter_bug.cgi"><span>File
- [%= terms.aBug %]</span></a>
- </li>
- <li>
- <a id="query" href="query.cgi"><span>Search</span></a>
- </li>
- <li>
- <a id="account"
- [% IF user.id %]
- href="userprefs.cgi"><span>User Preferences</span></a>
- [% ELSIF Param('createemailregexp')
- && user.authorizer.user_can_create_account
- %]
- href="createaccount.cgi"><span>Open a New Account</span></a>
- [% ELSE %]
- href="?GoAheadAndLogIn=1"><span>Log In</span></a>
- [% END %]
- </li>
- </ul>
- </div>
+ <a id="enter_bug" class="bz_common_actions"
+ href="enter_bug.cgi"><span>File [% terms.aBug %]</span></a>
+ <a id="query" class="bz_common_actions"
+ href="query.cgi"><span>Search</span></a>
+ <a id="account" class="bz_common_actions"
+ [% IF user.id %]
+ href="userprefs.cgi"><span>User Preferences</span></a>
+ [% ELSIF Param('createemailregexp')
+ && user.authorizer.user_can_create_account
+ %]
+ href="createaccount.cgi"><span>Open a New Account</span></a>
+ [% ELSE %]
+ href="?GoAheadAndLogIn=1"><span>Log In</span></a>
+ [% END %]
<form id="quicksearchForm" name="quicksearchForm" action="buglist.cgi"
onsubmit="return checkQuicksearch(this);">
<div>
<input id="quicksearch_main" type="text" name="quicksearch"
- title="Quick Search"
- onfocus="quicksearchHelpText(this.id, 'hide');"
- onblur="quicksearchHelpText(this.id, 'show');"
- >
+ placeholder="Enter [% terms.abug %] number or some search terms"
+ title="Quick Search">
<input id="find" type="submit" value="Quick Search">
<ul class="additional_links" id="quicksearch_links">
<li>
diff --git a/template/en/default/list/change-columns.html.tmpl b/template/en/default/list/change-columns.html.tmpl
index ff7e5d371..c5e02d684 100644
--- a/template/en/default/list/change-columns.html.tmpl
+++ b/template/en/default/list/change-columns.html.tmpl
@@ -23,7 +23,7 @@
[% PROCESS global/header.html.tmpl
title = "Change Columns"
- javascript_urls = "js/change-columns.js"
+ javascript_urls = [ "js/change-columns.js" ]
onload = "initChangeColumns()"
%]
diff --git a/template/en/default/list/edit-multiple.html.tmpl b/template/en/default/list/edit-multiple.html.tmpl
index 92e578e8f..8a1de5f2d 100644
--- a/template/en/default/list/edit-multiple.html.tmpl
+++ b/template/en/default/list/edit-multiple.html.tmpl
@@ -282,10 +282,20 @@
[% USE Bugzilla %]
[%# Show all legal values and all fields, ignoring visibility controls. %]
- [% bug = 0 %]
- [% FOREACH field = Bugzilla.active_custom_fields %]
+ [% bug = default.defined ? default : 0 %]
+ [% custom_fields = [] %]
+ [% IF one_product.defined %]
+ [% custom_fields = Bugzilla.active_custom_fields(product=>one_product) %]
+ [% ELSE %]
+ [% custom_fields = Bugzilla.active_custom_fields %]
+ [% END %]
+ [% FOREACH field = custom_fields %]
+ [%# BMO hook for controlling field visibility %]
+ [% Hook.process('custom_field', 'list/edit-multiple.html.tmpl', ) %]
+ [% NEXT IF field.hidden %]
<tr>
- [% PROCESS bug/field.html.tmpl value = dontchange
+ [% PROCESS bug/field.html.tmpl bug = bug
+ value = dontchange
editable = 1
allow_dont_change = 1 %]
</tr>
@@ -427,6 +437,7 @@
[% FOREACH r = resolutions %]
[% NEXT IF !r %]
[% NEXT IF r == "DUPLICATE" || r == "MOVED" %]
+ [% NEXT IF r == "EXPIRED" AND user.login != "gerv@mozilla.org" %]
<option value="[% r FILTER html %]">[% display_value("resolution", r) FILTER html %]</option>
[% END %]
</select>
diff --git a/template/en/default/list/list.html.tmpl b/template/en/default/list/list.html.tmpl
index 4eeff5e64..e7b635395 100644
--- a/template/en/default/list/list.html.tmpl
+++ b/template/en/default/list/list.html.tmpl
@@ -42,10 +42,11 @@
[%# Page Header #%]
[%############################################################################%]
+[% url_filtered_title = title FILTER uri %]
[% PROCESS global/header.html.tmpl
title = title
style = style
- atomlink = "buglist.cgi?$urlquerypart&title=$title&ctype=atom"
+ atomlink = "buglist.cgi?$urlquerypart&title=$url_filtered_title&ctype=atom"
yui = [ 'autocomplete', 'calendar' ]
javascript_urls = [ "js/util.js", "js/field.js" ]
style_urls = [ "skins/standard/buglist.css" ]
@@ -58,10 +59,16 @@
</span>
[% IF debug %]
- <p class="bz_query">[% query FILTER html %]</p>
- [% IF query_explain.defined %]
- <pre class="bz_query_explain">[% query_explain FILTER html %]</pre>
- [% END %]
+ <div class="bz_query_debug">
+ <p>Total execution time: [% query_time FILTER html %] seconds</p>
+ [% FOREACH query = queries %]
+ <p>[% query.sql FILTER html %]</p>
+ <p>Execution time: [% query.time FILTER html %] seconds</p>
+ [% IF query.explain %]
+ <pre>[% query.explain FILTER html %]</pre>
+ [% END %]
+ [% END %]
+ </div>
[% END %]
[% IF user.settings.display_quips.value == 'on' %]
@@ -84,7 +91,7 @@
'notequals', 'regexp', 'notregexp', 'lessthan', 'lessthaneq',
'greaterthan', 'greaterthaneq', 'changedbefore', 'changedafter',
'changedfrom', 'changedto', 'changedby', 'notsubstring', 'nowords',
- 'nowordssubstr', 'notmatches',
+ 'nowordssubstr', 'notmatches', 'isempty', 'isnotempty'
] %]
<ul class="search_description">
[% FOREACH desc_item = search_description %]
@@ -201,31 +208,27 @@
<td>&nbsp;</td>
<td valign="middle" class="bz_query_links">
+ [%# Links to more things users can do with this bug list. %]
+ [% Hook.process("links") %]
+
<a href="buglist.cgi?
[% urlquerypart FILTER html %]&amp;ctype=csv&amp;human=1">CSV</a> |
<a href="buglist.cgi?
[% urlquerypart FILTER html %]&amp;title=
- [%- title FILTER html %]&amp;ctype=atom">Feed</a> |
+ [%- title FILTER uri %]&amp;ctype=atom">Feed</a> |
<a href="buglist.cgi?
- [% urlquerypart FILTER html %]&amp;ctype=ics">iCalendar</a> |
+ [% urlquerypart FILTER html %]&amp;ctype=ics">iCalendar</a>
+
+ <br>
<a href="colchange.cgi?
[% urlquerypart FILTER html %]&amp;query_based_on=
- [% defaultsavename OR searchname FILTER uri %]">Change&nbsp;Columns</a> |
+ [% defaultsavename OR searchname FILTER uri %]">Change&nbsp;Columns</a>
[% IF bugs.size > 1 && caneditbugs && !dotweak %]
- <a href="buglist.cgi?[% urlquerypart FILTER html %]
+ | <a href="buglist.cgi?[% urlquerypart FILTER html %]
[%- "&order=$qorder" FILTER html IF order %]&amp;tweak=1"
>Change&nbsp;Several&nbsp;[% terms.Bugs %]&nbsp;at&nbsp;Once</a>
- |
[% END %]
-
- [% IF bugowners && user.id %]
- <a href="mailto:
- [% bugowners FILTER html %]">Send&nbsp;Mail&nbsp;to&nbsp;[% terms.Bug %]&nbsp;Assignees</a> |
- [% END %]
-
- [%# Links to more things users can do with this bug list. %]
- [% Hook.process("links") %]
</td>
[% END %]
diff --git a/template/en/default/list/table.html.tmpl b/template/en/default/list/table.html.tmpl
index a074fcbd0..b1ab82a41 100644
--- a/template/en/default/list/table.html.tmpl
+++ b/template/en/default/list/table.html.tmpl
@@ -42,6 +42,7 @@
[% field_descs.reporter_realname = field_descs.reporter %]
[% field_descs.qa_contact_realname = field_descs.qa_contact %]
+[%# Setting maxlength => 0 means no limit. We set it for performance reasons. %]
[% abbrev =
{
"bug_severity" => { maxlength => 3 , title => "Sev" } ,
@@ -55,19 +56,21 @@
"qa_contact" => { maxlength => 30 , ellipsis => "..." , title => "QAContact" } ,
"qa_contact_realname" => { maxlength => 20 , ellipsis => "..." , title => "QAContact" } ,
"resolution" => { maxlength => 4 } ,
- "short_desc" => { wrap => 1 } ,
+ "short_desc" => { maxlength => 0, wrap => 1 } ,
"short_short_desc" => { maxlength => 60 , ellipsis => "..." , wrap => 1 } ,
- "status_whiteboard" => { title => "Whiteboard" , wrap => 1 } ,
- "keywords" => { wrap => 1 } ,
- "flagtypes.name" => { wrap => 1 } ,
+ "status_whiteboard" => { maxlength => 0, title => "Whiteboard" , wrap => 1 } ,
+ "keywords" => { maxlength => 0, wrap => 1 } ,
+ "dependson" => { maxlength => 0, wrap => 1 } ,
+ "blocked" => { maxlength => 0, wrap => 1 } ,
+ "flagtypes.name" => { maxlength => 0, wrap => 1 } ,
"component" => { maxlength => 8 , title => "Comp" } ,
"product" => { maxlength => 8 } ,
"version" => { maxlength => 5 , title => "Vers" } ,
"op_sys" => { maxlength => 4 } ,
"bug_file_loc" => { maxlength => 30 } ,
- "target_milestone" => { title => "TargetM" } ,
- "longdescs.count" => { title => "# Comments" },
- "percentage_complete" => { format_value => "%d %%" } ,
+ "target_milestone" => { maxlength => 0, title => "TargetM" } ,
+ "longdescs.count" => { maxlength => 0, title => "# Comments" },
+ "percentage_complete" => { maxlength => 0, format_value => "%d %%" } ,
}
%]
@@ -80,12 +83,14 @@
[%############################################################################%]
[% tableheader = BLOCK %]
- <table class="bz_buglist" cellspacing="0" cellpadding="4" width="100%">
+ <table class="bz_buglist sortable" cellspacing="0" cellpadding="4" width="100%">
+ <thead>
<tr class="bz_buglist_header bz_first_buglist_header">
[% IF dotweak %]
- <th>&nbsp;</th>
+ <th class="sorttable_nosort">&nbsp;</th>
[% END %]
- <th colspan="[% splitheader ? 2 : 1 %]" class="first-child">
+ <th colspan="[% splitheader ? 2 : 1 %]" class="first-child
+ sorted_[% lsearch(order_columns, 'bug_id') FILTER html %]">
<a href="buglist.cgi?
[% urlquerypart FILTER html %]&amp;order=
[% PROCESS new_order id='bug_id' %]
@@ -100,7 +105,7 @@
[% FOREACH id = displaycolumns %]
[% NEXT UNLESS loop.count() % 2 == 0 %]
[% column = columns.$id %]
- [% PROCESS columnheader %]
+ [% PROCESS columnheader key=loop.count() %]
[% END %]
</tr><tr class="bz_buglist_header">
@@ -112,7 +117,7 @@
[% FOREACH id = displaycolumns %]
[% NEXT IF loop.count() % 2 == 0 %]
[% column = columns.$id %]
- [% PROCESS columnheader %]
+ [% PROCESS columnheader key=loop.count() %]
[% END %]
[% ELSE %]
@@ -125,10 +130,13 @@
[% END %]
</tr>
+ </thead>
[% END %]
[% BLOCK columnheader %]
- <th colspan="[% splitheader ? 2 : 1 %]">
+ <th colspan="[% splitheader ? 2 : 1 %]"
+ class="sortable_column_[% key FILTER html %]
+ sorted_[% lsearch(order_columns, id) FILTER html %]">
<a href="buglist.cgi?[% urlquerypart FILTER html %]&amp;order=
[% PROCESS new_order %]
[%-#%]&amp;query_based_on=
@@ -151,13 +159,13 @@
[% END %]
[% BLOCK order_arrow %]
- [% IF order.match("^$id DESC") %]
+ [% IF order.search("^$id DESC") %]
<span class="bz_sort_order_primary">&#x25BC;</span>
- [% ELSIF order.match("^$id(,\\s*|\$)") %]
+ [% ELSIF order.search("^$id(,\\s*|\$)") %]
<span class="bz_sort_order_primary">&#x25B2;</span>
- [% ELSIF order.match("\\b$id DESC") %]
+ [% ELSIF order.search("\\b$id DESC") %]
<span class="bz_sort_order_secondary">&#x25BC;</span>
- [% ELSIF order.match("\\b$id(,\\s*|\$)") %]
+ [% ELSIF order.search("\\b$id(,\\s*|\$)") %]
<span class="bz_sort_order_secondary">&#x25B2;</span>
[% END %]
[% END %]
@@ -168,6 +176,7 @@
[% tableheader %]
+<tbody class="sorttable_body">
[% FOREACH bug = bugs %]
[% count = loop.count() %]
@@ -192,13 +201,24 @@
</td>
[% FOREACH column = displaycolumns %]
- <td [% 'style="white-space: nowrap"' IF NOT abbrev.$column.wrap %]
- class="bz_[% column FILTER css_class_quote %]_column">
- [% IF abbrev.$column.maxlength %]
+ [% col_abbrev = abbrev.$column %]
+ <td [% 'style="white-space: nowrap"' IF NOT col_abbrev.wrap %]
+ class="bz_[% column FILTER css_class_quote %]_column"
+ [% SWITCH column %]
+ [% CASE 'opendate' %]
+ sorttable_customkey="[% bug.opentime FILTER html %]"
+ [% CASE 'changeddate' %]
+ sorttable_customkey="[% bug.changedtime FILTER html %]"
+ [% CASE columns_sortkey.keys %]
+ [% SET sortkey = columns_sortkey.$column.${bug.$column} %]
+ sorttable_customkey="[% sortkey FILTER html %]"
+ [% END %]
+ >
+ [% IF col_abbrev.maxlength %]
<span title="[%- display_value(column, bug.$column) FILTER html %]">
[% END %]
- [% IF abbrev.$column.format_value %]
- [%- bug.$column FILTER format(abbrev.$column.format_value) FILTER html -%]
+ [% IF col_abbrev.format_value %]
+ [%- bug.$column FILTER format(col_abbrev.format_value) FILTER html -%]
[% ELSIF column == 'actual_time' ||
column == 'remaining_time' ||
column == 'estimated_time' %]
@@ -206,16 +226,20 @@
[%# Display the login name of the user if their real name is empty. %]
[% ELSIF column.match('_realname$') && bug.$column == '' %]
[% SET login_column = column.remove('_realname$') %]
- [% bug.${login_column}.truncate(abbrev.$column.maxlength,
- abbrev.$column.ellipsis) FILTER html %]
+ [% bug.${login_column}.truncate(col_abbrev.maxlength,
+ col_abbrev.ellipsis) FILTER html %]
[% ELSIF column == 'short_desc' || column == "short_short_desc" %]
<a href="show_bug.cgi?id=[% bug.bug_id FILTER html %]">
- [%- bug.$column.truncate(abbrev.$column.maxlength, abbrev.$column.ellipsis) FILTER html -%]
+ [%- bug.$column.truncate(col_abbrev.maxlength, col_abbrev.ellipsis) FILTER html -%]
+ </a>
+ [% ELSIF bug_fields.$column.type == constants.FIELD_TYPE_BUG_ID %]
+ <a href="show_bug.cgi?id=[% bug.$column FILTER html %]">
+ [%- bug.$column.truncate(col_abbrev.maxlength, col_abbrev.ellipsis) FILTER html -%]
</a>
[% ELSE %]
- [%- display_value(column, bug.$column).truncate(abbrev.$column.maxlength, abbrev.$column.ellipsis) FILTER html -%]
+ [%- display_value(column, bug.$column).truncate(col_abbrev.maxlength, col_abbrev.ellipsis) FILTER html -%]
[% END %]
- [% IF abbrev.$column.maxlength %]
+ [% IF col_abbrev.maxlength %]
</span>
[% END %]
</td>
@@ -223,11 +247,12 @@
</tr>
- [% IF loop.last() && time_info.time_present == 1 %]
+ [% IF time_info.time_present %]
[% PROCESS time_summary_line %]
[% END %]
[% END %]
+</tbody>
</table>
diff --git a/template/en/default/pages/bugzilla.dtd.tmpl b/template/en/default/pages/bugzilla.dtd.tmpl
new file mode 100644
index 000000000..f7fc1b4ad
--- /dev/null
+++ b/template/en/default/pages/bugzilla.dtd.tmpl
@@ -0,0 +1,179 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Dawn Endico <endico@mozilla.org>
+ # Dave Miller <justdave@syndicomm.com>
+ # Bradley Baetz <bbaetz@student.usyd.edu.au>
+ # Myk Mylez <myk@mozilla.org>
+ # Colin Ogilvie <mozilla@colinogilvie.co.uk>
+ # Joel Peshkin <bugreport@peshkin.net>
+ # Frédéric Buclin <LpSolit@gmail.com>
+ # Gervase Markham <gerv@gerv.net>
+ # Max Kanat-Alexander <mkanat@bugzilla.org>
+ # David Lawrence <dkl@mozilla.com>
+ #
+ #%]
+[% USE Bugzilla %]
+<!ELEMENT [% "bugzilla" %] (bug+)>
+<!ATTLIST [% "bugzilla" %]
+ version CDATA #REQUIRED
+ urlbase CDATA #REQUIRED
+ maintainer CDATA #REQUIRED
+ exporter CDATA #IMPLIED
+>
+<!ELEMENT [% "bug" %] (bug_id,
+ (alias?,
+ creation_ts,
+ short_desc,
+ delta_ts,
+ reporter_accessible,
+ cclist_accessible,
+ classification_id,
+ classification,
+ product,
+ component,
+ version,
+ rep_platform,
+ op_sys,
+ bug_status,
+ resolution?,
+ dup_id?,
+ see_also*,
+ bug_file_loc?,
+ status_whiteboard?,
+ keywords*,
+ priority,
+ bug_severity,
+ target_milestone?,
+ dependson*,
+ blocked*,
+ everconfirmed,
+ reporter,
+ assigned_to,
+ cc*,
+ (estimated_time,
+ remaining_time,
+ actual_time,
+ deadline?)?,
+ qa_contact?,
+[% FOREACH field = Bugzilla.active_custom_fields %]
+ [%+ field.name FILTER xml -%]
+ [%- IF field.type == constants.FIELD_TYPE_MULTI_SELECT %]*[% ELSE %]?[% END %],
+[% END %]
+ votes?,
+ token?,
+ group*,
+ flag*,
+ long_desc*,
+ attachment*)?)>
+<!ATTLIST [% "bug" %]
+ error (NotFound | NotPermitted | InvalidBugId) #IMPLIED
+>
+<!ELEMENT bug_id (#PCDATA)>
+<!ELEMENT alias (#PCDATA)>
+<!ELEMENT reporter_accessible (#PCDATA)>
+<!ELEMENT cclist_accessible (#PCDATA)>
+<!ELEMENT exporter (#PCDATA)>
+<!ELEMENT urlbase (#PCDATA)>
+<!ELEMENT bug_status (#PCDATA)>
+<!ELEMENT classification_id (#PCDATA)>
+<!ELEMENT classification (#PCDATA)>
+<!ELEMENT product (#PCDATA)>
+<!ELEMENT priority (#PCDATA)>
+<!ELEMENT version (#PCDATA)>
+<!ELEMENT rep_platform (#PCDATA)>
+<!ELEMENT assigned_to (#PCDATA)>
+<!ATTLIST assigned_to
+ name CDATA #REQUIRED
+>
+<!ELEMENT delta_ts (#PCDATA)>
+<!ELEMENT component (#PCDATA)>
+<!ELEMENT reporter (#PCDATA)>
+<!ATTLIST reporter
+ name CDATA #REQUIRED
+>
+<!ELEMENT target_milestone (#PCDATA)>
+<!ELEMENT bug_severity (#PCDATA)>
+<!ELEMENT creation_ts (#PCDATA)>
+<!ELEMENT qa_contact (#PCDATA)>
+<!ATTLIST qa_contact
+ name CDATA #REQUIRED
+>
+<!ELEMENT status_whiteboard (#PCDATA)>
+<!ELEMENT op_sys (#PCDATA)>
+<!ELEMENT resolution (#PCDATA)>
+<!ELEMENT dup_id (#PCDATA)>
+<!ELEMENT bug_file_loc (#PCDATA)>
+<!ELEMENT short_desc (#PCDATA)>
+<!ELEMENT keywords (#PCDATA)>
+<!ELEMENT dependson (#PCDATA)>
+<!ELEMENT blocked (#PCDATA)>
+<!ELEMENT everconfirmed (#PCDATA)>
+<!ELEMENT cc (#PCDATA)>
+<!ELEMENT see_also (#PCDATA)>
+<!ELEMENT votes (#PCDATA)>
+<!ELEMENT token (#PCDATA)>
+<!ELEMENT group (#PCDATA)>
+<!ATTLIST group
+ id CDATA #REQUIRED
+>
+<!ELEMENT estimated_time (#PCDATA)>
+<!ELEMENT remaining_time (#PCDATA)>
+<!ELEMENT actual_time (#PCDATA)>
+<!ELEMENT deadline (#PCDATA)>
+[% FOREACH field = Bugzilla.active_custom_fields %]
+<!ELEMENT [% field.name FILTER xml %] (#PCDATA)>
+[% END %]
+<!ELEMENT long_desc (commentid, attachid?, who, bug_when, work_time?, thetext)>
+<!ATTLIST long_desc
+ isprivate (0|1) #REQUIRED
+>
+<!ELEMENT commentid (#PCDATA)>
+<!ELEMENT who (#PCDATA)>
+<!ATTLIST who
+ name CDATA #REQUIRED
+>
+<!ELEMENT bug_when (#PCDATA)>
+<!ELEMENT work_time (#PCDATA)>
+<!ELEMENT thetext (#PCDATA)>
+<!ELEMENT attachment (attachid, date, delta_ts, desc, filename, type, size, attacher, token?, data?, flag*)>
+<!ATTLIST attachment
+ isobsolete (0|1) #REQUIRED
+ ispatch (0|1) #REQUIRED
+ isprivate (0|1) #REQUIRED
+ isurl (0|1) #REQUIRED
+>
+<!ELEMENT attacher (#PCDATA)>
+<!ELEMENT attachid (#PCDATA)>
+<!ELEMENT date (#PCDATA)>
+<!ELEMENT desc (#PCDATA)>
+<!ELEMENT filename (#PCDATA)>
+<!ELEMENT type (#PCDATA)>
+<!ELEMENT size (#PCDATA)>
+<!ELEMENT data (#PCDATA)>
+<!ATTLIST data
+ encoding (base64) #IMPLIED
+>
+<!ELEMENT flag EMPTY>
+<!ATTLIST flag
+ name CDATA #REQUIRED
+ id CDATA #REQUIRED
+ type_id CDATA #REQUIRED
+ status CDATA #REQUIRED
+ setter CDATA #REQUIRED
+ requestee CDATA #IMPLIED
+>
diff --git a/template/en/default/pages/fields.html.tmpl b/template/en/default/pages/fields.html.tmpl
index 2794e1cc4..8c763d73f 100644
--- a/template/en/default/pages/fields.html.tmpl
+++ b/template/en/default/pages/fields.html.tmpl
@@ -62,34 +62,41 @@
</dt>
<dd class="unconfirmed">
This [% terms.bug %] has recently been added to the database.
- Nobody has confirmed that this [% terms.bug %] is valid. Users
+ Nobody has validated that this [% terms.bug %] is true. Users
who have the "canconfirm" permission set may confirm
- this [% terms.bug %], changing its state to
- <b>[% display_value("bug_status", "CONFIRMED") FILTER html %]</b>.
- Or, it may be directly resolved and marked
+ this [% terms.bug %], changing its state to [% display_value("bug_status", "NEW") FILTER html %]. Or, it may be
+ directly resolved and marked [% display_value("bug_status", "RESOLVED") FILTER html %].
+ </dd>
+ <dt>
+ <b>[% display_value("bug_status", "NEW") FILTER html %]</b>
+ </dt>
+ <dd>
+ This [% terms.bug %] has recently been added to the assignee's
+ list of [% terms.bugs %] and must be processed. [% terms.Bugs %] in
+ this state may be accepted, and become <b>[% display_value("bug_status", "ASSIGNED") FILTER html %]</b>, passed
+ on to someone else, and remain <b>[% display_value("bug_status", "NEW") FILTER html %]</b>, or resolved and marked
<b>[% display_value("bug_status", "RESOLVED") FILTER html %]</b>.
</dd>
- <dt class="confirmed">
- [% display_value("bug_status", "CONFIRMED") FILTER html %]
+ <dt>
+ <b>[% display_value("bug_status", "ASSIGNED") FILTER html %]</b>
</dt>
- <dd class="confirmed">
- This [% terms.bug %] is valid and has recently been filed.
- [%+ terms.Bugs %] in this state become
- <b>[% display_value("bug_status", "IN_PROGRESS") FILTER html %]</b>
- when somebody is working on them, or become resolved and marked
- <b>[% display_value("bug_status", "RESOLVED") FILTER html %]</b>.
+ <dd>
+ This [% terms.bug %] is not yet resolved, but is assigned to the
+ proper person. From here [% terms.bugs %] can be given to another
+ person and become <b>[% display_value("bug_status", "NEW") FILTER html %]</b>, or
+ resolved and become <b>[% display_value("bug_status", "RESOLVED") FILTER html %]</b>.
</dd>
- <dt class="in_progress">
- [% display_value("bug_status", "IN_PROGRESS") FILTER html %]
+ <dt>
+ <b>[% display_value("bug_status", "REOPENED") FILTER html %]</b>
</dt>
- <dd class="in_progress">
- This [% terms.bug %] is not yet resolved, but is assigned to the
- proper person who is working on the [% terms.bug %]. From here,
- [%+ terms.bugs %] can be given to another person and become
- <b>[% display_value("bug_status", "CONFIRMED") FILTER html %]</b>, or
- resolved and become
+ <dd>
+ This [% terms.bug %] was once resolved, but the resolution was
+ deemed incorrect. For example, a <b>[% display_value("resolution", "WORKSFORME") FILTER html %]</b> [% terms.bug %] is
+ <b>[% display_value("bug_status", "REOPENED") FILTER html %]</b> when more information shows up and
+ the [% terms.bug %] is now reproducible. From here [% terms.bugs %] are
+ either marked <b>[% display_value("bug_status", "ASSIGNED") FILTER html %]</b> or
<b>[% display_value("bug_status", "RESOLVED") FILTER html %]</b>.
</dd>
@@ -124,9 +131,10 @@
[% display_value("bug_status", "VERIFIED") FILTER html %]
</dt>
<dd class="verified">
- QA has looked at the [% terms.bug %] and the resolution and
- agrees that the appropriate resolution has been taken. This is
- the final status for [% terms.bugs %].
+ QA has looked at the [% terms.bug %] and the resolution and
+ agrees that the appropriate resolution has been taken.
+ Any zombie [% terms.bugs %] who choose to walk the earth again must
+ do so by becoming <b>[% display_value("bug_status", "REOPENED") FILTER html %]</b>.
</dd>
[% Hook.process('closed-status') %]
@@ -163,10 +171,9 @@
</dt>
<dd class="duplicate">
The problem is a duplicate of an existing [% terms.bug %].
- When [% terms.abug %] is marked as a
- <b>[% display_value("resolution", "DUPLICATE") FILTER html %]</b>,
- you will see which [% terms.bug %] it is a duplicate of,
- next to the resolution.
+ Marking [% terms.abug %] duplicate requires the [% terms.bug %]#
+ of the duplicating [% terms.bug %] and will at least put
+ that [% terms.bug %] number in the description field.
</dd>
<dt class="worksforme">
@@ -195,6 +202,11 @@
field => field } %]
[% END %]
+[%# This field is not a real one, but its label is visible in bugs. %]
+
+[% field_help_map.Importance = { help => help_html.importance,
+ field => "importance" } %]
+
[%# These are fields that don't need to be documented, either because
# they have docs somewhere else in the UI, or they don't show up on bugs.
# %]
@@ -208,6 +220,7 @@
'owner_idle_time',
'bug_status',
'resolution',
+ 'assignee_last_login',
] %]
<dl class="field_descriptions">
diff --git a/template/en/default/pages/quicksearch.html.tmpl b/template/en/default/pages/quicksearch.html.tmpl
index 901f05467..18bf4dfb1 100644
--- a/template/en/default/pages/quicksearch.html.tmpl
+++ b/template/en/default/pages/quicksearch.html.tmpl
@@ -229,6 +229,52 @@
(<kbd>url</kbd> OR <kbd>location</kbd>) AND (<kbd>bar</kbd> OR
<kbd>field</kbd>) AND (NOT <kbd>focus</kbd>)</p>
</li>
+
+ <li>
+ The default operator, colon (:), performs a <strong>substring</strong>
+ match of the value. The following operators are supported:
+ <ul>
+ <li>
+ <strong>:</strong> (substring):<br>
+ <kbd><em>summary:foo</em></kbd> will search for [% terms.bugs %]
+ where the <kbd>summary</kbd> contains <kbd>foo</kbd>.
+ </li>
+ <li>
+ <strong>=</strong> (equals):<br>
+ <kbd><em>summary=foo</em></kbd> will search for [% terms.bugs %]
+ where the <kbd>summary</kbd> is exactly <kbd>foo</kbd>.
+ </li>
+ <li>
+ <strong>!=</strong> (notequals):<br>
+ <kbd><em>summary!=foo</em></kbd> will search for [% terms.bugs %]
+ where the <kbd>summary</kbd> is not <kbd>foo</kbd>.
+ </li>
+ <li>
+ <strong>&gt;</strong> (greaterthan):<br>
+ <kbd><em>creation_ts&gt;-2w</em></kbd> will search for [% terms.bugs %]
+ where that were created between two weeks ago and now, excluding [%
+ terms.bugs %] exactly two weeks old.
+ </li>
+ <li>
+ <strong>&gt;=</strong> (greaterthaneq):<br>
+ <kbd><em>creation_ts&gt;=-2w</em></kbd> will search for [% terms.bugs %]
+ where that were created between two weeks ago and now, including [%
+ terms.bugs %] exactly two weeks old.
+ </li>
+ <li>
+ <strong>&lt;</strong> (lessthan):<br>
+ <kbd><em>creation_ts&lt;-2w</em></kbd> will search for [% terms.bugs %]
+ where that were created more than two weeks ago, excluding [%
+ terms.bugs %] exactly two weeks old.
+ </li>
+ <li>
+ <strong>&lt;=</strong> (lessthaneq):<br>
+ <kbd><em>creation_ts&lt;=-2w</em></kbd> will search for [% terms.bugs %]
+ where that were created more than two weeks ago, including [%
+ terms.bugs %] exactly two weeks old.
+ </li>
+ </ul>
+ </li>
</ul>
<h2 id="shortcuts">Advanced Shortcuts</h2>
@@ -303,6 +349,14 @@
<strong>#</strong><em>value</em>
</td>
</tr>
+ <tr>
+ <td class="field_name">Comment Searching</td>
+ <td class="field_nickname">
+ Allows overriding of the comment searching preference.<br>
+ "<strong>++comments</strong>" will always enable comment searching.<br>
+ "<strong>--comments</strong>" will always disable searching.<br>
+ </td>
+ </tr>
[% IF Param('usestatuswhiteboard') %]
<tr>
<td class="field_name">[% field_descs.short_desc FILTER html %]
diff --git a/template/en/default/reports/components.html.tmpl b/template/en/default/reports/components.html.tmpl
index ef7d5ae6d..b2a21ccc1 100644
--- a/template/en/default/reports/components.html.tmpl
+++ b/template/en/default/reports/components.html.tmpl
@@ -22,6 +22,7 @@
[%# INTERFACE:
# product: object. The product for which we want to display component
# descriptions.
+ # component: string. The name of the component to hilight in the browser
#%]
[% title = BLOCK %]
@@ -39,6 +40,8 @@
[% numcols = 2 %]
[% END %]
+<h2>[% mark FILTER html %]</h2>
+
<table cellpadding="0" cellspacing="0" id="components_header_table">
<tr>
<td class="instructions">
@@ -81,9 +84,11 @@
[%############################################################################%]
[% BLOCK describe_comp %]
- <tr id="[% comp.name FILTER html %]">
+ <tr id="[% comp.name FILTER html %]"
+ [%- IF comp.name == component_mark %] class="component_hilite"[% END %]>
<td rowspan="2" class="component_name">
- <a href="buglist.cgi?product=
+ <a name="[% comp.name FILTER html %]"
+ href="buglist.cgi?product=
[%- product.name FILTER uri %]&amp;component=
[%- comp.name FILTER uri %]&amp;resolution=---">
[% comp.name FILTER html %]</a>
@@ -97,7 +102,7 @@
</td>
[% END %]
</tr>
- <tr>
+ <tr[% IF comp.name == component_mark %] class="component_hilite"[% END %]>
<td colspan="[% numcols - 1 %]" class="component_description">
[% comp.description FILTER html_light %]
</td>
diff --git a/template/en/default/reports/report.html.tmpl b/template/en/default/reports/report.html.tmpl
index 94725ae81..38b64df0b 100644
--- a/template/en/default/reports/report.html.tmpl
+++ b/template/en/default/reports/report.html.tmpl
@@ -81,7 +81,9 @@
%]
[% IF debug %]
- <p>[% query FILTER html %]</p>
+ [% FOREACH query = queries %]
+ <p>[% query.sql FILTER html %]</p>
+ [% END %]
[% END %]
<div align="center">
diff --git a/template/en/default/request/email.txt.tmpl b/template/en/default/request/email.txt.tmpl
index 54bed2e25..6241dd34f 100644
--- a/template/en/default/request/email.txt.tmpl
+++ b/template/en/default/request/email.txt.tmpl
@@ -25,7 +25,8 @@
[% bugidsummary = bug.bug_id _ ': ' _ bug.short_desc %]
[% attidsummary = attachment.id _ ': ' _ attachment.description %]
[% flagtype_name = flag ? flag.type.name : old_flag.type.name %]
-[% statuses = { '+' => "granted" , '-' => 'denied' , 'X' => "canceled" ,
+[%# Upstreaming: denied (bug 621883) %]
+[% statuses = { '+' => "granted" , '-' => 'not granted' , 'X' => "canceled" ,
'?' => "asked" } %]
[% to_identity = "" %]
@@ -53,6 +54,10 @@ Subject: [% flagtype_name %] [%+ subject_status %]: [[% terms.Bug %] [%+ bug.bug
[Attachment [% attachment.id %]] [% attachment.description FILTER clean_text %][% END %]
Date: [% date %]
X-Bugzilla-Type: request
+[%- IF flag.requestee %]
+X-Bugzilla-Flag-Requestee: [% flag.requestee.email %]
+[% END %]
+[%+ INCLUDE "email/header-common.txt.tmpl" %]
[%+ threadingmarker %]
[%+ USE wrap -%]
@@ -91,3 +96,6 @@ Attachment [% attidsummary %]
[% END %]
[%- END %]
+
+--
+@@body-headers@@
diff --git a/template/en/default/request/queue.csv.tmpl b/template/en/default/request/queue.csv.tmpl
new file mode 100644
index 000000000..c6d962b4f
--- /dev/null
+++ b/template/en/default/request/queue.csv.tmpl
@@ -0,0 +1,46 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0. #%]
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+
+[% column_headers = {
+ "type" => "Flag",
+ "status" => field_descs.bug_status,
+ "bug_summary" => field_descs.short_desc,
+ "bug_id" => field_descs.bug_id,
+ "attach_summary" => "Attachment Description",
+ "attach_id" => "Attachment ID",
+ "requester" => "Requester",
+ "requestee" => "Requestee",
+ "created" => "Created",
+ "category" => field_descs.product _ ": " _ field_descs.component,
+} %]
+
+[% display_columns = ["requester", "requestee", "type", "status",
+ "bug_id", "bug_summary", "attach_id",
+ "attach_summary", "created", "category"] %]
+
+[% IF requests.size == 0 %]
+No requests.
+[% ELSE %]
+ [% FOREACH column = display_columns %]
+ [% column_headers.$column FILTER csv %][% ',' IF NOT loop.last() %]
+ [% END %]
+
+ [% FOREACH request = requests %]
+ [% FOREACH column = display_columns %]
+ [% IF column == 'created' %]
+ [% request.$column FILTER time FILTER csv %]
+ [% ELSIF column.match('^requeste') %]
+ [% request.$column FILTER email FILTER csv %]
+ [% ELSE %]
+ [% request.$column FILTER csv %]
+ [% END %][% ',' IF NOT loop.last() %]
+ [% END %]
+
+ [% END %]
+[% END %]
diff --git a/template/en/default/request/queue.html.tmpl b/template/en/default/request/queue.html.tmpl
index 57650de55..261db0438 100644
--- a/template/en/default/request/queue.html.tmpl
+++ b/template/en/default/request/queue.html.tmpl
@@ -25,10 +25,6 @@
[% PROCESS global/header.html.tmpl
title="Request Queue"
- style = "
- table.requests th { text-align: left; }
- table#filtering th { text-align: right; }
- "
onload="var f = document.request_form; selectProduct(f.product, f.component, null, null, 'Any');"
javascript_urls=["js/productform.js", "js/field.js"]
style_urls = ['skins/standard/buglist.css']
@@ -161,10 +157,22 @@ to some group are shown by default.
} %]
[% PROCESS "global/select-menu.html.tmpl" name="group" options=groups default=cgi.param('group') %]
</td>
+ </tr>
+ <tr>
+ <th></th>
+ <td>
+ <label><input type="radio" name="do_union" value="0"
+ [% 'checked="checked"' IF !cgi.param('do_union') %]>AND *</label>
+ <label><input type="radio" name="do_union" value="1"
+ [% 'checked="checked"' IF cgi.param('do_union') %]>OR *</label>
+ </td>
+ <td colspan="3"></td>
<td><input type="submit" id="filter" value="Filter"></td>
</tr>
</table>
+ <p>(* The logical conjunction/disjunction between the requester
+ and the requestee)</p>
</form>
[% column_headers = {
@@ -198,7 +206,10 @@ to some group are shown by default.
[% PROCESS start_new_table %]
[% END %]
[% buglist.${request.bug_id} = 1 %]
- <tr>
+
+ <tr class="bz_bugitem bz_[% request.bug_severity FILTER css_class_quote -%]
+ bz_[% request.priority FILTER css_class_quote -%]
+ bz_[% request.bug_status FILTER css_class_quote %]">
[% FOREACH column = display_columns %]
[% NEXT IF column == group_field || excluded_columns.contains(column) %]
<td>
@@ -209,6 +220,8 @@ to some group are shown by default.
</tr>
[% END %]
[% PROCESS display_buglist %]
+ <br><br>
+ <a href="request.cgi?[% urlquerypart FILTER html %]&amp;ctype=csv">(view entire list as CSV)</a>
[% END %]
[% PROCESS global/footer.html.tmpl %]
@@ -238,7 +251,7 @@ to some group are shown by default.
[% BLOCK display_bug %]
<a href="show_bug.cgi?id=[% request.bug_id %]"
[%- ' class="bz_secure"' IF request.restricted %]>
- [% request.bug_id %]: [%+ request.bug_summary FILTER html %]</a>
+ [% request.bug_id %] ([% request.priority FILTER html %]/[% request.bug_severity FILTER html %]): [%+ request.bug_summary FILTER html %]</a>
[% END %]
[% BLOCK display_attachment %]
diff --git a/template/en/default/rest.html.tmpl b/template/en/default/rest.html.tmpl
new file mode 100644
index 000000000..0b8321dd1
--- /dev/null
+++ b/template/en/default/rest.html.tmpl
@@ -0,0 +1,19 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+ <head>
+ <title>Bugzilla::REST::API</title>
+ <link href="[% urlbase FILTER none %][% 'skins/standard/global.css' FILTER mtime %]"
+ rel="stylesheet" type="text/css">
+ </head>
+ <body>
+ <pre>[% result FILTER html %]</pre>
+ </body>
+</html>
diff --git a/template/en/default/search/boolean-charts.html.tmpl b/template/en/default/search/boolean-charts.html.tmpl
index 878589cea..3fb1f8eae 100644
--- a/template/en/default/search/boolean-charts.html.tmpl
+++ b/template/en/default/search/boolean-charts.html.tmpl
@@ -47,6 +47,8 @@
"changedby",
"matches",
"notmatches",
+ "isempty",
+ "isnotempty",
] %]
<div class="bz_section_title" id="custom_search_filter">
@@ -78,6 +80,10 @@
<script type="text/javascript" src="[% 'js/history.js/native.history.js' FILTER mtime %]"></script>
<script type="text/javascript">
redirect_html4_browsers();
+ [%# These are alternative labels for the AND and OR options in and_all_select %]
+ var cs_and_label = 'Match ALL of the following:';
+ var cs_or_label = 'Match ANY of the following:';
+ cs_reconfigure('custom_search_last_row');
</script>
</div>
@@ -134,7 +140,8 @@
(
[% indent_level = indent_level + 1 %]
[% ELSIF condition.f == "CP" %]
- <input type="hidden" name="f[% cond_num FILTER html %]" value="CP">
+ <input type="hidden" name="f[% cond_num FILTER html %]"
+ id="f[% cond_num FILTER html %]" value="CP">
)
[% ELSE %]
<select name="f[% cond_num FILTER html %]" title="Field"
@@ -178,9 +185,11 @@
<div class="any_all_select">
<select name="[% name FILTER html %]" id="[% name FILTER html %]"
onchange="fix_query_string(this)">
- <option value="AND">Match ALL of the following:</option>
+ <option value="AND">Match ALL of the following separately:</option>
<option value="OR" [% ' selected="selected"' IF selected == "OR" %]>
- Match ANY of the following:</option>
+ Match ANY of the following separately:</option>
+ <option value="AND_G" [% ' selected' IF selected == "AND_G" %]>
+ Match ALL of the following against the same field:</option>
</select>
[% IF with_advanced_link %]
<a id="custom_search_advanced_controller"
diff --git a/template/en/default/search/field.html.tmpl b/template/en/default/search/field.html.tmpl
index defc94cc3..ae7ca1ad4 100644
--- a/template/en/default/search/field.html.tmpl
+++ b/template/en/default/search/field.html.tmpl
@@ -71,7 +71,7 @@
YAHOO.bugzilla.keywordAutocomplete.init('[% field.name FILTER js %]',
'keyword_autocomplete');
</script>
- [% CASE constants.FIELD_TYPE_DATETIME %]
+ [% CASE [constants.FIELD_TYPE_DATETIME, constants.FIELD_TYPE_DATE] %]
[% INCLUDE "bug/field-label.html.tmpl"
field = field
tag_name = "span"
@@ -115,7 +115,7 @@
<select name="[% field.name FILTER html%]"
id="[% field.name FILTER html %]"
[% IF onchange %] onchange="[% onchange FILTER html %]"[% END %]
- multiple="multiple" size="7">
+ multiple="multiple" size="9">
[% legal_values = ${field.name} %]
[% IF field.name == "component" %]
[% legal_values = ${"component_"} %]
diff --git a/template/en/default/search/form.html.tmpl b/template/en/default/search/form.html.tmpl
index 241ade088..1b9b8310f 100644
--- a/template/en/default/search/form.html.tmpl
+++ b/template/en/default/search/form.html.tmpl
@@ -330,9 +330,12 @@ TUI_hide_default('information_query');
label=> "a ${field_descs.cc} list member" } %]
[% PROCESS role_types field = { count => n, name => "emaillongdesc",
label=> " a ${field_descs.commenter}" } %]
+ [% PROCESS role_types field = { count => n, name => "emailbug_mentor",
+ label => " a ${field_descs.bug_mentor}" } %]
<select name="emailtype[% n %]">
[% FOREACH qv = [
{ name => "substring", description => "contains" },
+ { name => "notsubstring", description => "doesn't contain" },
{ name => "exact", description => "is" },
{ name => "notequals", description => "is not" },
{ name => "regexp", description => "matches regexp" },
@@ -356,8 +359,8 @@ TUI_hide_default('information_query');
[% END %]
</div>
[% END %]
- [% Hook.process('email_numbering_end') %]
- </div>
+ [% Hook.process('email_numbering_end') %]
+ </div>
[%# *** Bug Changes *** %]
<div class="bz_section_title" id="history_filter">
<div id="history_query_controller" class="arrow">&#9660;</div>
diff --git a/template/en/default/search/search-google.html.tmpl b/template/en/default/search/search-google.html.tmpl
new file mode 100644
index 000000000..f363248a5
--- /dev/null
+++ b/template/en/default/search/search-google.html.tmpl
@@ -0,0 +1,45 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Search " _ terms.Bugs _ " using Google"
+%]
+
+[% WRAPPER search/tabs.html.tmpl %]
+
+<p>
+ Use the <a href="http://www.google.com">Google</a> search engine to search
+ for [% terms.Bugzilla +%] [%+ terms.bugs %]. Find the [% terms.bugs %] you are
+ looking for by entering words that best describe it.
+</p>
+
+<p>
+ For example, if the [% terms.bug %] you are looking for is a browser crash when
+ you go to a secure web site with an embedded Flash animation, you might search
+ for "crash secure SSL flash".
+</p>
+
+<p>
+ <span style="color:red;">*</span>
+ Google only indexes publicly viewable [% terms.bugs %] and all may not be represented.
+<p>
+
+<form method="get" action="https://www.google.com/search">
+<input type="hidden" name="sitesearch" value="bugzilla.mozilla.org">
+ <nobr>
+ <input type="text" name="q" size="60" maxlength="255" value="">
+ <input type="submit" value="Search">
+ </nobr>
+</form>
+
+[% END %]
+
+[% PROCESS global/footer.html.tmpl %]
+
diff --git a/template/en/default/search/search-instant.html.tmpl b/template/en/default/search/search-instant.html.tmpl
new file mode 100644
index 000000000..5d75d1996
--- /dev/null
+++ b/template/en/default/search/search-instant.html.tmpl
@@ -0,0 +1,85 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Instant Search"
+ javascript_urls = [ 'extensions/GuidedBugEntry/web/js/products.js',
+ 'js/instant-search.js', ]
+ yui = [ 'datatable', 'container' ]
+%]
+
+[% UNLESS default.exists('product') && default.product.size %]
+ [% default.product = [ 'Firefox' ] %]
+[% END %]
+
+<script>
+YAHOO.bugzilla.instantSearch.setLabels( {
+ id: "[% field_descs.bug_id FILTER js %]",
+ summary: "[% field_descs.short_desc FILTER js %]",
+ component: "[% field_descs.component FILTER js %]",
+ status: "[% field_descs.bug_status FILTER js %]",
+});
+</script>
+
+[% WRAPPER search/tabs.html.tmpl %]
+
+<p>
+ This page provides instant results; however, only the [% terms.bug %]'s summary
+ is searched. Products related to the selected product may also be searched.
+</p>
+
+<table>
+ <tr>
+ <td align="right" valign="baseline">
+ <b><label for="product">Product:</label></b>
+ </td>
+ <td>
+ <select name="product" id="product">
+ [% IF Param('useclassification') %]
+ [% FOREACH c = classification %]
+ <optgroup label="[% c.name FILTER html %]">
+ [% FOREACH p = user.get_selectable_products(c.id) %]
+ [% IF p.components.size %]
+ <option value="[% p.name FILTER html %]"
+ [% " selected" IF lsearch(default.product, p.name) != -1 %]>
+ [% p.name FILTER html %]
+ </option>
+ [% END %]
+ [% END %]
+ </optgroup>
+ [% END %]
+ [% ELSE %]
+ [% FOREACH p = product %]
+ <option value="[% p.name FILTER html %]"
+ [% " selected" IF lsearch(default.product, p.name) != -1 %]>
+ [% p.name FILTER html %]
+ </option>
+ [% END %]
+ [% END %]
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <td align="right" valign="baseline">
+ <b><label for="content">Words:</label></b>
+ </td>
+ <td>
+ <input id="content" spellcheck="true" size="60"
+ value="[% default.content.0 FILTER html %]">
+ </td>
+ </tr>
+</table>
+<br>
+
+<div id="results"></div>
+
+[% END %]
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/template/en/default/search/search-specific.html.tmpl b/template/en/default/search/search-specific.html.tmpl
index 9ef299425..7e5de2c4a 100644
--- a/template/en/default/search/search-specific.html.tmpl
+++ b/template/en/default/search/search-specific.html.tmpl
@@ -98,7 +98,7 @@ for "crash secure SSL flash".
<label for="content">Words:</label>
</th>
<td>
- <input name="content" size="40" id="content"
+ <input name="content" size="60" id="content"
value="[% default.content.0 FILTER html %]">
<script type="text/javascript"> <!--
document.forms['queryform'].content.focus();
@@ -107,6 +107,15 @@ for "crash secure SSL flash".
</td>
</tr>
<tr>
+ <td>&nbsp;</td>
+ <td>
+ <input type="hidden" name="comments" value="0">
+ <input type="checkbox" id="comments" name="comments"
+ value="1" [% 'checked' IF cgi.param("comments") %]>
+ <label for="comments">Search comments</label>
+ </td>
+ </tr>
+ <tr>
<td></td>
<td>
diff --git a/template/en/default/search/tabs.html.tmpl b/template/en/default/search/tabs.html.tmpl
index 119b30fde..26ad4f39b 100644
--- a/template/en/default/search/tabs.html.tmpl
+++ b/template/en/default/search/tabs.html.tmpl
@@ -24,10 +24,14 @@
#%]
[% WRAPPER global/tabs.html.tmpl
- tabs = [ { name => 'specific', label => "Simple Search",
+ tabs = [ { name => 'instant', label => "Instant Search",
+ link => "query.cgi?format=instant" },
+ { name => 'specific', label => "Simple Search",
link => "query.cgi?format=specific" },
{ name => 'advanced', label => "Advanced Search",
- link => "query.cgi?format=advanced" } ]
+ link => "query.cgi?format=advanced" },
+ { name => 'google', label => 'Google Search',
+ link => "query.cgi?format=google" } ]
current_tab_name = query_format || format || "advanced"
%]
diff --git a/template/en/default/setup/strings.txt.pl b/template/en/default/setup/strings.txt.pl
index c96fc014e..103a2a3f5 100644
--- a/template/en/default/setup/strings.txt.pl
+++ b/template/en/default/setup/strings.txt.pl
@@ -102,9 +102,11 @@ END
feature_jsonrpc_faster => 'Make JSON-RPC Faster',
feature_new_charts => 'New Charts',
feature_old_charts => 'Old Charts',
+ feature_memcached => 'Memcached Support',
feature_mod_perl => 'mod_perl',
feature_moving => 'Move Bugs Between Installations',
feature_patch_viewer => 'Patch Viewer',
+ feature_rest => 'REST Interface',
feature_smtp_auth => 'SMTP Authentication',
feature_updates => 'Automatic Update Notifications',
feature_xmlrpc => 'XML-RPC Interface',
diff --git a/token.cgi b/token.cgi
index a321974c3..e87e24fab 100755
--- a/token.cgi
+++ b/token.cgi
@@ -226,6 +226,7 @@ sub changePassword {
SET cryptpassword = ?
WHERE userid = ?},
undef, ($cryptedpassword, $userid) );
+ Bugzilla->memcached->clear({ table => 'profiles', id => $userid });
$dbh->do('DELETE FROM tokens WHERE token = ?', undef, $token);
$dbh->bz_commit_transaction();
@@ -276,6 +277,7 @@ sub changeEmail {
SET login_name = ?
WHERE userid = ?},
undef, ($new_email, $userid));
+ Bugzilla->memcached->clear({ table => 'profiles', id => $userid });
$dbh->do('DELETE FROM tokens WHERE token = ?', undef, $token);
$dbh->do(q{DELETE FROM tokens WHERE userid = ?
AND tokentype = 'emailnew'}, undef, $userid);
@@ -325,6 +327,7 @@ sub cancelChangeEmail {
SET login_name = ?
WHERE userid = ?},
undef, ($old_email, $userid));
+ Bugzilla->memcached->clear({ table => 'profiles', id => $userid });
# email has changed, so rederive groups
diff --git a/userprefs.cgi b/userprefs.cgi
index f0d5a8e53..d33de74ad 100755
--- a/userprefs.cgi
+++ b/userprefs.cgi
@@ -33,6 +33,7 @@ use Bugzilla::Search;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::User;
+use Bugzilla::User::Setting qw(clear_settings_cache);
use Bugzilla::Token;
my $template = Bugzilla->template;
@@ -79,6 +80,9 @@ sub DoAccount {
sub SaveAccount {
my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh;
+
+ $dbh->bz_start_transaction;
+
my $user = Bugzilla->user;
my $oldpassword = $cgi->param('old_password');
@@ -101,12 +105,7 @@ sub SaveAccount {
validate_password($pwd1, $pwd2);
if ($oldpassword ne $pwd1) {
- my $cryptedpassword = bz_crypt($pwd1);
- $dbh->do(q{UPDATE profiles
- SET cryptpassword = ?
- WHERE userid = ?},
- undef, ($cryptedpassword, $user->id));
-
+ $user->set_password($pwd1);
# Invalidate all logins except for the current one
Bugzilla->logout(LOGOUT_KEEP_CURRENT);
}
@@ -137,10 +136,9 @@ sub SaveAccount {
}
}
- my $realname = trim($cgi->param('realname'));
- trick_taint($realname); # Only used in a placeholder
- $dbh->do("UPDATE profiles SET realname = ? WHERE userid = ?",
- undef, ($realname, $user->id));
+ $user->set_name($cgi->param('realname'));
+ $user->update({ keep_session => 1, keep_tokens => 1 });
+ $dbh->bz_commit_transaction;
}
@@ -150,7 +148,7 @@ sub DoSettings {
my $settings = $user->settings;
$vars->{'settings'} = $settings;
- my @setting_list = keys %$settings;
+ my @setting_list = sort keys %$settings;
$vars->{'setting_names'} = \@setting_list;
$vars->{'has_settings_enabled'} = 0;
@@ -188,6 +186,7 @@ sub SaveSettings {
}
}
$vars->{'settings'} = $user->settings(1);
+ clear_settings_cache($user->id);
}
sub DoEmail {
@@ -338,6 +337,47 @@ sub SaveEmail {
$dbh->bz_commit_transaction();
}
+
+ ###########################################################################
+ # Ignore Bugs
+ ###########################################################################
+ my %ignored_bugs = map { $_->{'id'} => 1 } @{$user->bugs_ignored};
+
+ # Validate the new bugs to ignore by checking that they exist and also
+ # if the user gave an alias
+ my @add_ignored = split(/[\s,]+/, $cgi->param('add_ignored_bugs'));
+ @add_ignored = map { Bugzilla::Bug->check($_)->id } @add_ignored;
+ map { $ignored_bugs{$_} = 1 } @add_ignored;
+
+ # Remove any bug ids the user no longer wants to ignore
+ foreach my $key (grep(/^remove_ignored_bug_/, $cgi->param)) {
+ my ($bug_id) = $key =~ /(\d+)$/;
+ delete $ignored_bugs{$bug_id};
+ }
+
+ # Update the database with any changes made
+ my ($removed, $added) = diff_arrays([ map { $_->{'id'} } @{$user->bugs_ignored} ],
+ [ keys %ignored_bugs ]);
+
+ if (scalar @$removed || scalar @$added) {
+ $dbh->bz_start_transaction();
+
+ if (scalar @$removed) {
+ $dbh->do('DELETE FROM email_bug_ignore WHERE user_id = ? AND ' .
+ $dbh->sql_in('bug_id', $removed),
+ undef, $user->id);
+ }
+ if (scalar @$added) {
+ my $sth = $dbh->prepare('INSERT INTO email_bug_ignore
+ (user_id, bug_id) VALUES (?, ?)');
+ $sth->execute($user->id, $_) foreach @$added;
+ }
+
+ # Reset the cache of ignored bugs if the list changed.
+ delete $user->{bugs_ignored};
+
+ $dbh->bz_commit_transaction();
+ }
}
@@ -345,9 +385,9 @@ sub DoPermissions {
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
my (@has_bits, @set_bits);
-
+
my $groups = $dbh->selectall_arrayref(
- "SELECT DISTINCT name, description FROM groups WHERE id IN (" .
+ "SELECT DISTINCT name, description FROM groups WHERE id IN (" .
$user->groups_as_string . ") ORDER BY name");
foreach my $group (@$groups) {
my ($nam, $desc) = @$group;
@@ -470,12 +510,13 @@ sub SaveSavedSearches {
}
$user->flush_queries_cache;
-
+
# Update profiles.mybugslink.
my $showmybugslink = defined($cgi->param("showmybugslink")) ? 1 : 0;
$dbh->do("UPDATE profiles SET mybugslink = ? WHERE userid = ?",
- undef, ($showmybugslink, $user->id));
+ undef, ($showmybugslink, $user->id));
$user->{'showmybugslink'} = $showmybugslink;
+ Bugzilla->memcached->clear({ table => 'profiles', id => $user->id });
}
diff --git a/whine.pl b/whine.pl
index ad6067228..e6161cfeb 100755
--- a/whine.pl
+++ b/whine.pl
@@ -453,7 +453,7 @@ sub run_queries {
'user' => $args->{'recipient'}, # the search runs as the recipient
);
# If a query fails for whatever reason, it shouldn't kill the script.
- my $sqlquery = eval { $search->sql };
+ my $data = eval { $search->data };
if ($@) {
print STDERR get_text('whine_query_failed', { query_name => $thisquery->{'name'},
author => $args->{'author'},
@@ -461,15 +461,12 @@ sub run_queries {
next;
}
- $sth = $dbh->prepare($sqlquery);
- $sth->execute;
-
- while (my @row = $sth->fetchrow_array) {
+ foreach my $row (@$data) {
my $bug = {};
for my $field (@searchfields) {
my $fieldname = $field;
$fieldname =~ s/^bugs\.//; # No need for bugs.whatever
- $bug->{$fieldname} = shift @row;
+ $bug->{$fieldname} = shift @$row;
}
if ($thisquery->{'onemailperbug'}) {
diff --git a/xml.cgi b/xml.cgi
new file mode 100755
index 000000000..ce6a7c39b
--- /dev/null
+++ b/xml.cgi
@@ -0,0 +1,41 @@
+#!/usr/bin/perl -wT
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Dawn Endico <endico@mozilla.org>
+# Terry Weissman <terry@mozilla.org>
+# Gervase Markham <gerv@gerv.net>
+
+use strict;
+
+use lib qw(. lib);
+use Bugzilla;
+
+my $cgi = Bugzilla->cgi;
+
+# Convert comma/space separated elements into separate params
+my @ids = ();
+
+if (defined $cgi->param('id')) {
+ @ids = split (/[, ]+/, $cgi->param('id'));
+}
+
+my $ids = join('', map { $_ = "&id=" . $_ } @ids);
+
+print $cgi->redirect("show_bug.cgi?ctype=xml$ids");
diff --git a/xt/lib/Bugzilla/Test/Search/FieldTest.pm b/xt/lib/Bugzilla/Test/Search/FieldTest.pm
index ee25f2dc6..a625127c9 100644
--- a/xt/lib/Bugzilla/Test/Search/FieldTest.pm
+++ b/xt/lib/Bugzilla/Test/Search/FieldTest.pm
@@ -562,13 +562,13 @@ sub do_tests {
my $sql;
TODO: {
local $TODO = $search_broken if $search_broken;
- lives_ok { $sql = $search->sql } "$name: generate SQL";
+ lives_ok { $sql = $search->_sql } "$name: generate SQL";
}
my $results;
SKIP: {
skip "Can't run SQL without any SQL", 1 if !defined $sql;
- $results = $self->_test_sql($sql);
+ $results = $self->_test_sql($search);
}
$self->_test_content($results, $sql);
@@ -585,12 +585,11 @@ sub _test_search_object_creation {
}
sub _test_sql {
- my ($self, $sql) = @_;
- my $dbh = Bugzilla->dbh;
+ my ($self, $search) = @_;
my $name = $self->name;
my $results;
- lives_ok { $results = $dbh->selectall_arrayref($sql) } "$name: Run SQL Query"
- or diag($sql);
+ lives_ok { $results = $search->data } "$name: Run SQL Query"
+ or diag($search->_sql);
return $results;
}