From 438f4872ac8cf3d54644fd730e71324e194ef715 Mon Sep 17 00:00:00 2001 From: Gideon Greenspan Date: Wed, 14 Dec 2011 14:39:59 +0200 Subject: [PATCH] 1.5-beta-1 --- .htaccess | 8 + CHANGELOG.html | 9 + LICENSE.html | 9 + README.html | 9 + index.php | 36 + qa-config-example.php | 166 ++ qa-content/jquery-1.7.1.min.js | 4 + qa-content/qa-admin.js | 143 ++ qa-content/qa-ask.js | 283 +++ qa-content/qa-page.js | 137 ++ qa-content/qa-question.js | 262 +++ qa-external-example/qa-external-users.php | 624 ++++++ qa-include/qa-ajax-answer.php | 111 + qa-include/qa-ajax-asktitle.php | 114 + qa-include/qa-ajax-category.php | 49 + qa-include/qa-ajax-click-admin.php | 42 + qa-include/qa-ajax-click-answer.php | 119 + qa-include/qa-ajax-click-comment.php | 100 + qa-include/qa-ajax-comment.php | 113 + qa-include/qa-ajax-favorite.php | 56 + qa-include/qa-ajax-mailing.php | 57 + qa-include/qa-ajax-notice.php | 52 + qa-include/qa-ajax-recalc.php | 51 + qa-include/qa-ajax-show-comments.php | 77 + qa-include/qa-ajax-version.php | 60 + qa-include/qa-ajax-vote.php | 65 + qa-include/qa-ajax.php | 92 + qa-include/qa-app-admin.php | 529 +++++ qa-include/qa-app-blobs.php | 70 + qa-include/qa-app-captcha.php | 96 + qa-include/qa-app-cookies.php | 82 + qa-include/qa-app-emails.php | 158 ++ qa-include/qa-app-events.php | 92 + qa-include/qa-app-favorites.php | 80 + qa-include/qa-app-format.php | 1678 ++++++++++++++ qa-include/qa-app-limits.php | 196 ++ qa-include/qa-app-mailing.php | 156 ++ qa-include/qa-app-options.php | 712 ++++++ qa-include/qa-app-post-create.php | 242 ++ qa-include/qa-app-post-update.php | 774 +++++++ qa-include/qa-app-posts.php | 433 ++++ qa-include/qa-app-q-list.php | 205 ++ qa-include/qa-app-recalc.php | 647 ++++++ qa-include/qa-app-search.php | 144 ++ qa-include/qa-app-updates.php | 57 + qa-include/qa-app-users-edit.php | 392 ++++ qa-include/qa-app-users.php | 768 +++++++ qa-include/qa-app-votes.php | 299 +++ qa-include/qa-base.php | 1320 +++++++++++ qa-include/qa-blob.php | 94 + qa-include/qa-check-lang.php | 253 +++ qa-include/qa-class.phpmailer.php | 1912 ++++++++++++++++ qa-include/qa-class.smtp.php | 1064 +++++++++ qa-include/qa-db-admin.php | 544 +++++ qa-include/qa-db-blobs.php | 102 + qa-include/qa-db-cache.php | 78 + qa-include/qa-db-cookies.php | 83 + qa-include/qa-db-events.php | 183 ++ qa-include/qa-db-favorites.php | 209 ++ qa-include/qa-db-hotness.php | 76 + qa-include/qa-db-install.php | 1319 +++++++++++ qa-include/qa-db-limits.php | 91 + qa-include/qa-db-maxima.php | 76 + qa-include/qa-db-messages.php | 49 + qa-include/qa-db-metas.php | 201 ++ qa-include/qa-db-notices.php | 73 + qa-include/qa-db-options.php | 47 + qa-include/qa-db-points.php | 221 ++ qa-include/qa-db-post-create.php | 358 +++ qa-include/qa-db-post-update.php | 318 +++ qa-include/qa-db-recalc.php | 359 +++ qa-include/qa-db-selects.php | 1466 +++++++++++++ qa-include/qa-db-users.php | 273 +++ qa-include/qa-db-votes.php | 137 ++ qa-include/qa-db.php | 737 +++++++ qa-include/qa-editor-basic.php | 80 + qa-include/qa-event-limits.php | 107 + qa-include/qa-event-notify.php | 224 ++ qa-include/qa-event-updates.php | 147 ++ qa-include/qa-external-users-wp.php | 148 ++ qa-include/qa-feed.php | 432 ++++ qa-include/qa-filter-basic.php | 146 ++ qa-include/qa-htmLawed.php | 711 ++++++ qa-include/qa-image.php | 100 + qa-include/qa-index.php | 180 ++ qa-include/qa-install.php | 325 +++ qa-include/qa-lang-admin.php | 263 +++ qa-include/qa-lang-emails.php | 82 + qa-include/qa-lang-main.php | 226 ++ qa-include/qa-lang-misc.php | 95 + qa-include/qa-lang-options.php | 274 +++ qa-include/qa-lang-profile.php | 78 + qa-include/qa-lang-question.php | 165 ++ qa-include/qa-lang-users.php | 128 ++ qa-include/qa-page-account.php | 403 ++++ qa-include/qa-page-activity.php | 92 + qa-include/qa-page-admin-categories.php | 604 +++++ qa-include/qa-page-admin-default.php | 1642 ++++++++++++++ qa-include/qa-page-admin-flagged.php | 132 ++ qa-include/qa-page-admin-hidden.php | 157 ++ qa-include/qa-page-admin-moderate.php | 137 ++ qa-include/qa-page-admin-pages.php | 570 +++++ qa-include/qa-page-admin-plugins.php | 224 ++ qa-include/qa-page-admin-points.php | 183 ++ qa-include/qa-page-admin-recalc.php | 107 + qa-include/qa-page-admin-stats.php | 264 +++ qa-include/qa-page-admin-userfields.php | 233 ++ qa-include/qa-page-admin-usertitles.php | 182 ++ qa-include/qa-page-admin-widgets.php | 328 +++ qa-include/qa-page-answers.php | 84 + qa-include/qa-page-ask.php | 264 +++ qa-include/qa-page-categories.php | 112 + qa-include/qa-page-comments.php | 83 + qa-include/qa-page-confirm.php | 140 ++ qa-include/qa-page-default.php | 181 ++ qa-include/qa-page-favorites.php | 155 ++ qa-include/qa-page-feedback.php | 177 ++ qa-include/qa-page-forgot.php | 119 + qa-include/qa-page-hot.php | 84 + qa-include/qa-page-ip.php | 212 ++ qa-include/qa-page-login.php | 174 ++ qa-include/qa-page-logout.php | 44 + qa-include/qa-page-message.php | 230 ++ qa-include/qa-page-not-found.php | 49 + qa-include/qa-page-question-post.php | 890 ++++++++ qa-include/qa-page-question-submit.php | 361 +++ qa-include/qa-page-question-view.php | 965 ++++++++ qa-include/qa-page-question.php | 423 ++++ qa-include/qa-page-questions.php | 147 ++ qa-include/qa-page-register.php | 179 ++ qa-include/qa-page-reset.php | 136 ++ qa-include/qa-page-search.php | 155 ++ qa-include/qa-page-tag.php | 96 + qa-include/qa-page-tags.php | 83 + qa-include/qa-page-unanswered.php | 148 ++ qa-include/qa-page-unsubscribe.php | 85 + qa-include/qa-page-updates.php | 119 + qa-include/qa-page-user.php | 711 ++++++ qa-include/qa-page-users-blocked.php | 89 + qa-include/qa-page-users-special.php | 89 + qa-include/qa-page-users.php | 81 + qa-include/qa-page.php | 772 +++++++ qa-include/qa-search-basic.php | 150 ++ qa-include/qa-theme-base.php | 1954 +++++++++++++++++ qa-include/qa-url-test.php | 41 + qa-include/qa-util-debug.php | 147 ++ qa-include/qa-util-image.php | 186 ++ qa-include/qa-util-sort.php | 109 + qa-include/qa-util-string.php | 716 ++++++ qa-include/qa-viewer-basic.php | 168 ++ qa-include/qa-widget-activity-count.php | 74 + qa-include/qa-widget-ask-box.php | 109 + qa-include/qa-widget-related-qs.php | 111 + qa-lang/en-GB/qa-lang-admin.php | 35 + qa-plugin/basic-adsense/qa-basic-adsense.php | 128 ++ qa-plugin/basic-adsense/qa-plugin.php | 52 + qa-plugin/event-logger/qa-event-logger.php | 241 ++ qa-plugin/event-logger/qa-plugin.php | 52 + .../example-page/qa-example-lang-default.php | 34 + .../example-page/qa-example-lang-en-GB.php | 34 + qa-plugin/example-page/qa-example-page.php | 111 + qa-plugin/example-page/qa-plugin.php | 53 + qa-plugin/facebook-login/JSON.php | 933 ++++++++ qa-plugin/facebook-login/base_facebook.php | 1143 ++++++++++ qa-plugin/facebook-login/facebook.php | 93 + .../facebook-login/qa-facebook-login.php | 211 ++ qa-plugin/facebook-login/qa-plugin.php | 52 + .../qa-mouseover-admin-form.php | 85 + .../mouseover-layer/qa-mouseover-layer.php | 74 + qa-plugin/mouseover-layer/qa-plugin.php | 53 + qa-plugin/recaptcha-captcha/qa-plugin.php | 52 + .../qa-recaptcha-captcha.php | 152 ++ qa-plugin/recaptcha-captcha/recaptchalib.php | 274 +++ qa-plugin/tag-cloud-widget/qa-plugin.php | 52 + qa-plugin/tag-cloud-widget/qa-tag-cloud.php | 159 ++ qa-plugin/wysiwyg-editor/.htaccess | 24 + qa-plugin/wysiwyg-editor/LICENSE.html | 1327 +++++++++++ qa-plugin/wysiwyg-editor/adapters/jquery.js | 6 + qa-plugin/wysiwyg-editor/ckeditor.js | 149 ++ qa-plugin/wysiwyg-editor/ckeditor_basic.js | 8 + .../wysiwyg-editor/ckeditor_basic_source.js | 20 + qa-plugin/wysiwyg-editor/ckeditor_source.js | 35 + qa-plugin/wysiwyg-editor/config.js | 11 + qa-plugin/wysiwyg-editor/contents.css | 25 + qa-plugin/wysiwyg-editor/images/spacer.gif | Bin 0 -> 43 bytes qa-plugin/wysiwyg-editor/lang/_languages.js | 6 + .../lang/_translationstatus.txt | 61 + qa-plugin/wysiwyg-editor/lang/af.js | 6 + qa-plugin/wysiwyg-editor/lang/ar.js | 6 + qa-plugin/wysiwyg-editor/lang/bg.js | 6 + qa-plugin/wysiwyg-editor/lang/bn.js | 6 + qa-plugin/wysiwyg-editor/lang/bs.js | 6 + qa-plugin/wysiwyg-editor/lang/ca.js | 6 + qa-plugin/wysiwyg-editor/lang/cs.js | 6 + qa-plugin/wysiwyg-editor/lang/cy.js | 6 + qa-plugin/wysiwyg-editor/lang/da.js | 6 + qa-plugin/wysiwyg-editor/lang/de.js | 6 + qa-plugin/wysiwyg-editor/lang/el.js | 6 + qa-plugin/wysiwyg-editor/lang/en-au.js | 6 + qa-plugin/wysiwyg-editor/lang/en-ca.js | 6 + qa-plugin/wysiwyg-editor/lang/en-gb.js | 6 + qa-plugin/wysiwyg-editor/lang/en.js | 6 + qa-plugin/wysiwyg-editor/lang/eo.js | 6 + qa-plugin/wysiwyg-editor/lang/es.js | 6 + qa-plugin/wysiwyg-editor/lang/et.js | 6 + qa-plugin/wysiwyg-editor/lang/eu.js | 6 + qa-plugin/wysiwyg-editor/lang/fa.js | 6 + qa-plugin/wysiwyg-editor/lang/fi.js | 6 + qa-plugin/wysiwyg-editor/lang/fo.js | 6 + qa-plugin/wysiwyg-editor/lang/fr-ca.js | 6 + qa-plugin/wysiwyg-editor/lang/fr.js | 6 + qa-plugin/wysiwyg-editor/lang/gl.js | 6 + qa-plugin/wysiwyg-editor/lang/gu.js | 6 + qa-plugin/wysiwyg-editor/lang/he.js | 6 + qa-plugin/wysiwyg-editor/lang/hi.js | 6 + qa-plugin/wysiwyg-editor/lang/hr.js | 6 + qa-plugin/wysiwyg-editor/lang/hu.js | 6 + qa-plugin/wysiwyg-editor/lang/is.js | 6 + qa-plugin/wysiwyg-editor/lang/it.js | 6 + qa-plugin/wysiwyg-editor/lang/ja.js | 6 + qa-plugin/wysiwyg-editor/lang/ka.js | 6 + qa-plugin/wysiwyg-editor/lang/km.js | 6 + qa-plugin/wysiwyg-editor/lang/ko.js | 6 + qa-plugin/wysiwyg-editor/lang/lt.js | 6 + qa-plugin/wysiwyg-editor/lang/lv.js | 6 + qa-plugin/wysiwyg-editor/lang/mn.js | 6 + qa-plugin/wysiwyg-editor/lang/ms.js | 6 + qa-plugin/wysiwyg-editor/lang/nb.js | 6 + qa-plugin/wysiwyg-editor/lang/nl.js | 6 + qa-plugin/wysiwyg-editor/lang/no.js | 6 + qa-plugin/wysiwyg-editor/lang/pl.js | 6 + qa-plugin/wysiwyg-editor/lang/pt-br.js | 6 + qa-plugin/wysiwyg-editor/lang/pt.js | 6 + qa-plugin/wysiwyg-editor/lang/ro.js | 6 + qa-plugin/wysiwyg-editor/lang/ru.js | 6 + qa-plugin/wysiwyg-editor/lang/sk.js | 6 + qa-plugin/wysiwyg-editor/lang/sl.js | 6 + qa-plugin/wysiwyg-editor/lang/sr-latn.js | 6 + qa-plugin/wysiwyg-editor/lang/sr.js | 6 + qa-plugin/wysiwyg-editor/lang/sv.js | 6 + qa-plugin/wysiwyg-editor/lang/th.js | 6 + qa-plugin/wysiwyg-editor/lang/tr.js | 6 + qa-plugin/wysiwyg-editor/lang/uk.js | 6 + qa-plugin/wysiwyg-editor/lang/vi.js | 6 + qa-plugin/wysiwyg-editor/lang/zh-cn.js | 6 + qa-plugin/wysiwyg-editor/lang/zh.js | 6 + .../plugins/a11yhelp/dialogs/a11yhelp.js | 7 + .../plugins/a11yhelp/lang/en.js | 6 + .../plugins/a11yhelp/lang/he.js | 6 + .../plugins/about/dialogs/about.js | 6 + .../plugins/about/dialogs/logo_ckeditor.png | Bin 0 -> 2759 bytes .../wysiwyg-editor/plugins/adobeair/plugin.js | 6 + .../wysiwyg-editor/plugins/ajax/plugin.js | 6 + .../wysiwyg-editor/plugins/autogrow/plugin.js | 6 + .../wysiwyg-editor/plugins/bbcode/plugin.js | 9 + .../plugins/clipboard/dialogs/paste.js | 7 + .../colordialog/dialogs/colordialog.js | 7 + .../plugins/devtools/lang/en.js | 6 + .../wysiwyg-editor/plugins/devtools/plugin.js | 6 + .../plugins/dialog/dialogDefinition.js | 4 + .../wysiwyg-editor/plugins/div/dialogs/div.js | 8 + .../plugins/docprops/dialogs/docprops.js | 10 + .../wysiwyg-editor/plugins/docprops/plugin.js | 6 + .../plugins/find/dialogs/find.js | 10 + .../plugins/flash/dialogs/flash.js | 9 + .../plugins/flash/images/placeholder.png | Bin 0 -> 256 bytes .../plugins/forms/dialogs/button.js | 6 + .../plugins/forms/dialogs/checkbox.js | 6 + .../plugins/forms/dialogs/form.js | 6 + .../plugins/forms/dialogs/hiddenfield.js | 6 + .../plugins/forms/dialogs/radio.js | 6 + .../plugins/forms/dialogs/select.js | 9 + .../plugins/forms/dialogs/textarea.js | 6 + .../plugins/forms/dialogs/textfield.js | 6 + .../plugins/forms/images/hiddenfield.gif | Bin 0 -> 105 bytes .../plugins/iframe/dialogs/iframe.js | 7 + .../plugins/iframe/images/placeholder.png | Bin 0 -> 449 bytes .../plugins/iframedialog/plugin.js | 6 + .../plugins/image/dialogs/image.js | 13 + .../plugins/link/dialogs/anchor.js | 6 + .../plugins/link/dialogs/link.js | 12 + .../plugins/link/images/anchor.gif | Bin 0 -> 184 bytes .../plugins/liststyle/dialogs/liststyle.js | 7 + .../plugins/pagebreak/images/pagebreak.gif | Bin 0 -> 54 bytes .../plugins/pastefromword/filter/default.js | 11 + .../plugins/pastetext/dialogs/pastetext.js | 6 + .../placeholder/dialogs/placeholder.js | 6 + .../plugins/placeholder/lang/en.js | 6 + .../plugins/placeholder/lang/he.js | 6 + .../plugins/placeholder/placeholder.gif | Bin 0 -> 96 bytes .../plugins/placeholder/plugin.js | 6 + .../plugins/scayt/dialogs/options.js | 8 + .../plugins/scayt/dialogs/toolbar.css | 6 + .../showblocks/images/block_address.png | Bin 0 -> 171 bytes .../showblocks/images/block_blockquote.png | Bin 0 -> 181 bytes .../plugins/showblocks/images/block_div.png | Bin 0 -> 136 bytes .../plugins/showblocks/images/block_h1.png | Bin 0 -> 127 bytes .../plugins/showblocks/images/block_h2.png | Bin 0 -> 134 bytes .../plugins/showblocks/images/block_h3.png | Bin 0 -> 131 bytes .../plugins/showblocks/images/block_h4.png | Bin 0 -> 133 bytes .../plugins/showblocks/images/block_h5.png | Bin 0 -> 133 bytes .../plugins/showblocks/images/block_h6.png | Bin 0 -> 129 bytes .../plugins/showblocks/images/block_p.png | Bin 0 -> 119 bytes .../plugins/showblocks/images/block_pre.png | Bin 0 -> 136 bytes .../plugins/smiley/dialogs/smiley.js | 7 + .../plugins/smiley/images/angel_smile.gif | Bin 0 -> 465 bytes .../plugins/smiley/images/angry_smile.gif | Bin 0 -> 443 bytes .../plugins/smiley/images/broken_heart.gif | Bin 0 -> 192 bytes .../plugins/smiley/images/confused_smile.gif | Bin 0 -> 464 bytes .../plugins/smiley/images/cry_smile.gif | Bin 0 -> 468 bytes .../plugins/smiley/images/devil_smile.gif | Bin 0 -> 436 bytes .../smiley/images/embaressed_smile.gif | Bin 0 -> 442 bytes .../plugins/smiley/images/envelope.gif | Bin 0 -> 426 bytes .../plugins/smiley/images/heart.gif | Bin 0 -> 183 bytes .../plugins/smiley/images/kiss.gif | Bin 0 -> 241 bytes .../plugins/smiley/images/lightbulb.gif | Bin 0 -> 368 bytes .../plugins/smiley/images/omg_smile.gif | Bin 0 -> 451 bytes .../plugins/smiley/images/regular_smile.gif | Bin 0 -> 450 bytes .../plugins/smiley/images/sad_smile.gif | Bin 0 -> 460 bytes .../plugins/smiley/images/shades_smile.gif | Bin 0 -> 449 bytes .../plugins/smiley/images/teeth_smile.gif | Bin 0 -> 442 bytes .../plugins/smiley/images/thumbs_down.gif | Bin 0 -> 408 bytes .../plugins/smiley/images/thumbs_up.gif | Bin 0 -> 396 bytes .../plugins/smiley/images/tounge_smile.gif | Bin 0 -> 446 bytes .../images/whatchutalkingabout_smile.gif | Bin 0 -> 452 bytes .../plugins/smiley/images/wink_smile.gif | Bin 0 -> 458 bytes .../specialchar/dialogs/specialchar.js | 7 + .../plugins/specialchar/lang/en.js | 6 + .../plugins/styles/styles/default.js | 6 + .../plugins/stylesheetparser/plugin.js | 6 + .../plugins/table/dialogs/table.js | 9 + .../plugins/tableresize/plugin.js | 7 + .../plugins/tabletools/dialogs/tableCell.js | 8 + .../plugins/templates/dialogs/templates.js | 7 + .../plugins/templates/templates/default.js | 6 + .../templates/templates/images/template1.gif | Bin 0 -> 375 bytes .../templates/templates/images/template2.gif | Bin 0 -> 333 bytes .../templates/templates/images/template3.gif | Bin 0 -> 422 bytes .../plugins/uicolor/dialogs/uicolor.js | 7 + .../wysiwyg-editor/plugins/uicolor/lang/en.js | 6 + .../wysiwyg-editor/plugins/uicolor/lang/he.js | 6 + .../wysiwyg-editor/plugins/uicolor/plugin.js | 6 + .../plugins/uicolor/uicolor.gif | Bin 0 -> 1108 bytes .../plugins/uicolor/yui/assets/hue_bg.png | Bin 0 -> 1120 bytes .../plugins/uicolor/yui/assets/hue_thumb.png | Bin 0 -> 195 bytes .../uicolor/yui/assets/picker_mask.png | Bin 0 -> 12174 bytes .../uicolor/yui/assets/picker_thumb.png | Bin 0 -> 192 bytes .../plugins/uicolor/yui/assets/yui.css | 6 + .../wysiwyg-editor/plugins/uicolor/yui/yui.js | 76 + .../plugins/wsc/dialogs/ciframe.html | 49 + .../plugins/wsc/dialogs/tmpFrameset.html | 52 + .../plugins/wsc/dialogs/wsc.css | 6 + .../wysiwyg-editor/plugins/wsc/dialogs/wsc.js | 7 + .../wysiwyg-editor/plugins/xml/plugin.js | 6 + qa-plugin/wysiwyg-editor/qa-plugin.php | 53 + .../wysiwyg-editor/qa-wysiwyg-editor.php | 219 ++ .../wysiwyg-editor/qa-wysiwyg-upload.php | 130 ++ .../wysiwyg-editor/skins/kama/dialog.css | 10 + .../wysiwyg-editor/skins/kama/editor.css | 13 + qa-plugin/wysiwyg-editor/skins/kama/icons.png | Bin 0 -> 5598 bytes .../wysiwyg-editor/skins/kama/icons_rtl.png | Bin 0 -> 5600 bytes .../skins/kama/images/dialog_sides.gif | Bin 0 -> 48 bytes .../skins/kama/images/dialog_sides.png | Bin 0 -> 178 bytes .../skins/kama/images/dialog_sides_rtl.png | Bin 0 -> 181 bytes .../wysiwyg-editor/skins/kama/images/mini.gif | Bin 0 -> 183 bytes .../skins/kama/images/noimage.png | Bin 0 -> 2115 bytes .../skins/kama/images/sprites.png | Bin 0 -> 7086 bytes .../skins/kama/images/sprites_ie6.png | Bin 0 -> 2724 bytes .../skins/kama/images/toolbar_start.gif | Bin 0 -> 105 bytes qa-plugin/wysiwyg-editor/skins/kama/skin.js | 7 + .../wysiwyg-editor/skins/kama/templates.css | 6 + .../skins/office2003/dialog.css | 9 + .../skins/office2003/editor.css | 14 + .../wysiwyg-editor/skins/office2003/icons.png | Bin 0 -> 5598 bytes .../skins/office2003/icons_rtl.png | Bin 0 -> 5600 bytes .../skins/office2003/images/dialog_sides.gif | Bin 0 -> 48 bytes .../skins/office2003/images/dialog_sides.png | Bin 0 -> 178 bytes .../office2003/images/dialog_sides_rtl.png | Bin 0 -> 181 bytes .../skins/office2003/images/mini.gif | Bin 0 -> 183 bytes .../skins/office2003/images/noimage.png | Bin 0 -> 2115 bytes .../skins/office2003/images/sprites.png | Bin 0 -> 6119 bytes .../skins/office2003/images/sprites_ie6.png | Bin 0 -> 2715 bytes .../wysiwyg-editor/skins/office2003/skin.js | 6 + .../skins/office2003/templates.css | 6 + qa-plugin/wysiwyg-editor/skins/v2/dialog.css | 9 + qa-plugin/wysiwyg-editor/skins/v2/editor.css | 13 + qa-plugin/wysiwyg-editor/skins/v2/icons.png | Bin 0 -> 5598 bytes .../wysiwyg-editor/skins/v2/icons_rtl.png | Bin 0 -> 5600 bytes .../skins/v2/images/dialog_sides.gif | Bin 0 -> 48 bytes .../skins/v2/images/dialog_sides.png | Bin 0 -> 178 bytes .../skins/v2/images/dialog_sides_rtl.png | Bin 0 -> 181 bytes .../wysiwyg-editor/skins/v2/images/mini.gif | Bin 0 -> 183 bytes .../skins/v2/images/noimage.png | Bin 0 -> 2115 bytes .../skins/v2/images/sprites.png | Bin 0 -> 5389 bytes .../skins/v2/images/sprites_ie6.png | Bin 0 -> 492 bytes .../skins/v2/images/toolbar_start.gif | Bin 0 -> 105 bytes qa-plugin/wysiwyg-editor/skins/v2/skin.js | 6 + .../wysiwyg-editor/skins/v2/templates.css | 6 + .../wysiwyg-editor/themes/default/theme.js | 8 + qa-plugin/xml-sitemap/qa-plugin.php | 52 + qa-plugin/xml-sitemap/qa-xml-sitemap.php | 301 +++ qa-theme/Candy/a-count-icon-ie6.png | Bin 0 -> 599 bytes qa-theme/Candy/a-count-icon.png | Bin 0 -> 1571 bytes qa-theme/Candy/answer-icon-ie6.png | Bin 0 -> 673 bytes qa-theme/Candy/answer-icon.png | Bin 0 -> 1139 bytes qa-theme/Candy/approve-icon.png | Bin 0 -> 1701 bytes qa-theme/Candy/button-bg.png | Bin 0 -> 210 bytes qa-theme/Candy/button-hover-bg.png | Bin 0 -> 219 bytes qa-theme/Candy/claim-icon-ie6.png | Bin 0 -> 786 bytes qa-theme/Candy/claim-icon.png | Bin 0 -> 997 bytes qa-theme/Candy/close-icon.png | Bin 0 -> 1945 bytes qa-theme/Candy/comment-icon-ie6.png | Bin 0 -> 300 bytes qa-theme/Candy/comment-icon.png | Bin 0 -> 645 bytes qa-theme/Candy/delete-icon-ie6.png | Bin 0 -> 3948 bytes qa-theme/Candy/delete-icon.png | Bin 0 -> 3871 bytes qa-theme/Candy/edit-icon-ie6.png | Bin 0 -> 849 bytes qa-theme/Candy/edit-icon.png | Bin 0 -> 1149 bytes qa-theme/Candy/error-bg.png | Bin 0 -> 158 bytes qa-theme/Candy/favorite-heart.png | Bin 0 -> 10883 bytes qa-theme/Candy/feed-icon-14x14.png | Bin 0 -> 689 bytes qa-theme/Candy/flag-icon-ie6.png | Bin 0 -> 902 bytes qa-theme/Candy/flag-icon.png | Bin 0 -> 1067 bytes qa-theme/Candy/follow-icon-ie6.png | Bin 0 -> 767 bytes qa-theme/Candy/follow-icon.png | Bin 0 -> 956 bytes qa-theme/Candy/footer-bg.png | Bin 0 -> 204 bytes qa-theme/Candy/form-cell-bg.png | Bin 0 -> 890 bytes qa-theme/Candy/hidden.png | Bin 0 -> 415 bytes qa-theme/Candy/hide-icon-ie6.png | Bin 0 -> 1215 bytes qa-theme/Candy/hide-icon.png | Bin 0 -> 1043 bytes qa-theme/Candy/nav-main-bg.png | Bin 0 -> 368 bytes qa-theme/Candy/nav-main-sel-bg.png | Bin 0 -> 269 bytes qa-theme/Candy/nav-sub-bg.png | Bin 0 -> 181 bytes qa-theme/Candy/page-bg.png | Bin 0 -> 330 bytes qa-theme/Candy/post-bg.png | Bin 0 -> 143 bytes qa-theme/Candy/post-selected-bg.png | Bin 0 -> 190 bytes qa-theme/Candy/qa-styles.css | 473 ++++ qa-theme/Candy/qa-theme.php | 39 + qa-theme/Candy/reject-icon.png | Bin 0 -> 1937 bytes qa-theme/Candy/reopen-icon.png | Bin 0 -> 1807 bytes qa-theme/Candy/reshow-icon-ie6.png | Bin 0 -> 859 bytes qa-theme/Candy/reshow-icon.png | Bin 0 -> 994 bytes qa-theme/Candy/select-star.gif | Bin 0 -> 4000 bytes qa-theme/Candy/selected-star.gif | Bin 0 -> 4117 bytes qa-theme/Candy/sidebar-bg.png | Bin 0 -> 212 bytes qa-theme/Candy/suggest-bg.png | Bin 0 -> 179 bytes qa-theme/Candy/tag-icon-ie6.png | Bin 0 -> 692 bytes qa-theme/Candy/tag-icon.png | Bin 0 -> 563 bytes qa-theme/Candy/unflag-icon-ie6.png | Bin 0 -> 906 bytes qa-theme/Candy/unflag-icon.png | Bin 0 -> 1111 bytes qa-theme/Candy/vote-buttons-ie6.png | Bin 0 -> 3736 bytes qa-theme/Candy/vote-buttons.png | Bin 0 -> 11398 bytes qa-theme/Default/favorite-plus.png | Bin 0 -> 2698 bytes qa-theme/Default/feed-icon-14x14.png | Bin 0 -> 689 bytes qa-theme/Default/hidden.png | Bin 0 -> 415 bytes qa-theme/Default/qa-styles.css | 416 ++++ qa-theme/Default/select-star.png | Bin 0 -> 1770 bytes qa-theme/Default/selected-star.png | Bin 0 -> 1693 bytes qa-theme/Default/vote-buttons.gif | Bin 0 -> 1954 bytes 458 files changed, 51345 insertions(+) create mode 100644 .htaccess create mode 100644 CHANGELOG.html create mode 100644 LICENSE.html create mode 100644 README.html create mode 100644 index.php create mode 100644 qa-config-example.php create mode 100644 qa-content/jquery-1.7.1.min.js create mode 100644 qa-content/qa-admin.js create mode 100644 qa-content/qa-ask.js create mode 100644 qa-content/qa-page.js create mode 100644 qa-content/qa-question.js create mode 100644 qa-external-example/qa-external-users.php create mode 100644 qa-include/qa-ajax-answer.php create mode 100644 qa-include/qa-ajax-asktitle.php create mode 100644 qa-include/qa-ajax-category.php create mode 100644 qa-include/qa-ajax-click-admin.php create mode 100644 qa-include/qa-ajax-click-answer.php create mode 100644 qa-include/qa-ajax-click-comment.php create mode 100644 qa-include/qa-ajax-comment.php create mode 100644 qa-include/qa-ajax-favorite.php create mode 100644 qa-include/qa-ajax-mailing.php create mode 100644 qa-include/qa-ajax-notice.php create mode 100644 qa-include/qa-ajax-recalc.php create mode 100644 qa-include/qa-ajax-show-comments.php create mode 100644 qa-include/qa-ajax-version.php create mode 100644 qa-include/qa-ajax-vote.php create mode 100644 qa-include/qa-ajax.php create mode 100644 qa-include/qa-app-admin.php create mode 100644 qa-include/qa-app-blobs.php create mode 100644 qa-include/qa-app-captcha.php create mode 100644 qa-include/qa-app-cookies.php create mode 100644 qa-include/qa-app-emails.php create mode 100644 qa-include/qa-app-events.php create mode 100644 qa-include/qa-app-favorites.php create mode 100644 qa-include/qa-app-format.php create mode 100644 qa-include/qa-app-limits.php create mode 100644 qa-include/qa-app-mailing.php create mode 100644 qa-include/qa-app-options.php create mode 100644 qa-include/qa-app-post-create.php create mode 100644 qa-include/qa-app-post-update.php create mode 100644 qa-include/qa-app-posts.php create mode 100644 qa-include/qa-app-q-list.php create mode 100644 qa-include/qa-app-recalc.php create mode 100644 qa-include/qa-app-search.php create mode 100644 qa-include/qa-app-updates.php create mode 100644 qa-include/qa-app-users-edit.php create mode 100644 qa-include/qa-app-users.php create mode 100644 qa-include/qa-app-votes.php create mode 100644 qa-include/qa-base.php create mode 100644 qa-include/qa-blob.php create mode 100644 qa-include/qa-check-lang.php create mode 100644 qa-include/qa-class.phpmailer.php create mode 100644 qa-include/qa-class.smtp.php create mode 100644 qa-include/qa-db-admin.php create mode 100644 qa-include/qa-db-blobs.php create mode 100644 qa-include/qa-db-cache.php create mode 100644 qa-include/qa-db-cookies.php create mode 100644 qa-include/qa-db-events.php create mode 100644 qa-include/qa-db-favorites.php create mode 100644 qa-include/qa-db-hotness.php create mode 100644 qa-include/qa-db-install.php create mode 100644 qa-include/qa-db-limits.php create mode 100644 qa-include/qa-db-maxima.php create mode 100644 qa-include/qa-db-messages.php create mode 100644 qa-include/qa-db-metas.php create mode 100644 qa-include/qa-db-notices.php create mode 100644 qa-include/qa-db-options.php create mode 100644 qa-include/qa-db-points.php create mode 100644 qa-include/qa-db-post-create.php create mode 100644 qa-include/qa-db-post-update.php create mode 100644 qa-include/qa-db-recalc.php create mode 100644 qa-include/qa-db-selects.php create mode 100644 qa-include/qa-db-users.php create mode 100644 qa-include/qa-db-votes.php create mode 100644 qa-include/qa-db.php create mode 100644 qa-include/qa-editor-basic.php create mode 100644 qa-include/qa-event-limits.php create mode 100644 qa-include/qa-event-notify.php create mode 100644 qa-include/qa-event-updates.php create mode 100644 qa-include/qa-external-users-wp.php create mode 100644 qa-include/qa-feed.php create mode 100644 qa-include/qa-filter-basic.php create mode 100644 qa-include/qa-htmLawed.php create mode 100644 qa-include/qa-image.php create mode 100644 qa-include/qa-index.php create mode 100644 qa-include/qa-install.php create mode 100644 qa-include/qa-lang-admin.php create mode 100644 qa-include/qa-lang-emails.php create mode 100644 qa-include/qa-lang-main.php create mode 100644 qa-include/qa-lang-misc.php create mode 100644 qa-include/qa-lang-options.php create mode 100644 qa-include/qa-lang-profile.php create mode 100644 qa-include/qa-lang-question.php create mode 100644 qa-include/qa-lang-users.php create mode 100644 qa-include/qa-page-account.php create mode 100644 qa-include/qa-page-activity.php create mode 100644 qa-include/qa-page-admin-categories.php create mode 100644 qa-include/qa-page-admin-default.php create mode 100644 qa-include/qa-page-admin-flagged.php create mode 100644 qa-include/qa-page-admin-hidden.php create mode 100644 qa-include/qa-page-admin-moderate.php create mode 100644 qa-include/qa-page-admin-pages.php create mode 100644 qa-include/qa-page-admin-plugins.php create mode 100644 qa-include/qa-page-admin-points.php create mode 100644 qa-include/qa-page-admin-recalc.php create mode 100644 qa-include/qa-page-admin-stats.php create mode 100644 qa-include/qa-page-admin-userfields.php create mode 100644 qa-include/qa-page-admin-usertitles.php create mode 100644 qa-include/qa-page-admin-widgets.php create mode 100644 qa-include/qa-page-answers.php create mode 100644 qa-include/qa-page-ask.php create mode 100644 qa-include/qa-page-categories.php create mode 100644 qa-include/qa-page-comments.php create mode 100644 qa-include/qa-page-confirm.php create mode 100644 qa-include/qa-page-default.php create mode 100644 qa-include/qa-page-favorites.php create mode 100644 qa-include/qa-page-feedback.php create mode 100644 qa-include/qa-page-forgot.php create mode 100644 qa-include/qa-page-hot.php create mode 100644 qa-include/qa-page-ip.php create mode 100644 qa-include/qa-page-login.php create mode 100644 qa-include/qa-page-logout.php create mode 100644 qa-include/qa-page-message.php create mode 100644 qa-include/qa-page-not-found.php create mode 100644 qa-include/qa-page-question-post.php create mode 100644 qa-include/qa-page-question-submit.php create mode 100644 qa-include/qa-page-question-view.php create mode 100644 qa-include/qa-page-question.php create mode 100644 qa-include/qa-page-questions.php create mode 100644 qa-include/qa-page-register.php create mode 100644 qa-include/qa-page-reset.php create mode 100644 qa-include/qa-page-search.php create mode 100644 qa-include/qa-page-tag.php create mode 100644 qa-include/qa-page-tags.php create mode 100644 qa-include/qa-page-unanswered.php create mode 100644 qa-include/qa-page-unsubscribe.php create mode 100644 qa-include/qa-page-updates.php create mode 100644 qa-include/qa-page-user.php create mode 100644 qa-include/qa-page-users-blocked.php create mode 100644 qa-include/qa-page-users-special.php create mode 100644 qa-include/qa-page-users.php create mode 100644 qa-include/qa-page.php create mode 100644 qa-include/qa-search-basic.php create mode 100644 qa-include/qa-theme-base.php create mode 100644 qa-include/qa-url-test.php create mode 100644 qa-include/qa-util-debug.php create mode 100644 qa-include/qa-util-image.php create mode 100644 qa-include/qa-util-sort.php create mode 100644 qa-include/qa-util-string.php create mode 100644 qa-include/qa-viewer-basic.php create mode 100644 qa-include/qa-widget-activity-count.php create mode 100644 qa-include/qa-widget-ask-box.php create mode 100644 qa-include/qa-widget-related-qs.php create mode 100644 qa-lang/en-GB/qa-lang-admin.php create mode 100644 qa-plugin/basic-adsense/qa-basic-adsense.php create mode 100644 qa-plugin/basic-adsense/qa-plugin.php create mode 100644 qa-plugin/event-logger/qa-event-logger.php create mode 100644 qa-plugin/event-logger/qa-plugin.php create mode 100644 qa-plugin/example-page/qa-example-lang-default.php create mode 100644 qa-plugin/example-page/qa-example-lang-en-GB.php create mode 100644 qa-plugin/example-page/qa-example-page.php create mode 100644 qa-plugin/example-page/qa-plugin.php create mode 100644 qa-plugin/facebook-login/JSON.php create mode 100644 qa-plugin/facebook-login/base_facebook.php create mode 100644 qa-plugin/facebook-login/facebook.php create mode 100644 qa-plugin/facebook-login/qa-facebook-login.php create mode 100644 qa-plugin/facebook-login/qa-plugin.php create mode 100644 qa-plugin/mouseover-layer/qa-mouseover-admin-form.php create mode 100644 qa-plugin/mouseover-layer/qa-mouseover-layer.php create mode 100644 qa-plugin/mouseover-layer/qa-plugin.php create mode 100644 qa-plugin/recaptcha-captcha/qa-plugin.php create mode 100644 qa-plugin/recaptcha-captcha/qa-recaptcha-captcha.php create mode 100644 qa-plugin/recaptcha-captcha/recaptchalib.php create mode 100644 qa-plugin/tag-cloud-widget/qa-plugin.php create mode 100644 qa-plugin/tag-cloud-widget/qa-tag-cloud.php create mode 100644 qa-plugin/wysiwyg-editor/.htaccess create mode 100644 qa-plugin/wysiwyg-editor/LICENSE.html create mode 100644 qa-plugin/wysiwyg-editor/adapters/jquery.js create mode 100644 qa-plugin/wysiwyg-editor/ckeditor.js create mode 100644 qa-plugin/wysiwyg-editor/ckeditor_basic.js create mode 100644 qa-plugin/wysiwyg-editor/ckeditor_basic_source.js create mode 100644 qa-plugin/wysiwyg-editor/ckeditor_source.js create mode 100644 qa-plugin/wysiwyg-editor/config.js create mode 100644 qa-plugin/wysiwyg-editor/contents.css create mode 100644 qa-plugin/wysiwyg-editor/images/spacer.gif create mode 100644 qa-plugin/wysiwyg-editor/lang/_languages.js create mode 100644 qa-plugin/wysiwyg-editor/lang/_translationstatus.txt create mode 100644 qa-plugin/wysiwyg-editor/lang/af.js create mode 100644 qa-plugin/wysiwyg-editor/lang/ar.js create mode 100644 qa-plugin/wysiwyg-editor/lang/bg.js create mode 100644 qa-plugin/wysiwyg-editor/lang/bn.js create mode 100644 qa-plugin/wysiwyg-editor/lang/bs.js create mode 100644 qa-plugin/wysiwyg-editor/lang/ca.js create mode 100644 qa-plugin/wysiwyg-editor/lang/cs.js create mode 100644 qa-plugin/wysiwyg-editor/lang/cy.js create mode 100644 qa-plugin/wysiwyg-editor/lang/da.js create mode 100644 qa-plugin/wysiwyg-editor/lang/de.js create mode 100644 qa-plugin/wysiwyg-editor/lang/el.js create mode 100644 qa-plugin/wysiwyg-editor/lang/en-au.js create mode 100644 qa-plugin/wysiwyg-editor/lang/en-ca.js create mode 100644 qa-plugin/wysiwyg-editor/lang/en-gb.js create mode 100644 qa-plugin/wysiwyg-editor/lang/en.js create mode 100644 qa-plugin/wysiwyg-editor/lang/eo.js create mode 100644 qa-plugin/wysiwyg-editor/lang/es.js create mode 100644 qa-plugin/wysiwyg-editor/lang/et.js create mode 100644 qa-plugin/wysiwyg-editor/lang/eu.js create mode 100644 qa-plugin/wysiwyg-editor/lang/fa.js create mode 100644 qa-plugin/wysiwyg-editor/lang/fi.js create mode 100644 qa-plugin/wysiwyg-editor/lang/fo.js create mode 100644 qa-plugin/wysiwyg-editor/lang/fr-ca.js create mode 100644 qa-plugin/wysiwyg-editor/lang/fr.js create mode 100644 qa-plugin/wysiwyg-editor/lang/gl.js create mode 100644 qa-plugin/wysiwyg-editor/lang/gu.js create mode 100644 qa-plugin/wysiwyg-editor/lang/he.js create mode 100644 qa-plugin/wysiwyg-editor/lang/hi.js create mode 100644 qa-plugin/wysiwyg-editor/lang/hr.js create mode 100644 qa-plugin/wysiwyg-editor/lang/hu.js create mode 100644 qa-plugin/wysiwyg-editor/lang/is.js create mode 100644 qa-plugin/wysiwyg-editor/lang/it.js create mode 100644 qa-plugin/wysiwyg-editor/lang/ja.js create mode 100644 qa-plugin/wysiwyg-editor/lang/ka.js create mode 100644 qa-plugin/wysiwyg-editor/lang/km.js create mode 100644 qa-plugin/wysiwyg-editor/lang/ko.js create mode 100644 qa-plugin/wysiwyg-editor/lang/lt.js create mode 100644 qa-plugin/wysiwyg-editor/lang/lv.js create mode 100644 qa-plugin/wysiwyg-editor/lang/mn.js create mode 100644 qa-plugin/wysiwyg-editor/lang/ms.js create mode 100644 qa-plugin/wysiwyg-editor/lang/nb.js create mode 100644 qa-plugin/wysiwyg-editor/lang/nl.js create mode 100644 qa-plugin/wysiwyg-editor/lang/no.js create mode 100644 qa-plugin/wysiwyg-editor/lang/pl.js create mode 100644 qa-plugin/wysiwyg-editor/lang/pt-br.js create mode 100644 qa-plugin/wysiwyg-editor/lang/pt.js create mode 100644 qa-plugin/wysiwyg-editor/lang/ro.js create mode 100644 qa-plugin/wysiwyg-editor/lang/ru.js create mode 100644 qa-plugin/wysiwyg-editor/lang/sk.js create mode 100644 qa-plugin/wysiwyg-editor/lang/sl.js create mode 100644 qa-plugin/wysiwyg-editor/lang/sr-latn.js create mode 100644 qa-plugin/wysiwyg-editor/lang/sr.js create mode 100644 qa-plugin/wysiwyg-editor/lang/sv.js create mode 100644 qa-plugin/wysiwyg-editor/lang/th.js create mode 100644 qa-plugin/wysiwyg-editor/lang/tr.js create mode 100644 qa-plugin/wysiwyg-editor/lang/uk.js create mode 100644 qa-plugin/wysiwyg-editor/lang/vi.js create mode 100644 qa-plugin/wysiwyg-editor/lang/zh-cn.js create mode 100644 qa-plugin/wysiwyg-editor/lang/zh.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/a11yhelp/dialogs/a11yhelp.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/a11yhelp/lang/en.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/a11yhelp/lang/he.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/about/dialogs/about.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/about/dialogs/logo_ckeditor.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/adobeair/plugin.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/ajax/plugin.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/autogrow/plugin.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/bbcode/plugin.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/clipboard/dialogs/paste.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/colordialog/dialogs/colordialog.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/devtools/lang/en.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/devtools/plugin.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/dialog/dialogDefinition.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/div/dialogs/div.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/docprops/dialogs/docprops.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/docprops/plugin.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/find/dialogs/find.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/flash/dialogs/flash.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/flash/images/placeholder.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/forms/dialogs/button.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/forms/dialogs/checkbox.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/forms/dialogs/form.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/forms/dialogs/hiddenfield.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/forms/dialogs/radio.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/forms/dialogs/select.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/forms/dialogs/textarea.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/forms/dialogs/textfield.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/forms/images/hiddenfield.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/iframe/dialogs/iframe.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/iframe/images/placeholder.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/iframedialog/plugin.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/image/dialogs/image.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/link/dialogs/anchor.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/link/dialogs/link.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/link/images/anchor.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/liststyle/dialogs/liststyle.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/pagebreak/images/pagebreak.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/pastefromword/filter/default.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/pastetext/dialogs/pastetext.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/placeholder/dialogs/placeholder.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/placeholder/lang/en.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/placeholder/lang/he.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/placeholder/placeholder.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/placeholder/plugin.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/scayt/dialogs/options.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/scayt/dialogs/toolbar.css create mode 100644 qa-plugin/wysiwyg-editor/plugins/showblocks/images/block_address.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/showblocks/images/block_blockquote.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/showblocks/images/block_div.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/showblocks/images/block_h1.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/showblocks/images/block_h2.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/showblocks/images/block_h3.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/showblocks/images/block_h4.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/showblocks/images/block_h5.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/showblocks/images/block_h6.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/showblocks/images/block_p.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/showblocks/images/block_pre.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/dialogs/smiley.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/angel_smile.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/angry_smile.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/broken_heart.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/confused_smile.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/cry_smile.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/devil_smile.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/embaressed_smile.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/envelope.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/heart.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/kiss.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/lightbulb.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/omg_smile.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/regular_smile.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/sad_smile.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/shades_smile.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/teeth_smile.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/thumbs_down.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/thumbs_up.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/tounge_smile.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/whatchutalkingabout_smile.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/smiley/images/wink_smile.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/specialchar/dialogs/specialchar.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/specialchar/lang/en.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/styles/styles/default.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/stylesheetparser/plugin.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/table/dialogs/table.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/tableresize/plugin.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/tabletools/dialogs/tableCell.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/templates/dialogs/templates.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/templates/templates/default.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/templates/templates/images/template1.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/templates/templates/images/template2.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/templates/templates/images/template3.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/uicolor/dialogs/uicolor.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/uicolor/lang/en.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/uicolor/lang/he.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/uicolor/plugin.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/uicolor/uicolor.gif create mode 100644 qa-plugin/wysiwyg-editor/plugins/uicolor/yui/assets/hue_bg.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/uicolor/yui/assets/hue_thumb.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/uicolor/yui/assets/picker_mask.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/uicolor/yui/assets/picker_thumb.png create mode 100644 qa-plugin/wysiwyg-editor/plugins/uicolor/yui/assets/yui.css create mode 100644 qa-plugin/wysiwyg-editor/plugins/uicolor/yui/yui.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/wsc/dialogs/ciframe.html create mode 100644 qa-plugin/wysiwyg-editor/plugins/wsc/dialogs/tmpFrameset.html create mode 100644 qa-plugin/wysiwyg-editor/plugins/wsc/dialogs/wsc.css create mode 100644 qa-plugin/wysiwyg-editor/plugins/wsc/dialogs/wsc.js create mode 100644 qa-plugin/wysiwyg-editor/plugins/xml/plugin.js create mode 100644 qa-plugin/wysiwyg-editor/qa-plugin.php create mode 100644 qa-plugin/wysiwyg-editor/qa-wysiwyg-editor.php create mode 100644 qa-plugin/wysiwyg-editor/qa-wysiwyg-upload.php create mode 100644 qa-plugin/wysiwyg-editor/skins/kama/dialog.css create mode 100644 qa-plugin/wysiwyg-editor/skins/kama/editor.css create mode 100644 qa-plugin/wysiwyg-editor/skins/kama/icons.png create mode 100644 qa-plugin/wysiwyg-editor/skins/kama/icons_rtl.png create mode 100644 qa-plugin/wysiwyg-editor/skins/kama/images/dialog_sides.gif create mode 100644 qa-plugin/wysiwyg-editor/skins/kama/images/dialog_sides.png create mode 100644 qa-plugin/wysiwyg-editor/skins/kama/images/dialog_sides_rtl.png create mode 100644 qa-plugin/wysiwyg-editor/skins/kama/images/mini.gif create mode 100644 qa-plugin/wysiwyg-editor/skins/kama/images/noimage.png create mode 100644 qa-plugin/wysiwyg-editor/skins/kama/images/sprites.png create mode 100644 qa-plugin/wysiwyg-editor/skins/kama/images/sprites_ie6.png create mode 100644 qa-plugin/wysiwyg-editor/skins/kama/images/toolbar_start.gif create mode 100644 qa-plugin/wysiwyg-editor/skins/kama/skin.js create mode 100644 qa-plugin/wysiwyg-editor/skins/kama/templates.css create mode 100644 qa-plugin/wysiwyg-editor/skins/office2003/dialog.css create mode 100644 qa-plugin/wysiwyg-editor/skins/office2003/editor.css create mode 100644 qa-plugin/wysiwyg-editor/skins/office2003/icons.png create mode 100644 qa-plugin/wysiwyg-editor/skins/office2003/icons_rtl.png create mode 100644 qa-plugin/wysiwyg-editor/skins/office2003/images/dialog_sides.gif create mode 100644 qa-plugin/wysiwyg-editor/skins/office2003/images/dialog_sides.png create mode 100644 qa-plugin/wysiwyg-editor/skins/office2003/images/dialog_sides_rtl.png create mode 100644 qa-plugin/wysiwyg-editor/skins/office2003/images/mini.gif create mode 100644 qa-plugin/wysiwyg-editor/skins/office2003/images/noimage.png create mode 100644 qa-plugin/wysiwyg-editor/skins/office2003/images/sprites.png create mode 100644 qa-plugin/wysiwyg-editor/skins/office2003/images/sprites_ie6.png create mode 100644 qa-plugin/wysiwyg-editor/skins/office2003/skin.js create mode 100644 qa-plugin/wysiwyg-editor/skins/office2003/templates.css create mode 100644 qa-plugin/wysiwyg-editor/skins/v2/dialog.css create mode 100644 qa-plugin/wysiwyg-editor/skins/v2/editor.css create mode 100644 qa-plugin/wysiwyg-editor/skins/v2/icons.png create mode 100644 qa-plugin/wysiwyg-editor/skins/v2/icons_rtl.png create mode 100644 qa-plugin/wysiwyg-editor/skins/v2/images/dialog_sides.gif create mode 100644 qa-plugin/wysiwyg-editor/skins/v2/images/dialog_sides.png create mode 100644 qa-plugin/wysiwyg-editor/skins/v2/images/dialog_sides_rtl.png create mode 100644 qa-plugin/wysiwyg-editor/skins/v2/images/mini.gif create mode 100644 qa-plugin/wysiwyg-editor/skins/v2/images/noimage.png create mode 100644 qa-plugin/wysiwyg-editor/skins/v2/images/sprites.png create mode 100644 qa-plugin/wysiwyg-editor/skins/v2/images/sprites_ie6.png create mode 100644 qa-plugin/wysiwyg-editor/skins/v2/images/toolbar_start.gif create mode 100644 qa-plugin/wysiwyg-editor/skins/v2/skin.js create mode 100644 qa-plugin/wysiwyg-editor/skins/v2/templates.css create mode 100644 qa-plugin/wysiwyg-editor/themes/default/theme.js create mode 100644 qa-plugin/xml-sitemap/qa-plugin.php create mode 100644 qa-plugin/xml-sitemap/qa-xml-sitemap.php create mode 100644 qa-theme/Candy/a-count-icon-ie6.png create mode 100644 qa-theme/Candy/a-count-icon.png create mode 100644 qa-theme/Candy/answer-icon-ie6.png create mode 100644 qa-theme/Candy/answer-icon.png create mode 100644 qa-theme/Candy/approve-icon.png create mode 100644 qa-theme/Candy/button-bg.png create mode 100644 qa-theme/Candy/button-hover-bg.png create mode 100644 qa-theme/Candy/claim-icon-ie6.png create mode 100644 qa-theme/Candy/claim-icon.png create mode 100644 qa-theme/Candy/close-icon.png create mode 100644 qa-theme/Candy/comment-icon-ie6.png create mode 100644 qa-theme/Candy/comment-icon.png create mode 100644 qa-theme/Candy/delete-icon-ie6.png create mode 100644 qa-theme/Candy/delete-icon.png create mode 100644 qa-theme/Candy/edit-icon-ie6.png create mode 100644 qa-theme/Candy/edit-icon.png create mode 100644 qa-theme/Candy/error-bg.png create mode 100644 qa-theme/Candy/favorite-heart.png create mode 100644 qa-theme/Candy/feed-icon-14x14.png create mode 100644 qa-theme/Candy/flag-icon-ie6.png create mode 100644 qa-theme/Candy/flag-icon.png create mode 100644 qa-theme/Candy/follow-icon-ie6.png create mode 100644 qa-theme/Candy/follow-icon.png create mode 100644 qa-theme/Candy/footer-bg.png create mode 100644 qa-theme/Candy/form-cell-bg.png create mode 100644 qa-theme/Candy/hidden.png create mode 100644 qa-theme/Candy/hide-icon-ie6.png create mode 100644 qa-theme/Candy/hide-icon.png create mode 100644 qa-theme/Candy/nav-main-bg.png create mode 100644 qa-theme/Candy/nav-main-sel-bg.png create mode 100644 qa-theme/Candy/nav-sub-bg.png create mode 100644 qa-theme/Candy/page-bg.png create mode 100644 qa-theme/Candy/post-bg.png create mode 100644 qa-theme/Candy/post-selected-bg.png create mode 100644 qa-theme/Candy/qa-styles.css create mode 100644 qa-theme/Candy/qa-theme.php create mode 100644 qa-theme/Candy/reject-icon.png create mode 100644 qa-theme/Candy/reopen-icon.png create mode 100644 qa-theme/Candy/reshow-icon-ie6.png create mode 100644 qa-theme/Candy/reshow-icon.png create mode 100644 qa-theme/Candy/select-star.gif create mode 100644 qa-theme/Candy/selected-star.gif create mode 100644 qa-theme/Candy/sidebar-bg.png create mode 100644 qa-theme/Candy/suggest-bg.png create mode 100644 qa-theme/Candy/tag-icon-ie6.png create mode 100644 qa-theme/Candy/tag-icon.png create mode 100644 qa-theme/Candy/unflag-icon-ie6.png create mode 100644 qa-theme/Candy/unflag-icon.png create mode 100644 qa-theme/Candy/vote-buttons-ie6.png create mode 100644 qa-theme/Candy/vote-buttons.png create mode 100644 qa-theme/Default/favorite-plus.png create mode 100644 qa-theme/Default/feed-icon-14x14.png create mode 100644 qa-theme/Default/hidden.png create mode 100644 qa-theme/Default/qa-styles.css create mode 100644 qa-theme/Default/select-star.png create mode 100644 qa-theme/Default/selected-star.png create mode 100644 qa-theme/Default/vote-buttons.gif diff --git a/.htaccess b/.htaccess new file mode 100644 index 000000000..c04cce10f --- /dev/null +++ b/.htaccess @@ -0,0 +1,8 @@ +DirectoryIndex index.php +RewriteEngine On +#RewriteBase /your-sub-directory +RewriteCond %{REQUEST_URI} ^(.*)//(.*)$ +RewriteRule . %1/%2 [R=301,L] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^.*$ index.php?qa-rewrite=$0&%{QUERY_STRING} [L] diff --git a/CHANGELOG.html b/CHANGELOG.html new file mode 100644 index 000000000..be912f891 --- /dev/null +++ b/CHANGELOG.html @@ -0,0 +1,9 @@ + + + + + + + Redirecting... if nothing happens, click here. + + \ No newline at end of file diff --git a/LICENSE.html b/LICENSE.html new file mode 100644 index 000000000..f802697b1 --- /dev/null +++ b/LICENSE.html @@ -0,0 +1,9 @@ + + + + + + + Redirecting... if nothing happens, click here. + + \ No newline at end of file diff --git a/README.html b/README.html new file mode 100644 index 000000000..75699164d --- /dev/null +++ b/README.html @@ -0,0 +1,9 @@ + + + + + + + Redirecting... if nothing happens, click here. + + \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 000000000..84286bc89 --- /dev/null +++ b/index.php @@ -0,0 +1,36 @@ + 'topics', + 'categories' => 'sections', + 'users' => 'contributors', + 'user' => 'contributor', + ); +*/ + +/* + Set QA_EXTERNAL_USERS to true to use your user identification code in qa-external/qa-external-users.php + This allows you to integrate with your existing user database and management system. For more details, + consult the online documentation on installing Question2Answer with single sign-on. + + The constants QA_EXTERNAL_LANG and QA_EXTERNAL_EMAILER are deprecated from Q2A 1.5 since the same + effect can now be achieved in plugins by using function overrides. +*/ + + define('QA_EXTERNAL_USERS', false); + +/* + Out-of-the-box WordPress 3.x integration - to integrate with your WordPress site and user + database, define QA_WORDPRESS_INTEGRATE_PATH as the full path to the WordPress directory + containing wp-load.php. You do not need to set the QA_MYSQL_* constants above since these + will be taken from WordPress automatically. See online documentation for more details. + + define('QA_WORDPRESS_INTEGRATE_PATH', '/PATH/TO/WORDPRESS'); +*/ + +/* + Some settings to help optimize your Question2Answer site's performance. + + If QA_HTML_COMPRESSION is true, HTML web pages will be output using Gzip compression, if + the user's browser indicates this is supported. This will increase the performance of your + site, but may make debugging harder if PHP does not complete execution. + + QA_MAX_LIMIT_START is the maximum start parameter that can be requested, for paging through + long lists of questions, etc... As the start parameter gets higher, queries tend to get + slower, since MySQL must examine more information. Very high start numbers are usually only + requested by search engine robots anyway. + + If a word is used QA_IGNORED_WORDS_FREQ times or more in a particular way, it is ignored + when searching or finding related questions. This saves time by ignoring words which are so + common that they are probably not worth matching on. + + Set QA_ALLOW_UNINDEXED_QUERIES to true if you don't mind running some database queries which + are not indexed efficiently. For example, this will enable browsing unanswered questions per + category. If your database becomes large, these queries could become costly. + + Set QA_OPTIMIZE_LOCAL_DB to true if your web server and MySQL are running on the same box. + When viewing a page on your site, this will use many simple MySQL queries instead of fewer + complex ones, which makes sense since there is no latency for localhost access. + + Set QA_OPTIMIZE_DISTANT_DB to true if your web server and MySQL are far enough apart to + create significant latency. This will minimize the number of database queries as much as + is possible, even at the cost of significant additional processing at each end. + + Set QA_PERSISTENT_CONN_DB to true to use persistent database connections. Only use this if + you are absolutely sure it is a good idea under your setup - generally it is not. + For more information: http://www.php.net/manual/en/features.persistent-connections.php + + Set QA_DEBUG_PERFORMANCE to true to show detailed performance profiling information at the + bottom of every Question2Answer page. +*/ + + define('QA_HTML_COMPRESSION', true); + define('QA_MAX_LIMIT_START', 19999); + define('QA_IGNORED_WORDS_FREQ', 10000); + define('QA_ALLOW_UNINDEXED_QUERIES', false); + define('QA_OPTIMIZE_LOCAL_DB', false); + define('QA_OPTIMIZE_DISTANT_DB', false); + define('QA_PERSISTENT_CONN_DB', false); + define('QA_DEBUG_PERFORMANCE', false); + +/* + And lastly... if you want to, you can predefine any constant from qa-db-maxima.php in this + file to override the default setting. Just make sure you know what you're doing! +*/ + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-content/jquery-1.7.1.min.js b/qa-content/jquery-1.7.1.min.js new file mode 100644 index 000000000..979ed0824 --- /dev/null +++ b/qa-content/jquery-1.7.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.7.1 jquery.com | jquery.org/license */ +(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cv(a){if(!ck[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cl||(cl=c.createElement("iframe"),cl.frameBorder=cl.width=cl.height=0),b.appendChild(cl);if(!cm||!cl.createElement)cm=(cl.contentWindow||cl.contentDocument).document,cm.write((c.compatMode==="CSS1Compat"?"":"")+""),cm.close();d=cm.createElement(a),cm.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cl)}ck[a]=e}return ck[a]}function cu(a,b){var c={};f.each(cq.concat.apply([],cq.slice(0,b)),function(){c[this]=a});return c}function ct(){cr=b}function cs(){setTimeout(ct,0);return cr=f.now()}function cj(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ci(){try{return new a.XMLHttpRequest}catch(b){}}function cc(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){if(c!=="border")for(;g=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?parseFloat(d):j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.1",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
a",d=q.getElementsByTagName("*"),e=q.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=q.getElementsByTagName("input")[0],b={leadingWhitespace:q.firstChild.nodeType===3,tbody:!q.getElementsByTagName("tbody").length,htmlSerialize:!!q.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:q.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete q.test}catch(s){b.deleteExpando=!1}!q.addEventListener&&q.attachEvent&&q.fireEvent&&(q.attachEvent("onclick",function(){b.noCloneEvent=!1}),q.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),q.appendChild(i),k=c.createDocumentFragment(),k.appendChild(q.lastChild),b.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,k.removeChild(i),k.appendChild(q),q.innerHTML="",a.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",q.style.width="2px",q.appendChild(j),b.reliableMarginRight=(parseInt((a.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(q.attachEvent)for(o in{submit:1,change:1,focusin:1})n="on"+o,p=n in q,p||(q.setAttribute(n,"return;"),p=typeof q[n]=="function"),b[o+"Bubbles"]=p;k.removeChild(q),k=g=h=j=q=i=null,f(function(){var a,d,e,g,h,i,j,k,m,n,o,r=c.getElementsByTagName("body")[0];!r||(j=1,k="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",m="visibility:hidden;border:0;",n="style='"+k+"border:5px solid #000;padding:0;'",o="
"+""+"
",a=c.createElement("div"),a.style.cssText=m+"width:0;height:0;position:static;top:0;margin-top:"+j+"px",r.insertBefore(a,r.firstChild),q=c.createElement("div"),a.appendChild(q),q.innerHTML="
t
",l=q.getElementsByTagName("td"),p=l[0].offsetHeight===0,l[0].style.display="",l[1].style.display="none",b.reliableHiddenOffsets=p&&l[0].offsetHeight===0,q.innerHTML="",q.style.width=q.style.paddingLeft="1px",f.boxModel=b.boxModel=q.offsetWidth===2,typeof q.style.zoom!="undefined"&&(q.style.display="inline",q.style.zoom=1,b.inlineBlockNeedsLayout=q.offsetWidth===2,q.style.display="",q.innerHTML="
",b.shrinkWrapBlocks=q.offsetWidth!==2),q.style.cssText=k+m,q.innerHTML=o,d=q.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,i={doesNotAddBorder:e.offsetTop!==5,doesAddBorderForTableAndCells:h.offsetTop===5},e.style.position="fixed",e.style.top="20px",i.fixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",i.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,i.doesNotIncludeMarginInBodyOffset=r.offsetTop!==j,r.removeChild(a),q=a=null,f.extend(b,i))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;h=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/\bhover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")}; +f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&i.push({elem:this,matches:d.slice(e)});for(j=0;j0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
","
"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function() +{for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||!bc.test("<"+a.nodeName)?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");b===c?bh.appendChild(o):U(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return br.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bq,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bq.test(g)?g.replace(bq,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,b){var c,d,e;b=b.replace(bs,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b)));return c}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bt.test(f)&&bu.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bD=/%20/g,bE=/\[\]$/,bF=/\r?\n/g,bG=/#.*$/,bH=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bI=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bJ=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bK=/^(?:GET|HEAD)$/,bL=/^\/\//,bM=/\?/,bN=/)<[^<]*)*<\/script>/gi,bO=/^(?:select|textarea)/i,bP=/\s+/,bQ=/([?&])_=[^&]*/,bR=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bS=f.fn.load,bT={},bU={},bV,bW,bX=["*/"]+["*"];try{bV=e.href}catch(bY){bV=c.createElement("a"),bV.href="",bV=bV.href}bW=bR.exec(bV.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bS)return bS.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bN,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bO.test(this.nodeName)||bI.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bF,"\r\n")}}):{name:b.name,value:c.replace(bF,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b_(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b_(a,b);return a},ajaxSettings:{url:bV,isLocal:bJ.test(bW[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bX},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bZ(bT),ajaxTransport:bZ(bU),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cb(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cc(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bH.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bG,"").replace(bL,bW[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bP),d.crossDomain==null&&(r=bR.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bW[1]&&r[2]==bW[2]&&(r[3]||(r[1]==="http:"?80:443))==(bW[3]||(bW[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),b$(bT,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bK.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bM.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bQ,"$1_="+x);d.url=y+(y===d.url?(bM.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bX+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=b$(bU,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)ca(g,a[g],c,e);return d.join("&").replace(bD,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cd=f.now(),ce=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cd++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ce.test(b.url)||e&&ce.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ce,l),b.url===j&&(e&&(k=k.replace(ce,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cf=a.ActiveXObject?function(){for(var a in ch)ch[a](0,1)}:!1,cg=0,ch;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ci()||cj()}:ci,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cf&&delete ch[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cg,cf&&(ch||(ch={},f(a).unload(cf)),ch[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ck={},cl,cm,cn=/^(?:toggle|show|hide)$/,co=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cp,cq=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cr;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cu("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cy(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cy(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/qa-content/qa-admin.js b/qa-content/qa-admin.js new file mode 100644 index 000000000..e44a8e6df --- /dev/null +++ b/qa-content/qa-admin.js @@ -0,0 +1,143 @@ +/* + Question2Answer (c) Gideon Greenspan + + http://www.question2answer.org/ + + + File: qa-content/qa-admin.js + Version: See define()s at top of qa-include/qa-base.php + Description: Javascript for admin pages to handle Ajax-triggered operations + + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + More about this license: http://www.question2answer.org/license.php +*/ + +var qa_recalc_running=0; + +window.onbeforeunload=function(event) +{ + if (qa_recalc_running>0) { + event=event||window.event; + var message=qa_warning_recalc; + event.returnValue=message; + return message; + } +} + +function qa_recalc_click(state, elem, value, noteid) +{ + if (elem.qa_recalc_running) { + elem.qa_recalc_stopped=true; + + } else { + elem.qa_recalc_running=true; + elem.qa_recalc_stopped=false; + qa_recalc_running++; + + document.getElementById(noteid).innerHTML=''; + elem.qa_original_value=elem.value; + if (value) + elem.value=value; + + qa_recalc_update(elem, state, noteid); + } + + return false; +} + +function qa_recalc_update(elem, state, noteid) +{ + if (state) + qa_ajax_post('recalc', {state:state}, + function(lines) { + if (lines[0]=='1') { + if (lines[2]) + document.getElementById(noteid).innerHTML=lines[2]; + + if (elem.qa_recalc_stopped) + qa_recalc_cleanup(elem); + else + qa_recalc_update(elem, lines[1], noteid); + + } else if (lines[0]=='0') { + document.getElementById(noteid).innerHTML=lines[2]; + qa_recalc_cleanup(elem); + + } else { + qa_ajax_error(); + qa_recalc_cleanup(elem); + } + } + ); + + else + qa_recalc_cleanup(elem); +} + +function qa_recalc_cleanup(elem) +{ + elem.value=elem.qa_original_value; + elem.qa_recalc_running=null; + qa_recalc_running--; +} + +function qa_mailing_start(noteid, pauseid) +{ + qa_ajax_post('mailing', {}, + function (lines) { + if (lines[0]=='1') { + document.getElementById(noteid).innerHTML=lines[1]; + window.setTimeout(function() { qa_mailing_start(noteid, pauseid); }, 1); // don't recurse + + } else if (lines[0]=='0') { + document.getElementById(noteid).innerHTML=lines[1]; + document.getElementById(pauseid).style.display='none'; + + } else { + qa_ajax_error(); + } + } + ); +} + +function qa_admin_click(target) +{ + var p=target.name.split('_'); + + var params={postid:p[1], action:p[2]}; + + qa_ajax_post('click_admin', params, + function (lines) { + if (lines[0]=='1') { + qa_conceal(document.getElementById('p'+p[1]), 'q_item'); + + } else { + qa_ajax_error(); + } + } + ); + + return false; +} + +function qa_version_check(uri, versionkey, version, urikey, elem) +{ + var params={uri:uri, versionkey:versionkey, version:version, urikey:urikey}; + + qa_ajax_post('version', params, + function (lines) { + if (lines[0]=='1') + document.getElementById(elem).innerHTML=lines[1]; + } + ); +} \ No newline at end of file diff --git a/qa-content/qa-ask.js b/qa-content/qa-ask.js new file mode 100644 index 000000000..81fb76c0c --- /dev/null +++ b/qa-content/qa-ask.js @@ -0,0 +1,283 @@ +/* + Question2Answer (c) Gideon Greenspan + + http://www.question2answer.org/ + + + File: qa-content/qa-ask.js + Version: See define()s at top of qa-include/qa-base.php + Description: Javascript for ask page and question editing, including tag auto-completion + + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + More about this license: http://www.question2answer.org/license.php +*/ + +function qa_title_change(value) +{ + qa_ajax_post('asktitle', {title:value}, function(lines) { + if (lines[0]=='1') { + if (lines[1].length) { + qa_tags_examples=lines[1]; + qa_tag_hints(); + } + + if (lines.length>2) { + var simelem=document.getElementById('similar'); + if (simelem) + simelem.innerHTML=lines.slice(2).join('\n'); + } + + } else if (lines[0]=='0') + alert(lines[1]); + else + qa_ajax_error(); + }); +} + +function qa_tag_click(link) +{ + var elem=document.getElementById('tags'); + var parts=qa_tag_typed_parts(elem); + + // removes any HTML tags and ampersand + var tag=link.innerHTML.replace(/<[^>]*>/g, '').replace('&', '&'); + + var separator=qa_tag_onlycomma ? ', ' : ' '; + + // replace if matches typed, otherwise append + var newvalue=(parts.typed && (tag.toLowerCase().indexOf(parts.typed.toLowerCase())>=0)) + ? (parts.before+separator+tag+separator+parts.after+separator) : (elem.value+separator+tag+separator); + + // sanitize and set value + if (qa_tag_onlycomma) + elem.value=newvalue.replace(/[\s,]*,[\s,]*/g, ', ').replace(/^[\s,]+/g, ''); + else + elem.value=newvalue.replace(/[\s,]+/g, ' ').replace(/^[\s,]+/g, ''); + + elem.focus(); + qa_tag_hints(); + + return false; +} + +function qa_tag_hints(skipcomplete) +{ + var elem=document.getElementById('tags'); + var parts=qa_tag_typed_parts(elem); + var html=''; + var completed=false; + + // first try to auto-complete + if (parts.typed && qa_tags_complete) { + html=qa_tags_to_html((qa_tags_examples+','+qa_tags_complete).split(','), parts.typed.toLowerCase().replace('&', '&')); + completed=html ? true : false; + } + + // otherwise show examples + if (qa_tags_examples && !completed) + html=qa_tags_to_html(qa_tags_examples.split(','), null); + + // set title visiblity and hint list + document.getElementById('tag_examples_title').style.display=(html && !completed) ? '' : 'none'; + document.getElementById('tag_complete_title').style.display=(html && completed) ? '' : 'none'; + document.getElementById('tag_hints').innerHTML=html; +} + +function qa_tags_to_html(tags, matchlc) +{ + var html=''; + var added=0; + var tagseen={}; + + for (var i=0; i=0) ) { // match if necessary + if (matchlc) { // if matching, show appropriate part in bold + var matchstart=taglc.indexOf(matchlc); + var matchend=matchstart+matchlc.length; + inner=''+tag.substring(0, matchstart)+''+ + tag.substring(matchstart, matchend)+''+tag.substring(matchend)+''; + } else // otherwise show as-is + inner=tag; + + html+=qa_tag_template.replace(/\^/g, inner.replace('$', '$$$$'))+' '; // replace ^ in template, escape $s + + if (++added>=qa_tags_max) + break; + } + } + } + + return html; +} + +function qa_caret_from_end(elem) +{ + if (document.selection) { // for IE + elem.focus(); + var sel=document.selection.createRange(); + sel.moveStart('character', -elem.value.length); + + return elem.value.length-sel.text.length; + + } else if (typeof(elem.selectionEnd)!='undefined') // other browsers + return elem.value.length-elem.selectionEnd; + + else // by default return safest value + return 0; +} + +function qa_tag_typed_parts(elem) +{ + var caret=elem.value.length-qa_caret_from_end(elem); + var active=elem.value.substring(0, caret); + var passive=elem.value.substring(active.length); + + // if the caret is in the middle of a word, move the end of word from passive to active + if ( + active.match(qa_tag_onlycomma ? /[^\s,][^,]*$/ : /[^\s,]$/) && + (adjoinmatch=passive.match(qa_tag_onlycomma ? /^[^,]*[^\s,][^,]*/ : /^[^\s,]+/)) + ) { + active+=adjoinmatch[0]; + passive=elem.value.substring(active.length); + } + + // find what has been typed so far + var typedmatch=active.match(qa_tag_onlycomma ? /[^\s,]+[^,]*$/ : /[^\s,]+$/) || ['']; + + return {before:active.substring(0, active.length-typedmatch[0].length), after:passive, typed:typedmatch[0]}; +} + +function qa_category_select(idprefix, startpath) +{ + var startval=startpath ? startpath.split("/") : []; + var setdescnow=true; + + for (var l=0; l<=qa_cat_maxdepth; l++) { + var elem=document.getElementById(idprefix+'_'+l); + + if (elem) { + if (l) { + if (l2) { + var subelem=elem.parentNode.insertBefore(document.createElement('span'), elem.nextSibling); + subelem.id=idprefix+'_'+l+'_sub'; + subelem.innerHTML=' '; + + var newelem=elem.cloneNode(false); + + newelem.name=newelem.id=idprefix+'_'+(l+1); + newelem.options.length=0; + + if (l ? qa_cat_allownosub : qa_cat_allownone) + newelem.options[0]=new Option(l ? '' : elem.options[0].text, '', true, true); + + for (var i=2; i=0) + callback(response.substr(headerpos+header.length).replace(/^\s+/, '').split("\n")); + else + callback([]); + + }, 'text').error(function(jqXHR) { if (jqXHR.readyState>0) callback([]) }); +} + +function qa_ajax_error() +{ + alert('Unexpected response from server - please try again or switch off Javascript.'); +} \ No newline at end of file diff --git a/qa-content/qa-question.js b/qa-content/qa-question.js new file mode 100644 index 000000000..37003f954 --- /dev/null +++ b/qa-content/qa-question.js @@ -0,0 +1,262 @@ +/* + Question2Answer (c) Gideon Greenspan + + http://www.question2answer.org/ + + + File: qa-content/qa-question.js + Version: See define()s at top of qa-include/qa-base.php + Description: Javascript to handle question page actions + + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + More about this license: http://www.question2answer.org/license.php +*/ + +var qa_element_revealed=null; + +function qa_toggle_element(elem) +{ + var e=elem ? document.getElementById(elem) : null; + + if (e && e.qa_disabled) + e=null; + + if (e && (qa_element_revealed==e)) { + qa_conceal(qa_element_revealed, 'form'); + qa_element_revealed=null; + + } else { + if (qa_element_revealed) + qa_conceal(qa_element_revealed, 'form'); + + if (e) { + if (e.qa_load && !e.qa_loaded) { + e.qa_load(); + e.qa_loaded=true; + } + + if (e.qa_show) + e.qa_show(); + + qa_reveal(e, 'form', function() { + var t=$(e).offset().top; + var h=$(e).height()+16; + var wt=$(window).scrollTop(); + var wh=$(window).height(); + + if ( (t(wt+wh)) ) + qa_scroll_page_to(t); + else if ((t+h)>(wt+wh)) + qa_scroll_page_to(t+h-wh); + + if (e.qa_focus) + e.qa_focus(); + }); + } + + qa_element_revealed=e; + } + + return !(e||!elem); // failed to find item +} + +function qa_submit_answer(questionid) +{ + var params=qa_form_params('a_form'); + + params.a_questionid=questionid; + + qa_ajax_post('answer', params, + function(lines) { + + if (lines[0]=='1') { + if (lines[1]<1) { + var b=document.getElementById('q_doanswer'); + if (b) + b.style.display='none'; + } + + qa_set_inner_html(document.getElementById('a_list_title'), 'a_list_title', lines[2]); + + var e=document.createElement('DIV'); + e.innerHTML=lines.slice(3).join("\n"); + + var c=e.firstChild; + c.style.display='none'; + + var l=document.getElementById('a_list'); + l.insertBefore(c, l.firstChild); + + var a=document.getElementById('anew'); + a.qa_disabled=true; + + qa_reveal(c, 'answer'); + qa_conceal(a, 'form'); + + } else if (lines[0]=='0') { + // document.forms['q_page_form'].elements['a_doadd2'].value=1; + document.forms['a_form'].submit(); + + } else { + qa_ajax_error(); + } + + } + ); + + return false; +} + +function qa_submit_comment(questionid, parentid) +{ + var params=qa_form_params('c_form_'+parentid); + + params.c_questionid=questionid; + params.c_parentid=parentid; + + qa_ajax_post('comment', params, + function (lines) { + + if (lines[0]=='1') { + var l=document.getElementById('c'+parentid+'_list'); + l.innerHTML=lines.slice(2).join("\n"); + l.style.display=''; + + var a=document.getElementById('c'+parentid); + a.qa_disabled=true; + + var c=document.getElementById(lines[1]); // id of comment + if (c) { + c.style.display='none'; + qa_reveal(c, 'comment'); + } + + qa_conceal(a, 'form'); + + } else if (lines[0]=='0') { + // document.forms['q_page_form'].elements['c'+parentid+'_doadd2'].value=1; + document.forms['c_form_'+parentid].submit(); + + } else { + qa_ajax_error(); + } + + } + ); + + return false; +} + +function qa_answer_click(answerid, questionid, target) +{ + var params={}; + + params.answerid=answerid; + params.questionid=questionid; + + params[target.name]=target.value; + + qa_ajax_post('click_a', params, + function (lines) { + if (lines[0]=='1') { + qa_set_inner_html(document.getElementById('a_list_title'), 'a_list_title', lines[1]); + + var l=document.getElementById('a'+answerid); + var h=lines.slice(2).join("\n"); + + if (h.length) + qa_set_outer_html(l, 'answer', h); + else + qa_conceal(l, 'answer'); + + } else { + document.forms['q_page_form'].elements['qa_click'].value=target.name; + document.forms['q_page_form'].submit(); + } + } + ); + + return false; +} + +function qa_comment_click(commentid, questionid, parentid, target) +{ + var params={}; + + params.commentid=commentid; + params.questionid=questionid; + params.parentid=parentid; + + params[target.name]=target.value; + + qa_ajax_post('click_c', params, + function (lines) { + if (lines[0]=='1') { + var l=document.getElementById('c'+commentid); + var h=lines.slice(1).join("\n"); + + if (h.length) + qa_set_outer_html(l, 'comment', h) + else + qa_conceal(l, 'comment'); + + } else { + document.forms['q_page_form'].elements['qa_click'].value=target.name; + document.forms['q_page_form'].submit(); + } + } + ); + + return false; +} + +function qa_show_comments(parentid) +{ + var params={}; + + params.c_parentid=parentid; + + qa_ajax_post('show_cs', params, + function (lines) { + if (lines[0]=='1') { + var l=document.getElementById('c'+parentid+'_list'); + l.innerHTML=lines.slice(1).join("\n"); + l.style.display='none'; + qa_reveal(l, 'comments'); + + } else { + qa_ajax_error(); + } + } + ); + + return false; +} + +function qa_form_params(formname) +{ + var es=document.forms[formname].elements; + var params={}; + + for (var i=0; i null, + 'register' => null, + 'logout' => null + ); + + /* + Example 1 - using absolute URLs, suitable if: + + * Your Q2A site: http://qa.mysite.com/ + * Your login page: http://www.mysite.com/login + * Your register page: http://www.mysite.com/register + * Your logout page: http://www.mysite.com/logout + + return array( + 'login' => 'http://www.mysite.com/login', + 'register' => 'http://www.mysite.com/register', + 'logout' => 'http://www.mysite.com/logout', + ); + + */ + + /* + Example 2 - using relative URLs, suitable if: + + * Your Q2A site: http://www.mysite.com/qa/ + * Your login page: http://www.mysite.com/login.php + * Your register page: http://www.mysite.com/register.php + * Your logout page: http://www.mysite.com/logout.php + + return array( + 'login' => $relative_url_prefix.'../login.php', + 'register' => $relative_url_prefix.'../register.php', + 'logout' => $relative_url_prefix.'../logout.php', + ); + */ + + /* + Example 3 - using relative URLs, and implementing $redirect_back_to_url + + In this example, your pages login.php, register.php and logout.php should read in the + parameter $_GET['redirect'], and redirect the user to the page specified by that + parameter once they have successfully logged in, registered or logged out. + + return array( + 'login' => $relative_url_prefix.'../login.php?redirect='.urlencode('qa/'.$redirect_back_to_url), + 'register' => $relative_url_prefix.'../register.php?redirect='.urlencode('qa/'.$redirect_back_to_url), + 'logout' => $relative_url_prefix.'../logout.php?redirect='.urlencode('qa/'.$redirect_back_to_url), + ); + */ + + } + + + function qa_get_logged_in_user() +/* + =========================================================================== + YOU MUST MODIFY THIS FUNCTION, BUT CAN DO SO AFTER Q2A CREATES ITS DATABASE + =========================================================================== + + qa_get_logged_in_user() + + You should check (using $_COOKIE, $_SESSION or whatever is appropriate) whether a user is + currently logged in. If not, return null. If so, return an array with the following elements: + + * userid: a user id appropriate for your response to qa_get_mysql_user_column_type() + * publicusername: a user description you are willing to show publicly, e.g. the username + * email: the logged in user's email address + * level: one of the QA_USER_LEVEL_* values below to denote the user's privileges: + + QA_USER_LEVEL_BASIC, QA_USER_LEVEL_EDITOR, QA_USER_LEVEL_ADMIN, QA_USER_LEVEL_SUPER + + The result of this function will be passed to your other function qa_get_logged_in_user_html() + so you may add any other elements to the returned array if they will be useful to you. + + Call qa_db_connection() to get the connection to the Q2A database. If your database is shared with + Q2A, you can use this with PHP's MySQL functions such as mysql_query() to run queries. + + In order to access the admin interface of your Q2A site, ensure that the array element 'level' + contains QA_USER_LEVEL_ADMIN or QA_USER_LEVEL_SUPER when you are logged in. +*/ + { + + // Until you edit this function, nobody is ever logged in + + return null; + + /* + Example 1 - suitable if: + + * You store the login state and user in a PHP session + * You use textual user identifiers that also serve as public usernames + * Your database is shared with the Q2A site + * Your database has a users table that contains emails + * The administrator has the user identifier 'admin' + + session_start(); + + if ($_SESSION['is_logged_in']) { + $userid=$_SESSION['logged_in_userid']; + + $qa_db_connection=qa_db_connection(); + + $result=mysql_fetch_assoc( + mysql_query( + "SELECT email FROM users WHERE userid='".mysql_real_escape_string($userid, $qa_db_connection)."'", + $qa_db_connection + ) + ); + + if (is_array($result)) + return array( + 'userid' => $userid, + 'publicusername' => $userid, + 'email' => $result['email'], + 'level' => ($userid=='admin') ? QA_USER_LEVEL_ADMIN : QA_USER_LEVEL_BASIC + ); + } + + return null; + */ + + /* + Example 2 - suitable if: + + * You store a session ID inside a cookie + * You use numerical user identifiers + * Your database is shared with the Q2A site + * Your database has a sessions table that maps session IDs to users + * Your database has a users table that contains usernames, emails and a flag for admin privileges + + if ($_COOKIE['sessionid']) { + $qa_db_connection=qa_db_connection(); + + $result=mysql_fetch_assoc( + mysql_query( + "SELECT userid, username, email, admin_flag FROM users WHERE userid=". + "(SELECT userid FROM sessions WHERE sessionid='".mysql_real_escape_string($_COOKIE['session_id'], $qa_db_connection)."')", + $qa_db_connection + ) + ); + + if (is_array($result)) + return array( + 'userid' => $result['userid'], + 'publicusername' => $result['username'], + 'email' => $result['email'], + 'level' => $result['admin_flag'] ? QA_USER_LEVEL_ADMIN : QA_USER_LEVEL_BASIC + ); + } + + return null; + */ + + } + + + function qa_get_user_email($userid) +/* + =========================================================================== + YOU MUST MODIFY THIS FUNCTION, BUT CAN DO SO AFTER Q2A CREATES ITS DATABASE + =========================================================================== + + qa_get_user_email($userid) + + Return the email address for user $userid, or null if you don't know it. + + Call qa_db_connection() to get the connection to the Q2A database. If your database is shared with + Q2A, you can use this with PHP's MySQL functions such as mysql_query() to run queries. +*/ + { + + // Until you edit this function, always return null + + return null; + + /* + Example 1 - suitable if: + + * Your database is shared with the Q2A site + * Your database has a users table that contains emails + + $qa_db_connection=qa_db_connection(); + + $result=mysql_fetch_assoc( + mysql_query( + "SELECT email FROM users WHERE userid='".mysql_real_escape_string($userid, $qa_db_connection)."'", + $qa_db_connection + ) + ); + + if (is_array($result)) + return $result['email']; + + return null; + */ + + } + + + function qa_get_userids_from_public($publicusernames) +/* + =========================================================================== + YOU MUST MODIFY THIS FUNCTION, BUT CAN DO SO AFTER Q2A CREATES ITS DATABASE + =========================================================================== + + qa_get_userids_from_public($publicusernames) + + You should take the array of public usernames in $publicusernames, and return an array which + maps those usernames to internal user ids. For each element of this array, the username you + were given should be in the key, with the corresponding user id in the value. + + Call qa_db_connection() to get the connection to the Q2A database. If your database is shared with + Q2A, you can use this with PHP's MySQL functions such as mysql_query() to run queries. If you + access this database or any other, try to use a single query instead of one per user. +*/ + { + + // Until you edit this function, always return null + + return null; + + /* + Example 1 - suitable if: + + * You use textual user identifiers that are also shown publicly + + $publictouserid=array(); + + foreach ($publicusernames as $publicusername) + $publictouserid[$publicusername]=$publicusername; + + return $publictouserid; + */ + + /* + Example 2 - suitable if: + + * You use numerical user identifiers + * Your database is shared with the Q2A site + * Your database has a users table that contains usernames + + $publictouserid=array(); + + if (count($publicusernames)) { + $qa_db_connection=qa_db_connection(); + + $escapedusernames=array(); + foreach ($publicusernames as $publicusername) + $escapedusernames[]="'".mysql_real_escape_string($publicusername, $qa_db_connection)."'"; + + $results=mysql_query( + 'SELECT username, userid FROM users WHERE username IN ('.implode(',', $escapedusernames).')', + $qa_db_connection + ); + + while ($result=mysql_fetch_assoc($results)) + $publictouserid[$result['username']]=$result['userid']; + } + + return $publictouserid; + */ + + } + + + function qa_get_public_from_userids($userids) +/* + =========================================================================== + YOU MUST MODIFY THIS FUNCTION, BUT CAN DO SO AFTER Q2A CREATES ITS DATABASE + =========================================================================== + + qa_get_public_from_userids($userids) + + This is exactly like qa_get_userids_from_public(), but works in the other direction. + + You should take the array of user identifiers in $userids, and return an array which maps + those to public usernames. For each element of this array, the userid you were given should + be in the key, with the corresponding username in the value. + + Call qa_db_connection() to get the connection to the Q2A database. If your database is shared with + Q2A, you can use this with PHP's MySQL functions such as mysql_query() to run queries. If you + access this database or any other, try to use a single query instead of one per user. +*/ + { + + // Until you edit this function, always return null + + return null; + + /* + Example 1 - suitable if: + + * You use textual user identifiers that are also shown publicly + + $useridtopublic=array(); + + foreach ($userids as $userid) + $useridtopublic[$userid]=$userid; + + return $useridtopublic; + */ + + /* + Example 2 - suitable if: + + * You use numerical user identifiers + * Your database is shared with the Q2A site + * Your database has a users table that contains usernames + + $useridtopublic=array(); + + if (count($userids)) { + $qa_db_connection=qa_db_connection(); + + $escapeduserids=array(); + foreach ($userids as $userid) + $escapeduserids[]="'".mysql_real_escape_string($userid, $qa_db_connection)."'"; + + $results=mysql_query( + 'SELECT username, userid FROM users WHERE userid IN ('.implode(',', $escapeduserids).')', + $qa_db_connection + ); + + while ($result=mysql_fetch_assoc($results)) + $useridtopublic[$result['userid']]=$result['username']; + } + + return $useridtopublic; + */ + + } + + + function qa_get_logged_in_user_html($logged_in_user, $relative_url_prefix) +/* + ========================================================================== + YOU MAY MODIFY THIS FUNCTION, BUT THE DEFAULT BELOW WILL WORK OK + ========================================================================== + + qa_get_logged_in_user_html($logged_in_user, $relative_url_prefix) + + You should return HTML code which identifies the logged in user, to be displayed next to the + logout link on the Q2A pages. This HTML will only be shown to the logged in user themselves. + + $logged_in_user is the array that you returned from qa_get_logged_in_user(). Hopefully this + contains enough information to generate the HTML without another database query, but if not, + call qa_db_connection() to get the connection to the Q2A database. + + $relative_url_prefix is a relative URL to the root of the Q2A site, which may be useful if + you want to include a link that uses relative URLs. If the Q2A site is in a subdirectory of + your site, $relative_url_prefix.'../' refers to your site root (see example 1). + + If you don't know what to display for a user, you can leave the default below. This will + show the public username, linked to the Q2A profile page for the user. +*/ + { + + // By default, show the public username linked to the Q2A profile page for the user + + $publicusername=$logged_in_user['publicusername']; + + return ''.htmlspecialchars($publicusername).''; + + /* + Example 1 - suitable if: + + * Your Q2A site: http://www.mysite.com/qa/ + * Your user pages: http://www.mysite.com/user/[username] + + $publicusername=$logged_in_user['publicusername']; + + return ''.htmlspecialchars($publicusername).''; + */ + + /* + Example 2 - suitable if: + + * Your Q2A site: http://qa.mysite.com/ + * Your user pages: http://www.mysite.com/[username]/ + * 16x16 user photos: http://www.mysite.com/[username]/photo-small.jpeg + + $publicusername=$logged_in_user['publicusername']; + + return ''. + ''.htmlspecialchars($publicusername).''; + */ + + } + + + function qa_get_users_html($userids, $should_include_link, $relative_url_prefix) +/* + ========================================================================== + YOU MAY MODIFY THIS FUNCTION, BUT THE DEFAULT BELOW WILL WORK OK + ========================================================================== + + qa_get_users_html($userids, $should_include_link, $relative_url_prefix) + + You should return an array of HTML to display for each user in $userids. For each element of + this array, the userid should be in the key, with the corresponding HTML in the value. + + Call qa_db_connection() to get the connection to the Q2A database. If your database is shared with + Q2A, you can use this with PHP's MySQL functions such as mysql_query() to run queries. If you + access this database or any other, try to use a single query instead of one per user. + + If $should_include_link is true, the HTML may include links to user profile pages. + If $should_include_link is false, links should not be included in the HTML. + + $relative_url_prefix is a relative URL to the root of the Q2A site, which may be useful if + you want to include links that uses relative URLs. If the Q2A site is in a subdirectory of + your site, $relative_url_prefix.'../' refers to your site root (see example 1). + + If you don't know what to display for a user, you can leave the default below. This will + show the public username, linked to the Q2A profile page for each user. +*/ + { + + // By default, show the public username linked to the Q2A profile page for each user + + $useridtopublic=qa_get_public_from_userids($userids); + + $usershtml=array(); + + foreach ($userids as $userid) { + $publicusername=$useridtopublic[$userid]; + + $usershtml[$userid]=htmlspecialchars($publicusername); + + if ($should_include_link) + $usershtml[$userid]=''.$usershtml[$userid].''; + } + + return $usershtml; + + /* + Example 1 - suitable if: + + * Your Q2A site: http://www.mysite.com/qa/ + * Your user pages: http://www.mysite.com/user/[username] + + $useridtopublic=qa_get_public_from_userids($userids); + + foreach ($userids as $userid) { + $publicusername=$useridtopublic[$userid]; + + $usershtml[$userid]=htmlspecialchars($publicusername); + + if ($should_include_link) + $usershtml[$userid]=''.$usershtml[$userid].''; + } + + return $usershtml; + */ + + /* + Example 2 - suitable if: + + * Your Q2A site: http://qa.mysite.com/ + * Your user pages: http://www.mysite.com/[username]/ + * User photos (16x16): http://www.mysite.com/[username]/photo-small.jpeg + + $useridtopublic=qa_get_public_from_userids($userids); + + foreach ($userids as $userid) { + $publicusername=$useridtopublic[$userid]; + + $usershtml[$userid]=''.htmlspecialchars($publicusername); + + if ($should_include_link) + $usershtml[$userid]=''.$usershtml[$userid].''; + } + + return $usershtml; + */ + + } + + + function qa_user_report_action($userid, $action) +/* + ========================================================================== + YOU MAY MODIFY THIS FUNCTION, BUT THE DEFAULT BELOW WILL WORK OK + ========================================================================== + + qa_user_report_action($userid, $action) + + Informs you about an action by user $userid that modified the database, such as posting, + voting, etc... If you wish, you may use this to log user activity or monitor for abuse. + + Call qa_db_connection() to get the connection to the Q2A database. If your database is shared with + Q2A, you can use this with PHP's MySQL functions such as mysql_query() to run queries. + + $action will be a string (such as 'q_edit') describing the action. These strings will match the + first $event parameter passed to the process_event(...) function in event modules. In fact, you might + be better off just using a plugin with an event module instead, since you'll get more information. + + FYI, you can get the IP address of the user from qa_remote_ip_address(). +*/ + { + // do nothing by default + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-ajax-answer.php b/qa-include/qa-ajax-answer.php new file mode 100644 index 000000000..9808051a7 --- /dev/null +++ b/qa-include/qa-ajax-answer.php @@ -0,0 +1,111 @@ +a_list_item($a_view); + + return; + } + } + } + + + echo "QA_AJAX_RESPONSE\n0\n"; // fall back to non-Ajax submission if there were any problems + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-ajax-asktitle.php b/qa-include/qa-ajax-asktitle.php new file mode 100644 index 000000000..33f293bb4 --- /dev/null +++ b/qa-include/qa-ajax-asktitle.php @@ -0,0 +1,114 @@ + $weight) { + if ($weight<$minweight) + break; + + $exampletags[]=$tag; + if (count($exampletags)>=$maxcount) + break; + } + + } else + $exampletags=array(); + + +// Output the response header and example tags + + echo "QA_AJAX_RESPONSE\n1\n"; + + echo strtr(implode(',', $exampletags), "\r\n", ' ')."\n"; + + +// Collect and output the list of related questions + + if ($doaskcheck) { + require_once QA_INCLUDE_DIR.'qa-app-format.php'; + + $count=0; + $minscore=qa_match_to_min_score(qa_opt('match_ask_check_qs')); + $maxcount=qa_opt('page_size_ask_check_qs'); + + foreach ($relatedquestions as $question) { + if ($question['score']<$minscore) + break; + + if (!$count) + echo qa_lang_html('question/ask_same_q').'
'; + + echo strtr( + ''.qa_html($question['title']).'
', + "\r\n", ' ' + )."\n"; + + if ((++$count)>=$maxcount) + break; + } + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-ajax-category.php b/qa-include/qa-ajax-category.php new file mode 100644 index 000000000..465739d58 --- /dev/null +++ b/qa-include/qa-ajax-category.php @@ -0,0 +1,49 @@ + $achildpost) + $achildposts[$key]=$achildpost+qa_page_q_post_rules($achildpost, $answer, $achildposts, null); + + $usershtml=qa_userids_handles_html(array_merge(array($answer), $achildposts), true); + + $a_view=qa_page_q_answer_view($question, $answer, ($answer['postid']==$question['selchildid']) && ($answer['type']=='A'), + $usershtml, false); + + $a_view['c_list']=qa_page_q_comment_follow_list($answer, $achildposts, false, $usershtml, false, null); + + $themeclass=qa_load_theme_class(qa_get_site_theme(), 'ajax-answer', null, null); + + + // ... send back the HTML for it + + echo "\n"; + + $themeclass->a_list_item($a_view); + } + + return; + } + } + + + echo "QA_AJAX_RESPONSE\n0\n"; // fall back to non-Ajax submission if something failed + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-ajax-click-comment.php b/qa-include/qa-ajax-click-comment.php new file mode 100644 index 000000000..cd9d79409 --- /dev/null +++ b/qa-include/qa-ajax-click-comment.php @@ -0,0 +1,100 @@ +c_list_item($c_view); + } + + return; + } + } + + + echo "QA_AJAX_RESPONSE\n0\n"; // fall back to non-Ajax submission if something failed + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-ajax-comment.php b/qa-include/qa-ajax-comment.php new file mode 100644 index 000000000..41ea30213 --- /dev/null +++ b/qa-include/qa-ajax-comment.php @@ -0,0 +1,113 @@ + $child) + $children[$key]=$child+qa_page_q_post_rules($child, $parent, $children, null); + + $usershtml=qa_userids_handles_html($children, true); + + qa_sort_by($children, 'created'); + + $c_list=qa_page_q_comment_follow_list($parent, $children, true, $usershtml, false, null); + + $themeclass=qa_load_theme_class(qa_get_site_theme(), 'ajax-comments', null, null); + + echo "QA_AJAX_RESPONSE\n1\n"; + + + // Send back the ID of the new comment + + echo qa_anchor('C', $commentid)."\n"; + + + // Send back the HTML + + foreach ($c_list['cs'] as $c_item) + $themeclass->c_list_item($c_item); + + return; + } + } + } + + echo "QA_AJAX_RESPONSE\n0\n"; // fall back to non-Ajax submission if there were any problems + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-ajax-favorite.php b/qa-include/qa-ajax-favorite.php new file mode 100644 index 000000000..f3ddd1e7d --- /dev/null +++ b/qa-include/qa-ajax-favorite.php @@ -0,0 +1,56 @@ +favorite_inner_html($favoriteform); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-ajax-mailing.php b/qa-include/qa-ajax-mailing.php new file mode 100644 index 000000000..2f19ef2cc --- /dev/null +++ b/qa-include/qa-ajax-mailing.php @@ -0,0 +1,57 @@ +=QA_USER_LEVEL_ADMIN) { + $starttime=time(); + + qa_mailing_perform_step(); + + if ($starttime==time()) + sleep(1); // make sure at least one second has passed + + $message=qa_mailing_progress_message(); + + if (isset($message)) + $continue=true; + else + $message=qa_lang('admin/mailing_complete'); + + } else + $message=qa_lang('admin/no_privileges'); + + + echo "QA_AJAX_RESPONSE\n".(int)$continue."\n".qa_html($message); + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-ajax-notice.php b/qa-include/qa-ajax-notice.php new file mode 100644 index 000000000..465688b47 --- /dev/null +++ b/qa-include/qa-ajax-notice.php @@ -0,0 +1,52 @@ +=QA_USER_LEVEL_ADMIN) { + $state=qa_post_text('state'); + $stoptime=time()+3; + + while ( qa_recalc_perform_step($state) && (time()<$stoptime) ) + ; + + $message=qa_recalc_get_message($state); + + } else { + $state=''; + $message=qa_lang('admin/no_privileges'); + } + + + echo "QA_AJAX_RESPONSE\n1\n".$state."\n".qa_html($message); + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-ajax-show-comments.php b/qa-include/qa-ajax-show-comments.php new file mode 100644 index 000000000..4bc7a0287 --- /dev/null +++ b/qa-include/qa-ajax-show-comments.php @@ -0,0 +1,77 @@ + $child) + $children[$key]=$child+qa_page_q_post_rules($child, $parent, $children, null); + + $usershtml=qa_userids_handles_html($children, true); + + qa_sort_by($children, 'created'); + + $c_list=qa_page_q_comment_follow_list($parent, $children, true, $usershtml, false, null); + + $themeclass=qa_load_theme_class(qa_get_site_theme(), 'ajax-comments', null, null); + + echo "QA_AJAX_RESPONSE\n1\n"; + + + // Send back the HTML + + foreach ($c_list['cs'] as $c_item) + $themeclass->c_list_item($c_item); + + return; + } + + + echo "QA_AJAX_RESPONSE\n0\n"; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-ajax-version.php b/qa-include/qa-ajax-version.php new file mode 100644 index 000000000..863f28fd2 --- /dev/null +++ b/qa-include/qa-ajax-version.php @@ -0,0 +1,60 @@ + $versionkey, + 'uri' => $urikey, + )); + + if (strlen(@$metadata['version'])) { + if (strcmp($metadata['version'], $version)) { + $response=qa_lang_html_sub('admin/version_get_x', qa_html('v'.$metadata['version'])); + + if (strlen(@$metadata['uri'])) + $response=''.$response.''; + + } else + $response=qa_lang_html('admin/version_latest'); + + } else + $response=qa_lang_html('admin/version_latest_unknown'); + + + echo "QA_AJAX_RESPONSE\n1\n".$response; + + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-ajax-vote.php b/qa-include/qa-ajax-vote.php new file mode 100644 index 000000000..0935f4a8e --- /dev/null +++ b/qa-include/qa-ajax-vote.php @@ -0,0 +1,65 @@ + qa_get_vote_view($post['basetype'], true), // behave as if on question page since the vote succeeded + )); + + $themeclass=qa_load_theme_class(qa_get_site_theme(), 'voting', null, null); + + echo "QA_AJAX_RESPONSE\n1\n"; + $themeclass->voting_inner_html($fields); + + } else + echo "QA_AJAX_RESPONSE\n0\n".$voteerror; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-ajax.php b/qa-include/qa-ajax.php new file mode 100644 index 000000000..b542c814b --- /dev/null +++ b/qa-include/qa-ajax.php @@ -0,0 +1,92 @@ + 'qa-ajax-notice.php', + 'favorite' => 'qa-ajax-favorite.php', + 'vote' => 'qa-ajax-vote.php', + 'recalc' => 'qa-ajax-recalc.php', + 'mailing' => 'qa-ajax-mailing.php', + 'version' => 'qa-ajax-version.php', + 'category' => 'qa-ajax-category.php', + 'asktitle' => 'qa-ajax-asktitle.php', + 'answer' => 'qa-ajax-answer.php', + 'comment' => 'qa-ajax-comment.php', + 'click_a' => 'qa-ajax-click-answer.php', + 'click_c' => 'qa-ajax-click-comment.php', + 'click_admin' => 'qa-ajax-click-admin.php', + 'show_cs' => 'qa-ajax-show-comments.php', + ); + + $operation=qa_post_text('qa_operation'); + + if (isset($routing[$operation])) { + qa_db_connect('qa_ajax_db_fail_handler'); + + require QA_INCLUDE_DIR.$routing[$operation]; + + qa_db_disconnect(); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-admin.php b/qa-include/qa-app-admin.php new file mode 100644 index 000000000..40ca69eb0 --- /dev/null +++ b/qa-include/qa-app-admin.php @@ -0,0 +1,529 @@ + [long name] +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $codetolanguage=array( // 2-letter language codes as per ISO 639-1 + 'ar' => 'Arabic - العربية', + 'bg' => 'Bulgarian - Български', + 'bn' => 'Bengali - বাংলা', + 'ca' => 'Catalan - Català', + 'cs' => 'Czech - Čeština', + 'cy' => 'Welsh - Cymraeg', + 'da' => 'Danish - Dansk', + 'de' => 'German - Deutsch', + 'el' => 'Greek - Ελληνικά', + 'en-GB' => 'English (UK)', + 'es' => 'Spanish - Español', + 'et' => 'Estonian - Eesti', + 'fa' => 'Persian - فارسی', + 'fi' => 'Finnish - Suomi', + 'fr' => 'French - Français', + 'he' => 'Hebrew - עברית', + 'hr' => 'Croatian - Hrvatski', + 'hu' => 'Hungarian - Magyar', + 'id' => 'Indonesian - Bahasa Indonesia', + 'is' => 'Icelandic - Íslenska', + 'it' => 'Italian - Italiano', + 'ja' => 'Japanese - 日本語', + 'kh' => 'Khmer - ភាសាខ្មែរ', + 'ko' => 'Korean - 한국어', + 'lt' => 'Lithuanian - Lietuvių', + 'nl' => 'Dutch - Nederlands', + 'no' => 'Norwegian - Norsk', + 'pl' => 'Polish - Polski', + 'pt' => 'Portuguese - Português', + 'ro' => 'Romanian - Română', + 'ru' => 'Russian - Русский', + 'sk' => 'Slovak - Slovenčina', + 'sl' => 'Slovenian - Slovenščina', + 'sr' => 'Serbian - Српски', + 'sv' => 'Swedish - Svenska', + 'th' => 'Thai - ไทย', + 'tr' => 'Turkish - Türkçe', + 'uk' => 'Ukrainian - Українська', + 'vi' => 'Vietnamese - Tiếng Việt', + 'zh-TW' => 'Chinese Traditional - 繁體中文', + 'zh' => 'Chinese Simplified - 简体中文', + ); + + $options=array('' => 'English (US)'); + + $directory=@opendir(QA_LANG_DIR); + if (is_resource($directory)) { + while (($code=readdir($directory))!==false) + if (is_dir(QA_LANG_DIR.$code) && isset($codetolanguage[$code])) + $options[$code]=$codetolanguage[$code]; + + closedir($directory); + } + + asort($options, SORT_STRING); + + return $options; + } + + + function qa_admin_theme_options() +/* + Return a sorted array of available themes, [theme name] => [theme name] +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $options=array(); + + $directory=@opendir(QA_THEME_DIR); + if (is_resource($directory)) { + while (($theme=readdir($directory))!==false) + if ( (substr($theme, 0, 1)!='.') && file_exists(QA_THEME_DIR.$theme.'/qa-styles.css') ) + $options[$theme]=$theme; + + closedir($directory); + } + + asort($options, SORT_STRING); + + return $options; + } + + + function qa_admin_place_options() +/* + Return an array of widget placement options, with keys matching the database value +*/ + { + return array( + 'FT' => qa_lang_html('options/place_full_top'), + 'FH' => qa_lang_html('options/place_full_below_nav'), + 'FL' => qa_lang_html('options/place_full_below_content'), + 'FB' => qa_lang_html('options/place_full_below_footer'), + 'MT' => qa_lang_html('options/place_main_top'), + 'MH' => qa_lang_html('options/place_main_below_title'), + 'ML' => qa_lang_html('options/place_main_below_lists'), + 'MB' => qa_lang_html('options/place_main_bottom'), + 'ST' => qa_lang_html('options/place_side_top'), + 'SH' => qa_lang_html('options/place_side_below_sidebar'), + 'SL' => qa_lang_html('options/place_side_below_categories'), + 'SB' => qa_lang_html('options/place_side_last'), + ); + } + + + function qa_admin_page_size_options($maximum) +/* + Return an array of page size options up to $maximum, [page size] => [page size] +*/ + { + $rawoptions=array(5, 10, 15, 20, 25, 30, 40, 50, 60, 80, 100, 120, 150, 200, 250, 300, 400, 500, 600, 800, 1000); + + $options=array(); + foreach ($rawoptions as $rawoption) { + if ($rawoption>$maximum) + break; + + $options[$rawoption]=$rawoption; + } + + return $options; + } + + + function qa_admin_match_options() +/* + Return an array of options representing matching precision, [value] => [label] +*/ + { + return array( + 5 => qa_lang_html('options/match_5'), + 4 => qa_lang_html('options/match_4'), + 3 => qa_lang_html('options/match_3'), + 2 => qa_lang_html('options/match_2'), + 1 => qa_lang_html('options/match_1'), + ); + } + + + function qa_admin_permit_options($widest, $narrowest, $doconfirms=true, $dopoints=true) +/* + Return an array of options representing permission restrictions, [value] => [label] + ranging from $widest to $narrowest. Set $doconfirms to whether email confirmations are on +*/ + { + require_once QA_INCLUDE_DIR.'qa-app-options.php'; + + $options=array( + QA_PERMIT_ALL => qa_lang_html('options/permit_all'), + QA_PERMIT_USERS => qa_lang_html('options/permit_users'), + QA_PERMIT_CONFIRMED => qa_lang_html('options/permit_confirmed'), + QA_PERMIT_POINTS => qa_lang_html('options/permit_points'), + QA_PERMIT_POINTS_CONFIRMED => qa_lang_html('options/permit_points_confirmed'), + QA_PERMIT_EXPERTS => qa_lang_html('options/permit_experts'), + QA_PERMIT_EDITORS => qa_lang_html('options/permit_editors'), + QA_PERMIT_MODERATORS => qa_lang_html('options/permit_moderators'), + QA_PERMIT_ADMINS => qa_lang_html('options/permit_admins'), + QA_PERMIT_SUPERS => qa_lang_html('options/permit_supers'), + ); + + foreach ($options as $key => $label) + if (($key<$narrowest) || ($key>$widest)) + unset($options[$key]); + + if (!$doconfirms) { + unset($options[QA_PERMIT_CONFIRMED]); + unset($options[QA_PERMIT_POINTS_CONFIRMED]); + } + + if (!$dopoints) { + unset($options[QA_PERMIT_POINTS]); + unset($options[QA_PERMIT_POINTS_CONFIRMED]); + } + + return $options; + } + + + function qa_admin_sub_navigation() +/* + Return the sub navigation structure common to admin pages +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $navigation=array(); + + if (qa_get_logged_in_level()>=QA_USER_LEVEL_ADMIN) { + $navigation['admin/general']=array( + 'label' => qa_lang('admin/general_title'), + 'url' => qa_path_html('admin/general'), + ); + + $navigation['admin/emails']=array( + 'label' => qa_lang('admin/emails_title'), + 'url' => qa_path_html('admin/emails'), + ); + + $navigation['admin/user']=array( + 'label' => qa_lang('admin/users_title'), + 'url' => qa_path_html('admin/users'), + ); + + $navigation['admin/layout']=array( + 'label' => qa_lang('admin/layout_title'), + 'url' => qa_path_html('admin/layout'), + ); + + $navigation['admin/posting']=array( + 'label' => qa_lang('admin/posting_title'), + 'url' => qa_path_html('admin/posting'), + ); + + $navigation['admin/viewing']=array( + 'label' => qa_lang('admin/viewing_title'), + 'url' => qa_path_html('admin/viewing'), + ); + + $navigation['admin/lists']=array( + 'label' => qa_lang('admin/lists_title'), + 'url' => qa_path_html('admin/lists'), + ); + + if (qa_using_categories()) + $navigation['admin/categories']=array( + 'label' => qa_lang('admin/categories_title'), + 'url' => qa_path_html('admin/categories'), + ); + + $navigation['admin/permissions']=array( + 'label' => qa_lang('admin/permissions_title'), + 'url' => qa_path_html('admin/permissions'), + ); + + $navigation['admin/pages']=array( + 'label' => qa_lang('admin/pages_title'), + 'url' => qa_path_html('admin/pages'), + ); + + $navigation['admin/feeds']=array( + 'label' => qa_lang('admin/feeds_title'), + 'url' => qa_path_html('admin/feeds'), + ); + + $navigation['admin/points']=array( + 'label' => qa_lang('admin/points_title'), + 'url' => qa_path_html('admin/points'), + ); + + $navigation['admin/spam']=array( + 'label' => qa_lang('admin/spam_title'), + 'url' => qa_path_html('admin/spam'), + ); + + $navigation['admin/stats']=array( + 'label' => qa_lang('admin/stats_title'), + 'url' => qa_path_html('admin/stats'), + ); + + if (!QA_FINAL_EXTERNAL_USERS) + $navigation['admin/mailing']=array( + 'label' => qa_lang('admin/mailing_title'), + 'url' => qa_path_html('admin/mailing'), + ); + + $navigation['admin/plugins']=array( + 'label' => qa_lang('admin/plugins_title'), + 'url' => qa_path_html('admin/plugins'), + ); + } + + if (!qa_user_permit_error('permit_moderate')) + $navigation['admin/moderate']=array( + 'label' => qa_lang('admin/moderate_title'), + 'url' => qa_path_html('admin/moderate'), + ); + + if (qa_opt('flagging_of_posts') && !qa_user_permit_error('permit_hide_show')) + $navigation['admin/flagged']=array( + 'label' => qa_lang('admin/flagged_title'), + 'url' => qa_path_html('admin/flagged'), + ); + + if ( (!qa_user_permit_error('permit_hide_show')) || (!qa_user_permit_error('permit_delete_hidden')) ) + $navigation['admin/hidden']=array( + 'label' => qa_lang('admin/hidden_title'), + 'url' => qa_path_html('admin/hidden'), + ); + + return $navigation; + } + + + function qa_admin_page_error() +/* + Return the error that needs to displayed on all admin pages, or null if none +*/ + { + @include_once QA_INCLUDE_DIR.'qa-db-install.php'; + + if (defined('QA_DB_VERSION_CURRENT') && (qa_opt('db_version')=QA_USER_LEVEL_ADMIN)) + return strtr( + qa_lang_html('admin/upgrade_db'), + + array( + '^1' => '', + '^2' => '', + ) + ); + else + return null; + } + + + function qa_admin_url_test_html() +/* + Return an HTML fragment to display for a URL test which has passed +*/ + { + return '; font-size:9px; color:#060; font-weight:bold; font-family:arial,sans-serif; border-color:#060;">OK<'; + } + + + function qa_admin_is_slug_reserved($requestpart) +/* + Returns whether a URL path beginning with $requestpart is reserved by the engine or a plugin page module +*/ + { + $requestpart=trim(strtolower($requestpart)); + $routing=qa_page_routing(); + + if (isset($routing[$requestpart]) || isset($routing[$requestpart.'/']) || is_numeric($requestpart)) + return true; + + $pathmap=qa_get_request_map(); + + foreach ($pathmap as $mappedrequest) + if (trim(strtolower($mappedrequest)) == $requestpart) + return true; + + switch ($requestpart) { + case '': + case 'qa': + case 'feed': + case 'install': + case 'url': + case 'image': + case 'ajax': + return true; + } + + $pagemodules=qa_load_modules_with('page', 'match_request'); + foreach ($pagemodules as $pagemodule) + if ($pagemodule->match_request($requestpart)) + return true; + + return false; + } + + + function qa_admin_single_click($postid, $action) +/* + Returns true if admin (hidden/flagged/approve) page $action performed on $postid is permitted by the current user + and was processed successfully +*/ + { + require_once QA_INCLUDE_DIR.'qa-app-posts.php'; + + $post=qa_post_get_full($postid); + + if (isset($post)) { + $userid=qa_get_logged_in_userid(); + $queued=(substr($post['type'], 1)=='_QUEUED'); + + switch ($action) { + case 'approve': + if ($queued && !qa_user_permit_error('permit_moderate')) { + qa_post_set_hidden($postid, false, null); + return true; + } + break; + + case 'reject': + if ($queued && !qa_user_permit_error('permit_moderate')) { + qa_post_set_hidden($postid, true, $userid); + return true; + } + break; + + case 'hide': + if ((!$queued) && !qa_user_permit_error('permit_hide_show')) { + qa_post_set_hidden($postid, true, $userid); + return true; + } + break; + + case 'reshow': + if ($post['hidden'] && !qa_user_permit_error('permit_hide_show')) { + qa_post_set_hidden($postid, false, $userid); + return true; + } + break; + + case 'delete': + if ($post['hidden'] && !qa_user_permit_error('permit_delete_hidden')) { + qa_post_delete($postid); + return true; + } + break; + + case 'clearflags': + require_once QA_INCLUDE_DIR.'qa-app-votes.php'; + + if (!qa_user_permit_error('permit_hide_show')) { + qa_flags_clear_all($post, $userid, qa_get_logged_in_handle(), null); + return true; + } + break; + } + } + + return false; + } + + + function qa_admin_check_clicks() +/* + Checks for a POSTed click on an admin (hidden/flagged/approve) page, and refresh the page if processed successfully (non Ajax) +*/ + { + if (qa_is_http_post()) + foreach ($_POST as $field => $value) + if (strpos($field, 'admin_')===0) { + @list($dummy, $postid, $action)=explode('_', $field); + + if (strlen($postid) && strlen($action) && qa_admin_single_click($postid, $action)) + qa_redirect(qa_request()); + } + } + + + function qa_admin_addon_metadata($contents, $fields) +/* + Retrieve metadata information from the $contents of a qa-theme.php or qa-plugin.php file, mapping via $fields +*/ + { + $metadata=array(); + + foreach ($fields as $key => $field) + if (preg_match('/'.str_replace(' ', '[ \t]*', preg_quote($field, '/')).':[ \t]*([^\n\f]*)[\n\f]/i', $contents, $matches)) + $metadata[$key]=trim($matches[1]); + + return $metadata; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-blobs.php b/qa-include/qa-app-blobs.php new file mode 100644 index 000000000..46bcf27e5 --- /dev/null +++ b/qa-include/qa-app-blobs.php @@ -0,0 +1,70 @@ + $blobid), $absolute ? qa_opt('site_url') : null, QA_URL_FORMAT_PARAMS); + } + + + function qa_get_max_upload_size() +/* + Return the maximum size of file that can be uploaded, based on database and PHP limits +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $mindb=16777215; // from MEDIUMBLOB column type + + $minphp=trim(ini_get('upload_max_filesize')); + + switch (strtolower(substr($minphp, -1))) { + case 'g': + $minphp*=1024; + case 'm': + $minphp*=1024; + case 'k': + $minphp*=1024; + } + + return min($mindb, $minphp); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-captcha.php b/qa-include/qa-app-captcha.php new file mode 100644 index 000000000..fd8487ae3 --- /dev/null +++ b/qa-include/qa-app-captcha.php @@ -0,0 +1,96 @@ +allow_captcha()); + } + + + function qa_set_up_captcha_field(&$qa_content, &$fields, $errors, $note=null) +/* + Prepare $qa_content for showing a captcha, adding the element to $fields, given previous $errors, and a $note to display +*/ + { + if (qa_captcha_available()) { + $captcha=qa_load_module('captcha', qa_opt('captcha_module')); + + $count=@++$qa_content['qa_captcha_count']; // work around fact that reCAPTCHA can only display per page + + if ($count>1) + $html='[captcha placeholder]'; // single captcha will be moved about the page, to replace this + else { + $qa_content['script_var']['qa_captcha_in']='qa_captcha_div_1'; + $html=$captcha->form_html($qa_content, @$errors['captcha']); + } + + $fields['captcha']=array( + 'type' => 'custom', + 'label' => qa_lang_html('misc/captcha_label'), + 'html' => '
'.$html.'
', + 'error' => @array_key_exists('captcha', $errors) ? qa_lang_html('misc/captcha_error') : null, + 'note' => $note, + ); + + return "if (qa_captcha_in!='qa_captcha_div_".$count."') { document.getElementById('qa_captcha_div_".$count."').innerHTML=document.getElementById(qa_captcha_in).innerHTML; document.getElementById(qa_captcha_in).innerHTML=''; qa_captcha_in='qa_captcha_div_".$count."'; }"; + } + + return ''; + } + + + function qa_captcha_validate_post(&$errors) +/* + Check if captcha is submitted correctly, and if not, set $errors['captcha'] to a descriptive string +*/ + { + if (qa_captcha_available()) { + $captcha=qa_load_module('captcha', qa_opt('captcha_module')); + + if (!$captcha->validate_post($error)) { + $errors['captcha']=$error; + return false; + } + } + + return true; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-cookies.php b/qa-include/qa-app-cookies.php new file mode 100644 index 000000000..cb51d9a44 --- /dev/null +++ b/qa-include/qa-app-cookies.php @@ -0,0 +1,82 @@ +0) + return false; + + require_once QA_INCLUDE_DIR.'qa-db-selects.php'; + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + if (isset($userid)) { + $needemail=!qa_email_validate(@$email); // take from user if invalid, e.g. @ used in practice + $needhandle=empty($handle); + + if ($needemail || $needhandle) { + if (QA_FINAL_EXTERNAL_USERS) { + if ($needhandle) { + $handles=qa_get_public_from_userids(array($userid)); + $handle=@$handles[$userid]; + } + + if ($needemail) + $email=qa_get_user_email($userid); + + } else { + $useraccount=qa_db_select_with_pending( + qa_db_user_account_selectspec($userid, true) + ); + + if ($needhandle) + $handle=@$useraccount['handle']; + + if ($needemail) + $email=@$useraccount['email']; + } + } + } + + if (isset($email) && qa_email_validate($email)) { + $subs['^site_title']=qa_opt('site_title'); + $subs['^handle']=$handle; + $subs['^email']=$email; + $subs['^open']="\n"; + $subs['^close']="\n"; + + return qa_send_email(array( + 'fromemail' => qa_opt('from_email'), + 'fromname' => qa_opt('site_title'), + 'toemail' => $email, + 'toname' => $handle, + 'subject' => strtr($subject, $subs), + 'body' => (empty($handle) ? '' : qa_lang_sub('emails/to_handle_prefix', $handle)).strtr($body, $subs), + 'html' => false, + )); + + } else + return false; + } + + + function qa_send_email($params) +/* + Send the email based on the $params array - the following keys are required (some can be empty): fromemail, + fromname, toemail, toname, subject, body, html +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-class.phpmailer.php'; + + $mailer=new PHPMailer(); + $mailer->CharSet='utf-8'; + + $mailer->From=$params['fromemail']; + $mailer->Sender=$params['fromemail']; + $mailer->FromName=$params['fromname']; + $mailer->AddAddress($params['toemail'], $params['toname']); + $mailer->Subject=$params['subject']; + $mailer->Body=$params['body']; + + if ($params['html']) + $mailer->IsHTML(true); + + if (qa_opt('smtp_active')) { + $mailer->IsSMTP(); + $mailer->Host=qa_opt('smtp_address'); + $mailer->Port=qa_opt('smtp_port'); + + if (qa_opt('smtp_secure')) + $mailer->SMTPSecure=qa_opt('smtp_secure'); + + if (qa_opt('smtp_authenticate')) { + $mailer->SMTPAuth=true; + $mailer->Username=qa_opt('smtp_username'); + $mailer->Password=qa_opt('smtp_password'); + } + } + + return $mailer->Send(); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-events.php b/qa-include/qa-app-events.php new file mode 100644 index 000000000..08a407ad2 --- /dev/null +++ b/qa-include/qa-app-events.php @@ -0,0 +1,92 @@ + $entityid); + break; + + case QA_ENTITY_USER: + $action=$favorite ? 'u_favorite' : 'u_unfavorite'; + $params=array('userid' => $entityid); + break; + + case QA_ENTITY_TAG: + $action=$favorite ? 'tag_favorite' : 'tag_unfavorite'; + $params=array('wordid' => $entityid); + break; + + case QA_ENTITY_CATEGORY: + $action=$favorite ? 'cat_favorite' : 'cat_unfavorite'; + $params=array('categoryid' => $entityid); + break; + + default: + qa_fatal_error('Favorite type not recognized'); + break; + } + + qa_report_event($action, $userid, $handle, $cookieid, $params); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-format.php b/qa-include/qa-app-format.php new file mode 100644 index 000000000..b1e2ec7bf --- /dev/null +++ b/qa-include/qa-app-format.php @@ -0,0 +1,1678 @@ + array( 'main/1_year' , 'main/x_years' ), + 2629800 => array( 'main/1_month' , 'main/x_months' ), + 604800 => array( 'main/1_week' , 'main/x_weeks' ), + 86400 => array( 'main/1_day' , 'main/x_days' ), + 3600 => array( 'main/1_hour' , 'main/x_hours' ), + 60 => array( 'main/1_minute' , 'main/x_minutes' ), + 1 => array( 'main/1_second' , 'main/x_seconds' ), + ); + + foreach ($scales as $scale => $phrases) + if ($seconds>=$scale) { + $count=floor($seconds/$scale); + + if ($count==1) + $string=qa_lang($phrases[0]); + else + $string=qa_lang_sub($phrases[1], $count); + + break; + } + + return $string; + } + + + function qa_post_is_by_user($post, $userid, $cookieid) +/* + Check if $post is by user $userid, or if post is anonymous and $userid not specified, then + check if $post is by the anonymous user identified by $cookieid +*/ + { + // In theory we should only test against NULL here, i.e. use isset($post['userid']) + // but the risk of doing so is so high (if a bug creeps in that allows userid=0) + // that I'm doing a tougher test. This will break under a zero user or cookie id. + + if (@$post['userid'] || $userid) + return @$post['userid']==$userid; + elseif (@$post['cookieid']) + return strcmp($post['cookieid'], $cookieid)==0; + + return false; + } + + + function qa_userids_handles_html($useridhandles, $microformats=false) +/* + Return array which maps the ['userid'] and/or ['lastuserid'] in each element of + $useridhandles to its HTML representation. For internal user management, corresponding + ['handle'] and/or ['lasthandle'] are required in each element. +*/ + { + require_once QA_INCLUDE_DIR.'qa-app-users.php'; + + if (QA_FINAL_EXTERNAL_USERS) { + $keyuserids=array(); + + foreach ($useridhandles as $useridhandle) { + if (isset($useridhandle['userid'])) + $keyuserids[$useridhandle['userid']]=true; + + if (isset($useridhandle['lastuserid'])) + $keyuserids[$useridhandle['lastuserid']]=true; + } + + if (count($keyuserids)) + return qa_get_users_html(array_keys($keyuserids), true, qa_path_to_root(), $microformats); + else + return array(); + + } else { + $usershtml=array(); + + foreach ($useridhandles as $useridhandle) { + if (isset($useridhandle['userid']) && $useridhandle['handle']) + $usershtml[$useridhandle['userid']]=qa_get_one_user_html($useridhandle['handle'], $microformats); + + if (isset($useridhandle['lastuserid']) && $useridhandle['lasthandle']) + $usershtml[$useridhandle['lastuserid']]=qa_get_one_user_html($useridhandle['lasthandle'], $microformats); + } + + return $usershtml; + } + } + + + function qa_tag_html($tag, $microformats=false) +/* + Convert textual $tag to HTML representation +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + return ''; + } + + + function qa_category_path($navcategories, $categoryid) +/* + Given $navcategories retrieved for $categoryid from the database (using qa_db_category_nav_selectspec(...)), + return an array of elements from $navcategories for the hierarchy down to $categoryid. +*/ + { + $upcategories=array(); + + for ($upcategory=@$navcategories[$categoryid]; isset($upcategory); $upcategory=@$navcategories[$upcategory['parentid']]) + $upcategories[$upcategory['categoryid']]=$upcategory; + + return array_reverse($upcategories, true); + } + + + function qa_category_path_html($navcategories, $categoryid) +/* + Given $navcategories retrieved for $categoryid from the database (using qa_db_category_nav_selectspec(...)), + return some HTML that shows the category hierarchy down to $categoryid. +*/ + { + $categories=qa_category_path($navcategories, $categoryid); + + $html=''; + foreach ($categories as $category) + $html.=(strlen($html) ? ' / ' : '').qa_html($category['title']); + + return $html; + } + + + function qa_category_path_request($navcategories, $categoryid) +/* + Given $navcategories retrieved for $categoryid from the database (using qa_db_category_nav_selectspec(...)), + return a Q2A request string that represents the category hierarchy down to $categoryid. +*/ + { + $categories=qa_category_path($navcategories, $categoryid); + + $request=''; + foreach ($categories as $category) + $request.=(strlen($request) ? '/' : '').$category['tags']; + + return $request; + } + + + function qa_ip_anchor_html($ip, $anchorhtml=null) +/* + Return HTML to use for $ip address, which links to appropriate page with $anchorhtml +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + if (!strlen($anchorhtml)) + $anchorhtml=qa_html($ip); + + return ''.$anchorhtml.''; + } + + + function qa_post_html_fields($post, $userid, $cookieid, $usershtml, $dummy, $options=array()) +/* + Given $post retrieved from database, return array of mostly HTML to be passed to theme layer. + $userid and $cookieid refer to the user *viewing* the page. + $usershtml is an array of [user id] => [HTML representation of user] built ahead of time. + $dummy is a placeholder (used to be $categories parameter but that's no longer needed) + $options is an array which sets what is displayed (see qa_post_html_defaults() in qa-app-options.php) + If something is missing from $post (e.g. ['content']), correponding HTML also omitted. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-app-updates.php'; + + if (isset($options['blockwordspreg'])) + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + $fields=array(); + $fields['raw']=$post; + + // Useful stuff used throughout function + + $postid=$post['postid']; + $isquestion=($post['basetype']=='Q'); + $isanswer=($post['basetype']=='A'); + $isbyuser=qa_post_is_by_user($post, $userid, $cookieid); + $anchor=urlencode(qa_anchor($post['basetype'], $postid)); + $elementid=isset($options['elementid']) ? $options['elementid'] : $anchor; + $microformats=@$options['microformats']; + $isselected=@$options['isselected']; + + // High level information + + $fields['hidden']=@$post['hidden']; + $fields['tags']='ID="'.qa_html($elementid).'"'; + + if ($microformats) + $fields['classes']='hentry '.($isquestion ? 'question' : ($isanswer ? ($isselected ? 'answer answer-selected' : 'answer') : 'comment')); + + // Question-specific stuff (title, URL, tags, answer count, category) + + if ($isquestion) { + if (isset($post['title'])) { + $fields['url']=qa_q_path_html($postid, $post['title']); + + if (isset($options['blockwordspreg'])) + $post['title']=qa_block_words_replace($post['title'], $options['blockwordspreg']); + + $fields['title']=qa_html($post['title']); + if ($microformats) + $fields['title']=''.$fields['title'].''; + + /*if (isset($post['score'])) // useful for setting match thresholds + $fields['title'].=' ('.$post['score'].')';*/ + } + + if (@$options['tagsview'] && isset($post['tags'])) { + $fields['q_tags']=array(); + + $tags=qa_tagstring_to_tags($post['tags']); + foreach ($tags as $tag) { + if (isset($options['blockwordspreg']) && count(qa_block_words_match_all($tag, $options['blockwordspreg']))) // skip censored tags + continue; + + $fields['q_tags'][]=qa_tag_html($tag, $microformats); + } + } + + if (@$options['answersview'] && isset($post['acount'])) { + $fields['answers_raw']=$post['acount']; + + $fields['answers']=($post['acount']==1) ? qa_lang_html_sub_split('main/1_answer', '1', '1') + : qa_lang_html_sub_split('main/x_answers', number_format($post['acount'])); + + $fields['answer_selected']=isset($post['selchildid']); + } + + if (@$options['viewsview'] && isset($post['views'])) { + $fields['views_raw']=$post['views']; + + $fields['views']=($post['views']==1) ? qa_lang_html_sub_split('main/1_view', '1', '1') : + qa_lang_html_sub_split('main/x_views', number_format($post['views'])); + } + + if (@$options['categoryview'] && isset($post['categoryname']) && isset($post['categorybackpath'])) + $fields['where']=qa_lang_html_sub_split('main/in_category_x', + ''.qa_html($post['categoryname']).''); + } + + // Answer-specific stuff (selection) + + if ($isanswer) { + $fields['selected']=$isselected; + + if ($isselected) + $fields['select_text']=qa_lang_html('question/select_text'); + } + + // Post content + + if (@$options['contentview'] && !empty($post['content'])) { + $viewer=qa_load_viewer($post['content'], $post['format']); + + $fields['content']=$viewer->get_html($post['content'], $post['format'], array( + 'blockwordspreg' => @$options['blockwordspreg'], + 'showurllinks' => @$options['showurllinks'], + 'linksnewwindow' => @$options['linksnewwindow'], + )); + + if ($microformats) + $fields['content']=''.$fields['content'].''; + + $fields['content']=''.$fields['content']; + // this is for backwards compatibility with any existing links using the old style of anchor + // that contained the post id only (changed to be valid under W3C specifications) + } + + // Voting stuff + + if (@$options['voteview']) { + $voteview=$options['voteview']; + + // Calculate raw values and pass through + + $upvotes=(int)@$post['upvotes']; + $downvotes=(int)@$post['downvotes']; + $netvotes=(int)($upvotes-$downvotes); + + $fields['upvotes_raw']=$upvotes; + $fields['downvotes_raw']=$downvotes; + $fields['netvotes_raw']=$netvotes; + + // Create HTML versions... + + $upvoteshtml=qa_html($upvotes); + $downvoteshtml=qa_html($downvotes); + + if ($netvotes>=1) + $netvoteshtml='+'.qa_html($netvotes); + elseif ($netvotes<=-1) + $netvoteshtml='–'.qa_html(-$netvotes); + else + $netvoteshtml='0'; + + // ...with microformats if appropriate + + if ($microformats) { + $netvoteshtml.=''. + ''; + $upvoteshtml=''.$upvoteshtml.''; + $downvoteshtml=''.$downvoteshtml.''; + } + + // Pass information on vote viewing + + // $voteview will be one of: + // updown, updown-disabled-level, updown-disabled-page, updown-uponly-level + // net, net-disabled-level, net-disabled-page, net-uponly-level + + $fields['vote_view']=(substr($voteview, 0, 6)=='updown') ? 'updown' : 'net'; + + $fields['upvotes_view']=($upvotes==1) ? qa_lang_html_sub_split('main/1_liked', $upvoteshtml, '1') + : qa_lang_html_sub_split('main/x_liked', $upvoteshtml); + + $fields['downvotes_view']=($downvotes==1) ? qa_lang_html_sub_split('main/1_disliked', $downvoteshtml, '1') + : qa_lang_html_sub_split('main/x_disliked', $downvoteshtml); + + $fields['netvotes_view']=(abs($netvotes)==1) ? qa_lang_html_sub_split('main/1_vote', $netvoteshtml, '1') + : qa_lang_html_sub_split('main/x_votes', $netvoteshtml); + + // Voting buttons + + $fields['vote_tags']='ID="voting_'.qa_html($postid).'"'; + $onclick='onClick="return qa_vote_click(this);"'; + + if ($fields['hidden']) { + $fields['vote_state']='disabled'; + $fields['vote_up_tags']='TITLE="'.qa_lang_html($isanswer ? 'main/vote_disabled_hidden_a' : 'main/vote_disabled_hidden_q').'"'; + $fields['vote_down_tags']=$fields['vote_up_tags']; + + } elseif ($isbyuser) { + $fields['vote_state']='disabled'; + $fields['vote_up_tags']='TITLE="'.qa_lang_html($isanswer ? 'main/vote_disabled_my_a' : 'main/vote_disabled_my_q').'"'; + $fields['vote_down_tags']=$fields['vote_up_tags']; + + } elseif (strpos($voteview, '-disabled-')) { + $fields['vote_state']=(@$post['uservote']>0) ? 'voted_up_disabled' : ((@$post['uservote']<0) ? 'voted_down_disabled' : 'disabled'); + + if (strpos($voteview, '-disabled-page')) + $fields['vote_up_tags']='TITLE="'.qa_lang_html('main/vote_disabled_q_page_only').'"'; + else + $fields['vote_up_tags']='TITLE="'.qa_lang_html('main/vote_disabled_level').'"'; + + $fields['vote_down_tags']=$fields['vote_up_tags']; + + } elseif (@$post['uservote']>0) { + $fields['vote_state']='voted_up'; + $fields['vote_up_tags']='TITLE="'.qa_lang_html('main/voted_up_popup').'" NAME="'.qa_html('vote_'.$postid.'_0_'.$elementid).'"'.$onclick; + $fields['vote_down_tags']=' '; + + } elseif (@$post['uservote']<0) { + $fields['vote_state']='voted_down'; + $fields['vote_up_tags']=' '; + $fields['vote_down_tags']='TITLE="'.qa_lang_html('main/voted_down_popup').'" NAME="'.qa_html('vote_'.$postid.'_0_'.$elementid).'" '.$onclick; + + } else { + $fields['vote_up_tags']='TITLE="'.qa_lang_html('main/vote_up_popup').'" NAME="'.qa_html('vote_'.$postid.'_1_'.$elementid).'" '.$onclick; + + if (strpos($voteview, '-uponly-level')) { + $fields['vote_state']='up_only'; + $fields['vote_down_tags']='TITLE="'.qa_lang_html('main/vote_disabled_down').'"'; + + } else { + $fields['vote_state']='enabled'; + $fields['vote_down_tags']='TITLE="'.qa_lang_html('main/vote_down_popup').'" NAME="'.qa_html('vote_'.$postid.'_-1_'.$elementid).'" '.$onclick; + } + } + } + + // Flag count + + if (@$options['flagsview'] && @$post['flagcount']) + $fields['flags']=($post['flagcount']==1) ? qa_lang_html_sub_split('main/1_flag', '1', '1') + : qa_lang_html_sub_split('main/x_flags', $post['flagcount']); + + // Created when and by whom + + $fields['meta_order']=qa_lang_html('main/meta_order'); // sets ordering of meta elements which can be language-specific + + if (@$options['whatview'] ) { + $fields['what']=qa_lang_html($isquestion ? 'main/asked' : ($isanswer ? 'main/answered' : 'main/commented')); + + if (@$options['whatlink'] && !$isquestion) + $fields['what_url']='?show='.qa_html($postid).'#'.qa_html($anchor); + } + + if (isset($post['created']) && @$options['whenview']) { + $fields['when']=qa_when_to_html($post['created'], @$options['fulldatedays']); + + if ($microformats) + $fields['when']['data']=''.$fields['when']['data'].''; + } + + if (@$options['whoview']) { + $fields['who']=qa_who_to_html($isbyuser, @$post['userid'], $usershtml, @$options['ipview'] ? @$post['createip'] : null, $microformats); + + if (isset($post['points'])) { + if (@$options['pointsview']) + $fields['who']['points']=($post['points']==1) ? qa_lang_html_sub_split('main/1_point', '1', '1') + : qa_lang_html_sub_split('main/x_points', qa_html(number_format($post['points']))); + + if (isset($options['pointstitle'])) + $fields['who']['title']=qa_get_points_title_html($post['points'], $options['pointstitle']); + } + + if (isset($post['level'])) + $fields['who']['level']=qa_html(qa_user_level_string($post['level'])); + } + + if ((!QA_FINAL_EXTERNAL_USERS) && (@$options['avatarsize']>0) && isset($post['flags'])) + $fields['avatar']=qa_get_user_avatar_html($post['flags'], $post['email'], $post['handle'], + $post['avatarblobid'], $post['avatarwidth'], $post['avatarheight'], $options['avatarsize']); + + // Updated when and by whom + + if ( + @$options['updateview'] && isset($post['updated']) && + (($post['updatetype']!=QA_UPDATE_SELECTED) || $isselected) && // only show selected change if it's still selected + ( // otherwise check if one of these conditions is fulfilled... + (!isset($post['created'])) || // ... we didn't show the created time (should never happen in practice) + ($post['hidden'] && ($post['updatetype']==QA_UPDATE_VISIBLE)) || // ... the post was hidden as the last action + (isset($post['closedbyid']) && ($post['updatetype']==QA_UPDATE_CLOSED)) || // ... the post was closed as the last action + (abs($post['updated']-$post['created'])>300) || // ... or over 5 minutes passed between create and update times + ($post['lastuserid']!=$post['userid']) // ... or it was updated by a different user + ) + ) { + switch ($post['updatetype']) { + case QA_UPDATE_TYPE: + case QA_UPDATE_PARENT: + $langstring='main/moved'; + break; + + case QA_UPDATE_CATEGORY: + $langstring='main/recategorized'; + break; + + case QA_UPDATE_VISIBLE: + $langstring=$post['hidden'] ? 'main/hidden' : 'main/reshown'; + break; + + case QA_UPDATE_CLOSED: + $langstring=isset($post['closedbyid']) ? 'main/closed' : 'main/reopened'; + break; + + case QA_UPDATE_TAGS: + $langstring='main/retagged'; + break; + + case QA_UPDATE_SELECTED: + $langstring='main/selected'; + break; + + default: + $langstring='main/edited'; + break; + } + + $fields['what_2']=qa_lang_html($langstring); + + if (@$options['whenview']) { + $fields['when_2']=qa_when_to_html($post['updated'], @$options['fulldatedays']); + + if ($microformats) + $fields['when_2']['data']=''.$fields['when_2']['data'].''; + } + + if (isset($post['lastuserid']) && @$options['whoview']) + $fields['who_2']=qa_who_to_html(isset($userid) && ($post['lastuserid']==$userid), $post['lastuserid'], $usershtml, @$options['ipview'] ? $post['lastip'] : null, false); + } + + // That's it! + + return $fields; + } + + + function qa_who_to_html($isbyuser, $postuserid, $usershtml, $ip=null, $microformats=false) +/* + Return array of split HTML (prefix, data, suffix) to represent author of post +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + if (isset($postuserid) && isset($usershtml[$postuserid])) { + $whohtml=$usershtml[$postuserid]; + if ($microformats) + $whohtml=''.$whohtml.''; + + } elseif ($isbyuser) + $whohtml=qa_lang_html('main/me'); + + else { + $whohtml=qa_lang_html('main/anonymous'); + + if (isset($ip)) + $whohtml=qa_ip_anchor_html($ip, $whohtml); + } + + return qa_lang_html_sub_split('main/by_x', $whohtml); + } + + + function qa_when_to_html($timestamp, $fulldatedays) +/* + Return array of split HTML (prefix, data, suffix) to represent unix $timestamp, with the full date shown if it's + more than $fulldatedays ago +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $interval=qa_opt('db_time')-$timestamp; + + if ( ($interval<0) || (isset($fulldatedays) && ($interval>(86400*$fulldatedays))) ) { // full style date + $stampyear=date('Y', $timestamp); + $thisyear=date('Y', qa_opt('db_time')); + + return array( + 'data' => qa_html(strtr(qa_lang(($stampyear==$thisyear) ? 'main/date_format_this_year' : 'main/date_format_other_years'), array( + '^day' => date((qa_lang('main/date_day_min_digits')==2) ? 'd' : 'j', $timestamp), + '^month' => qa_lang('main/date_month_'.date('n', $timestamp)), + '^year' => date((qa_lang('main/date_year_digits')==2) ? 'y' : 'Y', $timestamp), + ))), + ); + + } else // ago-style date + return qa_lang_html_sub_split('main/x_ago', qa_html(qa_time_to_string($interval))); + } + + + function qa_other_to_q_html_fields($question, $userid, $cookieid, $usershtml, $dummy, $options) +/* + Return array of mostly HTML to be passed to theme layer, to *link* to an answer, comment or edit on + $question, as retrieved from database, with fields prefixed 'o' for the answer, comment or edit. + $userid, $cookieid, $usershtml, $options are passed through to qa_post_html_fields(). +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-app-updates.php'; + + $fields=qa_post_html_fields($question, $userid, $cookieid, $usershtml, null, $options); + + switch ($question['obasetype'].'-'.@$question['oupdatetype']) { + case 'Q-': + $langstring='main/asked'; + break; + + case 'Q-'.QA_UPDATE_VISIBLE: + $langstring=$question['hidden'] ? 'main/hidden' : 'main/reshown'; + break; + + case 'Q-'.QA_UPDATE_CLOSED: + $langstring=isset($question['closedbyid']) ? 'main/closed' : 'main/reopened'; + break; + + case 'Q-'.QA_UPDATE_TAGS: + $langstring='main/retagged'; + break; + + case 'Q-'.QA_UPDATE_CATEGORY: + $langstring='main/recategorized'; + break; + + case 'A-': + $langstring='main/answered'; + break; + + case 'A-'.QA_UPDATE_SELECTED: + $langstring='main/answer_selected'; + break; + + case 'A-'.QA_UPDATE_VISIBLE: + $langstring=$question['ohidden'] ? 'main/hidden' : 'main/answer_reshown'; + break; + + case 'A-'.QA_UPDATE_CONTENT: + $langstring='main/answer_edited'; + break; + + case 'Q-'.QA_UPDATE_FOLLOWS: + $langstring='main/asked_related_q'; + break; + + case 'C-': + $langstring='main/commented'; + break; + + case 'C-'.QA_UPDATE_TYPE: + $langstring='main/comment_moved'; + break; + + case 'C-'.QA_UPDATE_VISIBLE: + $langstring=$question['ohidden'] ? 'main/hidden' : 'main/comment_reshown'; + break; + + case 'C-'.QA_UPDATE_CONTENT: + $langstring='main/comment_edited'; + break; + + case 'Q-'.QA_UPDATE_CONTENT: + default: + $langstring='main/edited'; + break; + } + + $fields['what']=qa_lang_html($langstring); + + if ( ($question['obasetype']!='Q') || (@$question['oupdatetype']==QA_UPDATE_FOLLOWS) ) + $fields['what_url']=qa_q_path_html($question['postid'], $question['title'], false, $question['obasetype'], $question['opostid']); + + if (@$options['contentview'] && !empty($question['ocontent'])) { + $viewer=qa_load_viewer($question['ocontent'], $question['oformat']); + + $fields['content']=$viewer->get_html($question['ocontent'], $question['oformat'], array( + 'blockwordspreg' => @$options['blockwordspreg'], + 'showurllinks' => @$options['showurllinks'], + 'linksnewwindow' => @$options['linksnewwindow'], + )); + } + + if (@$options['whenview']) + $fields['when']=qa_when_to_html($question['otime'], @$options['fulldatedays']); + + if (@$options['whoview']) { + $isbyuser=qa_post_is_by_user(array('userid' => $question['ouserid'], 'cookieid' => @$question['ocookieid']), $userid, $cookieid); + + $fields['who']=qa_who_to_html($isbyuser, $question['ouserid'], $usershtml, @$options['ipview'] ? @$question['oip'] : null, false); + + if (isset($question['opoints'])) { + if (@$options['pointsview']) + $fields['who']['points']=($question['opoints']==1) ? qa_lang_html_sub_split('main/1_point', '1', '1') + : qa_lang_html_sub_split('main/x_points', qa_html(number_format($question['opoints']))); + + if (isset($options['pointstitle'])) + $fields['who']['title']=qa_get_points_title_html($question['opoints'], $options['pointstitle']); + } + + if (isset($question['olevel'])) + $fields['who']['level']=qa_html(qa_user_level_string($question['olevel'])); + } + + unset($fields['flags']); + if (@$options['flagsview'] && @$question['oflagcount']) + $fields['flags']=($question['oflagcount']==1) ? qa_lang_html_sub_split('main/1_flag', '1', '1') + : qa_lang_html_sub_split('main/x_flags', $question['oflagcount']); + + unset($fields['avatar']); + if ((!QA_FINAL_EXTERNAL_USERS) && (@$options['avatarsize']>0)) + $fields['avatar']=qa_get_user_avatar_html($question['oflags'], $question['oemail'], $question['ohandle'], + $question['oavatarblobid'], $question['oavatarwidth'], $question['oavatarheight'], $options['avatarsize']); + + return $fields; + } + + + function qa_any_to_q_html_fields($question, $userid, $cookieid, $usershtml, $dummy, $options) +/* + Based on the elements in $question, return HTML to be passed to theme layer to link + to the question, or to an associated answer, comment or edit. +*/ + { + if (isset($question['opostid'])) + $fields=qa_other_to_q_html_fields($question, $userid, $cookieid, $usershtml, null, $options); + else + $fields=qa_post_html_fields($question, $userid, $cookieid, $usershtml, null, $options); + + return $fields; + } + + + function qa_any_sort_by_date($questions) +/* + Each element in $questions represents a question and optional associated answer, comment or edit, as retrieved from database. + Return it sorted by the date appropriate for each element, without removing duplicate references to the same question. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-util-sort.php'; + + foreach ($questions as $key => $question) // collect information about action referenced by each $question + $questions[$key]['sort']=-(isset($question['opostid']) ? $question['otime'] : $question['created']); + + qa_sort_by($questions, 'sort'); + + return $questions; + } + + + function qa_any_sort_and_dedupe($questions) +/* + Each element in $questions represents a question and optional associated answer, comment or edit, as retrieved from database. + Return it sorted by the date appropriate for each element, and keep only the first item related to each question. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-util-sort.php'; + + foreach ($questions as $key => $question) { // collect information about action referenced by each $question + if (isset($question['opostid'])) { + $questions[$key]['_time']=$question['otime']; + $questions[$key]['_type']=$question['obasetype']; + $questions[$key]['_userid']=@$question['ouserid']; + } else { + $questions[$key]['_time']=$question['created']; + $questions[$key]['_type']='Q'; + $questions[$key]['_userid']=$question['userid']; + } + + $questions[$key]['sort']=-$questions[$key]['_time']; + } + + qa_sort_by($questions, 'sort'); + + $keepquestions=array(); // now remove duplicate references to same question + foreach ($questions as $question) { // going in order from most recent to oldest + $laterquestion=@$keepquestions[$question['postid']]; + + if ((!isset($laterquestion)) || // keep this reference if there is no more recent one, or... + ( + (@$laterquestion['oupdatetype']) && // the more recent reference was an edit + (!@$question['oupdatetype']) && // this is not an edit + ($laterquestion['_type']==$question['_type']) && // the same part (Q/A/C) is referenced here + ($laterquestion['_userid']==$question['_userid']) && // the same user made the later edit + (abs($laterquestion['_time']-$question['_time'])<300) // the edit was within 5 minutes of creation + ) + ) + $keepquestions[$question['postid']]=$question; + } + + return $keepquestions; + } + + + function qa_any_get_userids_handles($questions) +/* + Each element in $questions represents a question and optional associated answer, comment or edit, as retrieved from database. + Return an array of elements (userid,handle) for the appropriate user for each element. +*/ + { + $userids_handles=array(); + + foreach ($questions as $question) + if (isset($question['opostid'])) + $userids_handles[]=array( + 'userid' => @$question['ouserid'], + 'handle' => @$question['ohandle'], + ); + + else + $userids_handles[]=array( + 'userid' => @$question['userid'], + 'handle' => @$question['handle'], + ); + + return $userids_handles; + } + + + function qa_html_convert_urls($html, $newwindow=false) +/* + Return $html with any URLs converted into links (with nofollow and in a new window if $newwindow) + URL regular expressions can get crazy: http://internet.ls-la.net/folklore/url-regexpr.html + So this is something quick and dirty that should do the trick in most cases +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + return trim(preg_replace('/([^A-Za-z0-9])((http|https|ftp):\/\/([^\s&<>"\'\.])+\.([^\s&<>"\']|&)+)/i', '\1\2', ' '.$html.' ')); + } + + + function qa_url_to_html_link($url, $newwindow=false) +/* + Return HTML representation of $url (if it appears to be an URL), linked with nofollow and in a new window if $newwindow +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + if (is_numeric(strpos($url, '.'))) { + $linkurl=$url; + if (!is_numeric(strpos($linkurl, ':/'))) + $linkurl='http://'.$linkurl; + + return ''.qa_html($url).''; + + } else + return qa_html($url); + } + + + function qa_insert_login_links($htmlmessage, $topage=null, $params=null) +/* + Return $htmlmessage with ^1...^6 substituted for links to log in or register or confirm email and come back to $topage with $params +*/ + { + require_once QA_INCLUDE_DIR.'qa-app-users.php'; + + $userlinks=qa_get_login_links(qa_path_to_root(), isset($topage) ? qa_path($topage, $params, '') : null); + + return strtr( + $htmlmessage, + + array( + '^1' => empty($userlinks['login']) ? '' : '', + '^2' => empty($userlinks['login']) ? '' : '', + '^3' => empty($userlinks['register']) ? '' : '', + '^4' => empty($userlinks['register']) ? '' : '', + '^5' => empty($userlinks['confirm']) ? '' : '', + '^6' => empty($userlinks['confirm']) ? '' : '', + ) + ); + } + + + function qa_html_page_links($request, $start, $pagesize, $count, $prevnext, $params=array(), $hasmore=false, $anchor=null) +/* + Return structure to pass through to theme layer to show linked page numbers for $request. + Q2A uses offset-based paging, i.e. pages are referenced in the URL by a 'start' parameter. + $start is current offset, there are $pagesize items per page and $count items in total + (unless $hasmore is true in which case there are at least $count items). + Show links to $prevnext pages before and after this one and include $params in the URLs. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $thispage=1+floor($start/$pagesize); + $lastpage=ceil(min($count, 1+QA_MAX_LIMIT_START)/$pagesize); + + if (($thispage>1) || ($lastpage>$thispage)) { + $links=array('label' => qa_lang_html('main/page_label'), 'items' => array()); + + $keypages[1]=true; + + for ($page=max(2, min($thispage, $lastpage)-$prevnext); $page<=min($thispage+$prevnext, $lastpage); $page++) + $keypages[$page]=true; + + $keypages[$lastpage]=true; + + if ($thispage>1) + $links['items'][]=array( + 'type' => 'prev', + 'label' => qa_lang_html('main/page_prev'), + 'page' => $thispage-1, + 'ellipsis' => false, + ); + + foreach (array_keys($keypages) as $page) + $links['items'][]=array( + 'type' => ($page==$thispage) ? 'this' : 'jump', + 'label' => $page, + 'page' => $page, + 'ellipsis' => (($page<$lastpage) || $hasmore) && (!isset($keypages[$page+1])), + ); + + if ($thispage<$lastpage) + $links['items'][]=array( + 'type' => 'next', + 'label' => qa_lang_html('main/page_next'), + 'page' => $thispage+1, + 'ellipsis' => false, + ); + + foreach ($links['items'] as $key => $link) + if ($link['page']!=$thispage) { + $params['start']=$pagesize*($link['page']-1); + $links['items'][$key]['url']=qa_path_html($request, $params, null, null, $anchor); + } + + } else + $links=null; + + return $links; + } + + + function qa_html_suggest_qs_tags($usingtags=false, $categoryrequest=null) +/* + Return HTML that suggests browsing all questions (in the category specified by $categoryrequest, if + it's not null) and also popular tags if $usingtags is true +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $hascategory=strlen($categoryrequest); + + $htmlmessage=$hascategory ? qa_lang_html('main/suggest_category_qs') : + ($usingtags ? qa_lang_html('main/suggest_qs_tags') : qa_lang_html('main/suggest_qs')); + + return strtr( + $htmlmessage, + + array( + '^1' => '', + '^2' => '', + '^3' => '', + '^4' => '', + ) + ); + } + + + function qa_html_suggest_ask($categoryid=null) +/* + Return HTML that suggest getting things started by asking a question, in $categoryid if not null +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $htmlmessage=qa_lang_html('main/suggest_ask'); + + return strtr( + $htmlmessage, + + array( + '^1' => '', + '^2' => '', + ) + ); + } + + + function qa_category_navigation($categories, $selectedid=null, $pathprefix='', $showqcount=true, $pathparams=null) +/* + Return the navigation structure for the category hierarchical menu, with $selectedid selected, + and links beginning with $pathprefix, and showing question counts if $showqcount +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $parentcategories=array(); + + foreach ($categories as $category) + $parentcategories[$category['parentid']][]=$category; + + $selecteds=qa_category_path($categories, $selectedid); + + return qa_category_navigation_sub($parentcategories, null, $selecteds, $pathprefix, $showqcount, $pathparams); + } + + + function qa_category_navigation_sub($parentcategories, $parentid, $selecteds, $pathprefix, $showqcount, $pathparams) +/* + Recursion function used by qa_category_navigation(...) to build hierarchical category menu. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $navigation=array(); + + if (!isset($parentid)) + $navigation['all']=array( + 'url' => qa_path_html($pathprefix, $pathparams), + 'label' => qa_lang_html('main/all_categories'), + 'selected' => !count($selecteds), + 'categoryid' => null, + ); + + if (isset($parentcategories[$parentid])) + foreach ($parentcategories[$parentid] as $category) + $navigation[qa_html($category['tags'])]=array( + 'url' => qa_path_html($pathprefix.$category['tags'], $pathparams), + 'label' => qa_html($category['title']), + 'popup' => qa_html(@$category['content']), + 'selected' => isset($selecteds[$category['categoryid']]), + 'note' => $showqcount ? ('('.qa_html(number_format($category['qcount'])).')') : null, + 'subnav' => qa_category_navigation_sub($parentcategories, $category['categoryid'], $selecteds, $pathprefix.$category['tags'].'/', $showqcount, $pathparams), + 'categoryid' => $category['categoryid'], + ); + + return $navigation; + } + + + function qa_users_sub_navigation() +/* + Return the sub navigation structure for user listing pages +*/ + { + if ((!QA_FINAL_EXTERNAL_USERS) && (qa_get_logged_in_level()>=QA_USER_LEVEL_MODERATOR)) { + return array( + 'users$' => array( + 'url' => qa_path_html('users'), + 'label' => qa_lang_html('main/highest_users'), + ), + + 'users/special' => array( + 'label' => qa_lang('users/special_users'), + 'url' => qa_path_html('users/special'), + ), + + 'users/blocked' => array( + 'label' => qa_lang('users/blocked_users'), + 'url' => qa_path_html('users/blocked'), + ), + ); + + } else + return null; + } + + + function qa_account_sub_navigation() +/* + Return the sub navigation structure for user account pages +*/ + { + return array( + 'account' => array( + 'label' => qa_lang_html('misc/nav_my_details'), + 'url' => qa_path_html('account'), + ), + + 'favorites' => array( + 'label' => qa_lang_html('misc/nav_my_favorites'), + 'url' => qa_path_html('favorites'), + ), + ); + } + + + function qa_custom_page_url($page) +/* + Return the url for $page retrieved from the database +*/ + { + return ($page['flags'] & QA_PAGE_FLAGS_EXTERNAL) + ? (is_numeric(strpos($page['tags'], '://')) ? $page['tags'] : qa_path_to_root().$page['tags']) + : qa_path($page['tags']); + } + + + function qa_navigation_add_page(&$navigation, $page) +/* + Add an element to the $navigation array corresponding to $page retrieved from the database +*/ + { + if ( + (!qa_permit_value_error($page['permit'], qa_get_logged_in_userid(), qa_get_logged_in_level(), qa_get_logged_in_flags())) || !isset($page['permit']) + ) + $navigation[($page['flags'] & QA_PAGE_FLAGS_EXTERNAL) ? ('custom-'.$page['pageid']) : $page['tags']]=array( + 'url' => qa_html(qa_custom_page_url($page)), + 'label' => qa_html($page['title']), + 'opposite' => ($page['nav']=='O'), + 'target' => ($page['flags'] & QA_PAGE_FLAGS_NEW_WINDOW) ? '_blank' : null, + ); + } + + + function qa_match_to_min_score($match) +/* + Convert an admin option for matching into a threshold for the score given by database search +*/ + { + return 10-2*$match; + } + + + function qa_set_display_rules(&$qa_content, $effects) +/* + For each [target] => [source] in $effects, set up $qa_content so that the visibility of the DOM element ID + target is equal to the checked state or boolean-casted value of the DOM element ID source. Each source can + also combine multiple DOM IDs using JavaScript(=PHP) operators. This is twisted but rather convenient. +*/ + { + $function='qa_display_rule_'.count(@$qa_content['script_lines']); + + $keysourceids=array(); + + foreach ($effects as $target => $sources) + if (preg_match_all('/[A-Za-z_][A-Za-z0-9_]*/', $sources, $matches)) // element names must be legal JS variable names + foreach ($matches[0] as $element) + $keysourceids[$element]=true; + + $funcscript=array("function ".$function."(first) {"); // build the Javascripts + $loadscript=array(); + + foreach ($keysourceids as $key => $dummy) { + $funcscript[]="\tvar e=document.getElementById(".qa_js($key).");"; + $funcscript[]="\tvar ".$key."=e && (e.checked || (e.options && e.options[e.selectedIndex].value));"; + $loadscript[]="var e=document.getElementById(".qa_js($key).");"; + $loadscript[]="if (e) {"; + $loadscript[]="\t".$key."_oldonclick=e.onclick;"; + $loadscript[]="\te.onclick=function() {"; + $loadscript[]="\t\t".$function."(false);"; + $loadscript[]="\t\tif (typeof ".$key."_oldonclick=='function')"; + $loadscript[]="\t\t\t".$key."_oldonclick();"; + $loadscript[]="\t}"; + $loadscript[]="}"; + } + + foreach ($effects as $target => $sources) { + $funcscript[]="\tvar e=document.getElementById(".qa_js($target).");"; + $funcscript[]="\tif (e) { var d=(".$sources."); if (first || (e.nodeName=='SPAN')) { e.style.display=d ? '' : 'none'; } else { if (d) { $(e).fadeIn(); } else { $(e).fadeOut(); } } }"; + } + + $funcscript[]="}"; + $loadscript[]=$function."(true);"; + + $qa_content['script_lines'][]=$funcscript; + $qa_content['script_onloads'][]=$loadscript; + } + + + function qa_set_up_tag_field(&$qa_content, &$field, $fieldname, $tags, $exampletags, $completetags, $maxtags) +/* + Set up $qa_content and $field (with HTML name $fieldname) for tag auto-completion, where + $exampletags are suggestions and $completetags are simply the most popular ones. Show up to $maxtags. +*/ + { + $template='^'; + + $qa_content['script_rel'][]='qa-content/qa-ask.js?'.QA_VERSION; + $qa_content['script_var']['qa_tag_template']=$template; + $qa_content['script_var']['qa_tag_onlycomma']=(int)qa_opt('tag_separator_comma'); + $qa_content['script_var']['qa_tags_examples']=qa_html(implode(',', $exampletags)); + $qa_content['script_var']['qa_tags_complete']=qa_html(implode(',', $completetags)); + $qa_content['script_var']['qa_tags_max']=(int)$maxtags; + + $separatorcomma=qa_opt('tag_separator_comma'); + + $field['label']=qa_lang_html($separatorcomma ? 'question/q_tags_comma_label' : 'question/q_tags_label'); + $field['value']=qa_html(implode($separatorcomma ? ', ' : ' ', $tags)); + $field['tags']='NAME="'.$fieldname.'" ID="tags" AUTOCOMPLETE="off" onKeyUp="qa_tag_hints();" onMouseUp="qa_tag_hints();"'; + + $sdn=' STYLE="display:none;"'; + + $field['note']= + ''.qa_lang_html('question/example_tags').''. + ''.qa_lang_html('question/matching_tags').''; + + foreach ($exampletags as $tag) + $field['note'].=str_replace('^', qa_html($tag), $template).' '; + + $field['note'].=''; + $field['note_force']=true; + } + + + function qa_get_tags_field_value($fieldname) +/* + Get a list of user-entered tags submitted from a field that was created with qa_set_up_tag_field(...) +*/ + { + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + $text=qa_post_text($fieldname); + + if (qa_opt('tag_separator_comma')) + return array_unique(preg_split('/\s*,\s*/', trim(qa_strtolower(strtr($text, '/', ' '))), -1, PREG_SPLIT_NO_EMPTY)); + else + return array_unique(qa_string_to_words($text, true, false, false, false)); + } + + + function qa_set_up_category_field(&$qa_content, &$field, $fieldname, $navcategories, $categoryid, $allownone, $allownosub, $maxdepth=null, $excludecategoryid=null) +/* + Set up $qa_content and $field (with HTML name $fieldname) for hierarchical category navigation, with the initial value + set to $categoryid (and $navcategories retrieved for $categoryid using qa_db_category_nav_selectspec(...)). + If $allownone is true, it will allow selection of no category. If $allownosub is true, it will allow a category to be + selected without selecting a subcategory within. Set $maxdepth to the maximum depth of category that can be selected + (or null for no maximum) and $excludecategoryid to a category that should not be included. +*/ + { + $pathcategories=qa_category_path($navcategories, $categoryid); + + $startpath=''; + foreach ($pathcategories as $category) + $startpath.='/'.$category['categoryid']; + + if (!isset($maxdepth)) + $maxdepth=QA_CATEGORY_DEPTH; + $maxdepth=min(QA_CATEGORY_DEPTH, $maxdepth); + + $qa_content['script_rel'][]='qa-content/qa-ask.js?'.QA_VERSION; + $qa_content['script_onloads'][]='qa_category_select('.qa_js($fieldname).', '.qa_js($startpath).');'; + + $qa_content['script_var']['qa_cat_exclude']=$excludecategoryid; + $qa_content['script_var']['qa_cat_allownone']=(int)$allownone; + $qa_content['script_var']['qa_cat_allownosub']=(int)$allownosub; + $qa_content['script_var']['qa_cat_maxdepth']=$maxdepth; + + $field['type']='select'; + $field['tags']='NAME="'.$fieldname.'_0" ID="'.$fieldname.'_0" onChange="qa_category_select('.qa_js($fieldname).');"'; + $field['options']=array(); + + // create the menu that will be shown if Javascript is disabled + + if ($allownone) + $field['options']['']=qa_lang_html('main/no_category'); // this is also copied to first menu created by Javascript + + $keycategoryids=array(); + + if ($allownosub) { + $category=@$navcategories[$categoryid]; + $upcategory=$category; + + while (true) { // first get supercategories + $upcategory=@$navcategories[$upcategory['parentid']]; + + if (!isset($upcategory)) + break; + + $keycategoryids[$upcategory['categoryid']]=true; + } + + $keycategoryids=array_reverse($keycategoryids, true); + + $depth=count($keycategoryids); // number of levels above + + if (isset($category)) { + $depth++; // to count category itself + + foreach ($navcategories as $navcategory) // now get siblings and self + if (!strcmp($navcategory['parentid'], $category['parentid'])) + $keycategoryids[$navcategory['categoryid']]=true; + } + + if ($depth<$maxdepth) + foreach ($navcategories as $navcategory) // now get children, if not too deep + if (!strcmp($navcategory['parentid'], $categoryid)) + $keycategoryids[$navcategory['categoryid']]=true; + + } else { + $haschildren=false; + + foreach ($navcategories as $navcategory) // check if it has any children + if (!strcmp($navcategory['parentid'], $categoryid)) + $haschildren=true; + + if (!$haschildren) + $keycategoryids[$categoryid]=true; // show this category if it has no children + } + + foreach ($keycategoryids as $keycategoryid => $dummy) + if (strcmp($keycategoryid, $excludecategoryid)) + $field['options'][$keycategoryid]=qa_category_path_html($navcategories, $keycategoryid); + + $field['value']=@$field['options'][$categoryid]; + $field['note']='
'; + } + + + function qa_get_category_field_value($fieldname) +/* + Get the user-entered category id submitted from a field that was created with qa_set_up_category_field(...) +*/ + { + for ($level=QA_CATEGORY_DEPTH; $level>=1; $level--) { + $levelid=qa_post_text($fieldname.'_'.$level); + if (strlen($levelid)) + return $levelid; + } + + if (!isset($levelid)) { // no Javascript-generated menu was present so take original menu + $levelid=qa_post_text($fieldname.'_0'); + if (strlen($levelid)) + return $levelid; + } + + return null; + } + + + function qa_set_up_notify_fields(&$qa_content, &$fields, $basetype, $login_email, $innotify, $inemail, $errors_email, $fieldprefix='') +/* + Set up $qa_content and add to $fields to allow user to set if they want to be notified regarding their post. + $basetype is 'Q', 'A' or 'C' for question, answer or comment. $login_email is the email of logged in user, + or null if this is an anonymous post. $innotify, $inemail and $errors_email are from previous submission/validation. +*/ + { + $fields['notify']=array( + 'tags' => 'NAME="'.$fieldprefix.'notify"', + 'type' => 'checkbox', + 'value' => qa_html($innotify), + ); + + switch ($basetype) { + case 'Q': + $labelaskemail=qa_lang_html('question/q_notify_email'); + $labelonly=qa_lang_html('question/q_notify_label'); + $labelgotemail=qa_lang_html('question/q_notify_x_label'); + break; + + case 'A': + $labelaskemail=qa_lang_html('question/a_notify_email'); + $labelonly=qa_lang_html('question/a_notify_label'); + $labelgotemail=qa_lang_html('question/a_notify_x_label'); + break; + + case 'C': + $labelaskemail=qa_lang_html('question/c_notify_email'); + $labelonly=qa_lang_html('question/c_notify_label'); + $labelgotemail=qa_lang_html('question/c_notify_x_label'); + break; + } + + if (empty($login_email)) { + $fields['notify']['label']= + ''.$labelaskemail.''. + ''; + + $fields['notify']['tags'].='ID="'.$fieldprefix.'notify" onclick="if (document.getElementById(\''.$fieldprefix.'notify\').checked) document.getElementById(\''.$fieldprefix.'email\').focus();"'; + $fields['notify']['tight']=true; + + $fields['email']=array( + 'id' => $fieldprefix.'email_display', + 'tags' => 'NAME="'.$fieldprefix.'email" ID="'.$fieldprefix.'email"', + 'value' => qa_html($inemail), + 'note' => qa_lang_html('question/notify_email_note'), + 'error' => qa_html($errors_email), + ); + + qa_set_display_rules($qa_content, array( + $fieldprefix.'email_display' => $fieldprefix.'notify', + $fieldprefix.'email_shown' => $fieldprefix.'notify', + $fieldprefix.'email_hidden' => '!'.$fieldprefix.'notify', + )); + + } else { + $fields['notify']['label']=str_replace('^', qa_html($login_email), $labelgotemail); + } + } + + + function qa_get_site_theme() +/* + Return the theme that should be used for displaying the page +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + return qa_opt(qa_is_mobile_probably() ? 'site_theme_mobile' : 'site_theme'); + } + + + function qa_load_theme_class($theme, $template, $content, $request) +/* + Return the initialized class for $theme (or the default if it's gone), passing $template, $content and $request. + Also applies any registered plugin layers. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_layers; + + // First load the default class + + require_once QA_INCLUDE_DIR.'qa-theme-base.php'; + + $classname='qa_html_theme_base'; + + // Then load the selected theme if valid, otherwise load the default theme + + if (!file_exists(QA_THEME_DIR.$theme.'/qa-styles.css')) + $theme='Default'; + + $themeroothtml=qa_html(qa_path_to_root().'qa-theme/'.$theme.'/'); + + if (file_exists(QA_THEME_DIR.$theme.'/qa-theme.php')) { + require_once QA_THEME_DIR.$theme.'/qa-theme.php'; + + if (class_exists('qa_html_theme')) + $classname='qa_html_theme'; + } + + // Then load any theme layers using some class-munging magic (substitute class names) + + $layerindex=0; + + foreach ($qa_layers as $layer) { + $layerphp=file_get_contents($layer['directory'].$layer['include']); + + if (strlen($layerphp)) { + $newclassname='qa_layer_'.(++$layerindex).'_from_'.preg_replace('/[^A-Za-z0-9_]+/', '_', basename($layer['include'])); + // include file name in layer class name to make debugging easier if there is an error + + if (preg_match('/\s+class\s+qa_html_theme_layer\s+extends\s+qa_html_theme_base\s+/im', $layerphp)!=1) + qa_fatal_error('Class for layer must be declared as "class qa_html_theme_layer extends qa_html_theme_base" in '.$layer['directory'].$layer['include']); + + $searchwordreplace=array( + 'qa_html_theme_layer' => $newclassname, + 'qa_html_theme_base' => $classname, + 'QA_HTML_THEME_LAYER_DIRECTORY' => "'".$layer['directory']."'", + 'QA_HTML_THEME_LAYER_URLTOROOT' => "'".qa_path_to_root().$layer['urltoroot']."'", + ); + + foreach ($searchwordreplace as $searchword => $replace) + if (preg_match_all('/\W('.preg_quote($searchword, '/').')\W/im', $layerphp, $matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) { + $searchmatches=array_reverse($matches[1]); // don't use preg_replace due to complication of escaping replacement phrase + + foreach ($searchmatches as $searchmatch) + $layerphp=substr_replace($layerphp, $replace, $searchmatch[1], strlen($searchmatch[0])); + } + + // echo '
'.htmlspecialchars($layerphp).'
'; // to debug munged code + + eval('?'.'>'.$layerphp); + + $classname=$newclassname; + } + } + + // Finally, instantiate the object + + $themeclass=new $classname($template, $content, $themeroothtml, $request); + + return $themeclass; + } + + + function qa_load_editor($content, $format, &$editorname) +/* + Return an instantiation of the appropriate editor module class, given $content in $format + Pass the preferred module name in $editorname, on return it will contain the name of the module used. +*/ + { + $maxeditor=qa_load_module('editor', $editorname); // take preferred one first + + if (isset($maxeditor) && method_exists($maxeditor, 'calc_quality')) { + $maxquality=$maxeditor->calc_quality($content, $format); + if ($maxquality>=0.5) + return $maxeditor; + + } else + $maxquality=0; + + $editormodules=qa_load_modules_with('editor', 'calc_quality'); + foreach ($editormodules as $tryname => $tryeditor) { + $tryquality=$tryeditor->calc_quality($content, $format); + + if ($tryquality>$maxquality) { + $maxeditor=$tryeditor; + $maxquality=$tryquality; + $editorname=$tryname; + } + } + + return $maxeditor; + } + + + function qa_editor_load_field($editor, &$qa_content, $content, $format, $fieldname, $rows, $focusnow=false, $loadnow=true) +/* + Return a form field from the $editor module while making necessary modifications to $qa_content. The parameters + $content, $format, $fieldname, $rows and $focusnow are passed through to the module's get_field() method. ($focusnow + is deprecated as a parameter to get_field() but it's still passed through for old editor modules.) Based on + $focusnow and $loadnow, also add the editor's load and/or focus scripts to $qa_content's onload handlers. +*/ + { + if (!isset($editor)) + qa_fatal_error('No editor found for format: '.$format); + + $field=$editor->get_field($qa_content, $content, $format, $fieldname, $rows, $focusnow); + + $onloads=array(); + + if ($loadnow && method_exists($editor, 'load_script')) + $onloads[]=$editor->load_script($fieldname); + + if ($focusnow && method_exists($editor, 'focus_script')) + $onloads[]=$editor->focus_script($fieldname); + + if (count($onloads)) + $qa_content['script_onloads'][]=$onloads; + + return $field; + } + + + function qa_load_viewer($content, $format) +/* + Return an instantiation of the appropriate viewer module class, given $content in $format +*/ + { + $maxviewer=null; + $maxquality=0; + + $viewermodules=qa_load_modules_with('viewer', 'calc_quality'); + + foreach ($viewermodules as $tryviewer) { + $tryquality=$tryviewer->calc_quality($content, $format); + + if ($tryquality>$maxquality) { + $maxviewer=$tryviewer; + $maxquality=$tryquality; + } + } + + return $maxviewer; + } + + + function qa_viewer_text($content, $format, $options=array()) +/* + Return the plain text rendering of $content in $format, passing $options to the appropriate module +*/ + { + $viewer=qa_load_viewer($content, $format); + return $viewer->get_text($content, $format, $options); + } + + + function qa_viewer_html($content, $format, $options=array()) +/* + Return the HTML rendering of $content in $format, passing $options to the appropriate module +*/ + { + $viewer=qa_load_viewer($content, $format); + return $viewer->get_html($content, $format, $options); + } + + + function qa_get_post_content($editorfield, $contentfield, &$ineditor, &$incontent, &$informat, &$intext) +/* + Retrieve the POST from an editor module's HTML field named $contentfield, where the editor's name was in HTML field $editorfield + Assigns the module's output to $incontent and $informat, editor's name in $ineditor, text rendering of content in $intext +*/ + { + $ineditor=qa_post_text($editorfield); + + $editor=qa_load_module('editor', $ineditor); + $readdata=$editor->read_post($contentfield); + $incontent=$readdata['content']; + $informat=$readdata['format']; + $intext=qa_viewer_text($incontent, $informat); + } + + + function qa_get_avatar_blob_html($blobid, $width, $height, $size, $padding=false) +/* + Return the HTML to display avatar $blobid whose stored size is $width and $height + Constrain the image to $size (width AND height) and pad it to that size if $padding is true +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-util-image.php'; + + if (strlen($blobid) && ($size>0)) { + qa_image_constrain($width, $height, $size); + + $html=''; + + if ($padding) { + $padleft=floor(($size-$width)/2); + $padright=$size-$width-$padleft; + $padtop=floor(($size-$height)/2); + $padbottom=$size-$height-$padtop; + $html=''.$html.''; + } + + return $html; + + } else + return null; + } + + + function qa_get_gravatar_html($email, $size) +/* + Return the HTML to display the Gravatar for $email, constrained to $size +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + if ($size>0) + return ''; + else + return null; + } + + + function qa_get_points_title_html($userpoints, $pointstitle) +/* + Retrieve the appropriate user title from $pointstitle for a user with $userpoints points, or null if none +*/ + { + foreach ($pointstitle as $points => $title) + if ($userpoints>=$points) + return $title; + + return null; + } + + + function qa_notice_form($noticeid, $content, $rawnotice=null) +/* + Return an form to add to the $qa_content['notices'] array for displaying a user notice with id $noticeid + and $content. Pass the raw database information for the notice in $rawnotice. +*/ + { + $elementid='notice_'.$noticeid; + + return array( + 'id' => qa_html($elementid), + 'raw' => $rawnotice, + 'form_tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + 'close_tags' => 'NAME="'.qa_html($elementid).'" onclick="return qa_notice_click(this);"', + 'content' => $content, + ); + } + + + function qa_favorite_form($entitytype, $entityid, $favorite, $title) +/* + Return a form to set in $qa_content['favorite'] for the favoriting button for entity $entitytype with $entityid. + Set $favorite to whether the entity is currently a favorite and a description title for the button in $title. +*/ + { + return array( + 'form_tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + 'favorite_tags' => 'ID="favoriting"', + ($favorite ? 'favorite_remove_tags' : 'favorite_add_tags') => + 'TITLE="'.qa_html($title).'" NAME="'.qa_html('favorite_'.$entitytype.'_'.$entityid.'_'.(int)!$favorite).'" onClick="return qa_favorite_click(this);"', + ); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-limits.php b/qa-include/qa-app-limits.php new file mode 100644 index 000000000..a269e11e1 --- /dev/null +++ b/qa-include/qa-app-limits.php @@ -0,0 +1,196 @@ +=$end1long) && ($iplong<=$end2long)) || (($iplong>=$end2long) && ($iplong<=$end1long)); + } + + } elseif (strlen($blockipclause)) + return preg_match('/^'.str_replace('\\*', '[0-9]+', preg_quote($blockipclause, '/')).'$/', $ip) > 0; + // preg_quote misses hyphens but that is OK here + } + + return false; + } + + + function qa_report_write_action($userid, $cookieid, $action, $questionid, $answerid, $commentid) +/* + Called after a database write $action performed by a user identified by $userid and/or $cookieid. +*/ + {} + + + function qa_limits_increment($userid, $action) +/* + Take note for rate limits that user $userid and/or the requesting IP just performed $action, + where $action is one of the QA_LIMIT_* constants defined above. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-db-limits.php'; + + $period=(int)(qa_opt('db_time')/3600); + + if (isset($userid)) + qa_db_limits_user_add($userid, $action, $period, 1); + + qa_db_limits_ip_add(qa_remote_ip_address(), $action, $period, 1); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-mailing.php b/qa-include/qa-app-mailing.php new file mode 100644 index 000000000..ce3e57b1e --- /dev/null +++ b/qa-include/qa-app-mailing.php @@ -0,0 +1,156 @@ +60) // if it's been a while, we assume there hasn't been continuous mailing... + $lasttime=$thistime-1; // ... so only do 1 second's worth + else // otherwise... + $lasttime=max($lasttime, $thistime-6); // ... don't do more than 6 seconds' worth + + $count=min(floor(($thistime-$lasttime)*$perminute/60), 100); // don't do more than 100 messages at a time + + if ($count>0) { + qa_opt('mailing_last_timestamp', $thistime+30); + // prevents a parallel call to qa_mailing_perform_step() from sending messages, unless we're very unlucky with timing (poor man's mutex) + + $sentusers=0; + $users=qa_db_users_get_mailing_next($lastuserid, $count); + + if (count($users)) { + foreach ($users as $user) + $lastuserid=max($lastuserid, $user['userid']); + + qa_opt('mailing_last_userid', $lastuserid); + qa_opt('mailing_done_users', qa_opt('mailing_done_users')+count($users)); + + foreach ($users as $user) + if (!($user['flags'] & QA_USER_FLAGS_NO_MAILINGS)) { + qa_mailing_send_one($user['userid'], $user['handle'], $user['email'], $user['emailcode']); + $sentusers++; + } + + qa_opt('mailing_last_timestamp', $lasttime+$sentusers*60/$perminute); // can be floating point result, based on number of mails actually sent + + } else + qa_mailing_stop(); + } + } + } + + + function qa_mailing_send_one($userid, $handle, $email, $emailcode) +/* + Send a single message from the mailing, to $userid with $handle and $email. + Pass the user's existing $emailcode if there is one, otherwise a new one will be set up +*/ + { + require_once QA_INCLUDE_DIR.'qa-app-emails.php'; + require_once QA_INCLUDE_DIR.'qa-db-users.php'; + + if (!strlen(trim($emailcode))) { + $emailcode=qa_db_user_rand_emailcode(); + qa_db_user_set($userid, 'emailcode', $emailcode); + } + + $unsubscribeurl=qa_path('unsubscribe', array('c' => $emailcode, 'u' => $handle), qa_opt('site_url')); + + return qa_send_email(array( + 'fromemail' => qa_opt('mailing_from_email'), + 'fromname' => qa_opt('mailing_from_name'), + 'toemail' => $email, + 'toname' => $handle, + 'subject' => qa_opt('mailing_subject'), + 'body' => trim(qa_opt('mailing_body'))."\n\n\n".qa_lang('users/unsubscribe').' '.$unsubscribeurl, + 'html' => false, + )); + } + + + function qa_mailing_progress_message() +/* + Return a message describing current progress in the mailing +*/ + { + if (strlen(qa_opt('mailing_last_userid'))) + return strtr(qa_lang('admin/mailing_progress'), array( + '^1' => number_format(qa_opt('mailing_done_users')), + '^2' => number_format(qa_opt('mailing_total_users')), + )); + else + return null; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-options.php b/qa-include/qa-app-options.php new file mode 100644 index 000000000..6bc1e9f69 --- /dev/null +++ b/qa-include/qa-app-options.php @@ -0,0 +1,712 @@ + [value] of settings for each option in $names. + If any options are missing from the database, set them to their defaults +*/ + { + global $qa_options_cache, $qa_options_loaded; + + // If any options not cached, retrieve them from database via standard pending mechanism + + if (!$qa_options_loaded) + qa_preload_options(); + + if (!$qa_options_loaded) { + require_once QA_INCLUDE_DIR.'qa-db-selects.php'; + + qa_load_options_results(array( + qa_db_get_pending_result('options'), + qa_db_get_pending_result('time'), + )); + } + + // Pull out the options specifically requested here, and assign defaults + + $options=array(); + foreach ($names as $name) { + if (!isset($qa_options_cache[$name])) { + $todatabase=true; + + switch ($name) { // don't write default to database if option was deprecated, or depends on site language (which could be changed) + case 'custom_sidebar': + case 'site_title': + case 'email_privacy': + case 'answer_needs_login': + case 'ask_needs_login': + case 'comment_needs_login': + case 'db_time': + $todatabase=false; + break; + } + + qa_set_option($name, qa_default_option($name), $todatabase); + } + + $options[$name]=$qa_options_cache[$name]; + } + + return $options; + } + + + function qa_opt_if_loaded($name) +/* + Return the value of option $name if it has already been loaded, otherwise return null + (used to prevent a database query if it's not essential for us to know the option value) +*/ + { + global $qa_options_cache; + + return @$qa_options_cache[$name]; + } + + + function qa_options_set_pending($names) +/* + This is deprecated since Q2A 1.3 now that all options are retrieved together. + Function kept for backwards compatibility with modified Q2A code bases. +*/ + {} + + + function qa_preload_options() +/* + Load all of the Q2A options from the database, unless QA_OPTIMIZE_DISTANT_DB is set in qa-config.php, + in which case queue the options for later retrieval +*/ + { + global $qa_options_loaded; + + if (!@$qa_options_loaded) { + $selectspecs=array( + 'options' => array( + 'columns' => array('title', 'content'), + 'source' => '^options', + 'arraykey' => 'title', + 'arrayvalue' => 'content', + ), + + 'time' => array( + 'columns' => array('title' => "'db_time'", 'content' => 'UNIX_TIMESTAMP(NOW())'), + 'arraykey' => 'title', + 'arrayvalue' => 'content', + ), + ); + + if (QA_OPTIMIZE_DISTANT_DB) { + require_once QA_INCLUDE_DIR.'qa-db-selects.php'; + + foreach ($selectspecs as $pendingid => $selectspec) + qa_db_queue_pending_select($pendingid, $selectspec); + + } else + qa_load_options_results(qa_db_multi_select($selectspecs)); + } + } + + + function qa_load_options_results($results) +/* + Load the options from the $results of the database selectspecs defined in qa_preload_options() +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_options_cache, $qa_options_loaded; + + foreach ($results as $result) + foreach ($result as $name => $value) + $qa_options_cache[$name]=$value; + + $qa_options_loaded=true; + } + + + function qa_set_option($name, $value, $todatabase=true) +/* + Set an option $name to $value (application level) in both cache and database, unless + $todatabase=false, in which case set it in the cache only +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_options_cache; + + if ($todatabase && isset($value)) + qa_db_set_option($name, $value); + + $qa_options_cache[$name]=$value; + } + + + function qa_reset_options($names) +/* + Reset the options in $names to their defaults +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + foreach ($names as $name) + qa_set_option($name, qa_default_option($name)); + } + + + function qa_default_option($name) +/* + Return the default value for option $name +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $fixed_defaults=array( + 'allow_change_usernames' => 1, + 'allow_close_questions' => 1, + 'allow_multi_answers' => 1, + 'allow_private_messages' => 1, + 'allow_self_answer' => 1, + 'allow_view_q_bots' => 1, + 'avatar_allow_gravatar' => 1, + 'avatar_allow_upload' => 1, + 'avatar_profile_size' => 200, + 'avatar_q_list_size' => 0, + 'avatar_q_page_a_size' => 40, + 'avatar_q_page_c_size' => 20, + 'avatar_q_page_q_size' => 50, + 'avatar_store_size' => 400, + 'avatar_users_size' => 30, + 'captcha_on_anon_post' => 1, + 'captcha_on_feedback' => 1, + 'captcha_on_register' => 1, + 'captcha_on_reset_password' => 1, + 'captcha_on_unconfirmed' => 0, + 'columns_tags' => 3, + 'columns_users' => 2, + 'comment_on_as' => 1, + 'comment_on_qs' => 0, + 'confirm_user_emails' => 1, + 'do_ask_check_qs' => 0, + 'do_complete_tags' => 1, + 'do_count_q_views' => 1, + 'do_example_tags' => 1, + 'feed_for_activity' => 1, + 'feed_for_qa' => 1, + 'feed_for_questions' => 1, + 'feed_for_unanswered' => 1, + 'feed_full_text' => 1, + 'feed_number_items' => 50, + 'feed_per_category' => 1, + 'feedback_enabled' => 1, + 'flagging_hide_after' => 5, + 'flagging_notify_every' => 2, + 'flagging_notify_first' => 1, + 'flagging_of_posts' => 1, + 'follow_on_as' => 1, + 'hot_weight_a_age' => 100, + 'hot_weight_answers' => 100, + 'hot_weight_q_age' => 100, + 'hot_weight_views' => 100, + 'hot_weight_votes' => 100, + 'mailing_per_minute' => 500, + 'match_ask_check_qs' => 3, + 'match_example_tags' => 3, + 'match_related_qs' => 3, + 'max_copy_user_updates' => 10, + 'max_len_q_title' => 120, + 'max_num_q_tags' => 5, + 'max_rate_ip_as' => 50, + 'max_rate_ip_cs' => 40, + 'max_rate_ip_flags' => 10, + 'max_rate_ip_logins' => 20, + 'max_rate_ip_messages' => 10, + 'max_rate_ip_qs' => 20, + 'max_rate_ip_registers' => 5, + 'max_rate_ip_uploads' => 20, + 'max_rate_ip_votes' => 600, + 'max_rate_user_as' => 25, + 'max_rate_user_cs' => 20, + 'max_rate_user_flags' => 5, + 'max_rate_user_messages' => 5, + 'max_rate_user_qs' => 10, + 'max_rate_user_uploads' => 10, + 'max_rate_user_votes' => 300, + 'max_store_user_updates' => 50, + 'min_len_a_content' => 12, + 'min_len_c_content' => 12, + 'min_len_q_content' => 0, + 'min_len_q_title' => 12, + 'min_num_q_tags' => 0, + 'moderate_notify_admin' => 1, + 'moderate_points_limit' => 150, + 'nav_ask' => 1, + 'nav_qa_not_home' => 1, + 'nav_questions' => 1, + 'nav_tags' => 1, + 'nav_unanswered' => 1, + 'nav_users' => 1, + 'neat_urls' => QA_URL_FORMAT_SAFEST, + 'notify_users_default' => 1, + 'page_size_activity' => 20, + 'page_size_ask_check_qs' => 5, + 'page_size_ask_tags' => 5, + 'page_size_home' => 20, + 'page_size_hot_qs' => 20, + 'page_size_q_as' => 10, + 'page_size_qs' => 20, + 'page_size_related_qs' => 5, + 'page_size_search' => 10, + 'page_size_tag_qs' => 20, + 'page_size_tags' => 30, + 'page_size_una_qs' => 20, + 'page_size_user_posts' => 20, + 'page_size_users' => 20, + 'pages_prev_next' => 3, + 'permit_anon_view_ips' => QA_PERMIT_EDITORS, + 'permit_close_q' => QA_PERMIT_EDITORS, + 'permit_delete_hidden' => QA_PERMIT_MODERATORS, + 'permit_edit_a' => QA_PERMIT_EXPERTS, + 'permit_edit_c' => QA_PERMIT_EDITORS, + 'permit_edit_q' => QA_PERMIT_EDITORS, + 'permit_flag' => QA_PERMIT_CONFIRMED, + 'permit_hide_show' => QA_PERMIT_EDITORS, + 'permit_moderate' => QA_PERMIT_EXPERTS, + 'permit_select_a' => QA_PERMIT_EXPERTS, + 'permit_view_q_page' => QA_PERMIT_ALL, + 'permit_vote_a' => QA_PERMIT_USERS, + 'permit_vote_down' => QA_PERMIT_USERS, + 'permit_vote_q' => QA_PERMIT_USERS, + 'points_a_selected' => 30, + 'points_a_voted_max_gain' => 20, + 'points_a_voted_max_loss' => 5, + 'points_base' => 100, + 'points_multiple' => 10, + 'points_post_a' => 4, + 'points_post_q' => 2, + 'points_q_voted_max_gain' => 10, + 'points_q_voted_max_loss' => 3, + 'points_select_a' => 3, + 'q_urls_title_length' => 50, + 'show_a_c_links' => 1, + 'show_a_form_immediate' => 'if_no_as', + 'show_c_reply_buttons' => 1, + 'show_custom_welcome' => 1, + 'show_fewer_cs_count' => 5, + 'show_fewer_cs_from' => 10, + 'show_full_date_days' => 7, + 'show_message_history' => 1, + 'show_selected_first' => 1, + 'show_url_links' => 1, + 'show_user_points' => 1, + 'show_user_titles' => 1, + 'show_when_created' => 1, + 'site_theme' => 'Default', + 'smtp_port' => 25, + 'sort_answers_by' => 'created', + 'tags_or_categories' => 'tc', + 'voting_on_as' => 1, + 'voting_on_qs' => 1, + ); + + if (isset($fixed_defaults[$name])) + $value=$fixed_defaults[$name]; + + else + switch ($name) { + case 'site_url': + $value='http://'.@$_SERVER['HTTP_HOST'].strtr(dirname($_SERVER['SCRIPT_NAME']), '\\', '/').'/'; + break; + + case 'site_title': + $value=qa_default_site_title(); + break; + + case 'site_theme_mobile': + $value=qa_opt('site_theme'); + break; + + case 'from_email': // heuristic to remove short prefix (e.g. www. or qa.) + $parts=explode('.', @$_SERVER['HTTP_HOST']); + + if ( (count($parts)>2) && (strlen($parts[0])<5) && !is_numeric($parts[0]) ) + unset($parts[0]); + + $value='no-reply@'.((count($parts)>1) ? implode('.', $parts) : 'example.com'); + break; + + case 'email_privacy': + $value=qa_lang_html('options/default_privacy'); + break; + + case 'show_custom_sidebar': + $value=strlen(qa_opt('custom_sidebar')) ? true : false; + break; + + case 'show_custom_header': + $value=strlen(qa_opt('custom_header')) ? true : false; + break; + + case 'show_custom_footer': + $value=strlen(qa_opt('custom_footer')) ? true : false; + break; + + case 'show_custom_in_head': + $value=strlen(qa_opt('custom_in_head')) ? true : false; + break; + + case 'custom_sidebar': + $value=qa_lang_sub('options/default_sidebar', qa_html(qa_opt('site_title'))); + break; + + case 'editor_for_qs': + case 'editor_for_as': + require_once QA_INCLUDE_DIR.'qa-app-format.php'; + + $value='-'; // to match none by default, i.e. choose based on who is best at editing HTML + qa_load_editor('', 'html', $value); + break; + + case 'permit_post_q': // convert from deprecated option if available + $value=qa_opt('ask_needs_login') ? QA_PERMIT_USERS : QA_PERMIT_ALL; + break; + + case 'permit_post_a': // convert from deprecated option if available + $value=qa_opt('answer_needs_login') ? QA_PERMIT_USERS : QA_PERMIT_ALL; + break; + + case 'permit_post_c': // convert from deprecated option if available + $value=qa_opt('comment_needs_login') ? QA_PERMIT_USERS : QA_PERMIT_ALL; + break; + + case 'permit_retag_cat': // convert from previous option that used to contain it too + $value=qa_opt('permit_edit_q'); + break; + + case 'points_vote_up_q': + case 'points_vote_down_q': + $oldvalue=qa_opt('points_vote_on_q'); + $value=is_numeric($oldvalue) ? $oldvalue : 1; + break; + + case 'points_vote_up_a': + case 'points_vote_down_a': + $oldvalue=qa_opt('points_vote_on_a'); + $value=is_numeric($oldvalue) ? $oldvalue : 1; + break; + + case 'points_per_q_voted_up': + case 'points_per_q_voted_down': + $oldvalue=qa_opt('points_per_q_voted'); + $value=is_numeric($oldvalue) ? $oldvalue : 1; + break; + + case 'points_per_a_voted_up': + case 'points_per_a_voted_down': + $oldvalue=qa_opt('points_per_a_voted'); + $value=is_numeric($oldvalue) ? $oldvalue : 2; + break; + + case 'captcha_module': + $captchamodules=qa_list_modules('captcha'); + if (count($captchamodules)) + $value=reset($captchamodules); + break; + + case 'mailing_from_name': + $value=qa_opt('site_title'); + break; + + case 'mailing_from_email': + $value=qa_opt('from_email'); + break; + + case 'mailing_subject': + $value=qa_lang_sub('options/default_subject', qa_opt('site_title')); + break; + + case 'mailing_body': + $value="\n\n\n--\n".qa_opt('site_title')."\n".qa_opt('site_url'); + break; + + default: // call option_default method in any registered modules + $moduletypes=qa_list_module_types(); + + foreach ($moduletypes as $moduletype) { + $modules=qa_load_modules_with($moduletype, 'option_default'); + + foreach ($modules as $module) { + $value=$module->option_default($name); + if (strlen($value)) + return $value; + } + } + + $value=''; + break; + } + + return $value; + } + + + function qa_default_site_title() +/* + Return a heuristic guess at the name of the site from the HTTP HOST +*/ + { + $parts=explode('.', @$_SERVER['HTTP_HOST']); + + $longestpart=''; + foreach ($parts as $part) + if (strlen($part)>strlen($longestpart)) + $longestpart=$part; + + return ((strlen($longestpart)>3) ? (ucfirst($longestpart).' ') : '').qa_lang('options/default_suffix'); + } + + + function qa_post_html_defaults($basetype, $full=false) +/* + Return an array of defaults for the $options parameter passed to qa_post_html_fields() and its ilk +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-app-users.php'; + + return array( + 'tagsview' => ($basetype=='Q') && qa_using_tags(), + 'categoryview' => ($basetype=='Q') && qa_using_categories(), + 'contentview' => $full, + 'voteview' => qa_get_vote_view($basetype, $full), + 'flagsview' => qa_opt('flagging_of_posts') && $full, + 'answersview' => $basetype=='Q', + 'viewsview' => ($basetype=='Q') && qa_opt('do_count_q_views') && qa_opt('show_view_counts'), + 'whatview' => true, + 'whatlink' => qa_opt('show_a_c_links'), + 'whenview' => qa_opt('show_when_created'), + 'ipview' => !qa_user_permit_error('permit_anon_view_ips'), + 'whoview' => true, + 'avatarsize' => qa_opt('avatar_q_list_size'), + 'pointsview' => qa_opt('show_user_points'), + 'pointstitle' => qa_opt('show_user_titles') ? qa_get_points_to_titles() : array(), + 'updateview' => true, + 'blockwordspreg' => qa_get_block_words_preg(), + 'showurllinks' => qa_opt('show_url_links'), + 'linksnewwindow' => qa_opt('links_in_new_window'), + 'microformats' => $full, + 'fulldatedays' => qa_opt('show_full_date_days'), + ); + } + + + function qa_get_vote_view($basetype, $full=false, $enabledif=true) +/* + Return $voteview parameter to pass to qa_post_html_fields() in qa-app-format.php for posts of $basetype (Q/A/C), + with buttons enabled if appropriate (based on whether $full post shown) unless $enabledif is false. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $disabledsuffix=''; + + if ($basetype=='Q') { + $view=qa_opt('voting_on_qs'); + + if (qa_user_permit_error('permit_vote_q')=='level') + $disabledsuffix='-disabled-level'; + elseif (!($enabledif && ($full || !qa_opt('voting_on_q_page_only')))) + $disabledsuffix='-disabled-page'; + elseif (qa_user_permit_error('permit_vote_down')=='level') + $disabledsuffix='-uponly-level'; + + } elseif ($basetype=='A') { + $view=qa_opt('voting_on_as'); + + if (qa_user_permit_error('permit_vote_a')=='level') + $disabledsuffix='-disabled-level'; + elseif (!$enabledif) + $disabledsuffix='-disabled-page'; + elseif (qa_user_permit_error('permit_vote_down')=='level') + $disabledsuffix='-uponly-level'; + + } else + $view=false; + + return $view ? ( (qa_opt('votes_separated') ? 'updown' : 'net').$disabledsuffix ) : false; + } + + + function qa_has_custom_home() +/* + Returns true if the home page has been customized, either due to admin setting, or $QA_CONST_PATH_MAP +*/ + { + return qa_opt('show_custom_home') || (array_search('', qa_get_request_map())!==false); + } + + + function qa_using_tags() +/* + Return whether the option is set to classify questions by tags +*/ + { + return strpos(qa_opt('tags_or_categories'), 't')!==false; + } + + + function qa_using_categories() +/* + Return whether the option is set to classify questions by categories +*/ + { + return strpos(qa_opt('tags_or_categories'), 'c')!==false; + } + + + function qa_get_block_words_preg() +/* + Return the regular expression fragment to match the blocked words options set in the database +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_blockwordspreg, $qa_blockwordspreg_set; + + if (!@$qa_blockwordspreg_set) { + $blockwordstring=qa_opt('block_bad_words'); + + if (strlen($blockwordstring)) { + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + $qa_blockwordspreg=qa_block_words_to_preg($blockwordstring); + + } else + $qa_blockwordspreg=null; + + $qa_blockwordspreg_set=true; + } + + return $qa_blockwordspreg; + } + + + function qa_get_points_to_titles() +/* + Return an array of [points] => [user title] from the 'points_to_titles' option, to pass to qa_get_points_title_html() +*/ + { + global $qa_points_title_cache; + + if (!is_array($qa_points_title_cache)) { + $qa_points_title_cache=array(); + + $pairs=explode(',', qa_opt('points_to_titles')); + foreach ($pairs as $pair) { + $spacepos=strpos($pair, ' '); + if (is_numeric($spacepos)) { + $points=trim(substr($pair, 0, $spacepos)); + $title=trim(substr($pair, $spacepos)); + + if (is_numeric($points) && strlen($title)) + $qa_points_title_cache[(int)$points]=$title; + } + } + + krsort($qa_points_title_cache, SORT_NUMERIC); + } + + return $qa_points_title_cache; + } + + + function qa_get_permit_options() +/* + Return an array of relevant permissions settings, based on other options +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $permits=array('permit_view_q_page', 'permit_post_q', 'permit_post_a'); + + if (qa_opt('comment_on_qs') || qa_opt('comment_on_as')) + $permits[]='permit_post_c'; + + if (qa_opt('voting_on_qs')) + $permits[]='permit_vote_q'; + + if (qa_opt('voting_on_as')) + $permits[]='permit_vote_a'; + + if (qa_opt('voting_on_qs') || qa_opt('voting_on_as')) + $permits[]='permit_vote_down'; + + if (qa_using_tags() || qa_using_categories()) + $permits[]='permit_retag_cat'; + + array_push($permits, 'permit_edit_q', 'permit_edit_a'); + + if (qa_opt('comment_on_qs') || qa_opt('comment_on_as')) + $permits[]='permit_edit_c'; + + if (qa_opt('allow_close_questions')) + $permits[]='permit_close_q'; + + array_push($permits, 'permit_select_a', 'permit_anon_view_ips'); + + if (qa_opt('flagging_of_posts')) + $permits[]='permit_flag'; + + $permits[]='permit_moderate'; + + array_push($permits, 'permit_hide_show', 'permit_delete_hidden'); + + return $permits; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-post-create.php b/qa-include/qa-app-post-create.php new file mode 100644 index 000000000..a816e4f75 --- /dev/null +++ b/qa-include/qa-app-post-create.php @@ -0,0 +1,242 @@ + $postid, + 'parentid' => @$followanswer['postid'], + 'parent' => $followanswer, + 'title' => $title, + 'content' => $content, + 'format' => $format, + 'text' => $text, + 'tags' => $tagstring, + 'categoryid' => $categoryid, + 'extra' => $extravalue, + 'notify' => $notify, + 'email' => $email, + )); + + return $postid; + } + + + function qa_array_filter_by_keys($inarray, $keys) +/* + Return an array containing the elements of $inarray whose key is in $keys +*/ + { + $outarray=array(); + + foreach ($keys as $key) + if (isset($inarray[$key])) + $outarray[$key]=$inarray[$key]; + + return $outarray; + } + + + function qa_suspend_post_indexing($suspend=true) +/* + Suspend the indexing (and unindexing) of posts via qa_post_index(...) and qa_post_unindex(...) + if $suspend is true, otherwise reinstate it. A counter is kept to allow multiple calls. +*/ + { + global $qa_post_indexing_suspended; + + $qa_post_indexing_suspended+=($suspend ? 1 : -1); + } + + + function qa_post_index($postid, $type, $questionid, $parentid, $title, $content, $format, $text, $tagstring) +/* + Add post $postid (which comes under $questionid) of $type (Q/A/C) to the database index, with $title, $text + and $tagstring. Calls through to all installed search modules. +*/ + { + global $qa_post_indexing_suspended; + + if ($qa_post_indexing_suspended>0) + return; + + // Send through to any search modules for indexing + + $searches=qa_load_modules_with('search', 'index_post'); + foreach ($searches as $search) + $search->index_post($postid, $type, $questionid, $parentid, $title, $content, $format, $text, $tagstring); + } + + + function qa_answer_create($userid, $handle, $cookieid, $content, $format, $text, $notify, $email, $question, $queued=false) +/* + Add an answer (application level) - create record, update appropriate counts, index it, send notifications. + $question should contain database record for the question this is an answer to. + See qa-app-posts.php for a higher-level function which is easier to use. +*/ + { + $postid=qa_db_post_create($queued ? 'A_QUEUED' : 'A', $question['postid'], $userid, isset($userid) ? null : $cookieid, + qa_remote_ip_address(), null, $content, $format, null, qa_combine_notify_email($userid, $notify, $email), $question['categoryid']); + + qa_db_posts_calc_category_path($postid); + + if (!$queued) { + if ($question['type']=='Q') // don't index answer if parent question is hidden or queued + qa_post_index($postid, 'A', $question['postid'], $question['postid'], null, $content, $format, $text, null); + + qa_db_post_acount_update($question['postid']); + qa_db_hotness_update($question['postid']); + qa_db_points_update_ifuser($userid, 'aposts'); + qa_db_acount_update(); + qa_db_unaqcount_update(); + } + + qa_report_event($queued ? 'a_queue' : 'a_post', $userid, $handle, $cookieid, array( + 'postid' => $postid, + 'parentid' => $question['postid'], + 'parent' => $question, + 'content' => $content, + 'format' => $format, + 'text' => $text, + 'categoryid' => $question['categoryid'], + 'notify' => $notify, + 'email' => $email, + )); + + return $postid; + } + + + function qa_comment_create($userid, $handle, $cookieid, $content, $format, $text, $notify, $email, $question, $parent, $commentsfollows, $queued=false) +/* + Add a comment (application level) - create record, update appropriate counts, index it, send notifications. + $question should contain database record for the question this is part of (as direct or comment on Q's answer). + If this is a comment on an answer, $answer should contain database record for the answer, otherwise null. + $commentsfollows should contain database records for all previous comments on the same question or answer, + but it can also contain other records that are ignored. + See qa-app-posts.php for a higher-level function which is easier to use. +*/ + { + require_once QA_INCLUDE_DIR.'qa-app-emails.php'; + require_once QA_INCLUDE_DIR.'qa-app-options.php'; + require_once QA_INCLUDE_DIR.'qa-app-format.php'; + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + if (!isset($parent)) + $parent=$question; // for backwards compatibility with old answer parameter + + $postid=qa_db_post_create($queued ? 'C_QUEUED' : 'C', $parent['postid'], $userid, isset($userid) ? null : $cookieid, + qa_remote_ip_address(), null, $content, $format, null, qa_combine_notify_email($userid, $notify, $email), $question['categoryid']); + + qa_db_posts_calc_category_path($postid); + + if (!$queued) { + if ( ($question['type']=='Q') && (($parent['type']=='Q') || ($parent['type']=='A')) ) // only index if antecedents fully visible + qa_post_index($postid, 'C', $question['postid'], $parent['postid'], null, $content, $format, $text, null); + + qa_db_points_update_ifuser($userid, 'cposts'); + qa_db_ccount_update(); + } + + $thread=array(); + + foreach ($commentsfollows as $comment) + if (($comment['type']=='C') && ($comment['parentid']==$parent['postid'])) // find just those for this parent, fully visible + $thread[]=$comment; + + qa_report_event($queued ? 'c_queue' : 'c_post', $userid, $handle, $cookieid, array( + 'postid' => $postid, + 'parentid' => $parent['postid'], + 'parenttype' => $parent['basetype'], + 'parent' => $parent, + 'questionid' => $question['postid'], + 'question' => $question, + 'thread' => $thread, + 'content' => $content, + 'format' => $format, + 'text' => $text, + 'categoryid' => $question['categoryid'], + 'notify' => $notify, + 'email' => $email, + )); + + return $postid; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-post-update.php b/qa-include/qa-app-post-update.php new file mode 100644 index 000000000..aa0f70f7d --- /dev/null +++ b/qa-include/qa-app-post-update.php @@ -0,0 +1,774 @@ + $oldquestion['postid'], + 'title' => $title, + 'content' => $content, + 'format' => $format, + 'text' => $text, + 'tags' => $tagstring, + 'extra' => $extravalue, + 'oldquestion' => $oldquestion, + 'oldtitle' => $oldquestion['title'], + 'oldcontent' => $oldquestion['content'], + 'oldformat' => $oldquestion['format'], + 'oldtags' => $oldquestion['tags'], + 'titlechanged' => $titlechanged, + 'contentchanged' => $contentchanged, + 'tagschanged' => $tagschanged, + )); + } + + + function qa_question_set_selchildid($userid, $handle, $cookieid, $oldquestion, $selchildid, $answers) +/* + Set the selected answer (application level) of $oldquestion to $selchildid. Pass details of the user doing this + in $userid, $handle and $cookieid, and the database records for all answers to the question in $answers. + Handles user points values and notifications. + See qa-app-posts.php for a higher-level function which is easier to use. +*/ + { + $oldselchildid=$oldquestion['selchildid']; + + qa_db_post_set_selchildid($oldquestion['postid'], isset($selchildid) ? $selchildid : null, $userid, qa_remote_ip_address()); + qa_db_points_update_ifuser($oldquestion['userid'], 'aselects'); + qa_db_unselqcount_update(); + + if (isset($oldselchildid) && isset($answers[$oldselchildid])) { + qa_db_points_update_ifuser($answers[$oldselchildid]['userid'], 'aselecteds'); + + qa_report_event('a_unselect', $userid, $handle, $cookieid, array( + 'parentid' => $oldquestion['postid'], + 'parent' => $oldquestion, + 'postid' => $oldselchildid, + 'answer' => $answers[$oldselchildid], + )); + } + + if (isset($selchildid)) { + qa_db_points_update_ifuser($answers[$selchildid]['userid'], 'aselecteds'); + + qa_report_event('a_select', $userid, $handle, $cookieid, array( + 'parentid' => $oldquestion['postid'], + 'parent' => $oldquestion, + 'postid' => $selchildid, + 'answer' => $answers[$selchildid], + )); + } + } + + + function qa_question_close_clear($oldquestion, $oldclosepost, $userid, $handle, $cookieid) +/* + Reopen $oldquestion if it was closed. Pass details of the user doing this in $userid, $handle and $cookieid, and the + $oldclosepost (to match $oldquestion['closedbyid']) if any. + See qa-app-posts.php for a higher-level function which is easier to use. +*/ + { + if (isset($oldquestion['closedbyid'])) { + qa_db_post_set_closed($oldquestion['postid'], null, $userid, qa_remote_ip_address()); + + if (isset($oldclosepost) && ($oldclosepost['parentid']==$oldquestion['postid'])) { + qa_post_unindex($oldclosepost['postid']); + qa_db_post_delete($oldclosepost['postid']); + } + + qa_report_event('q_reopen', $userid, $handle, $cookieid, array( + 'postid' => $oldquestion['postid'], + 'oldquestion' => $oldquestion, + )); + } + } + + + function qa_question_close_duplicate($oldquestion, $oldclosepost, $originalpostid, $userid, $handle, $cookieid) +/* + Close $oldquestion as a duplicate of the question with id $originalpostid. Pass details of the user doing this in + $userid, $handle and $cookieid, and the $oldclosepost (to match $oldquestion['closedbyid']) if any. See + qa-app-posts.php for a higher-level function which is easier to use. +*/ + { + qa_question_close_clear($oldquestion, $oldclosepost, $userid, $handle, $cookieid); + + qa_db_post_set_closed($oldquestion['postid'], $originalpostid, $userid, qa_remote_ip_address()); + + qa_report_event('q_close', $userid, $handle, $cookieid, array( + 'postid' => $oldquestion['postid'], + 'oldquestion' => $oldquestion, + 'reason' => 'duplicate', + 'originalid' => $originalpostid, + )); + } + + + function qa_question_close_other($oldquestion, $oldclosepost, $note, $userid, $handle, $cookieid) +/* + Close $oldquestion with the reason given in $note. Pass details of the user doing this in $userid, $handle and + $cookieid, and the $oldclosepost (to match $oldquestion['closedbyid']) if any. + See qa-app-posts.php for a higher-level function which is easier to use. +*/ + { + qa_question_close_clear($oldquestion, $oldclosepost, $userid, $handle, $cookieid); + + $postid=qa_db_post_create('NOTE', $oldquestion['postid'], $userid, isset($userid) ? null : $cookieid, + qa_remote_ip_address(), null, $note, '', null, null, $oldquestion['categoryid']); + + qa_db_posts_calc_category_path($postid); + + if ($oldquestion['type']=='Q') + qa_post_index($postid, 'NOTE', $oldquestion['postid'], $oldquestion['postid'], null, $note, '', $note, null); + + qa_db_post_set_closed($oldquestion['postid'], $postid, $userid, qa_remote_ip_address()); + + qa_report_event('q_close', $userid, $handle, $cookieid, array( + 'postid' => $oldquestion['postid'], + 'oldquestion' => $oldquestion, + 'reason' => 'other', + 'note' => $note, + )); + } + + + function qa_question_set_hidden($oldquestion, $hidden, $userid, $handle, $cookieid, $answers, $commentsfollows, $closepost=null) +/* + Set the hidden status (application level) of $oldquestion to $hidden. Pass details of the user doing this in + $userid, $handle and $cookieid, the database records for all answers to the question in $answers, the database + records for all comments on the question or the question's answers in $commentsfollows ($commentsfollows can also + contain records for follow-on questions which are ignored), and $closepost to match $oldquestion['closedbyid'] (if any). + Handles indexing, user points, cached counts and event reports. + See qa-app-posts.php for a higher-level function which is easier to use. +*/ + { + require_once QA_INCLUDE_DIR.'qa-app-format.php'; + require_once QA_INCLUDE_DIR.'qa-app-updates.php'; + + $wasqueued=($oldquestion['type']=='Q_QUEUED'); + + qa_post_unindex($oldquestion['postid']); + + foreach ($answers as $answer) + qa_post_unindex($answer['postid']); + + foreach ($commentsfollows as $comment) + if ($comment['basetype']=='C') + qa_post_unindex($comment['postid']); + + if (@$closepost['parentid']==$oldquestion['postid']) + qa_post_unindex($closepost['postid']); + + $setupdated=$hidden || !$wasqueued; // don't record approval of a post as an update action + + qa_db_post_set_type($oldquestion['postid'], $hidden ? 'Q_HIDDEN' : 'Q', + $setupdated ? $userid : null, $setupdated ? qa_remote_ip_address() : null, QA_UPDATE_VISIBLE); + + qa_db_category_path_qcount_update(qa_db_post_get_category_path($oldquestion['postid'])); + qa_db_points_update_ifuser($oldquestion['userid'], array('qposts', 'aselects')); + qa_db_qcount_update(); + qa_db_unaqcount_update(); + qa_db_unselqcount_update(); + qa_db_unupaqcount_update(); + + if (!$hidden) { + qa_post_index($oldquestion['postid'], 'Q', $oldquestion['postid'], $oldquestion['parentid'], $oldquestion['title'], + $oldquestion['content'], $oldquestion['format'], qa_viewer_text($oldquestion['content'], $oldquestion['format']), $oldquestion['tags']); + + foreach ($answers as $answer) + if ($answer['type']=='A') // even if question visible, don't index hidden or queued answers + qa_post_index($answer['postid'], $answer['type'], $oldquestion['postid'], $answer['parentid'], null, + $answer['content'], $answer['format'], qa_viewer_text($answer['content'], $answer['format']), null); + + foreach ($commentsfollows as $comment) + if ($comment['type']=='C') { + $answer=@$answers[$comment['parentid']]; + + if ( (!isset($answer)) || ($answer['type']=='A') ) // don't index comment if it or its parent is hidden + qa_post_index($comment['postid'], $comment['type'], $oldquestion['postid'], $comment['parentid'], null, + $comment['content'], $comment['format'], qa_viewer_text($comment['content'], $comment['format']), null); + } + + if ($closepost['parentid']==$oldquestion['postid']) + qa_post_index($closepost['postid'], $closepost['type'], $oldquestion['postid'], $closepost['parentid'], null, + $closepost['content'], $closepost['format'], qa_viewer_text($closepost['content'], $closepost['format']), null); + } + + qa_report_event($wasqueued ? ($hidden ? 'q_reject' : 'q_approve') : ($hidden ? 'q_hide' : 'q_reshow'), $userid, $handle, $cookieid, array( + 'postid' => $oldquestion['postid'], + 'oldquestion' => $oldquestion, + )); + + if ($wasqueued && !$hidden) { + require_once QA_INCLUDE_DIR.'qa-db-selects.php'; + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + qa_report_event('q_post', $oldquestion['userid'], $oldquestion['handle'], $oldquestion['cookieid'], array( + 'postid' => $oldquestion['postid'], + 'parentid' => $oldquestion['parentid'], + 'parent' => isset($oldquestion['parentid']) ? qa_db_single_select(qa_db_full_post_selectspec(null, $oldquestion['parentid'])) : null, + 'title' => $oldquestion['title'], + 'content' => $oldquestion['content'], + 'format' => $oldquestion['format'], + 'text' => qa_viewer_text($oldquestion['content'], $oldquestion['format']), + 'tags' => $oldquestion['tags'], + 'categoryid' => $oldquestion['categoryid'], + 'notify' => isset($oldquestion['notify']), + 'email' => qa_email_validate($oldquestion['notify']) ? $oldquestion['notify'] : null, + )); + } + } + + + function qa_question_set_category($oldquestion, $categoryid, $userid, $handle, $cookieid, $answers, $commentsfollows, $closepost=null) +/* + Sets the category (application level) of $oldquestion to $categoryid. Pass details of the user doing this in + $userid, $handle and $cookieid, the database records for all answers to the question in $answers, the database + records for all comments on the question or the question's answers in $commentsfollows ($commentsfollows can also + contain records for follow-on questions which are ignored), and $closepost to match $oldquestion['closedbyid'] (if any). + Handles cached counts and event reports and will reset category IDs and paths for all answers and comments. + See qa-app-posts.php for a higher-level function which is easier to use. +*/ + { + $oldpath=qa_db_post_get_category_path($oldquestion['postid']); + + qa_db_post_set_category($oldquestion['postid'], $categoryid, $userid, qa_remote_ip_address()); + qa_db_posts_calc_category_path($oldquestion['postid']); + + $newpath=qa_db_post_get_category_path($oldquestion['postid']); + + qa_db_category_path_qcount_update($oldpath); + qa_db_category_path_qcount_update($newpath); + + $otherpostids=array(); + foreach ($answers as $answer) + $otherpostids[]=$answer['postid']; + + foreach ($commentsfollows as $comment) + if ($comment['basetype']=='C') + $otherpostids[]=$comment['postid']; + + if (@$closepost['parentid']==$oldquestion['postid']) + $otherpostids[]=$closepost['postid']; + + qa_db_posts_set_category_path($otherpostids, $newpath); + + qa_report_event('q_move', $userid, $handle, $cookieid, array( + 'postid' => $oldquestion['postid'], + 'oldquestion' => $oldquestion, + 'categoryid' => $categoryid, + 'oldcategoryid' => $oldquestion['categoryid'], + )); + } + + + function qa_question_delete($oldquestion, $userid, $handle, $cookieid, $oldclosepost=null) +/* + Permanently delete a question (application level) from the database. The question must not have any answers or + comments on it. Pass details of the user doing this in $userid, $handle and $cookieid, and $closepost to match + $oldquestion['closedbyid'] (if any). Handles unindexing, votes, points, cached counts and event reports. + See qa-app-posts.php for a higher-level function which is easier to use. +*/ + { + require_once QA_INCLUDE_DIR.'qa-db-votes.php'; + + if ($oldquestion['type']!='Q_HIDDEN') + qa_fatal_error('Tried to delete a non-hidden question'); + + if (isset($oldclosepost) && ($oldclosepost['parentid']==$oldquestion['postid'])) { + qa_db_post_set_closed($oldquestion['postid'], null); // for foreign key constraint + qa_post_unindex($oldclosepost['postid']); + qa_db_post_delete($oldclosepost['postid']); + } + + $useridvotes=qa_db_uservote_post_get($oldquestion['postid']); + $oldpath=qa_db_post_get_category_path($oldquestion['postid']); + + qa_post_unindex($oldquestion['postid']); + qa_db_post_delete($oldquestion['postid']); // also deletes any related voteds due to cascading + + qa_db_category_path_qcount_update($oldpath); + qa_db_points_update_ifuser($oldquestion['userid'], array('qposts', 'aselects', 'qvoteds', 'upvoteds', 'downvoteds')); + + foreach ($useridvotes as $voteruserid => $vote) + qa_db_points_update_ifuser($voteruserid, ($vote>0) ? 'qupvotes' : 'qdownvotes'); + // could do this in one query like in qa_db_users_recalc_points() but this will do for now - unlikely to be many votes + + qa_db_qcount_update(); + qa_db_unaqcount_update(); + qa_db_unselqcount_update(); + qa_db_unupaqcount_update(); + + qa_report_event('q_delete', $userid, $handle, $cookieid, array( + 'postid' => $oldquestion['postid'], + 'oldquestion' => $oldquestion, + )); + } + + + function qa_question_set_userid($oldquestion, $userid, $handle, $cookieid) +/* + Set the author (application level) of $oldquestion to $userid and also pass $handle and $cookieid + of user. Updates points and reports events as appropriate. +*/ + { + qa_db_post_set_userid($oldquestion['postid'], $userid); + + qa_db_points_update_ifuser($oldquestion['userid'], array('qposts', 'aselects', 'qvoteds', 'upvoteds', 'downvoteds')); + qa_db_points_update_ifuser($userid, array('qposts', 'aselects', 'qvoteds', 'upvoteds', 'downvoteds')); + + qa_report_event('q_claim', $userid, $handle, $cookieid, array( + 'postid' => $oldquestion['postid'], + 'oldquestion' => $oldquestion, + )); + } + + + function qa_post_unindex($postid) +/* + Remove post $postid from our index and update appropriate word counts. Calls through to all search modules. +*/ + { + global $qa_post_indexing_suspended; + + if ($qa_post_indexing_suspended>0) + return; + + // Send through to any search modules for unindexing + + $searches=qa_load_modules_with('search', 'unindex_post'); + foreach ($searches as $search) + $search->unindex_post($postid); + } + + + function qa_answer_set_content($oldanswer, $content, $format, $text, $notify, $userid, $handle, $cookieid, $question) +/* + Change the fields of an answer (application level) to $content, $format and $notify, and reindex based on $text. + Pass the answer's database record before changes in $oldanswer, the question's in $question, and details of the + user doing this in $userid, $handle and $cookieid. Handle indexing and event reports as appropriate. + See qa-app-posts.php for a higher-level function which is easier to use. +*/ + { + qa_post_unindex($oldanswer['postid']); + + $contentchanged=strcmp($oldanswer['content'], $content) || strcmp($oldanswer['format'], $format); + + qa_db_post_set_content($oldanswer['postid'], $oldanswer['title'], $content, $format, $oldanswer['tags'], $notify, + $contentchanged ? $userid : null, $contentchanged ? qa_remote_ip_address() : null); + + if ( ($oldanswer['type']=='A') && ($question['type']=='Q') ) // don't index if question or answer are hidden/queued + qa_post_index($oldanswer['postid'], 'A', $question['postid'], $oldanswer['parentid'], null, $content, $format, $text, null); + + qa_report_event('a_edit', $userid, $handle, $cookieid, array( + 'postid' => $oldanswer['postid'], + 'parentid' => $oldanswer['parentid'], + 'content' => $content, + 'format' => $format, + 'text' => $text, + 'oldcontent' => $oldanswer['content'], + 'oldformat' => $oldanswer['format'], + 'oldanswer' => $oldanswer, + 'contentchanged' => $contentchanged, + )); + } + + + function qa_answer_set_hidden($oldanswer, $hidden, $userid, $handle, $cookieid, $question, $commentsfollows) +/* + Set the hidden status (application level) of $oldanswer to $hidden. Pass details of the user doing this + in $userid, $handle and $cookieid, the database record for the question in $question, and the database + records for all comments on the answer in $commentsfollows ($commentsfollows can also contain other + records which are ignored). Handles indexing, user points, cached counts and event reports. + See qa-app-posts.php for a higher-level function which is easier to use. +*/ + { + require_once QA_INCLUDE_DIR.'qa-app-format.php'; + + $wasqueued=($oldanswer['type']=='A_QUEUED'); + + qa_post_unindex($oldanswer['postid']); + + foreach ($commentsfollows as $comment) + if ( ($comment['basetype']=='C') && ($comment['parentid']==$oldanswer['postid']) ) + qa_post_unindex($comment['postid']); + + $setupdated=$hidden || !$wasqueued; // don't record approval of a post as an update action + + qa_db_post_set_type($oldanswer['postid'], $hidden ? 'A_HIDDEN' : 'A', + $setupdated ? $userid : null, $setupdated ? qa_remote_ip_address() : null, QA_UPDATE_VISIBLE); + + qa_db_points_update_ifuser($oldanswer['userid'], array('aposts', 'aselecteds')); + qa_db_post_acount_update($question['postid']); + qa_db_hotness_update($question['postid']); + qa_db_acount_update(); + qa_db_unaqcount_update(); + qa_db_unupaqcount_update(); + + if (($question['type']=='Q') && !$hidden) { // even if answer visible, don't index if question is hidden or queued + qa_post_index($oldanswer['postid'], 'A', $question['postid'], $oldanswer['parentid'], null, + $oldanswer['content'], $oldanswer['format'], qa_viewer_text($oldanswer['content'], $oldanswer['format']), null); + + foreach ($commentsfollows as $comment) + if ( ($comment['type']=='C') && ($comment['parentid']==$oldanswer['postid']) ) // and don't index hidden/queued comments + qa_post_index($comment['postid'], $comment['type'], $question['postid'], $comment['parentid'], null, + $comment['content'], $comment['format'], qa_viewer_text($comment['content'], $comment['format']), null); + } + + qa_report_event($wasqueued ? ($hidden ? 'a_reject' : 'a_approve') : ($hidden ? 'a_hide' : 'a_reshow'), $userid, $handle, $cookieid, array( + 'postid' => $oldanswer['postid'], + 'parentid' => $oldanswer['parentid'], + 'oldanswer' => $oldanswer, + )); + + if ($wasqueued && !$hidden) { + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + qa_report_event('a_post', $oldanswer['userid'], $oldanswer['handle'], $oldanswer['cookieid'], array( + 'postid' => $oldanswer['postid'], + 'parentid' => $oldanswer['parentid'], + 'parent' => $question, + 'content' => $oldanswer['content'], + 'format' => $oldanswer['format'], + 'text' => qa_viewer_text($oldanswer['content'], $oldanswer['format']), + 'categoryid' => $oldanswer['categoryid'], + 'notify' => isset($oldanswer['notify']), + 'email' => qa_email_validate($oldanswer['notify']) ? $oldanswer['notify'] : null, + )); + } + } + + + function qa_answer_delete($oldanswer, $question, $userid, $handle, $cookieid) +/* + Permanently delete an answer (application level) from the database. The answer must not have any comments or + follow-on questions. Pass the database record for the question in $question and details of the user doing this + in $userid, $handle and $cookieid. Handles unindexing, votes, points, cached counts and event reports. + See qa-app-posts.php for a higher-level function which is easier to use. +*/ + { + require_once QA_INCLUDE_DIR.'qa-db-votes.php'; + + if ($oldanswer['type']!='A_HIDDEN') + qa_fatal_error('Tried to delete a non-hidden answer'); + + $useridvotes=qa_db_uservote_post_get($oldanswer['postid']); + + qa_post_unindex($oldanswer['postid']); + qa_db_post_delete($oldanswer['postid']); // also deletes any related voteds due to cascading + + if ($question['selchildid']==$oldanswer['postid']) { + qa_db_post_set_selchildid($question['postid'], null); + qa_db_points_update_ifuser($question['userid'], 'aselects'); + qa_db_unselqcount_update(); + } + + qa_db_points_update_ifuser($oldanswer['userid'], array('aposts', 'aselecteds', 'avoteds', 'upvoteds', 'downvoteds')); + + foreach ($useridvotes as $userid => $vote) + qa_db_points_update_ifuser($userid, ($vote>0) ? 'aupvotes' : 'adownvotes'); + // could do this in one query like in qa_db_users_recalc_points() but this will do for now - unlikely to be many votes + + qa_db_post_acount_update($question['postid']); + qa_db_hotness_update($question['postid']); + qa_db_acount_update(); + qa_db_unaqcount_update(); + qa_db_unupaqcount_update(); + + qa_report_event('a_delete', $userid, $handle, $cookieid, array( + 'postid' => $oldanswer['postid'], + 'parentid' => $oldanswer['parentid'], + 'oldanswer' => $oldanswer, + )); + } + + + function qa_answer_set_userid($oldanswer, $userid, $handle, $cookieid) +/* + Set the author (application level) of $oldanswer to $userid and also pass $handle and $cookieid + of user. Updates points and reports events as appropriate. +*/ + { + qa_db_post_set_userid($oldanswer['postid'], $userid); + + qa_db_points_update_ifuser($oldanswer['userid'], array('aposts', 'aselecteds', 'avoteds', 'upvoteds', 'downvoteds')); + qa_db_points_update_ifuser($userid, array('aposts', 'aselecteds', 'avoteds', 'upvoteds', 'downvoteds')); + + qa_report_event('a_claim', $userid, $handle, $cookieid, array( + 'postid' => $oldanswer['postid'], + 'parentid' => $oldanswer['parentid'], + 'oldanswer' => $oldanswer, + )); + } + + + function qa_comment_set_content($oldcomment, $content, $format, $text, $notify, $userid, $handle, $cookieid, $question, $parent) +/* + Change the fields of a comment (application level) to $content, $format and $notify, and reindex based on $text. + Pass the comment's database record before changes in $oldcomment, details of the user doing this in $userid, + $handle and $cookieid, the antecedent question in $question and the answer's database record in $answer if this + is a comment on an answer, otherwise null. Handles unindexing and event reports. + See qa-app-posts.php for a higher-level function which is easier to use. +*/ + { + if (!isset($parent)) + $parent=$question; // for backwards compatibility with old answer parameter + + qa_post_unindex($oldcomment['postid']); + + $contentchanged=strcmp($oldcomment['content'], $content) || strcmp($oldcomment['format'], $format); + + qa_db_post_set_content($oldcomment['postid'], $oldcomment['title'], $content, $format, $oldcomment['tags'], $notify, + $contentchanged ? $userid : null, $contentchanged ? qa_remote_ip_address() : null); + + if ( ($oldcomment['type']=='C') && ($question['type']=='Q') && (($parent['type']=='Q') || ($parent['type']=='A')) ) // all must be visible + qa_post_index($oldcomment['postid'], 'C', $question['postid'], $oldcomment['parentid'], null, $content, $format, $text, null); + + qa_report_event('c_edit', $userid, $handle, $cookieid, array( + 'postid' => $oldcomment['postid'], + 'parentid' => $oldcomment['parentid'], + 'parenttype' => $parent['basetype'], + 'questionid' => $question['postid'], + 'content' => $content, + 'format' => $format, + 'text' => $text, + 'oldcontent' => $oldcomment['content'], + 'oldformat' => $oldcomment['format'], + 'oldcomment' => $oldcomment, + 'contentchanged' => $contentchanged, + )); + } + + + function qa_answer_to_comment($oldanswer, $parentid, $content, $format, $text, $notify, $userid, $handle, $cookieid, $question, $answers, $commentsfollows) +/* + Convert an answer to a comment (application level) and set its fields to $content, $format and $notify. + Pass the answer's database record before changes in $oldanswer, the new comment's $parentid to be, details of the + user doing this in $userid, $handle and $cookieid, the antecedent question's record in $question, the records for + all answers to that question in $answers, and the records for all comments on the (old) answer and questions + following from the (old) answer in $commentsfollows ($commentsfollows can also contain other records which are ignored). + Handles indexing (based on $text), user points, cached counts and event reports. +*/ + { + $parent=isset($answers[$parentid]) ? $answers[$parentid] : $question; + + qa_post_unindex($oldanswer['postid']); + + $contentchanged=strcmp($oldanswer['content'], $content) || strcmp($oldanswer['format'], $format); + + qa_db_post_set_type($oldanswer['postid'], substr_replace($oldanswer['type'], 'C', 0, 1), $userid, qa_remote_ip_address(), QA_UPDATE_TYPE); + qa_db_post_set_parent($oldanswer['postid'], $parentid); + qa_db_post_set_content($oldanswer['postid'], $oldanswer['title'], $content, $format, $oldanswer['tags'], $notify, + $contentchanged ? $userid : null, $contentchanged ? qa_remote_ip_address() : null); + + foreach ($commentsfollows as $commentfollow) + if ($commentfollow['parentid']==$oldanswer['postid']) // do same thing for comments and follows + qa_db_post_set_parent($commentfollow['postid'], $parentid); + + qa_db_points_update_ifuser($oldanswer['userid'], array('aposts', 'aselecteds', 'cposts')); + + qa_db_post_acount_update($question['postid']); + qa_db_hotness_update($question['postid']); + qa_db_acount_update(); + qa_db_ccount_update(); + qa_db_unaqcount_update(); + qa_db_unupaqcount_update(); + + if ( ($oldanswer['type']=='A') && ($question['type']=='Q') && (($parent['type']=='Q') || ($parent['type']=='A')) ) // only if all fully visible + qa_post_index($oldanswer['postid'], 'C', $question['postid'], $parentid, null, $content, $format, $text, null); + + qa_report_event('a_to_c', $userid, $handle, $cookieid, array( + 'postid' => $oldanswer['postid'], + 'parentid' => $parentid, + 'parenttype' => $parent['basetype'], + 'questionid' => $question['postid'], + 'content' => $content, + 'format' => $format, + 'text' => $text, + 'oldcontent' => $oldanswer['content'], + 'oldformat' => $oldanswer['format'], + 'oldanswer' => $oldanswer, + 'contentchanged' => $contentchanged, + )); + } + + + function qa_comment_set_hidden($oldcomment, $hidden, $userid, $handle, $cookieid, $question, $parent) +/* + Set the hidden status (application level) of $oldcomment to $hidden. Pass the antecedent question's record in $question, + details of the user doing this in $userid, $handle and $cookieid, and the answer's database record in $answer if this + is a comment on an answer, otherwise null. Handles indexing, user points, cached counts and event reports. + See qa-app-posts.php for a higher-level function which is easier to use. +*/ + { + require_once QA_INCLUDE_DIR.'qa-app-format.php'; + + if (!isset($parent)) + $parent=$question; // for backwards compatibility with old answer parameter + + $wasqueued=($oldcomment['type']=='C_QUEUED'); + + qa_post_unindex($oldcomment['postid']); + + $setupdated=$hidden || !$wasqueued; // don't record approval of a post as an update action + + qa_db_post_set_type($oldcomment['postid'], $hidden ? 'C_HIDDEN' : 'C', + $setupdated ? $userid : null, $setupdated ? qa_remote_ip_address() : null, QA_UPDATE_VISIBLE); + + qa_db_points_update_ifuser($oldcomment['userid'], array('cposts')); + qa_db_ccount_update(); + + if ( ($question['type']=='Q') && (($parent['type']=='Q') || ($parent['type']=='A')) && !$hidden) // only index if none of the things it depends on are hidden or queued + qa_post_index($oldcomment['postid'], 'C', $question['postid'], $oldcomment['parentid'], null, + $oldcomment['content'], $oldcomment['format'], qa_viewer_text($oldcomment['content'], $oldcomment['format']), null); + + qa_report_event($wasqueued ? ($hidden ? 'c_reject' : 'c_approve') : ($hidden ? 'c_hide' : 'c_reshow'), $userid, $handle, $cookieid, array( + 'postid' => $oldcomment['postid'], + 'parentid' => $oldcomment['parentid'], + 'oldcomment' => $oldcomment, + 'parenttype' => $parent['basetype'], + 'questionid' => $question['postid'], + )); + + if ($wasqueued && !$hidden) { + require_once QA_INCLUDE_DIR.'qa-db-selects.php'; + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + $commentsfollows=qa_db_single_select(qa_db_full_child_posts_selectspec(null, $oldcomment['parentid'])); + $thread=array(); + + foreach ($commentsfollows as $comment) + if (($comment['type']=='C') && ($comment['parentid']==$parent['postid'])) + $thread[]=$comment; + + qa_report_event('c_post', $oldcomment['userid'], $oldcomment['handle'], $oldcomment['cookieid'], array( + 'postid' => $oldcomment['postid'], + 'parentid' => $oldcomment['parentid'], + 'parenttype' => $parent['basetype'], + 'parent' => $parent, + 'questionid' => $question['postid'], + 'question' => $question, + 'thread' => $thread, + 'content' => $oldcomment['content'], + 'format' => $oldcomment['format'], + 'text' => qa_viewer_text($oldcomment['content'], $oldcomment['format']), + 'categoryid' => $oldcomment['categoryid'], + 'notify' => isset($oldcomment['notify']), + 'email' => qa_email_validate($oldcomment['notify']) ? $oldcomment['notify'] : null, + )); + } + } + + + function qa_comment_delete($oldcomment, $question, $parent, $userid, $handle, $cookieid) +/* + Permanently delete a comment in $oldcomment (application level) from the database. Pass the database question in $question + and the answer's database record in $answer if this is a comment on an answer, otherwise null. Pass details of the user + doing this in $userid, $handle and $cookieid. Handles unindexing, points, cached counts and event reports. + See qa-app-posts.php for a higher-level function which is easier to use. +*/ + { + if (!isset($parent)) + $parent=$question; // for backwards compatibility with old answer parameter + + if ($oldcomment['type']!='C_HIDDEN') + qa_fatal_error('Tried to delete a non-hidden comment'); + + qa_post_unindex($oldcomment['postid']); + qa_db_post_delete($oldcomment['postid']); + qa_db_points_update_ifuser($oldcomment['userid'], array('cposts')); + qa_db_ccount_update(); + + qa_report_event('c_delete', $userid, $handle, $cookieid, array( + 'postid' => $oldcomment['postid'], + 'parentid' => $oldcomment['parentid'], + 'oldcomment' => $oldcomment, + 'parenttype' => $parent['basetype'], + 'questionid' => $question['postid'], + )); + } + + + function qa_comment_set_userid($oldcomment, $userid, $handle, $cookieid) +/* + Set the author (application level) of $oldcomment to $userid and also pass $handle and $cookieid + of user. Updates points and reports events as appropriate. +*/ + { + qa_db_post_set_userid($oldcomment['postid'], $userid); + + qa_db_points_update_ifuser($oldcomment['userid'], array('cposts')); + qa_db_points_update_ifuser($userid, array('cposts')); + + qa_report_event('c_claim', $userid, $handle, $cookieid, array( + 'postid' => $oldcomment['postid'], + 'parentid' => $oldcomment['parentid'], + 'oldcomment' => $oldcomment, + )); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-posts.php b/qa-include/qa-app-posts.php new file mode 100644 index 000000000..fae907c46 --- /dev/null +++ b/qa-include/qa-app-posts.php @@ -0,0 +1,433 @@ +get_text($content, $format, array()); + } + + + function qa_post_tags_to_tagstring($tags) +/* + Return tagstring to store in the database based on $tags as an array or a comma-separated string. +*/ + { + if (is_array($tags)) + $tags=implode(',', $tags); + + return qa_tags_to_tagstring(array_unique(preg_split('/\s*,\s*/', qa_strtolower(strtr($tags, '/', ' ')), -1, PREG_SPLIT_NO_EMPTY))); + } + + + function qa_post_get_question_answers($questionid) +/* + Return the full database records for all answers to question $questionid +*/ + { + $answers=array(); + + $childposts=qa_db_single_select(qa_db_full_child_posts_selectspec(null, $questionid)); + + foreach ($childposts as $postid => $post) + if ($post['basetype']=='A') + $answers[$postid]=$post; + + return $answers; + } + + + function qa_post_get_question_commentsfollows($questionid) +/* + Return the full database records for all comments or follow-on questions for question $questionid or its answers +*/ + { + $commentsfollows=array(); + + list($childposts, $achildposts)=qa_db_multi_select(array( + qa_db_full_child_posts_selectspec(null, $questionid), + qa_db_full_a_child_posts_selectspec(null, $questionid), + )); + + foreach ($childposts as $postid => $post) + if ($post['basetype']=='C') + $commentsfollows[$postid]=$post; + + foreach ($achildposts as $postid => $post) + if ( ($post['basetype']=='Q') || ($post['basetype']=='C') ) + $commentsfollows[$postid]=$post; + + return $commentsfollows; + } + + + function qa_post_get_question_closepost($questionid) +/* + Return the full database record for the post which closed $questionid, if there is any +*/ + { + return qa_db_single_select(qa_db_post_close_post_selectspec($questionid)); + } + + + function qa_post_get_answer_commentsfollows($answerid) +/* + Return the full database records for all comments or follow-on questions for answer $answerid +*/ + { + $commentsfollows=array(); + + $childposts=qa_db_single_select(qa_db_full_child_posts_selectspec(null, $answerid)); + + foreach ($childposts as $postid => $post) + if ( ($post['basetype']=='Q') || ($post['basetype']=='C') ) + $commentsfollows[$postid]=$post; + + return $commentsfollows; + } + + + function qa_post_parent_to_question($parent) +/* + Return $parent if it's the database record for a question, otherwise return the database record for its parent +*/ + { + if ($parent['basetype']=='Q') + $question=$parent; + else + $question=qa_post_get_full($parent['parentid'], 'Q'); + + return $question; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-q-list.php b/qa-include/qa-app-q-list.php new file mode 100644 index 000000000..88cd0e807 --- /dev/null +++ b/qa-include/qa-app-q-list.php @@ -0,0 +1,205 @@ + 'METHOD="POST" ACTION="'.qa_self_html().'"', + ); + + $qa_content['q_list']['qs']=array(); + + if (count($questions)) { + $qa_content['title']=$sometitle; + + $options=qa_post_html_defaults('Q'); + if (isset($categorypathprefix)) + $options['categorypathprefix']=$categorypathprefix; + + foreach ($questions as $question) + $qa_content['q_list']['qs'][]=qa_any_to_q_html_fields($question, $userid, qa_cookie_get(), $usershtml, null, $options); + + } else + $qa_content['title']=$nonetitle; + + if (isset($userid) && isset($categoryid) && isset($categoryisfavorite)) + $qa_content['favorite']=qa_favorite_form(QA_ENTITY_CATEGORY, $categoryid, $categoryisfavorite, + qa_lang_sub($categoryisfavorite ? 'main/remove_x_favorites' : 'main/add_category_x_favorites', $navcategories[$categoryid]['title'])); + + if (isset($count) && isset($pagesize)) + $qa_content['page_links']=qa_html_page_links(qa_request(), $start, $pagesize, $count, qa_opt('pages_prev_next'), $pagelinkparams); + + if (empty($qa_content['page_links'])) + $qa_content['suggest_next']=$suggest; + + if (qa_using_categories() && count($navcategories) && isset($categorypathprefix)) + $qa_content['navigation']['cat']=qa_category_navigation($navcategories, $categoryid, $categorypathprefix, $categoryqcount, $categoryparams); + + if (isset($feedpathprefix) && (qa_opt('feed_per_category') || !isset($categoryid)) ) + $qa_content['feed']=array( + 'url' => qa_path_html(qa_feed_request($feedpathprefix.(isset($categoryid) ? ('/'.qa_category_path_request($navcategories, $categoryid)) : ''))), + 'label' => strip_tags($sometitle), + ); + + return $qa_content; + } + + + function qa_qs_sub_navigation($sort, $categoryslugs) +/* + Return the sub navigation structure common to question listing pages +*/ + { + $request='questions'; + + if (isset($categoryslugs)) + foreach ($categoryslugs as $slug) + $request.='/'.$slug; + + $navigation=array( + 'recent' => array( + 'label' => qa_lang('main/nav_most_recent'), + 'url' => qa_path_html($request), + ), + + 'hot' => array( + 'label' => qa_lang('main/nav_hot'), + 'url' => qa_path_html($request, array('sort' => 'hot')), + ), + + 'votes' => array( + 'label' => qa_lang('main/nav_most_votes'), + 'url' => qa_path_html($request, array('sort' => 'votes')), + ), + + 'answers' => array( + 'label' => qa_lang('main/nav_most_answers'), + 'url' => qa_path_html($request, array('sort' => 'answers')), + ), + + 'views' => array( + 'label' => qa_lang('main/nav_most_views'), + 'url' => qa_path_html($request, array('sort' => 'views')), + ), + ); + + if (isset($navigation[$sort])) + $navigation[$sort]['selected']=true; + else + $navigation['recent']['selected']=true; + + if (!qa_opt('do_count_q_views')) + unset($navigation['views']); + + return $navigation; + } + + + function qa_unanswered_sub_navigation($by, $categoryslugs) +/* + Return the sub navigation structure common to unanswered pages +*/ + { + $request='unanswered'; + + if (isset($categoryslugs)) + foreach ($categoryslugs as $slug) + $request.='/'.$slug; + + $navigation=array( + 'answers' => array( + 'label' => qa_lang('main/nav_no_answer'), + 'url' => qa_path_html($request), + ), + + 'selected' => array( + 'label' => qa_lang('main/nav_no_selected_answer'), + 'url' => qa_path_html($request, array('by' => 'selected')), + ), + + 'upvotes' => array( + 'label' => qa_lang('main/nav_no_upvoted_answer'), + 'url' => qa_path_html($request, array('by' => 'upvotes')), + ), + ); + + if (isset($navigation[$by])) + $navigation[$by]['selected']=true; + else + $navigation['answers']['selected']=true; + + if (!qa_opt('voting_on_as')) + unset($navigation['upvotes']); + + return $navigation; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-recalc.php b/qa-include/qa-app-recalc.php new file mode 100644 index 000000000..14c325dcd --- /dev/null +++ b/qa-include/qa-app-recalc.php @@ -0,0 +1,647 @@ + $post) + qa_post_index($postid, $post['type'], $post['questionid'], $post['parentid'], $post['title'], $post['content'], + $post['format'], qa_viewer_text($post['content'], $post['format']), $post['tags']); + + $next=1+$lastpostid; + $done+=count($posts); + $continue=true; + + } else { + qa_db_truncate_indexes($next); + qa_recalc_transition($state, 'doreindexposts_wordcount'); + } + break; + + case 'doreindexposts_wordcount': + $wordids=qa_db_words_prepare_for_recounting($next, 1000); + + if (count($wordids)) { + $lastwordid=max($wordids); + + qa_db_words_recount($next, $lastwordid); + + $next=1+$lastwordid; + $done+=count($wordids); + $continue=true; + + } else { + qa_db_tagcount_update(); // this is quick so just do it here + qa_recalc_transition($state, 'doreindexposts_complete'); + } + break; + + case 'dorecountposts': + qa_recalc_transition($state, 'dorecountposts_postcount'); + break; + + case 'dorecountposts_postcount': + qa_db_qcount_update(); + qa_db_acount_update(); + qa_db_ccount_update(); + qa_db_unaqcount_update(); + qa_db_unselqcount_update(); + + qa_recalc_transition($state, 'dorecountposts_votecount'); + break; + + case 'dorecountposts_votecount': + $postids=qa_db_posts_get_for_recounting($next, 1000); + + if (count($postids)) { + $lastpostid=max($postids); + + qa_db_posts_votes_recount($next, $lastpostid); + + $next=1+$lastpostid; + $done+=count($postids); + $continue=true; + + } else + qa_recalc_transition($state, 'dorecountposts_acount'); + break; + + case 'dorecountposts_acount': + $postids=qa_db_posts_get_for_recounting($next, 1000); + + if (count($postids)) { + $lastpostid=max($postids); + + qa_db_posts_answers_recount($next, $lastpostid); + + $next=1+$lastpostid; + $done+=count($postids); + $continue=true; + + } else { + qa_db_unupaqcount_update(); + qa_recalc_transition($state, 'dorecountposts_complete'); + } + break; + + case 'dorecalcpoints': + qa_recalc_transition($state, 'dorecalcpoints_usercount'); + break; + + case 'dorecalcpoints_usercount': + qa_db_userpointscount_update(); // for progress update - not necessarily accurate + qa_recalc_transition($state, 'dorecalcpoints_recalc'); + break; + + case 'dorecalcpoints_recalc': + $userids=qa_db_users_get_for_recalc_points($next, 10); + + if (count($userids)) { + $lastuserid=max($userids); + + qa_db_users_recalc_points($next, $lastuserid); + + $next=1+$lastuserid; + $done+=count($userids); + $continue=true; + + } else { + qa_db_truncate_userpoints($next); + qa_db_userpointscount_update(); // quick so just do it here + qa_recalc_transition($state, 'dorecalcpoints_complete'); + } + break; + + case 'dorefillevents': + qa_recalc_transition($state, 'dorefillevents_qcount'); + break; + + case 'dorefillevents_qcount': + qa_db_qcount_update(); + qa_recalc_transition($state, 'dorefillevents_refill'); + break; + + case 'dorefillevents_refill': + $questionids=qa_db_qs_get_for_event_refilling($next, 1); + + if (count($questionids)) { + require_once QA_INCLUDE_DIR.'qa-app-events.php'; + require_once QA_INCLUDE_DIR.'qa-app-updates.php'; + require_once QA_INCLUDE_DIR.'qa-util-sort.php'; + + $lastquestionid=max($questionids); + + foreach ($questionids as $questionid) { + + // Retrieve all posts relating to this question + + list($question, $childposts, $achildposts)=qa_db_select_with_pending( + qa_db_full_post_selectspec(null, $questionid), + qa_db_full_child_posts_selectspec(null, $questionid), + qa_db_full_a_child_posts_selectspec(null, $questionid) + ); + + // Merge all posts while preserving keys as postids + + $posts=array($questionid => $question); + + foreach ($childposts as $postid => $post) + $posts[$postid]=$post; + + foreach ($achildposts as $postid => $post) + $posts[$postid]=$post; + + // Creation and editing of each post + + foreach ($posts as $postid => $post) { + $followonq=($post['basetype']=='Q') && ($postid!=$questionid); + + qa_create_event_for_q_user($questionid, $postid, $followonq ? QA_UPDATE_FOLLOWS : null, $post['userid'], @$posts[$post['parentid']]['userid'], $post['created']); + + if (isset($post['updated']) && !$followonq) + qa_create_event_for_q_user($questionid, $postid, $post['updatetype'], $post['lastuserid'], $post['userid'], $post['updated']); + } + + // Tags and categories of question + + qa_create_event_for_tags($question['tags'], $questionid, null, $question['userid'], $question['created']); + qa_create_event_for_category($question['categoryid'], $questionid, null, $question['userid'], $question['created']); + + // Collect comment threads + + $parentidcomments=array(); + + foreach ($posts as $postid => $post) + if ($post['basetype']=='C') + $parentidcomments[$post['parentid']][$postid]=$post; + + // For each comment thread, notify all previous comment authors of each comment in the thread (could get slow) + + foreach ($parentidcomments as $parentid => $comments) { + $keyuserids=array(); + + qa_sort_by($comments, 'created'); + + foreach ($comments as $comment) { + foreach ($keyuserids as $keyuserid => $dummy) + if ( ($keyuserid != $comment['userid']) && ($keyuserid != @$posts[$parentid]['userid']) ) + qa_db_event_create_not_entity($keyuserid, $questionid, $comment['postid'], null, $comment['userid'], $comment['created']); + + if (isset($comment['userid'])) + $keyuserids[$comment['userid']]=true; + } + } + } + + $next=1+$lastquestionid; + $done+=count($questionids); + $continue=true; + + } else + qa_recalc_transition($state, 'dorefillevents_complete'); + break; + + case 'dorecalccategories': + qa_recalc_transition($state, 'dorecalccategories_postcount'); + break; + + case 'dorecalccategories_postcount': + qa_db_acount_update(); + qa_db_ccount_update(); + + qa_recalc_transition($state, 'dorecalccategories_postupdate'); + break; + + case 'dorecalccategories_postupdate': + $postids=qa_db_posts_get_for_recategorizing($next, 100); + + if (count($postids)) { + $lastpostid=max($postids); + + qa_db_posts_recalc_categoryid($next, $lastpostid); + qa_db_posts_calc_category_path($next, $lastpostid); + + $next=1+$lastpostid; + $done+=count($postids); + $continue=true; + + } else { + qa_recalc_transition($state, 'dorecalccategories_recount'); + } + break; + + case 'dorecalccategories_recount': + $categoryids=qa_db_categories_get_for_recalcs($next, 10); + + if (count($categoryids)) { + $lastcategoryid=max($categoryids); + + foreach ($categoryids as $categoryid) + qa_db_ifcategory_qcount_update($categoryid); + + $next=1+$lastcategoryid; + $done+=count($categoryids); + $continue=true; + + } else { + qa_recalc_transition($state, 'dorecalccategories_backpaths'); + } + break; + + case 'dorecalccategories_backpaths': + $categoryids=qa_db_categories_get_for_recalcs($next, 10); + + if (count($categoryids)) { + $lastcategoryid=max($categoryids); + + qa_db_categories_recalc_backpaths($next, $lastcategoryid); + + $next=1+$lastcategoryid; + $done+=count($categoryids); + $continue=true; + + } else { + qa_recalc_transition($state, 'dorecalccategories_complete'); + } + break; + + case 'dodeletehidden': + qa_recalc_transition($state, 'dodeletehidden_comments'); + break; + + case 'dodeletehidden_comments': + $posts=qa_db_posts_get_for_deleting('C', $next, 1); + + if (count($posts)) { + require_once QA_INCLUDE_DIR.'qa-app-posts.php'; + + $postid=$posts[0]; + + qa_post_delete($postid); + + $next=1+$postid; + $done++; + $continue=true; + + } else + qa_recalc_transition($state, 'dodeletehidden_answers'); + break; + + case 'dodeletehidden_answers': + $posts=qa_db_posts_get_for_deleting('A', $next, 1); + + if (count($posts)) { + require_once QA_INCLUDE_DIR.'qa-app-posts.php'; + + $postid=$posts[0]; + + qa_post_delete($postid); + + $next=1+$postid; + $done++; + $continue=true; + + } else + qa_recalc_transition($state, 'dodeletehidden_questions'); + break; + + case 'dodeletehidden_questions': + $posts=qa_db_posts_get_for_deleting('Q', $next, 1); + + if (count($posts)) { + require_once QA_INCLUDE_DIR.'qa-app-posts.php'; + + $postid=$posts[0]; + + qa_post_delete($postid); + + $next=1+$postid; + $done++; + $continue=true; + + } else + qa_recalc_transition($state, 'dodeletehidden_complete'); + break; + + default: + $state=''; + break; + } + + if ($continue) + $state=$operation.','.$length.','.$next.','.$done; + + return $continue && ($done<$length); + } + + + function qa_recalc_transition(&$state, $operation) +/* + Change the $state to represent the beginning of a new $operation +*/ + { + $state=$operation.','.qa_recalc_stage_length($operation).',0,0'; + } + + + function qa_recalc_stage_length($operation) +/* + Return how many steps there will be in recalculation $operation +*/ + { + switch ($operation) { + case 'doreindexposts_reindex': + $length=qa_opt('cache_qcount')+qa_opt('cache_acount')+qa_opt('cache_ccount'); + break; + + case 'doreindexposts_wordcount': + $length=qa_db_count_words(); + break; + + case 'dorecalcpoints_recalc': + $length=qa_opt('cache_userpointscount'); + break; + + case 'dorecountposts_votecount': + case 'dorecountposts_acount': + case 'dorecalccategories_postupdate': + $length=qa_db_count_posts(); + break; + + case 'dorefillevents_refill': + $length=qa_opt('cache_qcount')+qa_db_count_posts('Q_HIDDEN'); + break; + + case 'dorecalccategories_recount': + case 'dorecalccategories_backpaths': + $length=qa_db_count_categories(); + break; + + case 'dodeletehidden_comments': + $length=count(qa_db_posts_get_for_deleting('C')); + break; + + case 'dodeletehidden_answers': + $length=count(qa_db_posts_get_for_deleting('A')); + break; + + case 'dodeletehidden_questions': + $length=count(qa_db_posts_get_for_deleting('Q')); + break; + + default: + $length=0; + break; + } + + return $length; + } + + + function qa_recalc_get_message($state) +/* + Return a string which gives a user-viewable version of $state +*/ + { + @list($operation, $length, $next, $done)=explode(',', $state); + + $done=(int)$done; + $length=(int)$length; + + switch ($operation) { + case 'doreindexposts_postcount': + case 'dorecountposts_postcount': + case 'dorecalccategories_postcount': + case 'dorefillevents_qcount': + $message=qa_lang('admin/recalc_posts_count'); + break; + + case 'doreindexposts_reindex': + $message=strtr(qa_lang('admin/reindex_posts_reindexed'), array( + '^1' => number_format($done), + '^2' => number_format($length) + )); + break; + + case 'doreindexposts_wordcount': + $message=strtr(qa_lang('admin/reindex_posts_wordcounted'), array( + '^1' => number_format($done), + '^2' => number_format($length) + )); + break; + + case 'dorecountposts_votecount': + $message=strtr(qa_lang('admin/recount_posts_votes_recounted'), array( + '^1' => number_format($done), + '^2' => number_format($length) + )); + break; + + case 'dorecountposts_acount': + $message=strtr(qa_lang('admin/recount_posts_as_recounted'), array( + '^1' => number_format($done), + '^2' => number_format($length) + )); + break; + + case 'doreindexposts_complete': + $message=qa_lang('admin/reindex_posts_complete'); + break; + + case 'dorecountposts_complete': + $message=qa_lang('admin/recount_posts_complete'); + break; + + case 'dorecalcpoints_usercount': + $message=qa_lang('admin/recalc_points_usercount'); + break; + + case 'dorecalcpoints_recalc': + $message=strtr(qa_lang('admin/recalc_points_recalced'), array( + '^1' => number_format($done), + '^2' => number_format($length) + )); + break; + + case 'dorecalcpoints_complete': + $message=qa_lang('admin/recalc_points_complete'); + break; + + case 'dorefillevents_refill': + $message=strtr(qa_lang('admin/refill_events_refilled'), array( + '^1' => number_format($done), + '^2' => number_format($length) + )); + break; + + case 'dorefillevents_complete': + $message=qa_lang('admin/refill_events_complete'); + break; + + case 'dorecalccategories_postupdate': + $message=strtr(qa_lang('admin/recalc_categories_updated'), array( + '^1' => number_format($done), + '^2' => number_format($length) + )); + break; + + case 'dorecalccategories_recount': + $message=strtr(qa_lang('admin/recalc_categories_recounting'), array( + '^1' => number_format($done), + '^2' => number_format($length) + )); + break; + + case 'dorecalccategories_backpaths': + $message=strtr(qa_lang('admin/recalc_categories_backpaths'), array( + '^1' => number_format($done), + '^2' => number_format($length) + )); + break; + + case 'dorecalccategories_complete': + $message=qa_lang('admin/recalc_categories_complete'); + break; + + case 'dodeletehidden_comments': + $message=strtr(qa_lang('admin/hidden_comments_deleted'), array( + '^1' => number_format($done), + '^2' => number_format($length) + )); + break; + + case 'dodeletehidden_answers': + $message=strtr(qa_lang('admin/hidden_answers_deleted'), array( + '^1' => number_format($done), + '^2' => number_format($length) + )); + break; + + case 'dodeletehidden_questions': + $message=strtr(qa_lang('admin/hidden_questions_deleted'), array( + '^1' => number_format($done), + '^2' => number_format($length) + )); + break; + + case 'dodeletehidden_complete': + $message=qa_lang('admin/delete_hidden_complete'); + break; + + default: + $message=''; + break; + } + + return $message; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-search.php b/qa-include/qa-app-search.php new file mode 100644 index 000000000..7a007b2f4 --- /dev/null +++ b/qa-include/qa-app-search.php @@ -0,0 +1,144 @@ +1) { + $tryname=qa_opt('search_module'); // use chosen one if it's available + + if (isset($searchmodules[$tryname])) + $module=$searchmodules[$tryname]; + } + + // Get the results + + $results=$module->process_search($query, $start, $count, $userid, $absoluteurls, $fullcontent); + + // Work out what additional information (if any) we need to retrieve for the results + + $keypostidgetfull=array(); + $keypostidgettype=array(); + $keypostidgetquestion=array(); + $keypageidgetpage=array(); + + foreach ($results as $result) { + if (isset($result['question_postid']) && !isset($result['question'])) + $keypostidgetfull[$result['question_postid']]=true; + + if (isset($result['match_postid'])) { + if (!( (isset($result['question_postid'])) || (isset($result['question'])) )) + $keypostidgetquestion[$result['match_postid']]=true; // we can also get $result['match_type'] from this + + elseif (!isset($result['match_type'])) + $keypostidgettype[$result['match_postid']]=true; + } + + if (isset($result['page_pageid']) && !isset($result['page'])) + $keypageidgetpage[$result['page_pageid']]=true; + } + + // Perform the appropriate database queries + + @list($postidfull, $postidtype, $postidquestion, $pageidpage)=qa_db_select_with_pending( + count($keypostidgetfull) ? qa_db_posts_selectspec($userid, array_keys($keypostidgetfull), $fullcontent) : null, + count($keypostidgettype) ? qa_db_posts_basetype_selectspec(array_keys($keypostidgettype)) : null, + count($keypostidgetquestion) ? qa_db_posts_to_qs_selectspec($userid, array_keys($keypostidgetquestion), $fullcontent) : null, + count($keypageidgetpage) ? qa_db_pages_selectspec(null, array_keys($keypageidgetpage)) : null + ); + + // Supplement the results as appropriate + + foreach ($results as $key => $result) { + if (isset($result['question_postid']) && !isset($result['question'])) + if (@$postidfull[$result['question_postid']]['basetype']=='Q') + $result['question']=@$postidfull[$result['question_postid']]; + + if (isset($result['match_postid'])) { + if (!( (isset($result['question_postid'])) || (isset($result['question'])) )) { + $result['question']=@$postidquestion[$result['match_postid']]; + + if (!isset($result['match_type'])) + $result['match_type']=@$result['question']['obasetype']; + + } elseif (!isset($result['match_type'])) + $result['match_type']=@$postidtype[$result['match_postid']]; + } + + if (isset($result['question']) && !isset($result['question_postid'])) + $result['question_postid']=$result['question']['postid']; + + if (isset($result['page_pageid']) && !isset($result['page'])) + $result['page']=@$pageidpage[$result['page_pageid']]; + + if (!isset($result['title'])) { + if (isset($result['question'])) + $result['title']=$result['question']['title']; + elseif (isset($result['page'])) + $result['title']=$result['page']['heading']; + } + + if (!isset($result['url'])) { + if (isset($result['question'])) + $result['url']=qa_q_path($result['question']['postid'], $result['question']['title'], + $absoluteurls, @$result['match_type'], @$result['match_postid']); + elseif (isset($result['page'])) + $result['url']=qa_path($result['page']['tags'], null, qa_opt('site_url')); + } + + $results[$key]=$result; + } + + // Return the results + + return $results; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-updates.php b/qa-include/qa-app-updates.php new file mode 100644 index 000000000..14273c692 --- /dev/null +++ b/qa-include/qa-app-updates.php @@ -0,0 +1,57 @@ +filter_handle($handle, $olduser); + if (isset($error)) { + $errors['handle']=$error; + break; + } + } + + if (!isset($errors['handle'])) { // first test through filters, then check for duplicates here + $handleusers=qa_db_user_find_by_handle($handle); + if (count($handleusers) && ( (!isset($olduser['userid'])) || (array_search($olduser['userid'], $handleusers)===false) ) ) + $errors['handle']=qa_lang('users/handle_exists'); + } + + $filtermodules=qa_load_modules_with('filter', 'filter_email'); + + $error=null; + foreach ($filtermodules as $filtermodule) { + $error=$filtermodule->filter_email($email, $olduser); + if (isset($error)) { + $errors['email']=$error; + break; + } + } + + if (!isset($errors['email'])) { + $emailusers=qa_db_user_find_by_email($email); + if (count($emailusers) && ( (!isset($olduser['userid'])) || (array_search($olduser['userid'], $emailusers)===false) ) ) + $errors['email']=qa_lang('users/email_exists'); + } + + return $errors; + } + + + function qa_handle_make_valid($handle) +/* + Make $handle valid and unique in the database - if $allowuserid is set, allow it to match that user only +*/ + { + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + require_once QA_INCLUDE_DIR.'qa-db-maxima.php'; + + if (!strlen($handle)) + $handle=qa_lang('users/registered_user'); + + $handle=preg_replace('/[\\@\\+\\/]/', ' ', $handle); + + for ($attempt=0; $attempt<=99; $attempt++) { + $suffix=$attempt ? (' '.$attempt) : ''; + $tryhandle=qa_substr($handle, 0, QA_DB_MAX_HANDLE_LENGTH-strlen($suffix)).$suffix; + + $filtermodules=qa_load_modules_with('filter', 'filter_handle'); + foreach ($filtermodules as $filtermodule) + $filtermodule->filter_handle($handle, null); // filter first without worrying about errors, since our goal is to get a valid one + + $haderror=false; + + foreach ($filtermodules as $filtermodule) { + $error=$filtermodule->filter_handle($handle, null); // now check for errors after we've filtered + if (isset($error)) + $haderror=true; + } + + if (!$haderror) { + $handleusers=qa_db_user_find_by_handle($tryhandle); + if (!count($handleusers)) + return $tryhandle; + } + } + + qa_fatal_error('Could not create a valid and unique handle'); + } + + + function qa_password_validate($password, $olduser=null) +/* + Return an array with a single element (key 'password') if user-entered $password is valid, otherwise an empty array. + Works by calling through to all filter modules. +*/ + { + $error=null; + $filtermodules=qa_load_modules_with('filter', 'validate_password'); + + foreach ($filtermodules as $filtermodule) { + $error=$filtermodule->validate_password($password, $olduser); + if (isset($error)) + break; + } + + if (!isset($error)) { + $minpasslen=max(QA_MIN_PASSWORD_LEN, 1); + if (qa_strlen($password)<$minpasslen) + $error=qa_lang_sub('users/password_min', $minpasslen); + } + + if (isset($error)) + return array('password' => $error); + + return array(); + } + + + function qa_create_new_user($email, $password, $handle, $level=QA_USER_LEVEL_BASIC, $confirmed=false) +/* + Create a new user (application level) with $email, $password, $handle and $level. + Set $confirmed to true if the email address has been confirmed elsewhere. + Handles user points, notification and optional email confirmation. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-db-users.php'; + require_once QA_INCLUDE_DIR.'qa-db-points.php'; + require_once QA_INCLUDE_DIR.'qa-app-options.php'; + require_once QA_INCLUDE_DIR.'qa-app-emails.php'; + require_once QA_INCLUDE_DIR.'qa-app-cookies.php'; + + $userid=qa_db_user_create($email, $password, $handle, $level, qa_remote_ip_address()); + qa_db_points_update_ifuser($userid, null); + + if ($confirmed) + qa_db_user_set_flag($userid, QA_USER_FLAGS_EMAIL_CONFIRMED, true); + + if (qa_opt('show_notice_welcome')) + qa_db_user_set_flag($userid, QA_USER_FLAGS_WELCOME_NOTICE, true); + + $custom=qa_opt('show_custom_welcome') ? trim(qa_opt('custom_welcome')) : ''; + + if (qa_opt('confirm_user_emails') && ($level qa_get_new_confirm_url($userid, $handle) + )); + + if (qa_opt('confirm_user_required')) + qa_db_user_set_flag($userid, QA_USER_FLAGS_MUST_CONFIRM, true); + + } else + $confirm=''; + + qa_send_notification($userid, $email, $handle, qa_lang('emails/welcome_subject'), qa_lang('emails/welcome_body'), array( + '^password' => isset($password) ? $password : qa_lang('users/password_to_set'), + '^url' => qa_opt('site_url'), + '^custom' => strlen($custom) ? ($custom."\n\n") : '', + '^confirm' => $confirm, + )); + + qa_report_event('u_register', $userid, $handle, qa_cookie_get(), array( + 'email' => $email, + 'level' => $level, + )); + + return $userid; + } + + + function qa_delete_user($userid) +/* + Delete $userid and all their votes and flags. Their posts will become anonymous. + Handles recalculations of votes and flags for posts this user has affected. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-db-votes.php'; + require_once QA_INCLUDE_DIR.'qa-db-users.php'; + require_once QA_INCLUDE_DIR.'qa-db-post-update.php'; + require_once QA_INCLUDE_DIR.'qa-db-points.php'; + + $postids=qa_db_uservoteflag_user_get($userid); // posts this user has flagged or voted on, whose counts need updating + + qa_db_user_delete($userid); + + foreach ($postids as $postid) { // hoping there aren't many of these - saves a lot of new SQL code... + qa_db_post_recount_votes($postid); + qa_db_post_recount_flags($postid); + } + + $postuserids=qa_db_posts_get_userids($postids); + + foreach ($postuserids as $postuserid) + qa_db_points_update_ifuser($postuserid, array('avoteds','qvoteds', 'upvoteds', 'downvoteds')); + } + + + function qa_send_new_confirm($userid) +/* + Set a new email confirmation code for the user and send it out +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-db-users.php'; + require_once QA_INCLUDE_DIR.'qa-db-selects.php'; + require_once QA_INCLUDE_DIR.'qa-app-emails.php'; + + $userinfo=qa_db_select_with_pending(qa_db_user_account_selectspec($userid, true)); + + if (!qa_send_notification($userid, $userinfo['email'], $userinfo['handle'], qa_lang('emails/confirm_subject'), qa_lang('emails/confirm_body'), array( + '^url' => qa_get_new_confirm_url($userid, $userinfo['handle']), + ))) + qa_fatal_error('Could not send email confirmation'); + } + + + function qa_get_new_confirm_url($userid, $handle) +/* + Set a new email confirmation code for the user and return the corresponding link +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-db-users.php'; + + $emailcode=qa_db_user_rand_emailcode(); + qa_db_user_set($userid, 'emailcode', $emailcode); + + return qa_path('confirm', array('c' => $emailcode, 'u' => $handle), qa_opt('site_url')); + } + + + function qa_complete_confirm($userid, $email, $handle) +/* + Complete the email confirmation process for the user +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-db-users.php'; + require_once QA_INCLUDE_DIR.'qa-app-cookies.php'; + + qa_db_user_set_flag($userid, QA_USER_FLAGS_EMAIL_CONFIRMED, true); + qa_db_user_set_flag($userid, QA_USER_FLAGS_MUST_CONFIRM, false); + qa_db_user_set($userid, 'emailcode', ''); // to prevent re-use of the code + + qa_report_event('u_confirmed', $userid, $handle, qa_cookie_get(), array( + 'email' => $email, + )); + } + + + function qa_start_reset_user($userid) +/* + Start the 'I forgot my password' process for $userid, sending reset code +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-db-users.php'; + require_once QA_INCLUDE_DIR.'qa-app-options.php'; + require_once QA_INCLUDE_DIR.'qa-app-emails.php'; + require_once QA_INCLUDE_DIR.'qa-db-selects.php'; + + qa_db_user_set($userid, 'emailcode', qa_db_user_rand_emailcode()); + + $userinfo=qa_db_select_with_pending(qa_db_user_account_selectspec($userid, true)); + + if (!qa_send_notification($userid, $userinfo['email'], $userinfo['handle'], qa_lang('emails/reset_subject'), qa_lang('emails/reset_body'), array( + '^code' => $userinfo['emailcode'], + '^url' => qa_path('reset', array('c' => $userinfo['emailcode'], 'e' => $userinfo['email']), qa_opt('site_url')), + ))) + qa_fatal_error('Could not send reset password email'); + } + + + function qa_complete_reset_user($userid) +/* + Successfully finish the 'I forgot my password' process for $userid, sending new password +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + require_once QA_INCLUDE_DIR.'qa-app-options.php'; + require_once QA_INCLUDE_DIR.'qa-app-emails.php'; + require_once QA_INCLUDE_DIR.'qa-app-cookies.php'; + require_once QA_INCLUDE_DIR.'qa-db-selects.php'; + + $password=qa_random_alphanum(max(QA_MIN_PASSWORD_LEN, QA_NEW_PASSWORD_LEN)); + + $userinfo=qa_db_select_with_pending(qa_db_user_account_selectspec($userid, true)); + + if (!qa_send_notification($userid, $userinfo['email'], $userinfo['handle'], qa_lang('emails/new_password_subject'), qa_lang('emails/new_password_body'), array( + '^password' => $password, + '^url' => qa_opt('site_url'), + ))) + qa_fatal_error('Could not send new password - password not reset'); + + qa_db_user_set_password($userid, $password); // do this last, to be safe + qa_db_user_set($userid, 'emailcode', ''); // so can't be reused + + qa_report_event('u_reset', $userid, $userinfo['handle'], qa_cookie_get(), array( + 'email' => $userinfo['email'], + )); + } + + + function qa_logged_in_user_flush() +/* + Flush any information about the currently logged in user, so it is retrieved from database again +*/ + { + global $qa_cached_logged_in_user; + + $qa_cached_logged_in_user=null; + } + + + function qa_set_user_avatar($userid, $imagedata, $oldblobid=null) +/* + Set the avatar of $userid to the image in $imagedata, and remove $oldblobid from the database if not null +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-util-image.php'; + + $imagedata=qa_image_constrain_data($imagedata, $width, $height, qa_opt('avatar_store_size')); + + if (isset($imagedata)) { + require_once QA_INCLUDE_DIR.'qa-db-blobs.php'; + + $newblobid=qa_db_blob_create($imagedata, 'jpeg', null, $userid, null, qa_remote_ip_address()); + + if (isset($newblobid)) { + qa_db_user_set($userid, 'avatarblobid', $newblobid); + qa_db_user_set($userid, 'avatarwidth', $width); + qa_db_user_set($userid, 'avatarheight', $height); + qa_db_user_set_flag($userid, QA_USER_FLAGS_SHOW_AVATAR, true); + qa_db_user_set_flag($userid, QA_USER_FLAGS_SHOW_GRAVATAR, false); + + if (isset($oldblobid)) + qa_db_blob_delete($oldblobid); + + return true; + } + } + + return false; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-users.php b/qa-include/qa-app-users.php new file mode 100644 index 000000000..5c79be306 --- /dev/null +++ b/qa-include/qa-app-users.php @@ -0,0 +1,768 @@ +1) + qa_fatal_error('External login mapped to more than one user'); // should never happen + + if ($countusers) // user exists so log them in + qa_set_logged_in_user($users[0]['userid'], $users[0]['handle'], false, $source); + + else { // create and log in user + require_once QA_INCLUDE_DIR.'qa-app-users-edit.php'; + + qa_db_user_login_sync(true); + + $users=qa_db_user_login_find($source, $identifier); // check again after table is locked + + if (count($users)==1) { + qa_db_user_login_sync(false); + qa_set_logged_in_user($users[0]['userid'], $users[0]['handle'], false, $source); + + } else { + $handle=qa_handle_make_valid(@$fields['handle']); + + $userid=qa_create_new_user((string)@$fields['email'], null /* no password */, $handle, + isset($fields['level']) ? $fields['level'] : QA_USER_LEVEL_BASIC, @$fields['confirmed']); + + qa_db_user_login_add($userid, $source, $identifier); + qa_db_user_login_sync(false); + + $profilefields=array('name', 'location', 'website', 'about'); + + foreach ($profilefields as $fieldname) + if (strlen(@$fields[$fieldname])) + qa_db_user_profile_set($userid, $fieldname, $fields[$fieldname]); + + if (strlen(@$fields['avatar'])) + qa_set_user_avatar($userid, $fields['avatar']); + + qa_set_logged_in_user($userid, $handle, false, $source); + } + } + } + + + function qa_get_logged_in_userid() + /* + Return the userid of the currently logged in user, or null if none logged in + */ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_logged_in_userid_checked; + + $suffix=qa_session_var_suffix(); + + if (!$qa_logged_in_userid_checked) { // only check once + qa_start_session(); // this will load logged in userid from the native PHP session, but that's not enough + + $sessionuserid=@$_SESSION['qa_session_userid_'.$suffix]; + + if (isset($sessionuserid)) // check verify code matches + if (@$_SESSION['qa_session_verify_'.$suffix] != qa_session_verify_code($sessionuserid)) + qa_clear_session_user(); + + if (!empty($_COOKIE['qa_session'])) { + @list($handle, $sessioncode, $remember)=explode('/', $_COOKIE['qa_session']); + + if ($remember) + qa_set_session_cookie($handle, $sessioncode, $remember); // extend 'remember me' cookies each time + + $sessioncode=trim($sessioncode); // trim to prevent passing in blank values to match uninitiated DB rows + + // Try to recover session from the database if PHP session has timed out + if ( (!isset($_SESSION['qa_session_userid_'.$suffix])) && (!empty($handle)) && (!empty($sessioncode)) ) { + require_once QA_INCLUDE_DIR.'qa-db-selects.php'; + + $userinfo=qa_db_single_select(qa_db_user_account_selectspec($handle, false)); // don't get any pending + + if (strtolower(trim($userinfo['sessioncode'])) == strtolower($sessioncode)) + qa_set_session_user($userinfo['userid'], $userinfo['sessionsource']); + else + qa_clear_session_cookie(); // if cookie not valid, remove it to save future checks + } + } + + $qa_logged_in_userid_checked=true; + } + + return @$_SESSION['qa_session_userid_'.$suffix]; + } + + + function qa_get_logged_in_source() + /* + Get the source of the currently logged in user, from call to qa_log_in_external_user() or null if logged in normally + */ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $userid=qa_get_logged_in_userid(); + $suffix=qa_session_var_suffix(); + + if (isset($userid)) + return @$_SESSION['qa_session_source_'.$suffix]; + } + + + function qa_get_logged_in_user_field($field) + /* + Return $field of the currently logged in user, cache to ensure only one call to external code + */ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_cached_logged_in_user; + + $userid=qa_get_logged_in_userid(); + + if (isset($userid) && !isset($qa_cached_logged_in_user)) { + require_once QA_INCLUDE_DIR.'qa-db-selects.php'; + $qa_cached_logged_in_user=qa_db_get_pending_result('loggedinuser', qa_db_user_account_selectspec($userid, true)); + + if (!isset($qa_cached_logged_in_user)) { // the user can no longer be found (should be because they're deleted) + qa_clear_session_user(); + qa_fatal_error('The logged in user cannot be found'); + // it's too late here to proceed because the caller may already be branching based on whether someone is logged in + } + } + + return @$qa_cached_logged_in_user[$field]; + } + + + function qa_get_logged_in_points() + /* + Return the number of points of the currently logged in user, or null if none is logged in + */ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + return qa_get_logged_in_user_field('points'); + } + + + function qa_get_mysql_user_column_type() + /* + Return column type to use for users (if not using single sign-on integration) + */ + { + return 'INT UNSIGNED'; + } + + + function qa_get_one_user_html($handle, $microformats) + /* + Return HTML to display for user with username $handle + */ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + return strlen($handle) ? (''.qa_html($handle).'') : ''; + } + + + function qa_get_user_avatar_html($flags, $email, $handle, $blobid, $width, $height, $size, $padding=false) + /* + Return HTML to display for the user's avatar, constrained to $size pixels, with optional $padding to that size + Pass the user's fields $flags, $email, $handle, and avatar $blobid, $width and $height + */ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-app-format.php'; + + if (qa_opt('avatar_allow_gravatar') && ($flags & QA_USER_FLAGS_SHOW_GRAVATAR)) + $html=qa_get_gravatar_html($email, $size); + elseif (qa_opt('avatar_allow_upload') && (($flags & QA_USER_FLAGS_SHOW_AVATAR)) && isset($blobid)) + $html=qa_get_avatar_blob_html($blobid, $width, $height, $size, $padding); + elseif ( (qa_opt('avatar_allow_gravatar')||qa_opt('avatar_allow_upload')) && qa_opt('avatar_default_show') && strlen(qa_opt('avatar_default_blobid')) ) + $html=qa_get_avatar_blob_html(qa_opt('avatar_default_blobid'), qa_opt('avatar_default_width'), qa_opt('avatar_default_height'), $size, $padding); + else + $html=null; + + return (isset($html) && strlen($handle)) ? (''.$html.'') : $html; + } + + + function qa_get_user_email($userid) + /* + Return email address for user $userid (if not using single sign-on integration) + */ + { + $userinfo=qa_db_select_with_pending(qa_db_user_account_selectspec($userid, true)); + + return $userinfo['email']; + } + + + function qa_user_report_action($userid, $action) + /* + Called after a database write $action performed by a user $userid + */ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-db-users.php'; + + qa_db_user_written($userid, qa_remote_ip_address()); + } + + + function qa_user_level_string($level) + /* + Return textual representation of the user $level + */ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + if ($level>=QA_USER_LEVEL_SUPER) + $string='users/level_super'; + elseif ($level>=QA_USER_LEVEL_ADMIN) + $string='users/level_admin'; + elseif ($level>=QA_USER_LEVEL_MODERATOR) + $string='users/level_moderator'; + elseif ($level>=QA_USER_LEVEL_EDITOR) + $string='users/level_editor'; + elseif ($level>=QA_USER_LEVEL_EXPERT) + $string='users/level_expert'; + else + $string='users/registered_user'; + + return qa_lang($string); + } + + + function qa_get_login_links($rooturl, $tourl) + /* + Return an array of links to login, register, email confirm and logout pages (if not using single sign-on integration) + */ + { + return array( + 'login' => qa_path('login', isset($tourl) ? array('to' => $tourl) : null, $rooturl), + 'register' => qa_path('register', isset($tourl) ? array('to' => $tourl) : null, $rooturl), + 'confirm' => qa_path('confirm', null, $rooturl), + 'logout' => qa_path('logout', null, $rooturl), + ); + } + + } // end of: if (QA_FINAL_EXTERNAL_USERS) { ... } else { ... } + + + function qa_is_logged_in() +/* + Return whether someone is logged in at the moment +*/ + { + $userid=qa_get_logged_in_userid(); + return isset($userid); + } + + + function qa_get_logged_in_handle() +/* + Return displayable handle/username of currently logged in user, or null if none +*/ + { + return qa_get_logged_in_user_field(QA_FINAL_EXTERNAL_USERS ? 'publicusername' : 'handle'); + } + + + function qa_get_logged_in_email() +/* + Return email of currently logged in user, or null if none +*/ + { + return qa_get_logged_in_user_field('email'); + } + + + function qa_get_logged_in_level() +/* + Return level of currently logged in user, or null if none +*/ + { + return qa_get_logged_in_user_field('level'); + } + + + function qa_get_logged_in_flags() +/* + Return flags (see QA_USER_FLAGS_*) of currently logged in user, or null if none +*/ + { + return QA_FINAL_EXTERNAL_USERS ? 0 : qa_get_logged_in_user_field('flags'); + } + + + function qa_user_permit_error($permitoption=null, $limitaction=null) +/* + Check whether the logged in user has permission to perform $permitoption. If $permitoption is null, this simply + checks whether the user is blocked. Optionally provide an $limitaction (see top of qa-app-limits.php) to also check + against user or IP rate limits. + + Possible results, in order of priority (i.e. if more than one reason, the first will be given): + 'level' => a special privilege level (e.g. expert) or minimum number of points is required + 'login' => the user should login or register + 'userblock' => the user has been blocked + 'ipblock' => the ip address has been blocked + 'confirm' => the user should confirm their email address + 'limit' => the user or IP address has reached a rate limit (if $limitaction specified) + false => the operation can go ahead +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-app-limits.php'; + + $userid=qa_get_logged_in_userid(); + $flags=qa_get_logged_in_flags(); + + $error=qa_permit_error($permitoption, $userid, qa_get_logged_in_level(), $flags); + + if ((!$error) && qa_is_ip_blocked()) + $error='ipblock'; + + if ((!$error) && isset($userid) && ($flags & QA_USER_FLAGS_MUST_CONFIRM)) + $error='confirm'; + + if (isset($limitaction) && !$error) + if (qa_limits_remaining(qa_get_logged_in_userid(), $limitaction)<=0) + $error='limit'; + + return $error; + } + + + function qa_permit_error($permitoption, $userid, $userlevel, $userflags, $userpoints=null) +/* + Check whether $userid (null for no user) can perform $permitoption. Result as for qa_user_permit_error(...). + If appropriate, pass the user's level in $userlevel, flags in $userflags and points in $userpoints. + If $userid is currently logged in, you can set $userpoints=null to retrieve them only if necessary. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $permit=isset($permitoption) ? qa_opt($permitoption) : QA_PERMIT_ALL; + + if (isset($userid) && (($permit==QA_PERMIT_POINTS) || ($permit==QA_PERMIT_POINTS_CONFIRMED)) ) { // deal with points threshold by converting as appropriate + if ( (!isset($userpoints)) && ($userid==qa_get_logged_in_userid()) ) + $userpoints=qa_get_logged_in_points(); // allow late retrieval of points (to avoid unnecessary DB query when using external users) + + if ($userpoints>=qa_opt($permitoption.'_points')) + $permit=($permit==QA_PERMIT_POINTS_CONFIRMED) ? QA_PERMIT_CONFIRMED : QA_PERMIT_USERS; // convert if user has enough points + else + $permit=QA_PERMIT_EXPERTS; // otherwise, only special users pass + } + + return qa_permit_value_error($permit, $userid, $userlevel, $userflags); + } + + + function qa_permit_value_error($permit, $userid, $userlevel, $userflags) +/* + Check whether $userid of level $userlevel with $userflags can reach the permission level in $permit + (generally retrieved from an option, but not always). Result as for qa_user_permit_error(...). +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + if ($permit>=QA_PERMIT_ALL) + $error=false; + + elseif ($permit>=QA_PERMIT_USERS) + $error=isset($userid) ? false : 'login'; + + elseif ($permit>=QA_PERMIT_CONFIRMED) { + if (!isset($userid)) + $error='login'; + + elseif ( + QA_FINAL_EXTERNAL_USERS || // not currently supported by single sign-on integration + ($userlevel>=QA_USER_LEVEL_EXPERT) || // if user assigned to a higher level, no need + ($userflags & QA_USER_FLAGS_EMAIL_CONFIRMED) || // actual confirmation + (!qa_opt('confirm_user_emails')) // if this option off, we can't ask it of the user + ) + $error=false; + + else + $error='confirm'; + + } elseif ($permit>=QA_PERMIT_EXPERTS) + $error=(isset($userid) && ($userlevel>=QA_USER_LEVEL_EXPERT)) ? false : 'level'; + + elseif ($permit>=QA_PERMIT_EDITORS) + $error=(isset($userid) && ($userlevel>=QA_USER_LEVEL_EDITOR)) ? false : 'level'; + + elseif ($permit>=QA_PERMIT_MODERATORS) + $error=(isset($userid) && ($userlevel>=QA_USER_LEVEL_MODERATOR)) ? false : 'level'; + + elseif ($permit>=QA_PERMIT_ADMINS) + $error=(isset($userid) && ($userlevel>=QA_USER_LEVEL_ADMIN)) ? false : 'level'; + + else + $error=(isset($userid) && ($userlevel>=QA_USER_LEVEL_SUPER)) ? false : 'level'; + + if (isset($userid) && ($userflags & QA_USER_FLAGS_USER_BLOCKED) && ($error!='level')) + $error='userblock'; + + return $error; + } + + + function qa_user_use_captcha($captchaoption) +/* + Return whether a captcha should be presented to the current user for operation specified by $captchaoption +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $usecaptcha=false; + + if (qa_opt($captchaoption)) { + $userid=qa_get_logged_in_userid(); + + if ( (!isset($userid)) || !( + QA_FINAL_EXTERNAL_USERS || + (!qa_opt('captcha_on_unconfirmed')) || // we might not care about unconfirmed users + (!qa_opt('confirm_user_emails')) || // if this option off, we can't ask it of the user + (qa_get_logged_in_level()>=QA_USER_LEVEL_EXPERT) || // if assigned to a higher level, no need + (qa_get_logged_in_flags() & QA_USER_FLAGS_EMAIL_CONFIRMED) // actual confirmation + )) + $usecaptcha=true; + } + + return $usecaptcha; + } + + + function qa_user_moderation_reason() +/* + Return whether moderation is required for posts submitted by the current user. Possible results: + 'login' => moderation required because the user is not logged in + 'confirm' => moderation required because the user has not confirmed their email address + 'points' => moderation required because the user has insufficient points + false => moderation is not required +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $reason=false; + + $maxpermitpost=max(qa_opt('permit_post_q'), qa_opt('permit_post_a'), qa_opt('permit_post_c')); // used to see which options are shown in admin + + if ( + (qa_opt('moderate_anon_post') || ($maxpermitpost <= QA_PERMIT_USERS)) && // no one is moderated if anons can post unmoderated + (qa_get_logged_in_level() < QA_USER_LEVEL_EXPERT) && // experts and above aren't moderated + qa_user_permit_error('permit_moderate') // if the user can approve posts, no point in moderating theirs + ) { + $userid=qa_get_logged_in_userid(); + + if ( ($maxpermitpost > QA_PERMIT_USERS) && !isset($userid) ) + $reason='login'; + elseif ( ($maxpermitpost > QA_PERMIT_CONFIRMED) && qa_opt('confirm_user_emails') && qa_opt('moderate_unconfirmed') && !(qa_get_logged_in_flags() & QA_USER_FLAGS_EMAIL_CONFIRMED) ) + $reason='confirm'; + elseif ( ($maxpermitpost > QA_PERMIT_EXPERTS) && qa_opt('moderate_by_points') && (qa_get_logged_in_points() < qa_opt('moderate_points_limit') )) + $reason='points'; + } + + return $reason; + } + + + function qa_user_userfield_label($userfield) +/* + Return the label to display for $userfield as retrieved from the database, using default if no name set +*/ + { + if (isset($userfield['content'])) + return $userfield['content']; + + else { + $defaultlabels=array( + 'name' => 'users/full_name', + 'about' => 'users/about', + 'location' => 'users/location', + 'website' => 'users/website', + ); + + if (isset($defaultlabels[$userfield['title']])) + return qa_lang($defaultlabels[$userfield['title']]); + } + + return ''; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-app-votes.php b/qa-include/qa-app-votes.php new file mode 100644 index 000000000..4a9d0e508 --- /dev/null +++ b/qa-include/qa-app-votes.php @@ -0,0 +1,299 @@ +0) || ($oldvote>0) ) + $columns[]=$postisanswer ? 'aupvotes' : 'qupvotes'; + + if ( ($vote<0) || ($oldvote<0) ) + $columns[]=$postisanswer ? 'adownvotes' : 'qdownvotes'; + + qa_db_points_update_ifuser($userid, $columns); + + qa_db_points_update_ifuser($post['userid'], array($postisanswer ? 'avoteds' : 'qvoteds', 'upvoteds', 'downvoteds')); + + if ($post['basetype']=='Q') + qa_db_hotness_update($post['postid']); + + if ($vote<0) + $event=$postisanswer ? 'a_vote_down' : 'q_vote_down'; + elseif ($vote>0) + $event=$postisanswer ? 'a_vote_up' : 'q_vote_up'; + else + $event=$postisanswer ? 'a_vote_nil' : 'q_vote_nil'; + + qa_report_event($event, $userid, $handle, $cookieid, array( + 'postid' => $post['postid'], + 'vote' => $vote, + 'oldvote' => $oldvote, + )); + } + + + function qa_flag_error_html($post, $userid, $topage) +/* + Check if $userid can flag $post, on the page $topage. + Return an HTML error to display if there was a problem, or false if it's OK. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-db-selects.php'; + require_once QA_INCLUDE_DIR.'qa-app-options.php'; + require_once QA_INCLUDE_DIR.'qa-app-users.php'; + require_once QA_INCLUDE_DIR.'qa-app-limits.php'; + + if ( + is_array($post) && + qa_opt('flagging_of_posts') && + ( (!isset($post['userid'])) || (!isset($userid)) || ($post['userid']!=$userid) ) + ) { + + switch (qa_user_permit_error('permit_flag', QA_LIMIT_FLAGS)) { + case 'login': + return qa_insert_login_links(qa_lang_html('question/flag_must_login'), $topage); + break; + + case 'confirm': + return qa_insert_login_links(qa_lang_html('question/flag_must_confirm'), $topage); + break; + + case 'limit': + return qa_lang_html('question/flag_limit'); + break; + + default: + return qa_lang_html('users/no_permission'); + break; + + case false: + return false; + } + + } else + return qa_lang_html('question/flag_not_allowed'); // flagging option should not have been presented + } + + + function qa_flag_set_tohide($oldpost, $userid, $handle, $cookieid, $question) +/* + Set (application level) a flag by $userid (with $handle and $cookieid) on $oldpost which belongs to $question. + Handles recounting, admin notifications and event reports as appropriate. + Returns true if the post should now be hidden because it has accumulated enough flags. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-db-votes.php'; + require_once QA_INCLUDE_DIR.'qa-app-limits.php'; + + qa_db_userflag_set($oldpost['postid'], $userid, true); + qa_db_post_recount_flags($oldpost['postid']); + + switch ($oldpost['basetype']) { + case 'Q': + $event='q_flag'; + break; + + case 'A': + $event='a_flag'; + break; + + case 'C': + $event='c_flag'; + break; + } + + $post=qa_db_select_with_pending(qa_db_full_post_selectspec(null, $oldpost['postid'])); + + qa_report_event($event, $userid, $handle, $cookieid, array( + 'postid' => $oldpost['postid'], + 'oldpost' => $oldpost, + 'flagcount' => $post['flagcount'], + 'questionid' => $question['postid'], + 'question' => $question, + )); + + return ($post['flagcount']>=qa_opt('flagging_hide_after')) && !$post['hidden']; + } + + + function qa_flag_clear($oldpost, $userid, $handle, $cookieid) +/* + Clear (application level) a flag on $oldpost by $userid (with $handle and $cookieid). + Handles recounting and event reports as appropriate. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-db-votes.php'; + require_once QA_INCLUDE_DIR.'qa-app-limits.php'; + + qa_db_userflag_set($oldpost['postid'], $userid, false); + qa_db_post_recount_flags($oldpost['postid']); + + switch ($oldpost['basetype']) { + case 'Q': + $event='q_unflag'; + break; + + case 'A': + $event='a_unflag'; + break; + + case 'C': + $event='c_unflag'; + break; + } + + qa_report_event($event, $userid, $handle, $cookieid, array( + 'postid' => $oldpost['postid'], + 'oldpost' => $oldpost, + )); + } + + + function qa_flags_clear_all($oldpost, $userid, $handle, $cookieid) +/* + Clear (application level) all flags on $oldpost by $userid (with $handle and $cookieid). + Handles recounting and event reports as appropriate. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-db-votes.php'; + require_once QA_INCLUDE_DIR.'qa-app-limits.php'; + + qa_db_userflags_clear_all($oldpost['postid']); + qa_db_post_recount_flags($oldpost['postid']); + + switch ($oldpost['basetype']) { + case 'Q': + $event='q_clearflags'; + break; + + case 'A': + $event='a_clearflags'; + break; + + case 'C': + $event='c_clearflags'; + break; + } + + qa_report_event($event, $userid, $handle, $cookieid, array( + 'postid' => $oldpost['postid'], + 'oldpost' => $oldpost, + )); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-base.php b/qa-include/qa-base.php new file mode 100644 index 000000000..92e3c2dfb --- /dev/null +++ b/qa-include/qa-base.php @@ -0,0 +1,1320 @@ + $checkvalue) + if (isset($keyprotect[$checkkey])) + qa_fatal_error('My superglobals are not for overriding'); + else + unset($GLOBALS[$checkkey]); + } + } + + + function qa_initialize_constants_1() +/* + First stage of setting up Q2A constants, before (if necessary) loading WordPress integration +*/ + { + global $qa_request_map; + + define('QA_CATEGORY_DEPTH', 4); // you can't change this number! + + if (!defined('QA_BASE_DIR')) + define('QA_BASE_DIR', dirname(dirname(__FILE__)).'/'); // try out best if not set in index.php or qa-index.php - won't work with symbolic links + + define('QA_EXTERNAL_DIR', QA_BASE_DIR.'qa-external/'); + define('QA_INCLUDE_DIR', QA_BASE_DIR.'qa-include/'); + define('QA_LANG_DIR', QA_BASE_DIR.'qa-lang/'); + define('QA_THEME_DIR', QA_BASE_DIR.'qa-theme/'); + define('QA_PLUGIN_DIR', QA_BASE_DIR.'qa-plugin/'); + + if (!file_exists(QA_BASE_DIR.'qa-config.php')) + qa_fatal_error('The config file could not be found. Please read the instructions in qa-config-example.php.'); + + require_once QA_BASE_DIR.'qa-config.php'; + + $qa_request_map=is_array(@$QA_CONST_PATH_MAP) ? $QA_CONST_PATH_MAP : array(); + + if (defined('QA_WORDPRESS_INTEGRATE_PATH') && strlen(QA_WORDPRESS_INTEGRATE_PATH)) { + define('QA_FINAL_WORDPRESS_INTEGRATE_PATH', QA_WORDPRESS_INTEGRATE_PATH.((substr(QA_WORDPRESS_INTEGRATE_PATH, -1)=='/') ? '' : '/')); + define('QA_WORDPRESS_LOAD_FILE', QA_FINAL_WORDPRESS_INTEGRATE_PATH.'wp-load.php'); + + if (!is_readable(QA_WORDPRESS_LOAD_FILE)) + qa_fatal_error('Could not find wp-load.php file for WordPress integration - please check QA_WORDPRESS_INTEGRATE_PATH in qa-config.php'); + } + } + + + function qa_initialize_constants_2() +/* + Second stage of setting up Q2A constants, after (if necessary) loading WordPress integration +*/ + { + + // Default values if not set in qa-config.php + + @define('QA_COOKIE_DOMAIN', ''); + @define('QA_HTML_COMPRESSION', true); + @define('QA_MAX_LIMIT_START', 19999); + @define('QA_IGNORED_WORDS_FREQ', 10000); + @define('QA_ALLOW_UNINDEXED_QUERIES', false); + @define('QA_OPTIMIZE_LOCAL_DB', false); + @define('QA_OPTIMIZE_DISTANT_DB', false); + @define('QA_PERSISTENT_CONN_DB', false); + @define('QA_DEBUG_PERFORMANCE', false); + + // More for WordPress integration + + if (defined('QA_FINAL_WORDPRESS_INTEGRATE_PATH')) { + define('QA_FINAL_MYSQL_HOSTNAME', DB_HOST); + define('QA_FINAL_MYSQL_USERNAME', DB_USER); + define('QA_FINAL_MYSQL_PASSWORD', DB_PASSWORD); + define('QA_FINAL_MYSQL_DATABASE', DB_NAME); + define('QA_FINAL_EXTERNAL_USERS', true); + + // Undo WordPress's addition of magic quotes to various things (leave $_COOKIE as is since WP code might need that) + foreach ($_GET as $key => $value) + $_GET[$key]=strtr(stripslashes($value), array('\\\'' => '\'', '\"' => '"')); // also compensate for WordPress's .htaccess file + + foreach ($_POST as $key => $value) + $_POST[$key]=stripslashes($value); + + $_SERVER['PHP_SELF']=stripslashes($_SERVER['PHP_SELF']); + + } else { + define('QA_FINAL_MYSQL_HOSTNAME', QA_MYSQL_HOSTNAME); + define('QA_FINAL_MYSQL_USERNAME', QA_MYSQL_USERNAME); + define('QA_FINAL_MYSQL_PASSWORD', QA_MYSQL_PASSWORD); + define('QA_FINAL_MYSQL_DATABASE', QA_MYSQL_DATABASE); + define('QA_FINAL_EXTERNAL_USERS', QA_EXTERNAL_USERS); + } + + // Possible URL schemes for Q2A and the string used for url scheme testing + + define('QA_URL_FORMAT_INDEX', 0); // http://...../index.php/123/why-is-the-sky-blue + define('QA_URL_FORMAT_NEAT', 1); // http://...../123/why-is-the-sky-blue [requires .htaccess] + define('QA_URL_FORMAT_PARAM', 3); // http://...../?qa=123/why-is-the-sky-blue + define('QA_URL_FORMAT_PARAMS', 4); // http://...../?qa=123&qa_1=why-is-the-sky-blue + define('QA_URL_FORMAT_SAFEST', 5); // http://...../index.php?qa=123&qa_1=why-is-the-sky-blue + + define('QA_URL_TEST_STRING', '$&-_~#%\\@^*()=!()][`\';:|".{},<>?# π§½Жש'); // tests escaping, spaces, quote slashing and unicode - but not + and / + } + + + function qa_initialize_modularity() +/* + Gets everything ready to start using modules, layers and overrides +*/ + { + global $qa_modules, $qa_layers, $qa_override_files, $qa_overrides, $qa_direct; + + $qa_modules=array(); + $qa_layers=array(); + $qa_override_files=array(); + $qa_overrides=array(); + $qa_direct=array(); + } + + + function qa_register_core_modules() +/* + Register all modules that come as part of the Q2A core (as opposed to plugins) +*/ + { + qa_register_module('filter', 'qa-filter-basic.php', 'qa_filter_basic', ''); + qa_register_module('editor', 'qa-editor-basic.php', 'qa_editor_basic', ''); + qa_register_module('viewer', 'qa-viewer-basic.php', 'qa_viewer_basic', ''); + qa_register_module('event', 'qa-event-limits.php', 'qa_event_limits', 'Q2A Event Limits'); + qa_register_module('event', 'qa-event-notify.php', 'qa_event_notify', 'Q2A Event Notify'); + qa_register_module('event', 'qa-event-updates.php', 'qa_event_updates', 'Q2A Event Updates'); + qa_register_module('search', 'qa-search-basic.php', 'qa_search_basic', ''); + qa_register_module('widget', 'qa-widget-activity-count.php', 'qa_activity_count', 'Activity Count'); + qa_register_module('widget', 'qa-widget-ask-box.php', 'qa_ask_box', 'Ask Box'); + qa_register_module('widget', 'qa-widget-related-qs.php', 'qa_related_qs', 'Related Questions'); + } + + + function qa_load_plugin_files() +/* + Load all the qa-plugin.php files from plugins that are compatible with this version of Q2A +*/ + { + global $qa_plugin_directory, $qa_plugin_urltoroot; + + $pluginfiles=glob(QA_PLUGIN_DIR.'*/qa-plugin.php'); + + foreach ($pluginfiles as $pluginfile) + if (file_exists($pluginfile)) { + if (preg_match('/Plugin[ \t]*Minimum[ \t]*Question2Answer[ \t]*Version\:[ \t]*([0-9\.]+)\s/i', file_get_contents($pluginfile), $matches)) + if ( ((float)QA_VERSION>0) && ($matches[1]>(float)QA_VERSION) ) + continue; // skip plugin which requires a later version + + $qa_plugin_directory=dirname($pluginfile).'/'; + $qa_plugin_urltoroot=substr($qa_plugin_directory, strlen(QA_BASE_DIR)); + + require_once $pluginfile; + + $qa_plugin_directory=null; + $qa_plugin_urltoroot=null; + } + } + + + function qa_load_override_files() +/* + Apply all the function overrides in override files that have been registered by plugins +*/ + { + global $qa_override_files, $qa_overrides; + + $functionindex=array(); + + foreach ($qa_override_files as $index => $override) { + $functionsphp=file_get_contents($override['directory'].$override['include']); + + preg_match_all('/\Wfunction\s+(qa_[a-z_]+)\s*\(/im', $functionsphp, $rawmatches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE); + + $reversematches=array_reverse($rawmatches[1], true); // reverse so offsets remain correct as we step through + $postreplace=array(); + $suffix='_in_'.preg_replace('/[^A-Za-z0-9_]+/', '_', basename($override['include'])); + // include file name in defined function names to make debugging easier if there is an error + + foreach ($reversematches as $rawmatch) { + $function=strtolower($rawmatch[0]); + $position=$rawmatch[1]; + + if (isset($qa_overrides[$function])) + $postreplace[$function.'_base']=$qa_overrides[$function]; + + $newname=$function.'_override_'.(@++$functionindex[$function]).$suffix; + $functionsphp=substr_replace($functionsphp, $newname, $position, strlen($function)); + $qa_overrides[$function]=$newname; + } + + foreach ($postreplace as $oldname => $newname) + if (preg_match_all('/\W('.preg_quote($oldname).')\s*\(/im', $functionsphp, $matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) { + $searchmatches=array_reverse($matches[1]); + foreach ($searchmatches as $searchmatch) + $functionsphp=substr_replace($functionsphp, $newname, $searchmatch[1], strlen($searchmatch[0])); + } + + // echo '
'.htmlspecialchars($functionsphp).'
'; // to debug munged code + + eval('?'.'>'.$functionsphp); + } + } + + +// Functions for registering different varieties of Q2A modularity + + function qa_register_module($type, $include, $class, $name, $directory=QA_INCLUDE_DIR, $urltoroot=null) +/* + Register a module of $type named $name, whose class named $class is defined in file $include (or null if no include necessary) + If this module comes from a plugin, pass in the local plugin $directory and the $urltoroot relative url for that directory +*/ + { + global $qa_modules; + + $previous=@$qa_modules[$type][$name]; + + if (isset($previous)) + qa_fatal_error('A '.$type.' module named '.$name.' already exists. Please check there are no duplicate plugins. '. + "\n\nModule 1: ".$previous['directory'].$previous['include']."\nModule 2: ".$directory.$include); + + $qa_modules[$type][$name]=array( + 'directory' => $directory, + 'urltoroot' => $urltoroot, + 'include' => $include, + 'class' => $class, + ); + } + + + function qa_register_layer($include, $name, $directory=QA_INCLUDE_DIR, $urltoroot=null) +/* + Register a layer named $name, defined in file $include. If this layer comes from a plugin (as all currently do), + pass in the local plugin $directory and the $urltoroot relative url for that directory +*/ + { + global $qa_layers; + + $previous=@$qa_layers[$name]; + + if (isset($previous)) + qa_fatal_error('A layer named '.$name.' already exists. Please check there are no duplicate plugins. '. + "\n\nLayer 1: ".$previous['directory'].$previous['include']."\nLayer 2: ".$directory.$include); + + $qa_layers[$name]=array( + 'directory' => $directory, + 'urltoroot' => $urltoroot, + 'include' => $include, + ); + } + + + function qa_register_overrides($include, $directory=QA_INCLUDE_DIR, $urltoroot=null) +/* + Register a file $include containing override functions. If this file comes from a plugin (as all currently do), + pass in the local plugin $directory and the $urltoroot relative url for that directory +*/ + { + global $qa_override_files; + + $qa_override_files[]=array( + 'directory' => $directory, + 'urltoroot' => $urltoroot, + 'include' => $include + ); + } + + + function qa_register_phrases($pattern, $name) +/* + Register a set of language phrases, which should be accessed by the prefix $name/ in the qa_lang_*() functions. + Pass in the $pattern representing the PHP files that define these phrases, where * in the pattern is replaced with + the language code (e.g. 'fr') and/or 'default'. These files should be formatted like Q2A's qa-lang-*.php files. +*/ + { + global $qa_lang_file_pattern; + + if (file_exists(QA_INCLUDE_DIR.'qa-lang-'.$name.'.php')) + qa_fatal_error('The name "'.$name.'" for phrases is reserved and cannot be used by plugins.'."\n\nPhrases: ".$pattern); + + if (isset($qa_lang_file_pattern[$name])) + qa_fatal_error('A set of phrases named '.$name.' already exists. Please check there are no duplicate plugins. '. + "\n\nPhrases 1: ".$qa_lang_file_pattern[$name]."\nPhrases 2: ".$pattern); + + $qa_lang_file_pattern[$name]=$pattern; + } + + +// Function for registering varieties of Q2A modularity, which are (only) called from qa-plugin.php files + + function qa_register_plugin_module($type, $include, $class, $name) + +/* + Register a plugin module of $type named $name, whose class named $class is defined in file $include (or null if no include necessary) + This function relies on some global variable values and can only be called from a plugin's qa-plugin.php file +*/ + { + global $qa_plugin_directory, $qa_plugin_urltoroot; + + if (empty($qa_plugin_directory) || empty($qa_plugin_urltoroot)) + qa_fatal_error('qa_register_plugin_module() can only be called from a plugin qa-plugin.php file'); + + qa_register_module($type, $include, $class, $name, $qa_plugin_directory, $qa_plugin_urltoroot); + } + + + function qa_register_plugin_layer($include, $name) +/* + Register a plugin layer named $name, defined in file $include. Can only be called from a plugin's qa-plugin.php file +*/ + { + global $qa_plugin_directory, $qa_plugin_urltoroot; + + if (empty($qa_plugin_directory) || empty($qa_plugin_urltoroot)) + qa_fatal_error('qa_register_plugin_layer() can only be called from a plugin qa-plugin.php file'); + + qa_register_layer($include, $name, $qa_plugin_directory, $qa_plugin_urltoroot); + } + + + function qa_register_plugin_overrides($include) +/* + Register a plugin file $include containing override functions. Can only be called from a plugin's qa-plugin.php file +*/ + { + global $qa_plugin_directory, $qa_plugin_urltoroot; + + if (empty($qa_plugin_directory) || empty($qa_plugin_urltoroot)) + qa_fatal_error('qa_register_plugin_layer() can only be called from a plugin qa-plugin.php file'); + + qa_register_overrides($include, $qa_plugin_directory, $qa_plugin_urltoroot); + } + + + function qa_register_plugin_phrases($pattern, $name) +/* + Register a file name $pattern within a plugin directory containing language phrases accessed by the prefix $name +*/ + { + global $qa_plugin_directory, $qa_plugin_urltoroot; + + if (empty($qa_plugin_directory) || empty($qa_plugin_urltoroot)) + qa_fatal_error('qa_register_plugin_phrases() can only be called from a plugin qa-plugin.php file'); + + qa_register_phrases($qa_plugin_directory.$pattern, $name); + } + + +// Low-level functions used throughout Q2A + + function qa_call($function, $args) + { + switch (count($args)) { // call_user_func_array(...) is very slow, so we break out most cases + case 0: return $function(); + case 1: return $function($args[0]); + case 2: return $function($args[0], $args[1]); + case 3: return $function($args[0], $args[1], $args[2]); + case 4: return $function($args[0], $args[1], $args[2], $args[3]); + case 5: return $function($args[0], $args[1], $args[2], $args[3], $args[4]); + } + + return call_user_func_array($function, $args); + } + + + function qa_to_override($function) + { + global $qa_overrides, $qa_direct; + + if (strpos($function, '_override_')!==false) + qa_fatal_error('Override functions should not be calling qa_to_override()!'); + + if (isset($qa_overrides[$function])) { + if (@$qa_direct[$function]) + unset($qa_direct[$function]); // bypass the override just this once + else + return $qa_overrides[$function]; + } + + return null; + } + + + function qa_call_override($function, $args) + { + global $qa_overrides; + + if (strpos($function, '_override_')!==false) + qa_fatal_error('Override functions should not be calling qa_call_override()!'); + + if (!function_exists($function.'_base')) // define the base function the first time that it's needed + eval('function '.$function.'_base() { global $qa_direct; $qa_direct[\''.$function.'\']=true; return qa_call(\''.$function.'\', $args=func_get_args()); }'); + + return qa_call($qa_overrides[$function], $args); + } + + + function qa_exit($reason=null) + { + qa_report_process_stage('shutdown', $reason); + exit; + } + + + function qa_fatal_error($message) +/* + Display $message in the browser, write it to server error log, and then stop abruptly +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + echo 'Question2Answer fatal error:

'.qa_html($message, true).'

'; + @error_log('PHP Question2Answer fatal error: '.$message); + echo '

Stack trace:

'; + + $backtrace=array_reverse(array_slice(debug_backtrace(), 1)); + foreach ($backtrace as $trace) + echo ''. + qa_html(@$trace['function'].'() '.@$trace['file'].':'.@$trace['line']).'
'; + + + qa_exit('error'); + } + + +// Functions for listing, loading and getting info on modules + + function qa_list_module_types() +/* + Return an array of all the module types for which at least one module has been registered +*/ + { + global $qa_modules; + + return array_keys($qa_modules); + } + + + function qa_list_modules($type) +/* + Return a list of names of registered modules of $type +*/ + { + global $qa_modules; + + return is_array(@$qa_modules[$type]) ? array_keys($qa_modules[$type]) : array(); + } + + + function qa_get_module_info($type, $name) +/* + Return an array containing information about the module of $type named $name +*/ + { + global $qa_modules; + return @$qa_modules[$type][$name]; + } + + + function qa_load_module($type, $name) +/* + Return an instantiated class for module of $type named $name, whose functions can be called, or null if it doesn't exist +*/ + { + global $qa_modules; + + $module=@$qa_modules[$type][$name]; + + if (is_array($module)) { + if (isset($module['object'])) + return $module['object']; + + if (strlen(@$module['include'])) + require_once $module['directory'].$module['include']; + + if (strlen(@$module['class'])) { + $object=new $module['class']; + + if (method_exists($object, 'load_module')) + $object->load_module($module['directory'], qa_path_to_root().$module['urltoroot']); + + $qa_modules[$type][$name]['object']=$object; + return $object; + } + } + + return null; + } + + + function qa_load_modules_with($type, $method) +/* + Return an array of instantiated clases for modules of $type which have defined $method + (other modules of that type are also loaded but not included in the returned array) +*/ + { + $modules=array(); + + $trynames=qa_list_modules($type); + + foreach ($trynames as $tryname) { + $module=qa_load_module($type, $tryname); + + if (method_exists($module, $method)) + $modules[$tryname]=$module; + } + + return $modules; + } + + +// HTML and Javascript escaping and sanitization + + function qa_html($string, $multiline=false) +/* + Return HTML representation of $string, work well with blocks of text if $multiline is true +*/ + { + $html=htmlspecialchars((string)$string); + + if ($multiline) { + $html=preg_replace('/\r\n?/', "\n", $html); + $html=preg_replace('/(?<=\s) /', ' ', $html); + $html=str_replace("\t", '    ', $html); + $html=nl2br($html); + } + + return $html; + } + + + function qa_sanitize_html($html, $linksnewwindow=false) +/* + Return $html after ensuring it is safe, i.e. removing Javascripts and the like - uses htmLawed library + Links open in a new window if $linksnewwindow is true +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once 'qa-htmLawed.php'; + + global $qa_sanitize_html_newwindow; + + $qa_sanitize_html_newwindow=$linksnewwindow; + + $safe=htmLawed($html, array( + 'safe' => 1, + 'elements' => '*+embed+object', + 'schemes' => 'href: aim, feed, file, ftp, gopher, http, https, irc, mailto, news, nntp, sftp, ssh, telnet; *:file, http, https; style: !; classid:clsid', + 'keep_bad' => 0, + 'anti_link_spam' => array('/.*/', ''), + 'hook_tag' => 'qa_sanitize_html_hook_tag', + )); + + return $safe; + } + + + function qa_sanitize_html_hook_tag($element, $attributes) +/* + htmLawed hook function used to process tags in qa_sanitize_html(...) +*/ + { + global $qa_sanitize_html_newwindow; + + if ( ($element=='param') && (trim(strtolower(@$attributes['name']))=='allowscriptaccess') ) + $attributes['name']='allowscriptaccess_denied'; + + if ($element=='embed') + unset($attributes['allowscriptaccess']); + + if (($element=='a') && isset($attributes['href']) && $qa_sanitize_html_newwindow) + $attributes['target']='_blank'; + + $html='<'.$element; + foreach ($attributes as $key => $value) + $html.=' '.$key.'="'.$value.'"'; + + return $html.'>'; + } + + + function qa_js($value, $forcequotes=false) +/* + Return JavaScript representation of $value, putting in quotes if non-numeric or if $forcequote is true +*/ + { + if (is_numeric($value) && !$forcequotes) + return $value; + else + return "'".strtr($value, array( + "'" => "\\'", + '/' => '\\/', + '\\' => '\\\\', + "\n" => "\\n", + "\r" => "\\n", + ))."'"; + } + + +// Finding out more about the current request + + function qa_set_request($request, $relativeroot, $usedformat=null) +/* + Inform Q2A that the current request is $request (slash-separated, independent of the url scheme chosen), + that the relative path to the Q2A root apperas to be $relativeroot, and the url scheme appears to be $usedformat +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_request, $qa_root_url_relative, $qa_used_url_format; + + $qa_request=$request; + $qa_root_url_relative=$relativeroot; + $qa_used_url_format=$usedformat; + } + + + function qa_request() +/* + Returns the current Q2A request (slash-separated, independent of the url scheme chosen) +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_request; + return $qa_request; + } + + + function qa_request_part($part) +/* + Returns the indexed $part (as separated by slashes) of the current Q2A request, or null if it doesn't exist +*/ + { + $parts=explode('/', qa_request()); + return @$parts[$part]; + } + + + function qa_request_parts($start=0) +/* + Returns an array of parts (as separated by slashes) of the current Q2A request, starting at part $start +*/ + { + return array_slice(explode('/', qa_request()), $start); + } + + + function qa_gpc_to_string($string) +/* + Return string for incoming GET/POST/COOKIE value, stripping slashes if appropriate +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + return get_magic_quotes_gpc() ? stripslashes($string) : $string; + } + + + function qa_string_to_gpc($string) +/* + Return string with slashes added, if appropriate for later removal by qa_gpc_to_string() +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + return get_magic_quotes_gpc() ? addslashes($string) : $string; + } + + + function qa_get($field) +/* + Return string for incoming GET field, or null if it's not defined +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + return isset($_GET[$field]) ? qa_gpc_to_string($_GET[$field]) : null; + } + + + function qa_post_text($field) +/* + Return string for incoming POST field, or null if it's not defined. + While we're at it, trim() surrounding white space and converted to Unix line endings. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + return isset($_POST[$field]) ? preg_replace('/\r\n?/', "\n", trim(qa_gpc_to_string($_POST[$field]))) : null; + } + + + function qa_clicked($name) +/* + Return true if form button $name was clicked (as TYPE=SUBMIT/IMAGE) to create this page request. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + return isset($_POST[$name]) || isset($_POST[$name.'_x']); + } + + + function qa_remote_ip_address() +/* + Return the remote IP (v4) address of the user accessing the site, if it's available, or null otherwise +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + return @$_SERVER['REMOTE_ADDR']; + } + + + function qa_is_http_post() +/* + Return true if we are responding to an HTTP POST request +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + return ($_SERVER['REQUEST_METHOD']=='POST') || !empty($_POST); + } + + + function qa_is_https_probably() +/* + Return true if we appear to be responding to a secure HTTP request (but hard to be sure) +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + return (@$_SERVER['HTTPS'] && ($_SERVER['HTTPS']!='off')) || (@$_SERVER['SERVER_PORT']==443); + } + + + function qa_is_human_probably() +/* + Return true if it appears the page request is coming from a human using a web browser, rather than a search engine + or other bot. Based on a whitelist of terms in user agents, this can easily be tricked by a scraper or bad bot. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + $useragent=@$_SERVER['HTTP_USER_AGENT']; + + return (strlen($useragent)==0) || qa_string_matches_one($useragent, array( + 'MSIE', 'Firefox', 'Chrome', 'Safari', 'Opera', 'Gecko', 'MIDP', 'PLAYSTATION', 'Teleca', + 'BlackBerry', 'UP.Browser', 'Polaris', 'MAUI_WAP_Browser', 'iPad', 'iPhone', 'iPod' + )); + } + + + function qa_is_mobile_probably() +/* + Return true if it appears that the page request is coming from a mobile client rather than a desktop/laptop web browser +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + // inspired by: http://dangerousprototypes.com/docs/PhpBB3_MOD:_Replacement_mobile_browser_detection_for_mobile_themes + + $mobileheaders=array('HTTP_X_OPERAMINI_PHONE', 'HTTP_X_WAP_PROFILE', 'HTTP_PROFILE'); + + foreach ($mobileheaders as $header) + if (isset($_SERVER[$header])) + return true; + + if (qa_string_matches_one(strtolower(@$_SERVER['HTTP_USER_AGENT']), array( + 'android', 'phone', 'mobile', 'windows ce', 'palm', ' mobi', 'wireless', 'blackberry', 'opera mini', 'symbian', + 'nokia', 'samsung', 'ericsson,', 'vodafone/', 'kindle', 'ipod', 'wap1.', 'wap2.', 'sony', 'sanyo', 'sharp', + 'panasonic', 'philips', 'pocketpc', 'avantgo', 'blazer', 'ipaq', 'up.browser', 'up.link', 'mmp', 'smartphone', 'midp' + ))) + return true; + + return qa_string_matches_one(strtolower(@$_SERVER['HTTP_ACCEPT']), array( + 'application/vnd.wap.xhtml+xml', 'text/vnd.wap.wml' + )); + } + + +// Language phrase support + + function qa_lang($identifier) +/* + Return the translated string for $identifier, unless we're using external translation logic. + This will retrieve the 'site_language' option so make sure you've already loaded/set that if + loading an option now will cause a problem (see issue in qa_default_option()). The part of + $identifier before the slash (/) replaces the * in the qa-lang-*.php file references, and the + part after the / is the key of the array element to be taken from that file's returned result. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_lang_file_pattern, $qa_phrases_custom, $qa_phrases_lang, $qa_phrases_default; + + list($group, $label)=explode('/', $identifier, 2); + + // First look for a custom phrase + + if (!isset($qa_phrases_custom[$group])) { // only load each language file once + $phrases=@include QA_LANG_DIR.'custom/qa-lang-'.$group.'.php'; // can tolerate missing file or directory + $qa_phrases_custom[$group]=is_array($phrases) ? $phrases : array(); + } + + if (isset($qa_phrases_custom[$group][$label])) + return $qa_phrases_custom[$group][$label]; + + // Second look for a localized file + + $languagecode=qa_opt('site_language'); + + if (strlen($languagecode)) { + if (!isset($qa_phrases_lang[$group])) { + if (isset($qa_lang_file_pattern[$group])) + $include=str_replace('*', $languagecode, $qa_lang_file_pattern[$group]); + else + $include=QA_LANG_DIR.$languagecode.'/qa-lang-'.$group.'.php'; + + $phrases=@include $include; + $qa_phrases_lang[$group]=is_array($phrases) ? $phrases : array(); + } + + if (isset($qa_phrases_lang[$group][$label])) + return $qa_phrases_lang[$group][$label]; + } + + // Finally load the default + + if (!isset($qa_phrases_default[$group])) { // only load each default language file once + if (isset($qa_lang_file_pattern[$group])) + $include=str_replace('*', 'default', $qa_lang_file_pattern[$group]); + else + $include=QA_INCLUDE_DIR.'qa-lang-'.$group.'.php'; + + $qa_phrases_default[$group]=@include_once $include; + } + + if (isset($qa_phrases_default[$group][$label])) + return $qa_phrases_default[$group][$label]; + + return '['.$identifier.']'; // as a last resort, return the identifier to help in development + } + + + function qa_lang_sub($identifier, $textparam, $symbol='^') +/* + Return the translated string for $identifier, with $symbol substituted for $textparam +*/ + { + return str_replace($symbol, $textparam, qa_lang($identifier)); + } + + + function qa_lang_html($identifier) +/* + Return the translated string for $identifier, converted to HTML +*/ + { + return qa_html(qa_lang($identifier)); + } + + + function qa_lang_html_sub($identifier, $htmlparam, $symbol='^') +/* + Return the translated string for $identifier converted to HTML, with $symbol *then* substituted for $htmlparam +*/ + { + return str_replace($symbol, $htmlparam, qa_lang_html($identifier)); + } + + + function qa_lang_html_sub_split($identifier, $htmlparam, $symbol='^') +/* + Return an array containing the translated string for $identifier converted to HTML, then split into three, + with $symbol substituted for $htmlparam in the 'data' element, and obvious 'prefix' and 'suffix' elements +*/ + { + $html=qa_lang_html($identifier); + + $symbolpos=strpos($html, $symbol); + if (!is_numeric($symbolpos)) + qa_fatal_error('Missing '.$symbol.' in language string '.$identifier); + + return array( + 'prefix' => substr($html, 0, $symbolpos), + 'data' => $htmlparam, + 'suffix' => substr($html, $symbolpos+1), + ); + } + + +// Request and path generation + + function qa_path_to_root() +/* + Return the relative path to the Q2A root (if it's was previously set by qa_set_request()) +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_root_url_relative; + return $qa_root_url_relative; + } + + + function qa_get_request_map() +/* + Return an array of mappings of Q2A requests, as defined in the qa-config.php file +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_request_map; + return $qa_request_map; + } + + + function qa_path($request, $params=null, $rooturl=null, $neaturls=null, $anchor=null) +/* + Return the relative URI path for $request, with optional parameters $params and $anchor. + Slashes in $request will not be urlencoded, but any other characters will. + If $neaturls is set, use that, otherwise retrieve the option. If $rooturl is set, take + that as the root of the Q2A site, otherwise use path to root which was set elsewhere. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + if (!isset($neaturls)) { + require_once QA_INCLUDE_DIR.'qa-app-options.php'; + $neaturls=qa_opt('neat_urls'); + } + + if (!isset($rooturl)) + $rooturl=qa_path_to_root(); + + $url=$rooturl.( (empty($rooturl) || (substr($rooturl, -1)=='/') ) ? '' : '/'); + $paramsextra=''; + + $requestparts=explode('/', $request); + $pathmap=qa_get_request_map(); + + if (isset($pathmap[$requestparts[0]])) + $requestparts[0]=$pathmap[$requestparts[0]]; + + foreach ($requestparts as $index => $requestpart) + $requestparts[$index]=urlencode($requestpart); + $requestpath=implode('/', $requestparts); + + switch ($neaturls) { + case QA_URL_FORMAT_INDEX: + if (!empty($request)) + $url.='index.php/'.$requestpath; + break; + + case QA_URL_FORMAT_NEAT: + $url.=$requestpath; + break; + + case QA_URL_FORMAT_PARAM: + if (!empty($request)) + $paramsextra='?qa='.$requestpath; + break; + + default: + $url.='index.php'; + + case QA_URL_FORMAT_PARAMS: + if (!empty($request)) + foreach ($requestparts as $partindex => $requestpart) + $paramsextra.=(strlen($paramsextra) ? '&' : '?').'qa'.($partindex ? ('_'.$partindex) : '').'='.$requestpart; + break; + } + + if (isset($params)) + foreach ($params as $key => $value) + $paramsextra.=(strlen($paramsextra) ? '&' : '?').urlencode($key).'='.urlencode((string)$value); + + return $url.$paramsextra.( empty($anchor) ? '' : '#'.urlencode($anchor) ); + } + + + function qa_path_html($request, $params=null, $rooturl=null, $neaturls=null, $anchor=null) +/* + Return HTML representation of relative URI path for $request - see qa_path() for other parameters +*/ + { + return qa_html(qa_path($request, $params, $rooturl, $neaturls, $anchor)); + } + + + function qa_q_request($questionid, $title) +/* + Return the Q2A request for question $questionid, and make it search-engine friendly based on $title, which is + shortened if necessary by removing shorter words which are generally less meaningful. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-app-options.php'; + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + $title=qa_block_words_replace($title, qa_get_block_words_preg()); + + $words=qa_string_to_words($title, true, false, false); + + $wordlength=array(); + foreach ($words as $index => $word) + $wordlength[$index]=qa_strlen($word); + + $remaining=qa_opt('q_urls_title_length'); + + if (array_sum($wordlength)>$remaining) { + arsort($wordlength, SORT_NUMERIC); // sort with longest words first + + foreach ($wordlength as $index => $length) { + if ($remaining>0) + $remaining-=$length; + else + unset($words[$index]); + } + } + + $title=implode('-', $words); + if (qa_opt('q_urls_remove_accents')) + $title=qa_string_remove_accents($title); + + return (int)$questionid.'/'.$title; + } + + + function qa_anchor($basetype, $postid) +/* + Return the HTML anchor that should be used for post $postid with $basetype (Q/A/C) +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + return strtolower($basetype).$postid; // used to be $postid only but this violated HTML spec + } + + + function qa_q_path($questionid, $title, $absolute=false, $showtype=null, $showid=null) +/* + Return the URL for question $questionid with $title, possibly using $absolute URLs. + To link to a specific answer or comment in a question, set $showtype and $showid accordingly. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + if ( (($showtype=='A') || ($showtype=='C')) && isset($showid)) { + $params=array('show' => $showid); // due to pagination + $anchor=qa_anchor($showtype, $showid); + + } else { + $params=null; + $anchor=null; + } + + return qa_path(qa_q_request($questionid, $title), $params, $absolute ? qa_opt('site_url') : null, null, $anchor); + } + + + function qa_q_path_html($questionid, $title, $absolute=false, $showtype=null, $showid=null) +/* + Return the HTML representation of the URL for $questionid - other parameters as for qa_q_path() +*/ + { + return qa_html(qa_q_path($questionid, $title, $absolute, $showtype, $showid)); + } + + + function qa_feed_request($feed) +/* + Return the request for the specified $feed +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + return 'feed/'.$feed.'.rss'; + } + + + function qa_self_html() +/* + Return an HTML-ready relative URL for the current page, preserving GET parameters - this is useful for ACTION in FORMs +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_used_url_format; + + return qa_path_html(qa_request(), $_GET, null, $qa_used_url_format); + } + + + function qa_path_form_html($request, $params=null, $rooturl=null, $neaturls=null, $anchor=null) +/* + Return HTML for hidden fields to insert into a
on the page. + This is needed because any parameters on the URL will be lost when the form is submitted. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $path=qa_path($request, $params, $rooturl, $neaturls, $anchor); + $formhtml=''; + + $questionpos=strpos($path, '?'); + if (is_numeric($questionpos)) { + $params=explode('&', substr($path, $questionpos+1)); + + foreach ($params as $param) + if (preg_match('/^([^\=]*)(\=(.*))?$/', $param, $matches)) + $formhtml.=''; + } + + return $formhtml; + } + + + function qa_redirect($request, $params=null, $rooturl=null, $neaturls=null, $anchor=null) +/* + Redirect the user's web browser to $request and then we're done - see qa_path() for other parameters +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + qa_redirect_raw(qa_path($request, $params, $rooturl, $neaturls, $anchor)); + } + + + function qa_redirect_raw($url) +/* + Redirect the user's web browser to page $path which is already a URL +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + header('Location: '.$url); + qa_exit('redirect'); + } + + +// General utilities + + function qa_retrieve_url($url) +/* + Return the contents of remote $url, using file_get_contents() if possible, otherwise curl functions +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $contents=@file_get_contents($url); + + if ((!strlen($contents)) && function_exists('curl_exec')) { // try curl as a backup (if allow_url_fopen not set) + $curl=curl_init($url); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + $contents=@curl_exec($curl); + curl_close($curl); + } + + return $contents; + } + + + function qa_opt($name, $value=null) +/* + Shortcut to get or set an option value without specifying database +*/ + { + global $qa_options_cache; + + if ((!isset($value)) && isset($qa_options_cache[$name])) + return $qa_options_cache[$name]; // quick shortcut to reduce calls to qa_get_options() + + require_once QA_INCLUDE_DIR.'qa-app-options.php'; + + if (isset($value)) + qa_set_option($name, $value); + + $options=qa_get_options(array($name)); + + return $options[$name]; + } + + +// Event and process stage reporting + + function qa_suspend_event_reports($suspend=true) +/* + Suspend the reporting of events to event modules via qa_report_event(...) if $suspend is + true, otherwise reinstate it. A counter is kept to allow multiple calls. +*/ + { + global $qa_event_reports_suspended; + + $qa_event_reports_suspended+=($suspend ? 1 : -1); + } + + + function qa_report_event($event, $userid, $handle, $cookieid, $params=array()) +/* + Send a notification of event $event by $userid, $handle and $cookieid to all event modules, with extra $params +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_event_reports_suspended; + + if ($qa_event_reports_suspended>0) + return; + + $eventmodules=qa_load_modules_with('event', 'process_event'); + foreach ($eventmodules as $eventmodule) + $eventmodule->process_event($event, $userid, $handle, $cookieid, $params); + } + + + function qa_report_process_stage($method) // can have extra params + { + global $qa_process_reports_suspended; + + if (@$qa_process_reports_suspended) + return; + + $qa_process_reports_suspended=true; // prevent loop, e.g. because of an error + + $args=array_slice(func_get_args(), 1); + + $processmodules=qa_load_modules_with('process', $method); + foreach ($processmodules as $processmodule) + call_user_func_array(array($processmodule, $method), $args); + + $qa_process_reports_suspended=null; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-blob.php b/qa-include/qa-blob.php new file mode 100644 index 000000000..ea2b8f3a4 --- /dev/null +++ b/qa-include/qa-blob.php @@ -0,0 +1,94 @@ + + + + + Question2Answer Language Check + + + +Dark red = important to review.
'; + echo 'Light red = probably safe to ignore.'; + + echo '

Checking US English files in qa-include...

'; + + $includefiles=glob(QA_INCLUDE_DIR.'qa-*.php'); + + $definite=array(); + $probable=array(); + $possible=array(); + $defined=array(); + $english=array(); + $backmap=array(); + $substitutions=array(); + + output_start_includes(); + + foreach ($includefiles as $includefile) { + $contents=file_get_contents($includefile); + + preg_match_all('/qa_lang[a-z_]*\s*\(\s*[\'\"]([a-z]+)\/([0-9a-z_]+)[\'\"]/', $contents, $matches, PREG_SET_ORDER); + + foreach ($matches as $matchparts) + if ($matchparts[2]=='date_month_') { // special case for month names + for ($month=1; $month<=12; $month++) + @$definite[$matchparts[1]][$matchparts[2].$month]++; + + } else + @$definite[$matchparts[1]][$matchparts[2]]++; + + preg_match_all('/[\'\"]([a-z]+)\/([0-9a-z_]+)[\'\"]/', $contents, $matches, PREG_SET_ORDER); + + foreach ($matches as $matchparts) + @$probable[$matchparts[1]][$matchparts[2]]++; + + if (preg_match('|/qa-include/qa-lang-([a-z]+)\.php$|', $includefile, $matches)) { // it's a lang file + $prefix=$matches[1]; + + output_reading_include($includefile); + $phrases=@include $includefile; + + foreach ($phrases as $key => $value) { + @$defined[$prefix][$key]++; + $english[$prefix][$key]=$value; + $backmap[$value][]=array('prefix' => $prefix, 'key' => $key); + $substitutions[$prefix][$key]=get_phrase_substitutions($value); + } + + } else { // it's a different file + preg_match_all('/[\'\"\/]([0-9a-z_]+)[\'\"]/', $contents, $matches, PREG_SET_ORDER); + + foreach ($matches as $matchparts) + @$possible[$matchparts[1]]++; + } + } + + output_finish_includes(); + + foreach ($definite as $key => $valuecount) + foreach ($valuecount as $value => $count) + if (!@$defined[$key][$value]) + output_lang_issue($key, $value, 'used by '.$count.' file/s but not defined'); + + foreach ($defined as $key => $valuecount) + foreach ($valuecount as $value => $count) + if ( (!@$definite[$key][$value]) && (!@$probable[$key][$value]) && (!@$possible[$value]) ) + output_lang_issue($key, $value, 'defined but apparently not used'); + + foreach ($backmap as $phrase => $where) + if (count($where)>1) + foreach ($where as $onewhere) + output_lang_issue($onewhere['prefix'], $onewhere['key'], 'contains the shared phrase "'.$phrase.'"', false); + + require_once QA_INCLUDE_DIR.'qa-app-admin.php'; + + $languages=qa_admin_language_options(); + unset($languages['']); + + foreach ($languages as $code => $language) { + echo '

Checking '.$language.' files in qa-lang/'.$code.'...

'; + + $langdefined=array(); + $langdifferent=array(); + $langsubstitutions=array(); + $langincludefiles=glob(QA_LANG_DIR.$code.'/qa-*.php'); + $langnewphrases=array(); + + output_start_includes(); + + foreach ($langincludefiles as $langincludefile) + if (preg_match('/qa-lang-([a-z]+)\.php$/', $langincludefile, $matches)) { // it's a lang file + $prefix=$matches[1]; + + output_reading_include($langincludefile); + $phrases=@include $langincludefile; + + foreach ($phrases as $key => $value) { + @$langdefined[$prefix][$key]++; + $langdifferent[$prefix][$key]=($value!=@$english[$prefix][$key]); + $langsubstitutions[$prefix][$key]=get_phrase_substitutions($value); + } + } + + output_finish_includes(); + + foreach ($langdefined as $key => $valuecount) + foreach ($valuecount as $value => $count) { + if (!@$defined[$key][$value]) + output_lang_issue($key, $value, 'defined but not in US English files'); + + elseif (!$langdifferent[$key][$value]) + output_lang_issue($key, $value, 'identical to US English files', false); + + else + foreach ($substitutions[$key][$value] as $substitution => $subcount) + if (!@$langsubstitutions[$key][$value][$substitution]) + output_lang_issue($key, $value, 'omitted the substitution '.$substitution); + elseif ($subcount > @$langsubstitutions[$key][$value][$substitution]) + output_lang_issue($key, $value, 'has fewer of the substitution '.$substitution); + } + + foreach ($defined as $key => $valuecount) { + $showaserror=!(($key=='admin') || ($key=='options') || ($code=='en-GB')); + + if (@$langdefined[$key]) { + if (count($langdefined[$key]) < (count($valuecount)/2)) { // only a few phrases defined + output_lang_issue($key, null, 'few translations provided so will use US English defaults', $showaserror); + + } else + foreach ($valuecount as $value => $count) + if (!@$langdefined[$key][$value]) { + output_lang_issue($key, $value, 'undefined so will use US English defaults', $showaserror); + $langnewphrases[$key][$value]=$english[$key][$value]; + } + } else + output_lang_issue($key, null, 'no translations provided so will use US English defaults', $showaserror); + } + + foreach ($langnewphrases as $prefix => $phrases) { + echo '

'.$language.' phrases to add to qa-lang/'.$code.'/qa-lang-'.$prefix.'.php:

'; + + echo 'Copy and paste this into the middle of qa-lang/'.$code.'/qa-lang-'.$prefix.'.php then translate the right-hand sides after the => symbol.'; + + echo '
';
+			
+			foreach ($phrases as $key => $value)
+				echo ''."\t\t'".$key."' => \"".strtr($value, array('\\' => '\\\\', '"' => '\"', '$' => '\$', "\n" => '\n', "\t" => '\t'))."\",\n";
+				
+			echo '
'; + } + } + + + function output_lang_issue($prefix, $key, $issue, $error=true) + { + echo ''; + + echo 'qa-lang-'.qa_html($prefix).'.php:'; + + if (strlen($key)) + echo "'".qa_html($key)."'"; + + echo '   '.qa_html($issue).'
'; + } + + + function output_start_includes() + { + global $oneread; + + $oneread=false; + + echo '

Reading: '; + } + + + function output_reading_include($file) + { + global $oneread; + + echo ($oneread ? ', ' : '').htmlspecialchars(basename($file)); + flush(); + + $oneread=true; + } + + + function output_finish_includes() + { + echo '

'; + } + + + echo '

Finished scanning for problems!

'; + +?> + + + \ No newline at end of file diff --git a/qa-include/qa-class.phpmailer.php b/qa-include/qa-class.phpmailer.php new file mode 100644 index 000000000..3e5dc4f96 --- /dev/null +++ b/qa-include/qa-class.phpmailer.php @@ -0,0 +1,1912 @@ +ContentType = 'text/html'; + } else { + $this->ContentType = 'text/plain'; + } + } + + /** + * Sets Mailer to send message using SMTP. + * @return void + */ + function IsSMTP() { + $this->Mailer = 'smtp'; + } + + /** + * Sets Mailer to send message using PHP mail() function. + * @return void + */ + function IsMail() { + $this->Mailer = 'mail'; + } + + /** + * Sets Mailer to send message using the $Sendmail program. + * @return void + */ + function IsSendmail() { + $this->Mailer = 'sendmail'; + } + + /** + * Sets Mailer to send message using the qmail MTA. + * @return void + */ + function IsQmail() { + $this->Sendmail = '/var/qmail/bin/sendmail'; + $this->Mailer = 'sendmail'; + } + + ///////////////////////////////////////////////// + // METHODS, RECIPIENTS + ///////////////////////////////////////////////// + + /** + * Adds a "To" address. + * @param string $address + * @param string $name + * @return void + */ + function AddAddress($address, $name = '') { + $cur = count($this->to); + $this->to[$cur][0] = trim($address); + $this->to[$cur][1] = $name; + } + + /** + * Adds a "Cc" address. Note: this function works + * with the SMTP mailer on win32, not with the "mail" + * mailer. + * @param string $address + * @param string $name + * @return void + */ + function AddCC($address, $name = '') { + $cur = count($this->cc); + $this->cc[$cur][0] = trim($address); + $this->cc[$cur][1] = $name; + } + + /** + * Adds a "Bcc" address. Note: this function works + * with the SMTP mailer on win32, not with the "mail" + * mailer. + * @param string $address + * @param string $name + * @return void + */ + function AddBCC($address, $name = '') { + $cur = count($this->bcc); + $this->bcc[$cur][0] = trim($address); + $this->bcc[$cur][1] = $name; + } + + /** + * Adds a "Reply-To" address. + * @param string $address + * @param string $name + * @return void + */ + function AddReplyTo($address, $name = '') { + $cur = count($this->ReplyTo); + $this->ReplyTo[$cur][0] = trim($address); + $this->ReplyTo[$cur][1] = $name; + } + + ///////////////////////////////////////////////// + // METHODS, MAIL SENDING + ///////////////////////////////////////////////// + + /** + * Creates message and assigns Mailer. If the message is + * not sent successfully then it returns false. Use the ErrorInfo + * variable to view description of the error. + * @return bool + */ + function Send() { + $header = ''; + $body = ''; + $result = true; + + if((count($this->to) + count($this->cc) + count($this->bcc)) < 1) { + $this->SetError($this->Lang('provide_address')); + return false; + } + + /* Set whether the message is multipart/alternative */ + if(!empty($this->AltBody)) { + $this->ContentType = 'multipart/alternative'; + } + + $this->error_count = 0; // reset errors + $this->SetMessageType(); + $header .= $this->CreateHeader(); + $body = $this->CreateBody(); + + if($body == '') { + return false; + } + + /* Choose the mailer */ + switch($this->Mailer) { + case 'sendmail': + $result = $this->SendmailSend($header, $body); + break; + case 'smtp': + $result = $this->SmtpSend($header, $body); + break; + case 'mail': + $result = $this->MailSend($header, $body); + break; + default: + $result = $this->MailSend($header, $body); + break; + //$this->SetError($this->Mailer . $this->Lang('mailer_not_supported')); + //$result = false; + //break; + } + + return $result; + } + + /** + * Sends mail using the $Sendmail program. + * @access private + * @return bool + */ + function SendmailSend($header, $body) { + if ($this->Sender != '') { + $sendmail = sprintf("%s -oi -f %s -t", escapeshellcmd($this->Sendmail), escapeshellarg($this->Sender)); + } else { + $sendmail = sprintf("%s -oi -t", escapeshellcmd($this->Sendmail)); + } + + if(!@$mail = popen($sendmail, 'w')) { + $this->SetError($this->Lang('execute') . $this->Sendmail); + return false; + } + + fputs($mail, $header); + fputs($mail, $body); + + $result = pclose($mail); + if (version_compare(phpversion(), '4.2.3') == -1) { + $result = $result >> 8 & 0xFF; + } + if($result != 0) { + $this->SetError($this->Lang('execute') . $this->Sendmail); + return false; + } + return true; + } + + /** + * Sends mail using the PHP mail() function. + * @access private + * @return bool + */ + function MailSend($header, $body) { + + $to = ''; + for($i = 0; $i < count($this->to); $i++) { + if($i != 0) { $to .= ', '; } + $to .= $this->AddrFormat($this->to[$i]); + } + + $toArr = explode(',', $to); // changed from split() by Gideon Greenspan to avoid PHP 5.3 warning + + $params = sprintf("-oi -f %s", $this->Sender); + if ($this->Sender != '' && strlen(ini_get('safe_mode')) < 1) { + $old_from = ini_get('sendmail_from'); + ini_set('sendmail_from', $this->Sender); + if ($this->SingleTo === true && count($toArr) > 1) { + foreach ($toArr as $key => $val) { + $rt = @mail($val, $this->EncodeHeader($this->SecureHeader($this->Subject)), $body, $header, $params); + } + } else { + $rt = @mail($to, $this->EncodeHeader($this->SecureHeader($this->Subject)), $body, $header, $params); + } + } else { + if ($this->SingleTo === true && count($toArr) > 1) { + foreach ($toArr as $key => $val) { + $rt = @mail($val, $this->EncodeHeader($this->SecureHeader($this->Subject)), $body, $header, $params); + } + } else { + $rt = @mail($to, $this->EncodeHeader($this->SecureHeader($this->Subject)), $body, $header); + } + } + + if (isset($old_from)) { + ini_set('sendmail_from', $old_from); + } + + if(!$rt) { + $this->SetError($this->Lang('instantiate')); + return false; + } + + return true; + } + + /** + * Sends mail via SMTP using PhpSMTP (Author: + * Chris Ryan). Returns bool. Returns false if there is a + * bad MAIL FROM, RCPT, or DATA input. + * @access private + * @return bool + */ + function SmtpSend($header, $body) { + include_once($this->PluginDir . 'qa-class.smtp.php'); // Question2Answer mod: changed file name + $error = ''; + $bad_rcpt = array(); + + if(!$this->SmtpConnect()) { + return false; + } + + $smtp_from = ($this->Sender == '') ? $this->From : $this->Sender; + if(!$this->smtp->Mail($smtp_from)) { + $error = $this->Lang('from_failed') . $smtp_from; + $this->SetError($error); + $this->smtp->Reset(); + return false; + } + + /* Attempt to send attach all recipients */ + for($i = 0; $i < count($this->to); $i++) { + if(!$this->smtp->Recipient($this->to[$i][0])) { + $bad_rcpt[] = $this->to[$i][0]; + } + } + for($i = 0; $i < count($this->cc); $i++) { + if(!$this->smtp->Recipient($this->cc[$i][0])) { + $bad_rcpt[] = $this->cc[$i][0]; + } + } + for($i = 0; $i < count($this->bcc); $i++) { + if(!$this->smtp->Recipient($this->bcc[$i][0])) { + $bad_rcpt[] = $this->bcc[$i][0]; + } + } + + if(count($bad_rcpt) > 0) { // Create error message + for($i = 0; $i < count($bad_rcpt); $i++) { + if($i != 0) { + $error .= ', '; + } + $error .= $bad_rcpt[$i]; + } + $error = $this->Lang('recipients_failed') . $error; + $this->SetError($error); + $this->smtp->Reset(); + return false; + } + + if(!$this->smtp->Data($header . $body)) { + $this->SetError($this->Lang('data_not_accepted')); + $this->smtp->Reset(); + return false; + } + if($this->SMTPKeepAlive == true) { + $this->smtp->Reset(); + } else { + $this->SmtpClose(); + } + + return true; + } + + /** + * Initiates a connection to an SMTP server. Returns false if the + * operation failed. + * @access private + * @return bool + */ + function SmtpConnect() { + if($this->smtp == NULL) { + $this->smtp = new SMTP(); + } + + $this->smtp->do_debug = $this->SMTPDebug; + $hosts = explode(';', $this->Host); + $index = 0; + $connection = ($this->smtp->Connected()); + + /* Retry while there is no connection */ + while($index < count($hosts) && $connection == false) { + $hostinfo = array(); + if(false /*eregi('^(.+):([0-9]+)$', $hosts[$index], $hostinfo)*/) { // Question2Answer mod: removed eregi due to deprecation + $host = $hostinfo[1]; + $port = $hostinfo[2]; + } else { + $host = $hosts[$index]; + $port = $this->Port; + } + + if($this->smtp->Connect(((!empty($this->SMTPSecure))?$this->SMTPSecure.'://':'').$host, $port, $this->Timeout)) { + if ($this->Helo != '') { + $this->smtp->Hello($this->Helo); + } else { + $this->smtp->Hello($this->ServerHostname()); + } + + $connection = true; + if($this->SMTPAuth) { + if(!$this->smtp->Authenticate($this->Username, $this->Password)) { + $this->SetError($this->Lang('authenticate')); + $this->smtp->Reset(); + $connection = false; + } + } + } + $index++; + } + if(!$connection) { + $this->SetError($this->Lang('connect_host')); + } + + return $connection; + } + + /** + * Closes the active SMTP session if one exists. + * @return void + */ + function SmtpClose() { + if($this->smtp != NULL) { + if($this->smtp->Connected()) { + $this->smtp->Quit(); + $this->smtp->Close(); + } + } + } + + /** + * Sets the language for all class error messages. Returns false + * if it cannot load the language file. The default language type + * is English. + * @param string $lang_type Type of language (e.g. Portuguese: "br") + * @param string $lang_path Path to the language file directory + * @access public + * @return bool + */ + function SetLanguage($lang_type, $lang_path = 'language/') { + if(file_exists($lang_path.'phpmailer.lang-'.$lang_type.'.php')) { + include($lang_path.'phpmailer.lang-'.$lang_type.'.php'); + } elseif (file_exists($lang_path.'phpmailer.lang-en.php')) { + include($lang_path.'phpmailer.lang-en.php'); + } else { + $PHPMAILER_LANG = array(); + $PHPMAILER_LANG["provide_address"] = 'You must provide at least one ' . + $PHPMAILER_LANG["mailer_not_supported"] = ' mailer is not supported.'; + $PHPMAILER_LANG["execute"] = 'Could not execute: '; + $PHPMAILER_LANG["instantiate"] = 'Could not instantiate mail function.'; + $PHPMAILER_LANG["authenticate"] = 'SMTP Error: Could not authenticate.'; + $PHPMAILER_LANG["from_failed"] = 'The following From address failed: '; + $PHPMAILER_LANG["recipients_failed"] = 'SMTP Error: The following ' . + $PHPMAILER_LANG["data_not_accepted"] = 'SMTP Error: Data not accepted.'; + $PHPMAILER_LANG["connect_host"] = 'SMTP Error: Could not connect to SMTP host.'; + $PHPMAILER_LANG["file_access"] = 'Could not access file: '; + $PHPMAILER_LANG["file_open"] = 'File Error: Could not open file: '; + $PHPMAILER_LANG["encoding"] = 'Unknown encoding: '; + $PHPMAILER_LANG["signing"] = 'Signing Error: '; + } + $this->language = $PHPMAILER_LANG; + + return true; + } + + ///////////////////////////////////////////////// + // METHODS, MESSAGE CREATION + ///////////////////////////////////////////////// + + /** + * Creates recipient headers. + * @access private + * @return string + */ + function AddrAppend($type, $addr) { + $addr_str = $type . ': '; + $addr_str .= $this->AddrFormat($addr[0]); + if(count($addr) > 1) { + for($i = 1; $i < count($addr); $i++) { + $addr_str .= ', ' . $this->AddrFormat($addr[$i]); + } + } + $addr_str .= $this->LE; + + return $addr_str; + } + + /** + * Formats an address correctly. + * @access private + * @return string + */ + function AddrFormat($addr) { + if(empty($addr[1])) { + $formatted = $this->SecureHeader($addr[0]); + } else { + $formatted = $this->EncodeHeader($this->SecureHeader($addr[1]), 'phrase') . " <" . $this->SecureHeader($addr[0]) . ">"; + } + + return $formatted; + } + + /** + * Wraps message for use with mailers that do not + * automatically perform wrapping and for quoted-printable. + * Original written by philippe. + * @access private + * @return string + */ + function WrapText($message, $length, $qp_mode = false) { + $soft_break = ($qp_mode) ? sprintf(" =%s", $this->LE) : $this->LE; + // If utf-8 encoding is used, we will need to make sure we don't + // split multibyte characters when we wrap + $is_utf8 = (strtolower($this->CharSet) == "utf-8"); + + $message = $this->FixEOL($message); + if (substr($message, -1) == $this->LE) { + $message = substr($message, 0, -1); + } + + $line = explode($this->LE, $message); + $message = ''; + for ($i=0 ;$i < count($line); $i++) { + $line_part = explode(' ', $line[$i]); + $buf = ''; + for ($e = 0; $e $length)) { + $space_left = $length - strlen($buf) - 1; + if ($e != 0) { + if ($space_left > 20) { + $len = $space_left; + if ($is_utf8) { + $len = $this->UTF8CharBoundary($word, $len); + } elseif (substr($word, $len - 1, 1) == "=") { + $len--; + } elseif (substr($word, $len - 2, 1) == "=") { + $len -= 2; + } + $part = substr($word, 0, $len); + $word = substr($word, $len); + $buf .= ' ' . $part; + $message .= $buf . sprintf("=%s", $this->LE); + } else { + $message .= $buf . $soft_break; + } + $buf = ''; + } + while (strlen($word) > 0) { + $len = $length; + if ($is_utf8) { + $len = $this->UTF8CharBoundary($word, $len); + } elseif (substr($word, $len - 1, 1) == "=") { + $len--; + } elseif (substr($word, $len - 2, 1) == "=") { + $len -= 2; + } + $part = substr($word, 0, $len); + $word = substr($word, $len); + + if (strlen($word) > 0) { + $message .= $part . sprintf("=%s", $this->LE); + } else { + $buf = $part; + } + } + } else { + $buf_o = $buf; + $buf .= ($e == 0) ? $word : (' ' . $word); + + if (strlen($buf) > $length and $buf_o != '') { + $message .= $buf_o . $soft_break; + $buf = $word; + } + } + } + $message .= $buf . $this->LE; + } + + return $message; + } + + /** + * Finds last character boundary prior to maxLength in a utf-8 + * quoted (printable) encoded string. + * Original written by Colin Brown. + * @access private + * @param string $encodedText utf-8 QP text + * @param int $maxLength find last character boundary prior to this length + * @return int + */ + function UTF8CharBoundary($encodedText, $maxLength) { + $foundSplitPos = false; + $lookBack = 3; + while (!$foundSplitPos) { + $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack); + $encodedCharPos = strpos($lastChunk, "="); + if ($encodedCharPos !== false) { + // Found start of encoded character byte within $lookBack block. + // Check the encoded byte value (the 2 chars after the '=') + $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2); + $dec = hexdec($hex); + if ($dec < 128) { // Single byte character. + // If the encoded char was found at pos 0, it will fit + // otherwise reduce maxLength to start of the encoded char + $maxLength = ($encodedCharPos == 0) ? $maxLength : + $maxLength - ($lookBack - $encodedCharPos); + $foundSplitPos = true; + } elseif ($dec >= 192) { // First byte of a multi byte character + // Reduce maxLength to split at start of character + $maxLength = $maxLength - ($lookBack - $encodedCharPos); + $foundSplitPos = true; + } elseif ($dec < 192) { // Middle byte of a multi byte character, look further back + $lookBack += 3; + } + } else { + // No encoded character found + $foundSplitPos = true; + } + } + return $maxLength; + } + + /** + * Set the body wrapping. + * @access private + * @return void + */ + function SetWordWrap() { + if($this->WordWrap < 1) { + return; + } + + switch($this->message_type) { + case 'alt': + /* fall through */ + case 'alt_attachments': + $this->AltBody = $this->WrapText($this->AltBody, $this->WordWrap); + break; + default: + $this->Body = $this->WrapText($this->Body, $this->WordWrap); + break; + } + } + + /** + * Assembles message header. + * @access private + * @return string + */ + function CreateHeader() { + $result = ''; + + /* Set the boundaries */ + $uniq_id = md5(uniqid(time())); + $this->boundary[1] = 'b1_' . $uniq_id; + $this->boundary[2] = 'b2_' . $uniq_id; + + $result .= $this->HeaderLine('Date', $this->RFCDate()); + if($this->Sender == '') { + $result .= $this->HeaderLine('Return-Path', trim($this->From)); + } else { + $result .= $this->HeaderLine('Return-Path', trim($this->Sender)); + } + + /* To be created automatically by mail() */ + if($this->Mailer != 'mail') { + if(count($this->to) > 0) { + $result .= $this->AddrAppend('To', $this->to); + } elseif (count($this->cc) == 0) { + $result .= $this->HeaderLine('To', 'undisclosed-recipients:;'); + } + } + + $from = array(); + $from[0][0] = trim($this->From); + $from[0][1] = $this->FromName; + $result .= $this->AddrAppend('From', $from); + + /* sendmail and mail() extract Cc from the header before sending */ + if((($this->Mailer == 'sendmail') || ($this->Mailer == 'mail')) && (count($this->cc) > 0)) { + $result .= $this->AddrAppend('Cc', $this->cc); + } + + /* sendmail and mail() extract Bcc from the header before sending */ + if((($this->Mailer == 'sendmail') || ($this->Mailer == 'mail')) && (count($this->bcc) > 0)) { + $result .= $this->AddrAppend('Bcc', $this->bcc); + } + + if(count($this->ReplyTo) > 0) { + $result .= $this->AddrAppend('Reply-To', $this->ReplyTo); + } + + /* mail() sets the subject itself */ + if($this->Mailer != 'mail') { + $result .= $this->HeaderLine('Subject', $this->EncodeHeader($this->SecureHeader($this->Subject))); + } + + if($this->MessageID != '') { + $result .= $this->HeaderLine('Message-ID',$this->MessageID); + } else { + $result .= sprintf("Message-ID: <%s@%s>%s", $uniq_id, $this->ServerHostname(), $this->LE); + } + $result .= $this->HeaderLine('X-Priority', $this->Priority); + $result .= $this->HeaderLine('X-Mailer', 'PHPMailer (phpmailer.sourceforge.net) [version ' . $this->Version . ']'); + + if($this->ConfirmReadingTo != '') { + $result .= $this->HeaderLine('Disposition-Notification-To', '<' . trim($this->ConfirmReadingTo) . '>'); + } + + // Add custom headers + for($index = 0; $index < count($this->CustomHeader); $index++) { + $result .= $this->HeaderLine(trim($this->CustomHeader[$index][0]), $this->EncodeHeader(trim($this->CustomHeader[$index][1]))); + } + if (!$this->sign_key_file) { + $result .= $this->HeaderLine('MIME-Version', '1.0'); + $result .= $this->GetMailMIME(); + } + + return $result; + } + + /** + * Returns the message MIME. + * @access private + * @return string + */ + function GetMailMIME() { + $result = ''; + switch($this->message_type) { + case 'plain': + $result .= $this->HeaderLine('Content-Transfer-Encoding', $this->Encoding); + $result .= sprintf("Content-Type: %s; charset=\"%s\"", $this->ContentType, $this->CharSet); + break; + case 'attachments': + /* fall through */ + case 'alt_attachments': + if($this->InlineImageExists()){ + $result .= sprintf("Content-Type: %s;%s\ttype=\"text/html\";%s\tboundary=\"%s\"%s", 'multipart/related', $this->LE, $this->LE, $this->boundary[1], $this->LE); + } else { + $result .= $this->HeaderLine('Content-Type', 'multipart/mixed;'); + $result .= $this->TextLine("\tboundary=\"" . $this->boundary[1] . '"'); + } + break; + case 'alt': + $result .= $this->HeaderLine('Content-Type', 'multipart/alternative;'); + $result .= $this->TextLine("\tboundary=\"" . $this->boundary[1] . '"'); + break; + } + + if($this->Mailer != 'mail') { + $result .= $this->LE.$this->LE; + } + + return $result; + } + + /** + * Assembles the message body. Returns an empty string on failure. + * @access private + * @return string + */ + function CreateBody() { + $result = ''; + if ($this->sign_key_file) { + $result .= $this->GetMailMIME(); + } + + $this->SetWordWrap(); + + switch($this->message_type) { + case 'alt': + $result .= $this->GetBoundary($this->boundary[1], '', 'text/plain', ''); + $result .= $this->EncodeString($this->AltBody, $this->Encoding); + $result .= $this->LE.$this->LE; + $result .= $this->GetBoundary($this->boundary[1], '', 'text/html', ''); + $result .= $this->EncodeString($this->Body, $this->Encoding); + $result .= $this->LE.$this->LE; + $result .= $this->EndBoundary($this->boundary[1]); + break; + case 'plain': + $result .= $this->EncodeString($this->Body, $this->Encoding); + break; + case 'attachments': + $result .= $this->GetBoundary($this->boundary[1], '', '', ''); + $result .= $this->EncodeString($this->Body, $this->Encoding); + $result .= $this->LE; + $result .= $this->AttachAll(); + break; + case 'alt_attachments': + $result .= sprintf("--%s%s", $this->boundary[1], $this->LE); + $result .= sprintf("Content-Type: %s;%s" . "\tboundary=\"%s\"%s", 'multipart/alternative', $this->LE, $this->boundary[2], $this->LE.$this->LE); + $result .= $this->GetBoundary($this->boundary[2], '', 'text/plain', '') . $this->LE; // Create text body + $result .= $this->EncodeString($this->AltBody, $this->Encoding); + $result .= $this->LE.$this->LE; + $result .= $this->GetBoundary($this->boundary[2], '', 'text/html', '') . $this->LE; // Create the HTML body + $result .= $this->EncodeString($this->Body, $this->Encoding); + $result .= $this->LE.$this->LE; + $result .= $this->EndBoundary($this->boundary[2]); + $result .= $this->AttachAll(); + break; + } + + if($this->IsError()) { + $result = ''; + } else if ($this->sign_key_file) { + $file = tempnam("", "mail"); + $fp = fopen($file, "w"); + fwrite($fp, $result); + fclose($fp); + $signed = tempnam("", "signed"); + + if (@openssl_pkcs7_sign($file, $signed, "file://".$this->sign_cert_file, array("file://".$this->sign_key_file, $this->sign_key_pass), null)) { + $fp = fopen($signed, "r"); + $result = fread($fp, filesize($this->sign_key_file)); + $result = ''; + while(!feof($fp)){ + $result = $result . fread($fp, 1024); + } + fclose($fp); + } else { + $this->SetError($this->Lang("signing").openssl_error_string()); + $result = ''; + } + + unlink($file); + unlink($signed); + } + + return $result; + } + + /** + * Returns the start of a message boundary. + * @access private + */ + function GetBoundary($boundary, $charSet, $contentType, $encoding) { + $result = ''; + if($charSet == '') { + $charSet = $this->CharSet; + } + if($contentType == '') { + $contentType = $this->ContentType; + } + if($encoding == '') { + $encoding = $this->Encoding; + } + $result .= $this->TextLine('--' . $boundary); + $result .= sprintf("Content-Type: %s; charset = \"%s\"", $contentType, $charSet); + $result .= $this->LE; + $result .= $this->HeaderLine('Content-Transfer-Encoding', $encoding); + $result .= $this->LE; + + return $result; + } + + /** + * Returns the end of a message boundary. + * @access private + */ + function EndBoundary($boundary) { + return $this->LE . '--' . $boundary . '--' . $this->LE; + } + + /** + * Sets the message type. + * @access private + * @return void + */ + function SetMessageType() { + if(count($this->attachment) < 1 && strlen($this->AltBody) < 1) { + $this->message_type = 'plain'; + } else { + if(count($this->attachment) > 0) { + $this->message_type = 'attachments'; + } + if(strlen($this->AltBody) > 0 && count($this->attachment) < 1) { + $this->message_type = 'alt'; + } + if(strlen($this->AltBody) > 0 && count($this->attachment) > 0) { + $this->message_type = 'alt_attachments'; + } + } + } + + /* Returns a formatted header line. + * @access private + * @return string + */ + function HeaderLine($name, $value) { + return $name . ': ' . $value . $this->LE; + } + + /** + * Returns a formatted mail line. + * @access private + * @return string + */ + function TextLine($value) { + return $value . $this->LE; + } + + ///////////////////////////////////////////////// + // CLASS METHODS, ATTACHMENTS + ///////////////////////////////////////////////// + + /** + * Adds an attachment from a path on the filesystem. + * Returns false if the file could not be found + * or accessed. + * @param string $path Path to the attachment. + * @param string $name Overrides the attachment name. + * @param string $encoding File encoding (see $Encoding). + * @param string $type File extension (MIME) type. + * @return bool + */ + function AddAttachment($path, $name = '', $encoding = 'base64', $type = 'application/octet-stream') { + if(!@is_file($path)) { + $this->SetError($this->Lang('file_access') . $path); + return false; + } + + $filename = basename($path); + if($name == '') { + $name = $filename; + } + + $cur = count($this->attachment); + $this->attachment[$cur][0] = $path; + $this->attachment[$cur][1] = $filename; + $this->attachment[$cur][2] = $name; + $this->attachment[$cur][3] = $encoding; + $this->attachment[$cur][4] = $type; + $this->attachment[$cur][5] = false; // isStringAttachment + $this->attachment[$cur][6] = 'attachment'; + $this->attachment[$cur][7] = 0; + + return true; + } + + /** + * Attaches all fs, string, and binary attachments to the message. + * Returns an empty string on failure. + * @access private + * @return string + */ + function AttachAll() { + /* Return text of body */ + $mime = array(); + + /* Add all attachments */ + for($i = 0; $i < count($this->attachment); $i++) { + /* Check for string attachment */ + $bString = $this->attachment[$i][5]; + if ($bString) { + $string = $this->attachment[$i][0]; + } else { + $path = $this->attachment[$i][0]; + } + + $filename = $this->attachment[$i][1]; + $name = $this->attachment[$i][2]; + $encoding = $this->attachment[$i][3]; + $type = $this->attachment[$i][4]; + $disposition = $this->attachment[$i][6]; + $cid = $this->attachment[$i][7]; + + $mime[] = sprintf("--%s%s", $this->boundary[1], $this->LE); + $mime[] = sprintf("Content-Type: %s; name=\"%s\"%s", $type, $this->EncodeHeader($this->SecureHeader($name)), $this->LE); + $mime[] = sprintf("Content-Transfer-Encoding: %s%s", $encoding, $this->LE); + + if($disposition == 'inline') { + $mime[] = sprintf("Content-ID: <%s>%s", $cid, $this->LE); + } + + $mime[] = sprintf("Content-Disposition: %s; filename=\"%s\"%s", $disposition, $this->EncodeHeader($this->SecureHeader($name)), $this->LE.$this->LE); + + /* Encode as string attachment */ + if($bString) { + $mime[] = $this->EncodeString($string, $encoding); + if($this->IsError()) { + return ''; + } + $mime[] = $this->LE.$this->LE; + } else { + $mime[] = $this->EncodeFile($path, $encoding); + if($this->IsError()) { + return ''; + } + $mime[] = $this->LE.$this->LE; + } + } + + $mime[] = sprintf("--%s--%s", $this->boundary[1], $this->LE); + + return join('', $mime); + } + + /** + * Encodes attachment in requested format. Returns an + * empty string on failure. + * @access private + * @return string + */ + function EncodeFile ($path, $encoding = 'base64') { + if(!@$fd = fopen($path, 'rb')) { + $this->SetError($this->Lang('file_open') . $path); + return ''; + } + $magic_quotes = get_magic_quotes_runtime(); + set_magic_quotes_runtime(0); + $file_buffer = fread($fd, filesize($path)); + $file_buffer = $this->EncodeString($file_buffer, $encoding); + fclose($fd); + set_magic_quotes_runtime($magic_quotes); + + return $file_buffer; + } + + /** + * Encodes string to requested format. Returns an + * empty string on failure. + * @access private + * @return string + */ + function EncodeString ($str, $encoding = 'base64') { + $encoded = ''; + switch(strtolower($encoding)) { + case 'base64': + /* chunk_split is found in PHP >= 3.0.6 */ + $encoded = chunk_split(base64_encode($str), 76, $this->LE); + break; + case '7bit': + case '8bit': + $encoded = $this->FixEOL($str); + if (substr($encoded, -(strlen($this->LE))) != $this->LE) + $encoded .= $this->LE; + break; + case 'binary': + $encoded = $str; + break; + case 'quoted-printable': + $encoded = $this->EncodeQP($str); + break; + default: + $this->SetError($this->Lang('encoding') . $encoding); + break; + } + return $encoded; + } + + /** + * Encode a header string to best of Q, B, quoted or none. + * @access private + * @return string + */ + function EncodeHeader ($str, $position = 'text') { + $x = 0; + + switch (strtolower($position)) { + case 'phrase': + if (!preg_match('/[\200-\377]/', $str)) { + /* Can't use addslashes as we don't know what value has magic_quotes_sybase. */ + $encoded = addcslashes($str, "\0..\37\177\\\""); + if (($str == $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) { + return ($encoded); + } else { + return ("\"$encoded\""); + } + } + $x = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches); + break; + case 'comment': + $x = preg_match_all('/[()"]/', $str, $matches); + /* Fall-through */ + case 'text': + default: + $x += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches); + break; + } + + if ($x == 0) { + return ($str); + } + + $maxlen = 75 - 7 - strlen($this->CharSet); + /* Try to select the encoding which should produce the shortest output */ + if (strlen($str)/3 < $x) { + $encoding = 'B'; + if (function_exists('mb_strlen') && $this->HasMultiBytes($str)) { + // Use a custom function which correctly encodes and wraps long + // multibyte strings without breaking lines within a character + $encoded = $this->Base64EncodeWrapMB($str); + } else { + $encoded = base64_encode($str); + $maxlen -= $maxlen % 4; + $encoded = trim(chunk_split($encoded, $maxlen, "\n")); + } + } else { + $encoding = 'Q'; + $encoded = $this->EncodeQ($str, $position); + $encoded = $this->WrapText($encoded, $maxlen, true); + $encoded = str_replace('='.$this->LE, "\n", trim($encoded)); + } + + $encoded = preg_replace('/^(.*)$/m', " =?".$this->CharSet."?$encoding?\\1?=", $encoded); + $encoded = trim(str_replace("\n", $this->LE, $encoded)); + + return $encoded; + } + + /** + * Checks if a string contains multibyte characters. + * @access private + * @param string $str multi-byte text to wrap encode + * @return bool + */ + function HasMultiBytes($str) { + if (function_exists('mb_strlen')) { + return (strlen($str) > mb_strlen($str, $this->CharSet)); + } else { // Assume no multibytes (we can't handle without mbstring functions anyway) + return False; + } + } + + /** + * Correctly encodes and wraps long multibyte strings for mail headers + * without breaking lines within a character. + * Adapted from a function by paravoid at http://uk.php.net/manual/en/function.mb-encode-mimeheader.php + * @access private + * @param string $str multi-byte text to wrap encode + * @return string + */ + function Base64EncodeWrapMB($str) { + $start = "=?".$this->CharSet."?B?"; + $end = "?="; + $encoded = ""; + + $mb_length = mb_strlen($str, $this->CharSet); + // Each line must have length <= 75, including $start and $end + $length = 75 - strlen($start) - strlen($end); + // Average multi-byte ratio + $ratio = $mb_length / strlen($str); + // Base64 has a 4:3 ratio + $offset = $avgLength = floor($length * $ratio * .75); + + for ($i = 0; $i < $mb_length; $i += $offset) { + $lookBack = 0; + + do { + $offset = $avgLength - $lookBack; + $chunk = mb_substr($str, $i, $offset, $this->CharSet); + $chunk = base64_encode($chunk); + $lookBack++; + } + while (strlen($chunk) > $length); + + $encoded .= $chunk . $this->LE; + } + + // Chomp the last linefeed + $encoded = substr($encoded, 0, -strlen($this->LE)); + return $encoded; + } + + /** + * Encode string to quoted-printable. + * @access private + * @return string + */ + function EncodeQP( $input = '', $line_max = 76, $space_conv = false ) { + $hex = array('0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'); + $lines = preg_split('/(?:\r\n|\r|\n)/', $input); + $eol = "\r\n"; + $escape = '='; + $output = ''; + while( list(, $line) = each($lines) ) { + $linlen = strlen($line); + $newline = ''; + for($i = 0; $i < $linlen; $i++) { + $c = substr( $line, $i, 1 ); + $dec = ord( $c ); + if ( ( $i == 0 ) && ( $dec == 46 ) ) { // convert first point in the line into =2E + $c = '=2E'; + } + if ( $dec == 32 ) { + if ( $i == ( $linlen - 1 ) ) { // convert space at eol only + $c = '=20'; + } else if ( $space_conv ) { + $c = '=20'; + } + } elseif ( ($dec == 61) || ($dec < 32 ) || ($dec > 126) ) { // always encode "\t", which is *not* required + $h2 = floor($dec/16); + $h1 = floor($dec%16); + $c = $escape.$hex[$h2].$hex[$h1]; + } + if ( (strlen($newline) + strlen($c)) >= $line_max ) { // CRLF is not counted + $output .= $newline.$escape.$eol; // soft line break; " =\r\n" is okay + $newline = ''; + // check if newline first character will be point or not + if ( $dec == 46 ) { + $c = '=2E'; + } + } + $newline .= $c; + } // end of for + $output .= $newline.$eol; + } // end of while + return $output; + } + + /** + * Encode string to q encoding. + * @access private + * @return string + */ + function EncodeQ ($str, $position = 'text') { + /* There should not be any EOL in the string */ + $encoded = preg_replace("[\r\n]", '', $str); + + switch (strtolower($position)) { + case 'phrase': + $encoded = preg_replace("/([^A-Za-z0-9!*+\/ -])/e", "'='.sprintf('%02X', ord('\\1'))", $encoded); + break; + case 'comment': + $encoded = preg_replace("/([\(\)\"])/e", "'='.sprintf('%02X', ord('\\1'))", $encoded); + case 'text': + default: + /* Replace every high ascii, control =, ? and _ characters */ + $encoded = preg_replace('/([\000-\011\013\014\016-\037\075\077\137\177-\377])/e', + "'='.sprintf('%02X', ord('\\1'))", $encoded); + break; + } + + /* Replace every spaces to _ (more readable than =20) */ + $encoded = str_replace(' ', '_', $encoded); + + return $encoded; + } + + /** + * Adds a string or binary attachment (non-filesystem) to the list. + * This method can be used to attach ascii or binary data, + * such as a BLOB record from a database. + * @param string $string String attachment data. + * @param string $filename Name of the attachment. + * @param string $encoding File encoding (see $Encoding). + * @param string $type File extension (MIME) type. + * @return void + */ + function AddStringAttachment($string, $filename, $encoding = 'base64', $type = 'application/octet-stream') { + /* Append to $attachment array */ + $cur = count($this->attachment); + $this->attachment[$cur][0] = $string; + $this->attachment[$cur][1] = $filename; + $this->attachment[$cur][2] = $filename; + $this->attachment[$cur][3] = $encoding; + $this->attachment[$cur][4] = $type; + $this->attachment[$cur][5] = true; // isString + $this->attachment[$cur][6] = 'attachment'; + $this->attachment[$cur][7] = 0; + } + + /** + * Adds an embedded attachment. This can include images, sounds, and + * just about any other document. Make sure to set the $type to an + * image type. For JPEG images use "image/jpeg" and for GIF images + * use "image/gif". + * @param string $path Path to the attachment. + * @param string $cid Content ID of the attachment. Use this to identify + * the Id for accessing the image in an HTML form. + * @param string $name Overrides the attachment name. + * @param string $encoding File encoding (see $Encoding). + * @param string $type File extension (MIME) type. + * @return bool + */ + function AddEmbeddedImage($path, $cid, $name = '', $encoding = 'base64', $type = 'application/octet-stream') { + + if(!@is_file($path)) { + $this->SetError($this->Lang('file_access') . $path); + return false; + } + + $filename = basename($path); + if($name == '') { + $name = $filename; + } + + /* Append to $attachment array */ + $cur = count($this->attachment); + $this->attachment[$cur][0] = $path; + $this->attachment[$cur][1] = $filename; + $this->attachment[$cur][2] = $name; + $this->attachment[$cur][3] = $encoding; + $this->attachment[$cur][4] = $type; + $this->attachment[$cur][5] = false; + $this->attachment[$cur][6] = 'inline'; + $this->attachment[$cur][7] = $cid; + + return true; + } + + /** + * Returns true if an inline attachment is present. + * @access private + * @return bool + */ + function InlineImageExists() { + $result = false; + for($i = 0; $i < count($this->attachment); $i++) { + if($this->attachment[$i][6] == 'inline') { + $result = true; + break; + } + } + + return $result; + } + + ///////////////////////////////////////////////// + // CLASS METHODS, MESSAGE RESET + ///////////////////////////////////////////////// + + /** + * Clears all recipients assigned in the TO array. Returns void. + * @return void + */ + function ClearAddresses() { + $this->to = array(); + } + + /** + * Clears all recipients assigned in the CC array. Returns void. + * @return void + */ + function ClearCCs() { + $this->cc = array(); + } + + /** + * Clears all recipients assigned in the BCC array. Returns void. + * @return void + */ + function ClearBCCs() { + $this->bcc = array(); + } + + /** + * Clears all recipients assigned in the ReplyTo array. Returns void. + * @return void + */ + function ClearReplyTos() { + $this->ReplyTo = array(); + } + + /** + * Clears all recipients assigned in the TO, CC and BCC + * array. Returns void. + * @return void + */ + function ClearAllRecipients() { + $this->to = array(); + $this->cc = array(); + $this->bcc = array(); + } + + /** + * Clears all previously set filesystem, string, and binary + * attachments. Returns void. + * @return void + */ + function ClearAttachments() { + $this->attachment = array(); + } + + /** + * Clears all custom headers. Returns void. + * @return void + */ + function ClearCustomHeaders() { + $this->CustomHeader = array(); + } + + ///////////////////////////////////////////////// + // CLASS METHODS, MISCELLANEOUS + ///////////////////////////////////////////////// + + /** + * Adds the error message to the error container. + * Returns void. + * @access private + * @return void + */ + function SetError($msg) { + $this->error_count++; + $this->ErrorInfo = $msg; + } + + /** + * Returns the proper RFC 822 formatted date. + * @access private + * @return string + */ + function RFCDate() { + $tz = date('Z'); + $tzs = ($tz < 0) ? '-' : '+'; + $tz = abs($tz); + $tz = (int)($tz/3600)*100 + ($tz%3600)/60; + $result = sprintf("%s %s%04d", date('D, j M Y H:i:s'), $tzs, $tz); + + return $result; + } + + /** + * Returns the appropriate server variable. Should work with both + * PHP 4.1.0+ as well as older versions. Returns an empty string + * if nothing is found. + * @access private + * @return mixed + */ + function ServerVar($varName) { + global $HTTP_SERVER_VARS; + global $HTTP_ENV_VARS; + + if(!isset($_SERVER)) { + $_SERVER = $HTTP_SERVER_VARS; + if(!isset($_SERVER['REMOTE_ADDR'])) { + $_SERVER = $HTTP_ENV_VARS; // must be Apache + } + } + + if(isset($_SERVER[$varName])) { + return $_SERVER[$varName]; + } else { + return ''; + } + } + + /** + * Returns the server hostname or 'localhost.localdomain' if unknown. + * @access private + * @return string + */ + function ServerHostname() { + if ($this->Hostname != '') { + $result = $this->Hostname; + } elseif ($this->ServerVar('SERVER_NAME') != '') { + $result = $this->ServerVar('SERVER_NAME'); + } else { + $result = 'localhost.localdomain'; + } + + return $result; + } + + /** + * Returns a message in the appropriate language. + * @access private + * @return string + */ + function Lang($key) { + if(count($this->language) < 1) { + $this->SetLanguage('en'); // set the default language + } + + if(isset($this->language[$key])) { + return $this->language[$key]; + } else { + return 'Language string failed to load: ' . $key; + } + } + + /** + * Returns true if an error occurred. + * @return bool + */ + function IsError() { + return ($this->error_count > 0); + } + + /** + * Changes every end of line from CR or LF to CRLF. + * @access private + * @return string + */ + function FixEOL($str) { + $str = str_replace("\r\n", "\n", $str); + $str = str_replace("\r", "\n", $str); + $str = str_replace("\n", $this->LE, $str); + return $str; + } + + /** + * Adds a custom header. + * @return void + */ + function AddCustomHeader($custom_header) { + $this->CustomHeader[] = explode(':', $custom_header, 2); + } + + /** + * Evaluates the message and returns modifications for inline images and backgrounds + * @access public + * @return $message + */ + function MsgHTML($message,$basedir='') { + preg_match_all("/(src|background)=\"(.*)\"/Ui", $message, $images); + if(isset($images[2])) { + foreach($images[2] as $i => $url) { + // do not change urls for absolute images (thanks to corvuscorax) + if (!preg_match('/^[A-z][A-z]*:\/\//',$url)) { + $filename = basename($url); + $directory = dirname($url); + ($directory == '.')?$directory='':''; + $cid = 'cid:' . md5($filename); + $fileParts = split("\.", $filename); + $ext = $fileParts[1]; + $mimeType = $this->_mime_types($ext); + if ( strlen($basedir) > 1 && substr($basedir,-1) != '/') { $basedir .= '/'; } + if ( strlen($directory) > 1 && substr($directory,-1) != '/') { $directory .= '/'; } + if ( $this->AddEmbeddedImage($basedir.$directory.$filename, md5($filename), $filename, 'base64',$mimeType) ) { + $message = preg_replace("/".$images[1][$i]."=\"".preg_quote($url, '/')."\"/Ui", $images[1][$i]."=\"".$cid."\"", $message); + } + } + } + } + $this->IsHTML(true); + $this->Body = $message; + $textMsg = trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/s','',$message))); + if ( !empty($textMsg) && empty($this->AltBody) ) { + $this->AltBody = html_entity_decode($textMsg); + } + if ( empty($this->AltBody) ) { + $this->AltBody = 'To view this email message, open the email in with HTML compatibility!' . "\n\n"; + } + } + + /** + * Gets the mime type of the embedded or inline image + * @access private + * @return mime type of ext + */ + function _mime_types($ext = '') { + $mimes = array( + 'ai' => 'application/postscript', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'avi' => 'video/x-msvideo', + 'bin' => 'application/macbinary', + 'bmp' => 'image/bmp', + 'class' => 'application/octet-stream', + 'cpt' => 'application/mac-compactpro', + 'css' => 'text/css', + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dll' => 'application/octet-stream', + 'dms' => 'application/octet-stream', + 'doc' => 'application/msword', + 'dvi' => 'application/x-dvi', + 'dxr' => 'application/x-director', + 'eml' => 'message/rfc822', + 'eps' => 'application/postscript', + 'exe' => 'application/octet-stream', + 'gif' => 'image/gif', + 'gtar' => 'application/x-gtar', + 'htm' => 'text/html', + 'html' => 'text/html', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'hqx' => 'application/mac-binhex40', + 'js' => 'application/x-javascript', + 'lha' => 'application/octet-stream', + 'log' => 'text/plain', + 'lzh' => 'application/octet-stream', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mif' => 'application/vnd.mif', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mp2' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpga' => 'audio/mpeg', + 'oda' => 'application/oda', + 'pdf' => 'application/pdf', + 'php' => 'application/x-httpd-php', + 'php3' => 'application/x-httpd-php', + 'php4' => 'application/x-httpd-php', + 'phps' => 'application/x-httpd-php-source', + 'phtml' => 'application/x-httpd-php', + 'png' => 'image/png', + 'ppt' => 'application/vnd.ms-powerpoint', + 'ps' => 'application/postscript', + 'psd' => 'application/octet-stream', + 'qt' => 'video/quicktime', + 'ra' => 'audio/x-realaudio', + 'ram' => 'audio/x-pn-realaudio', + 'rm' => 'audio/x-pn-realaudio', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'rtf' => 'text/rtf', + 'rtx' => 'text/richtext', + 'rv' => 'video/vnd.rn-realvideo', + 'sea' => 'application/octet-stream', + 'shtml' => 'text/html', + 'sit' => 'application/x-stuffit', + 'so' => 'application/octet-stream', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'swf' => 'application/x-shockwave-flash', + 'tar' => 'application/x-tar', + 'text' => 'text/plain', + 'txt' => 'text/plain', + 'tgz' => 'application/x-tar', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'wav' => 'audio/x-wav', + 'wbxml' => 'application/vnd.wap.wbxml', + 'wmlc' => 'application/vnd.wap.wmlc', + 'word' => 'application/msword', + 'xht' => 'application/xhtml+xml', + 'xhtml' => 'application/xhtml+xml', + 'xl' => 'application/excel', + 'xls' => 'application/vnd.ms-excel', + 'xml' => 'text/xml', + 'xsl' => 'text/xml', + 'zip' => 'application/zip' + ); + return ( ! isset($mimes[strtolower($ext)])) ? 'application/octet-stream' : $mimes[strtolower($ext)]; + } + + /** + * Set (or reset) Class Objects (variables) + * + * Usage Example: + * $page->set('X-Priority', '3'); + * + * @access public + * @param string $name Parameter Name + * @param mixed $value Parameter Value + * NOTE: will not work with arrays, there are no arrays to set/reset + */ + function set ( $name, $value = '' ) { + if ( isset($this->$name) ) { + $this->$name = $value; + } else { + $this->SetError('Cannot set or reset variable ' . $name); + return false; + } + } + + /** + * Read a file from a supplied filename and return it. + * + * @access public + * @param string $filename Parameter File Name + */ + function getFile($filename) { + $return = ''; + if ($fp = fopen($filename, 'rb')) { + while (!feof($fp)) { + $return .= fread($fp, 1024); + } + fclose($fp); + return $return; + } else { + return false; + } + } + + /** + * Strips newlines to prevent header injection. + * @access private + * @param string $str String + * @return string + */ + function SecureHeader($str) { + $str = trim($str); + $str = str_replace("\r", "", $str); + $str = str_replace("\n", "", $str); + return $str; + } + + /** + * Set the private key file and password to sign the message. + * + * @access public + * @param string $key_filename Parameter File Name + * @param string $key_pass Password for private key + */ + function Sign($cert_filename, $key_filename, $key_pass) { + $this->sign_cert_file = $cert_filename; + $this->sign_key_file = $key_filename; + $this->sign_key_pass = $key_pass; + } + +} + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-class.smtp.php b/qa-include/qa-class.smtp.php new file mode 100644 index 000000000..abaad03ef --- /dev/null +++ b/qa-include/qa-class.smtp.php @@ -0,0 +1,1064 @@ +smtp_conn = 0; + $this->error = null; + $this->helo_rply = null; + + $this->do_debug = 0; + } + + /************************************************************* + * CONNECTION FUNCTIONS * + ***********************************************************/ + + /** + * Connect to the server specified on the port specified. + * If the port is not specified use the default SMTP_PORT. + * If tval is specified then a connection will try and be + * established with the server for that number of seconds. + * If tval is not specified the default is 30 seconds to + * try on the connection. + * + * SMTP CODE SUCCESS: 220 + * SMTP CODE FAILURE: 421 + * @access public + * @return bool + */ + function Connect($host,$port=0,$tval=30) { + # set the error val to null so there is no confusion + $this->error = null; + + # make sure we are __not__ connected + if($this->connected()) { + # ok we are connected! what should we do? + # for now we will just give an error saying we + # are already connected + $this->error = array("error" => "Already connected to a server"); + return false; + } + + if(empty($port)) { + $port = $this->SMTP_PORT; + } + + #connect to the smtp server + $this->smtp_conn = fsockopen($host, # the host of the server + $port, # the port to use + $errno, # error number if any + $errstr, # error message if any + $tval); # give up after ? secs + # verify we connected properly + if(empty($this->smtp_conn)) { + $this->error = array("error" => "Failed to connect to server", + "errno" => $errno, + "errstr" => $errstr); + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $this->error["error"] . + ": $errstr ($errno)" . $this->CRLF; + } + return false; + } + + # sometimes the SMTP server takes a little longer to respond + # so we will give it a longer timeout for the first read + // Windows still does not have support for this timeout function + if(substr(PHP_OS, 0, 3) != "WIN") + socket_set_timeout($this->smtp_conn, $tval, 0); + + # get any announcement stuff + $announce = $this->get_lines(); + + # set the timeout of any socket functions at 1/10 of a second + //if(function_exists("socket_set_timeout")) + // socket_set_timeout($this->smtp_conn, 0, 100000); + + if($this->do_debug >= 2) { + echo "SMTP -> FROM SERVER:" . $this->CRLF . $announce; + } + + return true; + } + + /** + * Performs SMTP authentication. Must be run after running the + * Hello() method. Returns true if successfully authenticated. + * @access public + * @return bool + */ + function Authenticate($username, $password) { + // Start authentication + fputs($this->smtp_conn,"AUTH LOGIN" . $this->CRLF); + + $rply = $this->get_lines(); + $code = substr($rply,0,3); + + if($code != 334) { + $this->error = + array("error" => "AUTH not accepted from server", + "smtp_code" => $code, + "smtp_msg" => substr($rply,4)); + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $this->error["error"] . + ": " . $rply . $this->CRLF; + } + return false; + } + + // Send encoded username + fputs($this->smtp_conn, base64_encode($username) . $this->CRLF); + + $rply = $this->get_lines(); + $code = substr($rply,0,3); + + if($code != 334) { + $this->error = + array("error" => "Username not accepted from server", + "smtp_code" => $code, + "smtp_msg" => substr($rply,4)); + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $this->error["error"] . + ": " . $rply . $this->CRLF; + } + return false; + } + + // Send encoded password + fputs($this->smtp_conn, base64_encode($password) . $this->CRLF); + + $rply = $this->get_lines(); + $code = substr($rply,0,3); + + if($code != 235) { + $this->error = + array("error" => "Password not accepted from server", + "smtp_code" => $code, + "smtp_msg" => substr($rply,4)); + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $this->error["error"] . + ": " . $rply . $this->CRLF; + } + return false; + } + + return true; + } + + /** + * Returns true if connected to a server otherwise false + * @access private + * @return bool + */ + function Connected() { + if(!empty($this->smtp_conn)) { + $sock_status = socket_get_status($this->smtp_conn); + if($sock_status["eof"]) { + # hmm this is an odd situation... the socket is + # valid but we are not connected anymore + if($this->do_debug >= 1) { + echo "SMTP -> NOTICE:" . $this->CRLF . + "EOF caught while checking if connected"; + } + $this->Close(); + return false; + } + return true; # everything looks good + } + return false; + } + + /** + * Closes the socket and cleans up the state of the class. + * It is not considered good to use this function without + * first trying to use QUIT. + * @access public + * @return void + */ + function Close() { + $this->error = null; # so there is no confusion + $this->helo_rply = null; + if(!empty($this->smtp_conn)) { + # close the connection and cleanup + fclose($this->smtp_conn); + $this->smtp_conn = 0; + } + } + + /*************************************************************** + * SMTP COMMANDS * + *************************************************************/ + + /** + * Issues a data command and sends the msg_data to the server + * finializing the mail transaction. $msg_data is the message + * that is to be send with the headers. Each header needs to be + * on a single line followed by a with the message headers + * and the message body being seperated by and additional . + * + * Implements rfc 821: DATA + * + * SMTP CODE INTERMEDIATE: 354 + * [data] + * . + * SMTP CODE SUCCESS: 250 + * SMTP CODE FAILURE: 552,554,451,452 + * SMTP CODE FAILURE: 451,554 + * SMTP CODE ERROR : 500,501,503,421 + * @access public + * @return bool + */ + function Data($msg_data) { + $this->error = null; # so no confusion is caused + + if(!$this->connected()) { + $this->error = array( + "error" => "Called Data() without being connected"); + return false; + } + + fputs($this->smtp_conn,"DATA" . $this->CRLF); + + $rply = $this->get_lines(); + $code = substr($rply,0,3); + + if($this->do_debug >= 2) { + echo "SMTP -> FROM SERVER:" . $this->CRLF . $rply; + } + + if($code != 354) { + $this->error = + array("error" => "DATA command not accepted from server", + "smtp_code" => $code, + "smtp_msg" => substr($rply,4)); + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $this->error["error"] . + ": " . $rply . $this->CRLF; + } + return false; + } + + # the server is ready to accept data! + # according to rfc 821 we should not send more than 1000 + # including the CRLF + # characters on a single line so we will break the data up + # into lines by \r and/or \n then if needed we will break + # each of those into smaller lines to fit within the limit. + # in addition we will be looking for lines that start with + # a period '.' and append and additional period '.' to that + # line. NOTE: this does not count towards are limit. + + # normalize the line breaks so we know the explode works + $msg_data = str_replace("\r\n","\n",$msg_data); + $msg_data = str_replace("\r","\n",$msg_data); + $lines = explode("\n",$msg_data); + + # we need to find a good way to determine is headers are + # in the msg_data or if it is a straight msg body + # currently I am assuming rfc 822 definitions of msg headers + # and if the first field of the first line (':' sperated) + # does not contain a space then it _should_ be a header + # and we can process all lines before a blank "" line as + # headers. + $field = substr($lines[0],0,strpos($lines[0],":")); + $in_headers = false; + if(!empty($field) && !strstr($field," ")) { + $in_headers = true; + } + + $max_line_length = 998; # used below; set here for ease in change + + while(list(,$line) = @each($lines)) { + $lines_out = null; + if($line == "" && $in_headers) { + $in_headers = false; + } + # ok we need to break this line up into several + # smaller lines + while(strlen($line) > $max_line_length) { + $pos = strrpos(substr($line,0,$max_line_length)," "); + + # Patch to fix DOS attack + if(!$pos) { + $pos = $max_line_length - 1; + } + + $lines_out[] = substr($line,0,$pos); + $line = substr($line,$pos + 1); + # if we are processing headers we need to + # add a LWSP-char to the front of the new line + # rfc 822 on long msg headers + if($in_headers) { + $line = "\t" . $line; + } + } + $lines_out[] = $line; + + # now send the lines to the server + while(list(,$line_out) = @each($lines_out)) { + if(strlen($line_out) > 0) + { + if(substr($line_out, 0, 1) == ".") { + $line_out = "." . $line_out; + } + } + fputs($this->smtp_conn,$line_out . $this->CRLF); + } + } + + # ok all the message data has been sent so lets get this + # over with aleady + fputs($this->smtp_conn, $this->CRLF . "." . $this->CRLF); + + $rply = $this->get_lines(); + $code = substr($rply,0,3); + + if($this->do_debug >= 2) { + echo "SMTP -> FROM SERVER:" . $this->CRLF . $rply; + } + + if($code != 250) { + $this->error = + array("error" => "DATA not accepted from server", + "smtp_code" => $code, + "smtp_msg" => substr($rply,4)); + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $this->error["error"] . + ": " . $rply . $this->CRLF; + } + return false; + } + return true; + } + + /** + * Expand takes the name and asks the server to list all the + * people who are members of the _list_. Expand will return + * back and array of the result or false if an error occurs. + * Each value in the array returned has the format of: + * [ ] + * The definition of is defined in rfc 821 + * + * Implements rfc 821: EXPN + * + * SMTP CODE SUCCESS: 250 + * SMTP CODE FAILURE: 550 + * SMTP CODE ERROR : 500,501,502,504,421 + * @access public + * @return string array + */ + function Expand($name) { + $this->error = null; # so no confusion is caused + + if(!$this->connected()) { + $this->error = array( + "error" => "Called Expand() without being connected"); + return false; + } + + fputs($this->smtp_conn,"EXPN " . $name . $this->CRLF); + + $rply = $this->get_lines(); + $code = substr($rply,0,3); + + if($this->do_debug >= 2) { + echo "SMTP -> FROM SERVER:" . $this->CRLF . $rply; + } + + if($code != 250) { + $this->error = + array("error" => "EXPN not accepted from server", + "smtp_code" => $code, + "smtp_msg" => substr($rply,4)); + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $this->error["error"] . + ": " . $rply . $this->CRLF; + } + return false; + } + + # parse the reply and place in our array to return to user + $entries = explode($this->CRLF,$rply); + while(list(,$l) = @each($entries)) { + $list[] = substr($l,4); + } + + return $list; + } + + /** + * Sends the HELO command to the smtp server. + * This makes sure that we and the server are in + * the same known state. + * + * Implements from rfc 821: HELO + * + * SMTP CODE SUCCESS: 250 + * SMTP CODE ERROR : 500, 501, 504, 421 + * @access public + * @return bool + */ + function Hello($host="") { + $this->error = null; # so no confusion is caused + + if(!$this->connected()) { + $this->error = array( + "error" => "Called Hello() without being connected"); + return false; + } + + # if a hostname for the HELO was not specified determine + # a suitable one to send + if(empty($host)) { + # we need to determine some sort of appopiate default + # to send to the server + $host = "localhost"; + } + + // Send extended hello first (RFC 2821) + if(!$this->SendHello("EHLO", $host)) + { + if(!$this->SendHello("HELO", $host)) + return false; + } + + return true; + } + + /** + * Sends a HELO/EHLO command. + * @access private + * @return bool + */ + function SendHello($hello, $host) { + fputs($this->smtp_conn, $hello . " " . $host . $this->CRLF); + + $rply = $this->get_lines(); + $code = substr($rply,0,3); + + if($this->do_debug >= 2) { + echo "SMTP -> FROM SERVER: " . $this->CRLF . $rply; + } + + if($code != 250) { + $this->error = + array("error" => $hello . " not accepted from server", + "smtp_code" => $code, + "smtp_msg" => substr($rply,4)); + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $this->error["error"] . + ": " . $rply . $this->CRLF; + } + return false; + } + + $this->helo_rply = $rply; + + return true; + } + + /** + * Gets help information on the keyword specified. If the keyword + * is not specified then returns generic help, ussually contianing + * A list of keywords that help is available on. This function + * returns the results back to the user. It is up to the user to + * handle the returned data. If an error occurs then false is + * returned with $this->error set appropiately. + * + * Implements rfc 821: HELP [ ] + * + * SMTP CODE SUCCESS: 211,214 + * SMTP CODE ERROR : 500,501,502,504,421 + * @access public + * @return string + */ + function Help($keyword="") { + $this->error = null; # to avoid confusion + + if(!$this->connected()) { + $this->error = array( + "error" => "Called Help() without being connected"); + return false; + } + + $extra = ""; + if(!empty($keyword)) { + $extra = " " . $keyword; + } + + fputs($this->smtp_conn,"HELP" . $extra . $this->CRLF); + + $rply = $this->get_lines(); + $code = substr($rply,0,3); + + if($this->do_debug >= 2) { + echo "SMTP -> FROM SERVER:" . $this->CRLF . $rply; + } + + if($code != 211 && $code != 214) { + $this->error = + array("error" => "HELP not accepted from server", + "smtp_code" => $code, + "smtp_msg" => substr($rply,4)); + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $this->error["error"] . + ": " . $rply . $this->CRLF; + } + return false; + } + + return $rply; + } + + /** + * Starts a mail transaction from the email address specified in + * $from. Returns true if successful or false otherwise. If True + * the mail transaction is started and then one or more Recipient + * commands may be called followed by a Data command. + * + * Implements rfc 821: MAIL FROM: + * + * SMTP CODE SUCCESS: 250 + * SMTP CODE SUCCESS: 552,451,452 + * SMTP CODE SUCCESS: 500,501,421 + * @access public + * @return bool + */ + function Mail($from) { + $this->error = null; # so no confusion is caused + + if(!$this->connected()) { + $this->error = array( + "error" => "Called Mail() without being connected"); + return false; + } + + $useVerp = ($this->do_verp ? "XVERP" : ""); + fputs($this->smtp_conn,"MAIL FROM:<" . $from . ">" . $useVerp . $this->CRLF); + + $rply = $this->get_lines(); + $code = substr($rply,0,3); + + if($this->do_debug >= 2) { + echo "SMTP -> FROM SERVER:" . $this->CRLF . $rply; + } + + if($code != 250) { + $this->error = + array("error" => "MAIL not accepted from server", + "smtp_code" => $code, + "smtp_msg" => substr($rply,4)); + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $this->error["error"] . + ": " . $rply . $this->CRLF; + } + return false; + } + return true; + } + + /** + * Sends the command NOOP to the SMTP server. + * + * Implements from rfc 821: NOOP + * + * SMTP CODE SUCCESS: 250 + * SMTP CODE ERROR : 500, 421 + * @access public + * @return bool + */ + function Noop() { + $this->error = null; # so no confusion is caused + + if(!$this->connected()) { + $this->error = array( + "error" => "Called Noop() without being connected"); + return false; + } + + fputs($this->smtp_conn,"NOOP" . $this->CRLF); + + $rply = $this->get_lines(); + $code = substr($rply,0,3); + + if($this->do_debug >= 2) { + echo "SMTP -> FROM SERVER:" . $this->CRLF . $rply; + } + + if($code != 250) { + $this->error = + array("error" => "NOOP not accepted from server", + "smtp_code" => $code, + "smtp_msg" => substr($rply,4)); + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $this->error["error"] . + ": " . $rply . $this->CRLF; + } + return false; + } + return true; + } + + /** + * Sends the quit command to the server and then closes the socket + * if there is no error or the $close_on_error argument is true. + * + * Implements from rfc 821: QUIT + * + * SMTP CODE SUCCESS: 221 + * SMTP CODE ERROR : 500 + * @access public + * @return bool + */ + function Quit($close_on_error=true) { + $this->error = null; # so there is no confusion + + if(!$this->connected()) { + $this->error = array( + "error" => "Called Quit() without being connected"); + return false; + } + + # send the quit command to the server + fputs($this->smtp_conn,"quit" . $this->CRLF); + + # get any good-bye messages + $byemsg = $this->get_lines(); + + if($this->do_debug >= 2) { + echo "SMTP -> FROM SERVER:" . $this->CRLF . $byemsg; + } + + $rval = true; + $e = null; + + $code = substr($byemsg,0,3); + if($code != 221) { + # use e as a tmp var cause Close will overwrite $this->error + $e = array("error" => "SMTP server rejected quit command", + "smtp_code" => $code, + "smtp_rply" => substr($byemsg,4)); + $rval = false; + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $e["error"] . ": " . + $byemsg . $this->CRLF; + } + } + + if(empty($e) || $close_on_error) { + $this->Close(); + } + + return $rval; + } + + /** + * Sends the command RCPT to the SMTP server with the TO: argument of $to. + * Returns true if the recipient was accepted false if it was rejected. + * + * Implements from rfc 821: RCPT TO: + * + * SMTP CODE SUCCESS: 250,251 + * SMTP CODE FAILURE: 550,551,552,553,450,451,452 + * SMTP CODE ERROR : 500,501,503,421 + * @access public + * @return bool + */ + function Recipient($to) { + $this->error = null; # so no confusion is caused + + if(!$this->connected()) { + $this->error = array( + "error" => "Called Recipient() without being connected"); + return false; + } + + fputs($this->smtp_conn,"RCPT TO:<" . $to . ">" . $this->CRLF); + + $rply = $this->get_lines(); + $code = substr($rply,0,3); + + if($this->do_debug >= 2) { + echo "SMTP -> FROM SERVER:" . $this->CRLF . $rply; + } + + if($code != 250 && $code != 251) { + $this->error = + array("error" => "RCPT not accepted from server", + "smtp_code" => $code, + "smtp_msg" => substr($rply,4)); + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $this->error["error"] . + ": " . $rply . $this->CRLF; + } + return false; + } + return true; + } + + /** + * Sends the RSET command to abort and transaction that is + * currently in progress. Returns true if successful false + * otherwise. + * + * Implements rfc 821: RSET + * + * SMTP CODE SUCCESS: 250 + * SMTP CODE ERROR : 500,501,504,421 + * @access public + * @return bool + */ + function Reset() { + $this->error = null; # so no confusion is caused + + if(!$this->connected()) { + $this->error = array( + "error" => "Called Reset() without being connected"); + return false; + } + + fputs($this->smtp_conn,"RSET" . $this->CRLF); + + $rply = $this->get_lines(); + $code = substr($rply,0,3); + + if($this->do_debug >= 2) { + echo "SMTP -> FROM SERVER:" . $this->CRLF . $rply; + } + + if($code != 250) { + $this->error = + array("error" => "RSET failed", + "smtp_code" => $code, + "smtp_msg" => substr($rply,4)); + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $this->error["error"] . + ": " . $rply . $this->CRLF; + } + return false; + } + + return true; + } + + /** + * Starts a mail transaction from the email address specified in + * $from. Returns true if successful or false otherwise. If True + * the mail transaction is started and then one or more Recipient + * commands may be called followed by a Data command. This command + * will send the message to the users terminal if they are logged + * in. + * + * Implements rfc 821: SEND FROM: + * + * SMTP CODE SUCCESS: 250 + * SMTP CODE SUCCESS: 552,451,452 + * SMTP CODE SUCCESS: 500,501,502,421 + * @access public + * @return bool + */ + function Send($from) { + $this->error = null; # so no confusion is caused + + if(!$this->connected()) { + $this->error = array( + "error" => "Called Send() without being connected"); + return false; + } + + fputs($this->smtp_conn,"SEND FROM:" . $from . $this->CRLF); + + $rply = $this->get_lines(); + $code = substr($rply,0,3); + + if($this->do_debug >= 2) { + echo "SMTP -> FROM SERVER:" . $this->CRLF . $rply; + } + + if($code != 250) { + $this->error = + array("error" => "SEND not accepted from server", + "smtp_code" => $code, + "smtp_msg" => substr($rply,4)); + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $this->error["error"] . + ": " . $rply . $this->CRLF; + } + return false; + } + return true; + } + + /** + * Starts a mail transaction from the email address specified in + * $from. Returns true if successful or false otherwise. If True + * the mail transaction is started and then one or more Recipient + * commands may be called followed by a Data command. This command + * will send the message to the users terminal if they are logged + * in and send them an email. + * + * Implements rfc 821: SAML FROM: + * + * SMTP CODE SUCCESS: 250 + * SMTP CODE SUCCESS: 552,451,452 + * SMTP CODE SUCCESS: 500,501,502,421 + * @access public + * @return bool + */ + function SendAndMail($from) { + $this->error = null; # so no confusion is caused + + if(!$this->connected()) { + $this->error = array( + "error" => "Called SendAndMail() without being connected"); + return false; + } + + fputs($this->smtp_conn,"SAML FROM:" . $from . $this->CRLF); + + $rply = $this->get_lines(); + $code = substr($rply,0,3); + + if($this->do_debug >= 2) { + echo "SMTP -> FROM SERVER:" . $this->CRLF . $rply; + } + + if($code != 250) { + $this->error = + array("error" => "SAML not accepted from server", + "smtp_code" => $code, + "smtp_msg" => substr($rply,4)); + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $this->error["error"] . + ": " . $rply . $this->CRLF; + } + return false; + } + return true; + } + + /** + * Starts a mail transaction from the email address specified in + * $from. Returns true if successful or false otherwise. If True + * the mail transaction is started and then one or more Recipient + * commands may be called followed by a Data command. This command + * will send the message to the users terminal if they are logged + * in or mail it to them if they are not. + * + * Implements rfc 821: SOML FROM: + * + * SMTP CODE SUCCESS: 250 + * SMTP CODE SUCCESS: 552,451,452 + * SMTP CODE SUCCESS: 500,501,502,421 + * @access public + * @return bool + */ + function SendOrMail($from) { + $this->error = null; # so no confusion is caused + + if(!$this->connected()) { + $this->error = array( + "error" => "Called SendOrMail() without being connected"); + return false; + } + + fputs($this->smtp_conn,"SOML FROM:" . $from . $this->CRLF); + + $rply = $this->get_lines(); + $code = substr($rply,0,3); + + if($this->do_debug >= 2) { + echo "SMTP -> FROM SERVER:" . $this->CRLF . $rply; + } + + if($code != 250) { + $this->error = + array("error" => "SOML not accepted from server", + "smtp_code" => $code, + "smtp_msg" => substr($rply,4)); + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $this->error["error"] . + ": " . $rply . $this->CRLF; + } + return false; + } + return true; + } + + /** + * This is an optional command for SMTP that this class does not + * support. This method is here to make the RFC821 Definition + * complete for this class and __may__ be implimented in the future + * + * Implements from rfc 821: TURN + * + * SMTP CODE SUCCESS: 250 + * SMTP CODE FAILURE: 502 + * SMTP CODE ERROR : 500, 503 + * @access public + * @return bool + */ + function Turn() { + $this->error = array("error" => "This method, TURN, of the SMTP ". + "is not implemented"); + if($this->do_debug >= 1) { + echo "SMTP -> NOTICE: " . $this->error["error"] . $this->CRLF; + } + return false; + } + + /** + * Verifies that the name is recognized by the server. + * Returns false if the name could not be verified otherwise + * the response from the server is returned. + * + * Implements rfc 821: VRFY + * + * SMTP CODE SUCCESS: 250,251 + * SMTP CODE FAILURE: 550,551,553 + * SMTP CODE ERROR : 500,501,502,421 + * @access public + * @return int + */ + function Verify($name) { + $this->error = null; # so no confusion is caused + + if(!$this->connected()) { + $this->error = array( + "error" => "Called Verify() without being connected"); + return false; + } + + fputs($this->smtp_conn,"VRFY " . $name . $this->CRLF); + + $rply = $this->get_lines(); + $code = substr($rply,0,3); + + if($this->do_debug >= 2) { + echo "SMTP -> FROM SERVER:" . $this->CRLF . $rply; + } + + if($code != 250 && $code != 251) { + $this->error = + array("error" => "VRFY failed on name '$name'", + "smtp_code" => $code, + "smtp_msg" => substr($rply,4)); + if($this->do_debug >= 1) { + echo "SMTP -> ERROR: " . $this->error["error"] . + ": " . $rply . $this->CRLF; + } + return false; + } + return $rply; + } + + /******************************************************************* + * INTERNAL FUNCTIONS * + ******************************************************************/ + + /** + * Read in as many lines as possible + * either before eof or socket timeout occurs on the operation. + * With SMTP we can tell if we have more lines to read if the + * 4th character is '-' symbol. If it is a space then we don't + * need to read anything else. + * @access private + * @return string + */ + function get_lines() { + $data = ""; + while($str = @fgets($this->smtp_conn,515)) { + if($this->do_debug >= 4) { + echo "SMTP -> get_lines(): \$data was \"$data\"" . + $this->CRLF; + echo "SMTP -> get_lines(): \$str is \"$str\"" . + $this->CRLF; + } + $data .= $str; + if($this->do_debug >= 4) { + echo "SMTP -> get_lines(): \$data is \"$data\"" . $this->CRLF; + } + # if the 4th character is a space then we are done reading + # so just break the loop + if(substr($str,3,1) == " ") { break; } + } + return $data; + } + +} + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-db-admin.php b/qa-include/qa-db-admin.php new file mode 100644 index 000000000..0fb4dfedc --- /dev/null +++ b/qa-include/qa-db-admin.php @@ -0,0 +1,544 @@ +# AND type='Q'", + $categoryid + )); + } + + + function qa_db_get_user_visible_postids($userid) +/* + Return list of postids of visible posts by $userid +*/ + { + return qa_db_read_all_values(qa_db_query_sub( + "SELECT postid FROM ^posts WHERE userid=# AND type IN ('Q', 'A', 'C')", + $userid + )); + } + + + function qa_db_get_ip_visible_postids($ip) +/* + Return list of postids of visible posts from $ip address +*/ + { + return qa_db_read_all_values(qa_db_query_sub( + "SELECT postid FROM ^posts WHERE createip=INET_ATON($) AND type IN ('Q', 'A', 'C')", + $ip + )); + } + + + function qa_db_postids_count_dependents($postids) +/* + Return an array whose keys contain the $postids which exist, and whose elements contain the number of other posts depending on each one +*/ + { + if (count($postids)) + return qa_db_read_all_assoc(qa_db_query_sub( + "SELECT postid, COALESCE(childcount, 0) AS count FROM ^posts LEFT JOIN (SELECT parentid, COUNT(*) AS childcount FROM ^posts WHERE parentid IN (#) AND LEFT(type, 1) IN ('A', 'C') GROUP BY parentid) x ON postid=x.parentid WHERE postid IN (#)", + $postids, $postids + ), 'postid', 'count'); + else + return array(); + } + + + function qa_db_category_last_pos($parentid) +/* + Return the maximum position of the categories with $parentid +*/ + { + return qa_db_read_one_value(qa_db_query_sub( + 'SELECT COALESCE(MAX(position), 0) FROM ^categories WHERE parentid<=>#', + $parentid + )); + } + + + function qa_db_category_child_depth($categoryid) +/* + Return how many levels of subcategory there are below $categoryid +*/ + { + // This is potentially a very slow query since it counts all the multi-generational offspring of a particular category + // But it's only used for admin purposes when moving a category around so I don't think it's worth making more efficient + // (Incidentally, this could be done by keeping a count for every category of how many generations of offspring it has.) + + $result=qa_db_read_one_assoc(qa_db_query_sub( + 'SELECT COUNT(child1.categoryid) AS count1, COUNT(child2.categoryid) AS count2, COUNT(child3.categoryid) AS count3 FROM ^categories AS child1 LEFT JOIN ^categories AS child2 ON child2.parentid=child1.categoryid LEFT JOIN ^categories AS child3 ON child3.parentid=child2.categoryid WHERE child1.parentid=#;', // requires QA_CATEGORY_DEPTH=4 + $categoryid + )); + + for ($depth=QA_CATEGORY_DEPTH-1; $depth>=1; $depth--) + if ($result['count'.$depth]) + return $depth; + + return 0; + } + + + function qa_db_category_create($parentid, $title, $tags) +/* + Create a new category with $parentid, $title (=name) and $tags (=slug) in the database +*/ + { + $lastpos=qa_db_category_last_pos($parentid); + + qa_db_query_sub( + 'INSERT INTO ^categories (parentid, title, tags, position) VALUES (#, $, $, #)', + $parentid, $title, $tags, 1+$lastpos + ); + + $categoryid=qa_db_last_insert_id(); + + qa_db_categories_recalc_backpaths($categoryid); + + return $categoryid; + } + + + function qa_db_categories_recalc_backpaths($firstcategoryid, $lastcategoryid=null) +/* + Recalculate the backpath columns for all categories from $firstcategoryid to $lastcategoryid (if specified) +*/ + { + if (!isset($lastcategoryid)) + $lastcategoryid=$firstcategoryid; + + qa_db_query_sub( + "UPDATE ^categories AS x, (SELECT cat1.categoryid, CONCAT_WS('/', cat1.tags, cat2.tags, cat3.tags, cat4.tags) AS backpath FROM ^categories AS cat1 LEFT JOIN ^categories AS cat2 ON cat1.parentid=cat2.categoryid LEFT JOIN ^categories AS cat3 ON cat2.parentid=cat3.categoryid LEFT JOIN ^categories AS cat4 ON cat3.parentid=cat4.categoryid WHERE cat1.categoryid BETWEEN # AND #) AS a SET x.backpath=a.backpath WHERE x.categoryid=a.categoryid", + $firstcategoryid, $lastcategoryid // requires QA_CATEGORY_DEPTH=4 + ); + } + + + function qa_db_category_rename($categoryid, $title, $tags) +/* + Set the name of $categoryid to $title and its slug to $tags in the database +*/ + { + qa_db_query_sub( + 'UPDATE ^categories SET title=$, tags=$ WHERE categoryid=#', + $title, $tags, $categoryid + ); + + qa_db_categories_recalc_backpaths($categoryid); // may also require recalculation of its offspring's backpaths + } + + + function qa_db_category_set_content($categoryid, $content) +/* + Set the content (=description) of $categoryid to $content +*/ + { + qa_db_query_sub( + 'UPDATE ^categories SET content=$ WHERE categoryid=#', + $content, $categoryid + ); + } + + + function qa_db_category_get_parent($categoryid) +/* + Return the parentid of $categoryid +*/ + { + return qa_db_read_one_value(qa_db_query_sub( + 'SELECT parentid FROM ^categories WHERE categoryid=#', + $categoryid + )); + } + + + function qa_db_category_set_position($categoryid, $newposition) +/* + Move the category $categoryid into position $newposition under its parent +*/ + { + qa_db_ordered_move('categories', 'categoryid', $categoryid, $newposition, + qa_db_apply_sub('parentid<=>#', array(qa_db_category_get_parent($categoryid)))); + } + + + function qa_db_category_set_parent($categoryid, $newparentid) +/* + Set the parent of $categoryid to $newparentid, placing it in last position (doesn't do necessary recalculations) +*/ + { + $oldparentid=qa_db_category_get_parent($categoryid); + + if (strcmp($oldparentid, $newparentid)) { // if we're changing parent, move to end of old parent, then end of new parent + $lastpos=qa_db_category_last_pos($oldparentid); + + qa_db_ordered_move('categories', 'categoryid', $categoryid, $lastpos, qa_db_apply_sub('parentid<=>#', array($oldparentid))); + + $lastpos=qa_db_category_last_pos($newparentid); + + qa_db_query_sub( + 'UPDATE ^categories SET parentid=#, position=# WHERE categoryid=#', + $newparentid, 1+$lastpos, $categoryid + ); + } + } + + + function qa_db_category_reassign($categoryid, $reassignid) +/* + Change the categoryid of any posts with (exact) $categoryid to $reassignid +*/ + { + qa_db_query_sub('UPDATE ^posts SET categoryid=# WHERE categoryid<=>#', $reassignid, $categoryid); + } + + + function qa_db_category_delete($categoryid) +/* + Delete the category $categoryid in the database +*/ + { + qa_db_ordered_delete('categories', 'categoryid', $categoryid, + qa_db_apply_sub('parentid<=>#', array(qa_db_category_get_parent($categoryid)))); + } + + + function qa_db_category_slug_to_id($parentid, $slug) +/* + Return the categoryid for the category with parent $parentid and $slug +*/ + { + return qa_db_read_one_value(qa_db_query_sub( + 'SELECT categoryid FROM ^categories WHERE parentid<=># AND tags=$', + $parentid, $slug + ), true); + } + + + function qa_db_page_create($title, $flags, $tags, $heading, $content, $permit=null) +/* + Create a new custom page (or link) in the database +*/ + { + $position=qa_db_read_one_value(qa_db_query_sub('SELECT 1+COALESCE(MAX(position), 0) FROM ^pages')); + + qa_db_query_sub( + 'INSERT INTO ^pages (title, nav, flags, permit, tags, heading, content, position) VALUES ($, \'\', #, #, $, $, $, #)', + $title, $flags, $permit, $tags, $heading, $content, $position + ); + + return qa_db_last_insert_id(); + } + + + function qa_db_page_set_fields($pageid, $title, $flags, $tags, $heading, $content, $permit=null) +/* + Set the fields of $pageid to the values provided in the database +*/ + { + qa_db_query_sub( + 'UPDATE ^pages SET title=$, flags=#, permit=#, tags=$, heading=$, content=$ WHERE pageid=#', + $title, $flags, $permit, $tags, $heading, $content, $pageid + ); + } + + + function qa_db_page_move($pageid, $nav, $newposition) +/* + Move the page $pageid into navigation menu $nav and position $newposition in the database +*/ + { + qa_db_query_sub( + 'UPDATE ^pages SET nav=$ WHERE pageid=#', + $nav, $pageid + ); + + qa_db_ordered_move('pages', 'pageid', $pageid, $newposition); + } + + + function qa_db_page_delete($pageid) +/* + Delete the page $pageid in the database +*/ + { + qa_db_ordered_delete('pages', 'pageid', $pageid); + } + + + function qa_db_ordered_move($table, $idcolumn, $id, $newposition, $conditionsql=null) +/* + Move the entity identified by $idcolumn=$id into position $newposition (within optional $conditionsql) in $table in the database +*/ + { + $andsql=isset($conditionsql) ? (' AND '.$conditionsql) : ''; + + qa_db_query_sub('LOCK TABLES ^'.$table.' WRITE'); + + $oldposition=qa_db_read_one_value(qa_db_query_sub('SELECT position FROM ^'.$table.' WHERE '.$idcolumn.'=#'.$andsql, $id)); + + if ($newposition!=$oldposition) { + $lastposition=qa_db_read_one_value(qa_db_query_sub('SELECT MAX(position) FROM ^'.$table.' WHERE TRUE'.$andsql)); + + $newposition=max(1, min($newposition, $lastposition)); // constrain it to within range + + qa_db_query_sub('UPDATE ^'.$table.' SET position=# WHERE '.$idcolumn.'=#'.$andsql, 1+$lastposition, $id); + // move it temporarily off the top because we have a unique key on the position column + + if ($newposition<$oldposition) + qa_db_query_sub('UPDATE ^'.$table.' SET position=position+1 WHERE position BETWEEN # AND #'.$andsql.' ORDER BY position DESC', $newposition, $oldposition); + else + qa_db_query_sub('UPDATE ^'.$table.' SET position=position-1 WHERE position BETWEEN # AND #'.$andsql.' ORDER BY position', $oldposition, $newposition); + + qa_db_query_sub('UPDATE ^'.$table.' SET position=# WHERE '.$idcolumn.'=#'.$andsql, $newposition, $id); + } + + qa_db_query_sub('UNLOCK TABLES'); + } + + + function qa_db_ordered_delete($table, $idcolumn, $id, $conditionsql=null) +/* + Delete the entity identified by $idcolumn=$id (and optional $conditionsql) in $table in the database +*/ + { + $andsql=isset($conditionsql) ? (' AND '.$conditionsql) : ''; + + qa_db_query_sub('LOCK TABLES ^'.$table.' WRITE'); + + $oldposition=qa_db_read_one_value(qa_db_query_sub('SELECT position FROM ^'.$table.' WHERE '.$idcolumn.'=#'.$andsql, $id)); + + qa_db_query_sub('DELETE FROM ^'.$table.' WHERE '.$idcolumn.'=#'.$andsql, $id); + + qa_db_query_sub('UPDATE ^'.$table.' SET position=position-1 WHERE position>#'.$andsql.' ORDER BY position', $oldposition); + + qa_db_query_sub('UNLOCK TABLES'); + } + + + function qa_db_userfield_create($title, $content, $flags) +/* + Create a new user field with (internal) tag $title, label $content, and $flags in the database +*/ + { + $position=qa_db_read_one_value(qa_db_query_sub('SELECT 1+COALESCE(MAX(position), 0) FROM ^userfields')); + + qa_db_query_sub( + 'INSERT INTO ^userfields (title, content, position, flags) VALUES ($, $, #, #)', + $title, $content, $position, $flags + ); + + return qa_db_last_insert_id(); + } + + + function qa_db_userfield_set_fields($fieldid, $content, $flags) +/* + Change the user field $fieldid to have label $content and $flags in the database (the title column cannot be changed once set) +*/ + { + qa_db_query_sub( + 'UPDATE ^userfields SET content=$, flags=# WHERE fieldid=#', + $content, $flags, $fieldid + ); + } + + + function qa_db_userfield_move($fieldid, $newposition) +/* + Move the user field $fieldid into position $newposition in the database +*/ + { + qa_db_ordered_move('userfields', 'fieldid', $fieldid, $newposition); + } + + + function qa_db_userfield_delete($fieldid) +/* + Delete the user field $fieldid in the database +*/ + { + qa_db_ordered_delete('userfields', 'fieldid', $fieldid); + } + + + function qa_db_widget_create($title, $tags) +/* + Return the ID of a new widget, to be displayed by the widget module named $title on templates within $tags (comma-separated list) +*/ + { + $position=qa_db_read_one_value(qa_db_query_sub('SELECT 1+COALESCE(MAX(position), 0) FROM ^widgets')); + + qa_db_query_sub( + 'INSERT INTO ^widgets (place, position, tags, title) VALUES (\'\', #, $, $)', + $position, $tags, $title + ); + + return qa_db_last_insert_id(); + } + + + function qa_db_widget_set_fields($widgetid, $tags) +/* + Set the comma-separated list of templates for $widgetid to $tags +*/ + { + qa_db_query_sub( + 'UPDATE ^widgets SET tags=$ WHERE widgetid=#', + $tags, $widgetid + ); + } + + + function qa_db_widget_move($widgetid, $place, $newposition) +/* + Move the widget $widgetit into position $position in the database's order, and show it in $place on the page +*/ + { + qa_db_query_sub( + 'UPDATE ^widgets SET place=$ WHERE widgetid=#', + $place, $widgetid + ); + + qa_db_ordered_move('widgets', 'widgetid', $widgetid, $newposition); + } + + + function qa_db_widget_delete($widgetid) +/* + Delete the widget $widgetid in the database +*/ + { + qa_db_ordered_delete('widgets', 'widgetid', $widgetid); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-db-blobs.php b/qa-include/qa-db-blobs.php new file mode 100644 index 000000000..8aa13a979 --- /dev/null +++ b/qa-include/qa-db-blobs.php @@ -0,0 +1,102 @@ + 0; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-db-cache.php b/qa-include/qa-db-cache.php new file mode 100644 index 000000000..940a315ed --- /dev/null +++ b/qa-include/qa-db-cache.php @@ -0,0 +1,78 @@ + 0; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-db-events.php b/qa-include/qa-db-events.php new file mode 100644 index 000000000..19a166668 --- /dev/null +++ b/qa-include/qa-db-events.php @@ -0,0 +1,183 @@ + T=sqrt(M) + + So we could keep track of the maximum number of event streams any user is subscribed to, and use its square root. + Instead of that, we adopt an on-the-fly approach. We start by setting T=10 (see 'max_copy_user_updates' in + qa-app-options.php) since it's no big deal to write 10 rows to a table. Recall that whenever an event stream gets + more than T subscribers, we switch those subscribers over to the shared stream. At that point, we check the maximum + number of (total) shared streams that any of those users are subscribed to. If this is above T, that means that our + maximum cost of retrieving a list of news updates is starting to go past our maximum cost of recording an event. So + we rebalance things out by increasing T as appropriate, for use in future cases. + + Note that once an event stream has made this switch, to be accessed only via its shared stream, we don't go back. +*/ + + + function qa_db_favorite_create($userid, $entitytype, $entityid) +/* + Add the entity $entitytype with $entityid to the favorites list of $userid. Handles switching streams across from + per-user to per-entity based on how many other users have favorited the entity (see long explanation above). If + appropriate, it also adds recent events from that entity to the user's event stream. +*/ + { + $threshold=qa_opt('max_copy_user_updates'); // if this many users subscribe to it, create a shared stream + + // Add in the favorite for this user, unshared events at first (will be switched later if appropriate) + + qa_db_query_sub( + 'INSERT IGNORE INTO ^userfavorites (userid, entitytype, entityid, nouserevents) VALUES ($, $, #, 0)', + $userid, $entitytype, $entityid + ); + + // See whether this entity already has another favoriter who uses its shared event stream + + $useshared=qa_db_read_one_value(qa_db_query_sub( + 'SELECT COUNT(*) FROM ^userfavorites WHERE entitytype=$ AND entityid=# AND nouserevents>0 LIMIT 1', + $entitytype, $entityid + )); + + // If not, check whether it's time to switch it over to a shared stream + + if (!$useshared) { + $favoriters=qa_db_read_one_value(qa_db_query_sub( + 'SELECT COUNT(*) FROM ^userfavorites WHERE entitytype=$ AND entityid=# LIMIT #', + $entitytype, $entityid, $threshold + )); + + $useshared=($favoriters >= $threshold); + } + + // If we're going to use the shared stream... + + if ($useshared) { + + // ... for all the people for whom we're switching this to a shared stream, find the highest number of other shared streams they have + + $maxshared=qa_db_read_one_value(qa_db_query_sub( + 'SELECT MAX(c) FROM (SELECT COUNT(*) AS c FROM ^userfavorites AS shared JOIN ^userfavorites AS unshared '. + 'WHERE shared.userid=unshared.userid AND shared.nouserevents>0 AND unshared.entitytype=$ AND unshared.entityid=# AND unshared.nouserevents=0 GROUP BY shared.userid) y', + $entitytype, $entityid + )); + + // ... if this number is greater than our current 'max_copy_user_updates' threshold, increase that threshold (see long comment above) + + if (($maxshared+1)>$threshold) + qa_opt('max_copy_user_updates', $maxshared+1); + + // ... now switch all unshared favoriters (including this new one) over to be shared + + qa_db_query_sub( + 'UPDATE ^userfavorites SET nouserevents=1 WHERE entitytype=$ AND entityid=# AND nouserevents=0', + $entitytype, $entityid + ); + + // Otherwise if we're going to record this in user-specific streams ... + + } else { + require_once QA_INCLUDE_DIR.'qa-db-events.php'; + + // ... copy across recent events from the shared stream + + qa_db_query_sub( + 'INSERT INTO ^userevents (userid, entitytype, entityid, questionid, lastpostid, updatetype, lastuserid, updated) '. + 'SELECT #, entitytype, entityid, questionid, lastpostid, updatetype, lastuserid, updated FROM '. + '^sharedevents WHERE entitytype=$ AND entityid=#', + $userid, $entitytype, $entityid + ); + + // ... and truncate the user's stream as appropriate + + qa_db_user_events_truncate($userid); + } + } + + + function qa_db_favorite_delete($userid, $entitytype, $entityid) +/* + Delete the entity $entitytype with $entityid from the favorites list of $userid, removing any corresponding events + from the user's stream. +*/ + { + qa_db_query_sub( + 'DELETE FROM ^userfavorites WHERE userid=$ AND entitytype=$ AND entityid=#', + $userid, $entitytype, $entityid + ); + + qa_db_query_sub( + 'DELETE FROM ^userevents WHERE userid=$ AND entitytype=$ AND entityid=#', + $userid, $entitytype, $entityid + ); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-db-hotness.php b/qa-include/qa-db-hotness.php new file mode 100644 index 000000000..c7105f51f --- /dev/null +++ b/qa-include/qa-db-hotness.php @@ -0,0 +1,76 @@ +=# AND parents.postid<=# AND LEFT(parents.type, 1)='Q' GROUP BY postid) AS a SET x.hotness=(". + '((TO_DAYS(a.qcreated)-734138)*86400.0+TIME_TO_SEC(a.qcreated))*# + '. // zero-point is Jan 1, 2010 + '((TO_DAYS(a.acreated)-734138)*86400.0+TIME_TO_SEC(a.acreated))*# + '. + '(a.acount+0.0)*# + '. + '(a.netvotes+0.0)*# + '. + '(a.views+0.0+#)*#'. + ')'.($viewincrement ? ', x.views=x.views+1, x.lastviewip=INET_ATON($)' : '').' WHERE x.postid=a.postid'; + + // Additional multiples based on empirical analysis of activity on Q2A meta site to give approx equal influence for all factors + + $arguments=array( + $firstpostid, + $lastpostid, + qa_opt('hot_weight_q_age'), + qa_opt('hot_weight_a_age'), + qa_opt('hot_weight_answers')*160000, + qa_opt('hot_weight_votes')*160000, + $viewincrement ? 1 : 0, + qa_opt('hot_weight_views')*4000, + ); + + if ($viewincrement) + $arguments[]=qa_remote_ip_address(); + + qa_db_query_raw(qa_db_apply_sub($query, $arguments)); + } + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-db-install.php b/qa-include/qa-db-install.php new file mode 100644 index 000000000..9f6f8a2f0 --- /dev/null +++ b/qa-include/qa-db-install.php @@ -0,0 +1,1319 @@ + [definition]. The column name is omitted for indexes. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + require_once QA_INCLUDE_DIR.'qa-db-maxima.php'; + require_once QA_INCLUDE_DIR.'qa-app-users.php'; + + /* + Important note on character encoding in database and PHP connection to MySQL + + [this note is no longer relevant since we *do* explicitly set the connection character set since Q2A 1.5 - see qa-db.php + */ + + /* + Other notes on the definitions below + + * In MySQL versions prior to 5.0.3, VARCHAR(x) columns will be silently converted to TEXT where x>255 + + * See box at top of qa-app-recalc.php for a list of redundant (non-normal) information in the database + + * Starting in version 1.2, we explicitly name keys and foreign key constraints, instead of allowing MySQL + to name these by default. Our chosen names match the default names that MySQL would have assigned, and + indeed *did* assign for people who installed an earlier version of Q2A. By naming them explicitly, we're + on more solid ground for possible future changes to indexes and foreign keys in the schema. + + * There are other foreign key constraints that it would be valid to add, but that would not serve much + purpose in terms of preventing inconsistent data being retrieved, and would just slow down some queries. + + * We name some columns here in a not entirely intuitive way. The reason is to match the names of columns in + other tables which are of a similar nature. This will save time and space when combining several SELECT + queries together via a UNION in qa_db_multi_select() - see comments in qa-db.php for more information. + */ + + $useridcoltype=qa_db_user_column_type_verify(); + + $tables=array( + 'users' => array( + 'userid' => $useridcoltype.' NOT NULL AUTO_INCREMENT', + 'created' => 'DATETIME NOT NULL', + 'createip' => 'INT UNSIGNED NOT NULL', // INET_ATON of IP address when created + 'email' => 'VARCHAR('.QA_DB_MAX_EMAIL_LENGTH.') NOT NULL', + 'handle' => 'VARCHAR('.QA_DB_MAX_HANDLE_LENGTH.') NOT NULL', // username + 'avatarblobid' => 'BIGINT UNSIGNED', // blobid of stored avatar + 'avatarwidth' => 'SMALLINT UNSIGNED', // pixel width of stored avatar + 'avatarheight' => 'SMALLINT UNSIGNED', // pixel height of stored avatar + 'passsalt' => 'BINARY(16)', // salt used to calculate passcheck - null if no passowrd set for direct login + 'passcheck' => 'BINARY(20)', // checksum from password and passsalt - null if no passowrd set for direct login + 'level' => 'TINYINT UNSIGNED NOT NULL', // basic, editor, admin, etc... + 'loggedin' => 'DATETIME NOT NULL', // time of last login + 'loginip' => 'INT UNSIGNED NOT NULL', // INET_ATON of IP address of last login + 'written' => 'DATETIME', // time of last write action done by user + 'writeip' => 'INT UNSIGNED', // INET_ATON of IP address of last write action done by user + 'emailcode' => 'CHAR(8) CHARACTER SET ascii NOT NULL DEFAULT \'\'', // for email confirmation or password reset + 'sessioncode' => 'CHAR(8) CHARACTER SET ascii NOT NULL DEFAULT \'\'', // for comparing against session cookie in browser + 'sessionsource' => 'VARCHAR (16) CHARACTER SET ascii DEFAULT \'\'', // e.g. facebook, openid, etc... + 'flags' => 'TINYINT UNSIGNED NOT NULL DEFAULT 0', // email confirmed, user blocked, show avatar/gravatar, accept direct messages + 'PRIMARY KEY (userid)', + 'KEY email (email)', + 'KEY handle (handle)', + 'KEY level (level)', + ), + + 'userlogins' => array( + 'userid' => $useridcoltype.' NOT NULL', + 'source' => 'VARCHAR (16) CHARACTER SET ascii NOT NULL', // e.g. facebook, openid, etc... + 'identifier' => 'VARBINARY (1024) NOT NULL', // depends on source, e.g. Facebook uid or OpenID url + 'identifiermd5' => 'BINARY (16) NOT NULL', // used to reduce size of index on identifier + 'KEY source (source, identifiermd5)', + 'KEY userid (userid)', + ), + + 'userprofile' => array( + 'userid' => $useridcoltype.' NOT NULL', + 'title' => 'VARCHAR('.QA_DB_MAX_PROFILE_TITLE_LENGTH.') NOT NULL', // profile field name + 'content' => 'VARCHAR('.QA_DB_MAX_PROFILE_CONTENT_LENGTH.') NOT NULL', // profile field value + 'UNIQUE userid (userid,title)', + ), + + 'userfields' => array( + 'fieldid' => 'SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT', + 'title' => 'VARCHAR('.QA_DB_MAX_PROFILE_TITLE_LENGTH.') NOT NULL', // to match title column in userprofile table + 'content' => 'VARCHAR('.QA_DB_MAX_PROFILE_TITLE_LENGTH.')', // for display on user profile pages - NULL means use default + 'position' => 'SMALLINT UNSIGNED NOT NULL', + 'flags' => 'TINYINT UNSIGNED NOT NULL', + 'PRIMARY KEY (fieldid)', + ), + + 'messages' => array( + 'messageid' => 'INT UNSIGNED NOT NULL AUTO_INCREMENT', + 'fromuserid' => $useridcoltype.' NOT NULL', + 'touserid' => $useridcoltype.' NOT NULL', + 'content' => 'VARCHAR('.QA_DB_MAX_CONTENT_LENGTH.') NOT NULL', + 'format' => 'VARCHAR('.QA_DB_MAX_FORMAT_LENGTH.') CHARACTER SET ascii NOT NULL', + 'created' => 'DATETIME NOT NULL', + 'PRIMARY KEY (messageid)', + 'KEY fromuserid (fromuserid, touserid, created)', + ), + + 'userfavorites' => array( + 'userid' => $useridcoltype.' NOT NULL', // the user who favorited the entity + 'entitytype' => "CHAR(1) CHARACTER SET ascii NOT NULL", // see qa-app-updates.php + 'entityid' => 'INT UNSIGNED NOT NULL', // favorited postid / userid / tag wordid / categoryid + 'nouserevents' => 'TINYINT UNSIGNED NOT NULL', // do we skip writing events to the user stream? + 'PRIMARY KEY (userid, entitytype, entityid)', + 'KEY userid (userid, nouserevents)', + 'KEY entitytype (entitytype, entityid, nouserevents)', + ), + + 'usernotices' => array( + 'noticeid' => 'INT UNSIGNED NOT NULL AUTO_INCREMENT', + 'userid' => $useridcoltype.' NOT NULL', // the user to whom the notice is directed + 'content' => 'VARCHAR('.QA_DB_MAX_CONTENT_LENGTH.') NOT NULL', + 'format' => 'VARCHAR('.QA_DB_MAX_FORMAT_LENGTH.') CHARACTER SET ascii NOT NULL', + 'tags' => 'VARCHAR('.QA_DB_MAX_CAT_PAGE_TAGS_LENGTH.')', // any additional information for a plugin to access + 'created' => 'DATETIME NOT NULL', + 'PRIMARY KEY (noticeid)', + 'KEY userid (userid, created)', + ), + + 'userevents' => array( + 'userid' => $useridcoltype.' NOT NULL', // the user to be informed about this event in their updates + 'entitytype' => "CHAR(1) CHARACTER SET ascii NOT NULL", // see qa-app-updates.php + 'entityid' => 'INT UNSIGNED NOT NULL', // favorited source of event - see userfavorites table - 0 means not from a favorite + 'questionid' => 'INT UNSIGNED NOT NULL', // the affected question + 'lastpostid' => 'INT UNSIGNED NOT NULL', // what part of question was affected + 'updatetype' => 'CHAR(1) CHARACTER SET ascii', // what was done to this part - see qa-app-updates.php + 'lastuserid' => $useridcoltype, // which user (if any) did this action + 'updated' => 'DATETIME NOT NULL', // when the event happened + 'KEY userid (userid, updated)', // for truncation + 'KEY questionid (questionid, userid)', // to limit number of events per question per stream + ), + + 'sharedevents' => array( + 'entitytype' => "CHAR(1) CHARACTER SET ascii NOT NULL", // see qa-app-updates.php + 'entityid' => 'INT UNSIGNED NOT NULL', // see userfavorites table + 'questionid' => 'INT UNSIGNED NOT NULL', // see userevents table + 'lastpostid' => 'INT UNSIGNED NOT NULL', // see userevents table + 'updatetype' => 'CHAR(1) CHARACTER SET ascii', // see userevents table + 'lastuserid' => $useridcoltype, // see userevents table + 'updated' => 'DATETIME NOT NULL', // see userevents table + 'KEY entitytype (entitytype, entityid, updated)', // for truncation + 'KEY questionid (questionid, entitytype, entityid)', // to limit number of events per question per stream + ), + + 'cookies' => array( + 'cookieid' => 'BIGINT UNSIGNED NOT NULL', + 'created' => 'DATETIME NOT NULL', + 'createip' => 'INT UNSIGNED NOT NULL', // INET_ATON of IP address when cookie created + 'written' => 'DATETIME', // time of last write action done by anon user with cookie + 'writeip' => 'INT UNSIGNED', // INET_ATON of IP address of last write action done by anon user with cookie + 'PRIMARY KEY (cookieid)', + ), + + 'categories' => array( + 'categoryid' => 'INT UNSIGNED NOT NULL AUTO_INCREMENT', + 'parentid' => 'INT UNSIGNED', + 'title' => 'VARCHAR('.QA_DB_MAX_CAT_PAGE_TITLE_LENGTH.') NOT NULL', // category name + 'tags' => 'VARCHAR('.QA_DB_MAX_CAT_PAGE_TAGS_LENGTH.') NOT NULL', // slug (url fragment) used to identify category + 'content' => 'VARCHAR('.QA_DB_MAX_CAT_CONTENT_LENGTH.') NOT NULL DEFAULT \'\'', // description of category + 'qcount' => 'INT UNSIGNED NOT NULL DEFAULT 0', + 'position' => 'SMALLINT UNSIGNED NOT NULL', + 'backpath' => 'VARCHAR('.(QA_CATEGORY_DEPTH*(QA_DB_MAX_CAT_PAGE_TAGS_LENGTH+1)).') NOT NULL DEFAULT \'\'', + // full slug path for category, with forward slash separators, in reverse order to make index from effective + 'PRIMARY KEY (categoryid)', + 'UNIQUE parentid (parentid, tags)', + 'UNIQUE parentid_2 (parentid, position)', + 'KEY backpath (backpath('.QA_DB_MAX_CAT_PAGE_TAGS_LENGTH.'))', + ), + + 'pages' => array( + 'pageid' => 'SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT', + 'title' => 'VARCHAR('.QA_DB_MAX_CAT_PAGE_TITLE_LENGTH.') NOT NULL', // title for navigation + 'nav' => 'CHAR(1) CHARACTER SET ascii NOT NULL', // which navigation does it go in (M=main, F=footer, B=before main, O=opposite main, other=none) + 'position' => 'SMALLINT UNSIGNED NOT NULL', // global ordering, which allows links to be ordered within each nav area + 'flags' => 'TINYINT UNSIGNED NOT NULL', // local or external, open in new window? + 'permit' => 'TINYINT UNSIGNED', // is there a minimum user level required for it (uses QA_PERMIT_* constants) + 'tags' => 'VARCHAR('.QA_DB_MAX_CAT_PAGE_TAGS_LENGTH.') NOT NULL', // slug (url fragment) for page, or url for external pages + 'heading' => 'VARCHAR('.QA_DB_MAX_TITLE_LENGTH.')', // for display within

tags + 'content' => 'MEDIUMTEXT', // remainder of page HTML + 'PRIMARY KEY (pageid)', + 'UNIQUE tags (tags)', + 'UNIQUE position (position)', + ), + + 'widgets' => array( + 'widgetid' => 'SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT', + 'place' => 'CHAR(2) CHARACTER SET ascii NOT NULL', + // full region: FT=very top of page, FH=below nav area, FL=above footer, FB = very bottom of page + // side region: ST=top of side, SH=below sidebar, SL=below categories, SB=very bottom of side + // main region: MT=top of main, MH=below page title, ML=above links, MB=very bottom of main region + 'position' => 'SMALLINT UNSIGNED NOT NULL', // global ordering, which allows widgets to be ordered within each place + 'tags' => 'VARCHAR('.QA_DB_MAX_WIDGET_TAGS_LENGTH.') CHARACTER SET ascii NOT NULL', // comma-separated list of templates to display on + 'title' => 'VARCHAR('.QA_DB_MAX_WIDGET_TITLE_LENGTH.') NOT NULL', // name of widget module that should be displayed + 'PRIMARY KEY (widgetid)', + 'UNIQUE position (position)', + ), + + 'posts' => array( + 'postid' => 'INT UNSIGNED NOT NULL AUTO_INCREMENT', + 'type' => "ENUM('Q', 'A', 'C', 'Q_HIDDEN', 'A_HIDDEN', 'C_HIDDEN', 'Q_QUEUED', 'A_QUEUED', 'C_QUEUED', 'NOTE') NOT NULL", + 'parentid' => 'INT UNSIGNED', // for follow on questions, all answers and comments + 'categoryid' => 'INT UNSIGNED', // this is the canonical final category id + 'catidpath1' => 'INT UNSIGNED', // the catidpath* columns are calculated from categoryid, for the full hierarchy of that category + 'catidpath2' => 'INT UNSIGNED', // note that QA_CATEGORY_DEPTH=4 + 'catidpath3' => 'INT UNSIGNED', + 'acount' => 'SMALLINT UNSIGNED NOT NULL DEFAULT 0', // number of answers (for questions) + 'amaxvote' => 'SMALLINT UNSIGNED NOT NULL DEFAULT 0', // highest netvotes of child answers (for questions) + 'selchildid' => 'INT UNSIGNED', // selected answer (for questions) + 'closedbyid' => 'INT UNSIGNED', // not null means question is closed + // if closed due to being a duplicate, this is the postid of that other question + // if closed for another reason, that reason should be added as a comment on the question, and this field is the comment's id + 'userid' => $useridcoltype, // which user wrote it + 'cookieid' => 'BIGINT UNSIGNED', // which cookie wrote it, if an anonymous post + 'createip' => 'INT UNSIGNED', // INET_ATON of IP address used to create the post + 'lastuserid' => $useridcoltype, // which user last modified it + 'lastip' => 'INT UNSIGNED', // INET_ATON of IP address which last modified the post + 'upvotes' => 'SMALLINT UNSIGNED NOT NULL DEFAULT 0', + 'downvotes' => 'SMALLINT UNSIGNED NOT NULL DEFAULT 0', + 'netvotes' => 'SMALLINT NOT NULL DEFAULT 0', + 'lastviewip' => 'INT UNSIGNED', // INET_ATON of IP address which last viewed the post + 'views' => 'INT UNSIGNED NOT NULL DEFAULT 0', + 'hotness' => 'FLOAT', + 'flagcount' => 'TINYINT UNSIGNED NOT NULL DEFAULT 0', + 'format' => 'VARCHAR('.QA_DB_MAX_FORMAT_LENGTH.') CHARACTER SET ascii NOT NULL DEFAULT \'\'', // format of content, e.g. 'html' + 'created' => 'DATETIME NOT NULL', + 'updated' => 'DATETIME', // time of last update + 'updatetype' => 'CHAR(1) CHARACTER SET ascii', // see qa-app-updates.php + 'title' => 'VARCHAR('.QA_DB_MAX_TITLE_LENGTH.')', + 'content' => 'VARCHAR('.QA_DB_MAX_CONTENT_LENGTH.')', + 'tags' => 'VARCHAR('.QA_DB_MAX_TAGS_LENGTH.')', // string of tags separated by commas + 'notify' => 'VARCHAR('.QA_DB_MAX_EMAIL_LENGTH.')', // email address, or @ to get from user, or NULL for none + 'PRIMARY KEY (postid)', + 'KEY type (type, created)', // for getting recent questions, answers, comments + 'KEY type_2 (type, acount, created)', // for getting unanswered questions + 'KEY type_4 (type, netvotes, created)', // for getting posts with the most votes + 'KEY type_5 (type, views, created)', // for getting questions with the most views + 'KEY type_6 (type, hotness)', // for getting 'hot' questions + 'KEY type_7 (type, amaxvote, created)', // for getting questions with no upvoted answers + 'KEY parentid (parentid, type)', // for getting a question's answers, any post's comments and follow-on questions + 'KEY userid (userid, type, created)', // for recent questions, answers or comments by a user + 'KEY selchildid (selchildid, type, created)', // for counting how many of a user's answers have been selected, unselected qs + 'KEY closedbyid (closedbyid)', // for the foreign key constraint + 'KEY catidpath1 (catidpath1, type, created)', // for getting question, answers or comments in a specific level category + 'KEY catidpath2 (catidpath2, type, created)', // note that QA_CATEGORY_DEPTH=4 + 'KEY catidpath3 (catidpath3, type, created)', + 'KEY categoryid (categoryid, type, created)', // this can also be used for searching the equivalent of catidpath4 + 'KEY createip (createip, created)', // for getting posts created by a specific IP address + 'KEY updated (updated, type)', // for getting recent edits across all categories + 'KEY flagcount (flagcount, created, type)', // for getting posts with the most flags + 'KEY catidpath1_2 (catidpath1, updated, type)', // for getting recent edits in a specific level category + 'KEY catidpath2_2 (catidpath2, updated, type)', // note that QA_CATEGORY_DEPTH=4 + 'KEY catidpath3_2 (catidpath3, updated, type)', + 'KEY categoryid_2 (categoryid, updated, type)', + 'KEY lastuserid (lastuserid, updated, type)', // for getting posts edited by a specific user + 'KEY lastip (lastip, updated, type)', // for getting posts edited by a specific IP address + 'CONSTRAINT ^posts_ibfk_2 FOREIGN KEY (parentid) REFERENCES ^posts(postid)', // ^posts_ibfk_1 is set later on userid + 'CONSTRAINT ^posts_ibfk_3 FOREIGN KEY (categoryid) REFERENCES ^categories(categoryid) ON DELETE SET NULL', + 'CONSTRAINT ^posts_ibfk_4 FOREIGN KEY (closedbyid) REFERENCES ^posts(postid)', + ), + + 'blobs' => array( + 'blobid' => 'BIGINT UNSIGNED NOT NULL', + 'format' => 'VARCHAR('.QA_DB_MAX_FORMAT_LENGTH.') CHARACTER SET ascii NOT NULL', // format e.g. 'jpeg', 'gif', 'png' + 'content' => 'MEDIUMBLOB NOT NULL', + 'filename' => 'VARCHAR('.QA_DB_MAX_BLOB_FILE_NAME_LENGTH.')', // name of source file (if appropriate) + 'userid' => $useridcoltype, // which user created it + 'cookieid' => 'BIGINT UNSIGNED', // which cookie created it + 'createip' => 'INT UNSIGNED', // INET_ATON of IP address that created it + 'created' => 'DATETIME', // when it was created + 'PRIMARY KEY (blobid)', + ), + + 'words' => array( + 'wordid' => 'INT UNSIGNED NOT NULL AUTO_INCREMENT', + 'word' => 'VARCHAR('.QA_DB_MAX_WORD_LENGTH.') NOT NULL', + 'titlecount' => 'INT UNSIGNED NOT NULL DEFAULT 0', // only counts one per post + 'contentcount' => 'INT UNSIGNED NOT NULL DEFAULT 0', // only counts one per post + 'tagwordcount' => 'INT UNSIGNED NOT NULL DEFAULT 0', // for words in tags - only counts one per post + 'tagcount' => 'INT UNSIGNED NOT NULL DEFAULT 0', // for tags as a whole - only counts one per post (though no duplicate tags anyway) + 'PRIMARY KEY (wordid)', + 'KEY word (word)', + 'KEY tagcount (tagcount)', // for sorting by most popular tags + ), + + 'titlewords' => array( + 'postid' => 'INT UNSIGNED NOT NULL', + 'wordid' => 'INT UNSIGNED NOT NULL', + 'KEY postid (postid)', + 'KEY wordid (wordid)', + 'CONSTRAINT ^titlewords_ibfk_1 FOREIGN KEY (postid) REFERENCES ^posts(postid) ON DELETE CASCADE', + 'CONSTRAINT ^titlewords_ibfk_2 FOREIGN KEY (wordid) REFERENCES ^words(wordid)', + ), + + 'contentwords' => array( + 'postid' => 'INT UNSIGNED NOT NULL', + 'wordid' => 'INT UNSIGNED NOT NULL', + 'count' => 'TINYINT UNSIGNED NOT NULL', // how many times word appears in the post - anything over 255 can be ignored + 'type' => "ENUM('Q', 'A', 'C', 'NOTE') NOT NULL", // the post's type (copied here for quick searching) + 'questionid' => 'INT UNSIGNED NOT NULL', // the id of the post's antecedent parent (here for quick searching) + 'KEY postid (postid)', + 'KEY wordid (wordid)', + 'CONSTRAINT ^contentwords_ibfk_1 FOREIGN KEY (postid) REFERENCES ^posts(postid) ON DELETE CASCADE', + 'CONSTRAINT ^contentwords_ibfk_2 FOREIGN KEY (wordid) REFERENCES ^words(wordid)', + ), + + 'tagwords' => array( + 'postid' => 'INT UNSIGNED NOT NULL', + 'wordid' => 'INT UNSIGNED NOT NULL', + 'KEY postid (postid)', + 'KEY wordid (wordid)', + 'CONSTRAINT ^tagwords_ibfk_1 FOREIGN KEY (postid) REFERENCES ^posts(postid) ON DELETE CASCADE', + 'CONSTRAINT ^tagwords_ibfk_2 FOREIGN KEY (wordid) REFERENCES ^words(wordid)', + ), + + 'posttags' => array( + 'postid' => 'INT UNSIGNED NOT NULL', + 'wordid' => 'INT UNSIGNED NOT NULL', + 'postcreated' => 'DATETIME NOT NULL', // created time of post (copied here for tag page's list of recent questions) + 'KEY postid (postid)', + 'KEY wordid (wordid,postcreated)', + 'CONSTRAINT ^posttags_ibfk_1 FOREIGN KEY (postid) REFERENCES ^posts(postid) ON DELETE CASCADE', + 'CONSTRAINT ^posttags_ibfk_2 FOREIGN KEY (wordid) REFERENCES ^words(wordid)', + ), + + 'uservotes' => array( + 'postid' => 'INT UNSIGNED NOT NULL', + 'userid' => $useridcoltype.' NOT NULL', + 'vote' => 'TINYINT NOT NULL', // -1, 0 or 1 + 'flag' => 'TINYINT NOT NULL', // 0 or 1 + 'UNIQUE userid (userid, postid)', + 'KEY postid (postid)', + 'CONSTRAINT ^uservotes_ibfk_1 FOREIGN KEY (postid) REFERENCES ^posts(postid) ON DELETE CASCADE', + ), + + // many userpoints columns could be unsigned but MySQL appears to mess up points calculations that go negative as a result + + 'userpoints' => array( + 'userid' => $useridcoltype.' NOT NULL', + 'points' => 'INT NOT NULL DEFAULT 0', // user's points as displayed, after final multiple + 'qposts' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of questions by user (excluding hidden/queued) + 'aposts' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of answers by user (excluding hidden/queued) + 'cposts' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of comments by user (excluding hidden/queued) + 'aselects' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of questions by user where they've selected an answer + 'aselecteds' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of answers by user that have been selected as the best + 'qupvotes' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of questions the user has voted up + 'qdownvotes' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of questions the user has voted down + 'aupvotes' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of answers the user has voted up + 'adownvotes' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of answers the user has voted down + 'qvoteds' => 'INT NOT NULL DEFAULT 0', // points from votes on this user's questions (applying per-question limits), before final multiple + 'avoteds' => 'INT NOT NULL DEFAULT 0', // points from votes on this user's answers (applying per-answer limits), before final multiple + 'upvoteds' => 'INT NOT NULL DEFAULT 0', // number of up votes received on this user's questions or answers + 'downvoteds' => 'INT NOT NULL DEFAULT 0', // number of down votes received on this user's questions or answers + 'bonus' => 'INT NOT NULL DEFAULT 0', // bonus assigned by administrator to a user + 'PRIMARY KEY (userid)', + 'KEY points (points)', + ), + + 'userlimits' => array( + 'userid' => $useridcoltype.' NOT NULL', + 'action' => 'CHAR(1) CHARACTER SET ascii NOT NULL', // see constants at top of qa-app-limits.php + 'period' => 'INT UNSIGNED NOT NULL', // integer representing hour of last action + 'count' => 'SMALLINT UNSIGNED NOT NULL', // how many of this action has been performed within that hour + 'UNIQUE userid (userid, action)', + ), + + // most columns in iplimits have the same meaning as those in userlimits + + 'iplimits' => array( + 'ip' => 'INT UNSIGNED NOT NULL', // INET_ATON of IP address + 'action' => 'CHAR(1) CHARACTER SET ascii NOT NULL', + 'period' => 'INT UNSIGNED NOT NULL', + 'count' => 'SMALLINT UNSIGNED NOT NULL', + 'UNIQUE ip (ip, action)', + ), + + 'options' => array( + 'title' => 'VARCHAR('.QA_DB_MAX_OPTION_TITLE_LENGTH.') NOT NULL', // name of option + 'content' => 'VARCHAR('.QA_DB_MAX_CONTENT_LENGTH.') NOT NULL', // value of option + 'PRIMARY KEY (title)', + ), + + 'cache' => array( + 'type' => 'CHAR(8) CHARACTER SET ascii NOT NULL', // e.g. 'avXXX' for avatar sized to XXX pixels square + 'cacheid' => 'BIGINT UNSIGNED DEFAULT 0', // optional further identifier, e.g. blobid on which cache entry is based + 'content' => 'MEDIUMBLOB NOT NULL', + 'created' => 'DATETIME NOT NULL', + 'lastread' => 'DATETIME NOT NULL', + 'PRIMARY KEY (type,cacheid)', + 'KEY (lastread)', + ), + + 'usermetas' => array( + 'userid' => $useridcoltype.' NOT NULL', + 'title' => 'VARCHAR('.QA_DB_MAX_META_TITLE_LENGTH.') NOT NULL', + 'content' => 'VARCHAR('.QA_DB_MAX_META_CONTENT_LENGTH.') NOT NULL', + 'PRIMARY KEY (userid, title)', + ), + + 'postmetas' => array( + 'postid' => 'INT UNSIGNED NOT NULL', + 'title' => 'VARCHAR('.QA_DB_MAX_META_TITLE_LENGTH.') NOT NULL', + 'content' => 'VARCHAR('.QA_DB_MAX_META_CONTENT_LENGTH.') NOT NULL', + 'PRIMARY KEY (postid, title)', + 'CONSTRAINT ^postmetas_ibfk_1 FOREIGN KEY (postid) REFERENCES ^posts(postid) ON DELETE CASCADE', + ), + + 'categorymetas' => array( + 'categoryid' => 'INT UNSIGNED NOT NULL', + 'title' => 'VARCHAR('.QA_DB_MAX_META_TITLE_LENGTH.') NOT NULL', + 'content' => 'VARCHAR('.QA_DB_MAX_META_CONTENT_LENGTH.') NOT NULL', + 'PRIMARY KEY (categoryid, title)', + 'CONSTRAINT ^categorymetas_ibfk_1 FOREIGN KEY (categoryid) REFERENCES ^categories(categoryid) ON DELETE CASCADE', + ), + + 'tagmetas' => array( + 'tag' => 'VARCHAR('.QA_DB_MAX_WORD_LENGTH.') NOT NULL', + 'title' => 'VARCHAR('.QA_DB_MAX_META_TITLE_LENGTH.') NOT NULL', + 'content' => 'VARCHAR('.QA_DB_MAX_META_CONTENT_LENGTH.') NOT NULL', + 'PRIMARY KEY (tag, title)', + ), + + ); + + if (QA_FINAL_EXTERNAL_USERS) { + unset($tables['users']); + unset($tables['userlogins']); + unset($tables['userprofile']); + unset($tables['userfields']); + unset($tables['messages']); + + } else { + $userforeignkey='FOREIGN KEY (userid) REFERENCES ^users(userid)'; + + $tables['userlogins'][]='CONSTRAINT ^userlogins_ibfk_1 '.$userforeignkey.' ON DELETE CASCADE'; + $tables['userprofile'][]='CONSTRAINT ^userprofile_ibfk_1 '.$userforeignkey.' ON DELETE CASCADE'; + $tables['posts'][]='CONSTRAINT ^posts_ibfk_1 '.$userforeignkey.' ON DELETE SET NULL'; + $tables['uservotes'][]='CONSTRAINT ^uservotes_ibfk_2 '.$userforeignkey.' ON DELETE CASCADE'; + $tables['userlimits'][]='CONSTRAINT ^userlimits_ibfk_1 '.$userforeignkey.' ON DELETE CASCADE'; + $tables['userfavorites'][]='CONSTRAINT ^userfavorites_ibfk_1 '.$userforeignkey.' ON DELETE CASCADE'; + $tables['usernotices'][]='CONSTRAINT ^usernotices_ibfk_1 '.$userforeignkey.' ON DELETE CASCADE'; + $tables['userevents'][]='CONSTRAINT ^userevents_ibfk_1 '.$userforeignkey.' ON DELETE CASCADE'; + $tables['usermetas'][]='CONSTRAINT ^usermetas_ibfk_1 '.$userforeignkey.' ON DELETE CASCADE'; + } + + return $tables; + } + + + function qa_array_to_lower_keys($array) +/* + Return $array with all keys converted to lower case +*/ + { + $keyarray=array(); + + foreach ($array as $value) + $keyarray[strtolower($value)]=true; + + return $keyarray; + } + + + function qa_db_missing_tables($definitions) +/* + Return a list of tables missing from the database, [table name] => [column/index definitions] +*/ + { + $keydbtables=qa_array_to_lower_keys(qa_db_list_tables_lc()); + + $missing=array(); + + foreach ($definitions as $rawname => $definition) + if (!isset($keydbtables[strtolower(qa_db_add_table_prefix($rawname))])) + $missing[$rawname]=$definition; + + return $missing; + } + + + function qa_db_missing_columns($table, $definition) +/* + Return a list of columns missing from $table in the database, given the full definition set in $definition +*/ + { + $keycolumns=qa_array_to_lower_keys(qa_db_read_all_values(qa_db_query_sub('SHOW COLUMNS FROM ^'.$table))); + + $missing=array(); + + foreach ($definition as $colname => $coldefn) + if ( (!is_int($colname)) && !isset($keycolumns[strtolower($colname)]) ) + $missing[$colname]=$coldefn; + + return $missing; + } + + + function qa_db_get_db_version() +/* + Return the current version of the Q2A database, to determine need for DB upgrades +*/ + { + $definitions=qa_db_table_definitions(); + + if (count(qa_db_missing_columns('options', $definitions['options']))==0) { + $version=(int)qa_db_read_one_value(qa_db_query_sub("SELECT content FROM ^options WHERE title='db_version'"), true); + + if ($version>0) + return $version; + } + + return null; + } + + + function qa_db_set_db_version($version) +/* + Set the current version in the database +*/ + { + qa_db_query_sub("REPLACE ^options (title,content) VALUES ('db_version', #)", $version); + } + + + function qa_db_check_tables() +/* + Return a string describing what is wrong with the database, or false if everything is just fine +*/ + { + qa_db_query_raw('UNLOCK TABLES'); // we could be inside a lock tables block + + $version=qa_db_read_one_value(qa_db_query_raw('SELECT VERSION()')); + + if (((float)$version)<4.1) + qa_fatal_error('MySQL version 4.1 or later is required - you appear to be running MySQL '.$version); + + $definitions=qa_db_table_definitions(); + $missing=qa_db_missing_tables($definitions); + + if (count($missing) == count($definitions)) + return 'none'; + + else { + if (!isset($missing['options'])) { + $version=qa_db_get_db_version(); + + if (isset($version) && ($version $definition) + if (qa_db_add_table_prefix($rawname)==(QA_MYSQL_TABLE_PREFIX.$rawname)) { + $datacount++; + + if (isset($missing[$rawname])) + $datamissing++; + } + + if ( ($datacount==$datamissing) && ($datamissing==count($missing)) ) + return 'non-users-missing'; + } + + return 'table-missing'; + + } else + foreach ($definitions as $table => $definition) + if (count(qa_db_missing_columns($table, $definition))) + return 'column-missing'; + } + + return false; + } + + + function qa_db_install_tables() +/* + Install any missing database tables and/or columns and automatically set version as latest. + This is not suitable for use if the database needs upgrading. +*/ + { + $definitions=qa_db_table_definitions(); + + $missingtables=qa_db_missing_tables($definitions); + + foreach ($missingtables as $rawname => $definition) { + qa_db_query_sub(qa_db_create_table_sql($rawname, $definition)); + + if ($rawname=='userfields') + qa_db_query_sub(qa_db_default_userfields_sql()); + } + + foreach ($definitions as $table => $definition) { + $missingcolumns=qa_db_missing_columns($table, $definition); + + foreach ($missingcolumns as $colname => $coldefn) + qa_db_query_sub('ALTER TABLE ^'.$table.' ADD COLUMN '.$colname.' '.$coldefn); + } + + qa_db_set_db_version(QA_DB_VERSION_CURRENT); + } + + + function qa_db_create_table_sql($rawname, $definition) +/* + Return the SQL command to create a table with $rawname and $definition obtained from qa_db_table_definitions() +*/ + { + $querycols=''; + foreach ($definition as $colname => $coldef) + if (isset($coldef)) + $querycols.=(strlen($querycols) ? ', ' : '').(is_int($colname) ? $coldef : ($colname.' '.$coldef)); + + return 'CREATE TABLE ^'.$rawname.' ('.$querycols.') ENGINE=InnoDB CHARSET=utf8'; + } + + + function qa_db_default_userfields_sql() +/* + Return the SQL to create the default entries in the userfields table (before 1.3 these were hard-coded in PHP) +*/ + { + $oldprofileflags=array( + 'name' => 0, + 'location' => 0, + 'website' => QA_FIELD_FLAGS_LINK_URL, + 'about' => QA_FIELD_FLAGS_MULTI_LINE, + ); + + $sql='INSERT INTO ^userfields (title, position, flags) VALUES '; // content column will be NULL, meaning use default from lang files + + $index=0; + foreach ($oldprofileflags as $title => $flags) + $sql.=($index ? ', ' : '')."('".qa_db_escape_string($title)."', ".(++$index).", ".(int)@$oldprofileflags[$title].")"; + + return $sql; + } + + + function qa_db_upgrade_tables() +/* + Upgrade the database schema to the latest version, outputting progress to the browser +*/ + { + require_once QA_INCLUDE_DIR.'qa-app-recalc.php'; + + $definitions=qa_db_table_definitions(); + $keyrecalc=array(); + + // Write-lock all Q2A tables before we start so no one can read or write anything + + $keydbtables=qa_array_to_lower_keys(qa_db_list_tables_lc()); + + foreach ($definitions as $rawname => $definition) + if (isset($keydbtables[strtolower(qa_db_add_table_prefix($rawname))])) + $locks[]='^'.$rawname.' WRITE'; + + $locktablesquery='LOCK TABLES '.implode(', ', $locks); + + qa_db_upgrade_query($locktablesquery); + + // Upgrade it step-by-step until it's up to date (do LOCK TABLES after ALTER TABLE because the lock can sometimes be lost) + + while (1) { + $version=qa_db_get_db_version(); + + if ($version>=QA_DB_VERSION_CURRENT) + break; + + $newversion=$version+1; + + qa_db_upgrade_progress(QA_DB_VERSION_CURRENT-$version.' upgrade step/s remaining...'); + + switch ($newversion) { + + // Up to here: Version 1.0 beta 1 + + case 2: + qa_db_upgrade_query('ALTER TABLE ^posts DROP COLUMN votes, ADD COLUMN (upvotes '.$definitions['posts']['upvotes']. + ', downvotes '.$definitions['posts']['downvotes'].')'); + qa_db_upgrade_query($locktablesquery); + $keyrecalc['dorecountposts']=true; + break; + + case 3: + qa_db_upgrade_query('ALTER TABLE ^userpoints ADD COLUMN (upvoteds '.$definitions['userpoints']['upvoteds']. + ', downvoteds '.$definitions['userpoints']['downvoteds'].')'); + qa_db_upgrade_query($locktablesquery); + $keyrecalc['dorecalcpoints']=true; + break; + + case 4: + qa_db_upgrade_query('ALTER TABLE ^posts ADD COLUMN lastuserid '.$definitions['posts']['lastuserid'].', CHANGE COLUMN updated updated '.$definitions['posts']['updated']); + qa_db_upgrade_query($locktablesquery); + qa_db_upgrade_query('UPDATE ^posts SET updated=NULL WHERE updated=0 OR updated=created'); + break; + + case 5: + qa_db_upgrade_query('ALTER TABLE ^contentwords ADD COLUMN (type '.$definitions['contentwords']['type'].', questionid '.$definitions['contentwords']['questionid'].')'); + qa_db_upgrade_query($locktablesquery); + $keyrecalc['doreindexposts']=true; + break; + + // Up to here: Version 1.0 beta 2 + + case 6: + qa_db_upgrade_query('ALTER TABLE ^userpoints ADD COLUMN cposts '.$definitions['userpoints']['cposts']); + qa_db_upgrade_query($locktablesquery); + $keyrecalc['dorecalcpoints']=true; + break; + + case 7: + if (!QA_FINAL_EXTERNAL_USERS) { + qa_db_upgrade_query('ALTER TABLE ^users ADD COLUMN sessioncode '.$definitions['users']['sessioncode']); + qa_db_upgrade_query($locktablesquery); + } + break; + + case 8: + qa_db_upgrade_query('ALTER TABLE ^posts ADD KEY (type, acount, created)'); + qa_db_upgrade_query($locktablesquery); + $keyrecalc['dorecountposts']=true; // for unanswered question count + break; + + // Up to here: Version 1.0 beta 3, 1.0, 1.0.1 beta, 1.0.1 + + case 9: + if (!QA_FINAL_EXTERNAL_USERS) { + qa_db_upgrade_query('ALTER TABLE ^users CHANGE COLUMN resetcode emailcode '.$definitions['users']['emailcode'].', ADD COLUMN flags '.$definitions['users']['flags']); + qa_db_upgrade_query($locktablesquery); + qa_db_upgrade_query('UPDATE ^users SET flags=1'); + } + break; + + case 10: + qa_db_upgrade_query(qa_db_create_table_sql('categories', array( + 'categoryid' => $definitions['categories']['categoryid'], + 'title' => $definitions['categories']['title'], + 'tags' => $definitions['categories']['tags'], + 'qcount' => $definitions['categories']['qcount'], + 'position' => $definitions['categories']['position'], + 'PRIMARY KEY (categoryid)', + 'UNIQUE tags (tags)', + 'UNIQUE position (position)', + ))); // hard-code list of columns and indexes to ensure we ignore any added at a later stage + + $locktablesquery.=', ^categories WRITE'; + qa_db_upgrade_query($locktablesquery); + break; + + case 11: + qa_db_upgrade_query('ALTER TABLE ^posts ADD CONSTRAINT ^posts_ibfk_2 FOREIGN KEY (parentid) REFERENCES ^posts(postid), ADD COLUMN categoryid '.$definitions['posts']['categoryid'].', ADD KEY categoryid (categoryid, type, created), ADD CONSTRAINT ^posts_ibfk_3 FOREIGN KEY (categoryid) REFERENCES ^categories(categoryid) ON DELETE SET NULL'); + // foreign key on parentid important now that deletion is possible + qa_db_upgrade_query($locktablesquery); + break; + + case 12: + qa_db_upgrade_query(qa_db_create_table_sql('pages', array( + 'pageid' => $definitions['pages']['pageid'], + 'title' => $definitions['pages']['title'], + 'nav' => $definitions['pages']['nav'], + 'position' => $definitions['pages']['position'], + 'flags' => $definitions['pages']['flags'], + 'tags' => $definitions['pages']['tags'], + 'heading' => $definitions['pages']['heading'], + 'content' => $definitions['pages']['content'], + 'PRIMARY KEY (pageid)', + 'UNIQUE tags (tags)', + 'UNIQUE position (position)', + ))); // hard-code list of columns and indexes to ensure we ignore any added at a later stage + $locktablesquery.=', ^pages WRITE'; + qa_db_upgrade_query($locktablesquery); + break; + + case 13: + qa_db_upgrade_query('ALTER TABLE ^posts ADD COLUMN createip '.$definitions['posts']['createip'].', ADD KEY createip (createip, created)'); + qa_db_upgrade_query($locktablesquery); + break; + + case 14: + qa_db_upgrade_query('ALTER TABLE ^userpoints DROP COLUMN qvotes, DROP COLUMN avotes, ADD COLUMN (qupvotes '.$definitions['userpoints']['qupvotes'].', qdownvotes '.$definitions['userpoints']['qdownvotes'].', aupvotes '.$definitions['userpoints']['aupvotes'].', adownvotes '.$definitions['userpoints']['adownvotes'].')'); + qa_db_upgrade_query($locktablesquery); + $keyrecalc['dorecalcpoints']=true; + break; + + // Up to here: Version 1.2 beta 1 + + case 15: + if (!QA_FINAL_EXTERNAL_USERS) + qa_db_upgrade_table_columns($definitions, 'users', array('emailcode', 'sessioncode', 'flags')); + + qa_db_upgrade_table_columns($definitions, 'posts', array('acount', 'upvotes', 'downvotes', 'format')); + qa_db_upgrade_table_columns($definitions, 'categories', array('qcount')); + qa_db_upgrade_table_columns($definitions, 'words', array('titlecount', 'contentcount', 'tagcount')); + qa_db_upgrade_table_columns($definitions, 'userpoints', array('points', 'qposts', 'aposts', 'cposts', + 'aselects', 'aselecteds', 'qupvotes', 'qdownvotes', 'aupvotes', 'adownvotes', 'qvoteds', 'avoteds', 'upvoteds', 'downvoteds')); + qa_db_upgrade_query($locktablesquery); + break; + + // Up to here: Version 1.2 (release) + + case 16: + qa_db_upgrade_table_columns($definitions, 'posts', array('format')); + qa_db_upgrade_query($locktablesquery); + $keyrecalc['doreindexposts']=true; // because of new treatment of apostrophes in words + break; + + case 17: + qa_db_upgrade_query('ALTER TABLE ^posts ADD KEY updated (updated, type), ADD KEY categoryid_2 (categoryid, updated, type)'); + qa_db_upgrade_query($locktablesquery); + break; + + case 18: + qa_db_upgrade_query('ALTER TABLE ^posts ADD COLUMN lastip '.$definitions['posts']['lastip'].', ADD KEY lastip (lastip, updated, type)'); + qa_db_upgrade_query($locktablesquery); + break; + + case 19: + if (!QA_FINAL_EXTERNAL_USERS) + qa_db_upgrade_query('ALTER TABLE ^users ADD COLUMN avatarblobid '.$definitions['users']['avatarblobid'].', ADD COLUMN avatarwidth '.$definitions['users']['avatarwidth'].', ADD COLUMN avatarheight '.$definitions['users']['avatarheight']); + + // hard-code list of columns and indexes to ensure we ignore any added at a later stage + + qa_db_upgrade_query(qa_db_create_table_sql('blobs', array( + 'blobid' => $definitions['blobs']['blobid'], + 'format' => $definitions['blobs']['format'], + 'content' => $definitions['blobs']['content'], + 'PRIMARY KEY (blobid)' + ))); + + qa_db_upgrade_query(qa_db_create_table_sql('cache', array( + 'type' => $definitions['cache']['type'], + 'cacheid' => $definitions['cache']['cacheid'], + 'content' => $definitions['cache']['content'], + 'created' => $definitions['cache']['created'], + 'lastread' => $definitions['cache']['lastread'], + 'PRIMARY KEY (type,cacheid)', + 'KEY (lastread)', + ))); // hard-code list of columns and indexes to ensure we ignore any added at a later stage + + $locktablesquery.=', ^blobs WRITE, ^cache WRITE'; + qa_db_upgrade_query($locktablesquery); + break; + + case 20: + if (!QA_FINAL_EXTERNAL_USERS) { + qa_db_upgrade_query(qa_db_create_table_sql('userlogins', array( + 'userid' => $definitions['userlogins']['userid'], + 'source' => $definitions['userlogins']['source'], + 'identifier' => $definitions['userlogins']['identifier'], + 'identifiermd5' => $definitions['userlogins']['identifiermd5'], + 'KEY source (source, identifiermd5)', + 'KEY userid (userid)', + 'CONSTRAINT ^userlogins_ibfk_1 FOREIGN KEY (userid) REFERENCES ^users(userid) ON DELETE CASCADE', + ))); + + qa_db_upgrade_query('ALTER TABLE ^users CHANGE COLUMN passsalt passsalt '.$definitions['users']['passsalt'].', CHANGE COLUMN passcheck passcheck '.$definitions['users']['passcheck']); + + $locktablesquery.=', ^userlogins WRITE'; + qa_db_upgrade_query($locktablesquery); + } + break; + + case 21: + if (!QA_FINAL_EXTERNAL_USERS) { + qa_db_upgrade_query(qa_db_create_table_sql('userfields', array( + 'fieldid' => $definitions['userfields']['fieldid'], + 'title' => $definitions['userfields']['title'], + 'content' => $definitions['userfields']['content'], + 'position' => $definitions['userfields']['position'], + 'flags' => $definitions['userfields']['flags'], + 'PRIMARY KEY (fieldid)', + ))); + + $locktablesquery.=', ^userfields WRITE'; + qa_db_upgrade_query($locktablesquery); + + qa_db_upgrade_query(qa_db_default_userfields_sql()); + } + break; + + // Up to here: Version 1.3 beta 1 + + case 22: + if (!QA_FINAL_EXTERNAL_USERS) { + qa_db_upgrade_query('ALTER TABLE ^users ADD COLUMN sessionsource '.$definitions['users']['sessionsource']); + qa_db_upgrade_query($locktablesquery); + } + break; + + // Up to here: Version 1.3 beta 2 and release + + case 23: + qa_db_upgrade_query(qa_db_create_table_sql('widgets', array( + 'widgetid' => $definitions['widgets']['widgetid'], + 'place' => $definitions['widgets']['place'], + 'position' => $definitions['widgets']['position'], + 'tags' => $definitions['widgets']['tags'], + 'title' => $definitions['widgets']['title'], + 'PRIMARY KEY (widgetid)', + 'UNIQUE position (position)', + ))); + + $locktablesquery.=', ^widgets WRITE'; + qa_db_upgrade_query($locktablesquery); + break; + + case 24: + qa_db_upgrade_query(qa_db_create_table_sql('tagwords', array( + 'postid' => $definitions['tagwords']['postid'], + 'wordid' => $definitions['tagwords']['wordid'], + 'KEY postid (postid)', + 'KEY wordid (wordid)', + 'CONSTRAINT ^tagwords_ibfk_1 FOREIGN KEY (postid) REFERENCES ^posts(postid) ON DELETE CASCADE', + 'CONSTRAINT ^tagwords_ibfk_2 FOREIGN KEY (wordid) REFERENCES ^words(wordid)', + ))); + + $locktablesquery.=', ^tagwords WRITE'; + + qa_db_upgrade_query('ALTER TABLE ^words ADD COLUMN tagwordcount '.$definitions['words']['tagwordcount']); + qa_db_upgrade_query($locktablesquery); + + $keyrecalc['doreindexposts']=true; + break; + + // Up to here: Version 1.4 developer preview + + case 25: + $keycolumns=qa_array_to_lower_keys(qa_db_read_all_values(qa_db_query_sub('SHOW COLUMNS FROM ^blobs'))); + // might be using blobs table shared with another installation, so check if we need to upgrade + + if (isset($keycolumns['filename'])) + qa_db_upgrade_progress('Skipping upgrading blobs table since it was already upgraded by another Q2A site sharing it.'); + + else { + qa_db_upgrade_query('ALTER TABLE ^blobs ADD COLUMN filename '.$definitions['blobs']['filename'].', ADD COLUMN userid '.$definitions['blobs']['userid'].', ADD COLUMN cookieid '.$definitions['blobs']['cookieid'].', ADD COLUMN createip '.$definitions['blobs']['createip'].', ADD COLUMN created '.$definitions['blobs']['created']); + qa_db_upgrade_query($locktablesquery); + } + break; + + case 26: + qa_db_upgrade_query('ALTER TABLE ^uservotes ADD COLUMN flag '.$definitions['uservotes']['flag']); + qa_db_upgrade_query($locktablesquery); + + qa_db_upgrade_query('ALTER TABLE ^posts ADD COLUMN flagcount '.$definitions['posts']['flagcount'].', ADD KEY type_3 (type, flagcount, created)'); + qa_db_upgrade_query($locktablesquery); + + $keyrecalc['dorecountposts']=true; + break; + + case 27: + qa_db_upgrade_query('ALTER TABLE ^posts ADD COLUMN netvotes '.$definitions['posts']['netvotes'].', ADD KEY type_4 (type, netvotes, created)'); + qa_db_upgrade_query($locktablesquery); + + $keyrecalc['dorecountposts']=true; + break; + + case 28: + qa_db_upgrade_query('ALTER TABLE ^posts ADD COLUMN views '.$definitions['posts']['views'].', ADD COLUMN hotness '.$definitions['posts']['hotness'].', ADD KEY type_5 (type, views, created), ADD KEY type_6 (type, hotness)'); + qa_db_upgrade_query($locktablesquery); + + $keyrecalc['dorecountposts']=true; + break; + + case 29: + qa_db_upgrade_query('ALTER TABLE ^posts ADD COLUMN lastviewip '.$definitions['posts']['lastviewip']); + qa_db_upgrade_query($locktablesquery); + break; + + case 30: + qa_db_upgrade_query('ALTER TABLE ^posts DROP FOREIGN KEY ^posts_ibfk_3'); // to allow category column types to be changed + qa_db_upgrade_query($locktablesquery); + + qa_db_upgrade_query('ALTER TABLE ^posts DROP KEY categoryid, DROP KEY categoryid_2'); + qa_db_upgrade_query($locktablesquery); + + qa_db_upgrade_query('ALTER TABLE ^categories CHANGE COLUMN categoryid categoryid '.$definitions['categories']['categoryid'].', ADD COLUMN parentid '.$definitions['categories']['parentid'].', ADD COLUMN backpath '.$definitions['categories']['backpath'].', ADD COLUMN content '.$definitions['categories']['content'].', DROP INDEX tags, DROP INDEX position, ADD UNIQUE parentid (parentid, tags), ADD UNIQUE parentid_2 (parentid, position), ADD KEY backpath (backpath('.QA_DB_MAX_CAT_PAGE_TAGS_LENGTH.'))'); + qa_db_upgrade_query($locktablesquery); + + qa_db_upgrade_query('ALTER TABLE ^posts CHANGE COLUMN categoryid categoryid '.$definitions['posts']['categoryid'].', ADD COLUMN catidpath1 '.$definitions['posts']['catidpath1'].', ADD COLUMN catidpath2 '.$definitions['posts']['catidpath2'].', ADD COLUMN catidpath3 '.$definitions['posts']['catidpath3']); // QA_CATEGORY_DEPTH=4 + qa_db_upgrade_query($locktablesquery); + + qa_db_upgrade_query('ALTER TABLE ^posts ADD KEY catidpath1 (catidpath1, type, created), ADD KEY catidpath2 (catidpath2, type, created), ADD KEY catidpath3 (catidpath3, type, created), ADD KEY categoryid (categoryid, type, created), ADD KEY catidpath1_2 (catidpath1, updated, type), ADD KEY catidpath2_2 (catidpath2, updated, type), ADD KEY catidpath3_2 (catidpath3, updated, type), ADD KEY categoryid_2 (categoryid, updated, type)'); + qa_db_upgrade_query($locktablesquery); + + qa_db_upgrade_query('ALTER TABLE ^posts ADD CONSTRAINT ^posts_ibfk_3 FOREIGN KEY (categoryid) REFERENCES ^categories(categoryid) ON DELETE SET NULL'); + qa_db_upgrade_query($locktablesquery); + + $keyrecalc['dorecalccategories']=true; + break; + + // Up to here: Version 1.4 betas and release + + case 31: + qa_db_upgrade_query('ALTER TABLE ^posts CHANGE COLUMN type type '.$definitions['posts']['type'].', ADD COLUMN updatetype '.$definitions['posts']['updatetype'].' AFTER updated, ADD COLUMN closedbyid '.$definitions['posts']['closedbyid'].' AFTER selchildid, ADD KEY closedbyid (closedbyid), ADD CONSTRAINT ^posts_ibfk_4 FOREIGN KEY (closedbyid) REFERENCES ^posts(postid)'); + qa_db_upgrade_query($locktablesquery); + break; + + case 32: + qa_db_upgrade_query("UPDATE ^posts SET updatetype=IF(INSTR(type, '_HIDDEN')>0, 'H', 'E') WHERE updated IS NOT NULL"); + break; + + case 33: + qa_db_upgrade_query('ALTER TABLE ^contentwords CHANGE COLUMN type type '.$definitions['contentwords']['type']); + qa_db_upgrade_query($locktablesquery); + break; + + case 34: + if (!QA_FINAL_EXTERNAL_USERS) { + $keytables=qa_array_to_lower_keys(qa_db_read_all_values(qa_db_query_sub('SHOW TABLES'))); + // might be using messages table shared with another installation, so check if we need to upgrade + + if (isset($keytables[qa_db_add_table_prefix('messages')])) + qa_db_upgrade_progress('Skipping messages table since it was already added by another Q2A site sharing these users.'); + + else { + qa_db_upgrade_query(qa_db_create_table_sql('messages', array( + 'messageid' => $definitions['messages']['messageid'], + 'fromuserid' => $definitions['messages']['fromuserid'], + 'touserid' => $definitions['messages']['touserid'], + 'content' => $definitions['messages']['content'], + 'format' => $definitions['messages']['format'], + 'created' => $definitions['messages']['created'], + 'PRIMARY KEY (messageid)', + 'KEY fromuserid (fromuserid, touserid, created)', + ))); + + $locktablesquery.=', ^messages WRITE'; + qa_db_upgrade_query($locktablesquery); + } + } + break; + + case 35: + qa_db_upgrade_query(qa_db_create_table_sql('userfavorites', array( + 'userid' => $definitions['userfavorites']['userid'], + 'entitytype' => $definitions['userfavorites']['entitytype'], + 'entityid' => $definitions['userfavorites']['entityid'], + 'nouserevents' => $definitions['userfavorites']['nouserevents'], + 'PRIMARY KEY (userid, entitytype, entityid)', + 'KEY userid (userid, nouserevents)', + 'KEY entitytype (entitytype, entityid, nouserevents)', + QA_FINAL_EXTERNAL_USERS ? null : 'CONSTRAINT ^userfavorites_ibfk_1 FOREIGN KEY (userid) REFERENCES ^users(userid) ON DELETE CASCADE', + ))); + + $locktablesquery.=', ^userfavorites WRITE'; + qa_db_upgrade_query($locktablesquery); + break; + + case 36: + qa_db_upgrade_query(qa_db_create_table_sql('userevents', array( + 'userid' => $definitions['userevents']['userid'], + 'entitytype' => $definitions['userevents']['entitytype'], + 'entityid' => $definitions['userevents']['entityid'], + 'questionid' => $definitions['userevents']['questionid'], + 'lastpostid' => $definitions['userevents']['lastpostid'], + 'updatetype' => $definitions['userevents']['updatetype'], + 'lastuserid' => $definitions['userevents']['lastuserid'], + 'updated' => $definitions['userevents']['updated'], + 'KEY userid (userid, updated)', + 'KEY questionid (questionid, userid)', + QA_FINAL_EXTERNAL_USERS ? null : 'CONSTRAINT ^userevents_ibfk_1 FOREIGN KEY (userid) REFERENCES ^users(userid) ON DELETE CASCADE', + ))); + + $locktablesquery.=', ^userevents WRITE'; + qa_db_upgrade_query($locktablesquery); + + $keyrecalc['dorefillevents']=true; + break; + + case 37: + qa_db_upgrade_query(qa_db_create_table_sql('sharedevents', array( + 'entitytype' => $definitions['sharedevents']['entitytype'], + 'entityid' => $definitions['sharedevents']['entityid'], + 'questionid' => $definitions['sharedevents']['questionid'], + 'lastpostid' => $definitions['sharedevents']['lastpostid'], + 'updatetype' => $definitions['sharedevents']['updatetype'], + 'lastuserid' => $definitions['sharedevents']['lastuserid'], + 'updated' => $definitions['sharedevents']['updated'], + 'KEY entitytype (entitytype, entityid, updated)', + 'KEY questionid (questionid, entitytype, entityid)', + ))); + + $locktablesquery.=', ^sharedevents WRITE'; + qa_db_upgrade_query($locktablesquery); + + $keyrecalc['dorefillevents']=true; + break; + + case 38: + qa_db_upgrade_query('ALTER TABLE ^posts ADD KEY lastuserid (lastuserid, updated, type)'); + qa_db_upgrade_query($locktablesquery); + break; + + case 39: + qa_db_upgrade_query('ALTER TABLE ^posts DROP KEY type_3, ADD KEY flagcount (flagcount, created, type)'); + qa_db_upgrade_query($locktablesquery); + break; + + case 40: + qa_db_upgrade_query('ALTER TABLE ^userpoints ADD COLUMN bonus '.$definitions['userpoints']['bonus'].' AFTER downvoteds'); + qa_db_upgrade_query($locktablesquery); + break; + + case 41: + qa_db_upgrade_query('ALTER TABLE ^pages ADD COLUMN permit '.$definitions['pages']['permit'].' AFTER flags'); + qa_db_upgrade_query($locktablesquery); + break; + + case 42: + qa_db_upgrade_query(qa_db_create_table_sql('usermetas', array( + 'userid' => $definitions['usermetas']['userid'], + 'title' => $definitions['usermetas']['title'], + 'content' => $definitions['usermetas']['content'], + 'PRIMARY KEY (userid, title)', + QA_FINAL_EXTERNAL_USERS ? null : 'CONSTRAINT ^usermetas_ibfk_1 FOREIGN KEY (userid) REFERENCES ^users(userid) ON DELETE CASCADE', + ))); + + $locktablesquery.=', ^usermetas WRITE'; + qa_db_upgrade_query($locktablesquery); + break; + + case 43: + qa_db_upgrade_query(qa_db_create_table_sql('postmetas', array( + 'postid' => $definitions['postmetas']['postid'], + 'title' => $definitions['postmetas']['title'], + 'content' => $definitions['postmetas']['content'], + 'PRIMARY KEY (postid, title)', + 'CONSTRAINT ^postmetas_ibfk_1 FOREIGN KEY (postid) REFERENCES ^posts(postid) ON DELETE CASCADE', + ))); + + $locktablesquery.=', ^postmetas WRITE'; + qa_db_upgrade_query($locktablesquery); + break; + + case 44: + qa_db_upgrade_query(qa_db_create_table_sql('categorymetas', array( + 'categoryid' => $definitions['categorymetas']['categoryid'], + 'title' => $definitions['categorymetas']['title'], + 'content' => $definitions['categorymetas']['content'], + 'PRIMARY KEY (categoryid, title)', + 'CONSTRAINT ^categorymetas_ibfk_1 FOREIGN KEY (categoryid) REFERENCES ^categories(categoryid) ON DELETE CASCADE', + ))); + + $locktablesquery.=', ^categorymetas WRITE'; + qa_db_upgrade_query($locktablesquery); + break; + + case 45: + qa_db_upgrade_query(qa_db_create_table_sql('tagmetas', array( + 'tag' => $definitions['tagmetas']['tag'], + 'title' => $definitions['tagmetas']['title'], + 'content' => $definitions['tagmetas']['content'], + 'PRIMARY KEY (tag, title)', + ))); + + $locktablesquery.=', ^tagmetas WRITE'; + qa_db_upgrade_query($locktablesquery); + break; + + case 46: + qa_db_upgrade_query('ALTER TABLE ^posts DROP KEY selchildid, ADD KEY selchildid (selchildid, type, created), ADD COLUMN amaxvote SMALLINT UNSIGNED NOT NULL DEFAULT 0 AFTER acount, ADD KEY type_7 (type, amaxvote, created)'); + qa_db_upgrade_query($locktablesquery); + + $keyrecalc['dorecountposts']=true; + break; + + case 47: + qa_db_upgrade_query(qa_db_create_table_sql('usernotices', array( + 'noticeid' => $definitions['usernotices']['noticeid'], + 'userid' => $definitions['usernotices']['userid'], + 'content' => $definitions['usernotices']['content'], + 'format' => $definitions['usernotices']['format'], + 'tags' => $definitions['usernotices']['tags'], + 'created' => $definitions['usernotices']['created'], + 'PRIMARY KEY (noticeid)', + 'KEY userid (userid, created)', + QA_FINAL_EXTERNAL_USERS ? null : 'CONSTRAINT ^usernotices_ibfk_1 FOREIGN KEY (userid) REFERENCES ^users(userid) ON DELETE CASCADE', + ))); + + $locktablesquery.=', ^usernotices WRITE'; + qa_db_upgrade_query($locktablesquery); + break; + + // Up to here: Version 1.5 beta 1 + + } + + qa_db_set_db_version($newversion); + + if (qa_db_get_db_version()!=$newversion) + qa_fatal_error('Could not increment database version'); + } + + qa_db_upgrade_query('UNLOCK TABLES'); + + // Perform any necessary recalculations, as determined by upgrade steps + + foreach ($keyrecalc as $state => $dummy) + while ($state) { + set_time_limit(60); + + $stoptime=time()+2; + + while ( qa_recalc_perform_step($state) && (time()<$stoptime) ) + ; + + qa_db_upgrade_progress(qa_recalc_get_message($state)); + } + } + + + function qa_db_upgrade_table_columns($definitions, $table, $columns) +/* + Reset the definitions of $columns in $table according to the $definitions array +*/ + { + $sqlchanges=array(); + + foreach ($columns as $column) + $sqlchanges[]='CHANGE COLUMN '.$column.' '.$column.' '.$definitions[$table][$column]; + + qa_db_upgrade_query('ALTER TABLE ^'.$table.' '.implode(', ', $sqlchanges)); + } + + + function qa_db_upgrade_query($query) +/* + Perform upgrade $query and output progress to the browser +*/ + { + qa_db_upgrade_progress('Running query: '.qa_db_apply_sub($query, array()).' ...'); + qa_db_query_sub($query); + } + + + function qa_db_upgrade_progress($text) +/* + Output $text to the browser (after converting to HTML) and do all we can to get it displayed +*/ + { + echo qa_html($text).str_repeat(' ', 1024)."

\n"; + flush(); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-db-limits.php b/qa-include/qa-db-limits.php new file mode 100644 index 000000000..e0df83c10 --- /dev/null +++ b/qa-include/qa-db-limits.php @@ -0,0 +1,91 @@ + value). +*/ + { + return qa_db_meta_get('usermetas', 'userid', $userid, $title); + } + + + function qa_db_postmeta_set($postid, $title, $content) +/* + Set the metadata for post $postid with key $title to value $content +*/ + { + qa_db_meta_set('postmetas', 'postid', $postid, $title, $content); + } + + + function qa_db_postmeta_clear($postid, $title) +/* + Clear the metadata for post $postid with key $title ($title can also be an array of keys) +*/ + { + qa_db_meta_clear('postmetas', 'postid', $postid, $title); + } + + + function qa_db_postmeta_get($postid, $title) +/* + Return the metadata value for post $postid with key $title ($title can also be an array of keys in which case this + returns an array of metadata key => value). +*/ + { + return qa_db_meta_get('postmetas', 'postid', $postid, $title); + } + + + function qa_db_categorymeta_set($categoryid, $title, $content) +/* + Set the metadata for category $categoryid with key $title to value $content +*/ + { + qa_db_meta_set('categorymetas', 'categoryid', $categoryid, $title, $content); + } + + + function qa_db_categorymeta_clear($categoryid, $title) +/* + Clear the metadata for category $categoryid with key $title ($title can also be an array of keys) +*/ + { + qa_db_meta_clear('categorymetas', 'categoryid', $categoryid, $title); + } + + + function qa_db_categorymeta_get($categoryid, $title) +/* + Return the metadata value for category $categoryid with key $title ($title can also be an array of keys in which + case this returns an array of metadata key => value). +*/ + { + return qa_db_meta_get('categorymetas', 'categoryid', $categoryid, $title); + } + + + function qa_db_tagmeta_set($tag, $title, $content) +/* + Set the metadata for tag $tag with key $title to value $content +*/ + { + qa_db_meta_set('tagmetas', 'tag', $tag, $title, $content); + } + + + function qa_db_tagmeta_clear($tag, $title) +/* + Clear the metadata for tag $tag with key $title ($title can also be an array of keys) +*/ + { + qa_db_meta_clear('tagmetas', 'tag', $tag, $title); + } + + + function qa_db_tagmeta_get($tag, $title) +/* + Return the metadata value for tag $tag with key $title ($title can also be an array of keys in which case this + returns an array of metadata key => value). +*/ + { + return qa_db_meta_get('tagmetas', 'tag', $tag, $title); + } + + + function qa_db_meta_set($metatable, $idcolumn, $idvalue, $title, $content) +/* + Internal general function to set metadata +*/ + { + qa_db_query_sub( + 'REPLACE ^'.$metatable.' ('.$idcolumn.', title, content) VALUES ($, $, $)', + $idvalue, $title, $content + ); + } + + + function qa_db_meta_clear($metatable, $idcolumn, $idvalue, $title) +/* + Internal general function to clear metadata +*/ + { + if (is_array($title)) { + if (count($title)) + qa_db_query_sub( + 'DELETE FROM ^'.$metatable.' WHERE '.$idcolumn.'=$ AND title IN ($)', + $idvalue, $title + ); + + } else + qa_db_query_sub( + 'DELETE FROM ^'.$metatable.' WHERE '.$idcolumn.'=$ AND title=$', + $idvalue, $title + ); + } + + + function qa_db_meta_get($metatable, $idcolumn, $idvalue, $title) +/* + Internal general function to return metadata +*/ + { + if (is_array($title)) { + if (count($title)) + return qa_db_read_all_assoc(qa_db_query_sub( + 'SELECT title, content FROM ^'.$metatable.' WHERE '.$idcolumn.'=$ AND title IN($)', + $idvalue, $title + ), 'title', 'content'); + else + return array(); + + } else + return qa_db_read_one_value(qa_db_query_sub( + 'SELECT content FROM ^'.$metatable.' WHERE '.$idcolumn.'=$ AND title=$', + $idvalue, $title + ), true); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-db-notices.php b/qa-include/qa-db-notices.php new file mode 100644 index 000000000..d51ad2883 --- /dev/null +++ b/qa-include/qa-db-notices.php @@ -0,0 +1,73 @@ + array( + 'multiple' => $options['points_multiple']*$options['points_post_q'], + 'formula' => "COUNT(*) AS qposts FROM ^posts AS userid_src WHERE userid~ AND type='Q'", + ), + + 'aposts' => array( + 'multiple' => $options['points_multiple']*$options['points_post_a'], + 'formula' => "COUNT(*) AS aposts FROM ^posts AS userid_src WHERE userid~ AND type='A'", + ), + + 'cposts' => array( + 'multiple' => 0, + 'formula' => "COUNT(*) AS cposts FROM ^posts AS userid_src WHERE userid~ AND type='C'", + ), + + 'aselects' => array( + 'multiple' => $options['points_multiple']*$options['points_select_a'], + 'formula' => "COUNT(*) AS aselects FROM ^posts AS userid_src WHERE userid~ AND type='Q' AND selchildid IS NOT NULL", + ), + + 'aselecteds' => array( + 'multiple' => $options['points_multiple']*$options['points_a_selected'], + 'formula' => "COUNT(*) AS aselecteds FROM ^posts AS userid_src JOIN ^posts AS questions ON questions.selchildid=userid_src.postid WHERE userid_src.userid~ AND userid_src.type='A' AND NOT (questions.userid<=>userid_src.userid)", + ), + + 'qupvotes' => array( + 'multiple' => $options['points_multiple']*$options['points_vote_up_q'], + 'formula' => "COUNT(*) AS qupvotes FROM ^uservotes AS userid_src JOIN ^posts ON userid_src.postid=^posts.postid WHERE userid_src.userid~ AND LEFT(^posts.type, 1)='Q' AND userid_src.vote>0", + ), + + 'qdownvotes' => array( + 'multiple' => $options['points_multiple']*$options['points_vote_down_q'], + 'formula' => "COUNT(*) AS qdownvotes FROM ^uservotes AS userid_src JOIN ^posts ON userid_src.postid=^posts.postid WHERE userid_src.userid~ AND LEFT(^posts.type, 1)='Q' AND userid_src.vote<0", + ), + + 'aupvotes' => array( + 'multiple' => $options['points_multiple']*$options['points_vote_up_a'], + 'formula' => "COUNT(*) AS aupvotes FROM ^uservotes AS userid_src JOIN ^posts ON userid_src.postid=^posts.postid WHERE userid_src.userid~ AND LEFT(^posts.type, 1)='A' AND userid_src.vote>0", + ), + + 'adownvotes' => array( + 'multiple' => $options['points_multiple']*$options['points_vote_down_a'], + 'formula' => "COUNT(*) AS adownvotes FROM ^uservotes AS userid_src JOIN ^posts ON userid_src.postid=^posts.postid WHERE userid_src.userid~ AND LEFT(^posts.type, 1)='A' AND userid_src.vote<0", + ), + + 'qvoteds' => array( + 'multiple' => $options['points_multiple'], + 'formula' => "COALESCE(SUM(". + "LEAST(".((int)$options['points_per_q_voted_up'])."*upvotes,".((int)$options['points_q_voted_max_gain']).")". + "-". + "LEAST(".((int)$options['points_per_q_voted_down'])."*downvotes,".((int)$options['points_q_voted_max_loss']).")". + "), 0) AS qvoteds FROM ^posts AS userid_src WHERE LEFT(type, 1)='Q' AND userid~", + ), + + 'avoteds' => array( + 'multiple' => $options['points_multiple'], + 'formula' => "COALESCE(SUM(". + "LEAST(".((int)$options['points_per_a_voted_up'])."*upvotes,".((int)$options['points_a_voted_max_gain']).")". + "-". + "LEAST(".((int)$options['points_per_a_voted_down'])."*downvotes,".((int)$options['points_a_voted_max_loss']).")". + "), 0) AS avoteds FROM ^posts AS userid_src WHERE LEFT(type, 1)='A' AND userid~", + ), + + 'upvoteds' => array( + 'multiple' => 0, + 'formula' => "COALESCE(SUM(upvotes), 0) AS upvoteds FROM ^posts AS userid_src WHERE userid~", + ), + + 'downvoteds' => array( + 'multiple' => 0, + 'formula' => "COALESCE(SUM(downvotes), 0) AS downvoteds FROM ^posts AS userid_src WHERE userid~", + ), + ); + } + + + function qa_db_points_update_ifuser($userid, $columns) +/* + Update the userpoints table in the database for $userid and $columns, plus the summary points column. + Set $columns to true for all, empty for none, an array for several, or a single value for one. + This dynamically builds some fairly crazy looking SQL, but it works, and saves repeat calculations. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + if (qa_should_update_counts() && isset($userid)) { + require_once QA_INCLUDE_DIR.'qa-app-options.php'; + + $calculations=qa_db_points_calculations(); + + if ($columns===true) + $keycolumns=$calculations; + elseif (empty($columns)) + $keycolumns=array(); + elseif (is_array($columns)) + $keycolumns=array_flip($columns); + else + $keycolumns=array($columns => true); + + $insertfields='userid, '; + $insertvalues='$, '; + $insertpoints=(int)qa_opt('points_base'); + + $updates=''; + $updatepoints=$insertpoints; + + foreach ($calculations as $field => $calculation) { + $multiple=(int)$calculation['multiple']; + + if (isset($keycolumns[$field])) { + $insertfields.=$field.', '; + $insertvalues.='@_'.$field.':=(SELECT '.$calculation['formula'].'), '; + $updates.=$field.'=@_'.$field.', '; + $insertpoints.='+('.(int)$multiple.'*@_'.$field.')'; + } + + $updatepoints.='+('.$multiple.'*'.(isset($keycolumns[$field]) ? '@_' : '').$field.')'; + } + + $query='INSERT INTO ^userpoints ('.$insertfields.'points) VALUES ('.$insertvalues.$insertpoints.') '. + 'ON DUPLICATE KEY UPDATE '.$updates.'points='.$updatepoints.'+bonus'; + + qa_db_query_raw(str_replace('~', "='".qa_db_escape_string($userid)."'", qa_db_apply_sub($query, array($userid)))); + // build like this so that a #, $ or ^ character in the $userid (if external integration) isn't substituted + + if (qa_db_insert_on_duplicate_inserted()) + qa_db_userpointscount_update(); + } + } + + + function qa_db_points_set_bonus($userid, $bonus) +/* + Set the number of explicit bonus points for $userid to $bonus +*/ + { + qa_db_query_sub( + "INSERT INTO ^userpoints (userid, bonus) VALUES ($, #) ON DUPLICATE KEY UPDATE bonus=#", + $userid, $bonus, $bonus + ); + } + + + function qa_db_userpointscount_update() +/* + Update the cached count in the database of the number of rows in the userpoints table +*/ + { + if (qa_should_update_counts()) + qa_db_query_sub("REPLACE ^options (title, content) SELECT 'cache_userpointscount', COUNT(*) FROM ^userpoints"); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-db-post-create.php b/qa-include/qa-db-post-create.php new file mode 100644 index 000000000..16fa2937b --- /dev/null +++ b/qa-include/qa-db-post-create.php @@ -0,0 +1,358 @@ + $count) + $rowstoadd[]=array($postid, $wordid, $count, $type, $questionid); + + qa_db_query_sub( + 'INSERT INTO ^contentwords (postid, wordid, count, type, questionid) VALUES #', + $rowstoadd + ); + } + } + + + function qa_db_tagwords_add_post_wordids($postid, $wordids) +/* + Add rows into the database index of individual tag words, where $postid contains the words $wordids +*/ + { + if (count($wordids)) { + $rowstoadd=array(); + foreach ($wordids as $wordid) + $rowstoadd[]=array($postid, $wordid); + + qa_db_query_sub( + 'INSERT INTO ^tagwords (postid, wordid) VALUES #', + $rowstoadd + ); + } + } + + + function qa_db_posttags_add_post_wordids($postid, $wordids) +/* + Add rows into the database index of whole tags, where $postid contains the tags $wordids +*/ + { + if (count($wordids)) + qa_db_query_sub( + 'INSERT INTO ^posttags (postid, wordid, postcreated) SELECT postid, wordid, created FROM ^words, ^posts WHERE postid=# AND wordid IN ($)', + $postid, $wordids + ); + } + + + function qa_db_word_mapto_ids($words) +/* + Return an array mapping each word in $words to its corresponding wordid in the database +*/ + { + if (count($words)) + return qa_db_read_all_assoc(qa_db_query_sub( + 'SELECT wordid, word FROM ^words WHERE word IN ($)', $words + ), 'word', 'wordid'); + else + return array(); + } + + + function qa_db_word_mapto_ids_add($words) +/* + Return an array mapping each word in $words to its corresponding wordid in the database, adding any that are missing +*/ + { + $wordtoid=qa_db_word_mapto_ids($words); + + $wordstoadd=array(); + foreach ($words as $word) + if (!isset($wordtoid[$word])) + $wordstoadd[]=$word; + + if (count($wordstoadd)) { + qa_db_query_sub('LOCK TABLES ^words WRITE'); // to prevent two requests adding the same word + + $wordtoid=qa_db_word_mapto_ids($words); // map it again in case table content changed before it was locked + + $rowstoadd=array(); + foreach ($words as $word) + if (!isset($wordtoid[$word])) + $rowstoadd[]=array($word); + + qa_db_query_sub('INSERT INTO ^words (word) VALUES $', $rowstoadd); + + qa_db_query_sub('UNLOCK TABLES'); + + $wordtoid=qa_db_word_mapto_ids($words); // do it one last time + } + + return $wordtoid; + } + + + function qa_db_word_titlecount_update($wordids) +/* + Update the titlecount column in the database for the words in $wordids, based on how many posts they appear in the title of +*/ + { + if (qa_should_update_counts() && count($wordids)) + qa_db_query_sub( + 'UPDATE ^words AS x, (SELECT ^words.wordid, COUNT(^titlewords.wordid) AS titlecount FROM ^words LEFT JOIN ^titlewords ON ^titlewords.wordid=^words.wordid WHERE ^words.wordid IN (#) GROUP BY wordid) AS a SET x.titlecount=a.titlecount WHERE x.wordid=a.wordid', + $wordids + ); + } + + + function qa_db_word_contentcount_update($wordids) +/* + Update the contentcount column in the database for the words in $wordids, based on how many posts they appear in the content of +*/ + { + if (qa_should_update_counts() && count($wordids)) + qa_db_query_sub( + 'UPDATE ^words AS x, (SELECT ^words.wordid, COUNT(^contentwords.wordid) AS contentcount FROM ^words LEFT JOIN ^contentwords ON ^contentwords.wordid=^words.wordid WHERE ^words.wordid IN (#) GROUP BY wordid) AS a SET x.contentcount=a.contentcount WHERE x.wordid=a.wordid', + $wordids + ); + } + + + function qa_db_word_tagwordcount_update($wordids) +/* + Update the tagwordcount column in the database for the individual tag words in $wordids, based on how many posts they appear in the tags of +*/ + { + if (qa_should_update_counts() && count($wordids)) + qa_db_query_sub( + 'UPDATE ^words AS x, (SELECT ^words.wordid, COUNT(^tagwords.wordid) AS tagwordcount FROM ^words LEFT JOIN ^tagwords ON ^tagwords.wordid=^words.wordid WHERE ^words.wordid IN (#) GROUP BY wordid) AS a SET x.tagwordcount=a.tagwordcount WHERE x.wordid=a.wordid', + $wordids + ); + } + + + function qa_db_word_tagcount_update($wordids) +/* + Update the tagcount column in the database for the whole tags in $wordids, based on how many posts they appear as tags of +*/ + { + if (qa_should_update_counts() && count($wordids)) + qa_db_query_sub( + 'UPDATE ^words AS x, (SELECT ^words.wordid, COUNT(^posttags.wordid) AS tagcount FROM ^words LEFT JOIN ^posttags ON ^posttags.wordid=^words.wordid WHERE ^words.wordid IN (#) GROUP BY wordid) AS a SET x.tagcount=a.tagcount WHERE x.wordid=a.wordid', + $wordids + ); + } + + + function qa_db_qcount_update() +/* + Update the cached count in the database of the number of questions (excluding hidden/queued) +*/ + { + if (qa_should_update_counts()) + qa_db_query_sub("REPLACE ^options (title, content) SELECT 'cache_qcount', COUNT(*) FROM ^posts WHERE type='Q'"); + } + + + function qa_db_acount_update() +/* + Update the cached count in the database of the number of answers (excluding hidden/queued) +*/ + { + if (qa_should_update_counts()) + qa_db_query_sub("REPLACE ^options (title, content) SELECT 'cache_acount', COUNT(*) FROM ^posts WHERE type='A'"); + } + + + function qa_db_ccount_update() +/* + Update the cached count in the database of the number of comments (excluding hidden/queued) +*/ + { + if (qa_should_update_counts()) + qa_db_query_sub("REPLACE ^options (title, content) SELECT 'cache_ccount', COUNT(*) FROM ^posts WHERE type='C'"); + } + + + function qa_db_tagcount_update() +/* + Update the cached count in the database of the number of different tags used +*/ + { + if (qa_should_update_counts()) + qa_db_query_sub("REPLACE ^options (title, content) SELECT 'cache_tagcount', COUNT(*) FROM ^words WHERE tagcount>0"); + } + + + function qa_db_unaqcount_update() +/* + Update the cached count in the database of the number of unanswered questions (excluding hidden/queued) +*/ + { + if (qa_should_update_counts()) + qa_db_query_sub("REPLACE ^options (title, content) SELECT 'cache_unaqcount', COUNT(*) FROM ^posts WHERE type='Q' AND acount=0"); + } + + + function qa_db_unselqcount_update() +/* + Update the cached count in the database of the number of questions with no answer selected (excluding hidden/queued) +*/ + { + if (qa_should_update_counts()) + qa_db_query_sub("REPLACE ^options (title, content) SELECT 'cache_unselqcount', COUNT(*) FROM ^posts WHERE type='Q' AND selchildid IS NULL"); + } + + + function qa_db_unupaqcount_update() +/* + Update the cached count in the database of the number of questions with no upvoted answers (excluding hidden/queued) +*/ + { + if (qa_should_update_counts()) + qa_db_query_sub("REPLACE ^options (title, content) SELECT 'cache_unupaqcount', COUNT(*) FROM ^posts WHERE type='Q' AND amaxvote=0"); + } + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-db-post-update.php b/qa-include/qa-db-post-update.php new file mode 100644 index 000000000..d5a74ae7e --- /dev/null +++ b/qa-include/qa-db-post-update.php @@ -0,0 +1,318 @@ +=# AND ( (^posts.type='Q') OR (^posts.type='A' AND parent.type<=>'Q') OR (^posts.type='C' AND parent.type<=>'Q') OR (^posts.type='C' AND parent.type<=>'A' AND grandparent.type<=>'Q') ) ORDER BY postid LIMIT #", + $startpostid, $count + ), 'postid'); + } + + + function qa_db_prepare_for_reindexing($firstpostid, $lastpostid) +/* + Prepare posts $firstpostid to $lastpostid for reindexing in the database by removing their prior index entries +*/ + { + qa_db_query_sub( + 'DELETE FROM ^titlewords WHERE postid>=# AND postid<=#', + $firstpostid, $lastpostid + ); + + qa_db_query_sub( + 'DELETE FROM ^contentwords WHERE postid>=# AND postid<=#', + $firstpostid, $lastpostid + ); + + qa_db_query_sub( + 'DELETE FROM ^tagwords WHERE postid>=# AND postid<=#', + $firstpostid, $lastpostid + ); + + qa_db_query_sub( + 'DELETE FROM ^posttags WHERE postid>=# AND postid<=#', + $firstpostid, $lastpostid + ); + } + + + function qa_db_truncate_indexes($firstpostid) +/* + Remove any rows in the database word indexes with postid from $firstpostid upwards +*/ + { + qa_db_query_sub( + 'DELETE FROM ^titlewords WHERE postid>=#', + $firstpostid + ); + + qa_db_query_sub( + 'DELETE FROM ^contentwords WHERE postid>=#', + $firstpostid + ); + + qa_db_query_sub( + 'DELETE FROM ^tagwords WHERE postid>=#', + $firstpostid + ); + + qa_db_query_sub( + 'DELETE FROM ^posttags WHERE postid>=#', + $firstpostid + ); + } + + + function qa_db_count_words() +/* + Return the number of words currently referenced in the database +*/ + { + return qa_db_read_one_value(qa_db_query_sub( + 'SELECT COUNT(*) FROM ^words' + )); + } + + + function qa_db_words_prepare_for_recounting($startwordid, $count) +/* + Return the ids of up to $count words in the database starting from $startwordid +*/ + { + return qa_db_read_all_values(qa_db_query_sub( + 'SELECT wordid FROM ^words WHERE wordid>=# ORDER BY wordid LIMIT #', + $startwordid, $count + )); + } + + + function qa_db_words_recount($firstwordid, $lastwordid) +/* + Recalculate the cached counts for words $firstwordid to $lastwordid in the database +*/ + { + qa_db_query_sub( + 'UPDATE ^words AS x, (SELECT ^words.wordid, COUNT(^titlewords.wordid) AS titlecount FROM ^words LEFT JOIN ^titlewords ON ^titlewords.wordid=^words.wordid WHERE ^words.wordid>=# AND ^words.wordid<=# GROUP BY wordid) AS a SET x.titlecount=a.titlecount WHERE x.wordid=a.wordid', + $firstwordid, $lastwordid + ); + + qa_db_query_sub( + 'UPDATE ^words AS x, (SELECT ^words.wordid, COUNT(^contentwords.wordid) AS contentcount FROM ^words LEFT JOIN ^contentwords ON ^contentwords.wordid=^words.wordid WHERE ^words.wordid>=# AND ^words.wordid<=# GROUP BY wordid) AS a SET x.contentcount=a.contentcount WHERE x.wordid=a.wordid', + $firstwordid, $lastwordid + ); + + qa_db_query_sub( + 'UPDATE ^words AS x, (SELECT ^words.wordid, COUNT(^tagwords.wordid) AS tagwordcount FROM ^words LEFT JOIN ^tagwords ON ^tagwords.wordid=^words.wordid WHERE ^words.wordid>=# AND ^words.wordid<=# GROUP BY wordid) AS a SET x.tagwordcount=a.tagwordcount WHERE x.wordid=a.wordid', + $firstwordid, $lastwordid + ); + + qa_db_query_sub( + 'UPDATE ^words AS x, (SELECT ^words.wordid, COUNT(^posttags.wordid) AS tagcount FROM ^words LEFT JOIN ^posttags ON ^posttags.wordid=^words.wordid WHERE ^words.wordid>=# AND ^words.wordid<=# GROUP BY wordid) AS a SET x.tagcount=a.tagcount WHERE x.wordid=a.wordid', + $firstwordid, $lastwordid + ); + + qa_db_query_sub( + 'DELETE FROM ^words WHERE wordid>=# AND wordid<=# AND titlecount=0 AND contentcount=0 AND tagwordcount=0 AND tagcount=0', + $firstwordid, $lastwordid + ); + } + + +// For recalculating numbers of votes and answers for questions... + + function qa_db_posts_get_for_recounting($startpostid, $count) +/* + Return the ids of up to $count posts in the database starting from $startpostid +*/ + { + return qa_db_read_all_values(qa_db_query_sub( + 'SELECT postid FROM ^posts WHERE postid>=# ORDER BY postid LIMIT #', + $startpostid, $count + )); + } + + + function qa_db_posts_votes_recount($firstpostid, $lastpostid) +/* + Recalculate the cached vote counts for posts $firstpostid to $lastpostid in the database +*/ + { + qa_db_query_sub( + 'UPDATE ^posts AS x, (SELECT ^posts.postid, COALESCE(SUM(GREATEST(0,^uservotes.vote)),0) AS upvotes, -COALESCE(SUM(LEAST(0,^uservotes.vote)),0) AS downvotes, COALESCE(SUM(IF(^uservotes.flag, 1, 0)),0) AS flagcount FROM ^posts LEFT JOIN ^uservotes ON ^uservotes.postid=^posts.postid WHERE ^posts.postid>=# AND ^posts.postid<=# GROUP BY postid) AS a SET x.upvotes=a.upvotes, x.downvotes=a.downvotes, x.netvotes=a.upvotes-a.downvotes, x.flagcount=a.flagcount WHERE x.postid=a.postid', + $firstpostid, $lastpostid + ); + + qa_db_hotness_update($firstpostid, $lastpostid); + } + + + function qa_db_posts_answers_recount($firstpostid, $lastpostid) +/* + Recalculate the cached answer counts for posts $firstpostid to $lastpostid in the database, along with the highest netvotes of any of their answers +*/ + { + require_once QA_INCLUDE_DIR.'qa-db-hotness.php'; + + qa_db_query_sub( + 'UPDATE ^posts AS x, (SELECT parents.postid, COUNT(children.postid) AS acount, COALESCE(GREATEST(MAX(children.netvotes), 0), 0) AS amaxvote FROM ^posts AS parents LEFT JOIN ^posts AS children ON parents.postid=children.parentid AND children.type=\'A\' WHERE parents.postid>=# AND parents.postid<=# GROUP BY postid) AS a SET x.acount=a.acount, x.amaxvote=a.amaxvote WHERE x.postid=a.postid', + $firstpostid, $lastpostid + ); + + qa_db_hotness_update($firstpostid, $lastpostid); + } + + +// For recalculating user points... + + function qa_db_users_get_for_recalc_points($startuserid, $count) +/* + Return the ids of up to $count users in the database starting from $startuserid + If using single sign-on integration, base this on user activity rather than the users table which we don't have +*/ + { + if (QA_FINAL_EXTERNAL_USERS) + return qa_db_read_all_values(qa_db_query_sub( + '(SELECT DISTINCT userid FROM ^posts WHERE userid>=# ORDER BY userid LIMIT #) UNION (SELECT DISTINCT userid FROM ^uservotes WHERE userid>=# ORDER BY userid LIMIT #)', + $startuserid, $count, $startuserid, $count + )); + else + return qa_db_read_all_values(qa_db_query_sub( + 'SELECT DISTINCT userid FROM ^users WHERE userid>=# ORDER BY userid LIMIT #', + $startuserid, $count + )); + } + + + function qa_db_users_recalc_points($firstuserid, $lastuserid) +/* + Recalculate all userpoints columns for users $firstuserid to $lastuserid in the database +*/ + { + require_once QA_INCLUDE_DIR.'qa-db-points.php'; + + $qa_userpoints_calculations=qa_db_points_calculations(); + + qa_db_query_sub( + 'DELETE FROM ^userpoints WHERE userid>=# AND userid<=# AND bonus=0', // delete those with no bonus + $firstuserid, $lastuserid + ); + + $zeropoints='points=0'; + foreach ($qa_userpoints_calculations as $field => $calculation) + $zeropoints.=', '.$field.'=0'; + + qa_db_query_sub( + 'UPDATE ^userpoints SET '.$zeropoints.' WHERE userid>=# AND userid<=#', // zero out the rest + $firstuserid, $lastuserid + ); + + if (QA_FINAL_EXTERNAL_USERS) + qa_db_query_sub( + 'INSERT IGNORE INTO ^userpoints (userid) SELECT DISTINCT userid FROM ^posts WHERE userid>=# AND userid<=# UNION SELECT DISTINCT userid FROM ^uservotes WHERE userid>=# AND userid<=#', + $firstuserid, $lastuserid, $firstuserid, $lastuserid + ); + else + qa_db_query_sub( + 'INSERT IGNORE INTO ^userpoints (userid) SELECT DISTINCT userid FROM ^users WHERE userid>=# AND userid<=#', + $firstuserid, $lastuserid + ); + + $updatepoints=(int)qa_opt('points_base'); + + foreach ($qa_userpoints_calculations as $field => $calculation) { + qa_db_query_sub( + 'UPDATE ^userpoints, (SELECT userid_src.userid, '.str_replace('~', ' BETWEEN # AND #', $calculation['formula']).' GROUP BY userid) AS results '. + 'SET ^userpoints.'.$field.'=results.'.$field.' WHERE ^userpoints.userid=results.userid', + $firstuserid, $lastuserid + ); + + $updatepoints.='+('.((int)$calculation['multiple']).'*'.$field.')'; + } + + qa_db_query_sub( + 'UPDATE ^userpoints SET points='.$updatepoints.'+bonus WHERE userid>=# AND userid<=#', + $firstuserid, $lastuserid + ); + } + + + function qa_db_truncate_userpoints($firstuserid) +/* + Remove any rows in the userpoints table from $firstuserid upwards +*/ + { + qa_db_query_sub( + 'DELETE FROM ^userpoints WHERE userid>=#', + $firstuserid + ); + } + + +// For refilling event streams... + + function qa_db_qs_get_for_event_refilling($startpostid, $count) +/* + Return the ids of up to $count questions in the database starting from $startpostid +*/ + { + return qa_db_read_all_values(qa_db_query_sub( + "SELECT postid FROM ^posts WHERE postid>=# AND LEFT(type, 1)='Q' ORDER BY postid LIMIT #", + $startpostid, $count + )); + } + + +// For recalculating categories... + + function qa_db_posts_get_for_recategorizing($startpostid, $count) +/* + Return the ids of up to $count posts (including queued/hidden) in the database starting from $startpostid +*/ + { + return qa_db_read_all_values(qa_db_query_sub( + "SELECT postid FROM ^posts WHERE postid>=# ORDER BY postid LIMIT #", + $startpostid, $count + )); + } + + + function qa_db_posts_recalc_categoryid($firstpostid, $lastpostid) +/* + Recalculate the (exact) categoryid for the posts (including queued/hidden) between $firstpostid and $lastpostid + in the database, where the category of comments and answers is set by the category of the antecedent question +*/ + { + qa_db_query_sub( + "UPDATE ^posts AS x, (SELECT ^posts.postid, IF(LEFT(parent.type, 1)='Q', parent.categoryid, grandparent.categoryid) AS categoryid FROM ^posts LEFT JOIN ^posts AS parent ON ^posts.parentid=parent.postid LEFT JOIN ^posts AS grandparent ON parent.parentid=grandparent.postid WHERE ^posts.postid BETWEEN # AND # AND LEFT(^posts.type, 1)!='Q') AS a SET x.categoryid=a.categoryid WHERE x.postid=a.postid", + $firstpostid, $lastpostid + ); + } + + + function qa_db_categories_get_for_recalcs($startcategoryid, $count) +/* + Return the ids of up to $count categories in the database starting from $startcategoryid +*/ + { + return qa_db_read_all_values(qa_db_query_sub( + "SELECT categoryid FROM ^categories WHERE categoryid>=# ORDER BY categoryid LIMIT #", + $startcategoryid, $count + )); + } + + +// For deleting hidden posts... + + function qa_db_posts_get_for_deleting($type, $startpostid=0, $limit=null) +/* + Return the ids of up to $limit posts of $type that can be deleted from the database (i.e. have no dependents) +*/ + { + $limitsql=isset($limit) ? (' LIMIT '.(int)$limit) : ''; + + return qa_db_read_all_values(qa_db_query_sub( + "SELECT ^posts.postid FROM ^posts LEFT JOIN ^posts AS child ON child.parentid=^posts.postid WHERE ^posts.type=$ AND ^posts.postid>=# AND child.postid IS NULL".$limitsql, + $type.'_HIDDEN', $startpostid + )); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-db-selects.php b/qa-include/qa-db-selects.php new file mode 100644 index 000000000..5e5e220fb --- /dev/null +++ b/qa-include/qa-db-selects.php @@ -0,0 +1,1466 @@ + $selectspec) // can pass null parameters + if (empty($selectspec)) + unset($selectspecs[$key]); + + if (is_array($qa_db_pending_selectspecs)) + foreach ($qa_db_pending_selectspecs as $pendingid => $selectspec) + if (!isset($qa_db_pending_results[$pendingid])) + $selectspecs['pending_'.$pendingid]=$selectspec; + + $outresults=qa_db_multi_select($selectspecs); + + if (is_array($qa_db_pending_selectspecs)) + foreach ($qa_db_pending_selectspecs as $pendingid => $selectspec) + if (!isset($qa_db_pending_results[$pendingid])) { + $qa_db_pending_results[$pendingid]=$outresults['pending_'.$pendingid]; + unset($outresults['pending_'.$pendingid]); + } + + return $singleresult ? $outresults[0] : $outresults; + } + + + function qa_db_queue_pending_select($pendingid, $selectspec) +/* + Queue a $selectspec for running later, with $pendingid (used for retrieval) +*/ + { + global $qa_db_pending_selectspecs; + + $qa_db_pending_selectspecs[$pendingid]=$selectspec; + } + + + function qa_db_get_pending_result($pendingid, $selectspec=null) +/* + Get the result of the queued SELECT query identified by $pendingid. Run the query if it hasn't run already. If + $selectspec is supplied, it doesn't matter if this hasn't been queued before - it will be queued and run now. +*/ + { + global $qa_db_pending_selectspecs, $qa_db_pending_results; + + if (isset($selectspec)) + qa_db_queue_pending_select($pendingid, $selectspec); + elseif (!isset($qa_db_pending_selectspecs[$pendingid])) + qa_fatal_error('Pending query was never set up: '.$pendingid); + + if (!isset($qa_db_pending_results[$pendingid])) + qa_db_select_with_pending(); + + return $qa_db_pending_results[$pendingid]; + } + + + function qa_db_flush_pending_result($pendingid) +/* + Remove the results of queued SELECT query identified by $pendingid if it has already been run. This means it will + run again if its results are requested via qa_db_get_pending_result() +*/ + { + global $qa_db_pending_results; + + unset($qa_db_pending_results[$pendingid]); + } + + + function qa_db_posts_basic_selectspec($voteuserid=null, $full=false, $user=true) +/* + Return the common selectspec used to build any selectspecs which retrieve posts from the database. + If $voteuserid is set, retrieve the vote made by a particular that user on each post. + If $full is true, get full information on the posts, instead of just information for listing pages. + If $user is true, get information about the user who wrote the post (or cookie if anonymous). +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $selectspec=array( + 'columns' => array( + '^posts.postid', '^posts.categoryid', '^posts.type', 'basetype' => 'LEFT(^posts.type, 1)', 'hidden' => "INSTR(^posts.type, '_HIDDEN')>0", + '^posts.acount', '^posts.selchildid', '^posts.closedbyid', '^posts.upvotes', '^posts.downvotes', '^posts.netvotes', '^posts.views', '^posts.hotness', + '^posts.flagcount', '^posts.title', '^posts.tags', 'created' => 'UNIX_TIMESTAMP(^posts.created)', + 'categoryname' => '^categories.title', 'categorybackpath' => "^categories.backpath", + ), + + 'arraykey' => 'postid', + 'source' => '^posts LEFT JOIN ^categories ON ^categories.categoryid=^posts.categoryid', + 'arguments' => array(), + ); + + if (isset($voteuserid)) { + $selectspec['columns']['uservote']='^uservotes.vote'; + $selectspec['columns']['userflag']='^uservotes.flag'; + $selectspec['source'].=' LEFT JOIN ^uservotes ON ^posts.postid=^uservotes.postid AND ^uservotes.userid=$'; + $selectspec['arguments'][]=$voteuserid; + } + + if ($full) { + $selectspec['columns']['content']='^posts.content'; + $selectspec['columns']['notify']='^posts.notify'; + $selectspec['columns']['updated']='UNIX_TIMESTAMP(^posts.updated)'; + $selectspec['columns']['updatetype']='^posts.updatetype'; + $selectspec['columns'][]='^posts.format'; + $selectspec['columns'][]='^posts.lastuserid'; + $selectspec['columns']['lastip']='INET_NTOA(^posts.lastip)'; + $selectspec['columns'][]='^posts.parentid'; + $selectspec['columns']['lastviewip']='INET_NTOA(^posts.lastviewip)'; + } + + if ($user) { + $selectspec['columns'][]='^posts.userid'; + $selectspec['columns'][]='^posts.cookieid'; + $selectspec['columns']['createip']='INET_NTOA(^posts.createip)'; + $selectspec['columns'][]='^userpoints.points'; + + if (!QA_FINAL_EXTERNAL_USERS) { + $selectspec['columns'][]='^users.flags'; + $selectspec['columns'][]='^users.level'; + $selectspec['columns']['email']='^users.email'; + $selectspec['columns']['handle']='^users.handle'; + $selectspec['columns'][]='^users.avatarblobid'; + $selectspec['columns'][]='^users.avatarwidth'; + $selectspec['columns'][]='^users.avatarheight'; + $selectspec['source'].=' LEFT JOIN ^users ON ^posts.userid=^users.userid'; + + if ($full) { + $selectspec['columns']['lasthandle']='lastusers.handle'; + $selectspec['source'].=' LEFT JOIN ^users AS lastusers ON ^posts.lastuserid=lastusers.userid'; + } + } + + $selectspec['source'].=' LEFT JOIN ^userpoints ON ^posts.userid=^userpoints.userid'; + } + + return $selectspec; + } + + + function qa_db_add_selectspec_opost(&$selectspec, $poststable, $fromupdated=false, $full=false) +/* + Supplement a selectspec returned by qa_db_posts_basic_selectspec() to get information about another post (answer or + comment) which is related to the main post (question) retrieved. Pass the name of table which will contain the other + post in $poststable. Set $fromupdated to true to get information about when this other post was edited, rather than + created. If $full is true, get full information on this other post. +*/ + { + $selectspec['arraykey']='opostid'; + + $selectspec['columns']['obasetype']='LEFT('.$poststable.'.type, 1)'; + $selectspec['columns']['ohidden']="INSTR(".$poststable.".type, '_HIDDEN')>0"; + $selectspec['columns']['opostid']=$poststable.'.postid'; + $selectspec['columns']['ouserid']=$poststable.($fromupdated ? '.lastuserid' : '.userid'); + $selectspec['columns']['ocookieid']=$poststable.'.cookieid'; + $selectspec['columns']['oip']='INET_NTOA('.$poststable.($fromupdated ? '.lastip' : '.createip').')'; + $selectspec['columns']['otime']='UNIX_TIMESTAMP('.$poststable.($fromupdated ? '.updated' : '.created').')'; + $selectspec['columns']['oflagcount']=$poststable.'.flagcount'; + + if ($fromupdated) + $selectspec['columns']['oupdatetype']=$poststable.'.updatetype'; + + if ($full) { + $selectspec['columns']['ocontent']=$poststable.'.content'; + $selectspec['columns']['oformat']=$poststable.'.format'; + + if (!$fromupdated) + $selectspec['columns']['oupdated']='UNIX_TIMESTAMP('.$poststable.'.updated)'; + } + } + + + function qa_db_add_selectspec_ousers(&$selectspec, $userstable, $pointstable) +/* + Supplement a selectspec returned by qa_db_posts_basic_selectspec() to get information about the author of another + post (answer or comment) which is related to the main post (question) retrieved. Pass the name of table which will + contain the other user's details in $userstable and the name of the table which will contain the other user's points + in $pointstable. +*/ + { + if (!QA_FINAL_EXTERNAL_USERS) { + $selectspec['columns']['oflags']=$userstable.'.flags'; + $selectspec['columns']['olevel']=$userstable.'.level'; + $selectspec['columns']['oemail']=$userstable.'.email'; + $selectspec['columns']['ohandle']=$userstable.'.handle'; + $selectspec['columns']['oavatarblobid']='BINARY '.$userstable.'.avatarblobid'; // cast to BINARY due to MySQL bug which renders it signed in a union + $selectspec['columns']['oavatarwidth']=$userstable.'.avatarwidth'; + $selectspec['columns']['oavatarheight']=$userstable.'.avatarheight'; + } + + $selectspec['columns']['opoints']=$pointstable.'.points'; + } + + + function qa_db_slugs_to_backpath($categoryslugs) +/* + Given $categoryslugs in order of the hierarchiy, return the equivalent value for the backpath column in the categories table +*/ + { + if (!is_array($categoryslugs)) // accept old-style string arguments for one category deep + $categoryslugs=array($categoryslugs); + + return implode('/', array_reverse($categoryslugs)); + } + + + function qa_db_categoryslugs_sql_args($categoryslugs, &$arguments) +/* + Return SQL code that represents the constraint of a post being in the category with $categoryslugs, or any of its subcategories +*/ + { + if (!is_array($categoryslugs)) // accept old-style string arguments for one category deep + $categoryslugs=strlen($categoryslugs) ? array($categoryslugs) : array(); + + $levels=count($categoryslugs); + + if (($levels>0) && ($levels<=QA_CATEGORY_DEPTH)) { + $arguments[]=qa_db_slugs_to_backpath($categoryslugs); + return (($levels==QA_CATEGORY_DEPTH) ? 'categoryid' : ('catidpath'.$levels)).'=(SELECT categoryid FROM ^categories WHERE backpath=$ LIMIT 1) AND '; + } + + return ''; + } + + + function qa_db_qs_selectspec($voteuserid, $sort, $start, $categoryslugs=null, $createip=null, $specialtype=false, $full=false, $count=null) +/* + Return the selectspec to retrieve questions (of type $specialtype if provided, or 'Q' by default) sorted by $sort, + restricted to $createip (if not null) and the category for $categoryslugs (if not null), with the corresponding vote + made by $voteuserid (if not null) and including $full content or not. Return $count (if null, a default is used) + questions starting from offset $start. +*/ + { + if (($specialtype=='Q') || ($specialtype=='Q_QUEUED')) + $type=$specialtype; + else + $type=$specialtype ? 'Q_HIDDEN' : 'Q'; // for backwards compatibility + + $count=isset($count) ? min($count, QA_DB_RETRIEVE_QS_AS) : QA_DB_RETRIEVE_QS_AS; + + switch ($sort) { + case 'acount': + case 'flagcount': + case 'netvotes': + case 'views': + $sortsql='ORDER BY ^posts.'.$sort.' DESC, ^posts.created DESC'; + break; + + case 'created': + case 'hotness': + $sortsql='ORDER BY ^posts.'.$sort.' DESC'; + break; + + default: + qa_fatal_error('qa_db_qs_selectspec() called with illegal sort value'); + break; + } + + $selectspec=qa_db_posts_basic_selectspec($voteuserid, $full); + + $selectspec['source'].=" JOIN (SELECT postid FROM ^posts WHERE ". + qa_db_categoryslugs_sql_args($categoryslugs, $selectspec['arguments']). + (isset($createip) ? "createip=INET_ATON($) AND " : ""). + "type=$ ".$sortsql." LIMIT #,#) y ON ^posts.postid=y.postid"; + + if (isset($createip)) + $selectspec['arguments'][]=$createip; + + array_push($selectspec['arguments'], $type, $start, $count); + + $selectspec['sortdesc']=$sort; + + return $selectspec; + } + + + function qa_db_unanswered_qs_selectspec($voteuserid, $by, $start, $categoryslugs=null, $specialtype=false, $full=false, $count=null) +/* + Return the selectspec to retrieve recent questions (of type $specialtype if provided, or 'Q' by default) which, + depending on $by, either (a) have no answers, (b) have on selected answers, or (c) have no upvoted answers. The + questions are restricted to the category for $categoryslugs (if not null), and will have the corresponding vote made + by $voteuserid (if not null) and will include $full content or not. Return $count (if null, a default is used) + questions starting from offset $start. +*/ + { + if (($specialtype=='Q') || ($specialtype=='Q_QUEUED')) + $type=$specialtype; + else + $type=$specialtype ? 'Q_HIDDEN' : 'Q'; // for backwards compatibility + + $count=isset($count) ? min($count, QA_DB_RETRIEVE_QS_AS) : QA_DB_RETRIEVE_QS_AS; + + switch ($by) { + case 'selchildid': + $bysql='selchildid IS NULL'; + break; + + case 'amaxvote': + $bysql='amaxvote=0'; + break; + + default: + $bysql='acount=0'; + break; + } + + + $selectspec=qa_db_posts_basic_selectspec($voteuserid, $full); + + $selectspec['source'].=" JOIN (SELECT postid FROM ^posts WHERE ".qa_db_categoryslugs_sql_args($categoryslugs, $selectspec['arguments'])."type=$ AND ".$bysql." ORDER BY ^posts.created DESC LIMIT #,#) y ON ^posts.postid=y.postid"; + + array_push($selectspec['arguments'], $type, $start, $count); + + $selectspec['sortdesc']='created'; + + return $selectspec; + } + + + function qa_db_recent_a_qs_selectspec($voteuserid, $start, $categoryslugs=null, $createip=null, $specialtype=false, $fullanswers=false, $count=null) +/* + Return the selectspec to retrieve the antecedent questions for recent answers (of type $specialtype if provided, or + 'A' by default), restricted to $createip (if not null) and the category for $categoryslugs (if not null), with the + corresponding vote on those questions made by $voteuserid (if not null). Return $count (if null, a default is used) + questions starting from offset $start. The selectspec will also retrieve some information about the answers + themselves (including the content if $fullanswers is true), in columns named with the prefix 'o'. +*/ + { + if (($specialtype=='A') || ($specialtype=='A_QUEUED')) + $type=$specialtype; + else + $type=$specialtype ? 'A_HIDDEN' : 'A'; // for backwards compatibility + + $count=isset($count) ? min($count, QA_DB_RETRIEVE_QS_AS) : QA_DB_RETRIEVE_QS_AS; + + $selectspec=qa_db_posts_basic_selectspec($voteuserid); + + qa_db_add_selectspec_opost($selectspec, 'aposts', false, $fullanswers); + qa_db_add_selectspec_ousers($selectspec, 'ausers', 'auserpoints'); + + $selectspec['source'].=" JOIN ^posts AS aposts ON ^posts.postid=aposts.parentid". + (QA_FINAL_EXTERNAL_USERS ? "" : " LEFT JOIN ^users AS ausers ON aposts.userid=ausers.userid"). + " LEFT JOIN ^userpoints AS auserpoints ON aposts.userid=auserpoints.userid". + " JOIN (SELECT postid FROM ^posts WHERE ". + qa_db_categoryslugs_sql_args($categoryslugs, $selectspec['arguments']). + (isset($createip) ? "createip=INET_ATON($) AND " : ""). + "type=$ ORDER BY ^posts.created DESC LIMIT #,#) y ON aposts.postid=y.postid". + ($specialtype ? '' : " WHERE ^posts.type='Q'"); + + if (isset($createip)) + $selectspec['arguments'][]=$createip; + + array_push($selectspec['arguments'], $type, $start, $count); + + $selectspec['sortdesc']='otime'; + + return $selectspec; + } + + + function qa_db_recent_c_qs_selectspec($voteuserid, $start, $categoryslugs=null, $createip=null, $specialtype=false, $fullcomments=false, $count=null) +/* + Return the selectspec to retrieve the antecedent questions for recent comments (of type $specialtype if provided, or + 'C' by default), restricted to $createip (if not null) and the category for $categoryslugs (if not null), with the + corresponding vote on those questions made by $voteuserid (if not null). Return $count (if null, a default is used) + questions starting from offset $start. The selectspec will also retrieve some information about the comments + themselves (including the content if $fullcomments is true), in columns named with the prefix 'o'. +*/ + { + if (($specialtype=='C') || ($specialtype=='C_QUEUED')) + $type=$specialtype; + else + $type=$specialtype ? 'C_HIDDEN' : 'C'; // for backwards compatibility + + $count=isset($count) ? min($count, QA_DB_RETRIEVE_QS_AS) : QA_DB_RETRIEVE_QS_AS; + + $selectspec=qa_db_posts_basic_selectspec($voteuserid); + + qa_db_add_selectspec_opost($selectspec, 'cposts', false, $fullcomments); + qa_db_add_selectspec_ousers($selectspec, 'cusers', 'cuserpoints'); + + $selectspec['source'].=" JOIN ^posts AS parentposts ON". + " ^posts.postid=(CASE LEFT(parentposts.type, 1) WHEN 'A' THEN parentposts.parentid ELSE parentposts.postid END)". + " JOIN ^posts AS cposts ON parentposts.postid=cposts.parentid". + (QA_FINAL_EXTERNAL_USERS ? "" : " LEFT JOIN ^users AS cusers ON cposts.userid=cusers.userid"). + " LEFT JOIN ^userpoints AS cuserpoints ON cposts.userid=cuserpoints.userid". + " JOIN (SELECT postid FROM ^posts WHERE ". + qa_db_categoryslugs_sql_args($categoryslugs, $selectspec['arguments']). + (isset($createip) ? "createip=INET_ATON($) AND " : ""). + "type=$ ORDER BY ^posts.created DESC LIMIT #,#) y ON cposts.postid=y.postid". + ($specialtype ? '' : " WHERE ^posts.type='Q' AND ((parentposts.type='Q') OR (parentposts.type='A'))"); + + if (isset($createip)) + $selectspec['arguments'][]=$createip; + + array_push($selectspec['arguments'], $type, $start, $count); + + $selectspec['sortdesc']='otime'; + + return $selectspec; + } + + + function qa_db_recent_edit_qs_selectspec($voteuserid, $start, $categoryslugs=null, $lastip=null, $onlyvisible=true, $fulledited=false, $count=null) +/* + Return the selectspec to retrieve the antecedent questions for recently edited posts, restricted to edits by $lastip + (if not null), the category for $categoryslugs (if not null) and only visible posts (if $onlyvisible), with the + corresponding vote on those questions made by $voteuserid (if not null). Return $count (if null, a default is used) + questions starting from offset $start. The selectspec will also retrieve some information about the edited posts + themselves (including the content if $fulledited is true), in columns named with the prefix 'o'. +*/ + { + $count=isset($count) ? min($count, QA_DB_RETRIEVE_QS_AS) : QA_DB_RETRIEVE_QS_AS; + + $selectspec=qa_db_posts_basic_selectspec($voteuserid); + + qa_db_add_selectspec_opost($selectspec, 'editposts', true, $fulledited); + qa_db_add_selectspec_ousers($selectspec, 'editusers', 'edituserpoints'); + + $selectspec['source'].=" JOIN ^posts AS parentposts ON". + " ^posts.postid=IF(LEFT(parentposts.type, 1)='Q', parentposts.postid, parentposts.parentid)". + " JOIN ^posts AS editposts ON parentposts.postid=IF(LEFT(editposts.type, 1)='Q', editposts.postid, editposts.parentid)". + (QA_FINAL_EXTERNAL_USERS ? "" : " LEFT JOIN ^users AS editusers ON editposts.lastuserid=editusers.userid"). + " LEFT JOIN ^userpoints AS edituserpoints ON editposts.lastuserid=edituserpoints.userid". + " JOIN (SELECT postid FROM ^posts WHERE ". + qa_db_categoryslugs_sql_args($categoryslugs, $selectspec['arguments']). + (isset($lastip) ? "lastip=INET_ATON($) AND " : ""). + ($onlyvisible ? "type IN ('Q', 'A', 'C')" : "1"). + " ORDER BY ^posts.updated DESC LIMIT #,#) y ON editposts.postid=y.postid". + ($onlyvisible ? " WHERE parentposts.type IN ('Q', 'A', 'C') AND ^posts.type IN ('Q', 'A', 'C')" : ""); + + if (isset($lastip)) + $selectspec['arguments'][]=$lastip; + + array_push($selectspec['arguments'], $start, $count); + + $selectspec['sortdesc']='otime'; + + return $selectspec; + } + + + function qa_db_flagged_post_qs_selectspec($voteuserid, $start, $fullflagged=false, $count=null) +/* + Return the selectspec to retrieve the antecedent questions for the most flagged posts, with the corresponding vote + on those questions made by $voteuserid (if not null). Return $count (if null, a default is used) questions starting + from offset $start. The selectspec will also retrieve some information about the flagged posts themselves (including + the content if $fullflagged is true). +*/ + { + $count=isset($count) ? min($count, QA_DB_RETRIEVE_QS_AS) : QA_DB_RETRIEVE_QS_AS; + + $selectspec=qa_db_posts_basic_selectspec($voteuserid); + + qa_db_add_selectspec_opost($selectspec, 'flagposts', false, $fullflagged); + qa_db_add_selectspec_ousers($selectspec, 'flagusers', 'flaguserpoints'); + + $selectspec['source'].=" JOIN ^posts AS parentposts ON". + " ^posts.postid=IF(LEFT(parentposts.type, 1)='Q', parentposts.postid, parentposts.parentid)". + " JOIN ^posts AS flagposts ON parentposts.postid=IF(LEFT(flagposts.type, 1)='Q', flagposts.postid, flagposts.parentid)". + (QA_FINAL_EXTERNAL_USERS ? "" : " LEFT JOIN ^users AS flagusers ON flagposts.userid=flagusers.userid"). + " LEFT JOIN ^userpoints AS flaguserpoints ON flagposts.userid=flaguserpoints.userid". + " JOIN (SELECT postid FROM ^posts WHERE flagcount>0 AND type IN ('Q', 'A', 'C') ORDER BY ^posts.flagcount DESC, ^posts.created DESC LIMIT #,#) y ON flagposts.postid=y.postid"; + + array_push($selectspec['arguments'], $start, $count); + + $selectspec['sortdesc']='oflagcount'; + $selectspec['sortdesc_2']='otime'; + + return $selectspec; + } + + + function qa_db_posts_selectspec($voteuserid, $postids, $full=false) +/* + Return the selectspec to retrieve the posts in $postids, with the corresponding vote on those posts made by + $voteuserid (if not null). Returns full information if $full is true. +*/ + { + $selectspec=qa_db_posts_basic_selectspec($voteuserid, $full); + + $selectspec['source'].=" WHERE ^posts.postid IN (#)"; + $selectspec['arguments'][]=$postids; + + return $selectspec; + } + + + function qa_db_posts_basetype_selectspec($postids) +/* + Return the selectspec to retrieve the basetype for the posts in $postids, as an array mapping postid => basetype +*/ + { + return array( + 'columns' => array('postid', 'basetype' => 'LEFT(type, 1)'), + 'source' => "^posts WHERE postid IN (#)", + 'arguments' => array($postids), + 'arraykey' => 'postid', + 'arrayvalue' => 'basetype', + ); + } + + + function qa_db_posts_to_qs_selectspec($voteuserid, $postids, $full=false) +/* + Return the selectspec to retrieve the basetype for the posts in $postids, as an array mapping postid => basetype +*/ + { + $selectspec=qa_db_posts_basic_selectspec($voteuserid, $full); + + $selectspec['columns']['obasetype']='LEFT(childposts.type, 1)'; + $selectspec['columns']['opostid']='childposts.postid'; + + $selectspec['source'].=" JOIN ^posts AS parentposts ON". + " ^posts.postid=IF(LEFT(parentposts.type, 1)='Q', parentposts.postid, parentposts.parentid)". + " JOIN ^posts AS childposts ON parentposts.postid=IF(LEFT(childposts.type, 1)='Q', childposts.postid, childposts.parentid)". + " WHERE childposts.postid IN (#)"; + + $selectspec['arraykey']='opostid'; + $selectspec['arguments'][]=$postids; + + return $selectspec; + } + + + function qa_db_full_post_selectspec($voteuserid, $postid) +/* + Return the selectspec to retrieve the full information for $postid, with the corresponding vote made by $voteuserid (if not null) +*/ + { + $selectspec=qa_db_posts_basic_selectspec($voteuserid, true); + + $selectspec['source'].=" WHERE ^posts.postid=#"; + $selectspec['arguments'][]=$postid; + $selectspec['single']=true; + + return $selectspec; + } + + + function qa_db_full_child_posts_selectspec($voteuserid, $parentid) +/* + Return the selectspec to retrieve the full information for all posts whose parent is $parentid, with the + corresponding vote made by $voteuserid (if not null) +*/ + { + $selectspec=qa_db_posts_basic_selectspec($voteuserid, true); + + $selectspec['source'].=" WHERE ^posts.parentid=#"; + $selectspec['arguments'][]=$parentid; + + return $selectspec; + } + + + function qa_db_full_a_child_posts_selectspec($voteuserid, $questionid) +/* + Return the selectspec to retrieve the full information for all posts whose parent is an answer which + has $questionid as its parent, with the corresponding vote made by $voteuserid (if not null) +*/ + { + $selectspec=qa_db_posts_basic_selectspec($voteuserid, true); + + $selectspec['source'].=" JOIN ^posts AS parents ON ^posts.parentid=parents.postid WHERE parents.parentid=# AND LEFT(parents.type, 1)='A'" ; + $selectspec['arguments'][]=$questionid; + + return $selectspec; + } + + + function qa_db_post_parent_q_selectspec($postid) +/* + Return the selectspec to retrieve the question for the parent of $postid (where $postid is of a follow-on question or comment), + i.e. the parent of $questionid's parent if $questionid's parent is an answer, otherwise $questionid's parent itself. +*/ + { + $selectspec=qa_db_posts_basic_selectspec(); + + $selectspec['source'].=" WHERE ^posts.postid=(SELECT IF(LEFT(parent.type, 1)='A', parent.parentid, parent.postid) FROM ^posts AS child LEFT JOIN ^posts AS parent ON parent.postid=child.parentid WHERE child.postid=#)"; + $selectspec['arguments']=array($postid); + $selectspec['single']=true; + + return $selectspec; + } + + + function qa_db_post_close_post_selectspec($questionid) +/* + Return the selectspec to retrieve the post (either duplicate question or explanatory note) which has closed $questionid, if any +*/ + { + $selectspec=qa_db_posts_basic_selectspec(null, true); + + $selectspec['source'].=" WHERE ^posts.postid=(SELECT closedbyid FROM ^posts WHERE postid=#)"; + $selectspec['arguments']=array($questionid); + $selectspec['single']=true; + + return $selectspec; + } + + + function qa_db_post_meta_selectspec($postid, $title) +/* + Return the selectspec to retrieve the metadata value for $postid with key $title +*/ + { + $selectspec=array( + 'columns' => array('title', 'content'), + 'source' => "^postmetas WHERE postid=# AND ".(is_array($title) ? "title IN ($)" : "title=$"), + 'arguments' => array($postid, $title), + 'arrayvalue' => 'content', + ); + + if (is_array($title)) + $selectspec['arraykey']='title'; + else + $selectspec['single']=true; + + return $selectspec; + } + + + function qa_db_related_qs_selectspec($voteuserid, $questionid, $count=null) +/* + Return the selectspec to retrieve the most closely related questions to $questionid, with the corresponding vote + made by $voteuserid (if not null). Return $count (if null, a default is used) questions. This works by looking for + other questions which have title words, tag words or an (exact) category in common. +*/ + { + $count=isset($count) ? min($count, QA_DB_RETRIEVE_QS_AS) : QA_DB_RETRIEVE_QS_AS; + + $selectspec=qa_db_posts_basic_selectspec($voteuserid); + + $selectspec['columns'][]='score'; + + // added LOG(postid)/1000000 here to ensure ordering is deterministic even if several posts have same score + + $selectspec['source'].=" JOIN (SELECT postid, SUM(score)+LOG(postid)/1000000 AS score FROM ((SELECT ^titlewords.postid, LOG(#/titlecount) AS score FROM ^titlewords JOIN ^words ON ^titlewords.wordid=^words.wordid JOIN ^titlewords AS source ON ^titlewords.wordid=source.wordid WHERE source.postid=# AND titlecount<#) UNION ALL (SELECT ^posttags.postid, 2*LOG(#/tagcount) AS score FROM ^posttags JOIN ^words ON ^posttags.wordid=^words.wordid JOIN ^posttags AS source ON ^posttags.wordid=source.wordid WHERE source.postid=# AND tagcount<#) UNION ALL (SELECT ^posts.postid, LOG(#/^categories.qcount) FROM ^posts JOIN ^categories ON ^posts.categoryid=^categories.categoryid AND ^posts.type='Q' WHERE ^categories.categoryid=(SELECT categoryid FROM ^posts WHERE postid=#) AND ^categories.qcount<#)) x WHERE postid!=# GROUP BY postid ORDER BY score DESC LIMIT #) y ON ^posts.postid=y.postid"; + + array_push($selectspec['arguments'], QA_IGNORED_WORDS_FREQ, $questionid, QA_IGNORED_WORDS_FREQ, QA_IGNORED_WORDS_FREQ, + $questionid, QA_IGNORED_WORDS_FREQ, QA_IGNORED_WORDS_FREQ, $questionid, QA_IGNORED_WORDS_FREQ, $questionid, $count); + + $selectspec['sortdesc']='score'; + + return $selectspec; + } + + + function qa_db_search_posts_selectspec($voteuserid, $titlewords, $contentwords, $tagwords, $handlewords, $handle, $start, $full=false, $count=null) +/* + Return the selectspec to retrieve the top question matches for a search, with the corresponding vote made by + $voteuserid (if not null) and including $full content or not. Return $count (if null, a default is used) questions + starting from offset $start. The search is performed for any of $titlewords in the title, $contentwords in the + content (of the question or an answer or comment for whom that is the antecedent question), $tagwords in tags, for + question author usernames which match a word in $handlewords or which match $handle as a whole. The results also + include a 'score' column based on the matching strength and post hotness, and a 'matchparts' column that tells us + where the score came from (since a question could get weight from a match in the question itself, and/or weight from + a match in its answers, comments, or comments on answers). The 'matchparts' is a comma-separated list of tuples + matchtype:matchpostid:matchscore to be used with qa_search_set_max_match(). +*/ + { + $count=isset($count) ? min($count, QA_DB_RETRIEVE_QS_AS) : QA_DB_RETRIEVE_QS_AS; + + // add LOG(postid)/1000000 here to ensure ordering is deterministic even if several posts have same score + // The score also gives a bonus for hot questions, where the bonus scales linearly with hotness. The hottest + // question gets a bonus equivalent to a matching unique tag, and the least hot question gets zero bonus. + + $selectspec=qa_db_posts_basic_selectspec($voteuserid, $full); + + $selectspec['columns'][]='score'; + $selectspec['columns'][]='matchparts'; + $selectspec['source'].=" JOIN (SELECT questionid, SUM(score)+2*(LOG(#)*(^posts.hotness-(SELECT MIN(hotness) FROM ^posts WHERE type='Q'))/((SELECT MAX(hotness) FROM ^posts WHERE type='Q')-(SELECT MIN(hotness) FROM ^posts WHERE type='Q')))+LOG(questionid)/1000000 AS score, GROUP_CONCAT(CONCAT_WS(':', matchposttype, matchpostid, ROUND(score,3))) AS matchparts FROM ("; + $selectspec['sortdesc']='score'; + array_push($selectspec['arguments'], QA_IGNORED_WORDS_FREQ); + + $selectparts=0; + + if (!empty($titlewords)) { + // At the indexing stage, duplicate words in title are ignored, so this doesn't count multiple appearances. + + $selectspec['source'].=($selectparts++ ? " UNION ALL " : ""). + "(SELECT postid AS questionid, LOG(#/titlecount) AS score, 'Q' AS matchposttype, postid AS matchpostid FROM ^titlewords JOIN ^words ON ^titlewords.wordid=^words.wordid WHERE word IN ($) AND titlecount<#)"; + + array_push($selectspec['arguments'], QA_IGNORED_WORDS_FREQ, $titlewords, QA_IGNORED_WORDS_FREQ); + } + + if (!empty($contentwords)) { + // (1-1/(1+count)) weights words in content based on their frequency: If a word appears once in content + // it's equivalent to 1/2 an appearance in the title (ignoring the contentcount/titlecount factor). + // If it appears an infinite number of times, it's equivalent to one appearance in the title. + // This will discourage keyword stuffing while still giving some weight to multiple appearances. + // On top of that, answer matches are worth half a question match, and comment/note matches half again. + + $selectspec['source'].=($selectparts++ ? " UNION ALL " : ""). + "(SELECT questionid, (1-1/(1+count))*LOG(#/contentcount)*(CASE ^contentwords.type WHEN 'Q' THEN 1.0 WHEN 'A' THEN 0.5 ELSE 0.25 END) AS score, ^contentwords.type AS matchposttype, ^contentwords.postid AS matchpostid FROM ^contentwords JOIN ^words ON ^contentwords.wordid=^words.wordid WHERE word IN ($) AND contentcount<#)"; + + array_push($selectspec['arguments'], QA_IGNORED_WORDS_FREQ, $contentwords, QA_IGNORED_WORDS_FREQ); + } + + if (!empty($tagwords)) { + // Appearances in the tag words count like 2 appearances in the title (ignoring the tagcount/titlecount factor). + // This is because tags express explicit semantic intent, whereas titles do not necessarily. + + $selectspec['source'].=($selectparts++ ? " UNION ALL " : ""). + "(SELECT postid AS questionid, 2*LOG(#/tagwordcount) AS score, 'Q' AS matchposttype, postid AS matchpostid FROM ^tagwords JOIN ^words ON ^tagwords.wordid=^words.wordid WHERE word IN ($) AND tagwordcount<#)"; + + array_push($selectspec['arguments'], QA_IGNORED_WORDS_FREQ, $tagwords, QA_IGNORED_WORDS_FREQ); + } + + if (!empty($handlewords)) { + if (QA_FINAL_EXTERNAL_USERS) { + require_once QA_INCLUDE_DIR.'qa-app-users.php'; + + $userids=qa_get_userids_from_public($handlewords); + + if (count($userids)) { + $selectspec['source'].=($selectparts++ ? " UNION ALL " : ""). + "(SELECT postid AS questionid, LOG(#/qposts) AS score, 'Q' AS matchposttype, postid AS matchpostid FROM ^posts JOIN ^userpoints ON ^posts.userid=^userpoints.userid WHERE ^posts.userid IN ($) AND type='Q')"; + + array_push($selectspec['arguments'], QA_IGNORED_WORDS_FREQ, $userids); + } + + } else { + $selectspec['source'].=($selectparts++ ? " UNION ALL " : ""). + "(SELECT postid AS questionid, LOG(#/qposts) AS score, 'Q' AS matchposttype, postid AS matchpostid FROM ^posts JOIN ^users ON ^posts.userid=^users.userid JOIN ^userpoints ON ^userpoints.userid=^users.userid WHERE handle IN ($) AND type='Q')"; + + array_push($selectspec['arguments'], QA_IGNORED_WORDS_FREQ, $handlewords); + } + } + + if (strlen($handle)) { // to allow searching for multi-word usernames (only works if search query contains full username and nothing else) + if (QA_FINAL_EXTERNAL_USERS) { + $userids=qa_get_userids_from_public(array($handle)); + $userid=@$userids[$handle]; + + if (strlen($userid)) { + $selectspec['source'].=($selectparts++ ? " UNION ALL " : ""). + "(SELECT postid AS questionid, LOG(#/qposts) AS score, 'Q' AS matchposttype, postid AS matchpostid FROM ^posts JOIN ^userpoints ON ^posts.userid=^userpoints.userid WHERE ^posts.userid=$ AND type='Q')"; + + array_push($selectspec['arguments'], QA_IGNORED_WORDS_FREQ, $userid); + } + + } else { + $selectspec['source'].=($selectparts++ ? " UNION ALL " : ""). + "(SELECT postid AS questionid, LOG(#/qposts) AS score, 'Q' AS matchposttype, postid AS matchpostid FROM ^posts JOIN ^users ON ^posts.userid=^users.userid JOIN ^userpoints ON ^userpoints.userid=^users.userid WHERE handle=$ AND type='Q')"; + + array_push($selectspec['arguments'], QA_IGNORED_WORDS_FREQ, $handle); + } + } + + if ($selectparts==0) + $selectspec['source'].='(SELECT NULL as questionid, 0 AS score, NULL AS matchposttype, NULL AS matchpostid FROM ^posts WHERE postid=NULL)'; + + $selectspec['source'].=") x LEFT JOIN ^posts ON ^posts.postid=questionid GROUP BY questionid ORDER BY score DESC LIMIT #,#) y ON ^posts.postid=y.questionid"; + + array_push($selectspec['arguments'], $start, $count); + + return $selectspec; + } + + + function qa_search_set_max_match($question, &$type, &$postid) +/* + Processes the matchparts column in $question which was returned from a search performed via qa_db_search_posts_selectspec() + Returns the id of the strongest matching answer or comment, or null if the question itself was the strongest match +*/ + { + $type='Q'; + $postid=$question['postid']; + $bestscore=null; + + $matchparts=explode(',', $question['matchparts']); + foreach ($matchparts as $matchpart) + if (sscanf($matchpart, '%1s:%f:%f', $matchposttype, $matchpostid, $matchscore)==3) + if ( (!isset($bestscore)) || ($matchscore>$bestscore) ) { + $bestscore=$matchscore; + $type=$matchposttype; + $postid=$matchpostid; + } + + return null; + } + + + function qa_db_full_category_selectspec($slugsorid, $isid) +/* + Return a selectspec to retrieve the full information on the category whose id is $slugsorid (if $isid is true), + otherwise whose backpath matches $slugsorid +*/ + { + if ($isid) + $identifiersql='categoryid=#'; + else { + $identifiersql='backpath=$'; + $slugsorid=qa_db_slugs_to_backpath($slugsorid); + } + + return array( + 'columns' => array('categoryid', 'parentid', 'title', 'tags', 'qcount', 'content', 'backpath'), + 'source' => '^categories WHERE '.$identifiersql, + 'arguments' => array($slugsorid), + 'single' => 'true', + ); + } + + + function qa_db_category_nav_selectspec($slugsorid, $isid, $ispostid=false, $full=false) +/* + Return the selectspec to retrieve ($full or not) info on the categories which "surround" the central category specified + by $slugsorid, $isid and $ispostid. The "surrounding" categories include all categories (even unrelated) at the + top level, any ancestors (at any level) of the category, the category's siblings and sub-categories (to one level). + The central category is specified as follows. If $isid AND $ispostid then $slugsorid is the ID of a post with the category. + Otherwise if $isid then $slugsorid is the category's own id. Otherwise $slugsorid is the full backpath of the category. +*/ + { + if ($isid) { + if ($ispostid) + $identifiersql='categoryid=(SELECT categoryid FROM ^posts WHERE postid=#)'; + else + $identifiersql='categoryid=#'; + + } else { + $identifiersql='backpath=$'; + $slugsorid=qa_db_slugs_to_backpath($slugsorid); + } + + $parentselects=array( // requires QA_CATEGORY_DEPTH=4 + 'SELECT NULL AS parentkey', // top level + 'SELECT grandparent.parentid FROM ^categories JOIN ^categories AS parent ON ^categories.parentid=parent.categoryid JOIN ^categories AS grandparent ON parent.parentid=grandparent.categoryid WHERE ^categories.'.$identifiersql, // 2 gens up + 'SELECT parent.parentid FROM ^categories JOIN ^categories AS parent ON ^categories.parentid=parent.categoryid WHERE ^categories.'.$identifiersql, + // 1 gen up + 'SELECT parentid FROM ^categories WHERE '.$identifiersql, // same gen + 'SELECT categoryid FROM ^categories WHERE '.$identifiersql, // gen below + ); + + $selectspec=array( + 'columns' => array('^categories.categoryid', '^categories.parentid', 'title' => '^categories.title', 'tags' => '^categories.tags', '^categories.qcount', '^categories.position'), + 'source' => '^categories JOIN ('.implode(' UNION ', $parentselects).') y ON ^categories.parentid<=>parentkey'.($full ? ' LEFT JOIN ^categories AS child ON child.parentid=^categories.categoryid GROUP BY ^categories.categoryid' : '').' ORDER BY ^categories.position', + 'arguments' => array($slugsorid, $slugsorid, $slugsorid, $slugsorid), + 'arraykey' => 'categoryid', + 'sortasc' => 'position', + ); + + if ($full) { + $selectspec['columns']['childcount']='COUNT(child.categoryid)'; + $selectspec['columns']['content']='^categories.content'; + $selectspec['columns']['backpath']='^categories.backpath'; + } + + return $selectspec; + } + + + function qa_db_category_sub_selectspec($categoryid) +/* + Return the selectspec to retrieve information on all subcategories of $categoryid (used for Ajax navigation of hierarchy) +*/ + { + return array( + 'columns' => array('categoryid', 'title', 'tags', 'qcount', 'position'), + 'source' => '^categories WHERE parentid<=># ORDER BY position', + 'arguments' => array($categoryid), + 'arraykey' => 'categoryid', + 'sortasc' => 'position', + ); + } + + + function qa_db_slugs_to_category_id_selectspec($slugs) +/* + Return the selectspec to retrieve a single category as specified by its $slugs (in order of hierarchy) +*/ + { + return array( + 'columns' => array('categoryid'), + 'source' => '^categories WHERE backpath=$', + 'arguments' => array(qa_db_slugs_to_backpath($slugs)), + 'arrayvalue' => 'categoryid', + 'single' => true, + ); + } + + + function qa_db_pages_selectspec($onlynavin=null, $onlypageids=null) +/* + Return the selectspec to retrieve the list of custom pages or links, ordered for display +*/ + { + $selectspec=array( + 'columns' => array('pageid', 'title', 'flags', 'permit', 'nav', 'tags', 'position', 'heading'), + 'arraykey' => 'pageid', + 'sortasc' => 'position', + ); + + if (isset($onlypageids)) { + $selectspec['source']='^pages WHERE pageid IN (#)'; + $selectspec['arguments']=array($onlypageids); + + } elseif (isset($onlynavin)) { + $selectspec['source']='^pages WHERE nav IN ($) ORDER BY position'; + $selectspec['arguments']=array($onlynavin); + + } else + $selectspec['source']='^pages ORDER BY position'; + + return $selectspec; + } + + + function qa_db_widgets_selectspec() +/* + Return the selectspec to retrieve the list of widgets, ordered for display +*/ + { + return array( + 'columns' => array('widgetid', 'place', 'position', 'tags', 'title'), + 'source' => '^widgets ORDER BY position', + 'sortasc' => 'position', + ); + } + + + function qa_db_page_full_selectspec($slugorpageid, $ispageid) +/* + Return the selectspec to retrieve the full information about a custom page +*/ + { + return array( + 'columns' => array('pageid', 'title', 'flags', 'permit', 'nav', 'tags', 'position', 'heading', 'content'), + 'source' => '^pages WHERE '.($ispageid ? 'pageid' : 'tags').'=$', + 'arguments' => array($slugorpageid), + 'single' => true, + ); + } + + + function qa_db_tag_recent_qs_selectspec($voteuserid, $tag, $start, $full=false, $count=null) +/* + Return the selectspec to retrieve the most recent questions with $tag, with the corresponding vote on those + questions made by $voteuserid (if not null) and including $full content or not. Return $count (if null, a default is + used) questions starting from $start. +*/ + { + $count=isset($count) ? min($count, QA_DB_RETRIEVE_QS_AS) : QA_DB_RETRIEVE_QS_AS; + + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + $selectspec=qa_db_posts_basic_selectspec($voteuserid, $full); + + // use two tests here - one which can use the index, and the other which narrows it down exactly - then limit to 1 just in case + $selectspec['source'].=" JOIN (SELECT postid FROM ^posttags WHERE wordid=(SELECT wordid FROM ^words WHERE word=$ AND word=$ COLLATE utf8_bin LIMIT 1) ORDER BY postcreated DESC LIMIT #,#) y ON ^posts.postid=y.postid"; + array_push($selectspec['arguments'], $tag, qa_strtolower($tag), $start, $count); + $selectspec['sortdesc']='created'; + + return $selectspec; + } + + + function qa_db_tag_word_selectspec($tag) +/* + Return the selectspec to retrieve the number of questions tagged with $tag (single value) +*/ + { + return array( + 'columns' => array('wordid', 'word', 'tagcount'), + 'source' => '^words WHERE word=$', + 'arguments' => array($tag), + 'single' => true, + ); + } + + + function qa_db_user_recent_qs_selectspec($voteuserid, $identifier, $count=null) +/* + Return the selectspec to retrieve recent questions by the user identified by $identifier, where $identifier is a + handle if we're using internal user management, or a userid if we're using external users. Also include the + corresponding vote on those questions made by $voteuserid (if not null). Return $count (if null, a default is used) + questions. +*/ + { + $count=isset($count) ? min($count, QA_DB_RETRIEVE_QS_AS) : QA_DB_RETRIEVE_QS_AS; + + $selectspec=qa_db_posts_basic_selectspec($voteuserid); + + $selectspec['source'].=" WHERE ^posts.userid=".(QA_FINAL_EXTERNAL_USERS ? "$" : "(SELECT userid FROM ^users WHERE handle=$ LIMIT 1)")." AND type='Q' ORDER BY ^posts.created DESC LIMIT #"; + array_push($selectspec['arguments'], $identifier, $count); + $selectspec['sortdesc']='created'; + + return $selectspec; + } + + + function qa_db_user_recent_a_qs_selectspec($voteuserid, $identifier, $count=null) +/* + Return the selectspec to retrieve the antecedent questions for recent answers by the user identified by $identifier + (see qa_db_user_recent_qs_selectspec() comment), with the corresponding vote on those questions made by $voteuserid + (if not null). Return $count (if null, a default is used) questions. The selectspec will also retrieve some + information about the answers themselves, in columns named with the prefix 'o'. +*/ + { + $count=isset($count) ? min($count, QA_DB_RETRIEVE_QS_AS) : QA_DB_RETRIEVE_QS_AS; + + $selectspec=qa_db_posts_basic_selectspec($voteuserid); + + qa_db_add_selectspec_opost($selectspec, 'aposts'); + + $selectspec['source'].=" JOIN ^posts AS aposts ON ^posts.postid=aposts.parentid". + " JOIN (SELECT postid FROM ^posts WHERE ". + " userid=".(QA_FINAL_EXTERNAL_USERS ? "$" : "(SELECT userid FROM ^users WHERE handle=$ LIMIT 1)"). + " AND type='A' ORDER BY created DESC LIMIT #) y ON aposts.postid=y.postid WHERE ^posts.type='Q'"; + + array_push($selectspec['arguments'], $identifier, $count); + $selectspec['sortdesc']='otime'; + + return $selectspec; + } + + + function qa_db_user_recent_c_qs_selectspec($voteuserid, $identifier, $count=null) +/* + Return the selectspec to retrieve the antecedent questions for recent comments by the user identified by $identifier + (see qa_db_user_recent_qs_selectspec() comment), with the corresponding vote on those questions made by $voteuserid + (if not null). Return $count (if null, a default is used) questions. The selectspec will also retrieve some + information about the comments themselves, in columns named with the prefix 'o'. +*/ + { + $count=isset($count) ? min($count, QA_DB_RETRIEVE_QS_AS) : QA_DB_RETRIEVE_QS_AS; + + $selectspec=qa_db_posts_basic_selectspec($voteuserid); + + qa_db_add_selectspec_opost($selectspec, 'cposts'); + + $selectspec['source'].=" JOIN ^posts AS parentposts ON". + " ^posts.postid=(CASE parentposts.type WHEN 'A' THEN parentposts.parentid ELSE parentposts.postid END)". + " JOIN ^posts AS cposts ON parentposts.postid=cposts.parentid". + " JOIN (SELECT postid FROM ^posts WHERE ". + " userid=".(QA_FINAL_EXTERNAL_USERS ? "$" : "(SELECT userid FROM ^users WHERE handle=$ LIMIT 1)"). + " AND type='C' ORDER BY created DESC LIMIT #) y ON cposts.postid=y.postid WHERE ^posts.type='Q' AND parentposts.type IN ('Q', 'A')"; + + array_push($selectspec['arguments'], $identifier, $count); + $selectspec['sortdesc']='otime'; + + return $selectspec; + } + + + function qa_db_user_recent_edit_qs_selectspec($voteuserid, $identifier, $count=null) +/* + Return the selectspec to retrieve the antecedent questions for recently edited posts by the user identified by + $identifier (see qa_db_user_recent_qs_selectspec() comment), with the corresponding vote on those questions made by + $voteuserid (if not null). Return $count (if null, a default is used) questions. The selectspec will also retrieve + some information about the edited posts themselves, in columns named with the prefix 'o'. +*/ + { + $count=isset($count) ? min($count, QA_DB_RETRIEVE_QS_AS) : QA_DB_RETRIEVE_QS_AS; + + $selectspec=qa_db_posts_basic_selectspec($voteuserid); + + qa_db_add_selectspec_opost($selectspec, 'editposts', true); + + $selectspec['source'].=" JOIN ^posts AS parentposts ON". + " ^posts.postid=IF(LEFT(parentposts.type, 1)='Q', parentposts.postid, parentposts.parentid)". + " JOIN ^posts AS editposts ON parentposts.postid=IF(LEFT(editposts.type, 1)='Q', editposts.postid, editposts.parentid)". + " JOIN (SELECT postid FROM ^posts WHERE ". + " lastuserid=".(QA_FINAL_EXTERNAL_USERS ? "$" : "(SELECT userid FROM ^users WHERE handle=$ LIMIT 1)"). + " AND type IN ('Q', 'A', 'C') ORDER BY updated DESC LIMIT #) y ON editposts.postid=y.postid ". + " WHERE parentposts.type IN ('Q', 'A', 'C') AND ^posts.type IN ('Q', 'A', 'C')"; + + array_push($selectspec['arguments'], $identifier, $count); + $selectspec['sortdesc']='otime'; + + return $selectspec; + } + + + function qa_db_popular_tags_selectspec($start, $count=null) +/* + Return the selectspec to retrieve the most popular tags. Return $count (if null, a default is used) tags, starting + from offset $start. The selectspec will produce a sorted array with tags in the key, and counts in the values. +*/ + { + $count=isset($count) ? min($count, QA_DB_RETRIEVE_TAGS) : QA_DB_RETRIEVE_TAGS; + + return array( + 'columns' => array('word', 'tagcount'), + 'source' => '^words JOIN (SELECT wordid FROM ^words WHERE tagcount>0 ORDER BY tagcount DESC LIMIT #,#) y ON ^words.wordid=y.wordid', + 'arguments' => array($start, $count), + 'arraykey' => 'word', + 'arrayvalue' => 'tagcount', + 'sortdesc' => 'tagcount', + ); + } + + + function qa_db_userfields_selectspec() +/* + Return the selectspec to retrieve the list of user profile fields, ordered for display +*/ + { + return array( + 'columns' => array('fieldid', 'title', 'content', 'flags', 'position'), + 'source' => '^userfields', + 'arraykey' => 'title', + 'sortasc' => 'position', + ); + } + + + function qa_db_user_account_selectspec($useridhandle, $isuserid) +/* + Return the selecspec to retrieve a single array with details of the account of the user identified by + $useridhandle, which should be a userid if $isuserid is true, otherwise $useridhandle should be a handle. +*/ + { + return array( + 'columns' => array( + '^users.userid', 'passsalt', 'passcheck' => 'HEX(passcheck)', 'email', 'level', 'emailcode', 'handle', + 'created' => 'UNIX_TIMESTAMP(created)', 'sessioncode', 'sessionsource', 'flags', 'loggedin' => 'UNIX_TIMESTAMP(loggedin)', + 'loginip' => 'INET_NTOA(loginip)', 'written' => 'UNIX_TIMESTAMP(written)', 'writeip' => 'INET_NTOA(writeip)', + 'avatarblobid', 'avatarwidth', 'avatarheight', 'points', + ), + + 'source' => '^users LEFT JOIN ^userpoints ON ^userpoints.userid=^users.userid WHERE ^users.'.($isuserid ? 'userid' : 'handle').'=$', + 'arguments' => array($useridhandle), + 'single' => true, + ); + } + + + function qa_db_user_profile_selectspec($useridhandle, $isuserid) +/* + Return the selectspec to retrieve all user profile information of the user identified by + $useridhandle (see qa_db_user_account_selectspec() comment), as an array of [field] => [value] +*/ + { + return array( + 'columns' => array('title', 'content'), + 'source' => '^userprofile WHERE userid='.($isuserid ? '$' : '(SELECT userid FROM ^users WHERE handle=$ LIMIT 1)'), + 'arguments' => array($useridhandle), + 'arraykey' => 'title', + 'arrayvalue' => 'content', + ); + } + + + function qa_db_user_notices_selectspec($userid) +/* + Return the selectspec to retrieve all notices for the user $userid +*/ + { + return array( + 'columns' => array('noticeid', 'content', 'format', 'tags', 'created' => 'UNIX_TIMESTAMP(created)'), + 'source' => '^usernotices WHERE userid=$ ORDER BY created', + 'arguments' => array($userid), + 'sortasc' => 'created', + ); + } + + + function qa_db_user_points_selectspec($identifier, $isuserid=QA_FINAL_EXTERNAL_USERS) +/* + Return the selectspec to retrieve all columns from the userpoints table for the user identified by $identifier + (see qa_db_user_recent_qs_selectspec() comment), as a single array +*/ + { + return array( + 'columns' => array('points', 'qposts', 'aposts', 'cposts', 'aselects', 'aselecteds', 'qupvotes', 'qdownvotes', 'aupvotes', 'adownvotes', 'qvoteds', 'avoteds', 'upvoteds', 'downvoteds', 'bonus'), + 'source' => '^userpoints WHERE userid='.($isuserid ? '$' : '(SELECT userid FROM ^users WHERE handle=$ LIMIT 1)'), + 'arguments' => array($identifier), + 'single' => true, + ); + } + + + function qa_db_user_rank_selectspec($identifier, $isuserid=QA_FINAL_EXTERNAL_USERS) +/* + Return the selectspec to calculate the rank in points of the user identified by $identifier + (see qa_db_user_recent_qs_selectspec() comment), as a single value +*/ + { + return array( + 'columns' => array('rank' => '1+COUNT(*)'), + 'source' => '^userpoints WHERE points>COALESCE((SELECT points FROM ^userpoints WHERE userid='.($isuserid ? '$' : '(SELECT userid FROM ^users WHERE handle=$ LIMIT 1)').'), 0)', + 'arguments' => array($identifier), + 'arrayvalue' => 'rank', + 'single' => true, + ); + } + + + function qa_db_top_users_selectspec($start, $count=null) +/* + Return the selectspec to get the top scoring users, with handles if we're using internal user management. Return + $count (if null, a default is used) users starting from the offset $start. +*/ + { + $count=isset($count) ? min($count, QA_DB_RETRIEVE_USERS) : QA_DB_RETRIEVE_USERS; + + if (QA_FINAL_EXTERNAL_USERS) + return array( + 'columns' => array('userid', 'points'), + 'source' => '^userpoints ORDER BY points DESC LIMIT #,#', + 'arguments' => array($start, $count), + 'arraykey' => 'userid', + 'sortdesc' => 'points', + ); + + else + return array( + 'columns' => array('^users.userid', 'handle', 'points', 'flags', '^users.email', 'avatarblobid', 'avatarwidth', 'avatarheight'), + 'source' => '^users JOIN (SELECT userid FROM ^userpoints ORDER BY points DESC LIMIT #,#) y ON ^users.userid=y.userid JOIN ^userpoints ON ^users.userid=^userpoints.userid', + 'arguments' => array($start, $count), + 'arraykey' => 'userid', + 'sortdesc' => 'points', + ); + } + + + function qa_db_users_from_level_selectspec($level) +/* + Return the selectspec to get information about users at a certain privilege level or higher +*/ + { + return array( + 'columns' => array('^users.userid', 'handle', 'level'), + 'source' => '^users WHERE level>=# ORDER BY level DESC', + 'arguments' => array($level), + 'sortdesc' => 'level', + ); + } + + + function qa_db_users_with_flag_selectspec($flag) +/* + Return the selectspec to get information about users with the $flag bit set (unindexed query) +*/ + { + return array( + 'columns' => array('^users.userid', 'handle', 'flags', 'level'), + 'source' => '^users WHERE (flags & #)', + 'arguments' => array($flag), + ); + } + + + function qa_db_recent_messages_selectspec($fromidentifier, $fromisuserid, $toidentifier, $toisuserid, $count=null) +/* + Return the selectspec to get recent private messages which have been sent from the user identified by + $fromidentifier+$fromisuserid to the user identified by $toidentifier+$toisuserid (see + qa_db_user_recent_qs_selectspec() comment). Return $count (if null, a default is used) messages. +*/ + { + $count=isset($count) ? min($count, QA_DB_RETRIEVE_MESSAGES) : QA_DB_RETRIEVE_MESSAGES; + + return array( + 'columns' => array('messageid', 'fromuserid', 'touserid', 'content', 'format', 'created' => 'UNIX_TIMESTAMP(created)'), + 'source' => '^messages WHERE fromuserid='.($fromisuserid ? "$" : "(SELECT userid FROM ^users WHERE handle=$ LIMIT 1)").' AND touserid='.($toisuserid ? "$" : "(SELECT userid FROM ^users WHERE handle=$ LIMIT 1)").' ORDER BY created DESC LIMIT #', + 'arguments' => array($fromidentifier, $toidentifier, $count), + 'arraykey' => 'messageid', + 'sortdesc' => 'created', + ); + } + + + function qa_db_is_favorite_selectspec($userid, $entitytype, $identifier) +/* + Return the selectspec to retrieve whether or not $userid has favorited entity $entitytype identifier by $identifier. + The $identifier should be a handle, word, backpath or postid for users, tags, categories and questions respectively. +*/ + { + require_once QA_INCLUDE_DIR.'qa-app-updates.php'; + + $selectspec=array( + 'columns' => array('flags' => 'COUNT(*)'), + 'source' => '^userfavorites WHERE userid=$ AND entitytype=$', + 'arrayvalue' => 'flags', + 'single' => true, + ); + + switch ($entitytype) { + case QA_ENTITY_USER: + $selectspec['source'].=' AND entityid=(SELECT userid FROM ^users WHERE handle=$ LIMIT 1)'; + break; + + case QA_ENTITY_TAG: + $selectspec['source'].=' AND entityid=(SELECT wordid FROM ^words WHERE word=$ LIMIT 1)'; + break; + + case QA_ENTITY_CATEGORY: + $selectspec['source'].=' AND entityid=(SELECT categoryid FROM ^categories WHERE backpath=$ LIMIT 1)'; + $identifier=qa_db_slugs_to_backpath($identifier); + break; + + default: + $selectspec['source'].=' AND entityid=$'; + break; + } + + $selectspec['arguments']=array($userid, $entitytype, $identifier); + + return $selectspec; + } + + + function qa_db_user_favorite_qs_selectspec($userid) +/* + Return the selectspec to retrieve an array of $userid's favorited questions, with the usual information. +*/ + { + require_once QA_INCLUDE_DIR.'qa-app-updates.php'; + + $selectspec=qa_db_posts_basic_selectspec($userid); + + $selectspec['source'].=" JOIN ^userfavorites ON ^posts.postid=^userfavorites.entityid WHERE ^userfavorites.userid=$ AND ^userfavorites.entitytype=$ AND ^posts.type='Q'"; + array_push($selectspec['arguments'], $userid, QA_ENTITY_QUESTION); + $selectspec['sortdesc']='created'; + + return $selectspec; + } + + + function qa_db_user_favorite_users_selectspec($userid) +/* + Return the selectspec to retrieve an array of $userid's favorited users, with information about those users' accounts. +*/ + { + require_once QA_INCLUDE_DIR.'qa-app-updates.php'; + + return array( + 'columns' => array('^users.userid', 'handle', 'points', 'flags', '^users.email', 'avatarblobid', 'avatarwidth', 'avatarheight'), + 'source' => "^users JOIN ^userpoints ON ^users.userid=^userpoints.userid JOIN ^userfavorites ON ^users.userid=^userfavorites.entityid WHERE ^userfavorites.userid=$ AND ^userfavorites.entitytype=$", + 'arguments' => array($userid, QA_ENTITY_USER), + 'sortasc' => 'handle', + ); + } + + + function qa_db_user_favorite_tags_selectspec($userid) +/* + Return the selectspec to retrieve an array of $userid's favorited tags, with information about those tags. +*/ + { + require_once QA_INCLUDE_DIR.'qa-app-updates.php'; + + return array( + 'columns' => array('word', 'tagcount'), + 'source' => "^words JOIN ^userfavorites ON ^words.wordid=^userfavorites.entityid WHERE ^userfavorites.userid=$ AND ^userfavorites.entitytype=$", + 'arguments' => array($userid, QA_ENTITY_TAG), + 'sortdesc' => 'tagcount', + ); + } + + + function qa_db_user_favorite_categories_selectspec($userid) +/* + Return the selectspec to retrieve an array of $userid's favorited categories, with information about those categories. +*/ + { + require_once QA_INCLUDE_DIR.'qa-app-updates.php'; + + return array( + 'columns' => array('categoryid', 'title', 'tags', 'qcount', 'backpath', 'content'), + 'source' => "^categories JOIN ^userfavorites ON ^categories.categoryid=^userfavorites.entityid WHERE ^userfavorites.userid=$ AND ^userfavorites.entitytype=$", + 'arguments' => array($userid, QA_ENTITY_CATEGORY), + 'sortasc' => 'title', + ); + } + + + function qa_db_user_updates_selectspec($userid, $forfavorites=true, $forcontent=true) +/* + Return the selectspec to retrieve the list of recent updates for $userid. Set $forfavorites to whether this should + include updates on the user's favorites and $forcontent to whether it should include responses to user's content. + This combines events from both the user's stream and the the shared stream for any entities which the user has + favorited and which no longer post to user streams (see long comment in qa-db-favorites.php). +*/ + { + require_once QA_INCLUDE_DIR.'qa-app-updates.php'; + + $selectspec=qa_db_posts_basic_selectspec($userid); + + $selectspec['columns']['obasetype']='LEFT(updateposts.type, 1)'; + $selectspec['columns']['oupdatetype']='fullevents.updatetype'; + $selectspec['columns']['ohidden']="INSTR(updateposts.type, '_HIDDEN')>0"; + $selectspec['columns']['opostid']='fullevents.lastpostid'; + $selectspec['columns']['ouserid']='fullevents.lastuserid'; + $selectspec['columns']['otime']='UNIX_TIMESTAMP(fullevents.updated)'; + + qa_db_add_selectspec_ousers($selectspec, 'eventusers', 'eventuserpoints'); + + if ($forfavorites) { // life is hard + $entitytypesql=$forcontent ? '' : " AND entitytype!=".qa_db_argument_to_mysql(QA_ENTITY_NONE, true); + + $selectspec['source'].=' JOIN '. + "(SELECT questionid, lastpostid, updatetype, lastuserid, updated FROM ^userevents WHERE userid=$".$entitytypesql. + " UNION SELECT questionid, lastpostid, updatetype, lastuserid, updated FROM ^sharedevents JOIN ^userfavorites ON ^sharedevents.entitytype=^userfavorites.entitytype AND ^sharedevents.entityid=^userfavorites.entityid AND ^userfavorites.nouserevents=1 WHERE userid=$) fullevents ON ^posts.postid=fullevents.questionid"; + + array_push($selectspec['arguments'], $userid, $userid); + + } else { // life is easy + $selectspec['source'].=" JOIN ^userevents AS fullevents ON ^posts.postid=fullevents.questionid AND fullevents.userid=$ AND fullevents.entitytype=".qa_db_argument_to_mysql(QA_ENTITY_NONE, true); + $selectspec['arguments'][]=$userid; + } + + $selectspec['source'].= + " JOIN ^posts AS updateposts ON updateposts.postid=fullevents.lastpostid AND updateposts.type IN ('Q', 'A', 'C')". + " AND (^posts.selchildid=fullevents.lastpostid OR NOT fullevents.updatetype<=>$) AND ^posts.type IN ('Q', 'A', 'C')". + (QA_FINAL_EXTERNAL_USERS ? '' : ' LEFT JOIN ^users AS eventusers ON fullevents.lastuserid=eventusers.userid'). + ' LEFT JOIN ^userpoints AS eventuserpoints ON fullevents.lastuserid=eventuserpoints.userid'; + $selectspec['arguments'][]=QA_UPDATE_SELECTED; + + unset($selectspec['arraykey']); // allow same question to be retrieved multiple times + + $selectspec['sortdesc']='otime'; + + return $selectspec; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-db-users.php b/qa-include/qa-db-users.php new file mode 100644 index 000000000..140e3f94f --- /dev/null +++ b/qa-include/qa-db-users.php @@ -0,0 +1,273 @@ +# ORDER BY userid LIMIT #', + $lastuserid, $count + )); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-db-votes.php b/qa-include/qa-db-votes.php new file mode 100644 index 000000000..050908e18 --- /dev/null +++ b/qa-include/qa-db-votes.php @@ -0,0 +1,137 @@ + [vote] +*/ + { + return qa_db_read_all_assoc(qa_db_query_sub( + 'SELECT userid, vote FROM ^uservotes WHERE postid=# AND vote!=0', + $postid + ), 'userid', 'vote'); + } + + + function qa_db_uservoteflag_user_get($userid) +/* + Returns all the postids from the database for posts that $userid has voted on or flagged +*/ + { + return qa_db_read_all_values(qa_db_query_sub( + 'SELECT postid FROM ^uservotes WHERE userid=# AND (vote!=0) OR (flag!=0)', + $userid + )); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-db.php b/qa-include/qa-db.php new file mode 100644 index 000000000..ff28af85d --- /dev/null +++ b/qa-include/qa-db.php @@ -0,0 +1,737 @@ +Database '.htmlspecialchars($type.' error '.$errno).'

'.nl2br(htmlspecialchars($error."\n\n".$query)); + qa_exit('error'); + } + } + + + function qa_db_connection($connect=true) +/* + Return the current connection to the Q2A database, connecting if necessary and $connect is true +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_db_connection; + + if ($connect && !is_resource($qa_db_connection)) { + qa_db_connect(); + + if (!is_resource($qa_db_connection)) + qa_fatal_error('Failed to connect to database'); + } + + return $qa_db_connection; + } + + + function qa_db_disconnect() +/* + Disconnect from the Q2A database +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_db_connection; + + if (is_resource($qa_db_connection)) { + qa_report_process_stage('db_disconnect'); + + if (!QA_PERSISTENT_CONN_DB) + if (!mysql_close($qa_db_connection)) + qa_fatal_error('Database disconnect failed'); + + $qa_db_connection=null; + } + } + + + function qa_db_query_raw($query) +/* + Run the raw $query, call the global failure handler if necessary, otherwise return the result resource. + If appropriate, also track the resources used by database queries, and the queries themselves, for performance debugging. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + if (QA_DEBUG_PERFORMANCE) { + global $qa_database_usage, $qa_database_queries; + + $oldtime=array_sum(explode(' ', microtime())); + $result=qa_db_query_execute($query); + $usedtime=array_sum(explode(' ', microtime()))-$oldtime; + + if (is_array($qa_database_usage)) { + $qa_database_usage['clock']+=$usedtime; + + if (strlen($qa_database_queries)<1048576) { // don't keep track of big tests + $gotrows=is_resource($result) ? mysql_num_rows($result) : null; + $gotcolumns=is_resource($result) ? mysql_num_fields($result) : null; + + $qa_database_queries.=$query."\n\n".sprintf('%.2f ms', $usedtime*1000). + (is_numeric($gotrows) ? (' - '.$gotrows.(($gotrows==1) ? ' row' : ' rows')) : ''). + (is_numeric($gotcolumns) ? (' - '.$gotcolumns.(($gotcolumns==1) ? ' column' : ' columns')) : ''). + "\n\n"; + } + + $qa_database_usage['queries']++; + } + + } else + $result=qa_db_query_execute($query); + + // @error_log('Question2Answer MySQL query: '.$query); + + if ($result===false) { + $db=qa_db_connection(); + qa_db_fail_error('query', mysql_errno($db), mysql_error($db), $query); + } + + return $result; + } + + + function qa_db_query_execute($query) +/* + Lower-level function to execute a query, which automatically retries if there is a MySQL deadlock error +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $db=qa_db_connection(); + + for ($attempt=0; $attempt<100; $attempt++) { + $result=mysql_query($query, $db); + + if (($result===false) && (mysql_errno($db)==1213)) + usleep(10000); // dead with InnoDB deadlock errors by waiting 0.01s then retrying + else + break; + } + + return $result; + } + + + function qa_db_escape_string($string) +/* + Return $string escaped for use in queries to the Q2A database (to which a connection must have been made) +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + return mysql_real_escape_string($string, qa_db_connection()); + } + + + function qa_db_argument_to_mysql($argument, $alwaysquote, $arraybrackets=false) +/* + Return $argument escaped for MySQL. Add quotes around it if $alwaysquote is true or it's not numeric. + If $argument is an array, return a comma-separated list of escaped elements, with or without $arraybrackets. +*/ + { + if (is_array($argument)) { + $parts=array(); + + foreach ($argument as $subargument) + $parts[]=qa_db_argument_to_mysql($subargument, $alwaysquote, true); + + if ($arraybrackets) + $result='('.implode(',', $parts).')'; + else + $result=implode(',', $parts); + + } elseif (isset($argument)) { + if ($alwaysquote || !is_numeric($argument)) + $result="'".qa_db_escape_string($argument)."'"; + else + $result=qa_db_escape_string($argument); + + } else + $result='NULL'; + + return $result; + } + + + function qa_db_add_table_prefix($rawname) +/* + Return the full name (with prefix) of database table $rawname, usually if it used after a ^ symbol +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $prefix=QA_MYSQL_TABLE_PREFIX; + + if (defined('QA_MYSQL_USERS_PREFIX')) + switch (strtolower($rawname)) { + case 'users': + case 'userlogins': + case 'userprofile': + case 'userfields': + case 'messages': + case 'cookies': + case 'blobs': + case 'cache': + $prefix=QA_MYSQL_USERS_PREFIX; + break; + } + + return $prefix.$rawname; + } + + + function qa_db_prefix_callback($matches) +/* + Callback function to add table prefixes, as used in qa_db_apply_sub() +*/ + { + return qa_db_add_table_prefix($matches[1]); + } + + + function qa_db_apply_sub($query, $arguments) +/* + Substitute ^, $ and # symbols in $query. ^ symbols are replaced with the table prefix set in qa-config.php. + $ and # symbols are replaced in order by the corresponding element in $arguments (if the element is an array, + it is converted recursively into comma-separated list). Each element in $arguments is escaped. + $ is replaced by the argument in quotes (even if it's a number), # only adds quotes if the argument is non-numeric. + It's important to use $ when matching a textual column since MySQL won't use indexes to compare text against numbers. +*/ + { + $query=preg_replace_callback('/\^([A-Za-z_0-9]+)/', 'qa_db_prefix_callback', $query); + + if (is_array($arguments)) { + $countargs=count($arguments); + $offset=0; + + for ($argument=0; $argument<$countargs; $argument++) { + $stringpos=strpos($query, '$', $offset); + $numberpos=strpos($query, '#', $offset); + + if ( ($stringpos===false) || ( ($numberpos!==false) && ($numberpos<$stringpos) ) ) { + $alwaysquote=false; + $position=$numberpos; + + } else { + $alwaysquote=true; + $position=$stringpos; + } + + if (!is_numeric($position)) + qa_fatal_error('Insufficient parameters in query: '.$query); + + $value=qa_db_argument_to_mysql($arguments[$argument], $alwaysquote); + $query=substr_replace($query, $value, $position, 1); + $offset=$position+strlen($value); // allows inserting strings which contain #/$ character + } + } + + return $query; + } + + + function qa_db_query_sub($query) // arguments for substitution retrieved using func_get_args() +/* + Run $query after substituting ^, # and $ symbols, and return the result resource (or call fail handler) +*/ + { + $funcargs=func_get_args(); + + return qa_db_query_raw(qa_db_apply_sub($query, array_slice($funcargs, 1))); + } + + + function qa_db_last_insert_id() +/* + Return the value of the auto-increment column for the last inserted row +*/ + { + return qa_db_read_one_value(qa_db_query_raw('SELECT LAST_INSERT_ID()')); + } + + + function qa_db_affected_rows() +/* + Does what it says on the tin +*/ + { + return mysql_affected_rows(qa_db_connection()); + } + + + function qa_db_insert_on_duplicate_inserted() +/* + For the previous INSERT ... ON DUPLICATE KEY UPDATE query, return whether an insert operation took place +*/ + { + return (qa_db_affected_rows()==1); + } + + + function qa_db_random_bigint() +/* + Return a random integer (as a string) for use in a BIGINT column. + Actual limit is 18,446,744,073,709,551,615 - we aim for 18,446,743,999,999,999,999 +*/ + { + return sprintf('%d%06d%06d', mt_rand(1,18446743), mt_rand(0,999999), mt_rand(0,999999)); + } + + + function qa_db_list_tables_lc() +/* + Return an array of the names of all tables in the Q2A database, converted to lower case +*/ + { + $tables=qa_db_read_all_values(qa_db_query_raw('SHOW TABLES')); + + foreach ($tables as $key => $table) + $tables[$key]=strtolower($table); + + return $tables; + } + + +/* + The selectspec array can contain the elements below. See qa-db-selects.php for lots of examples. + + By default, qa_db_single_select() and qa_db_multi_select() return the data for each selectspec as a numbered + array of arrays, one per row. The array for each row has column names in the keys, and data in the values. + But this can be changed using the 'arraykey', 'arrayvalue' and 'single' in the selectspec. + + Note that even if you specify ORDER BY in 'source', the final results may not be ordered. This is because + the SELECT could be done within a UNION that (annoyingly) doesn't maintain order. Use 'sortasc' or 'sortdesc' + to fix this. You can however rely on the combination of ORDER BY and LIMIT retrieving the appropriate records. + + + 'columns' => Array of names of columns to be retrieved (required) + + If a value in the columns array has an integer key, it is retrieved AS itself (in a SQL sense). + If a value in the columns array has a non-integer key, it is retrieved AS that key. + Values in the columns array can include table specifiers before the period. + + 'source' => Any SQL after FROM, including table names, JOINs, GROUP BY, ORDER BY, WHERE, etc... (required) + + 'arguments' => Substitutions in order for $s and #s in the query, applied in qa_db_apply_sub() above (required) + + 'arraykey' => Name of column to use for keys of the outer-level returned array, instead of numbers by default + + 'arrayvalue' => Name of column to use for values of outer-level returned array, instead of arrays by default + + 'single' => If true, return the array for a single row and don't embed it within an outer-level array + + 'sortasc' => Sort the output ascending by this column + + 'sortdesc' => Sort the output descending by this column + + + Why does qa_db_multi_select() combine usually unrelated SELECT statements into a single query? + + Because if the database and web servers are on different computers, there will be latency. + This way we ensure that every read pageview on the site requires only a single DB query, so + that we pay for this latency only one time. + + For writes we worry less, since the user is more likely to be expecting a delay. + + If QA_OPTIMIZE_LOCAL_DB is set in qa-config.php, we assume zero latency and go back to + simple queries, since this will allow both MySQL and PHP to provide quicker results. +*/ + + + function qa_db_single_select($selectspec) +/* + Return the data specified by a single $selectspec - see long comment above. +*/ + { + $query='SELECT '; + + foreach ($selectspec['columns'] as $columnas => $columnfrom) + $query.=$columnfrom.(is_int($columnas) ? '' : (' AS '.$columnas)).', '; + + $results=qa_db_read_all_assoc(qa_db_query_raw(qa_db_apply_sub( + substr($query, 0, -2).(strlen(@$selectspec['source']) ? (' FROM '.$selectspec['source']) : ''), + @$selectspec['arguments']) + ), @$selectspec['arraykey']); // arrayvalue is applied in qa_db_post_select() + + qa_db_post_select($results, $selectspec); // post-processing + + return $results; + } + + + function qa_db_multi_select($selectspecs) +/* + Return the data specified by each element of $selectspecs, where the keys of the + returned array match the keys of the supplied $selectspecs array. See long comment above. +*/ + { + if (!count($selectspecs)) + return array(); + + // Perform simple queries if the database is local or there are only 0 or 1 selectspecs + + if (QA_OPTIMIZE_LOCAL_DB || (count($selectspecs)<=1)) { + $outresults=array(); + + foreach ($selectspecs as $selectkey => $selectspec) + $outresults[$selectkey]=qa_db_single_select($selectspec); + + return $outresults; + } + + // Otherwise, parse columns for each spec to deal with columns without an 'AS' specification + + foreach ($selectspecs as $selectkey => $selectspec) { + $selectspecs[$selectkey]['outcolumns']=array(); + $selectspecs[$selectkey]['autocolumn']=array(); + + foreach ($selectspec['columns'] as $columnas => $columnfrom) { + if (is_int($columnas)) { + $periodpos=strpos($columnfrom, '.'); + $columnas=is_numeric($periodpos) ? substr($columnfrom, $periodpos+1) : $columnfrom; + $selectspecs[$selectkey]['autocolumn'][$columnas]=true; + } + + if (isset($selectspecs[$selectkey]['outcolumns'][$columnas])) + qa_fatal_error('Duplicate column name in qa_db_multi_select()'); + + $selectspecs[$selectkey]['outcolumns'][$columnas]=$columnfrom; + } + + if (isset($selectspec['arraykey'])) + if (!isset($selectspecs[$selectkey]['outcolumns'][$selectspec['arraykey']])) + qa_fatal_error('Used arraykey not in columns in qa_db_multi_select()'); + + if (isset($selectspec['arrayvalue'])) + if (!isset($selectspecs[$selectkey]['outcolumns'][$selectspec['arrayvalue']])) + qa_fatal_error('Used arrayvalue not in columns in qa_db_multi_select()'); + } + + // Work out the full list of columns used + + $outcolumns=array(); + foreach ($selectspecs as $selectspec) + $outcolumns=array_unique(array_merge($outcolumns, array_keys($selectspec['outcolumns']))); + + // Build the query based on this full list + + $query=''; + foreach ($selectspecs as $selectkey => $selectspec) { + $subquery="(SELECT '".qa_db_escape_string($selectkey)."'".(empty($query) ? ' AS selectkey' : ''); + + foreach ($outcolumns as $columnas) { + $subquery.=', '.(isset($selectspec['outcolumns'][$columnas]) ? $selectspec['outcolumns'][$columnas] : 'NULL'); + + if (empty($query) && !isset($selectspec['autocolumn'][$columnas])) + $subquery.=' AS '.$columnas; + } + + if (strlen(@$selectspec['source'])) + $subquery.=' FROM '.$selectspec['source']; + + $subquery.=')'; + + if (strlen($query)) + $query.=' UNION ALL '; + + $query.=qa_db_apply_sub($subquery, @$selectspec['arguments']); + } + + // Perform query and extract results + + $rawresults=qa_db_read_all_assoc(qa_db_query_raw($query)); + + $outresults=array(); + foreach ($selectspecs as $selectkey => $selectspec) + $outresults[$selectkey]=array(); + + foreach ($rawresults as $rawresult) { + $selectkey=$rawresult['selectkey']; + $selectspec=$selectspecs[$selectkey]; + + $keepresult=array(); + foreach ($selectspec['outcolumns'] as $columnas => $columnfrom) + $keepresult[$columnas]=$rawresult[$columnas]; + + if (isset($selectspec['arraykey'])) + $outresults[$selectkey][$keepresult[$selectspec['arraykey']]]=$keepresult; + else + $outresults[$selectkey][]=$keepresult; + } + + // Post-processing to apply various stuff include sorting request, since we can't rely on ORDER BY due to UNION + + foreach ($selectspecs as $selectkey => $selectspec) + qa_db_post_select($outresults[$selectkey], $selectspec); + + // Return results + + return $outresults; + } + + + function qa_db_post_select(&$outresult, $selectspec) +/* + Post-process $outresult according to $selectspec, applying 'sortasc', 'sortdesc', 'arrayvalue' and 'single' +*/ + { + // PHP's sorting algorithm is not 'stable', so we use '_order_' element to keep stability. + // By contrast, MySQL's ORDER BY does seem to give the results in a reliable order. + + if (isset($selectspec['sortasc'])) { + require_once QA_INCLUDE_DIR.'qa-util-sort.php'; + + $index=0; + foreach ($outresult as $key => $value) + $outresult[$key]['_order_']=$index++; + + qa_sort_by($outresult, $selectspec['sortasc'], '_order_'); + + } elseif (isset($selectspec['sortdesc'])) { + require_once QA_INCLUDE_DIR.'qa-util-sort.php'; + + if (isset($selectspec['sortdesc_2'])) + qa_sort_by($outresult, $selectspec['sortdesc'], $selectspec['sortdesc_2']); + + else { + $index=count($outresult); + foreach ($outresult as $key => $value) + $outresult[$key]['_order_']=$index--; + + qa_sort_by($outresult, $selectspec['sortdesc'], '_order_'); + } + + $outresult=array_reverse($outresult, true); + } + + if (isset($selectspec['arrayvalue'])) + foreach ($outresult as $key => $value) + $outresult[$key]=$value[$selectspec['arrayvalue']]; + + if (@$selectspec['single']) + $outresult=count($outresult) ? reset($outresult) : null; + } + + + function qa_db_read_all_assoc($result, $key=null, $value=null) +/* + Return the full results from the $result resource as an array. The key of each element in the returned array + is from column $key if specified, otherwise it's integer. The value of each element in the returned array + is from column $value if specified, otherwise it's a named array of all columns, given an array of arrays. +*/ + { + if (!is_resource($result)) + qa_fatal_error('Reading all assoc from invalid result'); + + $assocs=array(); + + while ($assoc=mysql_fetch_assoc($result)) { + if (isset($key)) + $assocs[$assoc[$key]]=isset($value) ? $assoc[$value] : $assoc; + else + $assocs[]=isset($value) ? $assoc[$value] : $assoc; + } + + return $assocs; + } + + + function qa_db_read_one_assoc($result, $allowempty=false) +/* + Return the first row from the $result resource as an array of [column name] => [column value]. + If there's no first row, throw a fatal error unless $allowempty is true. +*/ + { + if (!is_resource($result)) + qa_fatal_error('Reading one assoc from invalid result'); + + $assoc=mysql_fetch_assoc($result); + + if (!is_array($assoc)) { + if ($allowempty) + return null; + else + qa_fatal_error('Reading one assoc from empty results'); + } + + return $assoc; + } + + + function qa_db_read_all_values($result) +/* + Return a numbered array containing the first (and presumably only) column from the $result resource +*/ + { + if (!is_resource($result)) + qa_fatal_error('Reading column from invalid result'); + + $output=array(); + + while ($row=mysql_fetch_row($result)) + $output[]=$row[0]; + + return $output; + } + + + function qa_db_read_one_value($result, $allowempty=false) +/* + Return the first column of the first row (and presumably only cell) from the $result resource. + If there's no first row, throw a fatal error unless $allowempty is true. +*/ + { + if (!is_resource($result)) + qa_fatal_error('Reading one value from invalid result'); + + $row=mysql_fetch_row($result); + + if (!is_array($row)) { + if ($allowempty) + return null; + else + qa_fatal_error('Reading one value from empty results'); + } + + return $row[0]; + } + + + function qa_suspend_update_counts($suspend=true) +/* + Suspend the updating of counts (of many different types) in the database, to save time when making a lot of changes + if $suspend is true, otherwise reinstate it. A counter is kept to allow multiple calls. +*/ + { + global $qa_update_counts_suspended; + + $qa_update_counts_suspended+=($suspend ? 1 : -1); + } + + + function qa_should_update_counts() +/* + Returns whether counts should currently be updated (i.e. if count updating has not been suspended) +*/ + { + global $qa_update_counts_suspended; + + return ($qa_update_counts_suspended<=0); + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-editor-basic.php b/qa-include/qa-editor-basic.php new file mode 100644 index 000000000..2f412859d --- /dev/null +++ b/qa-include/qa-editor-basic.php @@ -0,0 +1,80 @@ + 'textarea', + 'tags' => 'NAME="'.$fieldname.'" ID="'.$fieldname.'"', + 'value' => qa_html($content), + 'rows' => $rows, + ); + } + + function focus_script($fieldname) + { + return "document.getElementById('".$fieldname."').focus();"; + } + + + function read_post($fieldname) + { + return array( + 'format' => '', + 'content' => qa_post_text($fieldname), + ); + } + + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-event-limits.php b/qa-include/qa-event-limits.php new file mode 100644 index 000000000..c53226ccd --- /dev/null +++ b/qa-include/qa-event-limits.php @@ -0,0 +1,107 @@ + $blockwordspreg)); + + qa_send_notification($followanswer['userid'], $followanswer['notify'], @$followanswer['handle'], qa_lang('emails/a_followed_subject'), qa_lang('emails/a_followed_body'), array( + '^q_handle' => isset($handle) ? $handle : qa_lang('main/anonymous'), + '^q_title' => qa_block_words_replace($params['title'], $blockwordspreg), + '^a_content' => $sendtext, + '^url' => qa_q_path($params['postid'], $params['title'], true), + )); + } + + if (qa_opt('notify_admin_q_post')) + qa_send_notification(null, qa_opt('feedback_email'), null, qa_lang('emails/q_posted_subject'), qa_lang('emails/q_posted_body'), array( + '^q_handle' => isset($handle) ? $handle : qa_lang('main/anonymous'), + '^q_title' => $params['title'], // don't censor title or content here since we want the admin to see bad words + '^q_content' => $params['text'], + '^url' => qa_q_path($params['postid'], $params['title'], true), + )); + + break; + + + case 'a_post': + $question=$params['parent']; + + if (isset($question['notify']) && !qa_post_is_by_user($question, $userid, $cookieid)) + qa_send_notification($question['userid'], $question['notify'], @$question['handle'], qa_lang('emails/q_answered_subject'), qa_lang('emails/q_answered_body'), array( + '^a_handle' => isset($handle) ? $handle : qa_lang('main/anonymous'), + '^q_title' => $question['title'], + '^a_content' => qa_block_words_replace($params['text'], qa_get_block_words_preg()), + '^url' => qa_q_path($question['postid'], $question['title'], true, 'A', $params['postid']), + )); + break; + + + case 'c_post': + $parent=$params['parent']; + $question=$params['question']; + + $senttoemail=array(); // to ensure each user or email gets only one notification about an added comment + $senttouserid=array(); + + switch ($parent['basetype']) { + case 'Q': + $subject=qa_lang('emails/q_commented_subject'); + $body=qa_lang('emails/q_commented_body'); + $context=$parent['title']; + break; + + case 'A': + $subject=qa_lang('emails/a_commented_subject'); + $body=qa_lang('emails/a_commented_body'); + $context=qa_viewer_text($parent['content'], $parent['format']); + break; + } + + $blockwordspreg=qa_get_block_words_preg(); + $sendhandle=isset($handle) ? $handle : qa_lang('main/anonymous'); + $sendcontext=qa_block_words_replace($context, $blockwordspreg); + $sendtext=qa_block_words_replace($params['text'], $blockwordspreg); + $sendurl=qa_q_path($question['postid'], $question['title'], true, $parent['basetype'], $parent['postid']); + + if (isset($parent['notify']) && !qa_post_is_by_user($parent, $userid, $cookieid)) { + $senduserid=$parent['userid']; + $sendemail=@$parent['notify']; + + if (qa_email_validate($sendemail)) + $senttoemail[$sendemail]=true; + elseif (isset($senduserid)) + $senttouserid[$senduserid]=true; + + qa_send_notification($senduserid, $sendemail, @$parent['handle'], $subject, $body, array( + '^c_handle' => $sendhandle, + '^c_context' => $sendcontext, + '^c_content' => $sendtext, + '^url' => $sendurl, + )); + } + + foreach ($params['thread'] as $comment) + if (isset($comment['notify']) && !qa_post_is_by_user($comment, $userid, $cookieid)) { + $senduserid=$comment['userid']; + $sendemail=@$comment['notify']; + + if (qa_email_validate($sendemail)) { + if (@$senttoemail[$sendemail]) + continue; + + $senttoemail[$sendemail]=true; + + } elseif (isset($senduserid)) { + if (@$senttouserid[$senduserid]) + continue; + + $senttouserid[$senduserid]=true; + } + + qa_send_notification($senduserid, $sendemail, @$comment['handle'], qa_lang('emails/c_commented_subject'), qa_lang('emails/c_commented_body'), array( + '^c_handle' => $sendhandle, + '^c_context' => $sendcontext, + '^c_content' => $sendtext, + '^url' => $sendurl, + )); + } + break; + + + case 'q_queue': + if (qa_opt('moderate_notify_admin')) + qa_send_notification(null, qa_opt('feedback_email'), null, qa_lang('emails/moderate_subject'), qa_lang('emails/moderate_body'), array( + '^p_handle' => isset($handle) ? $handle : qa_lang('main/anonymous'), + '^p_context' => trim(@$params['title']."\n\n".$params['text']), + '^url' => qa_q_path($params['postid'], $params['title'], true), + )); + break; + + + case 'a_queue': + if (qa_opt('moderate_notify_admin')) + qa_send_notification(null, qa_opt('feedback_email'), null, qa_lang('emails/moderate_subject'), qa_lang('emails/moderate_body'), array( + '^p_handle' => isset($handle) ? $handle : qa_lang('main/anonymous'), + '^p_context' => $params['text'], + '^url' => qa_q_path($params['parentid'], $params['parent']['title'], true, 'A', $params['postid']), + )); + break; + + + case 'c_queue': + if (qa_opt('moderate_notify_admin')) + qa_send_notification(null, qa_opt('feedback_email'), null, qa_lang('emails/moderate_subject'), qa_lang('emails/moderate_body'), array( + '^p_handle' => isset($handle) ? $handle : qa_lang('main/anonymous'), + '^p_context' => $params['text'], + '^url' => qa_q_path($params['questionid'], $params['question']['title'], true, 'C', $params['postid']), + )); + break; + + + case 'q_flag': + case 'a_flag': + case 'c_flag': + $flagcount=$params['flagcount']; + $oldpost=$params['oldpost']; + $notifycount=$flagcount-qa_opt('flagging_notify_first'); + + if ( ($notifycount>=0) && (($notifycount % qa_opt('flagging_notify_every'))==0) ) + qa_send_notification(null, qa_opt('feedback_email'), null, qa_lang('emails/flagged_subject'), qa_lang('emails/flagged_body'), array( + '^p_handle' => isset($oldpost['handle']) ? $oldpost['handle'] : qa_lang('main/anonymous'), + '^flags' => ($flagcount==1) ? qa_lang_html_sub('main/1_flag', '1', '1') : qa_lang_html_sub('main/x_flags', $flagcount), + '^p_context' => trim(@$oldpost['title']."\n\n".qa_viewer_text($oldpost['content'], $oldpost['format'])), + '^url' => qa_q_path($params['questionid'], $params['question']['title'], true, $oldpost['basetype'], $oldpost['postid']), + )); + break; + + + case 'a_select': + $answer=$params['answer']; + + if (isset($answer['notify']) && !qa_post_is_by_user($answer, $userid, $cookieid)) { + $blockwordspreg=qa_get_block_words_preg(); + $sendcontent=qa_viewer_text($answer['content'], $answer['format'], array('blockwordspreg' => $blockwordspreg)); + + qa_send_notification($answer['userid'], $answer['notify'], @$answer['handle'], qa_lang('emails/a_selected_subject'), qa_lang('emails/a_selected_body'), array( + '^s_handle' => isset($handle) ? $handle : qa_lang('main/anonymous'), + '^q_title' => qa_block_words_replace($params['parent']['title'], $blockwordspreg), + '^a_content' => $sendcontent, + '^url' => qa_q_path($params['parentid'], $params['parent']['title'], true, 'A', $params['postid']), + )); + } + break; + } + } + + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-event-updates.php b/qa-include/qa-event-updates.php new file mode 100644 index 000000000..e41f0fb3f --- /dev/null +++ b/qa-include/qa-event-updates.php @@ -0,0 +1,147 @@ + $dummy) + if ( ($keyuserid != $userid) && ($keyuserid != $params['parent']['userid']) ) + qa_db_event_create_not_entity($keyuserid, $params['questionid'], $params['postid'], null, $userid); + break; + + + case 'q_edit': + if ($params['titlechanged'] || $params['contentchanged']) + $updatetype=QA_UPDATE_CONTENT; + elseif ($params['tagschanged']) + $updatetype=QA_UPDATE_TAGS; + else + $updatetype=null; + + if (isset($updatetype)) { + qa_create_event_for_q_user($params['postid'], $params['postid'], $updatetype, $userid, $params['oldquestion']['userid']); + + if ($params['tagschanged']) + qa_create_event_for_tags($params['tags'], $params['postid'], QA_UPDATE_TAGS, $userid); + } + break; + + + case 'a_select': + qa_create_event_for_q_user($params['parentid'], $params['postid'], QA_UPDATE_SELECTED, $userid, $params['answer']['userid']); + break; + + + case 'q_reopen': + case 'q_close': + qa_create_event_for_q_user($params['postid'], $params['postid'], QA_UPDATE_CLOSED, $userid, $params['oldquestion']['userid']); + break; + + + case 'q_reshow': + qa_create_event_for_q_user($params['postid'], $params['postid'], QA_UPDATE_VISIBLE, $userid, $params['oldquestion']['userid']); + break; + + + case 'q_move': + qa_create_event_for_q_user($params['postid'], $params['postid'], QA_UPDATE_CATEGORY, $userid, $params['oldquestion']['userid']); + qa_create_event_for_category($params['categoryid'], $params['postid'], QA_UPDATE_CATEGORY, $userid); + break; + + + case 'a_edit': + if ($params['contentchanged']) + qa_create_event_for_q_user($params['parentid'], $params['postid'], QA_UPDATE_CONTENT, $userid, $params['oldanswer']['userid']); + break; + + + case 'a_reshow': + qa_create_event_for_q_user($params['parentid'], $params['postid'], QA_UPDATE_VISIBLE, $userid, $params['oldanswer']['userid']); + break; + + + case 'c_edit': + if ($params['contentchanged']) + qa_create_event_for_q_user($params['questionid'], $params['postid'], QA_UPDATE_CONTENT, $userid, $params['oldcomment']['userid']); + break; + + + case 'a_to_c': + if ($params['contentchanged']) + qa_create_event_for_q_user($params['questionid'], $params['postid'], QA_UPDATE_CONTENT, $userid, $params['oldanswer']['userid']); + else + qa_create_event_for_q_user($params['questionid'], $params['postid'], QA_UPDATE_TYPE, $userid, $params['oldanswer']['userid']); + break; + + + case 'c_reshow': + qa_create_event_for_q_user($params['questionid'], $params['postid'], QA_UPDATE_VISIBLE, $userid, $params['oldcomment']['userid']); + break; + } + } + + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-external-users-wp.php b/qa-include/qa-external-users-wp.php new file mode 100644 index 000000000..76498387f --- /dev/null +++ b/qa-include/qa-external-users-wp.php @@ -0,0 +1,148 @@ + wp_login_url(qa_opt('site_url').$redirect_back_to_url), + 'register' => site_url('wp-login.php?action=register'), + 'logout' => strtr(wp_logout_url(), array('&' => '&')), + ); + } + + + function qa_get_logged_in_user() + { + $wordpressuser=wp_get_current_user(); + + if ($wordpressuser->ID==0) + return null; + + else { + if (current_user_can('administrator')) + $level=QA_USER_LEVEL_ADMIN; + elseif (current_user_can('editor')) + $level=QA_USER_LEVEL_EDITOR; + elseif (current_user_can('contributor')) + $level=QA_USER_LEVEL_EXPERT; + else + $level=QA_USER_LEVEL_BASIC; + + return array( + 'userid' => $wordpressuser->ID, + 'publicusername' => $wordpressuser->user_nicename, + 'email' => $wordpressuser->user_email, + 'level' => $level, + ); + } + } + + + function qa_get_user_email($userid) + { + $user=get_userdata($userid); + + return @$user->user_email; + } + + + function qa_get_userids_from_public($publicusernames) + { + global $wpdb; + + if (count($publicusernames)) + return qa_db_read_all_assoc(qa_db_query_sub( + 'SELECT user_nicename, ID FROM '.$wpdb->base_prefix.'users WHERE user_nicename IN ($)', + $publicusernames + ), 'user_nicename', 'ID'); + else + return array(); + } + + + function qa_get_public_from_userids($userids) + { + global $wpdb; + + if (count($userids)) + return qa_db_read_all_assoc(qa_db_query_sub( + 'SELECT user_nicename, ID FROM '.$wpdb->base_prefix.'users WHERE ID IN (#)', + $userids + ), 'ID', 'user_nicename'); + else + return array(); + } + + + function qa_get_logged_in_user_html($logged_in_user, $relative_url_prefix) + { + $publicusername=$logged_in_user['publicusername']; + + return ''.htmlspecialchars($publicusername).''; + } + + + function qa_get_users_html($userids, $should_include_link, $relative_url_prefix) + { + $useridtopublic=qa_get_public_from_userids($userids); + + $usershtml=array(); + + foreach ($userids as $userid) { + $publicusername=$useridtopublic[$userid]; + + $usershtml[$userid]=htmlspecialchars($publicusername); + + if ($should_include_link) + $usershtml[$userid]=''.$usershtml[$userid].''; + } + + return $usershtml; + } + + + function qa_user_report_action($userid, $action) + { + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-feed.php b/qa-include/qa-feed.php new file mode 100644 index 000000000..908501e30 --- /dev/null +++ b/qa-include/qa-feed.php @@ -0,0 +1,432 @@ + $query); + + $questions=array(); + + foreach ($results as $result) { + $setarray=array( + 'title' => $result['title'], + 'url' => $result['url'], + ); + + if (isset($result['question'])) + $questions[]=array_merge($result['question'], $setarray); + elseif (isset($result['url'])) + $questions[]=$setarray; + } + break; + } + + +// Remove duplicate questions (perhaps referenced in an answer and a comment) and cut down to size + + require_once QA_INCLUDE_DIR.'qa-app-format.php'; + require_once QA_INCLUDE_DIR.'qa-app-updates.php'; + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + if ( ($feedtype!='search') && ($feedtype!='hot') ) // leave search results and hot questions sorted by relevance + $questions=qa_any_sort_and_dedupe($questions); + + $questions=array_slice($questions, 0, $count); + $blockwordspreg=qa_get_block_words_preg(); + + +// Prepare the XML output + + $lines=array(); + + $lines[]=''; + $lines[]=''; + $lines[]=''; + + $lines[]=''.qa_html($sitetitle.' - '.$title).''; + $lines[]=''.qa_path_html($linkrequest, $linkparams, $siteurl).''; + $lines[]='Powered by Question2Answer'; + + foreach ($questions as $question) { + + // Determine whether this is a question, answer or comment, and act accordingly + + $options=array('blockwordspreg' => @$blockwordspreg, 'showurllinks' => $showurllinks); + + $time=null; + $htmlcontent=null; + + if (isset($question['opostid'])) { + $time=$question['otime']; + + if ($full) + $htmlcontent=qa_viewer_html($question['ocontent'], $question['oformat'], $options); + + } elseif (isset($question['postid'])) { + $time=$question['created']; + + if ($full) + $htmlcontent=qa_viewer_html($question['content'], $question['format'], $options); + } + + if ($feedtype=='search') { + $titleprefix=''; + $urlhtml=qa_html($question['url']); + + } else { + switch (@$question['obasetype'].'-'.@$question['oupdatetype']) { + case 'Q-': + case '-': + $langstring=null; + break; + + case 'Q-'.QA_UPDATE_VISIBLE: + $langstring=$question['hidden'] ? 'misc/feed_hidden_prefix' : 'misc/feed_reshown_prefix'; + break; + + case 'Q-'.QA_UPDATE_CLOSED: + $langstring=isset($question['closedbyid']) ? 'misc/feed_closed_prefix' : 'misc/feed_reopened_prefix'; + break; + + case 'Q-'.QA_UPDATE_TAGS: + $langstring='misc/feed_retagged_prefix'; + break; + + case 'Q-'.QA_UPDATE_CATEGORY: + $langstring='misc/feed_recategorized_prefix'; + break; + + case 'A-': + $langstring='misc/feed_a_prefix'; + break; + + case 'A-'.QA_UPDATE_SELECTED: + $langstring='misc/feed_a_selected_prefix'; + break; + + case 'A-'.QA_UPDATE_VISIBLE: + $langstring=$question['ohidden'] ? 'misc/feed_hidden_prefix' : 'misc/feed_a_reshown_prefix'; + break; + + case 'A-'.QA_UPDATE_CONTENT: + $langstring='misc/feed_a_edited_prefix'; + break; + + case 'C-': + $langstring='misc/feed_c_prefix'; + break; + + case 'C-'.QA_UPDATE_TYPE: + $langstring='misc/feed_c_moved_prefix'; + break; + + case 'C-'.QA_UPDATE_VISIBLE: + $langstring=$question['ohidden'] ? 'misc/feed_hidden_prefix' : 'misc/feed_c_reshown_prefix'; + break; + + case 'C-'.QA_UPDATE_CONTENT: + $langstring='misc/feed_c_edited_prefix'; + break; + + case 'Q-'.QA_UPDATE_CONTENT: + default: + $langstring='misc/feed_edited_prefix'; + break; + + } + + $titleprefix=isset($langstring) ? qa_lang($langstring) : ''; + + $urlhtml=qa_q_path_html($question['postid'], $question['title'], true, @$question['obasetype'], @$question['opostid']) ; + } + + if (isset($blockwordspreg)) + $question['title']=qa_block_words_replace($question['title'], $blockwordspreg); + + // Build the inner XML structure for each item + + $lines[]=''; + $lines[]=''.qa_html($titleprefix.$question['title']).''; + $lines[]=''.$urlhtml.''; + + if (isset($htmlcontent)) + $lines[]=''.qa_html($htmlcontent).''; // qa_html() a second time to put HTML code inside XML wrapper + + if (isset($question['categoryname'])) + $lines[]=''.qa_html($question['categoryname']).''; + + $lines[]=''.$urlhtml.''; + + if (isset($time)) + $lines[]=''.qa_html(gmdate('r', $time)).''; + + $lines[]=''; + } + + $lines[]=''; + $lines[]=''; + + +// Disconnect here, once all output is ready to go + + qa_db_disconnect(); + + +// Output the XML - and we're done! + + header('Content-type: text/xml; charset=utf-8'); + echo implode("\n", $lines); + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-filter-basic.php b/qa-include/qa-filter-basic.php new file mode 100644 index 000000000..a17a2ef47 --- /dev/null +++ b/qa-include/qa-filter-basic.php @@ -0,0 +1,146 @@ +QA_DB_MAX_EMAIL_LENGTH) + return qa_lang_sub('main/max_length_x', QA_DB_MAX_EMAIL_LENGTH); + } + + + function filter_handle(&$handle, $olduser) + { + if (!strlen($handle)) + return qa_lang('users/handle_empty'); + + if (preg_match('/[\\@\\+\\/]/', $handle)) + return qa_lang_sub('users/handle_has_bad', '@ + /'); + + if (qa_strlen($handle)>QA_DB_MAX_HANDLE_LENGTH) + return qa_lang_sub('main/max_length_x', QA_DB_MAX_HANDLE_LENGTH); + } + + + function filter_question(&$question, &$errors, $oldquestion) + { + $this->validate_length($errors, 'title', @$question['title'], qa_opt('min_len_q_title'), + max(qa_opt('min_len_q_title'), min(qa_opt('max_len_q_title'), QA_DB_MAX_TITLE_LENGTH))); + + $this->validate_length($errors, 'content', @$question['content'], 0, QA_DB_MAX_CONTENT_LENGTH); // for storage + + $this->validate_length($errors, 'content', @$question['text'], qa_opt('min_len_q_content'), null); // for display + + if (isset($question['tags'])) { + $counttags=count($question['tags']); + $mintags=min(qa_opt('min_num_q_tags'), qa_opt('max_num_q_tags')); + + if ($counttags<$mintags) + $errors['tags']=qa_lang_sub('question/min_tags_x', $mintags); + elseif ($counttags>qa_opt('max_num_q_tags')) + $errors['tags']=qa_lang_sub('question/max_tags_x', qa_opt('max_num_q_tags')); + else + $this->validate_length($errors, 'tags', qa_tags_to_tagstring($question['tags']), 0, QA_DB_MAX_TAGS_LENGTH); // for storage + } + + $this->validate_post_email($errors, $question); + } + + + function filter_answer(&$answer, &$errors, $question, $oldanswer) + { + $this->validate_length($errors, 'content', @$answer['content'], 0, QA_DB_MAX_CONTENT_LENGTH); // for storage + $this->validate_length($errors, 'content', @$answer['text'], qa_opt('min_len_a_content'), null); // for display + $this->validate_post_email($errors, $answer); + } + + + function filter_comment(&$comment, &$errors, $question, $parent, $oldcomment) + { + $this->validate_length($errors, 'content', @$comment['content'], 0, QA_DB_MAX_CONTENT_LENGTH); // for storage + $this->validate_length($errors, 'content', @$comment['text'], qa_opt('min_len_c_content'), null); // for display + $this->validate_post_email($errors, $comment); + } + + + function filter_profile(&$profile, &$errors, $user, $oldprofile) + { + foreach ($profile as $field => $value) + $this->validate_length($errors, $field, $value, 0, QA_DB_MAX_PROFILE_CONTENT_LENGTH); + } + + + // The definitions below are not part of a standard filter module, but just used within this one + + function validate_length(&$errors, $field, $input, $minlength, $maxlength) + /* + Add textual element $field to $errors if length of $input is not between $minlength and $maxlength + */ + { + if (isset($input)) { + $length=qa_strlen($input); + + if ($length < $minlength) + $errors[$field]=($minlength==1) ? qa_lang('main/field_required') : qa_lang_sub('main/min_length_x', $minlength); + elseif (isset($maxlength) && ($length > $maxlength)) + $errors[$field]=qa_lang_sub('main/max_length_x', $maxlength); + } + } + + + function validate_post_email(&$errors, $post) + { + if (@$post['notify'] && strlen(@$post['email'])) { + $error=$this->filter_email($post['email'], null); + if (isset($error)) + $errors['email']=$error; + } + } + + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-htmLawed.php b/qa-include/qa-htmLawed.php new file mode 100644 index 000000000..3e7ae3339 --- /dev/null +++ b/qa-include/qa-htmLawed.php @@ -0,0 +1,711 @@ +1, 'abbr'=>1, 'acronym'=>1, 'address'=>1, 'applet'=>1, 'area'=>1, 'b'=>1, 'bdo'=>1, 'big'=>1, 'blockquote'=>1, 'br'=>1, 'button'=>1, 'caption'=>1, 'center'=>1, 'cite'=>1, 'code'=>1, 'col'=>1, 'colgroup'=>1, 'dd'=>1, 'del'=>1, 'dfn'=>1, 'dir'=>1, 'div'=>1, 'dl'=>1, 'dt'=>1, 'em'=>1, 'embed'=>1, 'fieldset'=>1, 'font'=>1, 'form'=>1, 'h1'=>1, 'h2'=>1, 'h3'=>1, 'h4'=>1, 'h5'=>1, 'h6'=>1, 'hr'=>1, 'i'=>1, 'iframe'=>1, 'img'=>1, 'input'=>1, 'ins'=>1, 'isindex'=>1, 'kbd'=>1, 'label'=>1, 'legend'=>1, 'li'=>1, 'map'=>1, 'menu'=>1, 'noscript'=>1, 'object'=>1, 'ol'=>1, 'optgroup'=>1, 'option'=>1, 'p'=>1, 'param'=>1, 'pre'=>1, 'q'=>1, 'rb'=>1, 'rbc'=>1, 'rp'=>1, 'rt'=>1, 'rtc'=>1, 'ruby'=>1, 's'=>1, 'samp'=>1, 'script'=>1, 'select'=>1, 'small'=>1, 'span'=>1, 'strike'=>1, 'strong'=>1, 'sub'=>1, 'sup'=>1, 'table'=>1, 'tbody'=>1, 'td'=>1, 'textarea'=>1, 'tfoot'=>1, 'th'=>1, 'thead'=>1, 'tr'=>1, 'tt'=>1, 'u'=>1, 'ul'=>1, 'var'=>1); // 86/deprecated+embed+ruby +if(!empty($C['safe'])){ + unset($e['applet'], $e['embed'], $e['iframe'], $e['object'], $e['script']); +} +$x = !empty($C['elements']) ? str_replace(array("\n", "\r", "\t", ' '), '', $C['elements']) : '*'; +if($x == '-*'){$e = array();} +elseif(strpos($x, '*') === false){$e = array_flip(explode(',', $x));} +else{ + if(isset($x[1])){ + preg_match_all('`(?:^|-|\+)[^\-+]+?(?=-|\+|$)`', $x, $m, PREG_SET_ORDER); + for($i=count($m); --$i>=0;){$m[$i] = $m[$i][0];} + foreach($m as $v){ + if($v[0] == '+'){$e[substr($v, 1)] = 1;} + if($v[0] == '-' && isset($e[($v = substr($v, 1))]) && !in_array('+'. $v, $m)){unset($e[$v]);} + } + } +} +$C['elements'] =& $e; +// config attrs +$x = !empty($C['deny_attribute']) ? str_replace(array("\n", "\r", "\t", ' '), '', $C['deny_attribute']) : ''; +$x = array_flip((isset($x[0]) && $x[0] == '*') ? explode('-', $x) : explode(',', $x. (!empty($C['safe']) ? ',on*' : ''))); +if(isset($x['on*'])){ + unset($x['on*']); + $x += array('onblur'=>1, 'onchange'=>1, 'onclick'=>1, 'ondblclick'=>1, 'onfocus'=>1, 'onkeydown'=>1, 'onkeypress'=>1, 'onkeyup'=>1, 'onmousedown'=>1, 'onmousemove'=>1, 'onmouseout'=>1, 'onmouseover'=>1, 'onmouseup'=>1, 'onreset'=>1, 'onselect'=>1, 'onsubmit'=>1); +} +$C['deny_attribute'] = $x; +// config URL +$x = (isset($C['schemes'][2]) && strpos($C['schemes'], ':')) ? strtolower($C['schemes']) : 'href: aim, feed, file, ftp, gopher, http, https, irc, mailto, news, nntp, sftp, ssh, telnet; *:file, http, https'; +$C['schemes'] = array(); +foreach(explode(';', str_replace(array(' ', "\t", "\r", "\n"), '', $x)) as $v){ + $x = $x2 = null; list($x, $x2) = explode(':', $v, 2); + if($x2){$C['schemes'][$x] = array_flip(explode(',', $x2));} +} +if(!isset($C['schemes']['*'])){$C['schemes']['*'] = array('file'=>1, 'http'=>1, 'https'=>1,);} +if(!empty($C['safe']) && empty($C['schemes']['style'])){$C['schemes']['style'] = array('!'=>1);} +$C['abs_url'] = isset($C['abs_url']) ? $C['abs_url'] : 0; +if(!isset($C['base_url']) or !preg_match('`^[a-zA-Z\d.+\-]+://[^/]+/(.+?/)?$`', $C['base_url'])){ + $C['base_url'] = $C['abs_url'] = 0; +} +// config rest +$C['and_mark'] = empty($C['and_mark']) ? 0 : 1; +$C['anti_link_spam'] = (isset($C['anti_link_spam']) && is_array($C['anti_link_spam']) && count($C['anti_link_spam']) == 2 && (empty($C['anti_link_spam'][0]) or hl_regex($C['anti_link_spam'][0])) && (empty($C['anti_link_spam'][1]) or hl_regex($C['anti_link_spam'][1]))) ? $C['anti_link_spam'] : 0; +$C['anti_mail_spam'] = isset($C['anti_mail_spam']) ? $C['anti_mail_spam'] : 0; +$C['balance'] = isset($C['balance']) ? (bool)$C['balance'] : 1; +$C['cdata'] = isset($C['cdata']) ? $C['cdata'] : (empty($C['safe']) ? 3 : 0); +$C['clean_ms_char'] = empty($C['clean_ms_char']) ? 0 : $C['clean_ms_char']; +$C['comment'] = isset($C['comment']) ? $C['comment'] : (empty($C['safe']) ? 3 : 0); +$C['css_expression'] = empty($C['css_expression']) ? 0 : 1; +$C['direct_list_nest'] = empty($C['direct_list_nest']) ? 0 : 1; +$C['hexdec_entity'] = isset($C['hexdec_entity']) ? $C['hexdec_entity'] : 1; +$C['hook'] = (!empty($C['hook']) && function_exists($C['hook'])) ? $C['hook'] : 0; +$C['hook_tag'] = (!empty($C['hook_tag']) && function_exists($C['hook_tag'])) ? $C['hook_tag'] : 0; +$C['keep_bad'] = isset($C['keep_bad']) ? $C['keep_bad'] : 6; +$C['lc_std_val'] = isset($C['lc_std_val']) ? (bool)$C['lc_std_val'] : 1; +$C['make_tag_strict'] = isset($C['make_tag_strict']) ? $C['make_tag_strict'] : 1; +$C['named_entity'] = isset($C['named_entity']) ? (bool)$C['named_entity'] : 1; +$C['no_deprecated_attr'] = isset($C['no_deprecated_attr']) ? $C['no_deprecated_attr'] : 1; +$C['parent'] = isset($C['parent'][0]) ? strtolower($C['parent']) : 'body'; +$C['show_setting'] = !empty($C['show_setting']) ? $C['show_setting'] : 0; +$C['style_pass'] = empty($C['style_pass']) ? 0 : 1; +$C['tidy'] = empty($C['tidy']) ? 0 : $C['tidy']; +$C['unique_ids'] = isset($C['unique_ids']) ? $C['unique_ids'] : 1; +$C['xml:lang'] = isset($C['xml:lang']) ? $C['xml:lang'] : 0; + +if(isset($GLOBALS['C'])){$reC = $GLOBALS['C'];} +$GLOBALS['C'] = $C; +$S = is_array($S) ? $S : hl_spec($S); +if(isset($GLOBALS['S'])){$reS = $GLOBALS['S'];} +$GLOBALS['S'] = $S; + +$t = preg_replace('`[\x00-\x08\x0b-\x0c\x0e-\x1f]`', '', $t); +if($C['clean_ms_char']){ + $x = array("\x7f"=>'', "\x80"=>'€', "\x81"=>'', "\x83"=>'ƒ', "\x85"=>'…', "\x86"=>'†', "\x87"=>'‡', "\x88"=>'ˆ', "\x89"=>'‰', "\x8a"=>'Š', "\x8b"=>'‹', "\x8c"=>'Œ', "\x8d"=>'', "\x8e"=>'Ž', "\x8f"=>'', "\x90"=>'', "\x95"=>'•', "\x96"=>'–', "\x97"=>'—', "\x98"=>'˜', "\x99"=>'™', "\x9a"=>'š', "\x9b"=>'›', "\x9c"=>'œ', "\x9d"=>'', "\x9e"=>'ž', "\x9f"=>'Ÿ'); + $x = $x + ($C['clean_ms_char'] == 1 ? array("\x82"=>'‚', "\x84"=>'„', "\x91"=>'‘', "\x92"=>'’', "\x93"=>'“', "\x94"=>'”') : array("\x82"=>'\'', "\x84"=>'"', "\x91"=>'\'', "\x92"=>'\'', "\x93"=>'"', "\x94"=>'"')); + $t = strtr($t, $x); +} +if($C['cdata'] or $C['comment']){$t = preg_replace_callback('``sm', 'hl_cmtcd', $t);} +$t = preg_replace_callback('`&([A-Za-z][A-Za-z0-9]{1,30}|#(?:[0-9]{1,8}|[Xx][0-9A-Fa-f]{1,7}));`', 'hl_ent', str_replace('&', '&', $t)); +if($C['unique_ids'] && !isset($GLOBALS['hl_Ids'])){$GLOBALS['hl_Ids'] = array();} +if($C['hook']){$t = $C['hook']($t, $C, $S);} +if($C['show_setting'] && preg_match('`^[a-z][a-z0-9_]*$`i', $C['show_setting'])){ + $GLOBALS[$C['show_setting']] = array('config'=>$C, 'spec'=>$S, 'time'=>microtime()); +} +// main +$t = preg_replace_callback('`<(?:(?:\s|$)|(?:[^>]*(?:>|$)))|>`m', 'hl_tag', $t); +$t = $C['balance'] ? hl_bal($t, $C['keep_bad'], $C['parent']) : $t; +$t = (($C['cdata'] or $C['comment']) && strpos($t, "\x01") !== false) ? str_replace(array("\x01", "\x02", "\x03", "\x04", "\x05"), array('', '', '&', '<', '>'), $t) : $t; +$t = $C['tidy'] ? hl_tidy($t, $C['tidy'], $C['parent']) : $t; +unset($C, $e); +if(isset($reC)){$GLOBALS['C'] = $reC;} +if(isset($reS)){$GLOBALS['S'] = $reS;} +return $t; +// eof +} + +function hl_attrval($t, $p){ +// check attr val against $S +$o = 1; $l = strlen($t); +foreach($p as $k=>$v){ + switch($k){ + case 'maxlen':if($l > $v){$o = 0;} + break; case 'minlen': if($l < $v){$o = 0;} + break; case 'maxval': if((float)($t) > $v){$o = 0;} + break; case 'minval': if((float)($t) < $v){$o = 0;} + break; case 'match': if(!preg_match($v, $t)){$o = 0;} + break; case 'nomatch': if(preg_match($v, $t)){$o = 0;} + break; case 'oneof': + $m = 0; + foreach(explode('|', $v) as $n){if($t == $n){$m = 1; break;}} + $o = $m; + break; case 'noneof': + $m = 1; + foreach(explode('|', $v) as $n){if($t == $n){$m = 0; break;}} + $o = $m; + break; default: + break; + } + if(!$o){break;} +} +return ($o ? $t : (isset($p['default']) ? $p['default'] : 0)); +// eof +} + +function hl_bal($t, $do=1, $in='div'){ +// balance tags +// by content +$cB = array('blockquote'=>1, 'form'=>1, 'map'=>1, 'noscript'=>1); // Block +$cE = array('area'=>1, 'br'=>1, 'col'=>1, 'embed'=>1, 'hr'=>1, 'img'=>1, 'input'=>1, 'isindex'=>1, 'param'=>1); // Empty +$cF = array('button'=>1, 'del'=>1, 'div'=>1, 'dd'=>1, 'fieldset'=>1, 'iframe'=>1, 'ins'=>1, 'li'=>1, 'noscript'=>1, 'object'=>1, 'td'=>1, 'th'=>1); // Flow; later context-wise dynamic move of ins & del to $cI +$cI = array('a'=>1, 'abbr'=>1, 'acronym'=>1, 'address'=>1, 'b'=>1, 'bdo'=>1, 'big'=>1, 'caption'=>1, 'cite'=>1, 'code'=>1, 'dfn'=>1, 'dt'=>1, 'em'=>1, 'font'=>1, 'h1'=>1, 'h2'=>1, 'h3'=>1, 'h4'=>1, 'h5'=>1, 'h6'=>1, 'i'=>1, 'kbd'=>1, 'label'=>1, 'legend'=>1, 'p'=>1, 'pre'=>1, 'q'=>1, 'rb'=>1, 'rt'=>1, 's'=>1, 'samp'=>1, 'small'=>1, 'span'=>1, 'strike'=>1, 'strong'=>1, 'sub'=>1, 'sup'=>1, 'tt'=>1, 'u'=>1, 'var'=>1); // Inline +$cN = array('a'=>array('a'=>1), 'button'=>array('a'=>1, 'button'=>1, 'fieldset'=>1, 'form'=>1, 'iframe'=>1, 'input'=>1, 'label'=>1, 'select'=>1, 'textarea'=>1), 'fieldset'=>array('fieldset'=>1), 'form'=>array('form'=>1), 'label'=>array('label'=>1), 'noscript'=>array('script'=>1), 'pre'=>array('big'=>1, 'font'=>1, 'img'=>1, 'object'=>1, 'script'=>1, 'small'=>1, 'sub'=>1, 'sup'=>1), 'rb'=>array('ruby'=>1), 'rt'=>array('ruby'=>1)); // Illegal +$cN2 = array_keys($cN); +$cR = array('blockquote'=>1, 'dir'=>1, 'dl'=>1, 'form'=>1, 'map'=>1, 'menu'=>1, 'noscript'=>1, 'ol'=>1, 'optgroup'=>1, 'rbc'=>1, 'rtc'=>1, 'ruby'=>1, 'select'=>1, 'table'=>1, 'tbody'=>1, 'tfoot'=>1, 'thead'=>1, 'tr'=>1, 'ul'=>1); +$cS = array('colgroup'=>array('col'=>1), 'dir'=>array('li'=>1), 'dl'=>array('dd'=>1, 'dt'=>1), 'menu'=>array('li'=>1), 'ol'=>array('li'=>1), 'optgroup'=>array('option'=>1), 'option'=>array('#pcdata'=>1), 'rbc'=>array('rb'=>1), 'rp'=>array('#pcdata'=>1), 'rtc'=>array('rt'=>1), 'ruby'=>array('rb'=>1, 'rbc'=>1, 'rp'=>1, 'rt'=>1, 'rtc'=>1), 'select'=>array('optgroup'=>1, 'option'=>1), 'script'=>array('#pcdata'=>1), 'table'=>array('caption'=>1, 'col'=>1, 'colgroup'=>1, 'tfoot'=>1, 'tbody'=>1, 'tr'=>1, 'thead'=>1), 'tbody'=>array('tr'=>1), 'tfoot'=>array('tr'=>1), 'textarea'=>array('#pcdata'=>1), 'thead'=>array('tr'=>1), 'tr'=>array('td'=>1, 'th'=>1), 'ul'=>array('li'=>1)); // Specific - immediate parent-child +if($GLOBALS['C']['direct_list_nest']){$cS['ol'] = $cS['ul'] += array('ol'=>1, 'ul'=>1);} +$cO = array('address'=>array('p'=>1), 'applet'=>array('param'=>1), 'blockquote'=>array('script'=>1), 'fieldset'=>array('legend'=>1, '#pcdata'=>1), 'form'=>array('script'=>1), 'map'=>array('area'=>1), 'object'=>array('param'=>1, 'embed'=>1)); // Other +$cT = array('colgroup'=>1, 'dd'=>1, 'dt'=>1, 'li'=>1, 'option'=>1, 'p'=>1, 'td'=>1, 'tfoot'=>1, 'th'=>1, 'thead'=>1, 'tr'=>1); // Omitable closing +// block/inline type; ins & del both type; #pcdata: text +$eB = array('address'=>1, 'blockquote'=>1, 'center'=>1, 'del'=>1, 'dir'=>1, 'dl'=>1, 'div'=>1, 'fieldset'=>1, 'form'=>1, 'ins'=>1, 'h1'=>1, 'h2'=>1, 'h3'=>1, 'h4'=>1, 'h5'=>1, 'h6'=>1, 'hr'=>1, 'isindex'=>1, 'menu'=>1, 'noscript'=>1, 'ol'=>1, 'p'=>1, 'pre'=>1, 'table'=>1, 'ul'=>1); +$eI = array('#pcdata'=>1, 'a'=>1, 'abbr'=>1, 'acronym'=>1, 'applet'=>1, 'b'=>1, 'bdo'=>1, 'big'=>1, 'br'=>1, 'button'=>1, 'cite'=>1, 'code'=>1, 'del'=>1, 'dfn'=>1, 'em'=>1, 'embed'=>1, 'font'=>1, 'i'=>1, 'iframe'=>1, 'img'=>1, 'input'=>1, 'ins'=>1, 'kbd'=>1, 'label'=>1, 'map'=>1, 'object'=>1, 'q'=>1, 'ruby'=>1, 's'=>1, 'samp'=>1, 'select'=>1, 'script'=>1, 'small'=>1, 'span'=>1, 'strike'=>1, 'strong'=>1, 'sub'=>1, 'sup'=>1, 'textarea'=>1, 'tt'=>1, 'u'=>1, 'var'=>1); +$eN = array('a'=>1, 'big'=>1, 'button'=>1, 'fieldset'=>1, 'font'=>1, 'form'=>1, 'iframe'=>1, 'img'=>1, 'input'=>1, 'label'=>1, 'object'=>1, 'ruby'=>1, 'script'=>1, 'select'=>1, 'small'=>1, 'sub'=>1, 'sup'=>1, 'textarea'=>1); // Exclude from specific ele; $cN values +$eO = array('area'=>1, 'caption'=>1, 'col'=>1, 'colgroup'=>1, 'dd'=>1, 'dt'=>1, 'legend'=>1, 'li'=>1, 'optgroup'=>1, 'option'=>1, 'param'=>1, 'rb'=>1, 'rbc'=>1, 'rp'=>1, 'rt'=>1, 'rtc'=>1, 'script'=>1, 'tbody'=>1, 'td'=>1, 'tfoot'=>1, 'thead'=>1, 'th'=>1, 'tr'=>1); // Missing in $eB & $eI +$eF = $eB + $eI; + +// $in sets allowed child +$in = ((isset($eF[$in]) && $in != '#pcdata') or isset($eO[$in])) ? $in : 'div'; +if(isset($cE[$in])){ + return (!$do ? '' : str_replace(array('<', '>'), array('<', '>'), $t)); +} +if(isset($cS[$in])){$inOk = $cS[$in];} +elseif(isset($cI[$in])){$inOk = $eI; $cI['del'] = 1; $cI['ins'] = 1;} +elseif(isset($cF[$in])){$inOk = $eF; unset($cI['del'], $cI['ins']);} +elseif(isset($cB[$in])){$inOk = $eB; unset($cI['del'], $cI['ins']);} +if(isset($cO[$in])){$inOk = $inOk + $cO[$in];} +if(isset($cN[$in])){$inOk = array_diff_assoc($inOk, $cN[$in]);} + +$t = explode('<', $t); +$ok = $q = array(); // $q seq list of open non-empty ele +ob_start(); + +for($i=-1, $ci=count($t); ++$i<$ci;){ + // allowed $ok in parent $p + if($ql = count($q)){ + $p = array_pop($q); + $q[] = $p; + if(isset($cS[$p])){$ok = $cS[$p];} + elseif(isset($cI[$p])){$ok = $eI; $cI['del'] = 1; $cI['ins'] = 1;} + elseif(isset($cF[$p])){$ok = $eF; unset($cI['del'], $cI['ins']);} + elseif(isset($cB[$p])){$ok = $eB; unset($cI['del'], $cI['ins']);} + if(isset($cO[$p])){$ok = $ok + $cO[$p];} + if(isset($cN[$p])){$ok = array_diff_assoc($ok, $cN[$p]);} + }else{$ok = $inOk; unset($cI['del'], $cI['ins']);} + // bad tags, & ele content + if(isset($e) && ($do == 1 or (isset($ok['#pcdata']) && ($do == 3 or $do == 5)))){ + echo '<', $s, $e, $a, '>'; + } + if(isset($x[0])){ + if($do < 3 or isset($ok['#pcdata'])){echo $x;} + elseif(strpos($x, "\x02\x04")){ + foreach(preg_split('`(\x01\x02[^\x01\x02]+\x02\x01)`', $x, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY) as $v){ + echo (substr($v, 0, 2) == "\x01\x02" ? $v : ($do > 4 ? preg_replace('`\S`', '', $v) : '')); + } + }elseif($do > 4){echo preg_replace('`\S`', '', $x);} + } + // get markup + if(!preg_match('`^(/?)([a-zA-Z1-6]+)([^>]*)>(.*)`sm', $t[$i], $r)){$x = $t[$i]; continue;} + $s = null; $e = null; $a = null; $x = null; list($all, $s, $e, $a, $x) = $r; + // close tag + if($s){ + if(isset($cE[$e]) or !in_array($e, $q)){continue;} // Empty/unopen + if($p == $e){array_pop($q); echo ''; unset($e); continue;} // Last open + $add = ''; // Nesting - close open tags that need to be + for($j=-1, $cj=count($q); ++$j<$cj;){ + if(($d = array_pop($q)) == $e){break;} + else{$add .= "";} + } + echo $add, ''; unset($e); continue; + } + // open tag + // $cB ele needs $eB ele as child + if(isset($cB[$e]) && strlen(trim($x))){ + $t[$i] = "{$e}{$a}>"; + array_splice($t, $i+1, 0, 'div>'. $x); unset($e, $x); ++$ci; --$i; continue; + } + if((($ql && isset($cB[$p])) or (isset($cB[$in]) && !$ql)) && !isset($eB[$e]) && !isset($ok[$e])){ + array_splice($t, $i, 0, 'div>'); unset($e, $x); ++$ci; --$i; continue; + } + // if no open ele, $in = parent; mostly immediate parent-child relation should hold + if(!$ql or !isset($eN[$e]) or !array_intersect($q, $cN2)){ + if(!isset($ok[$e])){ + if($ql && isset($cT[$p])){echo ''; unset($e, $x); --$i;} + continue; + } + if(!isset($cE[$e])){$q[] = $e;} + echo '<', $e, $a, '>'; unset($e); continue; + } + // specific parent-child + if(isset($cS[$p][$e])){ + if(!isset($cE[$e])){$q[] = $e;} + echo '<', $e, $a, '>'; unset($e); continue; + } + // nesting + $add = ''; + $q2 = array(); + for($k=-1, $kc=count($q); ++$k<$kc;){ + $d = $q[$k]; + $ok2 = array(); + if(isset($cS[$d])){$q2[] = $d; continue;} + $ok2 = isset($cI[$d]) ? $eI : $eF; + if(isset($cO[$d])){$ok2 = $ok2 + $cO[$d];} + if(isset($cN[$d])){$ok2 = array_diff_assoc($ok2, $cN[$d]);} + if(!isset($ok2[$e])){ + if(!$k && !isset($inOk[$e])){continue 2;} + $add = ""; + for(;++$k<$kc;){$add = "{$add}";} + break; + } + else{$q2[] = $d;} + } + $q = $q2; + if(!isset($cE[$e])){$q[] = $e;} + echo $add, '<', $e, $a, '>'; unset($e); continue; +} + +// end +if($ql = count($q)){ + $p = array_pop($q); + $q[] = $p; + if(isset($cS[$p])){$ok = $cS[$p];} + elseif(isset($cI[$p])){$ok = $eI; $cI['del'] = 1; $cI['ins'] = 1;} + elseif(isset($cF[$p])){$ok = $eF; unset($cI['del'], $cI['ins']);} + elseif(isset($cB[$p])){$ok = $eB; unset($cI['del'], $cI['ins']);} + if(isset($cO[$p])){$ok = $ok + $cO[$p];} + if(isset($cN[$p])){$ok = array_diff_assoc($ok, $cN[$p]);} +}else{$ok = $inOk; unset($cI['del'], $cI['ins']);} +if(isset($e) && ($do == 1 or (isset($ok['#pcdata']) && ($do == 3 or $do == 5)))){ + echo '<', $s, $e, $a, '>'; +} +if(isset($x[0])){ + if(strlen(trim($x)) && (($ql && isset($cB[$p])) or (isset($cB[$in]) && !$ql))){ + echo '

', $x, '
'; + } + elseif($do < 3 or isset($ok['#pcdata'])){echo $x;} + elseif(strpos($x, "\x02\x04")){ + foreach(preg_split('`(\x01\x02[^\x01\x02]+\x02\x01)`', $x, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY) as $v){ + echo (substr($v, 0, 2) == "\x01\x02" ? $v : ($do > 4 ? preg_replace('`\S`', '', $v) : '')); + } + }elseif($do > 4){echo preg_replace('`\S`', '', $x);} +} +while(!empty($q) && ($e = array_pop($q))){echo '';} +$o = ob_get_contents(); +ob_end_clean(); +return $o; +// eof +} + +function hl_cmtcd($t){ +// comment/CDATA sec handler +$t = $t[0]; +global $C; +if(!($v = $C[$n = $t[3] == '-' ? 'comment' : 'cdata'])){return $t;} +if($v == 1){return '';} +if($n == 'comment'){ + if(substr(($t = preg_replace('`--+`', '-', substr($t, 4, -3))), -1) != ' '){$t .= ' ';} +} +else{$t = substr($t, 1, -1);} +$t = $v == 2 ? str_replace(array('&', '<', '>'), array('&', '<', '>'), $t) : $t; +return str_replace(array('&', '<', '>'), array("\x03", "\x04", "\x05"), ($n == 'comment' ? "\x01\x02\x04!--$t--\x05\x02\x01" : "\x01\x01\x04$t\x05\x01\x01")); +// eof +} + +function hl_ent($t){ +// entitity handler +global $C; +$t = $t[1]; +static $U = array('quot'=>1,'amp'=>1,'lt'=>1,'gt'=>1); +static $N = array('fnof'=>'402', 'Alpha'=>'913', 'Beta'=>'914', 'Gamma'=>'915', 'Delta'=>'916', 'Epsilon'=>'917', 'Zeta'=>'918', 'Eta'=>'919', 'Theta'=>'920', 'Iota'=>'921', 'Kappa'=>'922', 'Lambda'=>'923', 'Mu'=>'924', 'Nu'=>'925', 'Xi'=>'926', 'Omicron'=>'927', 'Pi'=>'928', 'Rho'=>'929', 'Sigma'=>'931', 'Tau'=>'932', 'Upsilon'=>'933', 'Phi'=>'934', 'Chi'=>'935', 'Psi'=>'936', 'Omega'=>'937', 'alpha'=>'945', 'beta'=>'946', 'gamma'=>'947', 'delta'=>'948', 'epsilon'=>'949', 'zeta'=>'950', 'eta'=>'951', 'theta'=>'952', 'iota'=>'953', 'kappa'=>'954', 'lambda'=>'955', 'mu'=>'956', 'nu'=>'957', 'xi'=>'958', 'omicron'=>'959', 'pi'=>'960', 'rho'=>'961', 'sigmaf'=>'962', 'sigma'=>'963', 'tau'=>'964', 'upsilon'=>'965', 'phi'=>'966', 'chi'=>'967', 'psi'=>'968', 'omega'=>'969', 'thetasym'=>'977', 'upsih'=>'978', 'piv'=>'982', 'bull'=>'8226', 'hellip'=>'8230', 'prime'=>'8242', 'Prime'=>'8243', 'oline'=>'8254', 'frasl'=>'8260', 'weierp'=>'8472', 'image'=>'8465', 'real'=>'8476', 'trade'=>'8482', 'alefsym'=>'8501', 'larr'=>'8592', 'uarr'=>'8593', 'rarr'=>'8594', 'darr'=>'8595', 'harr'=>'8596', 'crarr'=>'8629', 'lArr'=>'8656', 'uArr'=>'8657', 'rArr'=>'8658', 'dArr'=>'8659', 'hArr'=>'8660', 'forall'=>'8704', 'part'=>'8706', 'exist'=>'8707', 'empty'=>'8709', 'nabla'=>'8711', 'isin'=>'8712', 'notin'=>'8713', 'ni'=>'8715', 'prod'=>'8719', 'sum'=>'8721', 'minus'=>'8722', 'lowast'=>'8727', 'radic'=>'8730', 'prop'=>'8733', 'infin'=>'8734', 'ang'=>'8736', 'and'=>'8743', 'or'=>'8744', 'cap'=>'8745', 'cup'=>'8746', 'int'=>'8747', 'there4'=>'8756', 'sim'=>'8764', 'cong'=>'8773', 'asymp'=>'8776', 'ne'=>'8800', 'equiv'=>'8801', 'le'=>'8804', 'ge'=>'8805', 'sub'=>'8834', 'sup'=>'8835', 'nsub'=>'8836', 'sube'=>'8838', 'supe'=>'8839', 'oplus'=>'8853', 'otimes'=>'8855', 'perp'=>'8869', 'sdot'=>'8901', 'lceil'=>'8968', 'rceil'=>'8969', 'lfloor'=>'8970', 'rfloor'=>'8971', 'lang'=>'9001', 'rang'=>'9002', 'loz'=>'9674', 'spades'=>'9824', 'clubs'=>'9827', 'hearts'=>'9829', 'diams'=>'9830', 'apos'=>'39', 'OElig'=>'338', 'oelig'=>'339', 'Scaron'=>'352', 'scaron'=>'353', 'Yuml'=>'376', 'circ'=>'710', 'tilde'=>'732', 'ensp'=>'8194', 'emsp'=>'8195', 'thinsp'=>'8201', 'zwnj'=>'8204', 'zwj'=>'8205', 'lrm'=>'8206', 'rlm'=>'8207', 'ndash'=>'8211', 'mdash'=>'8212', 'lsquo'=>'8216', 'rsquo'=>'8217', 'sbquo'=>'8218', 'ldquo'=>'8220', 'rdquo'=>'8221', 'bdquo'=>'8222', 'dagger'=>'8224', 'Dagger'=>'8225', 'permil'=>'8240', 'lsaquo'=>'8249', 'rsaquo'=>'8250', 'euro'=>'8364', 'nbsp'=>'160', 'iexcl'=>'161', 'cent'=>'162', 'pound'=>'163', 'curren'=>'164', 'yen'=>'165', 'brvbar'=>'166', 'sect'=>'167', 'uml'=>'168', 'copy'=>'169', 'ordf'=>'170', 'laquo'=>'171', 'not'=>'172', 'shy'=>'173', 'reg'=>'174', 'macr'=>'175', 'deg'=>'176', 'plusmn'=>'177', 'sup2'=>'178', 'sup3'=>'179', 'acute'=>'180', 'micro'=>'181', 'para'=>'182', 'middot'=>'183', 'cedil'=>'184', 'sup1'=>'185', 'ordm'=>'186', 'raquo'=>'187', 'frac14'=>'188', 'frac12'=>'189', 'frac34'=>'190', 'iquest'=>'191', 'Agrave'=>'192', 'Aacute'=>'193', 'Acirc'=>'194', 'Atilde'=>'195', 'Auml'=>'196', 'Aring'=>'197', 'AElig'=>'198', 'Ccedil'=>'199', 'Egrave'=>'200', 'Eacute'=>'201', 'Ecirc'=>'202', 'Euml'=>'203', 'Igrave'=>'204', 'Iacute'=>'205', 'Icirc'=>'206', 'Iuml'=>'207', 'ETH'=>'208', 'Ntilde'=>'209', 'Ograve'=>'210', 'Oacute'=>'211', 'Ocirc'=>'212', 'Otilde'=>'213', 'Ouml'=>'214', 'times'=>'215', 'Oslash'=>'216', 'Ugrave'=>'217', 'Uacute'=>'218', 'Ucirc'=>'219', 'Uuml'=>'220', 'Yacute'=>'221', 'THORN'=>'222', 'szlig'=>'223', 'agrave'=>'224', 'aacute'=>'225', 'acirc'=>'226', 'atilde'=>'227', 'auml'=>'228', 'aring'=>'229', 'aelig'=>'230', 'ccedil'=>'231', 'egrave'=>'232', 'eacute'=>'233', 'ecirc'=>'234', 'euml'=>'235', 'igrave'=>'236', 'iacute'=>'237', 'icirc'=>'238', 'iuml'=>'239', 'eth'=>'240', 'ntilde'=>'241', 'ograve'=>'242', 'oacute'=>'243', 'ocirc'=>'244', 'otilde'=>'245', 'ouml'=>'246', 'divide'=>'247', 'oslash'=>'248', 'ugrave'=>'249', 'uacute'=>'250', 'ucirc'=>'251', 'uuml'=>'252', 'yacute'=>'253', 'thorn'=>'254', 'yuml'=>'255'); +if($t[0] != '#'){ + return ($C['and_mark'] ? "\x06" : '&'). (isset($U[$t]) ? $t : (isset($N[$t]) ? (!$C['named_entity'] ? '#'. ($C['hexdec_entity'] > 1 ? 'x'. dechex($N[$t]) : $N[$t]) : $t) : 'amp;'. $t)). ';'; +} +if(($n = ctype_digit($t = substr($t, 1)) ? intval($t) : hexdec(substr($t, 1))) < 9 or ($n > 13 && $n < 32) or $n == 11 or $n == 12 or ($n > 126 && $n < 160 && $n != 133) or ($n > 55295 && ($n < 57344 or ($n > 64975 && $n < 64992) or $n == 65534 or $n == 65535 or $n > 1114111))){ + return ($C['and_mark'] ? "\x06" : '&'). "amp;#{$t};"; +} +return ($C['and_mark'] ? "\x06" : '&'). '#'. (((ctype_digit($t) && $C['hexdec_entity'] < 2) or !$C['hexdec_entity']) ? $n : 'x'. dechex($n)). ';'; +// eof +} + +function hl_prot($p, $c=null){ +// check URL scheme +global $C; +$b = $a = ''; +if($c == null){$c = 'style'; $b = $p[1]; $a = $p[3]; $p = trim($p[2]);} +$c = isset($C['schemes'][$c]) ? $C['schemes'][$c] : $C['schemes']['*']; +static $d = 'denied:'; +if(isset($c['!']) && substr($p, 0, 7) != $d){$p = "$d$p";} +if(isset($c['*']) or !strcspn($p, '#?;') or (substr($p, 0, 7) == $d)){return "{$b}{$p}{$a}";} // All ok, frag, query, param +if(preg_match('`^([a-z\d\-+.&#; ]+?)(:|&#(58|x3a);|%3a|\\\\0{0,4}3a).`i', $p, $m) && !isset($c[strtolower($m[1])])){ // Denied prot + return "{$b}{$d}{$p}{$a}"; +} +if($C['abs_url']){ + if($C['abs_url'] == -1 && strpos($p, $C['base_url']) === 0){ // Make url rel + $p = substr($p, strlen($C['base_url'])); + }elseif(empty($m[1])){ // Make URL abs + if(substr($p, 0, 2) == '//'){$p = substr($C['base_url'], 0, strpos($C['base_url'], ':')+1). $p;} + elseif($p[0] == '/'){$p = preg_replace('`(^.+?://[^/]+)(.*)`', '$1', $C['base_url']). $p;} + elseif(strcspn($p, './')){$p = $C['base_url']. $p;} + else{ + preg_match('`^([a-zA-Z\d\-+.]+://[^/]+)(.*)`', $C['base_url'], $m); + $p = preg_replace('`(?<=/)\./`', '', $m[2]. $p); + while(preg_match('`(?<=/)([^/]{3,}|[^/.]+?|\.[^/.]|[^/.]\.)/\.\./`', $p)){ + $p = preg_replace('`(?<=/)([^/]{3,}|[^/.]+?|\.[^/.]|[^/.]\.)/\.\./`', '', $p); + } + $p = $m[1]. $p; + } + } +} +return "{$b}{$p}{$a}"; +// eof +} + +function hl_regex($p){ +// ?regex +if(empty($p)){return 0;} +if($t = ini_get('track_errors')){$o = isset($php_errormsg) ? $php_errormsg : null;} +else{ini_set('track_errors', 1);} +unset($php_errormsg); +if(($d = ini_get('display_errors'))){ini_set('display_errors', 0);} +preg_match($p, ''); +if($d){ini_set('display_errors', 1);} +$r = isset($php_errormsg) ? 0 : 1; +if($t){$php_errormsg = isset($o) ? $o : null;} +else{ini_set('track_errors', 0);} +return $r; +// eof +} + +function hl_spec($t){ +// final $spec +$s = array(); +$t = str_replace(array("\t", "\r", "\n", ' '), '', preg_replace('/"(?>(`.|[^"])*)"/sme', 'substr(str_replace(array(";", "|", "~", " ", ",", "/", "(", ")", \'`"\'), array("\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08", "\""), "$0"), 1, -1)', trim($t))); +for($i = count(($t = explode(';', $t))); --$i>=0;){ + $w = $t[$i]; + if(empty($w) or ($e = strpos($w, '=')) === false or !strlen(($a = substr($w, $e+1)))){continue;} + $y = $n = array(); + foreach(explode(',', $a) as $v){ + if(!preg_match('`^([a-z:\-\*]+)(?:\((.*?)\))?`i', $v, $m)){continue;} + if(($x = strtolower($m[1])) == '-*'){$n['*'] = 1; continue;} + if($x[0] == '-'){$n[substr($x, 1)] = 1; continue;} + if(!isset($m[2])){$y[$x] = 1; continue;} + foreach(explode('/', $m[2]) as $m){ + if(empty($m) or ($p = strpos($m, '=')) == 0 or $p < 5){$y[$x] = 1; continue;} + $y[$x][strtolower(substr($m, 0, $p))] = str_replace(array("\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08"), array(";", "|", "~", " ", ",", "/", "(", ")"), substr($m, $p+1)); + } + if(isset($y[$x]['match']) && !hl_regex($y[$x]['match'])){unset($y[$x]['match']);} + if(isset($y[$x]['nomatch']) && !hl_regex($y[$x]['nomatch'])){unset($y[$x]['nomatch']);} + } + if(!count($y) && !count($n)){continue;} + foreach(explode(',', substr($w, 0, $e)) as $v){ + if(!strlen(($v = strtolower($v)))){continue;} + if(count($y)){$s[$v] = $y;} + if(count($n)){$s[$v]['n'] = $n;} + } +} +return $s; +// eof +} + +function hl_tag($t){ +// tag/attribute handler +global $C; +$t = $t[0]; +// invalid < > +if($t == '< '){return '< ';} +if($t == '>'){return '>';} +if(!preg_match('`^<(/?)([a-zA-Z][a-zA-Z1-6]*)([^>]*?)\s?>$`m', $t, $m)){ + return str_replace(array('<', '>'), array('<', '>'), $t); +}elseif(!isset($C['elements'][($e = strtolower($m[2]))])){ + return (($C['keep_bad']%2) ? str_replace(array('<', '>'), array('<', '>'), $t) : ''); +} +// attr string +$a = str_replace(array("\n", "\r", "\t"), ' ', trim($m[3])); +// tag transform +static $eD = array('applet'=>1, 'center'=>1, 'dir'=>1, 'embed'=>1, 'font'=>1, 'isindex'=>1, 'menu'=>1, 's'=>1, 'strike'=>1, 'u'=>1); // Deprecated +if($C['make_tag_strict'] && isset($eD[$e])){ + $trt = hl_tag2($e, $a, $C['make_tag_strict']); + if(!$e){return (($C['keep_bad']%2) ? str_replace(array('<', '>'), array('<', '>'), $t) : '');} +} +// close tag +static $eE = array('area'=>1, 'br'=>1, 'col'=>1, 'embed'=>1, 'hr'=>1, 'img'=>1, 'input'=>1, 'isindex'=>1, 'param'=>1); // Empty ele +if(!empty($m[1])){ + return (!isset($eE[$e]) ? "" : (($C['keep_bad'])%2 ? str_replace(array('<', '>'), array('<', '>'), $t) : '')); +} + +// open tag & attr +static $aN = array('abbr'=>array('td'=>1, 'th'=>1), 'accept-charset'=>array('form'=>1), 'accept'=>array('form'=>1, 'input'=>1), 'accesskey'=>array('a'=>1, 'area'=>1, 'button'=>1, 'input'=>1, 'label'=>1, 'legend'=>1, 'textarea'=>1), 'action'=>array('form'=>1), 'align'=>array('caption'=>1, 'embed'=>1, 'applet'=>1, 'iframe'=>1, 'img'=>1, 'input'=>1, 'object'=>1, 'legend'=>1, 'table'=>1, 'hr'=>1, 'div'=>1, 'h1'=>1, 'h2'=>1, 'h3'=>1, 'h4'=>1, 'h5'=>1, 'h6'=>1, 'p'=>1, 'col'=>1, 'colgroup'=>1, 'tbody'=>1, 'td'=>1, 'tfoot'=>1, 'th'=>1, 'thead'=>1, 'tr'=>1), 'alt'=>array('applet'=>1, 'area'=>1, 'img'=>1, 'input'=>1), 'archive'=>array('applet'=>1, 'object'=>1), 'axis'=>array('td'=>1, 'th'=>1), 'bgcolor'=>array('embed'=>1, 'table'=>1, 'tr'=>1, 'td'=>1, 'th'=>1), 'border'=>array('table'=>1, 'img'=>1, 'object'=>1), 'bordercolor'=>array('table'=>1, 'td'=>1, 'tr'=>1), 'cellpadding'=>array('table'=>1), 'cellspacing'=>array('table'=>1), 'char'=>array('col'=>1, 'colgroup'=>1, 'tbody'=>1, 'td'=>1, 'tfoot'=>1, 'th'=>1, 'thead'=>1, 'tr'=>1), 'charoff'=>array('col'=>1, 'colgroup'=>1, 'tbody'=>1, 'td'=>1, 'tfoot'=>1, 'th'=>1, 'thead'=>1, 'tr'=>1), 'charset'=>array('a'=>1, 'script'=>1), 'checked'=>array('input'=>1), 'cite'=>array('blockquote'=>1, 'q'=>1, 'del'=>1, 'ins'=>1), 'classid'=>array('object'=>1), 'clear'=>array('br'=>1), 'code'=>array('applet'=>1), 'codebase'=>array('object'=>1, 'applet'=>1), 'codetype'=>array('object'=>1), 'color'=>array('font'=>1), 'cols'=>array('textarea'=>1), 'colspan'=>array('td'=>1, 'th'=>1), 'compact'=>array('dir'=>1, 'dl'=>1, 'menu'=>1, 'ol'=>1, 'ul'=>1), 'coords'=>array('area'=>1, 'a'=>1), 'data'=>array('object'=>1), 'datetime'=>array('del'=>1, 'ins'=>1), 'declare'=>array('object'=>1), 'defer'=>array('script'=>1), 'dir'=>array('bdo'=>1), 'disabled'=>array('button'=>1, 'input'=>1, 'optgroup'=>1, 'option'=>1, 'select'=>1, 'textarea'=>1), 'enctype'=>array('form'=>1), 'face'=>array('font'=>1), 'flashvars'=>array('embed'=>1), 'for'=>array('label'=>1), 'frame'=>array('table'=>1), 'frameborder'=>array('iframe'=>1), 'headers'=>array('td'=>1, 'th'=>1), 'height'=>array('embed'=>1, 'iframe'=>1, 'td'=>1, 'th'=>1, 'img'=>1, 'object'=>1, 'applet'=>1), 'href'=>array('a'=>1, 'area'=>1), 'hreflang'=>array('a'=>1), 'hspace'=>array('applet'=>1, 'img'=>1, 'object'=>1), 'ismap'=>array('img'=>1, 'input'=>1), 'label'=>array('option'=>1, 'optgroup'=>1), 'language'=>array('script'=>1), 'longdesc'=>array('img'=>1, 'iframe'=>1), 'marginheight'=>array('iframe'=>1), 'marginwidth'=>array('iframe'=>1), 'maxlength'=>array('input'=>1), 'method'=>array('form'=>1), 'model'=>array('embed'=>1), 'multiple'=>array('select'=>1), 'name'=>array('button'=>1, 'embed'=>1, 'textarea'=>1, 'applet'=>1, 'select'=>1, 'form'=>1, 'iframe'=>1, 'img'=>1, 'a'=>1, 'input'=>1, 'object'=>1, 'map'=>1, 'param'=>1), 'nohref'=>array('area'=>1), 'noshade'=>array('hr'=>1), 'nowrap'=>array('td'=>1, 'th'=>1), 'object'=>array('applet'=>1), 'onblur'=>array('a'=>1, 'area'=>1, 'button'=>1, 'input'=>1, 'label'=>1, 'select'=>1, 'textarea'=>1), 'onchange'=>array('input'=>1, 'select'=>1, 'textarea'=>1), 'onfocus'=>array('a'=>1, 'area'=>1, 'button'=>1, 'input'=>1, 'label'=>1, 'select'=>1, 'textarea'=>1), 'onreset'=>array('form'=>1), 'onselect'=>array('input'=>1, 'textarea'=>1), 'onsubmit'=>array('form'=>1), 'pluginspage'=>array('embed'=>1), 'pluginurl'=>array('embed'=>1), 'prompt'=>array('isindex'=>1), 'readonly'=>array('textarea'=>1, 'input'=>1), 'rel'=>array('a'=>1), 'rev'=>array('a'=>1), 'rows'=>array('textarea'=>1), 'rowspan'=>array('td'=>1, 'th'=>1), 'rules'=>array('table'=>1), 'scope'=>array('td'=>1, 'th'=>1), 'scrolling'=>array('iframe'=>1), 'selected'=>array('option'=>1), 'shape'=>array('area'=>1, 'a'=>1), 'size'=>array('hr'=>1, 'font'=>1, 'input'=>1, 'select'=>1), 'span'=>array('col'=>1, 'colgroup'=>1), 'src'=>array('embed'=>1, 'script'=>1, 'input'=>1, 'iframe'=>1, 'img'=>1), 'standby'=>array('object'=>1), 'start'=>array('ol'=>1), 'summary'=>array('table'=>1), 'tabindex'=>array('a'=>1, 'area'=>1, 'button'=>1, 'input'=>1, 'object'=>1, 'select'=>1, 'textarea'=>1), 'target'=>array('a'=>1, 'area'=>1, 'form'=>1), 'type'=>array('a'=>1, 'embed'=>1, 'object'=>1, 'param'=>1, 'script'=>1, 'input'=>1, 'li'=>1, 'ol'=>1, 'ul'=>1, 'button'=>1), 'usemap'=>array('img'=>1, 'input'=>1, 'object'=>1), 'valign'=>array('col'=>1, 'colgroup'=>1, 'tbody'=>1, 'td'=>1, 'tfoot'=>1, 'th'=>1, 'thead'=>1, 'tr'=>1), 'value'=>array('input'=>1, 'option'=>1, 'param'=>1, 'button'=>1, 'li'=>1), 'valuetype'=>array('param'=>1), 'vspace'=>array('applet'=>1, 'img'=>1, 'object'=>1), 'width'=>array('embed'=>1, 'hr'=>1, 'iframe'=>1, 'img'=>1, 'object'=>1, 'table'=>1, 'td'=>1, 'th'=>1, 'applet'=>1, 'col'=>1, 'colgroup'=>1, 'pre'=>1), 'wmode'=>array('embed'=>1), 'xml:space'=>array('pre'=>1, 'script'=>1, 'style'=>1)); // Ele-specific +static $aNE = array('checked'=>1, 'compact'=>1, 'declare'=>1, 'defer'=>1, 'disabled'=>1, 'ismap'=>1, 'multiple'=>1, 'nohref'=>1, 'noresize'=>1, 'noshade'=>1, 'nowrap'=>1, 'readonly'=>1, 'selected'=>1); // Empty +static $aNP = array('action'=>1, 'cite'=>1, 'classid'=>1, 'codebase'=>1, 'data'=>1, 'href'=>1, 'longdesc'=>1, 'model'=>1, 'pluginspage'=>1, 'pluginurl'=>1, 'usemap'=>1); // Need scheme check; excludes style, on* & src +static $aNU = array('class'=>array('param'=>1, 'script'=>1), 'dir'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'iframe'=>1, 'param'=>1, 'script'=>1), 'id'=>array('script'=>1), 'lang'=>array('applet'=>1, 'br'=>1, 'iframe'=>1, 'param'=>1, 'script'=>1), 'xml:lang'=>array('applet'=>1, 'br'=>1, 'iframe'=>1, 'param'=>1, 'script'=>1), 'onclick'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'ondblclick'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'onkeydown'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'onkeypress'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'onkeyup'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'onmousedown'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'onmousemove'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'onmouseout'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'onmouseover'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'onmouseup'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'style'=>array('param'=>1, 'script'=>1), 'title'=>array('param'=>1, 'script'=>1)); // Univ & exceptions + +if($C['lc_std_val']){ + // predef attr vals for $eAL & $aNE ele + static $aNL = array('all'=>1, 'baseline'=>1, 'bottom'=>1, 'button'=>1, 'center'=>1, 'char'=>1, 'checkbox'=>1, 'circle'=>1, 'col'=>1, 'colgroup'=>1, 'cols'=>1, 'data'=>1, 'default'=>1, 'file'=>1, 'get'=>1, 'groups'=>1, 'hidden'=>1, 'image'=>1, 'justify'=>1, 'left'=>1, 'ltr'=>1, 'middle'=>1, 'none'=>1, 'object'=>1, 'password'=>1, 'poly'=>1, 'post'=>1, 'preserve'=>1, 'radio'=>1, 'rect'=>1, 'ref'=>1, 'reset'=>1, 'right'=>1, 'row'=>1, 'rowgroup'=>1, 'rows'=>1, 'rtl'=>1, 'submit'=>1, 'text'=>1, 'top'=>1); + static $eAL = array('a'=>1, 'area'=>1, 'bdo'=>1, 'button'=>1, 'col'=>1, 'form'=>1, 'img'=>1, 'input'=>1, 'object'=>1, 'optgroup'=>1, 'option'=>1, 'param'=>1, 'script'=>1, 'select'=>1, 'table'=>1, 'td'=>1, 'tfoot'=>1, 'th'=>1, 'thead'=>1, 'tr'=>1, 'xml:space'=>1); + $lcase = isset($eAL[$e]) ? 1 : 0; +} + +$depTr = 0; +if($C['no_deprecated_attr']){ + // dep attr:applicable ele + static $aND = array('align'=>array('caption'=>1, 'div'=>1, 'h1'=>1, 'h2'=>1, 'h3'=>1, 'h4'=>1, 'h5'=>1, 'h6'=>1, 'hr'=>1, 'img'=>1, 'input'=>1, 'legend'=>1, 'object'=>1, 'p'=>1, 'table'=>1), 'bgcolor'=>array('table'=>1, 'td'=>1, 'th'=>1, 'tr'=>1), 'border'=>array('img'=>1, 'object'=>1), 'bordercolor'=>array('table'=>1, 'td'=>1, 'tr'=>1), 'clear'=>array('br'=>1), 'compact'=>array('dl'=>1, 'ol'=>1, 'ul'=>1), 'height'=>array('td'=>1, 'th'=>1), 'hspace'=>array('img'=>1, 'object'=>1), 'language'=>array('script'=>1), 'name'=>array('a'=>1, 'form'=>1, 'iframe'=>1, 'img'=>1, 'map'=>1), 'noshade'=>array('hr'=>1), 'nowrap'=>array('td'=>1, 'th'=>1), 'size'=>array('hr'=>1), 'start'=>array('ol'=>1), 'type'=>array('li'=>1, 'ol'=>1, 'ul'=>1), 'value'=>array('li'=>1), 'vspace'=>array('img'=>1, 'object'=>1), 'width'=>array('hr'=>1, 'pre'=>1, 'td'=>1, 'th'=>1)); + static $eAD = array('a'=>1, 'br'=>1, 'caption'=>1, 'div'=>1, 'dl'=>1, 'form'=>1, 'h1'=>1, 'h2'=>1, 'h3'=>1, 'h4'=>1, 'h5'=>1, 'h6'=>1, 'hr'=>1, 'iframe'=>1, 'img'=>1, 'input'=>1, 'legend'=>1, 'li'=>1, 'map'=>1, 'object'=>1, 'ol'=>1, 'p'=>1, 'pre'=>1, 'script'=>1, 'table'=>1, 'td'=>1, 'th'=>1, 'tr'=>1, 'ul'=>1); + $depTr = isset($eAD[$e]) ? 1 : 0; +} + +// attr name-vals +if(strpos($a, "\x01") !== false){$a = preg_replace('`\x01[^\x01]*\x01`', '', $a);} // No comment/CDATA sec +$mode = 0; $a = trim($a, ' /'); $aA = array(); +while(strlen($a)){ + $w = 0; + switch($mode){ + case 0: // Name + if(preg_match('`^[a-zA-Z][\-a-zA-Z:]+`', $a, $m)){ + $nm = strtolower($m[0]); + $w = $mode = 1; $a = ltrim(substr_replace($a, '', 0, strlen($m[0]))); + } + break; case 1: + if($a[0] == '='){ // = + $w = 1; $mode = 2; $a = ltrim($a, '= '); + }else{ // No val + $w = 1; $mode = 0; $a = ltrim($a); + $aA[$nm] = ''; + } + break; case 2: // Val + if(preg_match('`^"[^"]*"`', $a, $m) or preg_match("`^'[^']*'`", $a, $m) or preg_match("`^\s*[^\s\"']+`", $a, $m)){ + $m = $m[0]; $w = 1; $mode = 0; $a = ltrim(substr_replace($a, '', 0, strlen($m))); + $aA[$nm] = trim(($m[0] == '"' or $m[0] == '\'') ? substr($m, 1, -1) : $m); + } + break; + } + if($w == 0){ // Parse errs, deal with space, " & ' + $a = preg_replace('`^(?:"[^"]*("|$)|\'[^\']*(\'|$)|\S)*\s*`', '', $a); + $mode = 0; + } +} +if($mode == 1){$aA[$nm] = '';} + +// clean attrs +global $S; +$rl = isset($S[$e]) ? $S[$e] : array(); +$a = array(); $nfr = 0; +foreach($aA as $k=>$v){ + if(((isset($C['deny_attribute']['*']) ? isset($C['deny_attribute'][$k]) : !isset($C['deny_attribute'][$k])) or isset($rl[$k])) && ((!isset($rl['n'][$k]) && !isset($rl['n']['*'])) or isset($rl[$k])) && (isset($aN[$k][$e]) or (isset($aNU[$k]) && !isset($aNU[$k][$e])))){ + if(isset($aNE[$k])){$v = $k;} + elseif(!empty($lcase) && (($e != 'button' or $e != 'input') or $k == 'type')){ // Rather loose but ?not cause issues + $v = (isset($aNL[($v2 = strtolower($v))])) ? $v2 : $v; + } + if($k == 'style' && !$C['style_pass']){ + if(false !== strpos($v, '&#')){ + static $sC = array(' '=>' ', ' '=>' ', 'E'=>'e', 'E'=>'e', 'e'=>'e', 'e'=>'e', 'X'=>'x', 'X'=>'x', 'x'=>'x', 'x'=>'x', 'P'=>'p', 'P'=>'p', 'p'=>'p', 'p'=>'p', 'S'=>'s', 'S'=>'s', 's'=>'s', 's'=>'s', 'I'=>'i', 'I'=>'i', 'i'=>'i', 'i'=>'i', 'O'=>'o', 'O'=>'o', 'o'=>'o', 'o'=>'o', 'N'=>'n', 'N'=>'n', 'n'=>'n', 'n'=>'n', 'U'=>'u', 'U'=>'u', 'u'=>'u', 'u'=>'u', 'R'=>'r', 'R'=>'r', 'r'=>'r', 'r'=>'r', 'L'=>'l', 'L'=>'l', 'l'=>'l', 'l'=>'l', '('=>'(', '('=>'(', ')'=>')', ')'=>')', ' '=>':', ' '=>':', '"'=>'"', '"'=>'"', '''=>"'", '''=>"'", '/'=>'/', '/'=>'/', '*'=>'*', '*'=>'*', '\'=>'\\', '\'=>'\\'); + $v = strtr($v, $sC); + } + $v = preg_replace_callback('`(url(?:\()(?: )*(?:\'|"|&(?:quot|apos);)?)(.+?)((?:\'|"|&(?:quot|apos);)?(?: )*(?:\)))`iS', 'hl_prot', $v); + $v = !$C['css_expression'] ? preg_replace('`expression`i', ' ', preg_replace('`\\\\\S|(/|(%2f))(\*|(%2a))`i', ' ', $v)) : $v; + }elseif(isset($aNP[$k]) or strpos($k, 'src') !== false or $k[0] == 'o'){ + $v = str_replace("\xad", ' ', (strpos($v, '&') !== false ? str_replace(array('­', '­', '­'), ' ', $v) : $v)); + $v = hl_prot($v, $k); + if($k == 'href'){ // X-spam + if($C['anti_mail_spam'] && strpos($v, 'mailto:') === 0){ + $v = str_replace('@', htmlspecialchars($C['anti_mail_spam']), $v); + }elseif($C['anti_link_spam']){ + $r1 = $C['anti_link_spam'][1]; + if(!empty($r1) && preg_match($r1, $v)){continue;} + $r0 = $C['anti_link_spam'][0]; + if(!empty($r0) && preg_match($r0, $v)){ + if(isset($a['rel'])){ + if(!preg_match('`\bnofollow\b`i', $a['rel'])){$a['rel'] .= ' nofollow';} + }elseif(isset($aA['rel'])){ + if(!preg_match('`\bnofollow\b`i', $aA['rel'])){$nfr = 1;} + }else{$a['rel'] = 'nofollow';} + } + } + } + } + if(isset($rl[$k]) && is_array($rl[$k]) && ($v = hl_attrval($v, $rl[$k])) === 0){continue;} + $a[$k] = str_replace('"', '"', $v); + } +} +if($nfr){$a['rel'] = isset($a['rel']) ? $a['rel']. ' nofollow' : 'nofollow';} + +// rqd attr +static $eAR = array('area'=>array('alt'=>'area'), 'bdo'=>array('dir'=>'ltr'), 'form'=>array('action'=>''), 'img'=>array('src'=>'', 'alt'=>'image'), 'map'=>array('name'=>''), 'optgroup'=>array('label'=>''), 'param'=>array('name'=>''), 'script'=>array('type'=>'text/javascript'), 'textarea'=>array('rows'=>'10', 'cols'=>'50')); +if(isset($eAR[$e])){ + foreach($eAR[$e] as $k=>$v){ + if(!isset($a[$k])){$a[$k] = isset($v[0]) ? $v : $k;} + } +} + +// depr attrs +if($depTr){ + $c = array(); + foreach($a as $k=>$v){ + if($k == 'style' or !isset($aND[$k][$e])){continue;} + if($k == 'align'){ + unset($a['align']); + if($e == 'img' && ($v == 'left' or $v == 'right')){$c[] = 'float: '. $v;} + elseif(($e == 'div' or $e == 'table') && $v == 'center'){$c[] = 'margin: auto';} + else{$c[] = 'text-align: '. $v;} + }elseif($k == 'bgcolor'){ + unset($a['bgcolor']); + $c[] = 'background-color: '. $v; + }elseif($k == 'border'){ + unset($a['border']); $c[] = "border: {$v}px"; + }elseif($k == 'bordercolor'){ + unset($a['bordercolor']); $c[] = 'border-color: '. $v; + }elseif($k == 'clear'){ + unset($a['clear']); $c[] = 'clear: '. ($v != 'all' ? $v : 'both'); + }elseif($k == 'compact'){ + unset($a['compact']); $c[] = 'font-size: 85%'; + }elseif($k == 'height' or $k == 'width'){ + unset($a[$k]); $c[] = $k. ': '. ($v[0] != '*' ? $v. (ctype_digit($v) ? 'px' : '') : 'auto'); + }elseif($k == 'hspace'){ + unset($a['hspace']); $c[] = "margin-left: {$v}px; margin-right: {$v}px"; + }elseif($k == 'language' && !isset($a['type'])){ + unset($a['language']); + $a['type'] = 'text/'. strtolower($v); + }elseif($k == 'name'){ + if($C['no_deprecated_attr'] == 2 or ($e != 'a' && $e != 'map')){unset($a['name']);} + if(!isset($a['id']) && preg_match('`[a-zA-Z][a-zA-Z\d.:_\-]*`', $v)){$a['id'] = $v;} + }elseif($k == 'noshade'){ + unset($a['noshade']); $c[] = 'border-style: none; border: 0; background-color: gray; color: gray'; + }elseif($k == 'nowrap'){ + unset($a['nowrap']); $c[] = 'white-space: nowrap'; + }elseif($k == 'size'){ + unset($a['size']); $c[] = 'size: '. $v. 'px'; + }elseif($k == 'start' or $k == 'value'){ + unset($a[$k]); + }elseif($k == 'type'){ + unset($a['type']); + static $ol_type = array('i'=>'lower-roman', 'I'=>'upper-roman', 'a'=>'lower-latin', 'A'=>'upper-latin', '1'=>'decimal'); + $c[] = 'list-style-type: '. (isset($ol_type[$v]) ? $ol_type[$v] : 'decimal'); + }elseif($k == 'vspace'){ + unset($a['vspace']); $c[] = "margin-top: {$v}px; margin-bottom: {$v}px"; + } + } + if(count($c)){ + $c = implode('; ', $c); + $a['style'] = isset($a['style']) ? rtrim($a['style'], ' ;'). '; '. $c. ';': $c. ';'; + } +} +// unique ID +if($C['unique_ids'] && isset($a['id'])){ + if(!preg_match('`^[A-Za-z][A-Za-z0-9_\-.:]*$`', ($id = $a['id'])) or (isset($GLOBALS['hl_Ids'][$id]) && $C['unique_ids'] == 1)){unset($a['id']); + }else{ + while(isset($GLOBALS['hl_Ids'][$id])){$id = $C['unique_ids']. $id;} + $GLOBALS['hl_Ids'][($a['id'] = $id)] = 1; + } +} +// xml:lang +if($C['xml:lang'] && isset($a['lang'])){ + $a['xml:lang'] = isset($a['xml:lang']) ? $a['xml:lang'] : $a['lang']; + if($C['xml:lang'] == 2){unset($a['lang']);} +} +// for transformed tag +if(!empty($trt)){ + $a['style'] = isset($a['style']) ? rtrim($a['style'], ' ;'). '; '. $trt : $trt; +} +// return with empty ele / +if(empty($C['hook_tag'])){ + $aA = ''; + foreach($a as $k=>$v){$aA .= " {$k}=\"{$v}\"";} + return "<{$e}{$aA}". (isset($eE[$e]) ? ' /' : ''). '>'; +} +else{return $C['hook_tag']($e, $a);} +// eof +} + +function hl_tag2(&$e, &$a, $t=1){ +// transform tag +if($e == 'center'){$e = 'div'; return 'text-align: center;';} +if($e == 'dir' or $e == 'menu'){$e = 'ul'; return '';} +if($e == 's' or $e == 'strike'){$e = 'span'; return 'text-decoration: line-through;';} +if($e == 'u'){$e = 'span'; return 'text-decoration: underline;';} +static $fs = array('0'=>'xx-small', '1'=>'xx-small', '2'=>'small', '3'=>'medium', '4'=>'large', '5'=>'x-large', '6'=>'xx-large', '7'=>'300%', '-1'=>'smaller', '-2'=>'60%', '+1'=>'larger', '+2'=>'150%', '+3'=>'200%', '+4'=>'300%'); +if($e == 'font'){ + $a2 = ''; + if(preg_match('`face\s*=\s*(\'|")([^=]+?)\\1`i', $a, $m) or preg_match('`face\s*=\s*([^"])(\S+)`i', $a, $m)){ + $a2 .= ' font-family: '. str_replace('"', '\'', trim($m[2])). ';'; + } + if(preg_match('`color\s*=\s*(\'|")?(.+?)(\\1|\s|$)`i', $a, $m)){ + $a2 .= ' color: '. trim($m[2]). ';'; + } + if(preg_match('`size\s*=\s*(\'|")?(.+?)(\\1|\s|$)`i', $a, $m) && isset($fs[($m = trim($m[2]))])){ + $a2 .= ' font-size: '. $fs[$m]. ';'; + } + $e = 'span'; return ltrim($a2); +} +if($t == 2){$e = 0; return 0;} +return ''; +// eof +} + +function hl_tidy($t, $w, $p){ +// Tidy/compact HTM +if(strpos(' pre,script,textarea', "$p,")){return $t;} +$t = str_replace(' ]*(?)\s+`', '`\s+`', '`(<\w[^>]*(?) `'), array(' $1', ' ', '$1'), preg_replace_callback(array('`(<(!\[CDATA\[))(.+?)(\]\]>)`sm', '`(<(!--))(.+?)(-->)`sm', '`(<(pre|script|textarea)[^>]*?>)(.+?)()`sm'), create_function('$m', 'return $m[1]. str_replace(array("<", ">", "\n", "\r", "\t", " "), array("\x01", "\x02", "\x03", "\x04", "\x05", "\x07"), $m[3]). $m[4];'), $t))); +if(($w = strtolower($w)) == -1){ + return str_replace(array("\x01", "\x02", "\x03", "\x04", "\x05", "\x07"), array('<', '>', "\n", "\r", "\t", ' '), $t); +} +$s = strpos(" $w", 't') ? "\t" : ' '; +$s = preg_match('`\d`', $w, $m) ? str_repeat($s, $m[0]) : str_repeat($s, ($s == "\t" ? 1 : 2)); +$n = preg_match('`[ts]([1-9])`', $w, $m) ? $m[1] : 0; +$a = array('br'=>1); +$b = array('button'=>1, 'input'=>1, 'option'=>1); +$c = array('caption'=>1, 'dd'=>1, 'dt'=>1, 'h1'=>1, 'h2'=>1, 'h3'=>1, 'h4'=>1, 'h5'=>1, 'h6'=>1, 'isindex'=>1, 'label'=>1, 'legend'=>1, 'li'=>1, 'object'=>1, 'p'=>1, 'pre'=>1, 'td'=>1, 'textarea'=>1, 'th'=>1); +$d = array('address'=>1, 'blockquote'=>1, 'center'=>1, 'colgroup'=>1, 'dir'=>1, 'div'=>1, 'dl'=>1, 'fieldset'=>1, 'form'=>1, 'hr'=>1, 'iframe'=>1, 'map'=>1, 'menu'=>1, 'noscript'=>1, 'ol'=>1, 'optgroup'=>1, 'rbc'=>1, 'rtc'=>1, 'ruby'=>1, 'script'=>1, 'select'=>1, 'table'=>1, 'tfoot'=>1, 'thead'=>1, 'tr'=>1, 'ul'=>1); +ob_start(); +if(isset($d[$p])){echo str_repeat($s, ++$n);} +$t = explode('<', $t); +echo ltrim(array_shift($t)); +for($i=-1, $j=count($t); ++$i<$j;){ + $r = ''; list($e, $r) = explode('>', $t[$i]); + $x = $e[0] == '/' ? 0 : (substr($e, -1) == '/' ? 1 : ($e[0] != '!' ? 2 : -1)); + $y = !$x ? ltrim($e, '/') : ($x > 0 ? substr($e, 0, strcspn($e, ' ')) : 0); + $e = "<$e>"; + if(isset($d[$y])){ + if(!$x){echo "\n", str_repeat($s, --$n), "$e\n", str_repeat($s, $n);} + else{echo "\n", str_repeat($s, $n), "$e\n", str_repeat($s, ($x != 1 ? ++$n : $n));} + echo ltrim($r); continue; + } + $f = "\n". str_repeat($s, $n); + if(isset($c[$y])){ + if(!$x){echo $e, $f, ltrim($r);} + else{echo $f, $e, $r;} + }elseif(isset($b[$y])){echo $f, $e, $r; + }elseif(isset($a[$y])){echo $e, $f, ltrim($r); + }elseif(!$y){echo $f, $e, $f, ltrim($r); + }else{echo $e, $r;} +} +$t = preg_replace('`[\n]\s*?[\n]+`', "\n", ob_get_contents()); +ob_end_clean(); +if(($l = strpos(" $w", 'r') ? (strpos(" $w", 'n') ? "\r\n" : "\r") : 0)){ + $t = str_replace("\n", $l, $t); +} +return str_replace(array("\x01", "\x02", "\x03", "\x04", "\x05", "\x07"), array('<', '>', "\n", "\r", "\t", ' '), $t); +// eof +} + +function hl_version(){ +// rel +return '1.1.10'; +// eof +} + +function kses($t, $h, $p=array('http', 'https', 'ftp', 'news', 'nntp', 'telnet', 'gopher', 'mailto')){ +// kses compat +foreach($h as $k=>$v){ + $h[$k]['n']['*'] = 1; +} +$C['cdata'] = $C['comment'] = $C['make_tag_strict'] = $C['no_deprecated_attr'] = $C['unique_ids'] = 0; +$C['keep_bad'] = 1; +$C['elements'] = count($h) ? strtolower(implode(',', array_keys($h))) : '-*'; +$C['hook'] = 'kses_hook'; +$C['schemes'] = '*:'. implode(',', $p); +return htmLawed($t, $C, $h); +// eof +} + +function kses_hook($t, &$C, &$S){ +// kses compat +return $t; +// eof +} \ No newline at end of file diff --git a/qa-include/qa-image.php b/qa-include/qa-image.php new file mode 100644 index 000000000..71c1522ed --- /dev/null +++ b/qa-include/qa-image.php @@ -0,0 +1,100 @@ +0) + $content=qa_image_constrain_data($blob['content'], $width, $height, $size); + else + $content=$blob['content']; + + if (isset($content)) { + header('Content-Type: image/jpeg'); + echo $content; + + if (strlen($content) && ($size>0)) { + $cachesizes=qa_get_options(array('avatar_profile_size', 'avatar_users_size', 'avatar_q_page_q_size', 'avatar_q_page_a_size', 'avatar_q_page_c_size', 'avatar_q_list_size')); + // to prevent cache being filled with inappropriate sizes + + if (array_search($size, $cachesizes)) + qa_db_cache_set($cachetype, $blobid, $content); + } + } + } + } + + qa_db_disconnect(); + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-index.php b/qa-include/qa-index.php new file mode 100644 index 000000000..196c24acf --- /dev/null +++ b/qa-include/qa-index.php @@ -0,0 +1,180 @@ + $requestpart) // remove any blank parts + if (!strlen($requestpart)) + unset($requestparts[$part]); + + reset($requestparts); + $key=key($requestparts); + + $replacement=array_search(@$requestparts[$key], qa_get_request_map()); + if ($replacement!==false) + $requestparts[$key]=$replacement; + + qa_set_request( + implode('/', $requestparts), + ($relativedepth>1) ? str_repeat('../', $relativedepth-1) : './', + $urlformat + ); + } + + qa_index_set_request(); + + + // Branch off to appropriate file for further handling + + $requestlower=strtolower(qa_request()); + + if ($requestlower=='install') + require QA_INCLUDE_DIR.'qa-install.php'; + + elseif ($requestlower==('url/test/'.QA_URL_TEST_STRING)) + require QA_INCLUDE_DIR.'qa-url-test.php'; + + else { + + // Enable gzip compression for output (needs to come early) + + if (QA_HTML_COMPRESSION) // on by default + if (substr($requestlower, 0, 6)!='admin/') // not for admin pages since some of these contain lengthy processes + if (extension_loaded('zlib') && !headers_sent()) + ob_start('ob_gzhandler'); + + if (substr($requestlower, 0, 5)=='feed/') + require QA_INCLUDE_DIR.'qa-feed.php'; + else + require QA_INCLUDE_DIR.'qa-page.php'; + } + } + + qa_report_process_stage('shutdown'); + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-install.php b/qa-include/qa-install.php new file mode 100644 index 000000000..066309f7f --- /dev/null +++ b/qa-include/qa-install.php @@ -0,0 +1,325 @@ + + + + + + + +init_queries(qa_db_list_tables_lc()); + + if (!empty($queries)) { + if (!is_array($queries)) + $queries=array($queries); + + foreach ($queries as $query) + qa_db_upgrade_query($query); + } + + $success.='The '.$modulename.' '.$moduletype.' module has completed database initialization.'; + } + + if (qa_clicked('super')) { + require_once QA_INCLUDE_DIR.'qa-db-users.php'; + require_once QA_INCLUDE_DIR.'qa-app-users-edit.php'; + + $inemail=qa_post_text('email'); + $inpassword=qa_post_text('password'); + $inhandle=qa_post_text('handle'); + + $fielderrors=array_merge( + qa_handle_email_filter($inhandle, $inemail), + qa_password_validate($inpassword) + ); + + if (empty($fielderrors)) { + require_once QA_INCLUDE_DIR.'qa-app-users.php'; + + $userid=qa_create_new_user($inemail, $inpassword, $inhandle, QA_USER_LEVEL_SUPER); + qa_set_logged_in_user($userid, $inhandle); + + qa_set_option('feedback_email', $inemail); + + $success.="Congratulations - Your Question2Answer site is ready to go!\n\nYou are logged in as the super administrator and can start changing settings.\n\nThank you for installing Question2Answer."; + } + } + } + + if (is_resource(qa_db_connection(false)) && !@$pass_failure_from_install) { + $check=qa_db_check_tables(); // see where the database is at + + switch ($check) { + case 'none': + if (@$pass_failure_errno==1146) // don't show error if we're in installation process + $errorhtml=''; + + $errorhtml.='Welcome to Question2Answer. It\'s time to set up your database!'; + + if (QA_FINAL_EXTERNAL_USERS) { + if (defined('QA_FINAL_WORDPRESS_INTEGRATE_PATH')) + $errorhtml.="\n\nWhen you click below, your Question2Answer site will be set up to integrate with the users of your WordPress site ".qa_html(get_option('blogname')).". Please consult the online documentation for more information."; + else + $errorhtml.="\n\nWhen you click below, your Question2Answer site will be set up to integrate with your existing user database and management. Users will be referenced with database column type ".qa_html(qa_get_mysql_user_column_type()).". Please consult the online documentation for more information."; + + $buttons=array('create' => 'Create Database'); + } else { + $errorhtml.="\n\nWhen you click below, your Question2Answer database will be set up to manage user identities and logins internally.\n\nIf you want to offer a single sign-on for an existing user base or website, please consult the online documentation before proceeding."; + $buttons=array('create' => 'Create Database including User Management'); + } + break; + + case 'old-version': + if (!@$pass_failure_from_install) + $errorhtml=''; // don't show error if we need to upgrade + + $errorhtml.='Your Question2Answer database needs to be upgraded for this version of the software.'; // don't show error before this + $buttons=array('upgrade' => 'Upgrade Database'); + break; + + case 'non-users-missing': + $errorhtml='This Question2Answer site is sharing its users with another Q2A site, but it needs some additional database tables for its own content. Click below to create them.'; + $buttons=array('nonuser' => 'Create Tables'); + break; + + case 'table-missing': + $errorhtml.='One or more tables are missing from your Question2Answer database.'; + $buttons=array('repair' => 'Repair Database'); + break; + + case 'column-missing': + $errorhtml.='One or more Question2Answer database tables are missing a column.'; + $buttons=array('repair' => 'Repair Database'); + break; + + default: + require_once QA_INCLUDE_DIR.'qa-db-admin.php'; + + if ( (!QA_FINAL_EXTERNAL_USERS) && (qa_db_count_users()==0) ) { + $errorhtml.="There are currently no users in the Question2Answer database.\n\nPlease enter your details below to create the super administrator:"; + $fields=array('handle' => 'Username:', 'password' => 'Password:', 'email' => 'Email address:'); + $buttons=array('super' => 'Create Super Administrator'); + + } else { + $tables=qa_db_list_tables_lc(); + + $moduletypes=qa_list_module_types(); + + foreach ($moduletypes as $moduletype) { + $modules=qa_load_modules_with($moduletype, 'init_queries'); + + foreach ($modules as $modulename => $module) { + $queries=$module->init_queries($tables); + if (!empty($queries)) { // also allows single query to be returned + $errorhtml.=strtr(qa_lang_html('admin/module_x_database_init'), array( + '^1' => qa_html($modulename), + '^2' => qa_html($moduletype), + '^3' => '', + '^4' => '', + )); + + $buttons=array('module' => 'Initialize Database'); + + $hidden['moduletype']=$moduletype; + $hidden['modulename']=$modulename; + break; + } + } + } + } + break; + } + } + + if (empty($errorhtml)) { + if (empty($success)) + $success='Your Question2Answer database has been checked with no problems.'; + + $suggest='Go to admin center'; + } + +?> + + + +'.nl2br(qa_html($success)).'

'; // green + + if (strlen($errorhtml)) + echo '

'.nl2br($errorhtml).'

'; // red + + if (strlen($suggest)) + echo '

'.$suggest.'

'; + + +// Very simple general form display logic (we don't use theme since it depends on tons of DB options) + + if (count($fields)) { + echo '

'; + + foreach ($fields as $name => $prompt) { + echo ''; + if (isset($fielderrors[$name])) + echo ''; + echo ''; + } + + echo '
'.qa_html($prompt).''.qa_html($fielderrors[$name]).'
'; + } + + foreach ($buttons as $name => $value) + echo ''; + + foreach ($hidden as $name => $value) + echo ''; + + qa_db_disconnect(); +?> + + + + \ No newline at end of file diff --git a/qa-include/qa-lang-admin.php b/qa-include/qa-lang-admin.php new file mode 100644 index 000000000..e2b1e535d --- /dev/null +++ b/qa-include/qa-lang-admin.php @@ -0,0 +1,263 @@ + 'Currently active widgets:', + 'add_category_button' => 'Add Category', + 'add_field_button' => 'Add Field', + 'add_link_button' => 'Add Link', + 'add_link_link' => ' - ^1add link^2', + 'add_new_field' => 'Add new field', + 'add_new_title' => 'Add new title', + 'add_page_button' => 'Add Page', + 'add_title_button' => 'Add Title', + 'add_widget_button' => 'Add Widget', + 'add_widget_link' => ' - ^1add widget^2', + 'admin_title' => 'Administration center', + 'after_footer' => 'After links in footer', + 'after_main_menu' => 'After tabs at top', + 'after_x_tab' => 'After "^" tab', + 'after_x' => 'After "^"', + 'moderate_title' => 'Moderate', + 'basic_editor' => 'Basic Editor', + 'before_main_menu' => 'Before tabs at top', + 'block_ips_note' => 'Use a hyphen for ranges or * to match any number. Examples: 192.168.0.4 , 192.168.0.0-192.168.0.31 , 192.168.0.*', + 'block_words_note' => 'Use a * to match any letters. Examples: doh (will only match exact word doh) , doh* (will match doh or dohno) , do*h (will match doh, dooh, dough).', + 'cancel_mailing_button' => 'Cancel Mailing', + 'categories_introduction' => 'To get started with categories, click the \'Add Category\' button.', + 'categories_not_shown' => 'Some questions have categories which will not be displayed.', + 'categories_title' => 'Categories', + 'categories' => 'Categories', + 'category_add_sub' => 'add sub-category', + 'category_added' => 'Category added', + 'category_already_used' => 'This is already being used by a category', + 'category_default_slug' => 'category-^', + 'category_description' => 'Optional category description:', + 'category_max_depth_x' => 'Some options may be hidden to prevent a category going deeper than ^ levels.', + 'category_move_parent' => 'move to different parent', + 'category_name_first' => 'Name of first category:', + 'category_name' => 'Category name:', + 'category_no_add_subs_x' => 'This category cannot have sub-categories because it is already ^ levels down.', + 'category_no_delete_subs' => 'This category cannot be deleted because it has a sub-category.', + 'category_no_sub_error' => '^q question/s in this category have no sub-category - ^1set sub-category^2', + 'category_no_sub_to' => 'Move questions in ^ with no sub-category to:', + 'category_no_subs' => 'None', + 'category_none_error' => '^q question/s currently have no category - ^1set category^2', + 'category_none_to' => 'Move questions with no category to:', + 'category_parent' => 'Parent category:', + 'category_saved' => 'Category saved', + 'category_slug' => 'Category slug - URL fragment:', + 'category_subs' => 'Sub-categories:', + 'category_top_level' => 'No parent (top level)', + 'characters' => 'characters', + 'check_language_suffix' => ' - ^1check language files^2', + 'click_name_edit' => 'Custom pages or links:', + 'database_cleanup' => 'Database clean-up operations', + 'delete_category_reassign' => 'Delete this category and reassign its questions to:', + 'delete_category' => 'Delete this category', + 'delete_field' => 'Delete this field', + 'delete_hidden_complete' => 'All hidden posts without dependents have been deleted', + 'delete_hidden_note' => ' - all hidden questions, answer and comments without dependents', + 'delete_hidden' => 'Delete hidden posts', + 'delete_link' => 'Delete this link', + 'delete_page' => 'Delete this page', + 'delete_stop' => 'Stop deleting', + 'delete_title' => 'Delete this title', + 'delete_widget_position' => 'Delete this widget from this position', + 'edit_custom_page' => 'Edit custom page', + 'edit_field' => ' - ^1edit field^2', + 'edit_link' => ' - ^1edit link^2', + 'edit_page' => ' - ^1edit page^2', + 'edit_title' => ' - ^1edit title^2', + 'emails_per_minute' => 'emails per minute', + 'emails_title' => 'Emails', + 'feed_link_example' => 'Example feed', + 'feed_link' => 'Feed', + 'feeds_title' => 'RSS feeds', + 'field_link_url' => 'Linked URL', + 'field_multi_line' => 'Multiple lines of text', + 'field_name' => 'Field name:', + 'field_single_line' => 'Single line of text', + 'field_type' => 'Content type:', + 'first' => 'First', + 'flagged_title' => 'Flagged', + 'from_anon' => 'From anonymous:', + 'from_users' => 'From users:', + 'general_title' => 'General', + 'hidden_answers_deleted' => 'Deleted ^1 of ^2 hidden answers without dependents...', + 'hidden_comments_deleted' => 'Deleted ^1 of ^2 hidden comments...', + 'hidden_questions_deleted' => 'Deleted ^1 of ^2 hidden questions without dependents...', + 'hotness_factors' => 'Relative importance for question hotness:', + 'hidden_title' => 'Hidden', + 'installed_plugins' => 'Installed plugins:', + 'ip_address_pages' => 'IP address pages', + 'layout_title' => 'Layout', + 'link_name' => 'Text of link:', + 'link_new_window' => 'Open link in a new window', + 'link_url' => 'URL of link - absolute or relative to Q2A root:', + 'lists_title' => 'Lists', + 'mailing_complete' => 'The mailing is complete', + 'mailing_explanation' => 'Users will be able to unsubscribe on their account page.', + 'mailing_progress' => 'Mailing completed for ^1 of ^2 users...', + 'mailing_title' => 'Mailing', + 'mailing_unsubscribe' => 'An unsubscribe link will be added at the bottom of every message.', + 'maintenance_admin_only' => 'Your site is in ^1maintenance^2 and is currently inaccessible to regular users.', + 'maximum_x' => ' (max ^)', + 'module_x_database_init' => 'The ^1 ^2 module requires some ^3database initialization^4.', + 'most_flagged_title' => 'Flagged content', + 'mysql_version' => 'MySQL version:', + 'nav_links_explanation' => 'Show navigation links:', + 'nav_qa_is_home' => 'Q&A (links to home page)', + 'neat_urls_note' => ' (requires ^1htaccess^2 file)', + 'no_approve_found' => 'No content is waiting for approval', + 'no_classification' => 'None', + 'no_flagged_found' => 'No flagged content found', + 'no_hidden_found' => 'No hidden content found', + 'no_image_gd' => 'The installed version of PHP was compiled without GD image support, so users cannot upload their avatars directly.', + 'no_link' => 'No link', + 'no_multibyte' => 'The installed version of PHP was compiled without multibyte string support. Searching will be less effective for non-Roman characters.', + 'no_plugin_options' => 'None of your installed plugins have options to display.', + 'no_privileges' => 'Only administrators may access this page.', + 'not_logged_in' => 'Please ^1log in^2 as the administrator to access this page.', + 'opposite_main_menu' => 'Far end of tabs at top', + 'options_reset' => 'Options reset', + 'options_saved' => 'Options saved', + 'options' => 'options', + 'page_already_used' => 'This is already being used by a page', + 'page_content_html' => 'Content to display in page - HTML allowed:', + 'page_default_slug' => 'page-^', + 'page_heading' => 'Heading to display at top of page:', + 'page_name' => 'Name of page (also used for tab or link):', + 'page_slug' => 'Page slug (URL fragment):', + 'pages_explanation' => 'Click the \'Add Page\' button to add custom content to your Q2A site, or \'Add Link\' to link to any other web page.', + 'pages_title' => 'Pages', + 'pause_mailing_button' => 'Pause Mailing', + 'per_ip_hour' => 'per IP/hour', + 'per_user_hour' => 'per user/hour', + 'permissions_title' => 'Permissions', + 'permit_to_view' => 'Visible for:', + 'php_version' => 'PHP version:', + 'pixels' => 'pixels', + 'plugin_module' => ' (plugin module: ^)', + 'plugin_pages_explanation' => 'Pages available via plugins:', + 'plugins_title' => 'Plugins', + 'points_defaults_shown' => 'Defaults shown below but NOT YET APPLIED:', + 'points_required' => 'Points required to receive title:', + 'points_title' => 'Points', + 'points' => 'points', + 'position' => 'Position:', + 'posting_title' => 'Posting', + 'profile_fields' => 'Extra fields on user profile:', + 'q2a_build_date' => 'Build date:', + 'q2a_db_size' => 'Database size:', + 'q2a_db_version' => 'Q2A database version:', + 'q2a_latest_version' => 'Latest version:', + 'q2a_version' => 'Question2Answer version:', + 'question_lists' => 'Question lists', + 'question_pages' => 'Question pages', + 'recalc_categories_backpaths' => 'Recalculating URL paths for ^1 of ^2 categories...', + 'recalc_categories_complete' => 'All categories were successfully recalculated.', + 'recalc_categories_note' => ' - for post categories and category counts', + 'recalc_categories_recounting' => 'Recounting questions for ^1 of ^2 categories...', + 'recalc_categories_updated' => 'Recalculated for ^1 of ^2 posts...', + 'recalc_categories' => 'Recalculate categories', + 'recalc_points_complete' => 'All user points were successfully recalculated.', + 'recalc_points_note' => ' - for user ranking and points displays', + 'recalc_points_recalced' => 'Recalculated for ^1 of ^2 users...', + 'recalc_points_usercount' => 'Estimating total number of users...', + 'recalc_points' => 'Recalculate user points', + 'recalc_posts_count' => 'Getting total number of questions, answers and comments...', + 'recalc_stop' => 'Stop recalculating', + 'recent_approve_title' => 'Recent content waiting for approval', + 'recent_hidden_title' => 'Recent hidden content', + 'recount_posts_as_recounted' => 'Recounted answers and hotness for ^1 of ^2 posts...', + 'recount_posts_complete' => 'All posts were successfully recounted.', + 'recount_posts_note' => ' - the number of answers, votes, flags and hotness for each post', + 'recount_posts_stop' => 'Stop recounting', + 'recount_posts_votes_recounted' => 'Recounted votes and flags for ^1 of ^2 posts...', + 'recount_posts' => 'Recount posts', + 'refill_events_complete' => 'All events streams were successfully refilled', + 'refill_events_note' => ' - for each user\'s list of updates', + 'refill_events_refilled' => 'Refilled for ^1 of ^2 questions...', + 'refill_events' => 'Refill event streams', + 'reindex_posts_complete' => 'All posts were successfully reindexed.', + 'reindex_posts_note' => ' - for searching and related question suggestions', + 'reindex_posts_reindexed' => 'Reindexed ^1 of ^2 posts...', + 'reindex_posts_stop' => 'Stop reindexing', + 'reindex_posts_wordcounted' => 'Recounted ^1 of ^2 words...', + 'reindex_posts' => 'Reindex posts', + 'requires_q2a_version' => 'Disabled - requires Question2Answer ^ or later', + 'reset_options_button' => 'Reset to Defaults', + 'resume_mailing_button' => 'Resume Mailing', + 'save_options_button' => 'Save Options', + 'save_recalc_button' => 'Save and Recalculate', + 'send_test_button' => 'Send Test to Me', + 'show_defaults_button' => 'Show Defaults', + 'slug_bad_chars' => 'The slug may not contain these characters: ^', + 'slug_reserved' => 'This slug is reserved for use by another page', + 'spam_title' => 'Spam', + 'start_mailing_button' => 'Start Mailing', + 'stats_title' => 'Stats', + 'stop_recalc_warning' => 'A database clean-up operation is running. If you close this page now, the operation will be interrupted.', + 'tag_pages' => 'Tag pages', + 'tags_and_categories' => 'Tags and Categories', + 'tags_not_shown' => 'Some questions have tags which will not be displayed.', + 'tags' => 'Tags', + 'test_sent_to_x' => 'The test message was sent to ^', + 'title_already_used' => 'This value is already being used by another title', + 'top_level_categories' => 'Top level categories:', + 'total_as' => 'Total answers:', + 'total_cs' => 'Total comments:', + 'total_qs' => 'Total questions:', + 'unnamed_plugin' => 'Unnamed Plugin', + 'upgrade_db' => 'Your Question2Answer database needs to be ^1upgraded^2 for this version.', + 'url_format_note' => 'Options with the ^ label are working for your site\'s configuration. For best search engine optimization (SEO), use the first ^ option available.', + 'user_pages' => 'User pages', + 'user_title' => 'User title - HTML allowed:', + 'user_titles' => 'User titles based on points:', + 'users_active' => 'Active users:', + 'users_must_have' => 'Users must have', + 'users_posted' => 'Users who posted:', + 'users_registered' => 'Registered users:', + 'users_title' => 'Users', + 'users_voted' => 'Users who voted:', + 'version_get_x' => 'get ^', + 'version_latest_unknown' => 'latest unknown', + 'version_latest' => 'latest', + 'viewing_title' => 'Viewing', + 'widget_all_pages' => 'Show widget in this position on all available pages', + 'widget_global_options' => ' - ^1options^2', + 'widget_name' => 'Name of widget:', + 'widget_no_positions' => 'This widget has already been added to every available position.', + 'widget_not_available' => 'This widget is not available. This could be because the plugin providing the widget is no longer installed.', + 'widget_pages_explanation' => 'Show widget in this position on the following pages:', + 'widgets_explanation' => 'Available widgets:', + ); + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-lang-emails.php b/qa-include/qa-lang-emails.php new file mode 100644 index 000000000..397581fca --- /dev/null +++ b/qa-include/qa-lang-emails.php @@ -0,0 +1,82 @@ + "Your answer on ^site_title has a new comment by ^c_handle:\n\n^open^c_content^close\n\nYour answer was:\n\n^open^c_context^close\n\nYou may respond by adding your own comment:\n\n^url\n\nThank you,\n\n^site_title", + 'a_commented_subject' => 'Your ^site_title answer has a new comment', + + 'a_followed_body' => "Your answer on ^site_title has a new related question by ^q_handle:\n\n^open^q_title^close\n\nYour answer was:\n\n^open^a_content^close\n\nClick below to answer the new question:\n\n^url\n\nThank you,\n\n^site_title", + 'a_followed_subject' => 'Your ^site_title answer has a related question', + + 'a_selected_body' => "Congratulations! Your answer on ^site_title has been selected as the best by ^s_handle:\n\n^open^a_content^close\n\nThe question was:\n\n^open^q_title^close\n\nClick below to see your answer:\n\n^url\n\nThank you,\n\n^site_title", + 'a_selected_subject' => 'Your ^site_title answer has been selected!', + + 'c_commented_body' => "A new comment by ^c_handle has been added after your comment on ^site_title:\n\n^open^c_content^close\n\nThe discussion is following:\n\n^open^c_context^close\n\nYou may respond by adding another comment:\n\n^url\n\nThank you,\n\n^site_title", + 'c_commented_subject' => 'Your ^site_title comment has been added to', + + 'confirm_body' => "Please click below to confirm your email address for ^site_title.\n\n^url\n\nThank you,\n^site_title", + 'confirm_subject' => '^site_title - Email Address Confirmation', + + 'feedback_body' => "Comments:\n^message\n\nName:\n^name\n\nEmail:\n^email\n\nPrevious page:\n^previous\n\nUser:\n^url\n\nIP address:\n^ip\n\nBrowser:\n^browser", + 'feedback_subject' => '^ feedback', + + 'flagged_body' => "A post by ^p_handle has received ^flags:\n\n^open^p_context^close\n\nClick below to see the post:\n\n^url\n\nThank you,\n\n^site_title", + 'flagged_subject' => '^site_title has a flagged post', + + 'moderate_body' => "A post by ^p_handle requires your approval:\n\n^open^p_context^close\n\nClick below to approve or reject the post:\n\n^url\n\nThank you,\n\n^site_title", + 'moderate_subject' => '^site_title moderation', + + 'new_password_body' => "Your new password for ^site_title is below.\n\nPassword: ^password\n\nIt is recommended to change this password immediately after logging in.\n\nThank you,\n^site_title\n^url", + 'new_password_subject' => '^site_title - Your New Password', + + 'private_message_body' => "You have been sent a private message by ^f_handle on ^site_title:\n\n^open^message^close\n\n^moreThank you,\n\n^site_title\n\n\nTo block private messages, visit your account page:\n^a_url", + 'private_message_info' => "More information about ^f_handle:\n\n^url\n\n", + 'private_message_reply' => "Click below to reply to ^f_handle by private message:\n\n^url\n\n", + 'private_message_subject' => 'Message from ^f_handle on ^site_title', + + 'q_answered_body' => "Your question on ^site_title has been answered by ^a_handle:\n\n^open^a_content^close\n\nYour question was:\n\n^open^q_title^close\n\nIf you like this answer, you may select it as the best:\n\n^url\n\nThank you,\n\n^site_title", + 'q_answered_subject' => 'Your ^site_title question was answered', + + 'q_commented_body' => "Your question on ^site_title has a new comment by ^c_handle:\n\n^open^c_content^close\n\nYour question was:\n\n^open^c_context^close\n\nYou may respond by adding your own comment:\n\n^url\n\nThank you,\n\n^site_title", + 'q_commented_subject' => 'Your ^site_title question has a new comment', + + 'q_posted_body' => "A new question has been asked by ^q_handle:\n\n^open^q_title\n\n^q_content^close\n\nClick below to see the question:\n\n^url\n\nThank you,\n\n^site_title", + 'q_posted_subject' => '^site_title has a new question', + + 'reset_body' => "Please click below to reset your password for ^site_title.\n\n^url\n\nAlternatively, enter the code below into the field provided.\n\nCode: ^code\n\nIf you did not ask to reset your password, please ignore this message.\n\nThank you,\n^site_title", + 'reset_subject' => '^site_title - Reset Forgotten Password', + + 'to_handle_prefix' => "^,\n\n", + + 'welcome_body' => "Thank you for registering for ^site_title.\n\n^custom^confirmYour login details are as follows:\n\nEmail: ^email\nPassword: ^password\n\nPlease keep this information safe for future reference.\n\nThank you,\n\n^site_title\n^url", + 'welcome_confirm' => "Please click below to confirm your email address.\n\n^url\n\n", + 'welcome_subject' => 'Welcome to ^site_title!', + ); + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-lang-main.php b/qa-include/qa-lang-main.php new file mode 100644 index 000000000..4a8e94cb8 --- /dev/null +++ b/qa-include/qa-lang-main.php @@ -0,0 +1,226 @@ + '1 answer', + '1_comment' => '1 comment', + '1_day' => '1 day', + '1_disliked' => '1 dislike', + '1_flag' => '1 flag', + '1_hour' => '1 hour', + '1_liked' => '1 like', + '1_minute' => '1 minute', + '1_month' => '1 month', + '1_point' => '1 point', + '1_question' => '1 question', + '1_second' => '1 second', + '1_tag' => '1 tag', + '1_user' => '1 user', + '1_view' => '1 view', + '1_vote' => '1 vote', + '1_week' => '1 week', + '1_year' => '1 year', + 'add_category_x_favorites' => 'Add category ^ to my favorites', + 'add_favorites' => 'Add to my favorites', + 'add_tag_x_favorites' => 'Add tag ^ to my favorites', + 'all_categories' => 'All categories', + 'anonymous' => 'anonymous', + 'answer_edited' => 'answer edited', + 'answer_reshown' => 'answer reshown', + 'answer_selected' => 'answer selected', + 'answered_qs_in_x' => 'Most answered questions in ^', + 'answered_qs_title' => 'Most answered questions', + 'answered' => 'answered', + 'asked_related_q' => 'asked related question', + 'asked' => 'asked', + 'by_x' => 'by ^', + 'cancel_button' => 'Cancel', + 'closed' => 'closed', + 'comment_edited' => 'comment edited', + 'comment_moved' => 'comment moved', + 'comment_reshown' => 'comment reshown', + 'commented' => 'commented', + 'date_day_min_digits' => 1, // 1 or 2 + 'date_format_other_years' => '^month ^day, ^year', + 'date_format_this_year' => '^month ^day', + 'date_month_1' => 'Jan', + 'date_month_2' => 'Feb', + 'date_month_3' => 'Mar', + 'date_month_4' => 'Apr', + 'date_month_5' => 'May', + 'date_month_6' => 'Jun', + 'date_month_7' => 'Jul', + 'date_month_8' => 'Aug', + 'date_month_9' => 'Sep', + 'date_month_10' => 'Oct', + 'date_month_11' => 'Nov', + 'date_month_12' => 'Dec', + 'date_year_digits' => 4, // 2 or 4 + 'edited' => 'edited', + 'field_required' => 'Please enter something in this field', + 'general_error' => 'A server error occurred - please try again.', + 'hidden' => 'hidden', + 'highest_users' => 'Top scoring users', + 'hot_qs_in_x' => 'Hot questions in ^', + 'hot_qs_title' => 'Hot questions', + 'image_not_read' => 'The image could not be read. Please upload one of: ^', + 'image_too_big_x_pc' => 'This image is too big. Please scale to ^% then try again.', + 'in_category_x' => 'in ^', + 'ip_address_x' => 'IP address ^', + 'logged_in_x' => 'Hello ^', + 'max_length_x' => 'Maximum length is ^ characters', + 'me' => 'me', + 'meta_order' => '^what^when^where^who', // you can reorder but DO NOT translate! e.g. <15 hours ago> + 'min_length_x' => 'Please provide more information - at least ^ characters', + 'moved' => 'moved', + 'nav_account' => 'My Account', + 'nav_activity' => 'All Activity', + 'nav_admin' => 'Admin', + 'nav_ask' => 'Ask a Question', + 'nav_categories' => 'Categories', + 'nav_feedback' => 'Send feedback', + 'nav_home' => 'Home', + 'nav_hot' => 'Hot!', + 'nav_login' => 'Login', + 'nav_logout' => 'Logout', + 'nav_most_answers' => 'Most answers', + 'nav_most_recent' => 'Recent', + 'nav_most_views' => 'Most views', + 'nav_most_votes' => 'Most votes', + 'nav_no_answer' => 'No answer', + 'nav_no_selected_answer' => 'No selected answer', + 'nav_no_upvoted_answer' => 'No upvoted answer', + 'nav_qa' => 'Q&A', + 'nav_qs' => 'Questions', + 'nav_register' => 'Register', + 'nav_tags' => 'Tags', + 'nav_unanswered' => 'Unanswered', + 'nav_updates' => 'My Updates', + 'nav_users' => 'Users', + 'no_active_users' => 'No active users found', + 'no_answers_found' => 'No answers found', + 'no_answers_in_x' => 'No answers in ^', + 'no_categories_found' => 'No categories found', + 'no_category' => 'No category', + 'no_comments_found' => 'No comments found', + 'no_comments_in_x' => 'No comments in ^', + 'no_questions_found' => 'No questions found', + 'no_questions_in_x' => 'No questions in ^', + 'no_related_qs_title' => 'No related questions found', + 'no_results_for_x' => 'No results found for ^', + 'no_tags_found' => 'No tags found', + 'no_una_questions_found' => 'No unanswered questions found', + 'no_una_questions_in_x' => 'No unanswered questions in ^', + 'no_unselected_qs_found' => 'No questions found without a selected answer', + 'no_unupvoteda_qs_found' => 'No questions found without an upvoted answer', + 'page_label' => 'Page:', + 'page_next' => 'next', + 'page_not_found' => 'Page not found', + 'page_prev' => 'prev', + 'popular_tags' => 'Most popular tags', + 'questions_tagged_x' => 'Recent questions tagged ^', + 'recategorized' => 'recategorized', + 'recent_activity_in_x' => 'Recent activity in ^', + 'recent_activity_title' => 'Recent activity', + 'recent_as_in_x' => 'Recently answered questions in ^', + 'recent_as_title' => 'Recently answered questions', + 'recent_cs_in_x' => 'Recently added comments in ^', + 'recent_cs_title' => 'Recently added comments', + 'recent_qs_as_in_x' => 'Recent questions and answers in ^', + 'recent_qs_as_title' => 'Recent questions and answers', + 'recent_qs_in_x' => 'Recent questions in ^', + 'recent_qs_title' => 'Recent questions', + 'related_qs_title' => 'Related questions', + 'remove_favorites' => 'Remove from my favorites', + 'remove_x_favorites' => 'Remove ^ from my favorites', + 'reopened' => 'reopened', + 'reshown' => 'reshown', + 'results_for_x' => 'Search results for ^', + 'retagged' => 'retagged', + 'save_button' => 'Save Changes', + 'search_button' => 'Search', + 'search_explanation' => 'Please enter some text into the search box and try again.', + 'search_title' => 'Search results', + 'selected' => 'selected', + 'send_button' => 'Send', + 'suggest_ask' => 'Help get things started by ^1asking a question^2.', + 'suggest_category_qs' => 'To see more, click for all the ^1questions in this category^2.', + 'suggest_qs_tags' => 'To see more, click for the ^1full list of questions^2 or ^3popular tags^4.', + 'suggest_qs' => 'To see more, click for the ^1full list of questions^2.', + 'unanswered_qs_in_x' => 'Questions without answers in ^', + 'unanswered_qs_title' => 'Recent questions without answers', + 'unselected_qs_in_x' => 'Questions without a selected answer in ^', + 'unselected_qs_title' => 'Recent questions without a selected answer', + 'unupvoteda_qs_in_x' => 'Questions without an upvoted answer in ^', + 'unupvoteda_qs_title' => 'Recent questions without an upvoted answer', + 'upload_limit' => 'Too many uploads - please try again in an hour', + 'view_q_must_confirm' => 'Please ^5confirm your email address^6 to view question pages.', + 'view_q_must_login' => 'Please ^1log in^2 or ^3register^4 to view question pages.', + 'viewed_qs_in_x' => 'Most viewed questions in ^', + 'viewed_qs_title' => 'Most viewed questions', + 'vote_disabled_down' => 'Voting down is only available to some users', + 'vote_disabled_hidden_a' => 'You cannot vote on hidden answers', + 'vote_disabled_hidden_q' => 'You cannot vote on hidden questions', + 'vote_disabled_level' => 'Voting is only available to some users', + 'vote_disabled_my_a' => 'You cannot vote on your own answers', + 'vote_disabled_my_q' => 'You cannot vote on your own questions', + 'vote_disabled_q_page_only' => 'Please view this question to vote', + 'vote_down_must_confirm' => 'Please ^5confirm your email address^6 to vote down.', + 'vote_down_popup' => 'Click to vote down', + 'vote_limit' => 'Too many votes received - please try again in an hour', + 'vote_must_confirm' => 'Please ^5confirm your email address^6 to vote.', + 'vote_must_login' => 'Please ^1log in^2 or ^3register^4 to vote.', + 'vote_not_allowed' => 'Voting on this is not allowed', + 'vote_up_popup' => 'Click to vote up', + 'voted_down_popup' => 'You have voted this down - click to remove vote', + 'voted_qs_in_x' => 'Highest voted questions in ^', + 'voted_qs_title' => 'Highest voted questions', + 'voted_up_popup' => 'You have voted this up - click to remove vote', + 'x_ago' => '^ ago', + 'x_answers' => '^ answers', + 'x_comments' => '^ comments', + 'x_days' => '^ days', + 'x_disliked' => '^ dislike', + 'x_flags' => '^ flags', + 'x_hours' => '^ hours', + 'x_liked' => '^ like', + 'x_minutes' => '^ minutes', + 'x_months' => '^ months', + 'x_points' => '^ points', + 'x_questions' => '^ questions', + 'x_seconds' => '^ seconds', + 'x_tags' => '^ tags', + 'x_users' => '^ users', + 'x_views' => '^ views', + 'x_votes' => '^ votes', + 'x_weeks' => '^ weeks', + 'x_years' => '^ years', + ); + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-lang-misc.php b/qa-include/qa-lang-misc.php new file mode 100644 index 000000000..017aa6380 --- /dev/null +++ b/qa-include/qa-lang-misc.php @@ -0,0 +1,95 @@ + 'Block IP address', + 'browse_categories' => 'Browse categories', + 'captcha_confirm_fix' => 'To avoid this verification in future, please ^5confirm your email address^6.', + 'captcha_error' => 'Please complete the anti-spam verification', + 'captcha_label' => 'Anti-spam verification:', + 'captcha_login_fix' => 'To avoid this verification in future, please ^1log in^2 or ^3register^4.', + 'feed_a_edited_prefix' => 'Answer edited: ', + 'feed_a_prefix' => 'Answered: ', + 'feed_a_reshown_prefix' => 'Answer reshown: ', + 'feed_a_selected_prefix' => 'Answer selected: ', + 'feed_c_edited_prefix' => 'Comment edited: ', + 'feed_c_moved_prefix' => 'Comment moved: ', + 'feed_c_prefix' => 'Commented: ', + 'feed_c_reshown_prefix' => 'Comment reshown: ', + 'feed_closed_prefix' => 'Closed: ', + 'feed_edited_prefix' => 'Edited: ', + 'feed_hidden_prefix' => 'Hidden: ', + 'feed_not_found' => 'Feed not found', + 'feed_recategorized_prefix' => 'Recategorized: ', + 'feed_reopened_prefix' => 'Reopened: ', + 'feed_reshown_prefix' => 'Reshown: ', + 'feed_retagged_prefix' => 'Retagged: ', + 'feedback_email' => 'Your email: (optional)', + 'feedback_empty' => 'Please use this field to send some comments or suggestions', + 'feedback_message' => 'Your comments or suggestions for ^:', + 'feedback_name' => 'Your name: (optional)', + 'feedback_sent' => 'Your message below was sent - thank you.', + 'feedback_title' => 'Send feedback', + 'hide_all_ip_button' => 'Hide all posts from this IP', + 'host_name' => 'Host name:', + 'matches_blocked_ips' => 'Matches blocked IP addresses:', + 'message_empty' => 'Please enter your message to send to this user', + 'message_explanation' => 'This will be sent as a notification from ^. Your email address will not be revealed unless you include it in the message.', + 'message_for_x' => 'Your message for ^:', + 'message_limit' => 'Too many private messages sent - please try again in an hour', + 'message_must_login' => 'Please ^1log in^2 or ^3register^4 to send private messages.', + 'message_received_x_ago' => 'Received ^ ago:', + 'message_recent_history' => 'Recent correspondence with ^', + 'message_sent_x_ago' => 'Sent ^ ago:', + 'message_sent' => 'Your private message below was sent', + 'my_favorites_title' => 'My favorites', + 'nav_all_my_updates' => 'All my updates', + 'nav_my_content' => 'My content', + 'nav_my_details' => 'My details', + 'nav_my_favorites' => 'My favorites', + 'no_activity_from_x' => 'No activity from ^', + 'no_favorite_categories' => 'No favorite categories', + 'no_favorite_qs' => 'No favorite questions', + 'no_favorite_tags' => 'No favorite tags', + 'no_favorite_users' => 'No favorite users', + 'no_recent_updates' => 'No recent updates', + 'no_updates_content' => 'No recent updates for my content', + 'no_updates_favorites' => 'No updates for my favorites', + 'private_message_title' => 'Send a private message', + 'recent_activity_from_x' => 'Recent activity from ^', + 'recent_updates_content' => 'Recent updates for my content', + 'recent_updates_favorites' => 'Recent updates for my favorites', + 'recent_updates_title' => 'Recent updates for me', + 'site_in_maintenance' => 'This site is currently down for maintenance - please come back soon.', + 'suggest_favorites_add' => 'To add a question or other item to your favorites, click the ^ at the top of its page.', + 'suggest_update_favorites' => 'For more updates, add items to ^1your favorites^2.', + 'unblock_ip_button' => 'Unblock IP address', + ); + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-lang-options.php b/qa-include/qa-lang-options.php new file mode 100644 index 000000000..fe1083f86 --- /dev/null +++ b/qa-include/qa-lang-options.php @@ -0,0 +1,274 @@ + 'Allow users with posts to change their username:', + 'allow_close_questions' => 'Allow questions to be manually closed:', + 'allow_multi_answers' => 'Allow multiple answers per user:', + 'allow_no_category' => 'Allow questions with no category', + 'allow_no_sub_category' => 'Allow questions with a category but no sub-category', + 'allow_private_messages' => 'Enable private messaging between users:', + 'allow_self_answer' => 'Allow users to answer their own question:', + 'allow_view_q_bots' => 'Allow search engines to view question pages', + 'avatar_allow_gravatar' => 'Allow ^1Gravatar^2 avatars:', + 'avatar_allow_upload' => 'Allow users to upload avatars:', + 'avatar_default_show' => 'Default avatar:', + 'avatar_profile_size' => 'Avatar size on user profile page:', + 'avatar_q_list_size' => 'Avatar size on question lists:', + 'avatar_q_page_a_size' => 'Avatar size on answers:', + 'avatar_q_page_c_size' => 'Avatar size on comments:', + 'avatar_q_page_q_size' => 'Avatar size on questions:', + 'avatar_store_size' => 'Maximum size for storing avatars:', + 'avatar_users_size' => 'Avatar size on top users page:', + 'block_bad_words' => 'Censored words - separate by spaces or commas:', + 'block_ips_write' => 'Blocked IP addresses - separate by spaces or commas:', + 'captcha_module' => 'Use captcha module:', + 'captcha_on_anon_post' => 'Use captcha for anonymous posts:', + 'captcha_on_feedback' => 'Use captcha on feedback form:', + 'captcha_on_register' => 'Use captcha for user registration:', + 'captcha_on_reset_password' => 'Use captcha on reset password form:', + 'captcha_on_unconfirmed' => 'Use captcha if email not confirmed:', + 'columns_tags' => 'Columns on Tags page:', + 'columns_users' => 'Columns on Users page:', + 'comment_on_as' => 'Allow comments on answers:', + 'comment_on_qs' => 'Allow comments on questions:', + 'confirm_user_emails' => 'Request confirmation of user emails:', + 'confirm_user_required' => 'New users must complete confirmation:', + 'custom_home_content' => 'Home page content - HTML allowed:', + 'custom_home_heading' => 'Home page heading:', + 'default_privacy' => 'Privacy: Your email address will not be shared or sold to third parties.', + 'default_sidebar' => "Welcome to ^, where you can ask questions and receive answers from other members of the community.", + 'default_subject' => 'A message from ^', + 'default_suffix' => 'Q&A', + 'do_ask_check_qs' => 'Check for similar questions when asking:', + 'do_close_on_select' => 'Close questions with a selected answer:', + 'do_complete_tags' => 'Show matching tags while typing:', + 'do_count_q_views' => 'Count the number of question views:', + 'do_example_tags' => 'Show example tags based on question:', + 'editor_for_as' => 'Default editor for answers:', + 'editor_for_cs' => 'Default editor for comments:', + 'editor_for_qs' => 'Default editor for questions:', + 'email_privacy' => 'Privacy note for email addresses - HTML allowed:', + 'extra_field_active' => 'Custom field for extra information on ask form:', + 'extra_field_display_label' => 'Show the extra information on question pages with label:', + 'extra_field_display' => 'Show the extra information on question pages', + 'feed_for_activity' => 'Feed for recent activity:', + 'feed_for_hot' => 'Feed for hot questions:', + 'feed_for_qa' => 'Feed for recent questions and answers:', + 'feed_for_questions' => 'Feed for recent questions:', + 'feed_for_search' => 'Feeds for search results:', + 'feed_for_tag_qs' => 'Feed for each tag\'s questions:', + 'feed_for_unanswered' => 'Feed for unanswered questions:', + 'feed_full_text' => 'Include full text in feeds:', + 'feed_number_items' => 'Maximum length of feeds:', + 'feed_per_category' => 'Individual feeds per category:', + 'feedback_email' => 'Email address for admin messages - not shown to users:', + 'feedback_enabled' => 'Provide a page for users to send feedback', + 'flagging_hide_after' => 'Automatically hide posts which reach:', + 'flagging_notify_every' => 'Email me again after every additional:', + 'flagging_notify_first' => 'Email me if a post receives:', + 'flagging_of_posts' => 'Allow posts to be flagged:', + 'follow_on_as' => 'Allow questions to be related to answers:', + 'from_email' => 'Sender address for messages from site:', + 'hot_weight_a_age' => 'Question has a new answer:', + 'hot_weight_answers' => 'Question has many answers:', + 'hot_weight_q_age' => 'Question is new:', + 'hot_weight_views' => 'Question has many views:', + 'hot_weight_votes' => 'Question has many up votes:', + 'links_in_new_window' => 'Open linked URLs in a new window:', + 'logo_height' => 'Logo height:', + 'logo_show' => 'Show a logo image in the page header', + 'logo_url' => 'URL of logo - absolute or relative to Q2A root:', + 'logo_width' => 'Logo width:', + 'mailing_body' => 'Body text:', + 'mailing_enabled' => 'Enable mass mailings to all users', + 'mailing_from_email' => 'From email address:', + 'mailing_from_name' => 'From name:', + 'mailing_per_minute' => 'Maximum mailing rate:', + 'mailing_subject' => 'Subject line:', + 'match_1' => 'Narrowest', + 'match_2' => 'Narrower', + 'match_3' => 'Default', + 'match_4' => 'Wider', + 'match_5' => 'Widest', + 'match_ask_check_qs' => 'Similar questions matching:', + 'match_example_tags' => 'Example tags matching:', + 'match_related_qs' => 'Related questions matching:', + 'max_len_q_title' => 'Maximum length of question title:', + 'max_num_q_tags' => 'Maximum number of tags:', + 'max_rate_ip_as' => 'Rate limit for adding answers:', + 'max_rate_ip_cs' => 'Rate limit for posting comments:', + 'max_rate_ip_flags' => 'Rate limit for flagging posts:', + 'max_rate_ip_logins' => 'Rate limit for logging in:', + 'max_rate_ip_messages' => 'Rate limit for sending private messages:', + 'max_rate_ip_qs' => 'Rate limit for asking questions:', + 'max_rate_ip_registers' => 'Rate limit for user registrations:', + 'max_rate_ip_uploads' => 'Rate limit for uploading files:', + 'max_rate_ip_votes' => 'Rate limit for voting:', + 'max_rate_user_as' => 'Maximum answers per user per hour:', + 'max_rate_user_cs' => 'Maximum comments per user per hour:', + 'max_rate_user_flags' => 'Maximum flags per user per hour:', + 'max_rate_user_messages' => 'Maximum private messages per user per hour:', + 'max_rate_user_qs' => 'Maximum questions per user per hour:', + 'max_rate_user_uploads' => 'Maximum uploads per user per hour:', + 'max_rate_user_votes' => 'Maximum votes per user per hour:', + 'min_len_a_content' => 'Minimum length of answer:', + 'min_len_c_content' => 'Minimum length of comment:', + 'min_len_q_content' => 'Minimum length of question body:', + 'min_len_q_title' => 'Minimum length of question title:', + 'min_num_q_tags' => 'Minimum number of tags:', + 'moderate_anon_post' => 'Use moderation for anonymous posts:', + 'moderate_by_points' => 'Use moderation for users with few points:', + 'moderate_notify_admin' => 'Email me when a post needs moderation:', + 'moderate_points_limit' => 'Use moderation for users with less than:', + 'moderate_unconfirmed' => 'Use moderation if email not confirmed:', + 'neat_urls' => 'URL structure:', + 'notify_admin_q_post' => 'Email this address when a question is posted', + 'notify_users_default' => 'Check email notification box by default:', + 'option_default' => 'Default', + 'page_size_activity' => 'Length of All Activity page:', + 'page_size_ask_check_qs' => 'Maximum similar questions to show:', + 'page_size_ask_tags' => 'Maximum tag hints to show:', + 'page_size_home' => 'Length of Q&A page:', + 'page_size_hot_qs' => 'Length of Hot! page:', + 'page_size_q_as' => 'Maximum answers per page:', + 'page_size_qs' => 'Length of Questions page:', + 'page_size_related_qs' => 'Maximum related questions:', + 'page_size_search' => 'Search results per page:', + 'page_size_tag_qs' => 'Questions on each tag page:', + 'page_size_tags' => 'Length of Tags page:', + 'page_size_una_qs' => 'Length of Unanswered page:', + 'page_size_user_posts' => 'Posts on each user page:', + 'page_size_users' => 'Length of Users page:', + 'pages_prev_next' => 'Links to previous/next pages:', + 'permit_admins' => 'Administrators', + 'permit_all' => 'Anybody', + 'permit_block' => 'Blocking or unblocking user or IPs:', + 'permit_confirmed' => 'Registered users with email confirmed', + 'permit_create_admins' => 'Creating administrators:', + 'permit_create_eds_mods' => 'Creating editors and moderators:', + 'permit_create_experts' => 'Creating experts:', + 'permit_delete_users' => 'Deleting users:', + 'permit_editors' => 'Editors, Moderators, Admins', + 'permit_experts' => 'Experts, Editors, Moderators, Admins', + 'permit_moderators' => 'Moderators and Admins', + 'permit_points_confirmed' => 'Registered & email confirmed & enough points', + 'permit_points' => 'Registered users with enough points', + 'permit_see_emails' => 'Viewing user email addresses:', + 'permit_supers' => 'Super Administrators', + 'permit_users' => 'Registered users', + 'place_full_below_content' => 'Full width - Below content', + 'place_full_below_footer' => 'Full width - Below footer', + 'place_full_below_nav' => 'Full width - Below navigation', + 'place_full_top' => 'Full width - Top of page', + 'place_main_below_lists' => 'Main area - Below lists', + 'place_main_below_title' => 'Main area - Below title', + 'place_main_bottom' => 'Main area - Bottom', + 'place_main_top' => 'Main area - Top', + 'place_side_below_categories' => 'Side panel - Below categories', + 'place_side_below_sidebar' => 'Side panel - Below sidebar box', + 'place_side_last' => 'Side panel - Last', + 'place_side_top' => 'Side panel - Top', + 'points_a_selected' => 'Having your answer selected as the best:', + 'points_a_voted_max_gain' => 'Limit from up votes on each answer:', + 'points_a_voted_max_loss' => 'Limit from down votes on each answer:', + 'points_base' => 'Add for all users:', + 'points_multiple' => 'Multiply all points:', + 'points_per_a_voted_down' => 'Per down vote on your answer:', + 'points_per_a_voted_up' => 'Per up vote on your answer:', + 'points_per_q_voted_down' => 'Per down vote on your question:', + 'points_per_q_voted_up' => 'Per up vote on your question:', + 'points_post_a' => 'Posting an answer:', + 'points_post_q' => 'Posting a question:', + 'points_q_voted_max_gain' => 'Limit from up votes on each question:', + 'points_q_voted_max_loss' => 'Limit from down votes on each question:', + 'points_select_a' => 'Selecting an answer for your question:', + 'points_vote_down_a' => 'Voting down an answer:', + 'points_vote_down_q' => 'Voting down a question:', + 'points_vote_up_a' => 'Voting up an answer:', + 'points_vote_up_q' => 'Voting up a question:', + 'q_urls_remove_accents' => 'Remove accents from question URLs:', + 'q_urls_title_length' => 'Question title length in URLs:', + 'search_module' => 'Use search module:', + 'show_a_form_immediate' => 'Show answer form immediately:', + 'show_always' => 'Always', + 'show_c_reply_buttons' => 'Show reply button on comments:', + 'show_custom_answer' => 'Custom message on answer form - HTML allowed:', + 'show_custom_ask' => 'Custom message on ask form - HTML allowed:', + 'show_custom_comment' => 'Custom message on comment form - HTML allowed:', + 'show_custom_footer' => 'Custom HTML at bottom of every page:', + 'show_custom_header' => 'Custom HTML at top of every page:', + 'show_custom_home' => 'Custom content in home page instead of Q&A', + 'show_custom_in_head' => 'Custom HTML in section of every page:', + 'show_custom_register' => 'Custom message on register form - HTML allowed:', + 'show_custom_sidebar' => 'Custom HTML in sidebar box on every page:', + 'show_custom_sidepanel' => 'Custom HTML in side panel on every page:', + 'show_custom_welcome' => 'Custom message in email sent to new registered users:', + 'show_fewer_cs_count' => 'If partially hidden, show most recent:', + 'show_fewer_cs_from' => 'Partially hide comments if more than:', + 'show_full_date_days' => 'Show full date after:', + 'show_home_description' => 'Include description for home page:', + 'show_if_no_as' => 'If no answers', + 'show_message_history' => 'Store and display private message history:', + 'show_never' => 'Never', + 'show_notice_visitor' => 'Notice at top for first time visitors - HTML allowed:', + 'show_notice_welcome' => 'Notice at top for new registered users - HTML allowed:', + 'show_selected_first' => 'Move selected answer to the top:', + 'show_url_links' => 'Detect and link URLs in posts:', + 'show_user_points' => 'Show points next to usernames:', + 'show_user_titles' => 'Show titles next to usernames:', + 'show_view_counts' => 'Show view count in question lists:', + 'show_when_created' => 'Show age of user posts:', + 'site_language' => 'Site language:', + 'site_maintenance' => 'Take site down for temporary maintenance', + 'site_theme_mobile' => 'Theme for mobiles:', + 'site_theme' => 'Site theme:', + 'site_title' => 'Q&A site name:', + 'site_url' => 'Preferred site URL:', + 'smtp_active' => 'Send email via SMTP instead of local mail', + 'smtp_address' => 'SMTP server address:', + 'smtp_authenticate' => 'Send SMTP username and password', + 'smtp_password' => 'SMTP password:', + 'smtp_port' => 'SMTP server port:', + 'smtp_secure_none' => 'None', + 'smtp_secure' => 'SMTP secure connection:', + 'smtp_username' => 'SMTP username:', + 'sort_answers_by' => 'Sort answers by:', + 'sort_time' => 'Time', + 'sort_votes' => 'Votes', + 'suspend_register_users' => 'Temporarily suspend new user registrations:', + 'tag_separator_comma' => 'Use comma as the only tag separator:', + 'tags_or_categories' => 'Question classification:', + 'votes_separated' => 'Show separate up and down votes:', + 'voting_on_as' => 'Allow voting on answers:', + 'voting_on_q_page_only' => 'Allow voting on question page only:', + 'voting_on_qs' => 'Allow voting on questions:', + ); + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-lang-profile.php b/qa-include/qa-lang-profile.php new file mode 100644 index 000000000..ca96aa8f5 --- /dev/null +++ b/qa-include/qa-lang-profile.php @@ -0,0 +1,78 @@ + ' (1 chosen as best)', + '1_down_vote' => '1 down vote', + '1_up_vote' => '1 up vote', + '1_with_best_chosen' => ' (1 with best answer chosen)', + 'activity_by_x' => 'Activity by ^', + 'answers' => 'Answers:', + 'bonus_points' => 'Bonus points:', + 'comments' => 'Comments:', + 'extra_privileges' => 'Extra privileges:', + 'gave_out' => 'Gave out:', + 'my_account_title' => 'My account details', + 'no_posts_by_x' => 'No posts by ^', + 'permit_anon_view_ips' => 'Viewing IPs of anonymous posts', + 'permit_close_q' => 'Closing any question', + 'permit_delete_hidden' => 'Deleting hidden posts', + 'permit_edit_a' => 'Editing any answer', + 'permit_edit_c' => 'Editing any comment', + 'permit_edit_q' => 'Editing any question', + 'permit_flag' => 'Flagging posts', + 'permit_hide_show' => 'Hiding or showing any post', + 'permit_moderate' => 'Approving or rejecting posts', + 'permit_post_a' => 'Answering questions', + 'permit_post_c' => 'Adding comments', + 'permit_post_q' => 'Asking questions', + 'permit_recat' => 'Recategorizing any question', + 'permit_retag' => 'Retagging any question', + 'permit_select_a' => 'Selecting answer for any question', + 'permit_view_q_page' => 'Viewing question pages', + 'permit_vote_a' => 'Voting on answers', + 'permit_vote_down' => 'Voting posts down', + 'permit_vote_q' => 'Voting on questions', + 'questions' => 'Questions:', + 'ranked_x' => ' (ranked #^)', + 'received' => 'Received:', + 'recent_activity_by_x' => 'Recent activity by ^', + 'score' => 'Score:', + 'send_private_message' => ' - ^1send private message^2', + 'set_bonus_button' => 'Update bonus', + 'title' => 'Title:', + 'user_x' => 'User ^', + 'voted_on' => 'Voted on:', + 'x_chosen_as_best' => ' (^ chosen as best)', + 'x_down_votes' => '^ down votes', + 'x_up_votes' => '^ up votes', + 'x_with_best_chosen' => ' (^ with best answer chosen)', + ); + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-lang-question.php b/qa-include/qa-lang-question.php new file mode 100644 index 000000000..2d1d1e5d9 --- /dev/null +++ b/qa-include/qa-lang-question.php @@ -0,0 +1,165 @@ + '1 Answer', + 'a_convert_to_c_on' => 'Convert this answer into a comment on:', + 'a_convert_to_c' => 'Convert this answer into a comment', + 'a_convert_warn_cs' => 'Warning: This conversion cannot be reversed and will also move this answer\'s comments.', + 'a_convert_warn' => 'Warning: This conversion cannot be reversed.', + 'a_notify_email' => 'Email me at this address if my answer is selected or commented on:', + 'a_notify_label' => 'Email me if my answer is selected or commented on', + 'a_notify_x_label' => 'Email me (^) if my answer is selected or commented on', + 'a_waiting_your_approval' => 'This answer is waiting for your approval', + 'a_your_waiting_approval' => 'Your answer is waiting for approval', + 'add_answer_button' => 'Add answer', + 'add_comment_button' => 'Add comment', + 'add_q_favorites' => 'Add this question to my favorites', + 'answer_button' => 'answer', + 'answer_limit' => 'Too many answers received - please try again in an hour', + 'answer_must_confirm' => 'Please ^5confirm your email address^6 to answer this question.', + 'answer_must_login' => 'Please ^1log in^2 or ^3register^4 to answer this question.', + 'answer_q_popup' => 'Answer this question', + 'approve_button' => 'approve', + 'ask_button' => 'Ask the Question', + 'ask_follow_from_a' => 'Your question will be related to this answer:', + 'ask_follow_title' => 'Ask a related question', + 'ask_limit' => 'Too many questions received - please try again in an hour', + 'ask_must_confirm' => 'Please ^5confirm your email address^6 to ask a question.', + 'ask_must_login' => 'Please ^1log in^2 or ^3register^4 to ask a question.', + 'ask_same_q' => 'Before proceeding, please check your question was not asked already:', + 'ask_title' => 'Ask a question', + 'c_notify_email' => 'Email me at this address if a comment is added after mine:', + 'c_notify_label' => 'Email me if a comment is added after mine', + 'c_notify_x_label' => 'Email me (^) if a comment is added after mine', + 'c_waiting_your_approval' => 'This comment is waiting for your approval', + 'c_your_waiting_approval' => 'Your comment is waiting for approval', + 'category_js_note' => 'To select any category, please enable Javascript in your web browser.', + 'category_required' => 'Please choose a category', + 'claim_button' => 'I wrote this', + 'clear_flags_button' => 'clear flags', + 'clear_flags_popup' => 'Remove flags by all users', + 'close_button' => 'close', + 'close_duplicate_error' => 'The duplicate question could not be found - please try entering the number from a different question URL, e.g. 123.', + 'close_duplicate' => 'This is a duplicate of another question', + 'close_form_button' => 'Close question', + 'close_form_title' => 'Close this question', + 'close_original_note' => 'You can also enter the question number from the URL, e.g. 123.', + 'close_original_title' => 'URL of the original question:', + 'close_q_popup' => 'Close this question to any new answers', + 'close_reason_title' => 'Reason for closing this question:', + 'closed_as_duplicate' => 'closed as a duplicate of:', + 'closed_with_note' => 'closed with the note:', + 'comment_a_popup' => 'Add a comment on this answer', + 'comment_button' => 'comment', + 'comment_limit' => 'Too many comments received - please try again in an hour', + 'comment_must_confirm' => 'Please ^5confirm your email address^6 to add a comment.', + 'comment_must_login' => 'Please ^1log in^2 or ^3register^4 to add a comment.', + 'comment_on_a' => 'On answer: ', + 'comment_on_q' => 'On question: ', + 'comment_q_popup' => 'Add a comment on this question', + 'delete_a_popup' => 'Delete this answer permanently', + 'delete_button' => 'delete', + 'delete_c_popup' => 'Delete this comment permanently', + 'delete_q_popup' => 'Delete this question permanently', + 'duplicate_content' => 'Your submission appears to be a duplicate.', + 'edit_a_popup' => 'Edit this answer', + 'edit_a_title' => 'Edit answer', + 'edit_button' => 'edit', + 'edit_c_popup' => 'Edit this comment', + 'edit_c_title' => 'Edit comment', + 'edit_must_confirm' => 'Please ^5confirm your email address^6 to edit this.', + 'edit_must_login' => 'Please ^1log in^2 or ^3register^4 to edit this.', + 'edit_q_popup' => 'Edit this question', + 'edit_q_title' => 'Edit Question', + 'example_tags' => 'Example tags: ', + 'flag_a_popup' => 'Flag this answer as spam or inappropriate', + 'flag_button' => 'flag', + 'flag_c_popup' => 'Flag this comment as spam or inappropriate', + 'flag_hide_button' => 'flag and hide', + 'flag_limit' => 'Too many posts flagged - please try again in an hour', + 'flag_must_confirm' => 'Please ^5confirm your email address^6 to flag posts.', + 'flag_must_login' => 'Please ^1log in^2 or ^3register^4 to flag posts.', + 'flag_not_allowed' => 'Flagging this is not allowed', + 'flag_q_popup' => 'Flag this question as spam or inappropriate', + 'follow_a_popup' => 'Ask a new question relating to this answer', + 'follow_button' => 'ask related question', + 'follows_a' => 'related to an answer for:', + 'follows_q' => 'about the question:', + 'hide_a_popup' => 'Hide this answer', + 'hide_button' => 'hide', + 'hide_c_popup' => 'Hide this comment', + 'hide_q_popup' => 'Hide this question', + 'matching_tags' => 'Matching tags: ', + 'max_tags_x' => 'A maximum of ^ tags are allowed', + 'min_tags_x' => 'Please provide at least ^ tag/s', + 'notify_email_note' => 'Privacy: Your email address will only be used for sending these notifications.', + 'q_category_label' => 'Category:', + 'q_content_label' => 'More information for the question:', + 'q_hidden_author' => 'This question has been hidden by its author', + 'q_hidden_flagged' => 'This question has been flagged and hidden', + 'q_hidden_other' => 'This question has been hidden', + 'q_notify_email' => 'Email me at this address if my question is answered or commented on:', + 'q_notify_label' => 'Email me if my question is answered or commented on', + 'q_notify_x_label' => 'Email me (^) if my question is answered or commented on', + 'q_tags_comma_label' => 'Tags - use comma (,) as a separator:', + 'q_tags_label' => 'Tags - use hyphens to combine words:', + 'q_title_label' => 'The question in one sentence:', + 'q_waiting_approval' => 'This question is waiting for approval', + 'q_waiting_your_approval' => 'This question is waiting for your approval', + 'q_your_waiting_approval' => 'Your question is waiting for approval', + 'recat_button' => 'recategorize', + 'recat_popup' => 'Change this question\'s category', + 'recat_q_title' => 'Recategorize question', + 'reject_button' => 'reject', + 'remove_q_favorites' => 'Remove this question from my favorites', + 'reopen_button' => 'reopen', + 'reply_button' => 'reply', + 'reply_c_popup' => 'Reply to this comment', + 'reshow_button' => 'reshow', + 'retag_button' => 'retag', + 'retag_cat_popup' => 'Change this question\'s category or tags', + 'retag_popup' => 'Change this question\'s tags', + 'retag_q_title' => 'Retag question', + 'select_popup' => 'Click to select as best answer', + 'select_text' => 'Best answer', + 'show_1_comment' => 'Show 1 comment', + 'show_1_previous_comment' => 'Show 1 previous comment', + 'show_x_comments' => 'Show ^ comments', + 'show_x_previous_comments' => 'Show ^ previous comments', + 'unflag_button' => 'unflag', + 'unflag_popup' => 'Remove the flag that you added', + 'unselect_popup' => 'Click to remove selection', + 'x_answers_title' => '^ Answers', + 'your_answer_title' => 'Your answer', + 'your_comment_a' => 'Your comment on this answer:', + 'your_comment_q' => 'Your comment on this question:', + ); + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-lang-users.php b/qa-include/qa-lang-users.php new file mode 100644 index 000000000..c5fab4f9d --- /dev/null +++ b/qa-include/qa-lang-users.php @@ -0,0 +1,128 @@ + 'About', + 'add_user_x_favorites' => 'Add user ^ to my favorites', + 'avatar_default' => 'Default', + 'avatar_gravatar' => 'Show my ^1Gravatar^2', + 'avatar_label' => 'Avatar:', + 'avatar_none' => 'None', + 'block_user_button' => 'Block User', + 'blocked_users' => 'Blocked users', + 'change_email_link' => ' - ^1change email^2', + 'change_password' => 'Change Password', + 'confirm_complete' => 'Thank you - your email address has been confirmed', + 'confirm_emailed' => 'A confirmation link has been emailed to you. Please click the link to confirm your email address.', + 'confirm_required' => 'To complete your registration, please click the confirmation link that has been emailed to you, or ^1request another^2.', + 'confirm_title' => 'Email Address Confirmation', + 'confirm_wrong_log_in' => 'Code not correct - please ^1log in^2 to send a new link', + 'confirm_wrong_resend' => 'Code not correct - please click below to send a new link', + 'delete_user_button' => 'Delete User', + 'edit_user_button' => 'Edit this User', + 'email_confirmed' => 'Confirmed', + 'email_exists' => 'Email already belongs to an account', + 'email_handle_label' => 'Email or Username:', + 'email_invalid' => 'Email is invalid - please check carefully', + 'email_label' => 'Email:', + 'email_not_confirmed' => 'Not yet confirmed', + 'email_please_confirm' => 'Please ^5confirm^6', + 'email_required' => 'Email address required - not public', + 'forgot_link' => 'I forgot my password', + 'full_name' => 'Full name', + 'handle_empty' => 'Username must not be empty', + 'handle_exists' => 'Username is taken - please try another', + 'handle_has_bad' => 'Username may not contain: ^', + 'handle_label' => 'Username:', + 'hide_all_user_button' => 'Hide all posts by this user', + 'last_login_label' => 'Last login:', + 'last_write_label' => 'Last write action:', + 'level_admin' => 'Administrator', + 'level_editor' => 'Editor', + 'level_expert' => 'Expert', + 'level_moderator' => 'Moderator', + 'level_super' => 'Super Administrator', + 'location' => 'Location', + 'log_in_to_access' => 'You may now ^1log in^2 to access your account.', + 'login_button' => 'Log In', + 'login_limit' => 'Too many login attempts - please try again in an hour', + 'login_title' => 'Log in', + 'mass_mailings_explanation' => 'Subscribe to emails sent out to all users', + 'mass_mailings' => 'Mass mailings:', + 'member_for' => 'Member for:', + 'member_type' => 'Type:', + 'new_password_1' => 'New password:', + 'new_password_2' => 'Retype new password:', + 'no_blocked_users' => 'No blocked users found', + 'no_permission' => 'You do not have permission to perform this operation', + 'old_password' => 'Old password:', + 'only_shown_admins' => '(only shown to admins)', + 'only_shown_moderators' => '(only shown to moderators)', + 'password_changed' => 'Password changed', + 'password_label' => 'Password:', + 'password_min' => 'Password must be at least ^ characters', + 'password_mismatch' => 'New passwords do not match', + 'password_none' => 'None. To log in directly, set a password below.', + 'password_sent' => 'Your new password was emailed to you', + 'password_to_set' => 'Please set on your account page', + 'password_wrong' => 'Password not correct', + 'private_messages_explanation' => 'Allow users to email me (without seeing my address)', + 'private_messages' => 'Private messages:', + 'profile_saved' => 'Profile saved', + 'register_button' => 'Register', + 'register_limit' => 'Too many registrations - please try again in an hour', + 'register_suspended' => 'Registration of new users has been temporarily suspended. Please try again soon.', + 'register_title' => 'Register as a new user', + 'registered_user' => 'Registered user', + 'remember_label' => 'Remember me on this computer', + 'remove_avatar' => 'Remove avatar:', + 'reset_code_another' => 'send another', + 'reset_code_emailed' => 'You have been emailed your reset code', + 'reset_code_label' => 'Code:', + 'reset_code_wrong' => 'Code not correct', + 'reset_title' => 'Reset Forgotten Password', + 'save_profile' => 'Save Profile', + 'save_user' => 'Save User', + 'send_confirm_button' => 'Send Confirmation Link', + 'send_password_button' => 'Send New Password', + 'send_reset_button' => 'Send Reset Password Email', + 'send_reset_note' => 'A message will be sent to your email address with instructions.', + 'special_users' => 'Special users', + 'unblock_user_button' => 'Unblock User', + 'unsubscribe_complete' => 'You have been unsubscribed from mass mailings sent out by ^0. You may resubscribe at any time via your ^1account^2 page.', + 'unsubscribe_title' => 'Unsubscribe', + 'unsubscribe_wrong_log_in' => 'Code not correct - please ^1log in^2 to unsubscribe', + 'unsubscribe' => 'Unsubscribe:', + 'user_blocked' => '(blocked)', + 'user_not_found' => 'User not found', + 'website' => 'Website', + 'x_ago_from_y' => '^1 ago from ^2', + ); + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-account.php b/qa-include/qa-page-account.php new file mode 100644 index 000000000..3d008322b --- /dev/null +++ b/qa-include/qa-page-account.php @@ -0,0 +1,403 @@ +filter_profile($inprofile, $errors, $useraccount, $userprofile); + + foreach ($userfields as $userfield) + if (!isset($errors[$userfield['fieldid']])) + qa_db_user_profile_set($userid, $userfield['title'], $inprofile[$userfield['fieldid']]); + + list($useraccount, $userprofile)=qa_db_select_with_pending( + qa_db_user_account_selectspec($userid, true), + qa_db_user_profile_selectspec($userid, true) + ); + + qa_report_event('u_save', $userid, $useraccount['handle'], qa_cookie_get()); + + if (empty($errors)) + qa_redirect('account', array('state' => 'profile-saved')); + + qa_logged_in_user_flush(); + } + + +// Process change password if clicked + + if (qa_clicked('dochangepassword')) { + require_once QA_INCLUDE_DIR.'qa-app-users-edit.php'; + + $inoldpassword=qa_post_text('oldpassword'); + $innewpassword1=qa_post_text('newpassword1'); + $innewpassword2=qa_post_text('newpassword2'); + + $errors=array(); + + if ($haspassword && (strtolower(qa_db_calc_passcheck($inoldpassword, $useraccount['passsalt'])) != strtolower($useraccount['passcheck']))) + $errors['oldpassword']=qa_lang('users/password_wrong'); + + $useraccount['password']=$inoldpassword; + $errors=$errors+qa_password_validate($innewpassword1, $useraccount); // array union + + if ($innewpassword1 != $innewpassword2) + $errors['newpassword2']=qa_lang('users/password_mismatch'); + + if (empty($errors)) { + qa_db_user_set_password($userid, $innewpassword1); + qa_db_user_set($userid, 'sessioncode', ''); // stop old 'Remember me' style logins from still working + qa_set_logged_in_user($userid, $useraccount['handle'], false, $useraccount['sessionsource']); // reinstate this specific session + + qa_report_event('u_password', $userid, $useraccount['handle'], qa_cookie_get()); + + qa_redirect('account', array('state' => 'password-changed')); + } + } + + +// Prepare content for theme + + $qa_content=qa_content_prepare(); + + $qa_content['title']=qa_lang_html('profile/my_account_title'); + + $qa_content['form_profile']=array( + 'tags' => 'ENCTYPE="multipart/form-data" METHOD="POST" ACTION="'.qa_self_html().'"', + + 'style' => 'wide', + + 'fields' => array( + 'duration' => array( + 'type' => 'static', + 'label' => qa_lang_html('users/member_for'), + 'value' => qa_time_to_string(qa_opt('db_time')-$useraccount['created']), + ), + + 'type' => array( + 'type' => 'static', + 'label' => qa_lang_html('users/member_type'), + 'value' => qa_html(qa_user_level_string($useraccount['level'])), + ), + + 'handle' => array( + 'label' => qa_lang_html('users/handle_label'), + 'tags' => 'NAME="handle"', + 'value' => qa_html(isset($inhandle) ? $inhandle : $useraccount['handle']), + 'error' => qa_html(@$errors['handle']), + 'type' => $changehandle ? 'text' : 'static', + ), + + 'email' => array( + 'label' => qa_lang_html('users/email_label'), + 'tags' => 'NAME="email"', + 'value' => qa_html(isset($inemail) ? $inemail : $useraccount['email']), + 'error' => isset($errors['email']) ? qa_html($errors['email']) : + (($doconfirms && !$isconfirmed) ? qa_insert_login_links(qa_lang_html('users/email_please_confirm')) : null), + ), + + 'messages' => array( + 'label' => qa_lang_html('users/private_messages'), + 'tags' => 'NAME="messages"', + 'type' => 'checkbox', + 'value' => !($useraccount['flags'] & QA_USER_FLAGS_NO_MESSAGES), + 'note' => qa_lang_html('users/private_messages_explanation'), + ), + + 'mailings' => array( + 'label' => qa_lang_html('users/mass_mailings'), + 'tags' => 'NAME="mailings"', + 'type' => 'checkbox', + 'value' => !($useraccount['flags'] & QA_USER_FLAGS_NO_MAILINGS), + 'note' => qa_lang_html('users/mass_mailings_explanation'), + ), + + 'avatar' => null, // for positioning + ), + + 'buttons' => array( + 'save' => array( + 'label' => qa_lang_html('users/save_profile'), + ), + ), + + 'hidden' => array( + 'dosaveprofile' => '1' + ), + ); + + if (qa_get_state()=='profile-saved') + $qa_content['form_profile']['ok']=qa_lang_html('users/profile_saved'); + + if (!qa_opt('allow_private_messages')) + unset($qa_content['form_profile']['fields']['messages']); + + if (!qa_opt('mailing_enabled')) + unset($qa_content['form_profile']['fields']['mailings']); + + +// Avatar upload stuff + + if (qa_opt('avatar_allow_gravatar') || qa_opt('avatar_allow_upload')) { + $avataroptions=array(); + + if (qa_opt('avatar_default_show') && strlen(qa_opt('avatar_default_blobid'))) { + $avataroptions['']=''. + qa_get_avatar_blob_html(qa_opt('avatar_default_blobid'), qa_opt('avatar_default_width'), qa_opt('avatar_default_height'), 32). + ' '.qa_lang_html('users/avatar_default'); + } else + $avataroptions['']=qa_lang_html('users/avatar_none'); + + $avatarvalue=$avataroptions['']; + + if (qa_opt('avatar_allow_gravatar')) { + $avataroptions['gravatar']=''. + qa_get_gravatar_html($useraccount['email'], 32).' '.strtr(qa_lang_html('users/avatar_gravatar'), array( + '^1' => '', + '^2' => '', + )).''; + + if ($useraccount['flags'] & QA_USER_FLAGS_SHOW_GRAVATAR) + $avatarvalue=$avataroptions['gravatar']; + } + + if (qa_has_gd_image() && qa_opt('avatar_allow_upload')) { + $avataroptions['uploaded']=''; + + if (isset($useraccount['avatarblobid'])) + $avataroptions['uploaded']=''. + qa_get_avatar_blob_html($useraccount['avatarblobid'], $useraccount['avatarwidth'], $useraccount['avatarheight'], 32). + ''.$avataroptions['uploaded']; + + if ($useraccount['flags'] & QA_USER_FLAGS_SHOW_AVATAR) + $avatarvalue=$avataroptions['uploaded']; + } + + $qa_content['form_profile']['fields']['avatar']=array( + 'type' => 'select-radio', + 'label' => qa_lang_html('users/avatar_label'), + 'tags' => 'NAME="avatar"', + 'options' => $avataroptions, + 'value' => $avatarvalue, + 'error' => qa_html(@$errors['avatar']), + ); + + } else + unset($qa_content['form_profile']['fields']['avatar']); + + +// Other profile fields + + foreach ($userfields as $userfield) { + $value=@$inprofile[$userfield['fieldid']]; + if (!isset($value)) + $value=@$userprofile[$userfield['title']]; + + $label=trim(qa_user_userfield_label($userfield), ':'); + if (strlen($label)) + $label.=':'; + + $qa_content['form_profile']['fields'][$userfield['title']]=array( + 'label' => qa_html($label), + 'tags' => 'NAME="field_'.$userfield['fieldid'].'"', + 'value' => qa_html($value), + 'error' => qa_html(@$errors[$userfield['fieldid']]), + 'rows' => ($userfield['flags'] & QA_FIELD_FLAGS_MULTI_LINE) ? 8 : null, + ); + } + + +// Raw information for plugin layers to access + + $qa_content['raw']['account']=$useraccount; + $qa_content['raw']['profile']=$userprofile; + $qa_content['raw']['points']=$userpoints; + + +// Change password form + + $qa_content['form_password']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + + 'style' => 'wide', + + 'title' => qa_lang_html('users/change_password'), + + 'fields' => array( + 'old' => array( + 'label' => qa_lang_html('users/old_password'), + 'tags' => 'NAME="oldpassword"', + 'value' => qa_html(@$inoldpassword), + 'type' => 'password', + 'error' => qa_html(@$errors['oldpassword']), + ), + + 'new_1' => array( + 'label' => qa_lang_html('users/new_password_1'), + 'tags' => 'NAME="newpassword1"', + 'type' => 'password', + 'error' => qa_html(@$errors['password']), + ), + + 'new_2' => array( + 'label' => qa_lang_html('users/new_password_2'), + 'tags' => 'NAME="newpassword2"', + 'type' => 'password', + 'error' => qa_html(@$errors['newpassword2']), + ), + ), + + 'buttons' => array( + 'change' => array( + 'label' => qa_lang_html('users/change_password'), + ), + ), + + 'hidden' => array( + 'dochangepassword' => '1', + ), + ); + + if (!$haspassword) { + $qa_content['form_password']['fields']['old']['type']='static'; + $qa_content['form_password']['fields']['old']['value']=qa_lang_html('users/password_none'); + } + + if (qa_get_state()=='password-changed') + $qa_content['form_profile']['ok']=qa_lang_html('users/password_changed'); + + + $qa_content['navigation']['sub']=qa_account_sub_navigation(); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-activity.php b/qa-include/qa-page-activity.php new file mode 100644 index 000000000..d0f0c0bb4 --- /dev/null +++ b/qa-include/qa-page-activity.php @@ -0,0 +1,92 @@ + $parentid); + + } else { + if (qa_clicked('doaddcategory')) + $editcategory=array(); + + elseif (qa_clicked('dosavecategory')) { + $parentid=qa_post_text('parent'); + $editcategory=array('parentid' => strlen($parentid) ? $parentid : null); + } + } + + $setmissing=qa_post_text('missing') || qa_get('missing'); + + $setparent=(!$setmissing) && (qa_post_text('setparent') || qa_get('setparent')) && isset($editcategory['categoryid']); + + $hassubcategory=false; + foreach ($categories as $category) + if (!strcmp($category['parentid'], $editcategoryid)) + $hassubcategory=true; + + + +// Process saving options + + $savedoptions=false; + if (qa_clicked('dosaveoptions')) { + qa_set_option('allow_no_category', (int)qa_post_text('option_allow_no_category')); + qa_set_option('allow_no_sub_category', (int)qa_post_text('option_allow_no_sub_category')); + $savedoptions=true; + } + + +// Process saving an old or new category + + if (qa_clicked('docancel')) { + if ($setmissing || $setparent) + qa_redirect(qa_request(), array('edit' => $editcategory['categoryid'])); + elseif (isset($editcategory['categoryid'])) + qa_redirect(qa_request()); + else + qa_redirect(qa_request(), array('edit' => @$editcategory['parentid'])); + + } elseif (qa_clicked('dosetmissing')) { + $inreassign=qa_get_category_field_value('reassign'); + qa_db_category_reassign($editcategory['categoryid'], $inreassign); + qa_redirect(qa_request(), array('recalc' => 1, 'edit' => $editcategory['categoryid'])); + + } elseif (qa_clicked('dosavecategory')) { + + if (qa_post_text('dodelete')) { + + if (!$hassubcategory) { + $inreassign=qa_get_category_field_value('reassign'); + qa_db_category_reassign($editcategory['categoryid'], $inreassign); + qa_db_category_delete($editcategory['categoryid']); + qa_redirect(qa_request(), array('recalc' => 1, 'edit' => $editcategory['parentid'])); + } + + } else { + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + $inname=qa_post_text('name'); + $incontent=qa_post_text('content'); + $inparentid=$setparent ? qa_get_category_field_value('parent') : $editcategory['parentid']; + $inposition=qa_post_text('position'); + $errors=array(); + + // Check the parent ID + + $incategories=qa_db_select_with_pending(qa_db_category_nav_selectspec($inparentid, true)); + + // Verify the name is legitimate for that parent ID + + if (empty($inname)) + $errors['name']=qa_lang('main/field_required'); + elseif (qa_strlen($inname)>QA_DB_MAX_CAT_PAGE_TITLE_LENGTH) + $errors['name']=qa_lang_sub('main/max_length_x', QA_DB_MAX_CAT_PAGE_TITLE_LENGTH); + else { + foreach ($incategories as $category) + if ( + (!strcmp($category['parentid'], $inparentid)) && + strcmp($category['categoryid'], @$editcategory['categoryid']) && + qa_strtolower($category['title']) == qa_strtolower($inname) + ) + $errors['name']=qa_lang('admin/category_already_used'); + } + + // Verify the slug is legitimate for that parent ID + + for ($attempt=0; $attempt<100; $attempt++) { + switch ($attempt) { + case 0: + $inslug=qa_post_text('slug'); + if (!isset($inslug)) + $inslug=implode('-', qa_string_to_words($inname)); + break; + + case 1: + $inslug=qa_lang_sub('admin/category_default_slug', $inslug); + break; + + default: + $inslug=qa_lang_sub('admin/category_default_slug', $attempt-1); + break; + } + + $matchcategoryid=qa_db_category_slug_to_id($inparentid, $inslug); // query against DB since MySQL ignores accents, etc... + + if (!isset($inparentid)) + $matchpage=qa_db_single_select(qa_db_page_full_selectspec($inslug, false)); + else + $matchpage=null; + + if (empty($inslug)) + $errors['slug']=qa_lang('main/field_required'); + elseif (qa_strlen($inslug)>QA_DB_MAX_CAT_PAGE_TAGS_LENGTH) + $errors['slug']=qa_lang_sub('main/max_length_x', QA_DB_MAX_CAT_PAGE_TAGS_LENGTH); + elseif (preg_match('/[\\+\\/]/', $inslug)) + $errors['slug']=qa_lang_sub('admin/slug_bad_chars', '+ /'); + elseif ( (!isset($inparentid)) && qa_admin_is_slug_reserved($inslug)) // only top level is a problem + $errors['slug']=qa_lang('admin/slug_reserved'); + elseif (isset($matchcategoryid) && strcmp($matchcategoryid, @$editcategory['categoryid'])) + $errors['slug']=qa_lang('admin/category_already_used'); + elseif (isset($matchpage)) + $errors['slug']=qa_lang('admin/page_already_used'); + else + unset($errors['slug']); + + if (isset($editcategory['categoryid']) || !isset($errors['slug'])) // don't try other options if editing existing category + break; + } + + // Perform appropriate database action + + if (empty($errors)) { + if (isset($editcategory['categoryid'])) { // changing existing category + qa_db_category_rename($editcategory['categoryid'], $inname, $inslug); + + $recalc=false; + + if ($setparent) { + qa_db_category_set_parent($editcategory['categoryid'], $inparentid); + $recalc=true; + } else { + qa_db_category_set_content($editcategory['categoryid'], $incontent); + qa_db_category_set_position($editcategory['categoryid'], $inposition); + $recalc=($hassubcategory && ($inslug !== $editcategory['tags'])); + } + + qa_redirect(qa_request(), array('edit' => $editcategory['categoryid'], 'saved' => true, 'recalc' => (int)$recalc)); + + } else { // creating a new one + $categoryid=qa_db_category_create($inparentid, $inname, $inslug); + + qa_db_category_set_content($categoryid, $incontent); + + if (isset($inposition)) + qa_db_category_set_position($categoryid, $inposition); + + qa_redirect(qa_request(), array('edit' => $inparentid, 'added' => true)); + } + } + } + } + + +// Prepare content for theme + + $qa_content=qa_content_prepare(); + + $qa_content['title']=qa_lang_html('admin/admin_title').' - '.qa_lang_html('admin/categories_title'); + + $qa_content['error']=qa_admin_page_error(); + + if ($setmissing) { + $qa_content['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_path_html(qa_request()).'"', + + 'style' => 'tall', + + 'fields' => array( + 'reassign' => array( + 'label' => isset($editcategory) + ? qa_lang_html_sub('admin/category_no_sub_to', qa_html($editcategory['title'])) + : qa_lang_html('admin/category_none_to'), + 'loose' => true, + ), + ), + + 'buttons' => array( + 'save' => array( + 'label' => qa_lang_html('main/save_button'), + ), + + 'cancel' => array( + 'tags' => 'NAME="docancel"', + 'label' => qa_lang_html('main/cancel_button'), + ), + ), + + 'hidden' => array( + 'dosetmissing' => '1', // for IE + 'edit' => @$editcategory['categoryid'], + 'missing' => '1', + ), + ); + + qa_set_up_category_field($qa_content, $qa_content['form']['fields']['reassign'], 'reassign', + $categories, @$editcategory['categoryid'], qa_opt('allow_no_category'), qa_opt('allow_no_sub_category')); + + + } elseif (isset($editcategory)) { + + $qa_content['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_path_html(qa_request()).'"', + + 'style' => 'tall', + + 'ok' => qa_get('saved') ? qa_lang_html('admin/category_saved') : (qa_get('added') ? qa_lang_html('admin/category_added') : null), + + 'fields' => array( + 'name' => array( + 'id' => 'name_display', + 'tags' => 'NAME="name" ID="name"', + 'label' => qa_lang_html(count($categories) ? 'admin/category_name' : 'admin/category_name_first'), + 'value' => qa_html(isset($inname) ? $inname : @$editcategory['title']), + 'error' => qa_html(@$errors['name']), + ), + + 'questions' => array(), + + 'delete' => array(), + + 'reassign' => array(), + + 'slug' => array( + 'id' => 'slug_display', + 'tags' => 'NAME="slug"', + 'label' => qa_lang_html('admin/category_slug'), + 'value' => qa_html(isset($inslug) ? $inslug : @$editcategory['tags']), + 'error' => qa_html(@$errors['slug']), + ), + + 'content' => array( + 'id' => 'content_display', + 'tags' => 'NAME="content"', + 'label' => qa_lang_html('admin/category_description'), + 'value' => qa_html(isset($incontent) ? $incontent : @$editcategory['content']), + 'error' => qa_html(@$errors['content']), + 'rows' => 2, + ), + ), + + 'buttons' => array( + 'save' => array( + 'label' => qa_lang_html(isset($editcategory['categoryid']) ? 'main/save_button' : 'admin/add_category_button'), + ), + + 'cancel' => array( + 'tags' => 'NAME="docancel"', + 'label' => qa_lang_html('main/cancel_button'), + ), + ), + + 'hidden' => array( + 'dosavecategory' => '1', // for IE + 'edit' => @$editcategory['categoryid'], + 'parent' => @$editcategory['parentid'], + 'setparent' => (int)$setparent, + ), + ); + + + if ($setparent) { + unset($qa_content['form']['fields']['delete']); + unset($qa_content['form']['fields']['reassign']); + unset($qa_content['form']['fields']['questions']); + unset($qa_content['form']['fields']['content']); + + $qa_content['form']['fields']['parent']=array( + 'label' => qa_lang_html('admin/category_parent'), + ); + + $childdepth=qa_db_category_child_depth($editcategory['categoryid']); + + qa_set_up_category_field($qa_content, $qa_content['form']['fields']['parent'], 'parent', + isset($incategories) ? $incategories : $categories, isset($inparentid) ? $inparentid : @$editcategory['parentid'], + true, true, QA_CATEGORY_DEPTH-1-$childdepth, @$editcategory['categoryid']); + + $qa_content['form']['fields']['parent']['options']['']=qa_lang_html('admin/category_top_level'); + + @$qa_content['form']['fields']['parent']['note'].=qa_lang_html_sub('admin/category_max_depth_x', QA_CATEGORY_DEPTH); + + } elseif (isset($editcategory['categoryid'])) { // existing category + if ($hassubcategory) { + $qa_content['form']['fields']['name']['note']=qa_lang_html('admin/category_no_delete_subs'); + unset($qa_content['form']['fields']['delete']); + unset($qa_content['form']['fields']['reassign']); + + } else { + $qa_content['form']['fields']['delete']=array( + 'tags' => 'NAME="dodelete" ID="dodelete"', + 'label' => + ''.qa_lang_html('admin/delete_category_reassign').''. + '', + 'value' => 0, + 'type' => 'checkbox', + ); + + $qa_content['form']['fields']['reassign']=array( + 'id' => 'reassign_display', + 'tags' => 'NAME="reassign"', + ); + + qa_set_up_category_field($qa_content, $qa_content['form']['fields']['reassign'], 'reassign', + $categories, $editcategory['parentid'], true, true, null, $editcategory['categoryid']); + } + + $qa_content['form']['fields']['questions']=array( + 'label' => qa_lang_html('admin/total_qs'), + 'type' => 'static', + 'value' => ''. + ( ($editcategory['qcount']==1) + ? qa_lang_html_sub('main/1_question', '1', '1') + : qa_lang_html_sub('main/x_questions', number_format($editcategory['qcount'])) + ).'', + ); + + if ($hassubcategory && !qa_opt('allow_no_sub_category')) { + $nosubcount=qa_db_count_categoryid_qs($editcategory['categoryid']); + + if ($nosubcount) + $qa_content['form']['fields']['questions']['error']= + strtr(qa_lang_html('admin/category_no_sub_error'), array( + '^q' => number_format($nosubcount), + '^1' => '', + '^2' => '', + )); + } + + qa_set_display_rules($qa_content, array( + 'position_display' => '!dodelete', + 'slug_display' => '!dodelete', + 'content_display' => '!dodelete', + 'parent_display' => '!dodelete', + 'children_display' => '!dodelete', + 'reassign_display' => 'dodelete', + 'reassign_shown' => 'dodelete', + 'reassign_hidden' => '!dodelete', + )); + + } else { // new category + unset($qa_content['form']['fields']['delete']); + unset($qa_content['form']['fields']['reassign']); + unset($qa_content['form']['fields']['slug']); + unset($qa_content['form']['fields']['questions']); + + $qa_content['focusid']='name'; + } + + if (!$setparent) { + $pathhtml=qa_category_path_html($categories, @$editcategory['parentid']); + + if (count($categories)) { + $qa_content['form']['fields']['parent']=array( + 'id' => 'parent_display', + 'label' => qa_lang_html('admin/category_parent'), + 'type' => 'static', + 'value' => (strlen($pathhtml) ? $pathhtml : qa_lang_html('admin/category_top_level')), + ); + + $qa_content['form']['fields']['parent']['value']= + ''. + $qa_content['form']['fields']['parent']['value'].''; + + if (isset($editcategory['categoryid'])) + $qa_content['form']['fields']['parent']['value'].=' - '. + ''.qa_lang_html('admin/category_move_parent').''; + } + + $positionoptions=array(); + + $previous=null; + $passedself=false; + + foreach ($categories as $key => $category) + if (!strcmp($category['parentid'], @$editcategory['parentid'])) { + if (isset($previous)) + $positionhtml=qa_lang_html_sub('admin/after_x', qa_html($passedself ? $category['title'] : $previous['title'])); + else + $positionhtml=qa_lang_html('admin/first'); + + $positionoptions[$category['position']]=$positionhtml; + + if (!strcmp($category['categoryid'], @$editcategory['categoryid'])) + $passedself=true; + + $previous=$category; + } + + if (isset($editcategory['position'])) + $positionvalue=$positionoptions[$editcategory['position']]; + + else { + $positionvalue=isset($previous) ? qa_lang_html_sub('admin/after_x', qa_html($previous['title'])) : qa_lang_html('admin/first'); + $positionoptions[1+@max(array_keys($positionoptions))]=$positionvalue; + } + + $qa_content['form']['fields']['position']=array( + 'id' => 'position_display', + 'tags' => 'NAME="position"', + 'label' => qa_lang_html('admin/position'), + 'type' => 'select', + 'options' => $positionoptions, + 'value' => $positionvalue, + ); + + if (isset($editcategory['categoryid'])) { + $catdepth=count(qa_category_path($categories, $editcategory['categoryid'])); + + if ($catdepth'.qa_html($category['title']).''. + ' ('.$category['qcount'].')'; + + if (!strlen($childrenhtml)) + $childrenhtml=qa_lang_html('admin/category_no_subs'); + + $childrenhtml.=' - '.qa_lang_html('admin/category_add_sub').''; + + $qa_content['form']['fields']['children']=array( + 'id' => 'children_display', + 'label' => qa_lang_html('admin/category_subs'), + 'type' => 'static', + 'value' => $childrenhtml, + ); + } else { + $qa_content['form']['fields']['name']['note']=qa_lang_html_sub('admin/category_no_add_subs_x', QA_CATEGORY_DEPTH); + } + + } + } + + } else { + $qa_content['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_path_html(qa_request()).'"', + + 'ok' => $savedoptions ? qa_lang_html('admin/options_saved') : null, + + 'style' => 'tall', + + 'fields' => array( + 'intro' => array( + 'label' => qa_lang_html('admin/categories_introduction'), + 'type' => 'static', + ), + ), + + 'buttons' => array( + 'save' => array( + 'tags' => 'NAME="dosaveoptions"', + 'label' => qa_lang_html('main/save_button'), + ), + + 'add' => array( + 'tags' => 'NAME="doaddcategory"', + 'label' => qa_lang_html('admin/add_category_button'), + ), + ), + ); + + if (count($categories)) { + unset($qa_content['form']['fields']['intro']); + + $navcategoryhtml=''; + + foreach ($categories as $category) + if (!isset($category['parentid'])) + $navcategoryhtml.=''. + qa_html($category['title']).' - '.qa_lang_html_sub('main/x_questions', $category['qcount']).'
'; + + $qa_content['form']['fields']['nav']=array( + 'label' => qa_lang_html('admin/top_level_categories'), + 'type' => 'static', + 'value' => $navcategoryhtml, + ); + + $qa_content['form']['fields']['allow_no_category']=array( + 'label' => qa_lang_html('options/allow_no_category'), + 'tags' => 'NAME="option_allow_no_category"', + 'type' => 'checkbox', + 'value' => qa_opt('allow_no_category'), + ); + + if (!qa_opt('allow_no_category')) { + $nocatcount=qa_db_count_categoryid_qs(null); + + if ($nocatcount) + $qa_content['form']['fields']['allow_no_category']['error']= + strtr(qa_lang_html('admin/category_none_error'), array( + '^q' => number_format($nocatcount), + '^1' => '', + '^2' => '', + )); + } + + $qa_content['form']['fields']['allow_no_sub_category']=array( + 'label' => qa_lang_html('options/allow_no_sub_category'), + 'tags' => 'NAME="option_allow_no_sub_category"', + 'type' => 'checkbox', + 'value' => qa_opt('allow_no_sub_category'), + ); + + } else + unset($qa_content['form']['buttons']['save']); + } + + if (qa_get('recalc')) { + $qa_content['form']['ok']=''.qa_lang_html('admin/recalc_categories').''; + + $qa_content['script_rel'][]='qa-content/qa-admin.js?'.QA_VERSION; + $qa_content['script_var']['qa_warning_recalc']=qa_lang('admin/stop_recalc_warning'); + + $qa_content['script_onloads'][]=array( + "qa_recalc_click('dorecalccategories', document.getElementById('recalc_ok'), null, 'recalc_ok');" + ); + } + + $qa_content['navigation']['sub']=qa_admin_sub_navigation(); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-admin-default.php b/qa-include/qa-page-admin-default.php new file mode 100644 index 000000000..13b74bd2c --- /dev/null +++ b/qa-include/qa-page-admin-default.php @@ -0,0 +1,1642 @@ + 'number', + 'avatar_q_list_size' => 'number', + 'avatar_q_page_a_size' => 'number', + 'avatar_q_page_c_size' => 'number', + 'avatar_q_page_q_size' => 'number', + 'avatar_store_size' => 'number', + 'avatar_users_size' => 'number', + 'columns_tags' => 'number', + 'columns_users' => 'number', + 'feed_number_items' => 'number', + 'flagging_hide_after' => 'number', + 'flagging_notify_every' => 'number', + 'flagging_notify_first' => 'number', + 'hot_weight_a_age' => 'number', + 'hot_weight_answers' => 'number', + 'hot_weight_q_age' => 'number', + 'hot_weight_views' => 'number', + 'hot_weight_votes' => 'number', + 'logo_height' => 'number-blank', + 'logo_width' => 'number-blank', + 'mailing_per_minute' => 'number', + 'max_len_q_title' => 'number', + 'max_num_q_tags' => 'number', + 'max_rate_ip_as' => 'number', + 'max_rate_ip_cs' => 'number', + 'max_rate_ip_flags' => 'number', + 'max_rate_ip_logins' => 'number', + 'max_rate_ip_messages' => 'number', + 'max_rate_ip_qs' => 'number', + 'max_rate_ip_registers' => 'number', + 'max_rate_ip_uploads' => 'number', + 'max_rate_ip_votes' => 'number', + 'max_rate_user_as' => 'number', + 'max_rate_user_cs' => 'number', + 'max_rate_user_flags' => 'number', + 'max_rate_user_messages' => 'number', + 'max_rate_user_qs' => 'number', + 'max_rate_user_uploads' => 'number', + 'max_rate_user_votes' => 'number', + 'min_len_a_content' => 'number', + 'min_len_c_content' => 'number', + 'min_len_q_content' => 'number', + 'min_len_q_title' => 'number', + 'min_num_q_tags' => 'number', + 'page_size_activity' => 'number', + 'page_size_ask_check_qs' => 'number', + 'page_size_ask_tags' => 'number', + 'page_size_home' => 'number', + 'page_size_hot_qs' => 'number', + 'page_size_q_as' => 'number', + 'page_size_qs' => 'number', + 'page_size_related_qs' => 'number', + 'page_size_search' => 'number', + 'page_size_tag_qs' => 'number', + 'page_size_tags' => 'number', + 'page_size_una_qs' => 'number', + 'page_size_user_posts' => 'number', + 'page_size_users' => 'number', + 'pages_prev_next' => 'number', + 'q_urls_title_length' => 'number', + 'show_fewer_cs_count' => 'number', + 'show_fewer_cs_from' => 'number', + 'show_full_date_days' => 'number', + 'smtp_port' => 'number', + + 'allow_change_usernames' => 'checkbox', + 'allow_close_questions' => 'checkbox', + 'allow_multi_answers' => 'checkbox', + 'allow_private_messages' => 'checkbox', + 'allow_self_answer' => 'checkbox', + 'allow_view_q_bots' => 'checkbox', + 'avatar_allow_gravatar' => 'checkbox', + 'avatar_allow_upload' => 'checkbox', + 'avatar_default_show' => 'checkbox', + 'captcha_on_anon_post' => 'checkbox', + 'captcha_on_feedback' => 'checkbox', + 'captcha_on_register' => 'checkbox', + 'captcha_on_reset_password' => 'checkbox', + 'captcha_on_unconfirmed' => 'checkbox', + 'comment_on_as' => 'checkbox', + 'comment_on_qs' => 'checkbox', + 'confirm_user_emails' => 'checkbox', + 'confirm_user_required' => 'checkbox', + 'do_ask_check_qs' => 'checkbox', + 'do_close_on_select' => 'checkbox', + 'do_complete_tags' => 'checkbox', + 'do_count_q_views' => 'checkbox', + 'do_example_tags' => 'checkbox', + 'extra_field_active' => 'checkbox', + 'extra_field_display' => 'checkbox', + 'feed_for_activity' => 'checkbox', + 'feed_for_hot' => 'checkbox', + 'feed_for_qa' => 'checkbox', + 'feed_for_questions' => 'checkbox', + 'feed_for_search' => 'checkbox', + 'feed_for_tag_qs' => 'checkbox', + 'feed_for_unanswered' => 'checkbox', + 'feed_full_text' => 'checkbox', + 'feed_per_category' => 'checkbox', + 'feedback_enabled' => 'checkbox', + 'flagging_of_posts' => 'checkbox', + 'follow_on_as' => 'checkbox', + 'links_in_new_window' => 'checkbox', + 'logo_show' => 'checkbox', + 'mailing_enabled' => 'checkbox', + 'moderate_anon_post' => 'checkbox', + 'moderate_by_points' => 'checkbox', + 'moderate_notify_admin' => 'checkbox', + 'moderate_points_limit' => 'number', + 'moderate_unconfirmed' => 'checkbox', + 'neat_urls' => 'checkbox', + 'notify_admin_q_post' => 'checkbox', + 'notify_users_default' => 'checkbox', + 'q_urls_remove_accents' => 'checkbox', + 'show_c_reply_buttons' => 'checkbox', + 'show_custom_answer' => 'checkbox', + 'show_custom_ask' => 'checkbox', + 'show_custom_comment' => 'checkbox', + 'show_custom_footer' => 'checkbox', + 'show_custom_header' => 'checkbox', + 'show_custom_home' => 'checkbox', + 'show_custom_in_head' => 'checkbox', + 'show_custom_register' => 'checkbox', + 'show_custom_sidebar' => 'checkbox', + 'show_custom_sidepanel' => 'checkbox', + 'show_custom_welcome' => 'checkbox', + 'show_home_description' => 'checkbox', + 'show_message_history' => 'checkbox', + 'show_notice_visitor' => 'checkbox', + 'show_notice_welcome' => 'checkbox', + 'show_selected_first' => 'checkbox', + 'show_url_links' => 'checkbox', + 'show_user_points' => 'checkbox', + 'show_user_titles' => 'checkbox', + 'show_view_counts' => 'checkbox', + 'show_when_created' => 'checkbox', + 'site_maintenance' => 'checkbox', + 'smtp_active' => 'checkbox', + 'smtp_authenticate' => 'checkbox', + 'suspend_register_users' => 'checkbox', + 'tag_separator_comma' => 'checkbox', + 'votes_separated' => 'checkbox', + 'voting_on_as' => 'checkbox', + 'voting_on_q_page_only' => 'checkbox', + 'voting_on_qs' => 'checkbox', + + 'smtp_password' => 'password', + ); + + $optionmaximum=array( + 'feed_number_items' => QA_DB_RETRIEVE_QS_AS, + 'max_len_q_title' => QA_DB_MAX_TITLE_LENGTH, + 'page_size_activity' => QA_DB_RETRIEVE_QS_AS, + 'page_size_ask_check_qs' => QA_DB_RETRIEVE_QS_AS, + 'page_size_ask_tags' => QA_DB_RETRIEVE_QS_AS, + 'page_size_home' => QA_DB_RETRIEVE_QS_AS, + 'page_size_hot_qs' => QA_DB_RETRIEVE_QS_AS, + 'page_size_qs' => QA_DB_RETRIEVE_QS_AS, + 'page_size_related_qs' => QA_DB_RETRIEVE_QS_AS, + 'page_size_search' => QA_DB_RETRIEVE_QS_AS, + 'page_size_tag_qs' => QA_DB_RETRIEVE_QS_AS, + 'page_size_tags' => QA_DB_RETRIEVE_TAGS, + 'page_size_una_qs' => QA_DB_RETRIEVE_QS_AS, + 'page_size_user_posts' => QA_DB_RETRIEVE_QS_AS, + 'page_size_users' => QA_DB_RETRIEVE_USERS, + ); + + $optionminimum=array( + 'flagging_hide_after' => 2, + 'flagging_notify_every' => 1, + 'flagging_notify_first' => 1, + 'max_num_q_tags' => 2, + 'page_size_activity' => 1, + 'page_size_ask_check_qs' => 3, + 'page_size_ask_tags' => 3, + 'page_size_home' => 1, + 'page_size_hot_qs' => 1, + 'page_size_q_as' => 1, + 'page_size_qs' => 1, + 'page_size_search' => 1, + 'page_size_tag_qs' => 1, + 'page_size_tags' => 1, + 'page_size_users' => 1, + ); + + +// Define the options to show (and some other visual stuff) based on request + + $formstyle='tall'; + $checkboxtodisplay=null; + + switch ($adminsection) { + case 'general': + $subtitle='admin/general_title'; + $showoptions=array('site_title', 'site_url', 'neat_urls', 'site_language', 'site_theme', 'site_theme_mobile', 'tags_or_categories', 'site_maintenance'); + break; + + case 'emails': + $subtitle='admin/emails_title'; + $showoptions=array( + 'from_email', 'feedback_email', 'notify_admin_q_post', 'feedback_enabled', 'email_privacy', + 'smtp_active', 'smtp_address', 'smtp_port', 'smtp_secure', 'smtp_authenticate', 'smtp_username', 'smtp_password' + ); + + $checkboxtodisplay=array( + 'smtp_address' => 'option_smtp_active', + 'smtp_port' => 'option_smtp_active', + 'smtp_secure' => 'option_smtp_active', + 'smtp_authenticate' => 'option_smtp_active', + 'smtp_username' => 'option_smtp_active && option_smtp_authenticate', + 'smtp_password' => 'option_smtp_active && option_smtp_authenticate', + ); + break; + + case 'users': + $subtitle='admin/users_title'; + + $showoptions=array('show_notice_visitor', 'notice_visitor'); + + if (!QA_FINAL_EXTERNAL_USERS) { + require_once QA_INCLUDE_DIR.'qa-util-image.php'; + + array_push($showoptions, 'show_custom_register', 'custom_register', 'show_notice_welcome', 'notice_welcome', 'show_custom_welcome', 'custom_welcome'); + + array_push($showoptions, '' ,'allow_change_usernames', 'allow_private_messages', 'show_message_history', '', 'avatar_allow_gravatar'); + + if (qa_has_gd_image()) + array_push($showoptions, 'avatar_allow_upload', 'avatar_store_size', 'avatar_default_show'); + + array_push($showoptions, '', 'avatar_profile_size', 'avatar_users_size', 'avatar_q_page_q_size', 'avatar_q_page_a_size', 'avatar_q_page_c_size', 'avatar_q_list_size', ''); + } + + $checkboxtodisplay=array( + 'custom_register' => 'option_show_custom_register', + 'custom_welcome' => 'option_show_custom_welcome', + 'notice_welcome' => 'option_show_notice_welcome', + 'notice_visitor' => 'option_show_notice_visitor', + 'show_message_history' => 'option_allow_private_messages', + 'avatar_store_size' => 'option_avatar_allow_upload', + 'avatar_default_show' => 'option_avatar_allow_gravatar || option_avatar_allow_upload', + 'avatar_profile_size' => 'option_avatar_allow_gravatar || option_avatar_allow_upload', + 'avatar_users_size' => 'option_avatar_allow_gravatar || option_avatar_allow_upload', + 'avatar_q_page_q_size' => 'option_avatar_allow_gravatar || option_avatar_allow_upload', + 'avatar_q_page_a_size' => 'option_avatar_allow_gravatar || option_avatar_allow_upload', + 'avatar_q_page_c_size' => 'option_avatar_allow_gravatar || option_avatar_allow_upload', + 'avatar_q_list_size' => 'option_avatar_allow_gravatar || option_avatar_allow_upload', + ); + + $formstyle='wide'; + break; + + case 'layout': + $subtitle='admin/layout_title'; + $showoptions=array('logo_show', 'logo_url', 'logo_width', 'logo_height', '', 'show_custom_sidebar', 'custom_sidebar', 'show_custom_sidepanel', 'custom_sidepanel', 'show_custom_header', 'custom_header', 'show_custom_footer', 'custom_footer', 'show_custom_in_head', 'custom_in_head', 'show_custom_home', 'custom_home_heading', 'custom_home_content', 'show_home_description', 'home_description', ''); + + $checkboxtodisplay=array( + 'logo_url' => 'option_logo_show', + 'logo_width' => 'option_logo_show', + 'logo_height' => 'option_logo_show', + 'custom_sidebar' => 'option_show_custom_sidebar', + 'custom_sidepanel' => 'option_show_custom_sidepanel', + 'custom_header' => 'option_show_custom_header', + 'custom_footer' => 'option_show_custom_footer', + 'custom_in_head' => 'option_show_custom_in_head', + 'custom_home_heading' => 'option_show_custom_home', + 'custom_home_content' => 'option_show_custom_home', + 'home_description' => 'option_show_home_description', + ); + break; + + case 'viewing': + $subtitle='admin/viewing_title'; + $showoptions=array('q_urls_title_length', 'q_urls_remove_accents', 'do_count_q_views', 'show_view_counts', '', 'voting_on_qs', 'voting_on_q_page_only', 'voting_on_as', 'votes_separated', '', 'show_url_links', 'links_in_new_window', 'show_when_created', 'show_full_date_days'); + + if (count(qa_get_points_to_titles())) + $showoptions[]='show_user_titles'; + + array_push($showoptions, 'show_user_points', '', 'sort_answers_by', 'show_selected_first', 'page_size_q_as', 'show_a_form_immediate'); + + if (qa_opt('comment_on_qs') || qa_opt('command_on_as')) + array_push($showoptions, 'show_fewer_cs_from', 'show_fewer_cs_count', 'show_c_reply_buttons'); + + $showoptins[]=''; + + $widgets=qa_db_single_select(qa_db_widgets_selectspec()); + + foreach ($widgets as $widget) + if ($widget['title']=='Related Questions') { + array_push($showoptions, 'match_related_qs', 'page_size_related_qs', ''); + break; + } + + $showoptions[]='pages_prev_next'; + + $formstyle='wide'; + + $checkboxtodisplay=array( + 'show_view_counts' => 'option_do_count_q_views', + 'votes_separated' => 'option_voting_on_qs || option_voting_on_as', + 'voting_on_q_page_only' => 'option_voting_on_qs', + 'show_full_date_days' => 'option_show_when_created', + ); + break; + + case 'lists': + $subtitle='admin/lists_title'; + + $showoptions=array('page_size_home', 'page_size_activity', 'page_size_qs', 'page_size_hot_qs', 'page_size_una_qs'); + + if (qa_using_tags()) + $showoptions[]='page_size_tag_qs'; + + array_push($showoptions, 'page_size_user_posts', ''); + + if (qa_using_tags()) + array_push($showoptions, 'page_size_tags', 'columns_tags'); + + array_push($showoptions, 'page_size_users', 'columns_users', ''); + + $searchmodules=qa_load_modules_with('search', 'process_search'); + + if (count($searchmodules)) + $showoptions[]='search_module'; + + $showoptions[]='page_size_search'; + + array_push($showoptions, '', 'admin/hotness_factors', 'hot_weight_q_age', 'hot_weight_a_age', 'hot_weight_answers', 'hot_weight_votes'); + + if (qa_opt('do_count_q_views')) + $showoptions[]='hot_weight_views'; + + $formstyle='wide'; + + break; + + case 'posting': + $getoptions=qa_get_options(array('tags_or_categories')); + + $subtitle='admin/posting_title'; + + $showoptions=array('do_close_on_select', 'allow_close_questions', 'allow_self_answer', 'allow_multi_answers', 'follow_on_as', 'comment_on_qs', 'comment_on_as', ''); + + if (count(qa_list_modules('editor'))>1) + array_push($showoptions, 'editor_for_qs', 'editor_for_as', 'editor_for_cs', ''); + + array_push($showoptions, 'show_custom_ask', 'custom_ask', 'extra_field_active', 'extra_field_prompt', 'extra_field_display', 'extra_field_label', 'show_custom_answer', 'custom_answer', 'show_custom_comment', 'custom_comment', ''); + + array_push($showoptions, 'min_len_q_title', 'max_len_q_title', 'min_len_q_content'); + + if (qa_using_tags()) + array_push($showoptions, 'min_num_q_tags', 'max_num_q_tags', 'tag_separator_comma'); + + array_push($showoptions, 'min_len_a_content', 'min_len_c_content', 'notify_users_default', '', 'block_bad_words', '', 'do_ask_check_qs', 'match_ask_check_qs', 'page_size_ask_check_qs', ''); + + if (qa_using_tags()) + array_push($showoptions, 'do_example_tags', 'match_example_tags', 'do_complete_tags', 'page_size_ask_tags'); + + $formstyle='wide'; + + $checkboxtodisplay=array( + 'editor_for_cs' => 'option_comment_on_qs || option_comment_on_as', + 'custom_ask' => 'option_show_custom_ask', + 'extra_field_prompt' => 'option_extra_field_active', + 'extra_field_display' => 'option_extra_field_active', + 'extra_field_label' => 'option_extra_field_active && option_extra_field_display', + 'extra_field_label_hidden' => '!option_extra_field_display', + 'extra_field_label_shown' => 'option_extra_field_display', + 'custom_answer' => 'option_show_custom_answer', + 'show_custom_comment' => 'option_comment_on_qs || option_comment_on_as', + 'custom_comment' => 'option_show_custom_comment && (option_comment_on_qs || option_comment_on_as)', + 'min_len_c_content' => 'option_comment_on_qs || option_comment_on_as', + 'match_ask_check_qs' => 'option_do_ask_check_qs', + 'page_size_ask_check_qs' => 'option_do_ask_check_qs', + 'match_example_tags' => 'option_do_example_tags', + 'page_size_ask_tags' => 'option_do_example_tags || option_do_complete_tags', + ); + break; + + case 'permissions': + $subtitle='admin/permissions_title'; + + $permitoptions=qa_get_permit_options(); + + $showoptions=array(); + $checkboxtodisplay=array(); + + foreach ($permitoptions as $permitoption) { + $showoptions[]=$permitoption; + + if ($permitoption=='permit_view_q_page') { + $showoptions[]='allow_view_q_bots'; + $checkboxtodisplay['allow_view_q_bots']='option_permit_view_q_page<'.qa_js(QA_PERMIT_ALL); + + } else { + $showoptions[]=$permitoption.'_points'; + $checkboxtodisplay[$permitoption.'_points']='(option_'.$permitoption.'=='.qa_js(QA_PERMIT_POINTS).') ||(option_'.$permitoption.'=='.qa_js(QA_PERMIT_POINTS_CONFIRMED).')'; + } + } + + $formstyle='wide'; + break; + + case 'feeds': + $subtitle='admin/feeds_title'; + + $showoptions=array('feed_for_questions', 'feed_for_qa', 'feed_for_activity'); + + array_push($showoptions, 'feed_for_hot', 'feed_for_unanswered'); + + if (qa_using_tags()) + $showoptions[]='feed_for_tag_qs'; + + if (qa_using_categories()) + $showoptions[]='feed_per_category'; + + array_push($showoptions, 'feed_for_search', '', 'feed_number_items', 'feed_full_text'); + + $formstyle='wide'; + + $checkboxtodisplay=array( + 'feed_per_category' => 'option_feed_for_qa || option_feed_for_questions || option_feed_for_unanswered || option_feed_for_activity', + ); + break; + + case 'spam': + $subtitle='admin/spam_title'; + + $showoptions=array(); + + $getoptions=qa_get_options(array('feedback_enabled', 'permit_post_q', 'permit_post_a', 'permit_post_c')); + + if (!QA_FINAL_EXTERNAL_USERS) + array_push($showoptions, 'confirm_user_emails', 'confirm_user_required', 'suspend_register_users', ''); + + $maxpermitpost=max($getoptions['permit_post_q'], $getoptions['permit_post_a'], $getoptions['permit_post_c']); + + $captchamodules=qa_list_modules('captcha'); + + if (count($captchamodules)) { + if (!QA_FINAL_EXTERNAL_USERS) + array_push($showoptions, 'captcha_on_register', 'captcha_on_reset_password'); + + if ($maxpermitpost > QA_PERMIT_USERS) + $showoptions[]='captcha_on_anon_post'; + + if ($maxpermitpost > QA_PERMIT_CONFIRMED) + $showoptions[]='captcha_on_unconfirmed'; + + if ($getoptions['feedback_enabled']) + $showoptions[]='captcha_on_feedback'; + + $showoptions[]='captcha_module'; + } + + $showoptions[]=''; + + if ($maxpermitpost > QA_PERMIT_USERS) + $showoptions[]='moderate_anon_post'; + + if ($maxpermitpost > QA_PERMIT_CONFIRMED) + $showoptions[]='moderate_unconfirmed'; + + if ($maxpermitpost > QA_PERMIT_EXPERTS) + array_push($showoptions, 'moderate_by_points', 'moderate_points_limit', 'moderate_notify_admin', ''); + + array_push($showoptions, 'flagging_of_posts', 'flagging_notify_first', 'flagging_notify_every', 'flagging_hide_after', ''); + + $checkboxtodisplay=array( + 'confirm_user_required' => 'option_confirm_user_emails', + 'flagging_hide_after' => 'option_flagging_of_posts', + 'flagging_notify_every' => 'option_flagging_of_posts', + 'flagging_notify_first' => 'option_flagging_of_posts', + 'max_rate_ip_flags' => 'option_flagging_of_posts', + 'max_rate_user_flags' => 'option_flagging_of_posts', + ); + + array_push($showoptions, 'block_ips_write', ''); + + if (!QA_FINAL_EXTERNAL_USERS) + array_push($showoptions, 'max_rate_ip_registers', 'max_rate_ip_logins', ''); + + array_push($showoptions, + 'max_rate_ip_qs', 'max_rate_user_qs', 'max_rate_ip_as', 'max_rate_user_as', 'max_rate_ip_cs', 'max_rate_user_cs', '', + 'max_rate_ip_votes', 'max_rate_user_votes', 'max_rate_ip_flags', 'max_rate_user_flags', 'max_rate_ip_uploads', 'max_rate_user_uploads' + ); + + if (qa_opt('allow_private_messages')) { + $showoptions[]='max_rate_ip_messages'; + $showoptions[]='max_rate_user_messages'; + } + + $formstyle='wide'; + + if ($maxpermitpost > QA_PERMIT_USERS) + $checkboxtodisplay=array_merge($checkboxtodisplay, array( + 'captcha_on_unconfirmed' => 'option_confirm_user_emails && option_captcha_on_anon_post', + 'captcha_module' => 'option_captcha_on_register || option_captcha_on_anon_post || option_captcha_on_reset_password || option_captcha_on_feedback', + 'moderate_unconfirmed' => 'option_confirm_user_emails && option_moderate_anon_post', + 'moderate_by_points' => 'option_moderate_anon_post', + 'moderate_points_limit' => 'option_moderate_anon_post && option_moderate_by_points', + 'moderate_points_label_off' => 'option_moderate_anon_post && !option_moderate_by_points', + 'moderate_points_label_on' => 'option_moderate_anon_post && option_moderate_by_points', + )); + else + $checkboxtodisplay=array_merge($checkboxtodisplay, array( + 'captcha_on_unconfirmed' => 'option_confirm_user_emails', + 'captcha_module' => 'option_captcha_on_register || option_captcha_on_unconfirmed || option_captcha_on_reset_password || option_captcha_on_feedback', + 'moderate_unconfirmed' => 'option_confirm_user_emails', + 'moderate_points_limit' => 'option_moderate_by_points', + 'moderate_points_label_off' => '!option_moderate_by_points', + 'moderate_points_label_on' => 'option_moderate_by_points', + )); + break; + + case 'mailing': + require_once QA_INCLUDE_DIR.'qa-app-mailing.php'; + + $subtitle='admin/mailing_title'; + + $showoptions=array('mailing_enabled', 'mailing_from_name', 'mailing_from_email', 'mailing_subject', 'mailing_body', 'mailing_per_minute'); + break; + + default: + $pagemodules=qa_load_modules_with('page', 'match_request'); + $request=qa_request(); + + foreach ($pagemodules as $pagemodule) + if ($pagemodule->match_request($request)) + return $pagemodule->process_request($request); + + return include QA_INCLUDE_DIR.'qa-page-not-found.php'; + break; + } + + +// Filter out blanks to get list of valid options + + $getoptions=array(); + foreach ($showoptions as $optionname) + if (strlen($optionname) && (strpos($optionname, '/')===false)) // empties represent spacers in forms + $getoptions[]=$optionname; + + +// Process user actions + + $errors=array(); + + $recalchotness=false; + $startmailing=false; + + $formokhtml=null; + + if (qa_clicked('doresetoptions')) { + qa_reset_options($getoptions); + $formokhtml=qa_lang_html('admin/options_reset'); + + } elseif (qa_clicked('dosaveoptions')) { + foreach ($getoptions as $optionname) { + $optionvalue=qa_post_text('option_'.$optionname); + + if ( + (@$optiontype[$optionname]=='number') || + (@$optiontype[$optionname]=='checkbox') || + ((@$optiontype[$optionname]=='number-blank') && strlen($optionvalue)) + ) + $optionvalue=(int)$optionvalue; + + if (isset($optionmaximum[$optionname])) + $optionvalue=min($optionmaximum[$optionname], $optionvalue); + + if (isset($optionminimum[$optionname])) + $optionvalue=max($optionminimum[$optionname], $optionvalue); + + switch ($optionname) { + case 'site_url': + if (substr($optionvalue, -1)!='/') // seems to be a very common mistake and will mess up URLs + $optionvalue.='/'; + break; + + case 'hot_weight_views': + case 'hot_weight_answers': + case 'hot_weight_votes': + case 'hot_weight_q_age': + case 'hot_weight_a_age': + if (qa_opt($optionname) != $optionvalue) + $recalchotness=true; + break; + + case 'block_ips_write': + require_once QA_INCLUDE_DIR.'qa-app-limits.php'; + $optionvalue=implode(' , ', qa_block_ips_explode($optionvalue)); + break; + + case 'block_bad_words': + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + $optionvalue=implode(' , ', qa_block_words_explode($optionvalue)); + break; + } + + qa_set_option($optionname, $optionvalue); + } + + $formokhtml=qa_lang_html('admin/options_saved'); + + // Uploading default avatar + + if (is_array(@$_FILES['avatar_default_file']) && $_FILES['avatar_default_file']['size']) { + require_once QA_INCLUDE_DIR.'qa-util-image.php'; + + $oldblobid=qa_opt('avatar_default_blobid'); + + $toobig=qa_image_file_too_big($_FILES['avatar_default_file']['tmp_name'], qa_opt('avatar_store_size')); + + if ($toobig) + $errors['avatar_default_show']=qa_lang_sub('main/image_too_big_x_pc', (int)($toobig*100)); + + else { + $imagedata=qa_image_constrain_data(file_get_contents($_FILES['avatar_default_file']['tmp_name']), $width, $height, qa_opt('avatar_store_size')); + + if (isset($imagedata)) { + require_once QA_INCLUDE_DIR.'qa-db-blobs.php'; + + $newblobid=qa_db_blob_create($imagedata, 'jpeg'); + + if (isset($newblobid)) { + qa_set_option('avatar_default_blobid', $newblobid); + qa_set_option('avatar_default_width', $width); + qa_set_option('avatar_default_height', $height); + qa_set_option('avatar_default_show', 1); + } + + if (strlen($oldblobid)) + qa_db_blob_delete($oldblobid); + + } else + $errors['avatar_default_show']=qa_lang_sub('main/image_not_read', implode(', ', qa_gd_image_formats())); + } + } + } + + +// Mailings management + + if ($adminsection=='mailing') { + if (qa_clicked('domailingtest')) { + $email=qa_get_logged_in_email(); + + if (qa_mailing_send_one(qa_get_logged_in_userid(), qa_get_logged_in_handle(), $email, qa_get_logged_in_user_field('emailcode'))) + $formokhtml=qa_lang_html_sub('admin/test_sent_to_x', qa_html($email)); + else + $formokhtml=qa_lang_html('main/general_error'); + } + + if (qa_clicked('domailingstart')) { + qa_mailing_start(); + $startmailing=true; + } + + if (qa_clicked('domailingresume')) + $startmailing=true; + + if (qa_clicked('domailingcancel')) + qa_mailing_stop(); + + $mailingprogress=qa_mailing_progress_message(); + + if (isset($mailingprogress)) { + $formokhtml=qa_html($mailingprogress); + + $checkboxtodisplay=array( + 'mailing_enabled' => '0', + ); + + } else { + $checkboxtodisplay=array( + 'mailing_from_name' => 'option_mailing_enabled', + 'mailing_from_email' => 'option_mailing_enabled', + 'mailing_subject' => 'option_mailing_enabled', + 'mailing_body' => 'option_mailing_enabled', + 'mailing_per_minute' => 'option_mailing_enabled', + 'domailingtest' => 'option_mailing_enabled', + 'domailingstart' => 'option_mailing_enabled', + ); + } + } + + +// Get the actual options + + $options=qa_get_options($getoptions); + + +// Prepare content for theme + + $qa_content=qa_content_prepare(); + + $qa_content['title']=qa_lang_html('admin/admin_title').' - '.qa_lang_html($subtitle); + + $qa_content['error']=qa_admin_page_error(); + + $qa_content['script_rel'][]='qa-content/qa-admin.js?'.QA_VERSION; + + $qa_content['form']=array( + 'ok' => $formokhtml, + + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'" NAME="admin_form" onsubmit="document.forms.admin_form.has_js.value=1; return true;"', + + 'style' => $formstyle, + + 'fields' => array(), + + 'buttons' => array( + 'save' => array( + 'label' => qa_lang_html('admin/save_options_button'), + ), + + 'reset' => array( + 'tags' => 'NAME="doresetoptions"', + 'label' => qa_lang_html('admin/reset_options_button'), + ), + ), + + 'hidden' => array( + 'dosaveoptions' => '1', // for IE + 'has_js' => '0', + ), + ); + + if ($recalchotness) { + $qa_content['form']['ok']=''; + + $qa_content['script_var']['qa_warning_recalc']=qa_lang('admin/stop_recalc_warning'); + + $qa_content['script_onloads'][]=array( + "qa_recalc_click('dorecountposts', document.getElementById('recalc_ok'), null, 'recalc_ok');" + ); + + } elseif ($startmailing) { + + if (qa_post_text('has_js')) { + $qa_content['form']['ok']=''.qa_html($mailingprogress).''; + + $qa_content['script_onloads'][]=array( + "qa_mailing_start('mailing_ok', 'domailingpause');" + ); + + } else { // rudimentary non-Javascript version of mass mailing loop + echo ''; + + while (true) { + qa_mailing_perform_step(); + + $message=qa_mailing_progress_message(); + + if (!isset($message)) + break; + + echo qa_html($message).str_repeat(' ', 1024)."
\n"; + + flush(); + sleep(1); + } + + echo qa_lang_html('admin/mailing_complete').'

'.qa_lang_html('admin/admin_title').' - '.qa_lang_html('admin/mailing_title').''; + + qa_exit(); + } + } + + + function qa_optionfield_make_select(&$optionfield, $options, $value, $default) + { + $optionfield['type']='select'; + $optionfield['options']=$options; + $optionfield['value']=isset($options[qa_html($value)]) ? $options[qa_html($value)] : @$options[$default]; + } + + $indented=false; + + foreach ($showoptions as $optionname) + if (empty($optionname)) { + $indented=false; + + $qa_content['form']['fields'][]=array( + 'type' => 'blank' + ); + + } elseif (strpos($optionname, '/')!==false) { + $qa_content['form']['fields'][]=array( + 'type' => 'static', + 'label' => qa_lang_html($optionname), + ); + + $indented=true; + + } else { + $type=@$optiontype[$optionname]; + if ($type=='number-blank') + $type='number'; + + $value=$options[$optionname]; + + $optionfield=array( + 'id' => $optionname, + 'label' => ($indented ? '– ' : '').qa_lang_html('options/'.$optionname), + 'tags' => 'NAME="option_'.$optionname.'" ID="option_'.$optionname.'"', + 'value' => qa_html($value), + 'type' => $type, + 'error' => qa_html(@$errors[$optionname]), + ); + + if (isset($optionmaximum[$optionname])) + $optionfield['note']=qa_lang_html_sub('admin/maximum_x', $optionmaximum[$optionname]); + + $feedrequest=null; + $feedisexample=false; + + switch ($optionname) { // special treatment for certain options + case 'site_language': + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + qa_optionfield_make_select($optionfield, qa_admin_language_options(), $value, ''); + + $optionfield['suffix']=strtr(qa_lang_html('admin/check_language_suffix'), array( + '^1' => '', + '^2' => '', + )); + + if (!qa_has_multibyte()) + $optionfield['error']=qa_lang_html('admin/no_multibyte'); + break; + + case 'neat_urls': + $neatoptions=array(); + + $rawoptions=array( + QA_URL_FORMAT_NEAT, + QA_URL_FORMAT_INDEX, + QA_URL_FORMAT_PARAM, + QA_URL_FORMAT_PARAMS, + QA_URL_FORMAT_SAFEST, + ); + + foreach ($rawoptions as $rawoption) + $neatoptions[$rawoption]= + ' '. + ''. + qa_html(urldecode(qa_path('123/why-do-birds-sing', null, '/', $rawoption))). + (($rawoption==QA_URL_FORMAT_NEAT) ? strtr(qa_lang_html('admin/neat_urls_note'), array( + '^1' => '', + '^2' => '', + )) : ''). + ''; + + qa_optionfield_make_select($optionfield, $neatoptions, $value, QA_URL_FORMAT_SAFEST); + + $optionfield['type']='select-radio'; + $optionfield['note']=qa_lang_html_sub('admin/url_format_note', ''.$namehtml.''; + } + + if (strlen(@$metadata['author'])) { + $authorhtml=qa_html($metadata['author']); + + if (strlen(@$metadata['author_uri'])) + $authorhtml=''.$authorhtml.''; + + $authorhtml=qa_lang_html_sub('main/by_x', $authorhtml); + + } else + $authorhtml=''; + + if (strlen(@$metadata['version']) && strlen(@$metadata['update'])) { + $elementid='version_check_'.$optionname; + + $updatehtml='(...)'; + + $qa_content['script_onloads'][]=array( + "qa_version_check(".qa_js($metadata['update']).", 'Theme Version', ".qa_js($metadata['version']).", 'Theme URI', ".qa_js($elementid).");" + ); + + } else + $updatehtml=''; + + $optionfield['suffix']=$namehtml.' '.$authorhtml.' '.$updatehtml; + break; + + case 'tags_or_categories': + qa_optionfield_make_select($optionfield, array( + '' => qa_lang_html('admin/no_classification'), + 't' => qa_lang_html('admin/tags'), + 'c' => qa_lang_html('admin/categories'), + 'tc' => qa_lang_html('admin/tags_and_categories'), + ), $value, 'tc'); + + $optionfield['error']=''; + + if (qa_opt('cache_tagcount') && !qa_using_tags()) + $optionfield['error'].=qa_lang_html('admin/tags_not_shown').' '; + + if (!qa_using_categories()) + foreach ($categories as $category) + if ($category['qcount']) { + $optionfield['error'].=qa_lang_html('admin/categories_not_shown'); + break; + } + break; + + case 'smtp_secure': + qa_optionfield_make_select($optionfield, array( + '' => qa_lang_html('options/smtp_secure_none'), + 'ssl' => 'SSL', + 'tls' => 'TLS', + ), $value, ''); + break; + + case 'custom_sidebar': + case 'custom_sidepanel': + case 'custom_header': + case 'custom_footer': + case 'custom_in_head': + case 'home_description': + unset($optionfield['label']); + $optionfield['rows']=6; + break; + + case 'custom_home_content': + $optionfield['rows']=16; + break; + + case 'show_custom_register': + case 'show_custom_welcome': + case 'show_notice_welcome': + case 'show_notice_visitor': + $optionfield['style']='tall'; + break; + + case 'custom_register': + case 'custom_welcome': + case 'notice_welcome': + case 'notice_visitor': + unset($optionfield['label']); + $optionfield['style']='tall'; + $optionfield['rows']=3; + break; + + case 'avatar_allow_gravatar': + $optionfield['label']=strtr($optionfield['label'], array( + '^1' => '', + '^2' => '', + )); + + if (!qa_has_gd_image()) { + $optionfield['style']='tall'; + $optionfield['error']=qa_lang_html('admin/no_image_gd'); + } + break; + + case 'avatar_store_size': + case 'avatar_profile_size': + case 'avatar_users_size': + case 'avatar_q_page_q_size': + case 'avatar_q_page_a_size': + case 'avatar_q_page_c_size': + case 'avatar_q_list_size': + $optionfield['note']=qa_lang_html('admin/pixels'); + break; + + case 'avatar_default_show'; + $qa_content['form']['tags'].='ENCTYPE="multipart/form-data"'; + $optionfield['label'].=' '. + qa_get_avatar_blob_html(qa_opt('avatar_default_blobid'), qa_opt('avatar_default_width'), qa_opt('avatar_default_height'), 32). + ' '; + break; + + case 'logo_width': + case 'logo_height': + $optionfield['suffix']=qa_lang_html('admin/pixels'); + break; + + case 'pages_prev_next': + qa_optionfield_make_select($optionfield, array(0 => 0, 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5), $value, 3); + break; + + case 'columns_tags': + case 'columns_users': + qa_optionfield_make_select($optionfield, array(1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5), $value, 2); + break; + + case 'min_len_q_title': + case 'q_urls_title_length': + case 'min_len_q_content': + case 'min_len_a_content': + case 'min_len_c_content': + $optionfield['note']=qa_lang_html('admin/characters'); + break; + + case 'min_num_q_tags': + case 'max_num_q_tags': + $optionfield['note']=qa_lang_html_sub('main/x_tags', ''); + break; + + case 'show_full_date_days': + $optionfield['note']=qa_lang_html_sub('main/x_days', ''); + break; + + case 'sort_answers_by': + qa_optionfield_make_select($optionfield, array( + 'created' => qa_lang_html('options/sort_time'), + 'votes' => qa_lang_html('options/sort_votes'), + ), $value, 'created'); + break; + + case 'page_size_q_as': + $optionfield['note']=qa_lang_html_sub('main/x_answers', ''); + break; + + case 'show_a_form_immediate': + qa_optionfield_make_select($optionfield, array( + 'always' => qa_lang_html('options/show_always'), + 'if_no_as' => qa_lang_html('options/show_if_no_as'), + 'never' => qa_lang_html('options/show_never'), + ), $value, 'if_no_as'); + break; + + case 'show_fewer_cs_from': + case 'show_fewer_cs_count': + $optionfield['note']=qa_lang_html_sub('main/x_comments', ''); + break; + + case 'match_related_qs': + case 'match_ask_check_qs': + case 'match_example_tags': + qa_optionfield_make_select($optionfield, qa_admin_match_options(), $value, 3); + break; + + case 'block_bad_words': + $optionfield['style']='tall'; + $optionfield['rows']=4; + $optionfield['note']=qa_lang_html('admin/block_words_note'); + break; + + case 'editor_for_qs': + case 'editor_for_as': + case 'editor_for_cs': + $editors=qa_list_modules('editor'); + + $selectoptions=array(); + $optionslinks=false; + + foreach ($editors as $editor) { + $selectoptions[qa_html($editor)]=strlen($editor) ? qa_html($editor) : qa_lang_html('admin/basic_editor'); + + if ($editor==$value) { + $module=qa_load_module('editor', $editor); + + if (method_exists($module, 'admin_form')) + $optionfield['note']=''.qa_lang_html('admin/options').''; + } + } + + qa_optionfield_make_select($optionfield, $selectoptions, $value, ''); + break; + + case 'show_custom_ask': + case 'extra_field_active': + case 'show_custom_answer': + case 'show_custom_comment': + $optionfield['style']='tall'; + break; + + case 'custom_ask': + case 'custom_answer': + case 'custom_comment': + $optionfield['style']='tall'; + unset($optionfield['label']); + $optionfield['rows']=3; + break; + + case 'extra_field_display': + $optionfield['style']='tall'; + $optionfield['label']=''.qa_lang_html('options/extra_field_display_label').''; + break; + + case 'extra_field_prompt': + case 'extra_field_label': + $optionfield['style']='tall'; + unset($optionfield['label']); + break; + + case 'search_module': + foreach ($searchmodules as $modulename => $module) { + $selectoptions[qa_html($modulename)]=strlen($modulename) ? qa_html($modulename) : qa_lang_html('options/option_default'); + + if (($modulename==$value) && method_exists($module, 'admin_form')) + $optionfield['note']=''.qa_lang_html('admin/options').''; + } + + qa_optionfield_make_select($optionfield, $selectoptions, $value, ''); + break; + + case 'hot_weight_q_age': + case 'hot_weight_a_age': + case 'hot_weight_answers': + case 'hot_weight_votes': + case 'hot_weight_views': + $optionfield['note']='/ 100'; + break; + + case 'moderate_by_points': + $optionfield['label']=''.qa_lang_html('options/moderate_points_limit').''; + break; + + case 'moderate_points_limit'; + unset($optionfield['label']); + $optionfield['note']=qa_lang_html('admin/points'); + break; + + case 'flagging_hide_after': + case 'flagging_notify_every': + case 'flagging_notify_first': + $optionfield['note']=qa_lang_html_sub('main/x_flags', ''); + break; + + case 'block_ips_write': + $optionfield['style']='tall'; + $optionfield['rows']=4; + $optionfield['note']=qa_lang_html('admin/block_ips_note'); + break; + + case 'allow_view_q_bots': + $optionfield['note']=$optionfield['label']; + unset($optionfield['label']); + break; + + case 'permit_view_q_page': + case 'permit_post_q': + case 'permit_post_a': + case 'permit_post_c': + case 'permit_vote_q': + case 'permit_vote_a': + case 'permit_vote_down': + case 'permit_edit_q': + case 'permit_retag_cat': + case 'permit_edit_a': + case 'permit_edit_c': + case 'permit_flag': + case 'permit_close_q': + case 'permit_select_a': + case 'permit_hide_show': + case 'permit_moderate': + case 'permit_delete_hidden': + case 'permit_anon_view_ips': + if ($optionname=='permit_retag_cat') + $optionfield['label']=qa_lang_html(qa_using_categories() ? 'profile/permit_recat' : 'profile/permit_retag').':'; + else + $optionfield['label']=qa_lang_html('profile/'.$optionname).':'; + + if ( ($optionname=='permit_view_q_page') || ($optionname=='permit_post_q') || ($optionname=='permit_post_a') || ($optionname=='permit_post_c') || ($optionname=='permit_anon_view_ips') ) + $widest=QA_PERMIT_ALL; + elseif ( ($optionname=='permit_close_q') || ($optionname=='permit_select_a') || ($optionname=='permit_moderate')|| ($optionname=='permit_hide_show') ) + $widest=QA_PERMIT_POINTS; + elseif ($optionname=='permit_delete_hidden') + $widest=QA_PERMIT_EDITORS; + else + $widest=QA_PERMIT_USERS; + + if ($optionname=='permit_view_q_page') + $narrowest=QA_PERMIT_CONFIRMED; + elseif ( ($optionname=='permit_edit_c') || ($optionname=='permit_close_q') || ($optionname=='permit_select_a') || ($optionname=='permit_moderate')|| ($optionname=='permit_hide_show') || ($optionname=='permit_anon_view_ips') ) + $narrowest=QA_PERMIT_MODERATORS; + elseif ( ($optionname=='permit_post_c') || ($optionname=='permit_edit_q') || ($optionname=='permit_retag_cat') || ($optionname=='permit_edit_a') || ($optionname=='permit_flag') ) + $narrowest=QA_PERMIT_EDITORS; + elseif ( ($optionname=='permit_vote_q') || ($optionname=='permit_vote_a') ) + $narrowest=QA_PERMIT_POINTS_CONFIRMED; + elseif ($optionname=='permit_delete_hidden') + $narrowest=QA_PERMIT_ADMINS; + else + $narrowest=QA_PERMIT_EXPERTS; + + $permitoptions=qa_admin_permit_options($widest, $narrowest, (!QA_FINAL_EXTERNAL_USERS) && qa_opt('confirm_user_emails')); + + if (count($permitoptions)>1) + qa_optionfield_make_select($optionfield, $permitoptions, $value, + ($value==QA_PERMIT_CONFIRMED) ? QA_PERMIT_USERS : min(array_keys($permitoptions))); + else { + $optionfield['type']='static'; + $optionfield['value']=reset($permitoptions); + } + break; + + case 'permit_post_q_points': + case 'permit_post_a_points': + case 'permit_post_c_points': + case 'permit_vote_q_points': + case 'permit_vote_a_points': + case 'permit_vote_down_points': + case 'permit_flag_points': + case 'permit_edit_q_points': + case 'permit_retag_cat_points': + case 'permit_edit_a_points': + case 'permit_edit_c_points': + case 'permit_close_q_points': + case 'permit_select_a_points': + case 'permit_hide_show_points': + case 'permit_moderate_points': + case 'permit_delete_hidden_points': + case 'permit_anon_view_ips_points': + unset($optionfield['label']); + $optionfield['type']='number'; + $optionfield['prefix']=qa_lang_html('admin/users_must_have').' '; + $optionfield['note']=qa_lang_html('admin/points'); + break; + + case 'feed_for_qa': + $feedrequest='qa'; + break; + + case 'feed_for_questions': + $feedrequest='questions'; + break; + + case 'feed_for_hot': + $feedrequest='hot'; + break; + + case 'feed_for_unanswered': + $feedrequest='unanswered'; + break; + + case 'feed_for_activity': + $feedrequest='activity'; + break; + + case 'feed_per_category': + if (count($categories)) { + $category=reset($categories); + $categoryslug=$category['tags']; + + } else + $categoryslug='example-category'; + + if (qa_opt('feed_for_qa')) + $feedrequest='qa'; + elseif (qa_opt('feed_for_questions')) + $feedrequest='questions'; + else + $feedrequest='activity'; + + $feedrequest.='/'.$categoryslug; + $feedisexample=true; + break; + + case 'feed_for_tag_qs': + $populartags=qa_db_select_with_pending(qa_db_popular_tags_selectspec(0, 1)); + + if (count($populartags)) { + reset($populartags); + $feedrequest='tag/'.key($populartags); + } else + $feedrequest='tag/singing'; + + $feedisexample=true; + break; + + case 'feed_for_search': + $feedrequest='search/why do birds sing'; + $feedisexample=true; + break; + + case 'captcha_module': + $captchaoptions=array(); + + foreach ($captchamodules as $modulename) { + $captchaoptions[qa_html($modulename)]=qa_html($modulename); + + if ($modulename==$value) { + $module=qa_load_module('captcha', $modulename); + + if (method_exists($module, 'admin_form')) + $optionfield['note']=''.qa_lang_html('admin/options').''; + } + } + + qa_optionfield_make_select($optionfield, $captchaoptions, $value, ''); + break; + + case 'max_rate_ip_as': + case 'max_rate_ip_cs': + case 'max_rate_ip_flags': + case 'max_rate_ip_logins': + case 'max_rate_ip_messages': + case 'max_rate_ip_qs': + case 'max_rate_ip_registers': + case 'max_rate_ip_uploads': + case 'max_rate_ip_votes': + $optionfield['note']=qa_lang_html('admin/per_ip_hour'); + break; + + case 'max_rate_user_as': + case 'max_rate_user_cs': + case 'max_rate_user_flags': + case 'max_rate_user_messages': + case 'max_rate_user_qs': + case 'max_rate_user_uploads': + case 'max_rate_user_votes': + unset($optionfield['label']); + $optionfield['note']=qa_lang_html('admin/per_user_hour'); + break; + + case 'mailing_per_minute': + $optionfield['suffix']=qa_lang_html('admin/emails_per_minute'); + break; + } + + if (isset($feedrequest) && $value) + $optionfield['note']=''.qa_lang_html($feedisexample ? 'admin/feed_link_example' : 'admin/feed_link').''; + + $qa_content['form']['fields'][$optionname]=$optionfield; + } + + +// Extra items for specific pages + + switch ($adminsection) { + case 'users': + if (!QA_FINAL_EXTERNAL_USERS) { + $userfields=qa_db_single_select(qa_db_userfields_selectspec()); + + $listhtml=''; + + foreach ($userfields as $userfield) { + $listhtml.='

  • '.qa_html(qa_user_userfield_label($userfield)).''; + + $listhtml.=strtr(qa_lang_html('admin/edit_field'), array( + '^1' => '', + '^2' => '', + )); + + $listhtml.='
  • '; + } + + $listhtml.='
  • '.qa_lang_html('admin/add_new_field').'
  • '; + + $qa_content['form']['fields']['userfields']=array( + 'label' => qa_lang_html('admin/profile_fields'), + 'style' => 'tall', + 'type' => 'custom', + 'html' => strlen($listhtml) ? '
      '.$listhtml.'
    ' : null, + ); + } + + $qa_content['form']['fields'][]=array('type' => 'blank'); + + $pointstitle=qa_get_points_to_titles(); + + $listhtml=''; + + foreach ($pointstitle as $points => $title) { + $listhtml.='
  • '.$title.' - '.(($points==1) ? qa_lang_html_sub('main/1_point', '1', '1') + : qa_lang_html_sub('main/x_points', qa_html(number_format($points)))); + + $listhtml.=strtr(qa_lang_html('admin/edit_title'), array( + '^1' => '', + '^2' => '', + )); + + $listhtml.='
  • '; + } + + $listhtml.='
  • '.qa_lang_html('admin/add_new_title').'
  • '; + + $qa_content['form']['fields']['usertitles']=array( + 'label' => qa_lang_html('admin/user_titles'), + 'style' => 'tall', + 'type' => 'custom', + 'html' => strlen($listhtml) ? '
      '.$listhtml.'
    ' : null, + ); + break; + + case 'layout': + $listhtml=''; + + $widgetmodules=qa_load_modules_with('widget', 'allow_template'); + + foreach ($widgetmodules as $tryname => $trywidget) + if (method_exists($trywidget, 'allow_region')) { + $listhtml.='
  • '.qa_html($tryname).''; + + $listhtml.=strtr(qa_lang_html('admin/add_widget_link'), array( + '^1' => '', + '^2' => '', + )); + + if (method_exists($trywidget, 'admin_form')) + $listhtml.=strtr(qa_lang_html('admin/widget_global_options'), array( + '^1' => '', + '^2' => '', + )); + + $listhtml.='
  • '; + } + + if (strlen($listhtml)) + $qa_content['form']['fields']['plugins']=array( + 'label' => qa_lang_html('admin/widgets_explanation'), + 'style' => 'tall', + 'type' => 'custom', + 'html' => '
      '.$listhtml.'
    ', + ); + + $widgets=qa_db_single_select(qa_db_widgets_selectspec()); + + $listhtml=''; + + $placeoptions=qa_admin_place_options(); + + foreach ($widgets as $widget) { + $listhtml.='
  • '.qa_html($widget['title']).' - '. + ''. + @$placeoptions[$widget['place']].''; + + $listhtml.='
  • '; + } + + if (strlen($listhtml)) + $qa_content['form']['fields']['widgets']=array( + 'label' => qa_lang_html('admin/active_widgets_explanation'), + 'type' => 'custom', + 'html' => '
      '.$listhtml.'
    ', + ); + + break; + + case 'permissions': + $qa_content['form']['fields']['permit_block']=array( + 'type' => 'static', + 'label' => qa_lang_html('options/permit_block'), + 'value' => qa_lang_html('options/permit_moderators'), + ); + + if (!QA_FINAL_EXTERNAL_USERS) { + $qa_content['form']['fields']['permit_create_experts']=array( + 'type' => 'static', + 'label' => qa_lang_html('options/permit_create_experts'), + 'value' => qa_lang_html('options/permit_moderators'), + ); + + $qa_content['form']['fields']['permit_see_emails']=array( + 'type' => 'static', + 'label' => qa_lang_html('options/permit_see_emails'), + 'value' => qa_lang_html('options/permit_admins'), + ); + + $qa_content['form']['fields']['permit_delete_users']=array( + 'type' => 'static', + 'label' => qa_lang_html('options/permit_delete_users'), + 'value' => qa_lang_html('options/permit_admins'), + ); + + $qa_content['form']['fields']['permit_create_eds_mods']=array( + 'type' => 'static', + 'label' => qa_lang_html('options/permit_create_eds_mods'), + 'value' => qa_lang_html('options/permit_admins'), + ); + + $qa_content['form']['fields']['permit_create_admins']=array( + 'type' => 'static', + 'label' => qa_lang_html('options/permit_create_admins'), + 'value' => qa_lang_html('options/permit_supers'), + ); + + } + break; + + case 'mailing': + require_once QA_INCLUDE_DIR.'qa-util-sort.php'; + + if (isset($mailingprogress)) { + unset($qa_content['form']['buttons']['save']); + unset($qa_content['form']['buttons']['reset']); + + if ($startmailing) { + unset($qa_content['form']['hidden']['dosaveoptions']); + + foreach ($showoptions as $optionname) + $qa_content['form']['fields'][$optionname]['type']='static'; + + $qa_content['form']['fields']['mailing_body']['value']=qa_html(qa_opt('mailing_body'), true); + + $qa_content['form']['buttons']['stop']=array( + 'tags' => 'NAME="domailingpause" ID="domailingpause"', + 'label' => qa_lang_html('admin/pause_mailing_button'), + ); + + } else { + $qa_content['form']['buttons']['resume']=array( + 'tags' => 'NAME="domailingresume"', + 'label' => qa_lang_html('admin/resume_mailing_button'), + ); + + $qa_content['form']['buttons']['cancel']=array( + 'tags' => 'NAME="domailingcancel"', + 'label' => qa_lang_html('admin/cancel_mailing_button'), + ); + } + + } else { + $qa_content['form']['buttons']['spacer']=array(); + + $qa_content['form']['buttons']['test']=array( + 'tags' => 'NAME="domailingtest" ID="domailingtest"', + 'label' => qa_lang_html('admin/send_test_button'), + ); + + $qa_content['form']['buttons']['start']=array( + 'tags' => 'NAME="domailingstart" ID="domailingstart"', + 'label' => qa_lang_html('admin/start_mailing_button'), + ); + } + + if (!$startmailing) { + $qa_content['form']['fields']['mailing_enabled']['note']=qa_lang_html('admin/mailing_explanation'); + $qa_content['form']['fields']['mailing_body']['rows']=12; + $qa_content['form']['fields']['mailing_body']['note']=qa_lang_html('admin/mailing_unsubscribe'); + } + break; + } + + + if (isset($checkboxtodisplay)) + qa_set_display_rules($qa_content, $checkboxtodisplay); + + $qa_content['navigation']['sub']=qa_admin_sub_navigation(); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-admin-flagged.php b/qa-include/qa-page-admin-flagged.php new file mode 100644 index 000000000..50827082d --- /dev/null +++ b/qa-include/qa-page-admin-flagged.php @@ -0,0 +1,132 @@ + array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + ), + + 'qs' => array(), + ); + + + if (count($questions)) { + foreach ($questions as $question) { + $postid=qa_html(isset($question['opostid']) ? $question['opostid'] : $question['postid']); + $elementid='p'.$postid; + + $htmloptions=qa_post_html_defaults('Q'); + $htmloptions['voteview']=false; + $htmloptions['tagsview']=($question['obasetype']=='Q'); + $htmloptions['answersview']=false; + $htmloptions['contentview']=true; + $htmloptions['flagsview']=true; + $htmloptions['elementid']=$elementid; + + $htmlfields=qa_any_to_q_html_fields($question, $userid, qa_cookie_get(), $usershtml, null, $htmloptions); + + if (isset($htmlfields['what_url'])) // link directly to relevant content + $htmlfields['url']=$htmlfields['what_url']; + + $htmlfields['form']=array( + 'style' => 'light', + + 'buttons' => array( + 'clearflags' => array( + 'tags' => 'NAME="admin_'.$postid.'_clearflags" onclick="return qa_admin_click(this);"', + 'label' => qa_lang_html('question/clear_flags_button'), + ), + + 'hide' => array( + 'tags' => 'NAME="admin_'.$postid.'_hide" onclick="return qa_admin_click(this);"', + 'label' => qa_lang_html('question/hide_button'), + ), + ), + ); + + $qa_content['q_list']['qs'][]=$htmlfields; + } + + } else + $qa_content['title']=qa_lang_html('admin/no_flagged_found'); + + + $qa_content['navigation']['sub']=qa_admin_sub_navigation(); + $qa_content['script_rel'][]='qa-content/qa-admin.js?'.QA_VERSION; + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-admin-hidden.php b/qa-include/qa-page-admin-hidden.php new file mode 100644 index 000000000..b7e15b10b --- /dev/null +++ b/qa-include/qa-page-admin-hidden.php @@ -0,0 +1,157 @@ + $question) + $qhiddenpostid[$key]=isset($question['opostid']) ? $question['opostid'] : $question['postid']; + + $dependcounts=qa_db_postids_count_dependents($qhiddenpostid); + + +// Prepare content for theme + + $qa_content=qa_content_prepare(); + + $qa_content['title']=qa_lang_html('admin/recent_hidden_title'); + + $qa_content['error']=qa_admin_page_error(); + + $qa_content['q_list']=array( + 'form' => array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + ), + + 'qs' => array(), + ); + + if (count($questions)) { + foreach ($questions as $key => $question) { + $elementid='p'.$qhiddenpostid[$key]; + + $htmloptions=qa_post_html_defaults('Q'); + $htmloptions['voteview']=false; + $htmloptions['tagsview']=!isset($question['opostid']); + $htmloptions['answersview']=false; + $htmloptions['updateview']=false; + $htmloptions['contentview']=true; + $htmloptions['flagsview']=true; + $htmloptions['elementid']=$elementid; + + $htmlfields=qa_any_to_q_html_fields($question, $userid, qa_cookie_get(), $usershtml, null, $htmloptions); + + if (isset($htmlfields['what_url'])) // link directly to relevant content + $htmlfields['url']=$htmlfields['what_url']; + + $htmlfields['what_2']=qa_lang_html('main/hidden'); + + if (@$htmloptions['whenview']) + $htmlfields['when_2']=qa_when_to_html($question[isset($question['opostid']) ? 'oupdated' : 'updated'], @$htmloptions['fulldatedays']); + + $buttons=array(); + + if ($allowhideshow) + $buttons['reshow']=array( + 'tags' => 'NAME="admin_'.qa_html($qhiddenpostid[$key]).'_reshow" onclick="return qa_admin_click(this);"', + 'label' => qa_lang_html('question/reshow_button'), + ); + + if ($allowdeletehidden && !$dependcounts[$qhiddenpostid[$key]]) + $buttons['delete']=array( + 'tags' => 'NAME="admin_'.qa_html($qhiddenpostid[$key]).'_delete" onclick="return qa_admin_click(this);"', + 'label' => qa_lang_html('question/delete_button'), + ); + + if (count($buttons)) + $htmlfields['form']=array( + 'style' => 'light', + 'buttons' => $buttons, + ); + + $qa_content['q_list']['qs'][]=$htmlfields; + } + + } else + $qa_content['title']=qa_lang_html('admin/no_hidden_found'); + + + $qa_content['navigation']['sub']=qa_admin_sub_navigation(); + $qa_content['script_rel'][]='qa-content/qa-admin.js?'.QA_VERSION; + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-admin-moderate.php b/qa-include/qa-page-admin-moderate.php new file mode 100644 index 000000000..fb2c4d4f1 --- /dev/null +++ b/qa-include/qa-page-admin-moderate.php @@ -0,0 +1,137 @@ + array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + ), + + 'qs' => array(), + ); + + if (count($questions)) { + foreach ($questions as $question) { + $postid=qa_html(isset($question['opostid']) ? $question['opostid'] : $question['postid']); + $elementid='p'.$postid; + + $htmloptions=qa_post_html_defaults('Q'); + $htmloptions['voteview']=false; + $htmloptions['tagsview']=!isset($question['opostid']); + $htmloptions['answersview']=false; + $htmloptions['contentview']=true; + $htmloptions['elementid']=$elementid; + + $htmlfields=qa_any_to_q_html_fields($question, $userid, qa_cookie_get(), $usershtml, null, $htmloptions); + + if (isset($htmlfields['what_url'])) // link directly to relevant content + $htmlfields['url']=$htmlfields['what_url']; + + $htmlfields['form']=array( + 'style' => 'light', + + 'buttons' => array( + 'approve' => array( + 'tags' => 'NAME="admin_'.$postid.'_approve" onclick="return qa_admin_click(this);"', + 'label' => qa_lang_html('question/approve_button'), + ), + + 'reject' => array( + 'tags' => 'NAME="admin_'.$postid.'_reject" onclick="return qa_admin_click(this);"', + 'label' => qa_lang_html('question/reject_button'), + ), + ), + ); + + $qa_content['q_list']['qs'][]=$htmlfields; + } + + } else + $qa_content['title']=qa_lang_html('admin/no_approve_found'); + + + $qa_content['navigation']['sub']=qa_admin_sub_navigation(); + $qa_content['script_rel'][]='qa-content/qa-admin.js?'.QA_VERSION; + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-admin-pages.php b/qa-include/qa-page-admin-pages.php new file mode 100644 index 000000000..247632e82 --- /dev/null +++ b/qa-include/qa-page-admin-pages.php @@ -0,0 +1,570 @@ + qa_get('text'), 'tags' => qa_get('url'), 'nav' => qa_get('nav'), 'position' => 1); + $isexternal=qa_clicked('doaddlink') || qa_get('doaddlink') || qa_post_text('external'); + + } elseif (isset($editpage)) + $isexternal=$editpage['flags'] & QA_PAGE_FLAGS_EXTERNAL; + + +// Check admin privileges (do late to allow one DB query) + + if (!qa_admin_check_privileges($qa_content)) + return $qa_content; + + +// Define an array of navigation settings we can change, option name => language key + + $hascustomhome=qa_has_custom_home(); + + $navoptions=array( + 'nav_home' => 'main/nav_home', + 'nav_activity' => 'main/nav_activity', + $hascustomhome ? 'nav_qa_not_home' : 'nav_qa_is_home' => $hascustomhome ? 'main/nav_qa' : 'admin/nav_qa_is_home', + 'nav_questions' => 'main/nav_qs', + 'nav_hot' => 'main/nav_hot', + 'nav_unanswered' => 'main/nav_unanswered', + 'nav_tags' => 'main/nav_tags', + 'nav_categories' => 'main/nav_categories', + 'nav_users' => 'main/nav_users', + 'nav_ask' => 'main/nav_ask', + ); + + $navpaths=array( + 'nav_home' => '', + 'nav_activity' => 'activity', + 'nav_qa_not_home' => 'qa', + 'nav_qa_is_home' => '', + 'nav_questions' => 'questions', + 'nav_hot' => 'hot', + 'nav_unanswered' => 'unanswered', + 'nav_tags' => 'tags', + 'nav_categories' => 'categories', + 'nav_users' => 'users', + 'nav_ask' => 'ask', + ); + + if (!qa_opt('show_custom_home')) + unset($navoptions['nav_home']); + + if (!qa_using_categories()) + unset($navoptions['nav_categories']); + + if (!qa_using_tags()) + unset($navoptions['nav_tags']); + + +// Process saving an old or new page + + if (qa_clicked('docancel')) + $editpage=null; + + elseif (qa_clicked('dosaveoptions') || qa_clicked('doaddpage') || qa_clicked('doaddlink')) { + foreach ($navoptions as $optionname => $langkey) + qa_set_option($optionname, (int)qa_post_text('option_'.$optionname)); + + } elseif (qa_clicked('dosavepage')) { + require_once QA_INCLUDE_DIR.'qa-db-admin.php'; + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + $reloadpages=false; + + if (qa_post_text('dodelete')) { + qa_db_page_delete($editpage['pageid']); + + $searchmodules=qa_load_modules_with('search', 'unindex_page'); + foreach ($searchmodules as $searchmodule) + $searchmodule->unindex_page($editpage['pageid']); + + $editpage=null; + $reloadpages=true; + + } else { + $inname=qa_post_text('name'); + $inposition=qa_post_text('position'); + $inpermit=(int)qa_post_text('permit'); + $inurl=qa_post_text('url'); + $innewwindow=qa_post_text('newwindow'); + $inheading=qa_post_text('heading'); + $incontent=qa_post_text('content'); + + $errors=array(); + + // Verify the name (navigation link) is legitimate + + if (empty($inname)) + $errors['name']=qa_lang('main/field_required'); + elseif (qa_strlen($inname)>QA_DB_MAX_CAT_PAGE_TITLE_LENGTH) + $errors['name']=qa_lang_sub('main/max_length_x', QA_DB_MAX_CAT_PAGE_TITLE_LENGTH); + else + foreach ($pages as $page) + if ( + ($page['pageid'] != @$editpage['pageid']) && + qa_strtolower($page['title']) == qa_strtolower($inname) + ) + $errors['name']=qa_lang('admin/page_already_used'); + + if ($isexternal) { + + // Verify the url is legitimate (vaguely) + + if (empty($inurl)) + $errors['url']=qa_lang('main/field_required'); + elseif (qa_strlen($inurl)>QA_DB_MAX_CAT_PAGE_TAGS_LENGTH) + $errors['url']=qa_lang_sub('main/max_length_x', QA_DB_MAX_CAT_PAGE_TAGS_LENGTH); + + } else { + + // Verify the heading is legitimate + + if (qa_strlen($inheading)>QA_DB_MAX_TITLE_LENGTH) + $errors['heading']=qa_lang_sub('main/max_length_x', QA_DB_MAX_TITLE_LENGTH); + + // Verify the slug is legitimate (and try some defaults if we're creating a new page, and it's not) + + for ($attempt=0; $attempt<100; $attempt++) { + switch ($attempt) { + case 0: + $inslug=qa_post_text('slug'); + if (!isset($inslug)) + $inslug=implode('-', qa_string_to_words($inname)); + break; + + case 1: + $inslug=qa_lang_sub('admin/page_default_slug', $inslug); + break; + + default: + $inslug=qa_lang_sub('admin/page_default_slug', $attempt-1); + break; + } + + list($matchcategoryid, $matchpage)=qa_db_select_with_pending( + qa_db_slugs_to_category_id_selectspec($inslug), + qa_db_page_full_selectspec($inslug, false) + ); + + if (empty($inslug)) + $errors['slug']=qa_lang('main/field_required'); + elseif (qa_strlen($inslug)>QA_DB_MAX_CAT_PAGE_TAGS_LENGTH) + $errors['slug']=qa_lang_sub('main/max_length_x', QA_DB_MAX_CAT_PAGE_TAGS_LENGTH); + elseif (preg_match('/[\\+\\/]/', $inslug)) + $errors['slug']=qa_lang_sub('admin/slug_bad_chars', '+ /'); + elseif (qa_admin_is_slug_reserved($inslug)) + $errors['slug']=qa_lang('admin/slug_reserved'); + elseif (isset($matchpage) && ($matchpage['pageid']!=@$editpage['pageid'])) + $errors['slug']=qa_lang('admin/page_already_used'); + elseif (isset($matchcategoryid)) + $errors['slug']=qa_lang('admin/category_already_used'); + else + unset($errors['slug']); + + if (isset($editpage['pageid']) || !isset($errors['slug'])) // don't try other options if editing existing page + break; + } + } + + // Perform appropriate database action + + if (isset($editpage['pageid'])) { // changing existing page + if ($isexternal) + qa_db_page_set_fields($editpage['pageid'], + isset($errors['name']) ? $editpage['title'] : $inname, + QA_PAGE_FLAGS_EXTERNAL | ($innewwindow ? QA_PAGE_FLAGS_NEW_WINDOW : 0), + isset($errors['url']) ? $editpage['tags'] : $inurl, + null, null, $inpermit); + + else { + $setheading=isset($errors['heading']) ? $editpage['heading'] : $inheading; + $setcontent=isset($errors['content']) ? $editpage['content'] : $incontent; + + qa_db_page_set_fields($editpage['pageid'], + isset($errors['name']) ? $editpage['title'] : $inname, + 0, + isset($errors['slug']) ? $editpage['tags'] : $inslug, + $setheading, $setcontent, $inpermit); + + $searchmodules=qa_load_modules_with('search', 'unindex_page'); + foreach ($searchmodules as $searchmodule) + $searchmodule->unindex_page($editpage['pageid']); + + $indextext=qa_viewer_text($setcontent, 'html'); + + $searchmodules=qa_load_modules_with('search', 'index_page'); + foreach ($searchmodules as $searchmodule) + $searchmodule->index_page($editpage['pageid'], $setheading, $setcontent, 'html', $indextext); + } + + qa_db_page_move($editpage['pageid'], substr($inposition, 0, 1), substr($inposition, 1)); + + $reloadpages=true; + + if (empty($errors)) + $editpage=null; + else + $editpage=@$pages[$editpage['pageid']]; + + } else { // creating a new one + if (empty($errors)) { + if ($isexternal) + $pageid=qa_db_page_create($inname, QA_PAGE_FLAGS_EXTERNAL | ($innewwindow ? QA_PAGE_FLAGS_NEW_WINDOW : 0), $inurl, null, null, $inpermit); + else { + $pageid=qa_db_page_create($inname, 0, $inslug, $inheading, $incontent, $inpermit); + + $indextext=qa_viewer_text($incontent, 'html'); + + $searchmodules=qa_load_modules_with('search', 'index_page'); + foreach ($searchmodules as $searchmodule) + $searchmodule->index_page($pageid, $inheading, $incontent, 'html', $indextext); + } + + qa_db_page_move($pageid, substr($inposition, 0, 1), substr($inposition, 1)); + + $editpage=null; + $reloadpages=true; + } + } + } + + if ($reloadpages) { + qa_db_flush_pending_result('navpages'); + $pages=qa_db_select_with_pending(qa_db_pages_selectspec()); + } + } + + +// Prepare content for theme + + $qa_content=qa_content_prepare(); + + $qa_content['title']=qa_lang_html('admin/admin_title').' - '.qa_lang_html('admin/pages_title'); + + $qa_content['error']=qa_admin_page_error(); + + if (isset($editpage)) { + $positionoptions=array(); + + if (!$isexternal) + $positionoptions['_'.max(1, @$editpage['position'])]=qa_lang_html('admin/no_link'); + + $navlangkey=array( + 'B' => 'admin/before_main_menu', + 'M' => 'admin/after_main_menu', + 'O' => 'admin/opposite_main_menu', + 'F' => 'admin/after_footer', + ); + + foreach ($navlangkey as $nav => $langkey) { + $previous=null; + $passedself=false; + $maxposition=0; + + foreach ($pages as $key => $page) + if ($page['nav']==$nav) { + if (isset($previous)) + $positionhtml=qa_lang_html_sub('admin/after_x_tab', qa_html($passedself ? $page['title'] : $previous['title'])); + else + $positionhtml=qa_lang_html($langkey); + + if ($page['pageid']==@$editpage['pageid']) + $passedself=true; + + $maxposition=max($maxposition, $page['position']); + $positionoptions[$nav.$page['position']]=$positionhtml; + + $previous=$page; + } + + if ((!isset($editpage['pageid'])) || $nav!=@$editpage['nav']) { + $positionvalue=isset($previous) ? qa_lang_html_sub('admin/after_x_tab', qa_html($previous['title'])) : qa_lang_html($langkey); + $positionoptions[$nav.(isset($previous) ? (1+$maxposition) : 1)]=$positionvalue; + } + } + + $positionvalue=@$positionoptions[$editpage['nav'].$editpage['position']]; + + $permitoptions=qa_admin_permit_options(QA_PERMIT_ALL, QA_PERMIT_ADMINS, false, false); + $permitvalue=@$permitoptions[$editpage['permit']]; + + $qa_content['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_path_html(qa_request()).'"', + + 'style' => 'tall', + + 'fields' => array( + 'name' => array( + 'tags' => 'NAME="name" ID="name"', + 'label' => qa_lang_html($isexternal ? 'admin/link_name' : 'admin/page_name'), + 'value' => qa_html(isset($inname) ? $inname : @$editpage['title']), + 'error' => qa_html(@$errors['name']), + ), + + 'delete' => array( + 'tags' => 'NAME="dodelete" ID="dodelete"', + 'label' => qa_lang_html($isexternal ? 'admin/delete_link' : 'admin/delete_page'), + 'value' => 0, + 'type' => 'checkbox', + ), + + 'position' => array( + 'id' => 'position_display', + 'tags' => 'NAME="position"', + 'label' => qa_lang_html('admin/position'), + 'type' => 'select', + 'options' => $positionoptions, + 'value' => $positionvalue, + ), + + 'permit' => array( + 'id' => 'permit_display', + 'tags' => 'NAME="permit"', + 'label' => qa_lang_html('admin/permit_to_view'), + 'type' => 'select', + 'options' => $permitoptions, + 'value' => $permitvalue, + ), + + 'slug' => array( + 'id' => 'slug_display', + 'tags' => 'NAME="slug"', + 'label' => qa_lang_html('admin/page_slug'), + 'value' => qa_html(isset($inslug) ? $inslug : @$editpage['tags']), + 'error' => qa_html(@$errors['slug']), + ), + + 'url' => array( + 'id' => 'url_display', + 'tags' => 'NAME="url"', + 'label' => qa_lang_html('admin/link_url'), + 'value' => qa_html(isset($inurl) ? $inurl : @$editpage['tags']), + 'error' => qa_html(@$errors['url']), + ), + + 'newwindow' => array( + 'id' => 'newwindow_display', + 'tags' => 'NAME="newwindow"', + 'label' => qa_lang_html('admin/link_new_window'), + 'value' => (isset($innewwindow) ? $innewwindow : (@$editpage['flags'] & QA_PAGE_FLAGS_NEW_WINDOW)) ? 1 : 0, + 'type' => 'checkbox', + ), + + 'heading' => array( + 'id' => 'heading_display', + 'tags' => 'NAME="heading"', + 'label' => qa_lang_html('admin/page_heading'), + 'value' => qa_html(isset($inheading) ? $inheading : @$editpage['heading']), + 'error' => qa_html(@$errors['heading']), + ), + + 'content' => array( + 'id' => 'content_display', + 'tags' => 'NAME="content"', + 'label' => qa_lang_html('admin/page_content_html'), + 'value' => qa_html(isset($incontent) ? $incontent : @$editpage['content']), + 'error' => qa_html(@$errors['content']), + 'rows' => 16, + ), + ), + + 'buttons' => array( + 'save' => array( + 'label' => qa_lang_html(isset($editpage['pageid']) ? 'main/save_button' : ($isexternal ? 'admin/add_link_button' : 'admin/add_page_button')), + ), + + 'cancel' => array( + 'tags' => 'NAME="docancel"', + 'label' => qa_lang_html('main/cancel_button'), + ), + ), + + 'hidden' => array( + 'dosavepage' => '1', // for IE + 'edit' => @$editpage['pageid'], + 'external' => (int)$isexternal, + ), + ); + + if ($isexternal) { + unset($qa_content['form']['fields']['slug']); + unset($qa_content['form']['fields']['heading']); + unset($qa_content['form']['fields']['content']); + + } else { + unset($qa_content['form']['fields']['url']); + unset($qa_content['form']['fields']['newwindow']); + } + + if (isset($editpage['pageid'])) + qa_set_display_rules($qa_content, array( + 'position_display' => '!dodelete', + 'permit_display' => '!dodelete', + ($isexternal ? 'url_display' : 'slug_display') => '!dodelete', + ($isexternal ? 'newwindow_display' : 'heading_display') => '!dodelete', + 'content_display' => '!dodelete', + )); + + else { + unset($qa_content['form']['fields']['slug']); + unset($qa_content['form']['fields']['delete']); + } + + $qa_content['focusid']='name'; + + } else { + + // List of standard navigation links + + $qa_content['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + + 'style' => 'tall', + + 'fields' => array(), + + 'buttons' => array( + 'save' => array( + 'tags' => 'NAME="dosaveoptions"', + 'label' => qa_lang_html('main/save_button'), + ), + + 'addpage' => array( + 'tags' => 'NAME="doaddpage"', + 'label' => qa_lang_html('admin/add_page_button'), + ), + + 'addlink' => array( + 'tags' => 'NAME="doaddlink"', + 'label' => qa_lang_html('admin/add_link_button'), + ), + ), + ); + + $qa_content['form']['fields']['navlinks']=array( + 'label' => qa_lang_html('admin/nav_links_explanation'), + 'type' => 'static', + 'tight' => true, + ); + + foreach ($navoptions as $optionname => $langkey) { + $qa_content['form']['fields'][$optionname]=array( + 'label' => ''.qa_lang_html($langkey).'', + 'tags' => 'NAME="option_'.$optionname.'"', + 'type' => 'checkbox', + 'value' => qa_opt($optionname), + ); + } + + $qa_content['form']['fields'][]=array( + 'type' => 'blank' + ); + + // List of suggested plugin pages + + $listhtml=''; + + $pagemodules=qa_load_modules_with('page', 'suggest_requests'); + + foreach ($pagemodules as $tryname => $trypage) { + $suggestrequests=$trypage->suggest_requests(); + + foreach ($suggestrequests as $suggestrequest) { + $listhtml.='
  • '.qa_html($suggestrequest['title']).''; + + $listhtml.=qa_lang_html_sub('admin/plugin_module', qa_html($tryname)); + + $listhtml.=strtr(qa_lang_html('admin/add_link_link'), array( + '^1' => '', + '^2' => '', + )); + + if (method_exists($trypage, 'admin_form')) + $listhtml.=' - '.qa_lang_html('admin/options').''; + + $listhtml.='
  • '; + } + } + + if (strlen($listhtml)) + $qa_content['form']['fields']['plugins']=array( + 'label' => qa_lang_html('admin/plugin_pages_explanation'), + 'type' => 'custom', + 'html' => '
      '.$listhtml.'
    ', + ); + + // List of custom pages or links + + $listhtml=''; + + foreach ($pages as $page) { + $listhtml.='
  • '.qa_html($page['title']).''; + + $listhtml.=strtr(qa_lang_html(($page['flags'] & QA_PAGE_FLAGS_EXTERNAL) ? 'admin/edit_link' : 'admin/edit_page'), array( + '^1' => '', + '^2' => '', + )); + + $listhtml.='
  • '; + } + + $qa_content['form']['fields']['pages']=array( + 'label' => strlen($listhtml) ? qa_lang_html('admin/click_name_edit') : qa_lang_html('admin/pages_explanation'), + 'type' => 'custom', + 'html' => strlen($listhtml) ? '
      '.$listhtml.'
    ' : null, + ); + } + + $qa_content['navigation']['sub']=qa_admin_sub_navigation(); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-admin-plugins.php b/qa-include/qa-page-admin-plugins.php new file mode 100644 index 000000000..d1f3f4074 --- /dev/null +++ b/qa-include/qa-page-admin-plugins.php @@ -0,0 +1,224 @@ + 'tall', + + 'fields' => array( + 'plugins' => array( + 'type' => 'custom', + 'label' => qa_lang_html('admin/installed_plugins'), + 'html' => '', + ), + ), + ); + + foreach ($pluginfiles as $pluginfile) { + $plugindirectory=dirname($pluginfile).'/'; + + $contents=file_get_contents($pluginfile); + + $metadata=qa_admin_addon_metadata($contents, array( + 'name' => 'Plugin Name', + 'uri' => 'Plugin URI', + 'description' => 'Plugin Description', + 'version' => 'Plugin Version', + 'date' => 'Plugin Date', + 'author' => 'Plugin Author', + 'author_uri' => 'Plugin Author URI', + 'license' => 'Plugin License', + 'min_q2a' => 'Plugin Minimum Question2Answer Version', + 'update' => 'Plugin Update Check URI', + )); + + if (strlen(@$metadata['name'])) + $namehtml=qa_html($metadata['name']); + else + $namehtml=qa_lang_html('admin/unnamed_plugin'); + + if (strlen(@$metadata['uri'])) + $namehtml=''.$namehtml.''; + + $namehtml=''.$namehtml.''; + + if (strlen(@$metadata['version'])) + $namehtml.=' v'.qa_html($metadata['version']); + + if (strlen(@$metadata['author'])) { + $authorhtml=qa_html($metadata['author']); + + if (strlen(@$metadata['author_uri'])) + $authorhtml=''.$authorhtml.''; + + $authorhtml=qa_lang_html_sub('main/by_x', $authorhtml); + + } else + $authorhtml=''; + + if (strlen(@$metadata['version']) && strlen(@$metadata['update'])) { + $elementid='version_check_'.md5($plugindirectory); + + $updatehtml='(...)'; + + $qa_content['script_onloads'][]=array( + "qa_version_check(".qa_js($metadata['update']).", 'Plugin Version', ".qa_js($metadata['version']).", 'Plugin URI', ".qa_js($elementid).");" + ); + + } else + $updatehtml=''; + + if (strlen(@$metadata['description'])) + $deschtml=qa_html($metadata['description']); + else + $deschtml=''; + + if (isset($pluginoptionanchors[$plugindirectory])) + foreach ($pluginoptionanchors[$plugindirectory] as $anchor) + $deschtml.=(strlen($deschtml) ? ' - ' : '').''.qa_lang_html('admin/options').''; + + $pluginhtml=$namehtml.' '.$authorhtml.' '.$updatehtml.'
    '.$deschtml.(strlen($deschtml) ? '
    ' : ''). + ''.qa_html($plugindirectory).''; + + if (is_numeric(@$metadata['min_q2a']) && ((float)QA_VERSION>0) && $metadata['min_q2a']>(float)QA_VERSION) + $pluginhtml=''.$pluginhtml.'
    '. + qa_lang_html_sub('admin/requires_q2a_version', qa_html($metadata['min_q2a'])).''; + + $qa_content['form']['fields'][]=array( + 'type' => 'custom', + 'html' => $pluginhtml, + ); + } + } + + $formadded=false; + + $moduletypes=qa_list_module_types(); + + foreach ($moduletypes as $type) { + $modules=qa_load_modules_with($type, 'admin_form'); + + foreach ($modules as $name => $module) { + $form=$module->admin_form($qa_content); + + if (!isset($form['title'])) + $form['title']=qa_html($name); + + $identifierhtml=qa_html(md5($type.'/'.$name)); + + $form['title']=''.$form['title'].''; + + if (!isset($form['tags'])) + $form['tags']='METHOD="POST" ACTION="'.qa_self_html().'#'.$identifierhtml.'"'; + + if (!isset($form['style'])) + $form['style']='tall'; + + $qa_content['form_'.$type.'_'.$name]=$form; + $formadded=true; + } + } + + foreach ($moduletypes as $type) { + $modules=qa_load_modules_with($type, 'init_queries'); + + foreach ($modules as $name => $module) { + $queries=$module->init_queries($tables); + + if (!empty($queries)) { + if (qa_is_http_post()) + qa_redirect('install'); + + else + $qa_content['error']=strtr(qa_lang_html('admin/module_x_database_init'), array( + '^1' => qa_html($name), + '^2' => qa_html($type), + '^3' => '', + '^4' => '', + )); + } + } + } + + if (!$formadded) + $qa_content['suggest_next']=qa_lang_html('admin/no_plugin_options'); + + $qa_content['navigation']['sub']=qa_admin_sub_navigation(); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-admin-points.php b/qa-include/qa-page-admin-points.php new file mode 100644 index 000000000..a9e1c82bd --- /dev/null +++ b/qa-include/qa-page-admin-points.php @@ -0,0 +1,183 @@ + 1)); + } + + $options=qa_get_options($optionnames); + } + + +// Prepare content for theme + + $qa_content=qa_content_prepare(); + + $qa_content['title']=qa_lang_html('admin/admin_title').' - '.qa_lang_html('admin/points_title'); + + $qa_content['error']=qa_admin_page_error(); + + $qa_content['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'" NAME="points_form" onsubmit="document.forms.points_form.has_js.value=1; return true;"', + + 'style' => 'wide', + + 'buttons' => array( + 'saverecalc' => array( + 'tags' => 'ID="dosaverecalc"', + 'label' => qa_lang_html('admin/save_recalc_button'), + ), + ), + + 'hidden' => array( + 'dosaverecalc' => '1', + 'has_js' => '0', + ), + ); + + + if (qa_clicked('doshowdefaults')) { + $qa_content['form']['ok']=qa_lang_html('admin/points_defaults_shown'); + + $qa_content['form']['buttons']['cancel']=array( + 'tags' => 'NAME="docancel"', + 'label' => qa_lang_html('main/cancel_button'), + ); + + } else { + if (qa_clicked('docancel')) + ; + elseif (qa_clicked('dosaverecalc')) { + $qa_content['form']['ok']=''; + + $qa_content['script_rel'][]='qa-content/qa-admin.js?'.QA_VERSION; + $qa_content['script_var']['qa_warning_recalc']=qa_lang('admin/stop_recalc_warning'); + + $qa_content['script_onloads'][]=array( + "qa_recalc_click('dorecalcpoints', document.getElementById('recalc_ok'), null, 'recalc_ok');" + ); + } + + $qa_content['form']['buttons']['showdefaults']=array( + 'tags' => 'NAME="doshowdefaults"', + 'label' => qa_lang_html('admin/show_defaults_button'), + ); + } + + + foreach ($optionnames as $optionname) { + $optionfield=array( + 'label' => qa_lang_html('options/'.$optionname), + 'tags' => 'NAME="option_'.$optionname.'"', + 'value' => qa_html($options[$optionname]), + 'type' => 'number', + 'note' => qa_lang_html('admin/points'), + ); + + switch ($optionname) { + case 'points_multiple': + $prefix='×'; + unset($optionfield['note']); + break; + + case 'points_per_q_voted_up': + case 'points_per_a_voted_up': + case 'points_q_voted_max_gain': + case 'points_a_voted_max_gain': + $prefix='+'; + break; + + case 'points_per_q_voted_down': + case 'points_per_a_voted_down': + case 'points_q_voted_max_loss': + case 'points_a_voted_max_loss': + $prefix='–'; + break; + + case 'points_base': + $prefix='+'; + break; + + default: + $prefix='+'; // for even alignment + break; + } + + $optionfield['prefix']=''.$prefix.''; + + $qa_content['form']['fields'][$optionname]=$optionfield; + } + + qa_array_insert($qa_content['form']['fields'], 'points_post_a', array('blank0' => array('type' => 'blank'))); + qa_array_insert($qa_content['form']['fields'], 'points_vote_up_q', array('blank1' => array('type' => 'blank'))); + qa_array_insert($qa_content['form']['fields'], 'points_multiple', array('blank2' => array('type' => 'blank'))); + + + $qa_content['navigation']['sub']=qa_admin_sub_navigation(); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-admin-recalc.php b/qa-include/qa-page-admin-recalc.php new file mode 100644 index 000000000..f41313e6b --- /dev/null +++ b/qa-include/qa-page-admin-recalc.php @@ -0,0 +1,107 @@ + + + + + + + + + +\n"; + + flush(); + sleep(1); // ... then rest for one + } + +?> + + + + + + + 'wide', + + 'fields' => array( + 'q2a_version' => array( + 'label' => qa_lang_html('admin/q2a_version'), + 'value' => qa_html(QA_VERSION), + ), + + 'q2a_date' => array( + 'label' => qa_lang_html('admin/q2a_build_date'), + 'value' => qa_html(QA_BUILD_DATE), + ), + + 'q2a_latest' => array( + 'label' => qa_lang_html('admin/q2a_latest_version'), + 'type' => 'custom', + 'html' => '', + ), + + 'break0' => array( + 'type' => 'blank', + ), + + 'db_version' => array( + 'label' => qa_lang_html('admin/q2a_db_version'), + 'value' => qa_html(qa_opt('db_version')), + ), + + 'db_size' => array( + 'label' => qa_lang_html('admin/q2a_db_size'), + 'value' => qa_html(number_format(qa_db_table_size()/1048576, 1).' MB'), + ), + + 'break1' => array( + 'type' => 'blank', + ), + + 'php_version' => array( + 'label' => qa_lang_html('admin/php_version'), + 'value' => qa_html(phpversion()), + ), + + 'mysql_version' => array( + 'label' => qa_lang_html('admin/mysql_version'), + 'value' => qa_html(qa_db_mysql_version()), + ), + + 'break2' => array( + 'type' => 'blank', + ), + + 'qcount' => array( + 'label' => qa_lang_html('admin/total_qs'), + 'value' => qa_html(number_format($qcount)), + ), + + 'qcount_users' => array( + 'label' => qa_lang_html('admin/from_users'), + 'value' => qa_html(number_format($qcount-$qcount_anon)), + ), + + 'qcount_anon' => array( + 'label' => qa_lang_html('admin/from_anon'), + 'value' => qa_html(number_format($qcount_anon)), + ), + + 'break3' => array( + 'type' => 'blank', + ), + + 'acount' => array( + 'label' => qa_lang_html('admin/total_as'), + 'value' => qa_html(number_format($acount)), + ), + + 'acount_users' => array( + 'label' => qa_lang_html('admin/from_users'), + 'value' => qa_html(number_format($acount-$acount_anon)), + ), + + 'acount_anon' => array( + 'label' => qa_lang_html('admin/from_anon'), + 'value' => qa_html(number_format($acount_anon)), + ), + + 'break4' => array( + 'type' => 'blank', + ), + + 'ccount' => array( + 'label' => qa_lang_html('admin/total_cs'), + 'value' => qa_html(number_format($ccount)), + ), + + 'ccount_users' => array( + 'label' => qa_lang_html('admin/from_users'), + 'value' => qa_html(number_format($ccount-$ccount_anon)), + ), + + 'ccount_anon' => array( + 'label' => qa_lang_html('admin/from_anon'), + 'value' => qa_html(number_format($ccount_anon)), + ), + + 'break5' => array( + 'type' => 'blank', + ), + + 'users' => array( + 'label' => qa_lang_html('admin/users_registered'), + 'value' => QA_FINAL_EXTERNAL_USERS ? '' : qa_html(number_format(qa_db_count_users())), + ), + + 'users_active' => array( + 'label' => qa_lang_html('admin/users_active'), + 'value' => qa_html(number_format((int)qa_opt('cache_userpointscount'))), + ), + + 'users_posted' => array( + 'label' => qa_lang_html('admin/users_posted'), + 'value' => qa_html(number_format(qa_db_count_active_users('posts'))), + ), + + 'users_voted' => array( + 'label' => qa_lang_html('admin/users_voted'), + 'value' => qa_html(number_format(qa_db_count_active_users('uservotes'))), + ), + ), + ); + + if (QA_FINAL_EXTERNAL_USERS) + unset($qa_content['form']['fields']['users']); + else + unset($qa_content['form']['fields']['users_active']); + + foreach ($qa_content['form']['fields'] as $index => $field) + if (empty($field['type'])) + $qa_content['form']['fields'][$index]['type']='static'; + + $qa_content['form_2']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_path_html('admin/recalc').'"', + + 'title' => qa_lang_html('admin/database_cleanup'), + + 'style' => 'basic', + + 'buttons' => array( + 'recount_posts' => array( + 'label' => qa_lang_html('admin/recount_posts'), + 'tags' => 'NAME="dorecountposts" onClick="return qa_recalc_click(this.name, this, '.qa_js(qa_lang('admin/recount_posts_stop')).', \'recount_posts_note\');"', + 'note' => ''.qa_lang_html('admin/recount_posts_note').'', + ), + + 'reindex_posts' => array( + 'label' => qa_lang_html('admin/reindex_posts'), + 'tags' => 'NAME="doreindexposts" onClick="return qa_recalc_click(this.name, this, '.qa_js(qa_lang('admin/reindex_posts_stop')).', \'reindex_posts_note\');"', + 'note' => ''.qa_lang_html('admin/reindex_posts_note').'', + ), + + 'recalc_points' => array( + 'label' => qa_lang_html('admin/recalc_points'), + 'tags' => 'NAME="dorecalcpoints" onClick="return qa_recalc_click(this.name, this, '.qa_js(qa_lang('admin/recalc_stop')).', \'recalc_points_note\');"', + 'note' => ''.qa_lang_html('admin/recalc_points_note').'', + ), + + 'refill_events' => array( + 'label' => qa_lang_html('admin/refill_events'), + 'tags' => 'NAME="dorefillevents" onClick="return qa_recalc_click(this.name, this, '.qa_js(qa_lang('admin/recalc_stop')).', \'refill_events_note\');"', + 'note' => ''.qa_lang_html('admin/refill_events_note').'', + ), + + 'recalc_categories' => array( + 'label' => qa_lang_html('admin/recalc_categories'), + 'tags' => 'NAME="dorecalccategories" onClick="return qa_recalc_click(this.name, this, '.qa_js(qa_lang('admin/recalc_stop')).', \'recalc_categories_note\');"', + 'note' => ''.qa_lang_html('admin/recalc_categories_note').'', + ), + + 'delete_hidden' => array( + 'label' => qa_lang_html('admin/delete_hidden'), + 'tags' => 'NAME="dodeletehidden" onClick="return qa_recalc_click(this.name, this, '.qa_js(qa_lang('admin/delete_stop')).', \'delete_hidden_note\');"', + 'note' => ''.qa_lang_html('admin/delete_hidden_note').'', + ), + ), + ); + + if (!qa_using_categories()) + unset($qa_content['form_2']['buttons']['recalc_categories']); + + $qa_content['script_rel'][]='qa-content/qa-admin.js?'.QA_VERSION; + $qa_content['script_var']['qa_warning_recalc']=qa_lang('admin/stop_recalc_warning'); + + $qa_content['navigation']['sub']=qa_admin_sub_navigation(); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-admin-userfields.php b/qa-include/qa-page-admin-userfields.php new file mode 100644 index 000000000..a3c881468 --- /dev/null +++ b/qa-include/qa-page-admin-userfields.php @@ -0,0 +1,233 @@ +QA_DB_MAX_PROFILE_TITLE_LENGTH) + $errors['name']=qa_lang_sub('main/max_length_x', QA_DB_MAX_PROFILE_TITLE_LENGTH); + + // Perform appropriate database action + + if (isset($editfield['fieldid'])) { // changing existing user field + qa_db_userfield_set_fields($editfield['fieldid'], isset($errors['name']) ? $editfield['content'] : $inname, $inflags); + qa_db_userfield_move($editfield['fieldid'], $inposition); + + if (empty($errors)) + qa_redirect('admin/users'); + + else { + $userfields=qa_db_select_with_pending(qa_db_userfields_selectspec()); // reload after changes + foreach ($userfields as $userfield) + if ($userfield['fieldid']==$editfield['fieldid']) + $editfield=$userfield; + } + + } elseif (empty($errors)) { // creating a new user field + + for ($attempt=0; $attempt<1000; $attempt++) { + $suffix=$attempt ? ('-'.(1+$attempt)) : ''; + $newtag=qa_substr(implode('-', qa_string_to_words($inname)), 0, QA_DB_MAX_PROFILE_TITLE_LENGTH-strlen($suffix)).$suffix; + $uniquetag=true; + + foreach ($userfields as $userfield) + if (qa_strtolower(trim($newtag)) == qa_strtolower(trim($userfield['title']))) + $uniquetag=false; + + if ($uniquetag) { + $fieldid=qa_db_userfield_create($newtag, $inname, $inflags); + qa_db_userfield_move($fieldid, $inposition); + qa_redirect('admin/users'); + } + } + + qa_fatal_error('Could not create a unique database tag'); + } + } + } + + +// Prepare content for theme + + $qa_content=qa_content_prepare(); + + $qa_content['title']=qa_lang_html('admin/admin_title').' - '.qa_lang_html('admin/users_title'); + + $qa_content['error']=qa_admin_page_error(); + + $positionoptions=array(); + $previous=null; + $passedself=false; + + foreach ($userfields as $userfield) { + if (isset($previous)) + $positionhtml=qa_lang_html_sub('admin/after_x', qa_html(qa_user_userfield_label($passedself ? $userfield : $previous))); + else + $positionhtml=qa_lang_html('admin/first'); + + $positionoptions[$userfield['position']]=$positionhtml; + + if ($userfield['fieldid']==@$editfield['fieldid']) + $passedself=true; + + $previous=$userfield; + } + + if (isset($editfield['position'])) + $positionvalue=$positionoptions[$editfield['position']]; + else { + $positionvalue=isset($previous) ? qa_lang_html_sub('admin/after_x', qa_html(qa_user_userfield_label($previous))) : qa_lang_html('admin/first'); + $positionoptions[1+@max(array_keys($positionoptions))]=$positionvalue; + } + + $typeoptions=array( + 0 => qa_lang_html('admin/field_single_line'), + QA_FIELD_FLAGS_MULTI_LINE => qa_lang_html('admin/field_multi_line'), + QA_FIELD_FLAGS_LINK_URL => qa_lang_html('admin/field_link_url'), + ); + + $qa_content['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_path_html(qa_request()).'"', + + 'style' => 'tall', + + 'fields' => array( + 'name' => array( + 'tags' => 'NAME="name" ID="name"', + 'label' => qa_lang_html('admin/field_name'), + 'value' => qa_html(isset($inname) ? $inname : qa_user_userfield_label($editfield)), + 'error' => qa_html(@$errors['name']), + ), + + 'type' => array( + 'id' => 'type_display', + 'tags' => 'NAME="flags"', + 'label' => qa_lang_html('admin/field_type'), + 'type' => 'select', + 'options' => $typeoptions, + 'value' => @$typeoptions[isset($inflags) ? $inflags : @$editfield['flags']], + ), + + 'delete' => array( + 'tags' => 'NAME="dodelete" ID="dodelete"', + 'label' => qa_lang_html('admin/delete_field'), + 'value' => 0, + 'type' => 'checkbox', + ), + + 'position' => array( + 'id' => 'position_display', + 'tags' => 'NAME="position"', + 'label' => qa_lang_html('admin/position'), + 'type' => 'select', + 'options' => $positionoptions, + 'value' => $positionvalue, + ), + ), + + 'buttons' => array( + 'save' => array( + 'label' => qa_lang_html(isset($editfield['fieldid']) ? 'main/save_button' : ('admin/add_field_button')), + ), + + 'cancel' => array( + 'tags' => 'NAME="docancel"', + 'label' => qa_lang_html('main/cancel_button'), + ), + ), + + 'hidden' => array( + 'dosavefield' => '1', // for IE + 'edit' => @$editfield['fieldid'], + ), + ); + + if (isset($editfield['fieldid'])) + qa_set_display_rules($qa_content, array( + 'position_display' => '!dodelete', + )); + else + unset($qa_content['form']['fields']['delete']); + + $qa_content['focusid']='name'; + + $qa_content['navigation']['sub']=qa_admin_sub_navigation(); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-admin-usertitles.php b/qa-include/qa-page-admin-usertitles.php new file mode 100644 index 000000000..8721b1e17 --- /dev/null +++ b/qa-include/qa-page-admin-usertitles.php @@ -0,0 +1,182 @@ + $title) + $option.=(strlen($option) ? ',' : '').$points.' '.$title; + + qa_set_option('points_to_titles', $option); + + if (empty($errors)) + qa_redirect('admin/users'); + } + + +// Prepare content for theme + + $qa_content=qa_content_prepare(); + + $qa_content['title']=qa_lang_html('admin/admin_title').' - '.qa_lang_html('admin/users_title'); + + $qa_content['error']=qa_admin_page_error(); + + $qa_content['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_path_html(qa_request()).'"', + + 'style' => 'tall', + + 'fields' => array( + 'title' => array( + 'tags' => 'NAME="title" ID="title"', + 'label' => qa_lang_html('admin/user_title'), + 'value' => qa_html(isset($intitle) ? $intitle : @$pointstitle[$oldpoints]), + 'error' => qa_html(@$errors['title']), + ), + + 'delete' => array( + 'tags' => 'NAME="dodelete" ID="dodelete"', + 'label' => qa_lang_html('admin/delete_title'), + 'value' => 0, + 'type' => 'checkbox', + ), + + 'points' => array( + 'id' => 'points_display', + 'tags' => 'NAME="points"', + 'label' => qa_lang_html('admin/points_required'), + 'type' => 'number', + 'value' => qa_html(isset($inpoints) ? $inpoints : @$oldpoints), + 'error' => qa_html(@$errors['points']), + ), + ), + + 'buttons' => array( + 'save' => array( + 'label' => qa_lang_html(isset($pointstitle[$oldpoints]) ? 'main/save_button' : ('admin/add_title_button')), + ), + + 'cancel' => array( + 'tags' => 'NAME="docancel"', + 'label' => qa_lang_html('main/cancel_button'), + ), + ), + + 'hidden' => array( + 'dosavetitle' => '1', // for IE + 'edit' => @$oldpoints, + ), + ); + + if (isset($pointstitle[$oldpoints])) + qa_set_display_rules($qa_content, array( + 'points_display' => '!dodelete', + )); + else + unset($qa_content['form']['fields']['delete']); + + $qa_content['focusid']='title'; + + $qa_content['navigation']['sub']=qa_admin_sub_navigation(); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-admin-widgets.php b/qa-include/qa-page-admin-widgets.php new file mode 100644 index 000000000..6c1b90a4f --- /dev/null +++ b/qa-include/qa-page-admin-widgets.php @@ -0,0 +1,328 @@ + qa_post_text('title')); + if (!isset($editwidget['title'])) + $editwidget['title']=qa_get('title'); + } + + $module=qa_load_module('widget', @$editwidget['title']); + + $widgetfound=isset($module); + + +// Check admin privileges (do late to allow one DB query) + + if (!qa_admin_check_privileges($qa_content)) + return $qa_content; + + +// Define an array of relevant templates we can use + + $templatelangkeys=array( + 'question' => 'admin/question_pages', + + 'qa' => 'main/recent_qs_as_title', + 'activity' => 'main/recent_activity_title', + 'questions' => 'admin/question_lists', + 'hot' => 'main/hot_qs_title', + 'unanswered' => 'main/unanswered_qs_title', + + 'tags' => 'main/popular_tags', + 'categories' => 'misc/browse_categories', + 'users' => 'main/highest_users', + 'ask' => 'question/ask_title', + + 'tag' => 'admin/tag_pages', + 'user' => 'admin/user_pages', + 'message' => 'misc/private_message_title', + + 'search' => 'main/search_title', + 'feedback' => 'misc/feedback_title', + + 'login' => 'users/login_title', + 'register' => 'users/register_title', + 'account' => 'profile/my_account_title', + + 'ip' => 'admin/ip_address_pages', + 'admin' => 'admin/admin_title', + ); + + $templateoptions=array(); + + if (isset($module) && method_exists($module, 'allow_template')) { + foreach ($templatelangkeys as $template => $langkey) + if ($module->allow_template($template)) + $templateoptions[$template]=qa_lang_html($langkey); + + if ($module->allow_template('custom')) + foreach ($pages as $page) + if (!($page['flags']&QA_PAGE_FLAGS_EXTERNAL)) + $templateoptions['custom-'.$page['pageid']]=qa_html($page['title']); + } + + +// Process saving an old or new widget + + if (qa_clicked('docancel')) + qa_redirect('admin/layout'); + + elseif (qa_clicked('dosavewidget')) { + require_once QA_INCLUDE_DIR.'qa-db-admin.php'; + + if (qa_post_text('dodelete')) { + qa_db_widget_delete($editwidget['widgetid']); + qa_redirect('admin/layout'); + + } else { + if ($widgetfound) { + $intitle=qa_post_text('title'); + $inposition=qa_post_text('position'); + $intemplates=array(); + + if (qa_post_text('template_all')) + $intemplates[]='all'; + + foreach (array_keys($templateoptions) as $template) + if (qa_post_text('template_'.$template)) + $intemplates[]=$template; + + $intags=implode(',', $intemplates); + + // Perform appropriate database action + + if (isset($editwidget['widgetid'])) { // changing existing widget + $widgetid=$editwidget['widgetid']; + qa_db_widget_set_fields($widgetid, $intags); + + } else + $widgetid=qa_db_widget_create($intitle, $intags); + + qa_db_widget_move($widgetid, substr($inposition, 0, 2), substr($inposition, 2)); + } + + qa_redirect('admin/layout'); + } + } + + +// Prepare content for theme + + $qa_content=qa_content_prepare(); + + $qa_content['title']=qa_lang_html('admin/admin_title').' - '.qa_lang_html('admin/layout_title'); + + $qa_content['error']=qa_admin_page_error(); + + $positionoptions=array(); + + $placeoptionhtml=qa_admin_place_options(); + + $regioncodes=array( + 'F' => 'full', + 'M' => 'main', + 'S' => 'side', + ); + + foreach ($placeoptionhtml as $place => $optionhtml) { + $region=$regioncodes[substr($place, 0, 1)]; + + $widgetallowed=method_exists($module, 'allow_region') && $module->allow_region($region); + + if ($widgetallowed) + foreach ($widgets as $widget) + if ( ($widget['place']==$place) && ($widget['title']==$editwidget['title']) && ($widget['widgetid']!==@$editwidget['widgetid']) ) + $widgetallowed=false; // don't allow two instances of same widget in same place + + if ($widgetallowed) { + $previous=null; + $passedself=false; + $maxposition=0; + + foreach ($widgets as $widget) + if ($widget['place']==$place) { + $positionhtml=$optionhtml; + + if (isset($previous)) + $positionhtml.=' - '.qa_lang_html_sub('admin/after_x', qa_html($passedself ? $widget['title'] : $previous['title'])); + + if ($widget['widgetid']==@$editwidget['widgetid']) + $passedself=true; + + $maxposition=max($maxposition, $widget['position']); + $positionoptions[$place.$widget['position']]=$positionhtml; + + $previous=$widget; + } + + if ((!isset($editwidget['widgetid'])) || $place!=@$editwidget['place']) { + $positionhtml=$optionhtml; + + if (isset($previous)) + $positionhtml.=' - '.qa_lang_html_sub('admin/after_x', $previous['title']); + + $positionoptions[$place.(isset($previous) ? (1+$maxposition) : 1)]=$positionhtml; + } + } + } + + $positionvalue=@$positionoptions[$editwidget['place'].$editwidget['position']]; + + $qa_content['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_path_html(qa_request()).'"', + + 'style' => 'tall', + + 'fields' => array( + 'title' => array( + 'label' => qa_lang_html('admin/widget_name').'   '.qa_html($editwidget['title']), + 'type' => 'static', + 'tight' => true, + ), + + 'position' => array( + 'id' => 'position_display', + 'tags' => 'NAME="position"', + 'label' => qa_lang_html('admin/position'), + 'type' => 'select', + 'options' => $positionoptions, + 'value' => $positionvalue, + ), + + 'delete' => array( + 'tags' => 'NAME="dodelete" ID="dodelete"', + 'label' => qa_lang_html('admin/delete_widget_position'), + 'value' => 0, + 'type' => 'checkbox', + ), + + 'all' => array( + 'id' => 'all_display', + 'label' => qa_lang_html('admin/widget_all_pages'), + 'type' => 'checkbox', + 'tags' => 'NAME="template_all" ID="template_all"', + 'value' => is_numeric(strpos(','.@$editwidget['tags'].',', ',all,')), + ), + + 'templates' => array( + 'id' => 'templates_display', + 'label' => qa_lang_html('admin/widget_pages_explanation'), + 'type' => 'custom', + 'html' => '', + ), + ), + + 'buttons' => array( + 'save' => array( + 'label' => qa_lang_html(isset($editwidget['widgetid']) ? 'main/save_button' : ('admin/add_widget_button')), + ), + + 'cancel' => array( + 'tags' => 'NAME="docancel"', + 'label' => qa_lang_html('main/cancel_button'), + ), + ), + + 'hidden' => array( + 'dosavewidget' => '1', // for IE + 'edit' => @$editwidget['widgetid'], + 'title' => @$editwidget['title'], + ), + ); + + foreach ($templateoptions as $template => $optionhtml) + $qa_content['form']['fields']['templates']['html'].= + ' '.$optionhtml.'
    '; + + if (isset($editwidget['widgetid'])) + qa_set_display_rules($qa_content, array( + 'templates_display' => '!(dodelete||template_all)', + 'all_display' => '!dodelete', + )); + + else { + unset($qa_content['form']['fields']['delete']); + qa_set_display_rules($qa_content, array( + 'templates_display' => '!template_all', + )); + } + + if (!$widgetfound) { + unset($qa_content['form']['fields']['title']['tight']); + $qa_content['form']['fields']['title']['error']=qa_lang_html('admin/widget_not_available'); + unset($qa_content['form']['fields']['position']); + unset($qa_content['form']['fields']['all']); + unset($qa_content['form']['fields']['templates']); + if (!isset($editwidget['widgetid'])) + unset($qa_content['form']['buttons']['save']); + + } elseif (!count($positionoptions)) { + unset($qa_content['form']['fields']['title']['tight']); + $qa_content['form']['fields']['title']['error']=qa_lang_html('admin/widget_no_positions'); + unset($qa_content['form']['fields']['position']); + unset($qa_content['form']['fields']['all']); + unset($qa_content['form']['fields']['templates']); + unset($qa_content['form']['buttons']['save']); + } + + $qa_content['navigation']['sub']=qa_admin_sub_navigation(); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-answers.php b/qa-include/qa-page-answers.php new file mode 100644 index 000000000..69a7f76ce --- /dev/null +++ b/qa-include/qa-page-answers.php @@ -0,0 +1,84 @@ + $followpostid) : null); + break; + + case 'confirm': + $qa_content['error']=qa_insert_login_links(qa_lang_html('question/ask_must_confirm'), qa_request(), isset($followpostid) ? array('follow' => $followpostid) : null); + break; + + case 'limit': + $qa_content['error']=qa_lang_html('question/ask_limit'); + break; + + default: + $qa_content['error']=qa_lang_html('users/no_permission'); + break; + } + + return $qa_content; + } + + +// Process input + + $usecaptcha=qa_user_use_captcha('captcha_on_anon_post'); + + $in['title']=qa_post_text('title'); // allow title and tags to be posted by an external form + $in['extra']=qa_opt('extra_field_active') ? qa_post_text('extra') : null; + $in['tags']=qa_get_tags_field_value('tags'); + + if (qa_clicked('doask')) { + require_once QA_INCLUDE_DIR.'qa-app-post-create.php'; + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + $in['notify']=qa_post_text('notify') ? true : false; + $in['email']=qa_post_text('email'); + $in['queued']=qa_user_moderation_reason() ? true : false; + + qa_get_post_content('editor', 'content', $in['editor'], $in['content'], $in['format'], $in['text']); + + $errors=array(); + + $filtermodules=qa_load_modules_with('filter', 'filter_question'); + foreach ($filtermodules as $filtermodule) + $filtermodule->filter_question($in, $errors, null); + + if (qa_using_categories() && count($categories) && (!qa_opt('allow_no_category')) && !isset($in['categoryid'])) + $errors['categoryid']=qa_lang_html('question/category_required'); // check this here because we need to know count($categories) + + if ($usecaptcha) { + require_once 'qa-app-captcha.php'; + qa_captcha_validate_post($errors); + } + + if (empty($errors)) { + $cookieid=isset($userid) ? qa_cookie_get() : qa_cookie_get_create(); // create a new cookie if necessary + + $questionid=qa_question_create($followanswer, $userid, qa_get_logged_in_handle(), $cookieid, + $in['title'], $in['content'], $in['format'], $in['text'], qa_tags_to_tagstring($in['tags']), + $in['notify'], $in['email'], $in['categoryid'], $in['extra'], $in['queued']); + + qa_redirect(qa_q_request($questionid, $in['title'])); // our work is done here + } + } + + +// Prepare content for theme + + $qa_content=qa_content_prepare(false, array_keys(qa_category_path($categories, @$in['categoryid']))); + + $qa_content['title']=qa_lang_html(isset($followanswer) ? 'question/ask_follow_title' : 'question/ask_title'); + + $editorname=isset($in['editor']) ? $in['editor'] : qa_opt('editor_for_qs'); + $editor=qa_load_editor(@$in['content'], @$in['format'], $editorname); + + $field=qa_editor_load_field($editor, $qa_content, @$in['content'], @$in['format'], 'content', 12, false); + $field['label']=qa_lang_html('question/q_content_label'); + $field['error']=qa_html(@$errors['content']); + + $custom=qa_opt('show_custom_ask') ? trim(qa_opt('custom_ask')) : ''; + + $qa_content['form']=array( + 'tags' => 'NAME="ask" METHOD="POST" ACTION="'.qa_self_html().'"', + + 'style' => 'tall', + + 'fields' => array( + 'custom' => array( + 'type' => 'custom', + 'note' => $custom, + ), + + 'title' => array( + 'label' => qa_lang_html('question/q_title_label'), + 'tags' => 'NAME="title" ID="title" AUTOCOMPLETE="off"', + 'value' => qa_html(@$in['title']), + 'error' => qa_html(@$errors['title']), + ), + + 'similar' => array( + 'type' => 'custom', + 'html' => '', + ), + + 'content' => $field, + ), + + 'buttons' => array( + 'ask' => array( + 'label' => qa_lang_html('question/ask_button'), + ), + ), + + 'hidden' => array( + 'editor' => qa_html($editorname), + 'doask' => '1', + ), + ); + + if (!strlen($custom)) + unset($qa_content['form']['fields']['custom']); + + if (qa_opt('do_ask_check_qs') || qa_opt('do_example_tags')) { + $qa_content['script_rel'][]='qa-content/qa-ask.js?'.QA_VERSION; + $qa_content['form']['fields']['title']['tags'].=' onChange="qa_title_change(this.value);"'; + + if (strlen(@$in['title'])) + $qa_content['script_onloads'][]='qa_title_change('.qa_js($in['title']).');'; + } + + if (isset($followanswer)) { + $viewer=qa_load_viewer($followanswer['content'], $followanswer['format']); + + $field=array( + 'type' => 'static', + 'label' => qa_lang_html('question/ask_follow_from_a'), + 'value' => $viewer->get_html($followanswer['content'], $followanswer['format'], array('blockwordspreg' => qa_get_block_words_preg())), + ); + + qa_array_insert($qa_content['form']['fields'], 'title', array('follows' => $field)); + } + + if (qa_using_categories() && count($categories)) { + $field=array( + 'label' => qa_lang_html('question/q_category_label'), + 'error' => qa_html(@$errors['categoryid']), + ); + + qa_set_up_category_field($qa_content, $field, 'category', $categories, $in['categoryid'], true, qa_opt('allow_no_sub_category')); + + if (!qa_opt('allow_no_category')) // don't auto-select a category even though one is required + $field['options']['']=''; + + qa_array_insert($qa_content['form']['fields'], 'content', array('category' => $field)); + } + + if (qa_opt('extra_field_active')) { + $field=array( + 'label' => qa_html(qa_opt('extra_field_prompt')), + 'tags' => 'NAME="extra"', + 'value' => qa_html(@$in['extra']), + 'error' => qa_html(@$errors['extra']), + ); + + qa_array_insert($qa_content['form']['fields'], null, array('extra' => $field)); + } + + if (qa_using_tags()) { + $field=array( + 'error' => qa_html(@$errors['tags']), + ); + + qa_set_up_tag_field($qa_content, $field, 'tags', isset($in['tags']) ? $in['tags'] : array(), array(), + qa_opt('do_complete_tags') ? array_keys($completetags) : array(), qa_opt('page_size_ask_tags')); + + qa_array_insert($qa_content['form']['fields'], null, array('tags' => $field)); + } + + qa_set_up_notify_fields($qa_content, $qa_content['form']['fields'], 'Q', qa_get_logged_in_email(), + isset($in['notify']) ? $in['notify'] : qa_opt('notify_users_default'), @$in['email'], @$errors['email']); + + if ($usecaptcha) { + require_once 'qa-app-captcha.php'; + qa_set_up_captcha_field($qa_content, $qa_content['form']['fields'], @$errors, + qa_insert_login_links(qa_lang_html(isset($userid) ? 'misc/captcha_confirm_fix' : 'misc/captcha_login_fix'))); + } + + $qa_content['focusid']='title'; + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-categories.php b/qa-include/qa-page-categories.php new file mode 100644 index 000000000..0875f78d0 --- /dev/null +++ b/qa-include/qa-page-categories.php @@ -0,0 +1,112 @@ + $navlink) { + $category=$categories[$navlink['categoryid']]; + + if (!$category['childcount']) + unset($navigation[$key]['url']); + elseif ($navlink['selected']) { + $navigation[$key]['state']='open'; + $navigation[$key]['url']=qa_path_html('categories/'.qa_category_path_request($categories, $category['parentid'])); + } else + $navigation[$key]['state']='closed'; + + $navigation[$key]['note']=''; + + $navigation[$key]['note'].= + ' - '.( ($category['qcount']==1) + ? qa_lang_html_sub('main/1_question', '1', '1') + : qa_lang_html_sub('main/x_questions', number_format($category['qcount'])) + ).''; + + if (strlen($category['content'])) + $navigation[$key]['note'].=qa_html(' - '.$category['content']); + + if (isset($navlink['subnav'])) + qa_category_nav_to_browse($navigation[$key]['subnav'], $categories, $categoryid); + } + } + + +// Prepare content for theme + + $qa_content=qa_content_prepare(false, array_keys(qa_category_path($categories, $categoryid))); + + $qa_content['title']=qa_lang_html('misc/browse_categories'); + + if (count($categories)) { + $navigation=qa_category_navigation($categories, $categoryid, 'categories/', false); + + unset($navigation['all']); + + qa_category_nav_to_browse($navigation, $categories, $categoryid); + + $qa_content['nav_list']=array( + 'nav' => $navigation, + 'type' => 'browse-cat', + ); + + } else { + $qa_content['title']=qa_lang_html('main/no_categories_found'); + $qa_content['suggest_next']=qa_html_suggest_qs_tags(qa_using_tags()); + } + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-comments.php b/qa-include/qa-page-comments.php new file mode 100644 index 000000000..51c700613 --- /dev/null +++ b/qa-include/qa-page-comments.php @@ -0,0 +1,83 @@ + '', + '^2' => '', + ) + ); + + } elseif (isset($loginuserid)) { // if logged in, allow sending a fresh link + if (strlen($incode)) + $qa_content['error']=qa_lang_html('users/confirm_wrong_resend'); + + $qa_content['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_path_html('confirm').'"', + + 'style' => 'tall', + + 'fields' => array( + 'email' => array( + 'label' => qa_lang_html('users/email_label'), + 'value' => qa_html(qa_get_logged_in_email()).strtr(qa_lang_html('users/change_email_link'), array( + '^1' => '', + '^2' => '', + )), + 'type' => 'static', + ), + ), + + 'buttons' => array( + 'send' => array( + 'tags' => 'NAME="dosendconfirm"', + 'label' => qa_lang_html('users/send_confirm_button'), + ), + ), + ); + + } else + $qa_content['error']=qa_insert_login_links(qa_lang_html('users/confirm_wrong_log_in'), 'confirm'); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-default.php b/qa-include/qa-page-default.php new file mode 100644 index 000000000..196cc01d5 --- /dev/null +++ b/qa-include/qa-page-default.php @@ -0,0 +1,181 @@ +=QA_USER_LEVEL_ADMIN) { + $qa_content['navigation']['sub']=array( + 'admin/pages' => array( + 'label' => qa_lang('admin/edit_custom_page'), + 'url' => qa_path_html('admin/pages', array('edit' => $custompage['pageid'])), + ), + ); + } + + } else + $qa_content['error']=qa_lang_html('users/no_permission'); + + return $qa_content; + } + + +// Then, see if we should redirect because the 'qa' page is the same as the home page + + if ($explicitqa && (!qa_is_http_post()) && !qa_has_custom_home()) + qa_redirect(qa_category_path_request($categories, $categoryid), $_GET); + + +// Then, if there's a slug that matches no category, check page modules provided by plugins + + if ( (!$explicitqa) && $countslugs && !isset($categoryid) ) { + $pagemodules=qa_load_modules_with('page', 'match_request'); + $request=qa_request(); + + foreach ($pagemodules as $pagemodule) + if ($pagemodule->match_request($request)) { + qa_set_template('plugin'); + return $pagemodule->process_request($request); + } + } + + +// Then, check whether we are showing a custom home page + + if ( (!$explicitqa) && (!$countslugs) && qa_opt('show_custom_home') ) { + qa_set_template('custom'); + $qa_content=qa_content_prepare(); + $qa_content['title']=qa_html(qa_opt('custom_home_heading')); + if (qa_opt('show_home_description')) + $qa_content['description']=qa_html(qa_opt('home_description')); + $qa_content['custom']=qa_opt('custom_home_content'); + return $qa_content; + } + + +// If we got this far, it's a good old-fashioned Q&A listing page + + require_once QA_INCLUDE_DIR.'qa-app-q-list.php'; + + qa_set_template('qa'); + $questions=qa_any_sort_and_dedupe(array_merge($questions1, $questions2)); + $pagesize=qa_opt('page_size_home'); + + if ($countslugs) { + if (!isset($categoryid)) + return include QA_INCLUDE_DIR.'qa-page-not-found.php'; + + $categorytitlehtml=qa_html($categories[$categoryid]['title']); + $sometitle=qa_lang_html_sub('main/recent_qs_as_in_x', $categorytitlehtml); + $nonetitle=qa_lang_html_sub('main/no_questions_in_x', $categorytitlehtml); + + } else { + $sometitle=qa_lang_html('main/recent_qs_as_title'); + $nonetitle=qa_lang_html('main/no_questions_found'); + } + + +// Prepare and return content for theme for Q&A listing page + + $qa_content=qa_q_list_page_content( + $questions, // questions + $pagesize, // questions per page + 0, // start offset + null, // total count (null to hide page links) + $sometitle, // title if some questions + $nonetitle, // title if no questions + $categories, // categories for navigation + $categoryid, // selected category id + true, // show question counts in category navigation + $explicitqa ? 'qa/' : '', // prefix for links in category navigation + qa_opt('feed_for_qa') ? 'qa' : null, // prefix for RSS feed paths (null to hide) + (count($questions)<$pagesize) // suggest what to do next + ? qa_html_suggest_ask($categoryid) + : qa_html_suggest_qs_tags(qa_using_tags(), qa_category_path_request($categories, $categoryid)), + null, // page link params + null, // category nav params + $favorite // has user favorited this category + ); + + if ( (!$explicitqa) && (!$countslugs) && qa_opt('show_home_description') ) + $qa_content['description']=qa_html(qa_opt('home_description')); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-favorites.php b/qa-include/qa-page-favorites.php new file mode 100644 index 000000000..10f670b76 --- /dev/null +++ b/qa-include/qa-page-favorites.php @@ -0,0 +1,155 @@ + count($questions) ? qa_lang_html('main/nav_qs') : qa_lang_html('misc/no_favorite_qs'), + ); + + if (count($questions)) { + $qa_content['q_list']['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + ); + + $qa_content['q_list']['qs']=array(); + + $options=qa_post_html_defaults('Q'); + + foreach ($questions as $question) + $qa_content['q_list']['qs'][]=qa_post_html_fields($question, $userid, qa_cookie_get(), $usershtml, null, $options); + } + + +// Favorite users + + if (!QA_FINAL_EXTERNAL_USERS) { + $qa_content['ranking_users']=array( + 'title' => count($users) ? qa_lang_html('main/nav_users') : qa_lang_html('misc/no_favorite_users'), + 'items' => array(), + 'rows' => ceil(count($users)/qa_opt('columns_users')), + 'type' => 'users' + ); + + foreach ($users as $user) + $qa_content['ranking_users']['items'][]=array( + 'label' => qa_get_user_avatar_html($user['flags'], $user['email'], $user['handle'], + $user['avatarblobid'], $user['avatarwidth'], $user['avatarheight'], qa_opt('avatar_users_size'), true).' '.$usershtml[$user['userid']], + 'score' => qa_html(number_format($user['points'])), + ); + } + + +// Favorite tags + + if (qa_using_tags()) { + $qa_content['ranking_tags']=array( + 'title' => count($tags) ? qa_lang_html('main/nav_tags') : qa_lang_html('misc/no_favorite_tags'), + 'items' => array(), + 'rows' => ceil(count($tags)/qa_opt('columns_tags')), + 'type' => 'tags' + ); + + foreach ($tags as $tag) + $qa_content['ranking_tags']['items'][]=array( + 'label' => qa_tag_html($tag['word']), + 'count' => number_format($tag['tagcount']), + ); + } + + +// Favorite categories + + if (qa_using_categories()) { + $qa_content['nav_list_categories']=array( + 'title' => count($categories) ? qa_lang_html('main/nav_categories') : qa_lang_html('misc/no_favorite_categories'), + 'nav' => array(), + 'type' => 'nav-cat', + ); + + foreach ($categories as $category) + $qa_content['nav_list_categories']['nav'][$category['categoryid']]=array( + 'label' => qa_html($category['title']), + 'state' => 'open', + 'note' => ' - '. + ( ($category['qcount']==1) + ? qa_lang_html_sub('main/1_question', '1', '1') + : qa_lang_html_sub('main/x_questions', number_format($category['qcount'])) + ).''. + (strlen($category['content']) ? qa_html(' - '.$category['content']) : ''), + ); + } + + +// Sub navigation for account pages and suggestion + + $qa_content['suggest_next']=qa_lang_html_sub('misc/suggest_favorites_add', ' '); + + $qa_content['navigation']['sub']=qa_account_sub_navigation(); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-feedback.php b/qa-include/qa-page-feedback.php new file mode 100644 index 000000000..32a03432c --- /dev/null +++ b/qa-include/qa-page-feedback.php @@ -0,0 +1,177 @@ + $inmessage, + '^name' => empty($inname) ? '-' : $inname, + '^email' => empty($inemail) ? '-' : $inemail, + '^previous' => empty($inreferer) ? '-' : $inreferer, + '^url' => isset($userid) ? qa_path('user/'.qa_get_logged_in_handle(), null, qa_opt('site_url')) : '-', + '^ip' => qa_remote_ip_address(), + '^browser' => @$_SERVER['HTTP_USER_AGENT'], + ); + + if (qa_send_email(array( + 'fromemail' => qa_email_validate(@$inemail) ? $inemail : qa_opt('from_email'), + 'fromname' => $inname, + 'toemail' => qa_opt('feedback_email'), + 'toname' => qa_opt('site_title'), + 'subject' => qa_lang_sub('emails/feedback_subject', qa_opt('site_title')), + 'body' => strtr(qa_lang('emails/feedback_body'), $subs), + 'html' => false, + ))) + $feedbacksent=true; + else + $page_error=qa_lang_html('main/general_error'); + + qa_report_event('feedback', $userid, qa_get_logged_in_handle(), qa_cookie_get(), array( + 'email' => $inemail, + 'name' => $inname, + 'message' => $inmessage, + 'previous' => $inreferer, + 'browser' => @$_SERVER['HTTP_USER_AGENT'], + )); + } + } + + +// Prepare content for theme + + $qa_content=qa_content_prepare(); + + $qa_content['title']=qa_lang_html('misc/feedback_title'); + + $qa_content['error']=@$page_error; + + $qa_content['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + + 'style' => 'tall', + + 'fields' => array( + 'message' => array( + 'type' => $feedbacksent ? 'static' : '', + 'label' => qa_lang_html_sub('misc/feedback_message', qa_opt('site_title')), + 'tags' => 'NAME="message" ID="message"', + 'value' => qa_html(@$inmessage), + 'rows' => 8, + 'error' => qa_html(@$errors['message']), + ), + + 'name' => array( + 'type' => $feedbacksent ? 'static' : '', + 'label' => qa_lang_html('misc/feedback_name'), + 'tags' => 'NAME="name"', + 'value' => qa_html(isset($inname) ? $inname : @$userprofile['name']), + ), + + 'email' => array( + 'type' => $feedbacksent ? 'static' : '', + 'label' => qa_lang_html('misc/feedback_email'), + 'tags' => 'NAME="email"', + 'value' => qa_html(isset($inemail) ? $inemail : qa_get_logged_in_email()), + 'note' => $feedbacksent ? null : qa_opt('email_privacy'), + ), + ), + + 'buttons' => array( + 'send' => array( + 'label' => qa_lang_html('main/send_button'), + ), + ), + + 'hidden' => array( + 'dofeedback' => '1', + 'referer' => qa_html(isset($inreferer) ? $inreferer : @$_SERVER['HTTP_REFERER']), + ), + ); + + if ($usecaptcha && !$feedbacksent) + qa_set_up_captcha_field($qa_content, $qa_content['form']['fields'], @$errors); + + + $qa_content['focusid']='message'; + + if ($feedbacksent) { + $qa_content['form']['ok']=qa_lang_html('misc/feedback_sent'); + unset($qa_content['form']['buttons']); + } + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-forgot.php b/qa-include/qa-page-forgot.php new file mode 100644 index 000000000..88d0b0a5c --- /dev/null +++ b/qa-include/qa-page-forgot.php @@ -0,0 +1,119 @@ + $inemailhandle)); // redirect to page where code is entered + } + + + } else + $inemailhandle=qa_get('e'); + + +// Prepare content for theme + + $qa_content=qa_content_prepare(); + + $qa_content['title']=qa_lang_html('users/reset_title'); + + $qa_content['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + + 'style' => 'tall', + + 'fields' => array( + 'email_handle' => array( + 'label' => qa_lang_html('users/email_handle_label'), + 'tags' => 'NAME="emailhandle" ID="emailhandle"', + 'value' => qa_html(@$inemailhandle), + 'error' => qa_html(@$errors['emailhandle']), + 'note' => qa_lang_html('users/send_reset_note'), + ), + ), + + 'buttons' => array( + 'send' => array( + 'label' => qa_lang_html('users/send_reset_button'), + ), + ), + + 'hidden' => array( + 'doforgot' => '1', + ), + ); + + if (qa_opt('captcha_on_reset_password')) + qa_set_up_captcha_field($qa_content, $qa_content['form']['fields'], @$errors); + + $qa_content['focusid']='emailhandle'; + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-hot.php b/qa-include/qa-page-hot.php new file mode 100644 index 000000000..2b8bd249f --- /dev/null +++ b/qa-include/qa-page-hot.php @@ -0,0 +1,84 @@ +=QA_USER_LEVEL_MODERATOR; + + +// Perform blocking or unblocking operations as appropriate + + if ($blockable) { + if (qa_clicked('doblock')) { + $oldblocked=qa_opt('block_ips_write'); + qa_set_option('block_ips_write', (strlen($oldblocked) ? ($oldblocked.' , ') : '').$ip); + qa_redirect(qa_request()); + } + + if (qa_clicked('dounblock')) { + require_once QA_INCLUDE_DIR.'qa-app-limits.php'; + + $blockipclauses=qa_block_ips_explode(qa_opt('block_ips_write')); + + foreach ($blockipclauses as $key => $blockipclause) + if (qa_block_ip_match($ip, $blockipclause)) + unset($blockipclauses[$key]); + + qa_set_option('block_ips_write', implode(' , ', $blockipclauses)); + qa_redirect(qa_request()); + } + + if (qa_clicked('dohideall') && !qa_user_permit_error('permit_hide_show')) { + require_once QA_INCLUDE_DIR.'qa-db-admin.php'; + require_once QA_INCLUDE_DIR.'qa-app-posts.php'; + + $postids=qa_db_get_ip_visible_postids($ip); + + foreach ($postids as $postid) + qa_post_set_hidden($postid, true, $userid); + + qa_redirect(qa_request()); + } + } + + +// Combine sets of questions and get information for users + + $questions=qa_any_sort_by_date(array_merge($qs, $qs_queued, $qs_hidden, $a_qs, $a_queued_qs, $a_hidden_qs, $c_qs, $c_queued_qs, $c_hidden_qs, $edit_qs)); + + $usershtml=qa_userids_handles_html(qa_any_get_userids_handles($questions)); + + $hostname=gethostbyaddr($ip); + + +// Prepare content for theme + + $qa_content=qa_content_prepare(); + + $qa_content['title']=qa_lang_html_sub('main/ip_address_x', qa_html($ip)); + + $qa_content['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + + 'style' => 'wide', + + 'fields' => array( + 'host' => array( + 'type' => 'static', + 'label' => qa_lang_html('misc/host_name'), + 'value' => qa_html($hostname), + ), + ), + ); + + + if ($blockable) { + require_once QA_INCLUDE_DIR.'qa-app-limits.php'; + + $blockipclauses=qa_block_ips_explode(qa_opt('block_ips_write')); + $matchclauses=array(); + + foreach ($blockipclauses as $blockipclause) + if (qa_block_ip_match($ip, $blockipclause)) + $matchclauses[]=$blockipclause; + + if (count($matchclauses)) { + $qa_content['form']['fields']['status']=array( + 'type' => 'static', + 'label' => qa_lang_html('misc/matches_blocked_ips'), + 'value' => qa_html(implode("\n", $matchclauses), true), + ); + + $qa_content['form']['buttons']['unblock']=array( + 'tags' => 'NAME="dounblock"', + 'label' => qa_lang_html('misc/unblock_ip_button'), + ); + + if (count($questions) && !qa_user_permit_error('permit_hide_show')) + $qa_content['form']['buttons']['hideall']=array( + 'tags' => 'NAME="dohideall"', + 'label' => qa_lang_html('misc/hide_all_ip_button'), + ); + + } else + $qa_content['form']['buttons']['block']=array( + 'tags' => 'NAME="doblock"', + 'label' => qa_lang_html('misc/block_ip_button'), + ); + } + + + $qa_content['q_list']['qs']=array(); + + if (count($questions)) { + $qa_content['q_list']['title']=qa_lang_html_sub('misc/recent_activity_from_x', qa_html($ip)); + + foreach ($questions as $question) { + $htmloptions=qa_post_html_defaults('Q'); + $htmloptions['tagsview']=false; + $htmloptions['voteview']=false; + $htmloptions['ipview']=false; + $htmloptions['answersview']=false; + $htmloptions['updateview']=false; + + $htmlfields=qa_any_to_q_html_fields($question, $userid, qa_cookie_get(), $usershtml, null, $htmloptions); + + if (isset($htmlfields['what_url'])) // link directly to relevant content + $htmlfields['url']=$htmlfields['what_url']; + + $hasother=isset($question['opostid']); + + if ($question[$hasother ? 'ohidden' : 'hidden']) { + $htmlfields['what_2']=qa_lang_html('main/hidden'); + + if (@$htmloptions['whenview']) + $htmlfields['when_2']=qa_when_to_html($question[$hasother ? 'oupdated' : 'updated'], @$htmloptions['fulldatedays']); + } + + $qa_content['q_list']['qs'][]=$htmlfields; + } + + } else + $qa_content['q_list']['title']=qa_lang_html_sub('misc/no_activity_from_x', qa_html($ip)); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-login.php b/qa-include/qa-page-login.php new file mode 100644 index 000000000..8795dd6b4 --- /dev/null +++ b/qa-include/qa-page-login.php @@ -0,0 +1,174 @@ + $inemailhandle)); + + $forgothtml=''.qa_lang_html('users/forgot_link').''; + + $qa_content['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + + 'style' => 'tall', + + 'ok' => $passwordsent ? qa_lang_html('users/password_sent') : null, + + 'fields' => array( + 'email_handle' => array( + 'label' => qa_lang_html('users/email_handle_label'), + 'tags' => 'NAME="emailhandle" ID="emailhandle"', + 'value' => qa_html(@$inemailhandle), + 'error' => qa_html(@$errors['emailhandle']), + ), + + 'password' => array( + 'type' => 'password', + 'label' => qa_lang_html('users/password_label'), + 'tags' => 'NAME="password" ID="password"', + 'value' => qa_html(@$inpassword), + 'error' => empty($errors['password']) ? '' : (qa_html(@$errors['password']).' - '.$forgothtml), + 'note' => $passwordsent ? qa_lang_html('users/password_sent') : $forgothtml, + ), + + 'remember' => array( + 'type' => 'checkbox', + 'label' => qa_lang_html('users/remember_label'), + 'tags' => 'NAME="remember"', + 'value' => @$inremember ? true : false, + ), + ), + + 'buttons' => array( + 'login' => array( + 'label' => qa_lang_html('users/login_button'), + ), + ), + + 'hidden' => array( + 'dologin' => '1', + ), + ); + + $loginmodules=qa_load_modules_with('login', 'login_html'); + + foreach ($loginmodules as $module) { + ob_start(); + $module->login_html(qa_opt('site_url').qa_get('to'), 'login'); + $html=ob_get_clean(); + + if (strlen($html)) + @$qa_content['custom'].='
    '.$html.'
    '; + } + + $qa_content['focusid']=(isset($inemailhandle) && !isset($errors['emailhandle'])) ? 'password' : 'emailhandle'; + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-logout.php b/qa-include/qa-page-logout.php new file mode 100644 index 000000000..d2537f208 --- /dev/null +++ b/qa-include/qa-page-logout.php @@ -0,0 +1,44 @@ + $fromhandle, + '^url' => qa_path($canreply ? ('message/'.$fromhandle) : ('user/'.$fromhandle), null, qa_opt('site_url')), + )); + + $subs=array( + '^message' => $inmessage, + '^f_handle' => $fromhandle, + '^f_url' => qa_path('user/'.$fromhandle, null, qa_opt('site_url')), + '^more' => $more, + '^a_url' => qa_path_html('account', null, qa_opt('site_url')), + ); + + if (qa_send_notification($toaccount['userid'], $toaccount['email'], $toaccount['handle'], + qa_lang('emails/private_message_subject'), qa_lang('emails/private_message_body'), $subs)) + $messagesent=true; + else + $page_error=qa_lang_html('main/general_error'); + + qa_report_event('u_message', $loginuserid, qa_get_logged_in_handle(), qa_cookie_get(), array( + 'userid' => $toaccount['userid'], + 'handle' => $toaccount['handle'], + 'message' => $inmessage, + )); + } + } + + +// Prepare content for theme + + $qa_content=qa_content_prepare(); + + $qa_content['title']=qa_lang_html('misc/private_message_title'); + + $qa_content['error']=@$page_error; + + $qa_content['form_message']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + + 'style' => 'tall', + + 'fields' => array( + 'message' => array( + 'type' => $messagesent ? 'static' : '', + 'label' => qa_lang_html_sub('misc/message_for_x', qa_get_one_user_html($handle, false)), + 'tags' => 'NAME="message" ID="message"', + 'value' => qa_html(@$inmessage, $messagesent), + 'rows' => 8, + 'note' => qa_lang_html_sub('misc/message_explanation', qa_html(qa_opt('site_title'))), + 'error' => qa_html(@$errors['message']), + ), + ), + + 'buttons' => array( + 'send' => array( + 'label' => qa_lang_html('main/send_button'), + ), + ), + + 'hidden' => array( + 'domessage' => '1', + ), + ); + + $qa_content['focusid']='message'; + + if ($messagesent) { + $qa_content['form_message']['ok']=qa_lang_html('misc/message_sent'); + unset($qa_content['form_message']['fields']['message']['note']); + unset($qa_content['form_message']['buttons']); + } + + +// If relevant, show recent message history + + if (qa_opt('show_message_history')) { + $recent=array_merge($torecent, $fromrecent); + + qa_sort_by($recent, 'created'); + + $showmessages=array_slice(array_reverse($recent, true), 0, QA_DB_RETRIEVE_MESSAGES); + + if (count($showmessages)) { + $qa_content['form_recent']=array( + 'title' => qa_lang_html_sub('misc/message_recent_history', qa_html($toaccount['handle'])), + 'style' => 'tall', + 'fields' => array(), + ); + + foreach ($showmessages as $message) { + $qa_content['form_recent']['fields'][]=array( + 'label' => qa_lang_html_sub( + ($message['touserid']==$toaccount['userid']) ? 'misc/message_sent_x_ago' : 'misc/message_received_x_ago', + qa_html(qa_time_to_string(qa_opt('db_time')-$message['created']))), + 'type' => 'static', + 'value' => qa_viewer_html($message['content'], $message['format']), + ); + } + } + } + + + $qa_content['raw']['account']=$toaccount; // for plugin layers to access + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-not-found.php b/qa-include/qa-page-not-found.php new file mode 100644 index 000000000..0d4948de0 --- /dev/null +++ b/qa-include/qa-page-not-found.php @@ -0,0 +1,49 @@ + $answer) { + $prefix='a'.$answerid.'_'; + + if (qa_page_q_single_click_a($answer, $question, $answers, $commentsfollows, true, $pageerror)) + qa_page_q_refresh($pagestart, null, 'A', $answerid); + + if ($answer['editbutton']) { + if (qa_clicked($prefix.'doedit')) + qa_page_q_refresh($pagestart, 'edit-'.$answerid); + + elseif (qa_clicked($prefix.'dosave') && qa_page_q_permit_edit($answer, 'permit_edit_a', $pageerror)) { + $editedtype=qa_page_q_edit_a_submit($answer, $question, $answers, $commentsfollows, $aeditin[$answerid], $aediterrors[$answerid]); + + if (isset($editedtype)) + qa_page_q_refresh($pagestart, null, $editedtype, $answerid); + + else { + $formtype='a_edit'; + $formpostid=$answerid; // keep editing if an error + } + + } elseif (($pagestate==('edit-'.$answerid)) && qa_page_q_permit_edit($answer, 'permit_edit_a', $pageerror)) { + $formtype='a_edit'; + $formpostid=$answerid; + } + } + + if ($answer['commentbutton']) { + if (qa_clicked($prefix.'docomment')) + qa_page_q_refresh($pagestart, 'comment-'.$answerid, 'A', $answerid); + + if (qa_clicked('c'.$answerid.'_doadd') || ($pagestate==('comment-'.$answerid))) + qa_page_q_do_comment($question, $answer, $commentsfollows, $pagestart, $usecaptcha, $cnewin, $cnewerrors, $formtype, $formpostid, $pageerror); + } + + if (qa_clicked($prefix.'dofollow')) { + $params=array('follow' => $answerid); + if (isset($question['categoryid'])) + $params['cat']=$question['categoryid']; + + qa_redirect('ask', $params); + } + } + + +// Process hide, show, delete, flag, unflag, edit or save button for comments + + foreach ($commentsfollows as $commentid => $comment) + if ($comment['basetype']=='C') { + $commentparent=@$answers[$comment['parentid']]; + if (!isset($commentparent)) + $commentparent=$question; + + $commentparenttype=$commentparent['basetype']; + + $prefix='c'.$commentid.'_'; + + if (qa_page_q_single_click_c($comment, $question, $commentparent, $pageerror)) + qa_page_q_refresh($pagestart, 'showcomments-'.$comment['parentid'], $commentparenttype, $comment['parentid']); + + if ($comment['editbutton']) { + if (qa_clicked($prefix.'doedit')) { + if (qa_page_q_permit_edit($comment, 'permit_edit_c', $pageerror)) // extra check here ensures error message is visible + qa_page_q_refresh($pagestart, 'edit-'.$commentid, $commentparenttype, $comment['parentid']); + + } elseif (qa_clicked($prefix.'dosave') && qa_page_q_permit_edit($comment, 'permit_edit_c', $pageerror)) { + + if (qa_page_q_edit_c_submit($comment, $question, $commentparent, $ceditin[$commentid], $cediterrors[$commentid])) + qa_page_q_refresh($pagestart, null, $commentparenttype, $comment['parentid']); + + else { + $formtype='c_edit'; + $formpostid=$commentid; // keep editing if an error + } + + } elseif (($pagestate==('edit-'.$commentid)) && qa_page_q_permit_edit($comment, 'permit_edit_c', $pageerror)) { + $formtype='c_edit'; + $formpostid=$commentid; + } + } + } + + +// Functions used above - also see functions in qa-page-question-submit.php (which are shared with Ajax) + + function qa_page_q_refresh($start=0, $state=null, $showtype=null, $showid=null) +/* + Redirects back to the question page, with the specified parameters +*/ + { + $params=array(); + + if ($start>0) + $params['start']=$start; + if (isset($state)) + $params['state']=$state; + + if (isset($showtype) && isset($showid)) { + $anchor=qa_anchor($showtype, $showid); + $params['show']=$showid; + } else + $anchor=null; + + qa_redirect(qa_request(), $params, null, null, $anchor); + } + + + function qa_page_q_permit_edit($post, $permitoption, &$error, $permitoption2=null) +/* + Returns whether the editing operation (as specified by $permitoption or $permitoption2) on $post is permitted. + If not, sets the $error variable appropriately +*/ + { + $permiterror=qa_user_permit_error($post['isbyuser'] ? null : $permitoption); + // if it's by the user, this will only check whether they are blocked + + if ($permiterror && isset($permitoption2)) { + $permiterror2=qa_user_permit_error($post['isbyuser'] ? null : $permitoption2); + + if ( ($permiterror=='level') || (!$permiterror2) ) // if it's a less strict error + $permiterror=$permiterror2; + } + + switch ($permiterror) { + case 'login': + $error=qa_insert_login_links(qa_lang_html('question/edit_must_login'), qa_request()); + break; + + case 'confirm': + $error=qa_insert_login_links(qa_lang_html('question/edit_must_confirm'), qa_request()); + break; + + default: + $error=qa_lang_html('users/no_permission'); + break; + + case false: + break; + } + + return !$permiterror; + } + + + function qa_page_q_edit_q_form(&$qa_content, $question, $in, $errors, $completetags, $categories) +/* + Returns a $qa_content form for editing the question and sets up other parts of $qa_content accordingly +*/ + { + $form=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + + 'style' => 'tall', + + 'fields' => array( + 'title' => array( + 'type' => $question['editable'] ? 'text' : 'static', + 'label' => qa_lang_html('question/q_title_label'), + 'tags' => 'NAME="q_title"', + 'value' => qa_html(($question['editable'] && isset($in['title'])) ? $in['title'] : $question['title']), + 'error' => qa_html(@$errors['title']), + ), + + 'category' => array( + 'label' => qa_lang_html('question/q_category_label'), + 'error' => qa_html(@$errors['categoryid']), + ), + + 'content' => array( + 'label' => qa_lang_html('question/q_content_label'), + 'error' => qa_html(@$errors['content']), + ), + + 'extra' => array( + 'label' => qa_html(qa_opt('extra_field_prompt')), + 'tags' => 'NAME="q_extra"', + 'value' => qa_html(isset($in['extra']) ? $in['extra'] : $question['extra']), + 'error' => qa_html(@$errors['extra']), + ), + + 'tags' => array( + 'error' => qa_html(@$errors['tags']), + ), + + ), + + 'buttons' => array( + 'save' => array( + 'label' => qa_lang_html('main/save_button'), + ), + + 'cancel' => array( + 'tags' => 'NAME="docancel"', + 'label' => qa_lang_html('main/cancel_button'), + ), + ), + + 'hidden' => array( + 'q_dosave' => '1', + ), + ); + + if ($question['editable']) { + $content=isset($in['content']) ? $in['content'] : $question['content']; + $format=isset($in['format']) ? $in['format'] : $question['format']; + + $editorname=isset($in['editor']) ? $in['editor'] : qa_opt('editor_for_qs'); + $editor=qa_load_editor($content, $format, $editorname); + + $form['fields']['content']=array_merge($form['fields']['content'], + qa_editor_load_field($editor, $qa_content, $content, $format, 'q_content', 12, true)); + + $form['hidden']['q_editor']=qa_html($editorname); + + } else + unset($form['fields']['content']); + + if (qa_using_categories() && count($categories) && $question['retagcatable']) + qa_set_up_category_field($qa_content, $form['fields']['category'], 'q_category', $categories, + isset($in['categoryid']) ? $in['categoryid'] : $question['categoryid'], + qa_opt('allow_no_category') || !isset($question['categoryid']), qa_opt('allow_no_sub_category')); + else + unset($form['fields']['category']); + + if (!($question['editable'] && qa_opt('extra_field_active'))) + unset($form['fields']['extra']); + + if (qa_using_tags() && $question['retagcatable']) + qa_set_up_tag_field($qa_content, $form['fields']['tags'], 'q_tags', isset($in['tags']) ? $in['tags'] : qa_tagstring_to_tags($question['tags']), + array(), $completetags, qa_opt('page_size_ask_tags')); + else + unset($form['fields']['tags']); + + if ($question['isbyuser']) + qa_set_up_notify_fields($qa_content, $form['fields'], 'Q', qa_get_logged_in_email(), + isset($in['notify']) ? $in['notify'] : !empty($question['notify']), + isset($in['email']) ? $in['email'] : @$question['notify'], @$errors['email'], 'q_'); + + return $form; + } + + + function qa_page_q_edit_q_submit($question, $answers, $commentsfollows, $closepost, &$in, &$errors) +/* + Processes a POSTed form for editing the question and returns true if successful +*/ + { + $in=array(); + + if ($question['editable']) { + $in['title']=qa_post_text('q_title'); + qa_get_post_content('q_editor', 'q_content', $in['editor'], $in['content'], $in['format'], $in['text']); + $in['extra']=qa_opt('extra_field_active') ? qa_post_text('q_extra') : null; + } + + if ($question['retagcatable']) { + if (qa_using_tags()) + $in['tags']=qa_get_tags_field_value('q_tags'); + + if (qa_using_categories()) + $in['categoryid']=qa_get_category_field_value('q_category'); + } + + if ($question['isbyuser']) { + $in['notify']=qa_post_text('q_notify') ? true : false; + $in['email']=qa_post_text('q_email'); + } + + // here the $in array only contains values for parts of the form that were displayed, so those are only ones checked by filters + + $errors=array(); + + $filtermodules=qa_load_modules_with('filter', 'filter_question'); + foreach ($filtermodules as $filtermodule) + $filtermodule->filter_question($in, $errors, $question); + + if (empty($errors)) { + $userid=qa_get_logged_in_userid(); + $handle=qa_get_logged_in_handle(); + $cookieid=qa_cookie_get(); + + // now we fill in the missing values in the $in array, so that we have everything we need for qa_question_set_content() + // we do things in this way to avoid any risk of a validation failure on elements the user can't see (e.g. due to admin setting changes) + + if (!$question['editable']) { + $in['title']=$question['title']; + $in['content']=$question['content']; + $in['format']=$question['format']; + $in['text']=qa_viewer_text($in['content'], $in['format']); + $in['extra']=$question['extra']; + } + + if (!isset($in['tags'])) + $in['tags']=qa_tagstring_to_tags($question['tags']); + + if (!isset($in['categoryid'])) + $in['categoryid']=$question['categoryid']; + + if (qa_using_categories() && strcmp($in['categoryid'], $question['categoryid'])) + qa_question_set_category($question, $in['categoryid'], $userid, $handle, $cookieid, $answers, $commentsfollows, $closepost); + + $setnotify=$question['isbyuser'] ? qa_combine_notify_email($question['userid'], $in['notify'], $in['email']) : $question['notify']; + + qa_question_set_content($question, $in['title'], $in['content'], $in['format'], $in['text'], + qa_tags_to_tagstring($in['tags']), $setnotify, $userid, $handle, $cookieid, $in['extra']); + + return true; + } + + return false; + } + + + function qa_page_q_close_q_form(&$qa_content, $question, $id, $in, $errors) +/* + Returns a $qa_content form for closing the question and sets up other parts of $qa_content accordingly +*/ + { + $form=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + + 'id' => $id, + + 'style' => 'tall', + + 'title' => qa_lang_html('question/close_form_title'), + + 'fields' => array( + 'duplicate' => array( + 'type' => 'checkbox', + 'tags' => 'NAME="q_close_duplicate" ID="q_close_duplicate" onchange="document.getElementById(\'q_close_details\').focus();"', + 'label' => qa_lang_html('question/close_duplicate'), + 'value' => @$in['duplicate'], + ), + + 'details' => array( + 'tags' => 'NAME="q_close_details" ID="q_close_details"', + 'label' => + ''.qa_lang_html('question/close_original_title').' '. + ''.qa_lang_html('question/close_reason_title').'', + 'note' => '', + 'value' => @$in['details'], + 'error' => qa_html(@$errors['details']), + ), + ), + + 'buttons' => array( + 'close' => array( + 'label' => qa_lang_html('question/close_form_button'), + ), + + 'cancel' => array( + 'tags' => 'NAME="docancel"', + 'label' => qa_lang_html('main/cancel_button'), + ), + ), + + 'hidden' => array( + 'doclose' => '1', + ), + ); + + qa_set_display_rules($qa_content, array( + 'close_label_duplicate' => 'q_close_duplicate', + 'close_label_other' => '!q_close_duplicate', + 'close_note_duplicate' => 'q_close_duplicate', + )); + + $qa_content['focusid']='q_close_details'; + + return $form; + } + + + function qa_page_q_close_q_submit($question, $closepost, &$in, &$errors) +/* + Processes a POSTed form for closing the question and returns true if successful +*/ + { + $in=array( + 'duplicate' => qa_post_text('q_close_duplicate'), + 'details' => qa_post_text('q_close_details'), + ); + + $userid=qa_get_logged_in_userid(); + $handle=qa_get_logged_in_handle(); + $cookieid=qa_cookie_get(); + + if ($in['duplicate']) { + // be liberal in what we accept, but there are two potential unlikely pitfalls here: + // a) URLs could have a fixed numerical path, e.g. http://qa.mysite.com/1/478/... + // b) There could be a question title which is just a number, e.g. http://qa.mysite.com/478/12345/... + // so we check if more than one question could match, and if so, show an error + + $parts=preg_split('|[=/&]|', $in['details'], -1, PREG_SPLIT_NO_EMPTY); + $keypostids=array(); + + foreach ($parts as $part) + if (preg_match('/^[0-9]+$/', $part)) + $keypostids[$part]=true; + + $questionids=qa_db_posts_filter_q_postids(array_keys($keypostids)); + + if ( (count($questionids)==1) && ($questionids[0]!=$question['postid']) ) { + qa_question_close_duplicate($question, $closepost, $questionids[0], $userid, $handle, $cookieid); + return true; + + } else + $errors['details']=qa_lang('question/close_duplicate_error'); + + } else { + if (strlen($in['details'])>0) { + qa_question_close_other($question, $closepost, $in['details'], $userid, $handle, $cookieid); + return true; + + } else + $errors['details']=qa_lang('main/field_required'); + } + + return false; + } + + + function qa_page_q_edit_a_form(&$qa_content, $id, $answer, $question, $answers, $commentsfollows, $in, $errors) +/* + Returns a $qa_content form for editing an answer and sets up other parts of $qa_content accordingly +*/ + { + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + $answerid=$answer['postid']; + $prefix='a'.$answerid.'_'; + + $content=isset($in['content']) ? $in['content'] : $answer['content']; + $format=isset($in['format']) ? $in['format'] : $answer['format']; + + $editorname=isset($in['editor']) ? $in['editor'] : qa_opt('editor_for_as'); + $editor=qa_load_editor($content, $format, $editorname); + + $hascomments=false; + foreach ($commentsfollows as $commentfollow) + if ($commentfollow['parentid']==$answerid) + $hascomments=true; + + $form=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + + 'id' => $id, + + 'title' => qa_lang_html('question/edit_a_title'), + + 'style' => 'tall', + + 'fields' => array( + 'content' => array_merge( + qa_editor_load_field($editor, $qa_content, $content, $format, $prefix.'content', 12), + array( + 'error' => qa_html(@$errors['content']), + ) + ), + ), + + 'buttons' => array( + 'save' => array( + 'label' => qa_lang_html('main/save_button'), + ), + + 'cancel' => array( + 'tags' => 'NAME="docancel"', + 'label' => qa_lang_html('main/cancel_button'), + ), + ), + + 'hidden' => array( + $prefix.'editor' => qa_html($editorname), + $prefix.'dosave' => '1', + ), + ); + + // Show option to convert this answer to a comment, if appropriate + + $commentonoptions=array(); + + $lastbeforeid=$question['postid']; // used to find last post created before this answer - this is default given + $lastbeforetime=$question['created']; + + if ($question['commentable']) + $commentonoptions[$question['postid']]= + qa_lang_html('question/comment_on_q').qa_html(qa_shorten_string_line($question['title'], 80)); + + foreach ($answers as $otheranswer) + if (($otheranswer['postid']!=$answerid) && ($otheranswer['created']<$answer['created']) && $otheranswer['commentable'] && !$otheranswer['hidden']) { + $commentonoptions[$otheranswer['postid']]= + qa_lang_html('question/comment_on_a').qa_html(qa_shorten_string_line(qa_viewer_text($otheranswer['content'], $otheranswer['format']), 80)); + + if ($otheranswer['created']>$lastbeforetime) { + $lastbeforeid=$otheranswer['postid']; + $lastebeforetime=$otheranswer['created']; + } + } + + if (count($commentonoptions)) { + $form['fields']['tocomment']=array( + 'tags' => 'NAME="'.$prefix.'dotoc" ID="'.$prefix.'dotoc"', + 'label' => ''.qa_lang_html('question/a_convert_to_c_on').''. + '', + 'type' => 'checkbox', + 'tight' => true, + ); + + $form['fields']['commenton']=array( + 'tags' => 'NAME="'.$prefix.'commenton"', + 'id' => $prefix.'commenton', + 'type' => 'select', + 'note' => qa_lang_html($hascomments ? 'question/a_convert_warn_cs' : 'question/a_convert_warn'), + 'options' => $commentonoptions, + 'value' => @$commentonoptions[$lastbeforeid], + ); + + qa_set_display_rules($qa_content, array( + $prefix.'commenton' => $prefix.'dotoc', + $prefix.'toshown' => $prefix.'dotoc', + $prefix.'tohidden' => '!'.$prefix.'dotoc', + )); + } + + // Show notification field if appropriate + + if ($answer['isbyuser']) + qa_set_up_notify_fields($qa_content, $form['fields'], 'A', qa_get_logged_in_email(), + isset($in['notify']) ? $in['notify'] : !empty($answer['notify']), + isset($in['email']) ? $in['email'] : @$answer['notify'], @$errors['email'], $prefix); + + return $form; + } + + + function qa_page_q_edit_a_submit($answer, $question, $answers, $commentsfollows, &$in, &$errors) +/* + Processes a POSTed form for editing an answer and returns the new type of the post if successful +*/ + { + $answerid=$answer['postid']; + $prefix='a'.$answerid.'_'; + + $in=array( + 'dotoc' => qa_post_text($prefix.'dotoc'), + 'commenton' => qa_post_text($prefix.'commenton'), + ); + + if ($answer['isbyuser']) { + $in['notify']=qa_post_text($prefix.'notify') ? true : false; + $in['email']=qa_post_text($prefix.'email'); + } + + qa_get_post_content($prefix.'editor', $prefix.'content', $in['editor'], $in['content'], $in['format'], $in['text']); + + $errors=array(); + + $filtermodules=qa_load_modules_with('filter', 'filter_answer'); + foreach ($filtermodules as $filtermodule) + $filtermodule->filter_answer($in, $errors, $question, $answer); + + if (empty($errors)) { + $userid=qa_get_logged_in_userid(); + $handle=qa_get_logged_in_handle(); + $cookieid=qa_cookie_get(); + + $setnotify=$answer['isbyuser'] ? qa_combine_notify_email($answer['userid'], $in['notify'], $in['email']) : $answer['notify']; + + if ($in['dotoc'] && ( + (($in['commenton']==$question['postid']) && $question['commentable']) || + (($in['commenton']!=$answerid) && @$answers[$in['commenton']]['commentable']) + )) { // convert to a comment + + if (qa_limits_remaining($userid, QA_LIMIT_COMMENTS)) { // already checked 'permit_post_c' + qa_answer_to_comment($answer, $in['commenton'], $in['content'], $in['format'], $in['text'], $setnotify, + $userid, $handle, $cookieid, $question, $answers, $commentsfollows); + + return 'C'; // to signify that redirect should be to the comment + + } else + $errors['content']=qa_lang_html('question/comment_limit'); // not really best place for error, but it will do + + } else { + qa_answer_set_content($answer, $in['content'], $in['format'], $in['text'], $setnotify, $userid, $handle, $cookieid, $question); + + return 'A'; + } + } + + return null; + } + + + function qa_page_q_do_comment($question, $parent, $commentsfollows, $pagestart, $usecaptcha, &$cnewin, &$cnewerrors, &$formtype, &$formpostid, &$error) +/* + Processes a request to add a comment to $parent, with antecedent $question, checking for permissions errors +*/ + { + $answer=($question['postid']==$parent['postid']) ? null : $parent; + $parentid=$parent['postid']; + + switch (qa_user_permit_error('permit_post_c', QA_LIMIT_COMMENTS)) { + case 'login': + $error=qa_insert_login_links(qa_lang_html('question/comment_must_login'), qa_request()); + break; + + case 'confirm': + $error=qa_insert_login_links(qa_lang_html('question/comment_must_confirm'), qa_request()); + break; + + case 'limit': + $error=qa_lang_html('question/comment_limit'); + break; + + default: + $error=qa_lang_html('users/no_permission'); + break; + + case false: + if (qa_clicked('c'.$parentid.'_doadd')) { + $commentid=qa_page_q_add_c_submit($question, $parent, $commentsfollows, $usecaptcha, $cnewin[$parentid], $cnewerrors[$parentid]); + + if (isset($commentid)) + qa_page_q_refresh($pagestart, null, $parent['basetype'], $parentid); + + else { + $formtype='c_add'; + $formpostid=$parentid; // show form again + } + + } else { + $formtype='c_add'; + $formpostid=$parentid; // show form first time + } + break; + } + } + + + function qa_page_q_edit_c_form(&$qa_content, $id, $comment, $in, $errors) +/* + Returns a $qa_content form for editing a comment and sets up other parts of $qa_content accordingly +*/ + { + $commentid=$comment['postid']; + $prefix='c'.$commentid.'_'; + + $content=isset($in['content']) ? $in['content'] : $comment['content']; + $format=isset($in['format']) ? $in['format'] : $comment['format']; + + $editorname=isset($in['editor']) ? $in['editor'] : qa_opt('editor_for_cs'); + $editor=qa_load_editor($content, $format, $editorname); + + $form=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + + 'id' => $id, + + 'title' => qa_lang_html('question/edit_c_title'), + + 'style' => 'tall', + + 'fields' => array( + 'content' => array_merge( + qa_editor_load_field($editor, $qa_content, $content, $format, $prefix.'content', 4, true), + array( + 'error' => qa_html(@$errors['content']), + ) + ), + ), + + 'buttons' => array( + 'save' => array( + 'label' => qa_lang_html('main/save_button'), + ), + + 'cancel' => array( + 'tags' => 'NAME="docancel"', + 'label' => qa_lang_html('main/cancel_button'), + ), + ), + + 'hidden' => array( + $prefix.'editor' => qa_html($editorname), + $prefix.'dosave' => '1', + ), + ); + + if ($comment['isbyuser']) + qa_set_up_notify_fields($qa_content, $form['fields'], 'C', qa_get_logged_in_email(), + isset($in['notify']) ? $in['notify'] : !empty($comment['notify']), + isset($in['email']) ? $in['email'] : @$comment['notify'], @$errors['email'], $prefix); + + return $form; + } + + + function qa_page_q_edit_c_submit($comment, $question, $parent, &$in, &$errors) +/* + Processes a POSTed form for editing a comment and returns true if successful +*/ + { + $commentid=$comment['postid']; + $prefix='c'.$commentid.'_'; + + $in=array(); + + if ($comment['isbyuser']) { + $in['notify']=qa_post_text($prefix.'notify') ? true : false; + $in['email']=qa_post_text($prefix.'email'); + } + + qa_get_post_content($prefix.'editor', $prefix.'content', $in['editor'], $in['content'], $in['format'], $in['text']); + + $errors=array(); + + $filtermodules=qa_load_modules_with('filter', 'filter_comment'); + foreach ($filtermodules as $filtermodule) + $filtermodule->filter_comment($in, $errors, $question, $parent, $comment); + + if (empty($errors)) { + $userid=qa_get_logged_in_userid(); + $handle=qa_get_logged_in_handle(); + $cookieid=qa_cookie_get(); + + qa_comment_set_content($comment, $in['content'], $in['format'], $in['text'], + $comment['isbyuser'] ? qa_combine_notify_email($comment['userid'], $in['notify'], $in['email']) : $comment['notify'], + $userid, $handle, $cookieid, $question, $parent); + + return true; + } + + return false; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-question-submit.php b/qa-include/qa-page-question-submit.php new file mode 100644 index 000000000..11b3fef78 --- /dev/null +++ b/qa-include/qa-page-question-submit.php @@ -0,0 +1,361 @@ + qa_post_text('a_notify') ? true : false, + 'email' => qa_post_text('a_email'), + 'queued' => qa_user_moderation_reason() ? true : false, + ); + + qa_get_post_content('a_editor', 'a_content', $in['editor'], $in['content'], $in['format'], $in['text']); + + $errors=array(); + + $filtermodules=qa_load_modules_with('filter', 'filter_answer'); + foreach ($filtermodules as $filtermodule) + $filtermodule->filter_answer($in, $errors, $question, null); + + if ($usecaptcha) + qa_captcha_validate_post($errors); + + if (empty($errors)) { + $testwords=implode(' ', qa_string_to_words($in['content'])); + + foreach ($answers as $answer) + if (!$answer['hidden']) + if (implode(' ', qa_string_to_words($answer['content'])) == $testwords) + $errors['content']=qa_lang_html('question/duplicate_content'); + } + + if (empty($errors)) { + $userid=qa_get_logged_in_userid(); + $handle=qa_get_logged_in_handle(); + $cookieid=isset($userid) ? qa_cookie_get() : qa_cookie_get_create(); // create a new cookie if necessary + + $answerid=qa_answer_create($userid, $handle, $cookieid, $in['content'], $in['format'], $in['text'], $in['notify'], $in['email'], + $question, $in['queued']); + + return $answerid; + } + + return null; + } + + + function qa_page_q_add_c_submit($question, $parent, $commentsfollows, $usecaptcha, &$in, &$errors) +/* + Processes a POSTed form to add a comment, returning the postid if successful, otherwise null. Pass in the antecedent + $question and the comment's $parent post. Set $usecaptcha to whether a captcha is required. Pass an array which + includes the other comments with the same parent in $commentsfollows (it can contain other posts which are ignored). + The form fields submitted will be passed out as an array in $in, as well as any $errors on those fields. +*/ + { + $parentid=$parent['postid']; + + $prefix='c'.$parentid.'_'; + + $in=array( + 'notify' => qa_post_text($prefix.'notify') ? true : false, + 'email' => qa_post_text($prefix.'email'), + 'queued' => qa_user_moderation_reason() ? true : false, + ); + + qa_get_post_content($prefix.'editor', $prefix.'content', $in['editor'], $in['content'], $in['format'], $in['text']); + + $errors=array(); + + $filtermodules=qa_load_modules_with('filter', 'filter_comment'); + foreach ($filtermodules as $filtermodule) + $filtermodule->filter_comment($in, $errors, $question, $parent, null); + + if ($usecaptcha) + qa_captcha_validate_post($errors); + + if (empty($errors)) { + $testwords=implode(' ', qa_string_to_words($in['content'])); + + foreach ($commentsfollows as $comment) + if (($comment['basetype']=='C') && ($comment['parentid']==$parentid) && !$comment['hidden']) + if (implode(' ', qa_string_to_words($comment['content'])) == $testwords) + $errors['content']=qa_lang_html('question/duplicate_content'); + } + + if (empty($errors)) { + $userid=qa_get_logged_in_userid(); + $handle=qa_get_logged_in_handle(); + $cookieid=isset($userid) ? qa_cookie_get() : qa_cookie_get_create(); // create a new cookie if necessary + + $commentid=qa_comment_create($userid, $handle, $cookieid, $in['content'], $in['format'], $in['text'], $in['notify'], $in['email'], + $question, $parent, $commentsfollows, $in['queued']); + + return $commentid; + } + + return null; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-question-view.php b/qa-include/qa-page-question-view.php new file mode 100644 index 000000000..e08aad43c --- /dev/null +++ b/qa-include/qa-page-question-view.php @@ -0,0 +1,965 @@ + $post) + switch ($post['type']) { + case 'A': + case 'A_HIDDEN': + case 'A_QUEUED': + $answers[$postid]=$post; + break; + } + + return $answers; + } + + + function qa_page_q_load_c_follows($question, $childposts, $achildposts) +/* + Given a $question, its $childposts and its answers $achildposts from the database, + return a list of comments or follow-on questions for that question or its answers +*/ + { + $commentsfollows=array(); + + foreach ($childposts as $postid => $post) + switch ($post['type']) { + case 'Q': // never show follow-on Qs which have been hidden, even to admins + case 'C': + case 'C_HIDDEN': + case 'C_QUEUED': + $commentsfollows[$postid]=$post; + break; + } + + foreach ($achildposts as $postid => $post) + switch ($post['type']) { + case 'Q': // never show follow-on Qs which have been hidden, even to admins + case 'C': + case 'C_HIDDEN': + case 'C_QUEUED': + $commentsfollows[$postid]=$post; + break; + } + + return $commentsfollows; + } + + + function qa_page_q_post_rules($post, $parentpost=null, $siblingposts=null, $childposts=null) +/* + Returns elements that can be added to $post which describe which operations the current user may perform on that + post. This function is a key part of Q2A's logic and is ripe for overriding by plugins. Pass $post's $parentpost if + there is one, or null otherwise. Pass an array which contains $post's siblings (i.e. other posts with the same type + and parent) in $siblingposts and $post's children in $childposts. Both of these latter arrays can contain additional + posts retrieved from the database, and these will be ignored. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $userid=qa_get_logged_in_userid(); + $cookieid=qa_cookie_get(); + + $rules['isbyuser']=qa_post_is_by_user($post, $userid, $cookieid); + $rules['queued']=(substr($post['type'], 1)=='_QUEUED'); + $rules['closed']=($post['basetype']=='Q') && (isset($post['closedbyid']) || (isset($post['selchildid']) && qa_opt('do_close_on_select'))); + + // Cache some responses to the user permission checks + + $permiterror_post_q=qa_user_permit_error('permit_post_q'); + $permiterror_post_a=qa_user_permit_error('permit_post_a'); + $permiterror_post_c=qa_user_permit_error('permit_post_c'); + + $permiterror_edit=qa_user_permit_error(($post['basetype']=='Q') ? 'permit_edit_q' : + (($post['basetype']=='A') ? 'permit_edit_a' : 'permit_edit_c')); + $permiterror_retagcat=qa_user_permit_error('permit_retag_cat'); + $permiterror_hide_show=qa_user_permit_error($rules['isbyuser'] ? null : 'permit_hide_show'); + $permiterror_close_open=qa_user_permit_error($rules['isbyuser'] ? null : 'permit_close_q'); + $permiterror_moderate=qa_user_permit_error('permit_moderate'); + + // General permissions + + $rules['authorlast']=((!isset($post['lastuserid'])) || ($post['lastuserid']===$post['userid'])); + $rules['viewable']=$post['hidden'] ? (!$permiterror_hide_show) : ($rules['queued'] ? ($rules['isbyuser'] || !$permiterror_moderate) : true); + + // Answer, comment and edit might show the button even if the user still needs to do something (e.g. log in) + + $rules['answerbutton']=($post['type']=='Q') && ($permiterror_post_a!='level') && (!$rules['closed']) && + (qa_opt('allow_self_answer') || !$rules['isbyuser']); + + $rules['commentbutton']=(($post['type']=='Q') || ($post['type']=='A')) && + ($permiterror_post_c!='level') && + qa_opt(($post['type']=='Q') ? 'comment_on_qs' : 'comment_on_as'); + $rules['commentable']=$rules['commentbutton'] && !$permiterror_post_c; + + $rules['editbutton']=(!$post['hidden']) && ($rules['isbyuser'] || (($permiterror_edit!='level') && (!$rules['queued']))) && !$rules['closed']; + $rules['editable']=$rules['editbutton'] && ($rules['isbyuser'] || !$permiterror_edit); + + $rules['retagcatbutton']=($post['basetype']=='Q') && (qa_using_tags() || qa_using_categories()) && + (!$post['hidden']) && ($rules['isbyuser'] || ($permiterror_retagcat!='level')); + $rules['retagcatable']=$rules['retagcatbutton'] && ($rules['isbyuser'] || !$permiterror_retagcat); + + if ($rules['editbutton'] && $rules['retagcatbutton']) { // only show one button since they lead to the same form + if ($rules['retagcatable'] && !$rules['editable']) + $rules['editbutton']=false; // if we can do this without getting an error, show that as the title + else + $rules['retagcatbutton']=false; + } + + $rules['aselectable']=($post['type']=='Q') && !qa_user_permit_error($rules['isbyuser'] ? null : 'permit_select_a'); + + $rules['flagbutton']=qa_opt('flagging_of_posts') && (!$rules['isbyuser']) && (!$post['hidden']) && (!$rules['queued']) && + (!@$post['userflag']) && (qa_user_permit_error('permit_flag')!='level'); + $rules['flagtohide']=$rules['flagbutton'] && (!qa_user_permit_error('permit_flag')) && (($post['flagcount']+1)>=qa_opt('flagging_hide_after')); + $rules['unflaggable']=@$post['userflag'] && (!$post['hidden']); + $rules['clearflaggable']=($post['flagcount']>=(@$post['userflag'] ? 2 : 1)) && !qa_user_permit_error('permit_hide_show'); + + // Other actions only show the button if it's immediately possible + + $notclosedbyother=!($rules['closed'] && isset($post['closedbyid']) && !$rules['authorlast']); + $nothiddenbyother=!($post['hidden'] && !$rules['authorlast']); + + $rules['closeable']=qa_opt('allow_close_questions') && ($post['type']=='Q') && (!$rules['closed']) && !$permiterror_close_open; + $rules['reopenable']=$rules['closed'] && isset($post['closedbyid']) && (!$permiterror_close_open) && (!$post['hidden']) && + ($notclosedbyother || !qa_user_permit_error('permit_close_q')); + // cannot reopen a question if it's been hidden, or if it was closed by someone else and you don't have global closing permissions + $rules['moderatable']=$rules['queued'] && !$permiterror_moderate; + $rules['hideable']=(!$post['hidden']) && ($rules['isbyuser'] || !$rules['queued']) && + (!$permiterror_hide_show) && ($notclosedbyother || !qa_user_permit_error('permit_hide_show')); + // cannot hide a question if it was closed by someone else and you don't have global hiding permissions + $rules['reshowable']=$post['hidden'] && (!$permiterror_hide_show) && (!qa_user_moderation_reason()) && + (($nothiddenbyother && !$post['flagcount']) || !qa_user_permit_error('permit_hide_show')); + // cannot reshow a question if it was hidden by someone else, or if it has flags - unless you have global hiding permissions + $rules['deleteable']=$post['hidden'] && !qa_user_permit_error('permit_delete_hidden'); + $rules['claimable']=(!isset($post['userid'])) && isset($userid) && (strcmp(@$post['cookieid'], $cookieid)==0) && + !(($post['basetype']=='Q') ? $permiterror_post_q : (($post['basetype']=='A') ? $permiterror_post_a : $permiterror_post_c)); + $rules['followable']=($post['type']=='A') ? qa_opt('follow_on_as') : false; + + // Check for claims that could break rules about self answering and mulltiple answers + + if ($rules['claimable'] && ($post['basetype']=='A')) { + if ( (!qa_opt('allow_self_answer')) && isset($parentpost) && qa_post_is_by_user($parentpost, $userid, $cookieid) ) + $rules['claimable']=false; + + if (isset($siblingposts) && !qa_opt('allow_multi_answers')) + foreach ($siblingposts as $siblingpost) + if ( ($siblingpost['parentid']==$post['parentid']) && ($siblingpost['basetype']=='A') && qa_post_is_by_user($siblingpost, $userid, $cookieid)) + $rules['claimable']=false; + } + + // Now make any changes based on the child posts + + if (isset($childposts)) + foreach ($childposts as $childpost) + if ( + ($childpost['parentid']==$post['postid']) && + ( ($childpost['basetype']=='A') || ($childpost['basetype']=='C') ) + ) { + $rules['deleteable']=false; + + if (($childpost['basetype']=='A') && qa_post_is_by_user($childpost, $userid, $cookieid)) { + if (!qa_opt('allow_multi_answers')) + $rules['answerbutton']=false; + + if (!qa_opt('allow_self_answer')) + $rules['claimable']=false; + } + } + + // Return the resulting rules + + return $rules; + } + + + function qa_page_q_question_view($question, $parentquestion, $closepost, $usershtml, $formrequested) +/* + Return the $qa_content['q_view'] element for $question as viewed by the current user. If this question is a + follow-on, pass the question for this question's parent answer in $parentquestion, otherwise null. If the question + is closed, pass the post used to close this question in $closepost, otherwise null. $usershtml should be an array + which maps userids to HTML user representations, including the question's author and (if present) last editor. If a + form has been explicitly requested for the page, set $formrequested to true - this will hide the buttons. +*/ + { + $questionid=$question['postid']; + $userid=qa_get_logged_in_userid(); + $cookieid=qa_cookie_get(); + + $htmloptions=qa_post_html_defaults('Q', true); + $htmloptions['answersview']=false; // answer count is displayed separately so don't show it here + $htmloptions['avatarsize']=qa_opt('avatar_q_page_q_size'); + $q_view=qa_post_html_fields($question, $userid, $cookieid, $usershtml, null, $htmloptions); + + + $q_view['main_form_tags']='METHOD="POST" ACTION="'.qa_self_html().'"'; + + + // Buttons for operating on the question + + if (!$formrequested) { // don't show if another form is currently being shown on page + $buttons=array(); + + if ($question['editbutton']) + $buttons['edit']=array( + 'tags' => 'NAME="q_doedit"', + 'label' => qa_lang_html('question/edit_button'), + 'popup' => qa_lang_html('question/edit_q_popup'), + ); + + $hascategories=qa_using_categories(); + + if ($question['retagcatbutton']) + $buttons['retagcat']=array( + 'tags' => 'NAME="q_doedit"', + 'label' => qa_lang_html($hascategories ? 'question/recat_button' : 'question/retag_button'), + 'popup' => qa_lang_html($hascategories + ? (qa_using_tags() ? 'question/retag_cat_popup' : 'question/recat_popup') + : 'question/retag_popup' + ), + ); + + if ($question['flagbutton']) + $buttons['flag']=array( + 'tags' => 'NAME="q_doflag"', + 'label' => qa_lang_html($question['flagtohide'] ? 'question/flag_hide_button' : 'question/flag_button'), + 'popup' => qa_lang_html('question/flag_q_popup'), + ); + + if ($question['unflaggable']) + $buttons['unflag']=array( + 'tags' => 'NAME="q_dounflag"', + 'label' => qa_lang_html('question/unflag_button'), + 'popup' => qa_lang_html('question/unflag_popup'), + ); + + if ($question['clearflaggable']) + $buttons['clearflags']=array( + 'tags' => 'NAME="q_doclearflags"', + 'label' => qa_lang_html('question/clear_flags_button'), + 'popup' => qa_lang_html('question/clear_flags_popup'), + ); + + if ($question['closeable']) + $buttons['close']=array( + 'tags' => 'NAME="q_doclose"', + 'label' => qa_lang_html('question/close_button'), + 'popup' => qa_lang_html('question/close_q_popup'), + ); + + if ($question['reopenable']) + $buttons['reopen']=array( + 'tags' => 'NAME="q_doreopen"', + 'label' => qa_lang_html('question/reopen_button'), + ); + + if ($question['moderatable']) { + $buttons['approve']=array( + 'tags' => 'NAME="q_doapprove"', + 'label' => qa_lang_html('question/approve_button'), + ); + + $buttons['reject']=array( + 'tags' => 'NAME="q_doreject"', + 'label' => qa_lang_html('question/reject_button'), + ); + } + + if ($question['hideable']) + $buttons['hide']=array( + 'tags' => 'NAME="q_dohide"', + 'label' => qa_lang_html('question/hide_button'), + 'popup' => qa_lang_html('question/hide_q_popup'), + ); + + if ($question['reshowable']) + $buttons['reshow']=array( + 'tags' => 'NAME="q_doreshow"', + 'label' => qa_lang_html('question/reshow_button'), + ); + + if ($question['deleteable']) + $buttons['delete']=array( + 'tags' => 'NAME="q_dodelete"', + 'label' => qa_lang_html('question/delete_button'), + 'popup' => qa_lang_html('question/delete_q_popup'), + ); + + if ($question['claimable']) + $buttons['claim']=array( + 'tags' => 'NAME="q_doclaim"', + 'label' => qa_lang_html('question/claim_button'), + ); + + if ($question['answerbutton']) // don't show if shown by default + $buttons['answer']=array( + 'tags' => 'NAME="q_doanswer" ID="q_doanswer" onClick="return qa_toggle_element(\'anew\')"', + 'label' => qa_lang_html('question/answer_button'), + 'popup' => qa_lang_html('question/answer_q_popup'), + ); + + if ($question['commentbutton']) + $buttons['comment']=array( + 'tags' => 'NAME="q_docomment" onClick="return qa_toggle_element(\'c'.$questionid.'\')"', + 'label' => qa_lang_html('question/comment_button'), + 'popup' => qa_lang_html('question/comment_q_popup'), + ); + + $q_view['form']=array( + 'style' => 'light', + 'buttons' => $buttons, + 'hidden' => array( + 'qa_click' => '', + ), + ); + } + + + // Information about the question of the answer that this question follows on from (or a question directly) + + if (isset($parentquestion)) + $q_view['follows']=array( + 'label' => qa_lang_html(($question['parentid']==$parentquestion['postid']) ? 'question/follows_q' : 'question/follows_a'), + 'title' => qa_html(qa_block_words_replace($parentquestion['title'], qa_get_block_words_preg())), + 'url' => qa_q_path_html($parentquestion['postid'], $parentquestion['title'], false, + ($question['parentid']==$parentquestion['postid']) ? 'Q' : 'A', $question['parentid']), + ); + + + // Information about the question that this question is a duplicate of (if appropriate) + + if (isset($closepost)) { + + if ($closepost['basetype']=='Q') { + $q_view['closed']=array( + 'label' => qa_lang_html('question/closed_as_duplicate'), + 'content' => qa_html(qa_block_words_replace($closepost['title'], qa_get_block_words_preg())), + 'url' => qa_q_path_html($closepost['postid'], $closepost['title']), + ); + + } elseif ($closepost['type']=='NOTE') { + $viewer=qa_load_viewer($closepost['content'], $closepost['format']); + + $q_view['closed']=array( + 'label' => qa_lang_html('question/closed_with_note'), + 'content' => $viewer->get_html($closepost['content'], $closepost['format'], array( + 'blockwordspreg' => qa_get_block_words_preg(), + )), + ); + } + } + + + // Extra value display + + if (strlen(@$question['extra']) && qa_opt('extra_field_active') && qa_opt('extra_field_display')) + $q_view['extra']=array( + 'label' => qa_html(qa_opt('extra_field_label')), + 'content' => qa_html(qa_block_words_replace($question['extra'], qa_get_block_words_preg())), + ); + + + return $q_view; + } + + + function qa_page_q_answer_view($question, $answer, $isselected, $usershtml, $formrequested) +/* + Returns an element to add to $qa_content['a_list']['as'] for $answer as viewed by $userid and $cookieid. Pass the + answer's $question and whether it $isselected. $usershtml should be an array which maps userids to HTML user + representations, including the answer's author and (if present) last editor. If a form has been explicitly requested + for the page, set $formrequested to true - this will hide the buttons. +*/ + { + $answerid=$answer['postid']; + $userid=qa_get_logged_in_userid(); + $cookieid=qa_cookie_get(); + + $htmloptions=qa_post_html_defaults('A', true); + $htmloptions['isselected']=$isselected; + $htmloptions['avatarsize']=qa_opt('avatar_q_page_a_size'); + $a_view=qa_post_html_fields($answer, $userid, $cookieid, $usershtml, null, $htmloptions); + + if ($answer['queued']) + $a_view['error']=$answer['isbyuser'] ? qa_lang_html('question/a_your_waiting_approval') : qa_lang_html('question/a_waiting_your_approval'); + + $a_view['main_form_tags']='METHOD="POST" ACTION="'.qa_self_html().'"'; + + + // Selection/unselect buttons and others for operating on the answer + + if (!$formrequested) { // don't show if another form is currently being shown on page + $prefix='a'.qa_html($answerid).'_'; + $clicksuffix=' onclick="return qa_answer_click('.qa_js($answerid).', '.qa_js($question['postid']).', this);"'; + + if ($question['aselectable'] && !$answer['hidden'] && !$answer['queued']) { + if ($isselected) + $a_view['unselect_tags']='TITLE="'.qa_lang_html('question/unselect_popup').'" NAME="'.$prefix.'dounselect"'.$clicksuffix; + else + $a_view['select_tags']='TITLE="'.qa_lang_html('question/select_popup').'" NAME="'.$prefix.'doselect"'.$clicksuffix; + } + + $buttons=array(); + + if ($answer['editbutton']) + $buttons['edit']=array( + 'tags' => 'NAME="'.$prefix.'doedit"', + 'label' => qa_lang_html('question/edit_button'), + 'popup' => qa_lang_html('question/edit_a_popup'), + ); + + if ($answer['flagbutton']) + $buttons['flag']=array( + 'tags' => 'NAME="'.$prefix.'doflag"'.$clicksuffix, + 'label' => qa_lang_html($answer['flagtohide'] ? 'question/flag_hide_button' : 'question/flag_button'), + 'popup' => qa_lang_html('question/flag_a_popup'), + ); + + if ($answer['unflaggable']) + $buttons['unflag']=array( + 'tags' => 'NAME="'.$prefix.'dounflag"'.$clicksuffix, + 'label' => qa_lang_html('question/unflag_button'), + 'popup' => qa_lang_html('question/unflag_popup'), + ); + + if ($answer['clearflaggable']) + $buttons['clearflags']=array( + 'tags' => 'NAME="'.$prefix.'doclearflags"'.$clicksuffix, + 'label' => qa_lang_html('question/clear_flags_button'), + 'popup' => qa_lang_html('question/clear_flags_popup'), + ); + + if ($answer['moderatable']) { + $buttons['approve']=array( + 'tags' => 'NAME="'.$prefix.'doapprove"'.$clicksuffix, + 'label' => qa_lang_html('question/approve_button'), + ); + + $buttons['reject']=array( + 'tags' => 'NAME="'.$prefix.'doreject"'.$clicksuffix, + 'label' => qa_lang_html('question/reject_button'), + ); + } + + if ($answer['hideable']) + $buttons['hide']=array( + 'tags' => 'NAME="'.$prefix.'dohide"'.$clicksuffix, + 'label' => qa_lang_html('question/hide_button'), + 'popup' => qa_lang_html('question/hide_a_popup'), + ); + + if ($answer['reshowable']) + $buttons['reshow']=array( + 'tags' => 'NAME="'.$prefix.'doreshow"'.$clicksuffix, + 'label' => qa_lang_html('question/reshow_button'), + ); + + if ($answer['deleteable']) + $buttons['delete']=array( + 'tags' => 'NAME="'.$prefix.'dodelete"'.$clicksuffix, + 'label' => qa_lang_html('question/delete_button'), + 'popup' => qa_lang_html('question/delete_a_popup'), + ); + + if ($answer['claimable']) + $buttons['claim']=array( + 'tags' => 'NAME="'.$prefix.'doclaim"'.$clicksuffix, + 'label' => qa_lang_html('question/claim_button'), + ); + + if ($answer['followable']) + $buttons['follow']=array( + 'tags' => 'NAME="'.$prefix.'dofollow"', + 'label' => qa_lang_html('question/follow_button'), + 'popup' => qa_lang_html('question/follow_a_popup'), + ); + + if ($answer['commentbutton']) + $buttons['comment']=array( + 'tags' => 'NAME="'.$prefix.'docomment" onClick="return qa_toggle_element(\'c'.$answerid.'\')"', + 'label' => qa_lang_html('question/comment_button'), + 'popup' => qa_lang_html('question/comment_a_popup'), + ); + + $a_view['form']=array( + 'style' => 'light', + 'buttons' => $buttons, + ); + } + + return $a_view; + } + + + function qa_page_q_comment_view($parent, $comment, $usershtml, $formrequested) +/* + Returns an element to add to the appropriate $qa_content[...]['c_list']['cs'] array for $comment as viewed by the + current user. Pass the comment's $parent post. $usershtml should be an array which maps userids to HTML user + representations, including the comments's author and (if present) last editor. If a form has been explicitly + requested for the page, set $formrequested to true - this will hide the buttons. +*/ + { + $commentid=$comment['postid']; + $questionid=($parent['basetype']=='Q') ? $parent['postid'] : $parent['parentid']; + $answerid=($parent['basetype']=='Q') ? null : $parent['postid']; + $userid=qa_get_logged_in_userid(); + $cookieid=qa_cookie_get(); + + $htmloptions=qa_post_html_defaults('C', true); + $htmloptions['avatarsize']=qa_opt('avatar_q_page_c_size'); + $c_view=qa_post_html_fields($comment, $userid, $cookieid, $usershtml, null, $htmloptions); + + if ($comment['queued']) + $c_view['error']=$comment['isbyuser'] ? qa_lang_html('question/c_your_waiting_approval') : qa_lang_html('question/c_waiting_your_approval'); + + + // Buttons for operating on this comment + + if (!$formrequested) { // don't show if another form is currently being shown on page + $prefix='c'.qa_html($commentid).'_'; + $clicksuffix=' onclick="return qa_comment_click('.qa_js($commentid).', '.qa_js($questionid).', '.qa_js($parent['postid']).', this);"'; + + $buttons=array(); + + if ($comment['editbutton']) + $buttons['edit']=array( + 'tags' => 'NAME="'.$prefix.'doedit"', + 'label' => qa_lang_html('question/edit_button'), + 'popup' => qa_lang_html('question/edit_c_popup'), + ); + + if ($comment['flagbutton']) + $buttons['flag']=array( + 'tags' => 'NAME="'.$prefix.'doflag"'.$clicksuffix, + 'label' => qa_lang_html($comment['flagtohide'] ? 'question/flag_hide_button' : 'question/flag_button'), + 'popup' => qa_lang_html('question/flag_c_popup'), + ); + + if ($comment['unflaggable']) + $buttons['unflag']=array( + 'tags' => 'NAME="'.$prefix.'dounflag"'.$clicksuffix, + 'label' => qa_lang_html('question/unflag_button'), + 'popup' => qa_lang_html('question/unflag_popup'), + ); + + if ($comment['clearflaggable']) + $buttons['clearflags']=array( + 'tags' => 'NAME="'.$prefix.'doclearflags"'.$clicksuffix, + 'label' => qa_lang_html('question/clear_flags_button'), + 'popup' => qa_lang_html('question/clear_flags_popup'), + ); + + if ($comment['moderatable']) { + $buttons['approve']=array( + 'tags' => 'NAME="'.$prefix.'doapprove"'.$clicksuffix, + 'label' => qa_lang_html('question/approve_button'), + ); + + $buttons['reject']=array( + 'tags' => 'NAME="'.$prefix.'doreject"'.$clicksuffix, + 'label' => qa_lang_html('question/reject_button'), + ); + } + + if ($comment['hideable']) + $buttons['hide']=array( + 'tags' => 'NAME="'.$prefix.'dohide"'.$clicksuffix, + 'label' => qa_lang_html('question/hide_button'), + 'popup' => qa_lang_html('question/hide_c_popup'), + ); + + if ($comment['reshowable']) + $buttons['reshow']=array( + 'tags' => 'NAME="'.$prefix.'doreshow"'.$clicksuffix, + 'label' => qa_lang_html('question/reshow_button'), + ); + + if ($comment['deleteable']) + $buttons['delete']=array( + 'tags' => 'NAME="'.$prefix.'dodelete"'.$clicksuffix, + 'label' => qa_lang_html('question/delete_button'), + 'popup' => qa_lang_html('question/delete_c_popup'), + ); + + if ($comment['claimable']) + $buttons['claim']=array( + 'tags' => 'NAME="'.$prefix.'doclaim"'.$clicksuffix, + 'label' => qa_lang_html('question/claim_button'), + ); + + if ($parent['commentbutton'] && qa_opt('show_c_reply_buttons') && ($comment['type']=='C')) + $buttons['comment']=array( + 'tags' => 'NAME="'.(($parent['basetype']=='Q') ? 'q' : 'a').qa_html($parent['postid']). + '_docomment" onClick="return qa_toggle_element(\'c'.qa_html($parent['postid']).'\')"', + 'label' => qa_lang_html('question/reply_button'), + 'popup' => qa_lang_html('question/reply_c_popup'), + ); + + $c_view['form']=array( + 'style' => 'light', + 'buttons' => $buttons, + ); + } + + return $c_view; + } + + + function qa_page_q_comment_follow_list($parent, $commentsfollows, $alwaysfull, $usershtml, $formrequested, $formpostid) +/* + Return an array $qa_content[...]['c_list'] for all of the comments and follow-on questions in $commentsfollows which + belong to post $parent, as viewed by the current user. If $alwaysfull then all comments will be included, otherwise + the list may be shortened with a 'show previous x comments' link. $usershtml should be an array which maps userids + to HTML user representations, including all comments' and follow on questions' authors and (if present) last + editors. If a form has been explicitly requested for the page, set $formrequested to true and pass the postid of the + post for the form in $formpostid - this will hide the buttons and remove the $formpostid comment from the list. +*/ + { + $parentid=$parent['postid']; + $userid=qa_get_logged_in_userid(); + $cookieid=qa_cookie_get(); + + $commentlist=array( + 'tags' => 'ID="c'.qa_html($parentid).'_list"', + 'cs' => array(), + ); + + $showcomments=array(); + + foreach ($commentsfollows as $commentfollowid => $commentfollow) + if (($commentfollow['parentid']==$parentid) && $commentfollow['viewable'] && ($commentfollowid!=$formpostid) ) + $showcomments[$commentfollowid]=$commentfollow; + + $countshowcomments=count($showcomments); + + if ( (!$alwaysfull) && ($countshowcomments > qa_opt('show_fewer_cs_from')) ) + $skipfirst=$countshowcomments-qa_opt('show_fewer_cs_count'); + else + $skipfirst=0; + + if ($skipfirst==$countshowcomments) { // showing none + if ($skipfirst==1) + $expandtitle=qa_lang_html('question/show_1_comment'); + else + $expandtitle=qa_lang_html_sub('question/show_x_comments', $skipfirst); + + } else { + if ($skipfirst==1) + $expandtitle=qa_lang_html('question/show_1_previous_comment'); + else + $expandtitle=qa_lang_html_sub('question/show_x_previous_comments', $skipfirst); + } + + if ($skipfirst>0) + $commentlist['cs'][$parentid]=array( + 'url' => '?state=showcomments-'.qa_html($parentid).'&show='.qa_html($parentid). + '#'.qa_html(urlencode(qa_anchor($parent['basetype'], $parentid))), + + 'expand_tags' => 'onClick="return qa_show_comments('.qa_js($parentid).');"', + + 'title' => $expandtitle, + ); + + foreach ($showcomments as $commentfollowid => $commentfollow) + if ($skipfirst>0) + $skipfirst--; + + elseif ($commentfollow['basetype']=='C') { + $commentlist['cs'][$commentfollowid]=qa_page_q_comment_view($parent, $commentfollow, $usershtml, $formrequested); + + } elseif ($commentfollow['basetype']=='Q') { + $htmloptions=qa_post_html_defaults('Q'); + $htmloptions['avatarsize']=qa_opt('avatar_q_page_c_size'); + + $commentlist['cs'][$commentfollowid]=qa_post_html_fields($commentfollow, $userid, $cookieid, $usershtml, null, $htmloptions); + } + + if (!count($commentlist['cs'])) + $commentlist['hidden']=true; + + return $commentlist; + } + + + function qa_page_q_add_a_form(&$qa_content, $formid, $usecaptcha, $questionid, $in, $errors, $loadfocusnow, $formrequested) +/* + Return a $qa_content form for adding an answer to $questionid. Pass an HTML element id to use for the form in + $formid and $usecaptcha if it should contain a captcha. Pass previous inputs from a submitted version of this form + in the array $in and resulting errors in $errors. If $loadfocusnow is true, the form will be loaded and focused + immediately. Set $formrequested to true if the user explicitly requested it, as opposed being shown automatically. +*/ + { + switch (qa_user_permit_error('permit_post_a')) { + case 'login': + $form=array( + 'title' => qa_insert_login_links(qa_lang_html('question/answer_must_login'), qa_request()) + ); + break; + + case 'confirm': + $form=array( + 'title' => qa_insert_login_links(qa_lang_html('question/answer_must_confirm'), qa_request()) + ); + break; + + case 'limit': + $form=array( + 'title' => qa_lang_html('question/answer_limit') + ); + break; + + default: + $form=array( + 'title' => qa_lang_html('users/no_permission') + ); + break; + + case false: + $editorname=isset($in['editor']) ? $in['editor'] : qa_opt('editor_for_as'); + $editor=qa_load_editor(@$in['content'], @$in['format'], $editorname); + + if (method_exists($editor, 'update_script')) + $updatescript=$editor->update_script('a_content'); + else + $updatescript=''; + + $custom=qa_opt('show_custom_answer') ? trim(qa_opt('custom_answer')) : ''; + + $form=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'" NAME="a_form"', + + 'title' => qa_lang_html('question/your_answer_title'), + + 'fields' => array( + 'custom' => array( + 'type' => 'custom', + 'note' => $custom, + ), + + 'content' => array_merge( + qa_editor_load_field($editor, $qa_content, @$in['content'], @$in['format'], 'a_content', 12, $loadfocusnow, $loadfocusnow), + array( + 'error' => qa_html(@$errors['content']), + ) + ), + ), + + 'buttons' => array( + 'answer' => array( + 'tags' => 'onClick="'.$updatescript.' return qa_submit_answer('.qa_js($questionid).');"', + 'label' => qa_lang_html('question/add_answer_button'), + ), + ), + + 'hidden' => array( + 'a_editor' => qa_html($editorname), + 'a_doadd' => '1', + ), + ); + + if (!strlen($custom)) + unset($form['fields']['custom']); + + if ($formrequested || !$loadfocusnow) + $form['buttons']['cancel']=array( + 'tags' => 'NAME="docancel"', + 'label' => qa_lang_html('main/cancel_button'), + ); + + qa_set_up_notify_fields($qa_content, $form['fields'], 'A', qa_get_logged_in_email(), + isset($in['notify']) ? $in['notify'] : qa_opt('notify_users_default'), @$in['email'], @$errors['email'], 'a_'); + + $onloads=array(); + + if ($usecaptcha) { + $userid=qa_get_logged_in_userid(); + + $captchaloadscript=qa_set_up_captcha_field($qa_content, $form['fields'], $errors, + qa_insert_login_links(qa_lang_html(isset($userid) ? 'misc/captcha_confirm_fix' : 'misc/captcha_login_fix'))); + + if (strlen($captchaloadscript)) + $onloads[]='document.getElementById('.qa_js($formid).').qa_show=function() { '.$captchaloadscript.' }'; + } + + if (!$loadfocusnow) { + if (method_exists($editor, 'load_script')) + $onloads[]='document.getElementById('.qa_js($formid).').qa_load=function() { '.$editor->load_script('a_content').' }'; + if (method_exists($editor, 'focus_script')) + $onloads[]='document.getElementById('.qa_js($formid).').qa_focus=function() { '.$editor->focus_script('a_content').' }'; + + $form['buttons']['cancel']['tags'].=' onClick="return qa_toggle_element();"'; + } + + if (count($onloads)) + $qa_content['script_onloads'][]=$onloads; + break; + } + + $form['id']=$formid; + $form['collapse']=!$loadfocusnow; + $form['style']='tall'; + + return $form; + } + + + function qa_page_q_add_c_form(&$qa_content, $questionid, $parentid, $formid, $usecaptcha, $in, $errors, $loadfocusnow) +/* + Returns a $qa_content form for adding a comment to post $parentid which is part of question $questionid. Pass an + HTML element id to use for the form in $formid and $usecaptcha if it should contain a captcha. Pass previous inputs + from a submitted version of this form in the array $in and resulting errors in $errors. If $loadfocusnow is true, + the form will be loaded and focused immediately. +*/ + { + switch (qa_user_permit_error('permit_post_c')) { + case 'login': + $form=array( + 'title' => qa_insert_login_links(qa_lang_html('question/comment_must_login'), qa_request()) + ); + break; + + case 'confirm': + $form=array( + 'title' => qa_insert_login_links(qa_lang_html('question/comment_must_confirm'), qa_request()) + ); + break; + + case 'limit': + $form=array( + 'title' => qa_lang_html('question/comment_limit') + ); + break; + + default: + $form=array( + 'title' => qa_lang_html('users/no_permission') + ); + break; + + case false: + $prefix='c'.$parentid.'_'; + + $editorname=isset($in['editor']) ? $in['editor'] : qa_opt('editor_for_cs'); + $editor=qa_load_editor(@$in['content'], @$in['format'], $editorname); + + if (method_exists($editor, 'update_script')) + $updatescript=$editor->update_script($prefix.'content'); + else + $updatescript=''; + + $custom=qa_opt('show_custom_comment') ? trim(qa_opt('custom_comment')) : ''; + + $form=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'" NAME="c_form_'.qa_html($parentid).'"', + + 'title' => qa_lang_html(($questionid==$parentid) ? 'question/your_comment_q' : 'question/your_comment_a'), + + 'fields' => array( + 'custom' => array( + 'type' => 'custom', + 'note' => $custom, + ), + + 'content' => array_merge( + qa_editor_load_field($editor, $qa_content, @$in['content'], @$in['format'], $prefix.'content', 4, $loadfocusnow, $loadfocusnow), + array( + 'error' => qa_html(@$errors['content']), + ) + ), + ), + + 'buttons' => array( + 'comment' => array( + 'tags' => 'onClick="'.$updatescript.' return qa_submit_comment('.qa_js($questionid).', '.qa_js($parentid).');"', + 'label' => qa_lang_html('question/add_comment_button'), + ), + + 'cancel' => array( + 'tags' => 'NAME="docancel"', + 'label' => qa_lang_html('main/cancel_button'), + ), + ), + + 'hidden' => array( + $prefix.'editor' => qa_html($editorname), + $prefix.'doadd' => '1', + ), + ); + + if (!strlen($custom)) + unset($form['fields']['custom']); + + qa_set_up_notify_fields($qa_content, $form['fields'], 'C', qa_get_logged_in_email(), + isset($in['notify']) ? $in['notify'] : qa_opt('notify_users_default'), $in['email'], @$errors['email'], $prefix); + + $onloads=array(); + + if ($usecaptcha) { + $userid=qa_get_logged_in_userid(); + + $captchaloadscript=qa_set_up_captcha_field($qa_content, $form['fields'], $errors, + qa_insert_login_links(qa_lang_html(isset($userid) ? 'misc/captcha_confirm_fix' : 'misc/captcha_login_fix'))); + + if (strlen($captchaloadscript)) + $onloads[]='document.getElementById('.qa_js($formid).').qa_show=function() { '.$captchaloadscript.' }'; + } + + if (!$loadfocusnow) { + if (method_exists($editor, 'load_script')) + $onloads[]='document.getElementById('.qa_js($formid).').qa_load=function() { '.$editor->load_script($prefix.'content').' }'; + if (method_exists($editor, 'focus_script')) + $onloads[]='document.getElementById('.qa_js($formid).').qa_focus=function() { '.$editor->focus_script($prefix.'content').' }'; + + $form['buttons']['cancel']['tags'].=' onClick="return qa_toggle_element()"'; + } + + if (count($onloads)) + $qa_content['script_onloads'][]=$onloads; + } + + $form['id']=$formid; + $form['collapse']=!$loadfocusnow; + $form['style']='tall'; + + return $form; + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-question.php b/qa-include/qa-page-question.php new file mode 100644 index 000000000..f17725225 --- /dev/null +++ b/qa-include/qa-page-question.php @@ -0,0 +1,423 @@ + $answer) { + $answers[$key]=$answer+qa_page_q_post_rules($answer, $question, $answers, $achildposts); + $answers[$key]['isselected']=($answer['postid']==$question['selchildid']); + } + + foreach ($commentsfollows as $key => $commentfollow) { + $parent=($commentfollow['parentid']==$questionid) ? $question : @$answers[$commentfollow['parentid']]; + $commentsfollows[$key]=$commentfollow+qa_page_q_post_rules($commentfollow, $parent, $commentsfollows, null); + } + } + + $usecaptcha=qa_user_use_captcha('captcha_on_anon_post'); + + +// Deal with question not found or not viewable, otherwise report the view event + + if (!isset($question)) + return include QA_INCLUDE_DIR.'qa-page-not-found.php'; + + if (!$question['viewable']) { + $qa_content=qa_content_prepare(); + + if ($question['queued']) + $qa_content['error']=qa_lang_html('question/q_waiting_approval'); + elseif ($question['flagcount'] && !isset($question['lastuserid'])) + $qa_content['error']=qa_lang_html('question/q_hidden_flagged'); + elseif ($question['authorlast']) + $qa_content['error']=qa_lang_html('question/q_hidden_author'); + else + $qa_content['error']=qa_lang_html('question/q_hidden_other'); + + $qa_content['suggest_next']=qa_html_suggest_qs_tags(qa_using_tags()); + + return $qa_content; + } + + $permiterror=qa_user_permit_error('permit_view_q_page'); + + if ( $permiterror && (qa_is_human_probably() || !qa_opt('allow_view_q_bots')) ) { + $qa_content=qa_content_prepare(); + $topage=qa_q_request($questionid, $question['title']); + + switch ($permiterror) { + case 'login': + $qa_content['error']=qa_insert_login_links(qa_lang_html('main/view_q_must_login'), $topage); + break; + + case 'confirm': + $qa_content['error']=qa_insert_login_links(qa_lang_html('main/view_q_must_confirm'), $topage); + break; + + default: + $qa_content['error']=qa_lang_html('users/no_permission'); + break; + } + + return $qa_content; + } + + +// If we're responding to an HTTP POST, include file that handles all posting/editing/etc... logic +// This is in a separate file because it's a *lot* of logic, and will slow down ordinary page views + + $pagestart=qa_get_start(); + $pagestate=qa_get_state(); + $showid=qa_get('show'); + $pageerror=null; + $formtype=null; + $formpostid=null; + $jumptoanchor=null; + $commentsall=null; + + if (substr($pagestate, 0, 13)=='showcomments-') { + $commentsall=substr($pagestate, 13); + $pagestate=null; + + } elseif (isset($showid)) { + foreach ($commentsfollows as $comment) + if ($comment['postid']==$showid) { + $commentsall=$comment['parentid']; + break; + } + } + + if (qa_is_http_post() || strlen($pagestate)) + require QA_INCLUDE_DIR.'qa-page-question-post.php'; + + $formrequested=isset($formtype); + + if ((!$formrequested) && $question['answerbutton']) { + $immedoption=qa_opt('show_a_form_immediate'); + + if ( ($immedoption=='always') || (($immedoption=='if_no_as') && (!$question['isbyuser']) && (!$question['acount'])) ) + $formtype='a_add'; // show answer form by default + } + + +// Get information on the users referenced + + $usershtml=qa_userids_handles_html(array_merge(array($question), $answers, $commentsfollows), true); + + +// Prepare content for theme + + $qa_content=qa_content_prepare(true, array_keys(qa_category_path($categories, $question['categoryid']))); + + if (isset($userid) && !$formrequested) + $qa_content['favorite']=qa_favorite_form(QA_ENTITY_QUESTION, $questionid, $favorite, + qa_lang($favorite ? 'question/remove_q_favorites' : 'question/add_q_favorites')); + + $qa_content['script_rel'][]='qa-content/qa-question.js?'.QA_VERSION; + + $qa_content['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'" NAME="q_page_form"', + 'hidden' => array( + 'qa_click' => '', // for simulating clicks in Javascript + ), + ); + + if (isset($pageerror)) + $qa_content['error']=$pageerror; // might also show voting error set in qa-index.php + + elseif ($question['queued']) + $qa_content['error']=$question['isbyuser'] ? qa_lang_html('question/q_your_waiting_approval') : qa_lang_html('question/q_waiting_your_approval'); + + if ($question['hidden']) + $qa_content['hidden']=true; + + qa_sort_by($commentsfollows, 'created'); + + +// Prepare content for the question... + + if ($formtype=='q_edit') { // ...in edit mode + $qa_content['title']=qa_lang_html($question['editable'] ? 'question/edit_q_title' : + (qa_using_categories() ? 'question/recat_q_title' : 'question/retag_q_title')); + $qa_content['form_q_edit']=qa_page_q_edit_q_form($qa_content, $question, @$qin, @$qerrors, $completetags, $categories); + $qa_content['q_view']['raw']=$question; + + } else { // ...in view mode + $qa_content['q_view']=qa_page_q_question_view($question, $parentquestion, $closepost, $usershtml, $formrequested); + + $qa_content['title']=$qa_content['q_view']['title']; + + $qa_content['description']=qa_html(qa_shorten_string_line(qa_viewer_text($question['content'], $question['format']), 150)); + + $qa_content['canonical']=qa_q_path_html($question['postid'], $question['title'], true); + + $categorykeyword=@$categories[$question['categoryid']]['title']; + + $qa_content['keywords']=qa_html(implode(',', array_merge( + (qa_using_categories() && strlen($categorykeyword)) ? array($categorykeyword) : array(), + qa_tagstring_to_tags($question['tags']) + ))); // as far as I know, META keywords have zero effect on search rankings or listings, but many people have asked for this + } + + +// Prepare content for an answer being edited (if any) or to be added + + if ($formtype=='a_edit') { + $qa_content['a_form']=qa_page_q_edit_a_form($qa_content, 'a'.$formpostid, $answers[$formpostid], + $question, $answers, $commentsfollows, @$aeditin[$formpostid], @$aediterrors[$formpostid]); + + $qa_content['a_form']['c_list']=qa_page_q_comment_follow_list($answers[$formpostid], $commentsfollows, + true, $usershtml, $formrequested, $formpostid); + + $jumptoanchor='a'.$formpostid; + + } elseif (($formtype=='a_add') || ($question['answerbutton'] && !$formrequested)) { + $qa_content['a_form']=qa_page_q_add_a_form($qa_content, 'anew', $usecaptcha, $questionid, @$anewin, @$anewerrors, $formtype=='a_add', $formrequested); + + if ($formrequested) + $jumptoanchor='anew'; + elseif ($formtype=='a_add') + $qa_content['script_onloads'][]=array( + "qa_element_revealed=document.getElementById('anew')" + ); + } + + +// Prepare content for comments on the question, plus add or edit comment forms + + if ($formtype=='q_close') { + $qa_content['q_view']['c_form']=qa_page_q_close_q_form($qa_content, $question, 'close', @$closein, @$closeerrors); + $jumptoanchor='close'; + + } elseif ((($formtype=='c_add') && ($formpostid==$questionid)) || ($question['commentbutton'] && !$formrequested) ) { // ...to be added + $qa_content['q_view']['c_form']=qa_page_q_add_c_form($qa_content, $questionid, $questionid, 'c'.$questionid, + $usecaptcha, @$cnewin[$questionid], @$cnewerrors[$questionid], $formtype=='c_add'); + + if (($formtype=='c_add') && ($formpostid==$questionid)) { + $jumptoanchor='c'.$questionid; + $commentsall=$questionid; + } + + } elseif (($formtype=='c_edit') && (@$commentsfollows[$formpostid]['parentid']==$questionid)) { // ...being edited + $qa_content['q_view']['c_form']=qa_page_q_edit_c_form($qa_content, 'c'.$formpostid, $commentsfollows[$formpostid], + @$ceditin[$formpostid], @$cediterrors[$formpostid]); + + $jumptoanchor='c'.$formpostid; + $commentsall=$questionid; + } + + $qa_content['q_view']['c_list']=qa_page_q_comment_follow_list($question, $commentsfollows, $commentsall==$questionid, + $usershtml, $formrequested, $formpostid); // ...for viewing + + +// Prepare content for existing answers (could be added to by Ajax) + + $qa_content['a_list']=array( + 'tags' => 'ID="a_list"', + 'as' => array(), + ); + + // sort according to the site preferences + + if (qa_opt('sort_answers_by')=='votes') { + foreach ($answers as $answerid => $answer) + $answers[$answerid]['sortvotes']=$answer['downvotes']-$answer['upvotes']; + + qa_sort_by($answers, 'sortvotes', 'created'); + + } else + qa_sort_by($answers, 'created'); + + // further changes to ordering to deal with queued, hidden and selected answers + + $countfortitle=$question['acount']; + $nextposition=10000; + $answerposition=array(); + + foreach ($answers as $answerid => $answer) + if ($answer['viewable']) { + $position=$nextposition++; + + if ($answer['hidden']) + $position+=10000; + + elseif ($answer['queued']) { + $position-=10000; + $countfortitle++; // include these in displayed count + + } elseif ($answer['isselected'] && qa_opt('show_selected_first')) + $position-=5000; + + $answerposition[$answerid]=$position; + } + + asort($answerposition, SORT_NUMERIC); + + // extract IDs and prepare for pagination + + $answerids=array_keys($answerposition); + $countforpages=count($answerids); + $pagesize=qa_opt('page_size_q_as'); + + // see if we need to display a particular answer + + if (isset($showid)) { + if (isset($commentsfollows[$showid])) + $showid=$commentsfollows[$showid]['parentid']; + + $position=array_search($showid, $answerids); + + if (is_numeric($position)) + $pagestart=floor($position/$pagesize)*$pagesize; + } + + // build the actual answer list + + $answerids=array_slice($answerids, $pagestart, $pagesize); + + foreach ($answerids as $answerid) { + $answer=$answers[$answerid]; + + if (!(($formtype=='a_edit') && ($formpostid==$answerid))) { + $a_view=qa_page_q_answer_view($question, $answer, $answer['isselected'], $usershtml, $formrequested); + + // Prepare content for comments on this answer, plus add or edit comment forms + + if ((($formtype=='c_add') && ($formpostid==$answerid)) || ($answer['commentbutton'] && !$formrequested) ) { // ...to be added + $a_view['c_form']=qa_page_q_add_c_form($qa_content, $questionid, $answerid, 'c'.$answerid, + $usecaptcha, @$cnewin[$answerid], @$cnewerrors[$answerid], $formtype=='c_add'); + + if (($formtype=='c_add') && ($formpostid==$answerid)) { + $jumptoanchor='c'.$answerid; + $commentsall=$answerid; + } + + } else if (($formtype=='c_edit') && (@$commentsfollows[$formpostid]['parentid']==$answerid)) { // ...being edited + $a_view['c_form']=qa_page_q_edit_c_form($qa_content, 'c'.$formpostid, $commentsfollows[$formpostid], + @$ceditin[$formpostid], @$cediterrors[$formpostid]); + + $jumptoanchor='c'.$formpostid; + $commentsall=$answerid; + } + + $a_view['c_list']=qa_page_q_comment_follow_list($answer, $commentsfollows, $commentsall==$answerid, + $usershtml, $formrequested, $formpostid); // ...for viewing + + // Add the answer to the list + + $qa_content['a_list']['as'][]=$a_view; + } + } + + if ($countfortitle==1) + $qa_content['a_list']['title']=qa_lang_html('question/1_answer_title'); + elseif ($countfortitle>0) + $qa_content['a_list']['title']=qa_lang_html_sub('question/x_answers_title', $countfortitle); + + $qa_content['a_list']['title']=''.@$qa_content['a_list']['title'].''; + + if (!$formrequested) + $qa_content['page_links']=qa_html_page_links(qa_request(), $pagestart, $pagesize, $countforpages, qa_opt('pages_prev_next'), array(), false, 'a_list_title'); + + +// Some generally useful stuff + + if (qa_using_categories() && count($categories)) + $qa_content['navigation']['cat']=qa_category_navigation($categories, $question['categoryid']); + + if (isset($jumptoanchor)) + $qa_content['script_onloads'][]=array( + 'qa_scroll_page_to($("#"+'.qa_js($jumptoanchor).').offset().top);' + ); + + +// Determine whether this request should be counted for page view statistics + + if ( + qa_opt('do_count_q_views') && + (!$formrequested) && + (!qa_is_http_post()) && + qa_is_human_probably() && + ( (!$question['views']) || ( // if it has more than zero views + ( ($question['lastviewip']!=qa_remote_ip_address()) || (!isset($question['lastviewip'])) ) && // then it must be different IP from last view + ( ($question['createip']!=qa_remote_ip_address()) || (!isset($question['createip'])) ) && // and different IP from the creator + ( ($question['userid']!=$userid) || (!isset($question['userid'])) ) && // and different user from the creator + ( ($question['cookieid']!=$cookieid) || (!isset($question['cookieid'])) ) // and different cookieid from the creator + ) ) + ) + $qa_content['inc_views_postid']=$questionid; + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-questions.php b/qa-include/qa-page-questions.php new file mode 100644 index 000000000..01e968dd2 --- /dev/null +++ b/qa-include/qa-page-questions.php @@ -0,0 +1,147 @@ + $sort); + + switch ($sort) { + case 'hot': + $sometitle=$countslugs ? qa_lang_html_sub('main/hot_qs_in_x', $categorytitlehtml) : qa_lang_html('main/hot_qs_title'); + $feedpathprefix=qa_opt('feed_for_hot') ? 'hot' : null; + break; + + case 'votes': + $sometitle=$countslugs ? qa_lang_html_sub('main/voted_qs_in_x', $categorytitlehtml) : qa_lang_html('main/voted_qs_title'); + break; + + case 'answers': + $sometitle=$countslugs ? qa_lang_html_sub('main/answered_qs_in_x', $categorytitlehtml) : qa_lang_html('main/answered_qs_title'); + break; + + case 'views': + $sometitle=$countslugs ? qa_lang_html_sub('main/viewed_qs_in_x', $categorytitlehtml) : qa_lang_html('main/viewed_qs_title'); + break; + + default: + $linkparams=array(); + $sometitle=$countslugs ? qa_lang_html_sub('main/recent_qs_in_x', $categorytitlehtml) : qa_lang_html('main/recent_qs_title'); + $categorypathprefix='questions/'; + $feedpathprefix=qa_opt('feed_for_questions') ? 'questions' : null; + break; + } + + +// Prepare and return content for theme + + $qa_content=qa_q_list_page_content( + $questions, // questions + qa_opt('page_size_qs'), // questions per page + $start, // start offset + $countslugs ? $categories[$categoryid]['qcount'] : qa_opt('cache_qcount'), // total count + $sometitle, // title if some questions + $nonetitle, // title if no questions + $categories, // categories for navigation + $categoryid, // selected category id + true, // show question counts in category navigation + $categorypathprefix, // prefix for links in category navigation + $feedpathprefix, // prefix for RSS feed paths + $countslugs ? qa_html_suggest_qs_tags(qa_using_tags()) : qa_html_suggest_ask($categoryid), // suggest what to do next + $linkparams, // extra parameters for page links + $linkparams, // category nav params + $favorite // has used favorited this category + ); + + if (QA_ALLOW_UNINDEXED_QUERIES || !$countslugs) + $qa_content['navigation']['sub']=qa_qs_sub_navigation($sort, $categoryslugs); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-register.php b/qa-include/qa-page-register.php new file mode 100644 index 000000000..df348ecfa --- /dev/null +++ b/qa-include/qa-page-register.php @@ -0,0 +1,179 @@ + 'METHOD="POST" ACTION="'.qa_self_html().'"', + + 'style' => 'tall', + + 'fields' => array( + 'custom' => array( + 'type' => 'custom', + 'note' => $custom, + ), + + 'handle' => array( + 'label' => qa_lang_html('users/handle_label'), + 'tags' => 'NAME="handle" ID="handle"', + 'value' => qa_html(@$inhandle), + 'error' => qa_html(@$errors['handle']), + ), + + 'password' => array( + 'type' => 'password', + 'label' => qa_lang_html('users/password_label'), + 'tags' => 'NAME="password" ID="password"', + 'value' => qa_html(@$inpassword), + 'error' => qa_html(@$errors['password']), + ), + + 'email' => array( + 'label' => qa_lang_html('users/email_label'), + 'tags' => 'NAME="email" ID="email"', + 'value' => qa_html(@$inemail), + 'note' => qa_opt('email_privacy'), + 'error' => qa_html(@$errors['email']), + ), + ), + + 'buttons' => array( + 'register' => array( + 'label' => qa_lang_html('users/register_button'), + ), + ), + + 'hidden' => array( + 'doregister' => '1', + ), + ); + + if (!strlen($custom)) + unset($qa_content['form']['fields']['custom']); + + if (qa_opt('captcha_on_register')) + qa_set_up_captcha_field($qa_content, $qa_content['form']['fields'], @$errors); + + $loginmodules=qa_load_modules_with('login', 'login_html'); + + foreach ($loginmodules as $module) { + ob_start(); + $module->login_html(qa_opt('site_url').qa_get('to'), 'register'); + $html=ob_get_clean(); + + if (strlen($html)) + @$qa_content['custom'].='
    '.$html.'
    '; + } + + $qa_content['focusid']=isset($errors['handle']) ? 'handle' + : (isset($errors['password']) ? 'password' + : (isset($errors['email']) ? 'email' : 'handle')); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-reset.php b/qa-include/qa-page-reset.php new file mode 100644 index 000000000..80898a538 --- /dev/null +++ b/qa-include/qa-page-reset.php @@ -0,0 +1,136 @@ + $inemailhandle, 'ps' => '1')); // redirect to login page + + } else + $errors['code']=qa_lang('users/reset_code_wrong'); + + } else + $errors['emailhandle']=qa_lang('users/user_not_found'); + + } else { + $inemailhandle=qa_get('e'); + $incode=qa_get('c'); + } + + +// Prepare content for theme + + $qa_content=qa_content_prepare(); + + $qa_content['title']=qa_lang_html('users/reset_title'); + + if (empty($inemailhandle) || isset($errors['emailhandle'])) + $forgotpath=qa_path('forgot'); + else + $forgotpath=qa_path('forgot', array('e' => $inemailhandle)); + + $qa_content['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + + 'style' => 'tall', + + 'ok' => empty($incode) ? qa_lang_html('users/reset_code_emailed') : null, + + 'fields' => array( + 'email_handle' => array( + 'label' => qa_lang_html('users/email_handle_label'), + 'tags' => 'NAME="emailhandle" ID="emailhandle"', + 'value' => qa_html(@$inemailhandle), + 'error' => qa_html(@$errors['emailhandle']), + ), + + 'code' => array( + 'label' => qa_lang_html('users/reset_code_label'), + 'tags' => 'NAME="code" ID="code"', + 'value' => qa_html(@$incode), + 'error' => qa_html(@$errors['code']), + 'note' => qa_lang_html('users/reset_code_emailed').' - '. + ''.qa_lang_html('users/reset_code_another').'', + ), + ), + + 'buttons' => array( + 'reset' => array( + 'label' => qa_lang_html('users/send_password_button'), + ), + ), + + 'hidden' => array( + 'doreset' => '1', + ), + ); + + $qa_content['focusid']=isset($errors['emailhandle']) ? 'emailhandle' : 'code'; + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-search.php b/qa-include/qa-page-search.php new file mode 100644 index 000000000..651fb083a --- /dev/null +++ b/qa-include/qa-page-search.php @@ -0,0 +1,155 @@ + $inquery, + 'start' => $start, + )); + } + + +// Prepare content for theme + + $qa_content=qa_content_prepare(true); + + if (strlen(qa_get('q'))) { + $qa_content['search']['value']=qa_html($inquery); + + if (count($results)) + $qa_content['title']=qa_lang_html_sub('main/results_for_x', qa_html($inquery)); + else + $qa_content['title']=qa_lang_html_sub('main/no_results_for_x', qa_html($inquery)); + + $qa_content['q_list']['form']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + ); + + $qa_content['q_list']['qs']=array(); + + $questionoptions=qa_post_html_defaults('Q'); + + foreach ($results as $result) + if (!isset($result['question'])) { // if we have any non-question results, display with less statistics + $questionoptions['voteview']=false; + $questionoptions['answersview']=false; + $questionoptions['viewsview']=false; + + $fakeoptions=$questionoptions; + $fakeoptions['whoview']=false; + $fakeoptions['whenview']=false; + $fakeoptions['whatview']=false; + + break; + } + + foreach ($results as $result) { + if (isset($result['question'])) + $fields=qa_post_html_fields($result['question'], $userid, qa_cookie_get(), $usershtml, null, $questionoptions); + + elseif (isset($result['url'])) + $fields=array( + 'what' => qa_html($result['url']), + 'meta_order' => qa_lang_html('main/meta_order'), + ); + + else + continue; // nothing to show here + + $fields['title']=qa_html($result['title']); + $fields['url']=qa_html($result['url']); + + $qa_content['q_list']['qs'][]=$fields; + } + + $qa_content['page_links']=qa_html_page_links(qa_request(), $start, $pagesize, $start+$gotcount, + qa_opt('pages_prev_next'), array('q' => $inquery), $gotcount>=$count); + + if (qa_opt('feed_for_search')) + $qa_content['feed']=array( + 'url' => qa_path_html(qa_feed_request('search/'.$inquery)), + 'label' => qa_lang_html_sub('main/results_for_x', qa_html($inquery)), + ); + + if (empty($qa_content['page_links'])) + $qa_content['suggest_next']=qa_html_suggest_qs_tags(qa_using_tags()); + + } else + $qa_content['error']=qa_lang_html('main/search_explanation'); + + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-tag.php b/qa-include/qa-page-tag.php new file mode 100644 index 000000000..36ca16c69 --- /dev/null +++ b/qa-include/qa-page-tag.php @@ -0,0 +1,96 @@ + 'METHOD="POST" ACTION="'.qa_self_html().'"', + ); + + $qa_content['q_list']['qs']=array(); + foreach ($questions as $postid => $question) + $qa_content['q_list']['qs'][]=qa_post_html_fields($question, $userid, qa_cookie_get(), $usershtml, + null, qa_post_html_defaults('Q')); + + $qa_content['page_links']=qa_html_page_links(qa_request(), $start, $pagesize, $tagword['tagcount'], qa_opt('pages_prev_next')); + + if (empty($qa_content['page_links'])) + $qa_content['suggest_next']=qa_html_suggest_qs_tags(true); + + if (qa_opt('feed_for_tag_qs')) + $qa_content['feed']=array( + 'url' => qa_path_html(qa_feed_request('tag/'.$tag)), + 'label' => qa_lang_html_sub('main/questions_tagged_x', qa_html($tag)), + ); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-tags.php b/qa-include/qa-page-tags.php new file mode 100644 index 000000000..9c8ed7c92 --- /dev/null +++ b/qa-include/qa-page-tags.php @@ -0,0 +1,83 @@ + array(), + 'rows' => ceil($pagesize/qa_opt('columns_tags')), + 'type' => 'tags' + ); + + if (count($populartags)) { + $output=0; + foreach ($populartags as $word => $count) { + $qa_content['ranking']['items'][]=array( + 'label' => qa_tag_html($word), + 'count' => number_format($count), + ); + + if ((++$output)>=$pagesize) + break; + } + + } else + $qa_content['title']=qa_lang_html('main/no_tags_found'); + + $qa_content['page_links']=qa_html_page_links(qa_request(), $start, $pagesize, $tagcount, qa_opt('pages_prev_next')); + + if (empty($qa_content['page_links'])) + $qa_content['suggest_next']=qa_html_suggest_ask(); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-unanswered.php b/qa-include/qa-page-unanswered.php new file mode 100644 index 000000000..1d62beec3 --- /dev/null +++ b/qa-include/qa-page-unanswered.php @@ -0,0 +1,148 @@ + $by); + + switch ($by) { + case 'selected': + if ($countslugs) { + $sometitle=qa_lang_html_sub('main/unselected_qs_in_x', $categorytitlehtml); + $nonetitle=qa_lang_html_sub('main/no_una_questions_in_x', $categorytitlehtml); + + } else { + $sometitle=qa_lang_html('main/unselected_qs_title'); + $nonetitle=qa_lang_html('main/no_unselected_qs_found'); + $count=qa_opt('cache_unselqcount'); + } + break; + + case 'upvotes': + if ($countslugs) { + $sometitle=qa_lang_html_sub('main/unupvoteda_qs_in_x', $categorytitlehtml); + $nonetitle=qa_lang_html_sub('main/no_una_questions_in_x', $categorytitlehtml); + + } else { + $sometitle=qa_lang_html('main/unupvoteda_qs_title'); + $nonetitle=qa_lang_html('main/no_unupvoteda_qs_found'); + $count=qa_opt('cache_unupaqcount'); + } + break; + + default: + $feedpathprefix=qa_opt('feed_for_unanswered') ? 'unanswered' : null; + $linkparams=array(); + + if ($countslugs) { + $sometitle=qa_lang_html_sub('main/unanswered_qs_in_x', $categorytitlehtml); + $nonetitle=qa_lang_html_sub('main/no_una_questions_in_x', $categorytitlehtml); + + } else { + $sometitle=qa_lang_html('main/unanswered_qs_title'); + $nonetitle=qa_lang_html('main/no_una_questions_found'); + $count=qa_opt('cache_unaqcount'); + } + break; + } + + +// Prepare and return content for theme + + $qa_content=qa_q_list_page_content( + $questions, // questions + qa_opt('page_size_una_qs'), // questions per page + $start, // start offset + @$count, // total count + $sometitle, // title if some questions + $nonetitle, // title if no questions + QA_ALLOW_UNINDEXED_QUERIES ? $categories : null, // categories for navigation (null since not shown on this page) + QA_ALLOW_UNINDEXED_QUERIES ? $categoryid : null, // selected category id (null since not relevant) + false, // show question counts in category navigation (null since not relevant) + 'unanswered/', // prefix for links in category navigation (null since no navigation) + $feedpathprefix, // prefix for RSS feed paths (null to hide) + qa_html_suggest_qs_tags(qa_using_tags()), // suggest what to do next + $linkparams, // extra parameters for page links + $linkparams // category nav params + ); + + $qa_content['navigation']['sub']=qa_unanswered_sub_navigation($by, $categoryslugs); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-unsubscribe.php b/qa-include/qa-page-unsubscribe.php new file mode 100644 index 000000000..bf146dd49 --- /dev/null +++ b/qa-include/qa-page-unsubscribe.php @@ -0,0 +1,85 @@ + qa_html(qa_opt('site_title')), + '^1' => '', + '^2' => '', + )); + else + $qa_content['error']=qa_insert_login_links(qa_lang_html('users/unsubscribe_wrong_log_in'), 'unsubscribe'); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-updates.php b/qa-include/qa-page-updates.php new file mode 100644 index 000000000..4f69c6598 --- /dev/null +++ b/qa-include/qa-page-updates.php @@ -0,0 +1,119 @@ + '', + '^2' => '', + )) : null // suggest what to do next + ); + + $qa_content['navigation']['sub']=array( + 'all' => array( + 'label' => qa_lang_html('misc/nav_all_my_updates'), + 'url' => qa_path_html('updates'), + 'selected' => $forfavorites && $forcontent, + ), + + 'favorites' => array( + 'label' => qa_lang_html('misc/nav_my_favorites'), + 'url' => qa_path_html('updates', array('show' => 'favorites')), + 'selected' => $forfavorites && !$forcontent, + ), + + 'myposts' => array( + 'label' => qa_lang_html('misc/nav_my_content'), + 'url' => qa_path_html('updates', array('show' => 'content')), + 'selected' => $forcontent && !$forfavorites, + ), + ); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-user.php b/qa-include/qa-page-user.php new file mode 100644 index 000000000..d4cd372bf --- /dev/null +++ b/qa-include/qa-page-user.php @@ -0,0 +1,711 @@ +=QA_USER_LEVEL_SUPER) || ($loginlevel>$useraccount['level'])) && + (!qa_user_permit_error()) + ) { // can't change self - or someone on your level (or higher, obviously) unless you're a super admin + + if ($loginlevel>=QA_USER_LEVEL_SUPER) + $maxlevelassign=QA_USER_LEVEL_SUPER; + + elseif ($loginlevel>=QA_USER_LEVEL_ADMIN) + $maxlevelassign=QA_USER_LEVEL_MODERATOR; + + elseif ($loginlevel>=QA_USER_LEVEL_MODERATOR) + $maxlevelassign=QA_USER_LEVEL_EXPERT; + + if ($loginlevel>=QA_USER_LEVEL_ADMIN) + $fieldseditable=true; + + if (isset($maxlevelassign) && ($useraccount['flags'] & QA_USER_FLAGS_USER_BLOCKED)) + $maxlevelassign=min($maxlevelassign, QA_USER_LEVEL_EDITOR); // if blocked, can't promote too high + } + + $usereditbutton=$fieldseditable || isset($maxlevelassign); + $userediting=$usereditbutton && (qa_get_state()=='edit'); + } + + +// Process edit or save button for user + + if (!QA_FINAL_EXTERNAL_USERS) { + $reloaduser=false; + + if ($usereditbutton) { + if (qa_clicked('docancel')) + qa_redirect(qa_request()); + + elseif (qa_clicked('doedit')) + qa_redirect(qa_request(), array('state' => 'edit')); + + elseif (qa_clicked('dosave')) { + require_once QA_INCLUDE_DIR.'qa-app-users-edit.php'; + require_once QA_INCLUDE_DIR.'qa-db-users.php'; + + $errors=array(); + + if (qa_post_text('removeavatar')) { + qa_db_user_set_flag($userid, QA_USER_FLAGS_SHOW_AVATAR, false); + qa_db_user_set_flag($userid, QA_USER_FLAGS_SHOW_GRAVATAR, false); + + if (isset($useraccount['avatarblobid'])) { + require_once QA_INCLUDE_DIR.'qa-db-blobs.php'; + + qa_db_user_set($userid, 'avatarblobid', null); + qa_db_user_set($userid, 'avatarwidth', null); + qa_db_user_set($userid, 'avatarheight', null); + qa_db_blob_delete($useraccount['avatarblobid']); + } + } + + if ($fieldseditable) { + $inemail=qa_post_text('email'); + + $filterhandle=$handle; // we're not filtering the handle... + $errors=qa_handle_email_filter($filterhandle, $inemail, $useraccount); + unset($errors['handle']); // ...and we don't care about any errors in it + + if (!isset($errors['email'])) + if ($inemail != $useraccount['email']) { + qa_db_user_set($userid, 'email', $inemail); + qa_db_user_set_flag($userid, QA_USER_FLAGS_EMAIL_CONFIRMED, false); + } + + $inprofile=array(); + foreach ($userfields as $userfield) + $inprofile[$userfield['fieldid']]=qa_post_text('field_'.$userfield['fieldid']); + + $filtermodules=qa_load_modules_with('filter', 'filter_profile'); + foreach ($filtermodules as $filtermodule) + $filtermodule->filter_profile($inprofile, $errors, $useraccount, $userprofile); + + foreach ($userfields as $userfield) + if (!isset($errors[$userfield['fieldid']])) + qa_db_user_profile_set($userid, $userfield['title'], $inprofile[$userfield['fieldid']]); + + if (count($errors)) + $userediting=true; + + qa_report_event('u_edit', $loginuserid, qa_get_logged_in_handle(), qa_cookie_get(), array( + 'userid' => $userid, + 'handle' => $useraccount['handle'], + )); + } + + if (isset($maxlevelassign)) { + $inlevel=min($maxlevelassign, (int)qa_post_text('level')); // constrain based on maximum permitted to prevent simple browser-based attack + + if ($inlevel != $useraccount['level']) { + qa_db_user_set($userid, 'level', $inlevel); + + qa_report_event('u_level', $loginuserid, qa_get_logged_in_handle(), qa_cookie_get(), array( + 'userid' => $userid, + 'handle' => $useraccount['handle'], + 'level' => $inlevel, + 'oldlevel' => $useraccount['level'], + )); + } + } + + + if (empty($errors)) + qa_redirect(qa_request()); + + list($useraccount, $userprofile)=qa_db_select_with_pending( + qa_db_user_account_selectspec($userid, true), + qa_db_user_profile_selectspec($userid, true) + ); + } + } + + if (isset($maxlevelassign) && ($useraccount['level'] $userid, + 'handle' => $useraccount['handle'], + )); + + qa_redirect(qa_request()); + } + + if (qa_clicked('dounblock')) { + require_once QA_INCLUDE_DIR.'qa-db-users.php'; + + qa_db_user_set_flag($userid, QA_USER_FLAGS_USER_BLOCKED, false); + + qa_report_event('u_unblock', $loginuserid, qa_get_logged_in_handle(), qa_cookie_get(), array( + 'userid' => $userid, + 'handle' => $useraccount['handle'], + )); + + qa_redirect(qa_request()); + } + + if (qa_clicked('dohideall') && !qa_user_permit_error('permit_hide_show')) { + require_once QA_INCLUDE_DIR.'qa-db-admin.php'; + require_once QA_INCLUDE_DIR.'qa-app-posts.php'; + + $postids=qa_db_get_user_visible_postids($userid); + + foreach ($postids as $postid) + qa_post_set_hidden($postid, true, $loginuserid); + + qa_redirect(qa_request()); + } + + if (qa_clicked('dodelete') && ($loginlevel>=QA_USER_LEVEL_ADMIN)) { + require_once QA_INCLUDE_DIR.'qa-app-users-edit.php'; + + qa_delete_user($userid); + + qa_report_event('u_delete', $loginuserid, qa_get_logged_in_handle(), qa_cookie_get(), array( + 'userid' => $userid, + 'handle' => $useraccount['handle'], + )); + + qa_redirect('users'); + } + } + } + + +// Process bonus setting button + + if ( ($loginlevel>=QA_USER_LEVEL_ADMIN) && qa_clicked('dosetbonus') ) { + require_once QA_INCLUDE_DIR.'qa-db-points.php'; + + qa_db_points_set_bonus($userid, (int)qa_post_text('bonus')); + qa_db_points_update_ifuser($userid, null); + qa_redirect(qa_request(), null, null, null, 'activity'); + } + + +// Get information on user references in answers and other stuff need for page + + $pagesize=qa_opt('page_size_user_posts'); + $questions=qa_any_sort_and_dedupe(array_merge($questions, $answerqs, $commentqs, $editqs)); + $questions=array_slice($questions, 0, $pagesize); + $usershtml=qa_userids_handles_html(qa_any_get_userids_handles($questions)); + $usershtml[$userid]=$userhtml; + + +// Prepare content for theme + + $qa_content=qa_content_prepare(true); + + $qa_content['title']=qa_lang_html_sub('profile/user_x', $userhtml); + + if (isset($loginuserid) && !QA_FINAL_EXTERNAL_USERS) + $qa_content['favorite']=qa_favorite_form(QA_ENTITY_USER, $useraccount['userid'], $favorite, + qa_lang_sub($favorite ? 'main/remove_x_favorites' : 'users/add_user_x_favorites', $handle)); + + +// General information about the user, only available if we're using internal user management + + if (!QA_FINAL_EXTERNAL_USERS) { + $qa_content['form_profile']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + + 'style' => 'wide', + + 'fields' => array( + 'avatar' => array( + 'type' => 'image', + 'style' => 'tall', + 'label' => '', + 'html' => qa_get_user_avatar_html($useraccount['flags'], $useraccount['email'], $useraccount['handle'], + $useraccount['avatarblobid'], $useraccount['avatarwidth'], $useraccount['avatarheight'], qa_opt('avatar_profile_size')), + ), + + 'removeavatar' => null, + + 'duration' => array( + 'type' => 'static', + 'label' => qa_lang_html('users/member_for'), + 'value' => qa_html(qa_time_to_string(qa_opt('db_time')-$useraccount['created'])), + ), + + 'level' => array( + 'type' => 'static', + 'label' => qa_lang_html('users/member_type'), + 'tags' => 'NAME="level"', + 'value' => qa_html(qa_user_level_string($useraccount['level'])), + 'note' => (($useraccount['flags'] & QA_USER_FLAGS_USER_BLOCKED) && isset($maxlevelassign)) ? qa_lang_html('users/user_blocked') : '', + ), + ), + ); + + if (empty($qa_content['form_profile']['fields']['avatar']['html'])) + unset($qa_content['form_profile']['fields']['avatar']); + + + // Private message form + + if ( qa_opt('allow_private_messages') && isset($loginuserid) && ($loginuserid!=$userid) && !($useraccount['flags'] & QA_USER_FLAGS_NO_MESSAGES) ) + $qa_content['form_profile']['fields']['level']['value'].=strtr(qa_lang_html('profile/send_private_message'), array( + '^1' => '', + '^2' => '', + )); + + + // Show any extra privileges due to user's level or their points + + $showpermits=array(); + $permitoptions=qa_get_permit_options(); + + foreach ($permitoptions as $permitoption) + if (qa_opt($permitoption) 'static', + 'label' => qa_lang_html('profile/extra_privileges'), + 'value' => qa_html(implode("\n", $showpermits), true), + 'rows' => count($showpermits), + ); + + + // Show email address only if we're an administrator + + if (($loginlevel>=QA_USER_LEVEL_ADMIN) && !qa_user_permit_error()) { + $doconfirms=qa_opt('confirm_user_emails') && ($useraccount['level'] $userediting ? 'text' : 'static', + 'label' => qa_lang_html('users/email_label'), + 'tags' => 'NAME="email"', + 'value' => qa_html(isset($inemail) ? $inemail : $useraccount['email']), + 'error' => qa_html(@$errors['email']), + 'note' => ($doconfirms ? (qa_lang_html($isconfirmed ? 'users/email_confirmed' : 'users/email_not_confirmed').' ') : ''). + qa_lang_html('users/only_shown_admins'), + ); + + } + + + // Show IP addresses and times for last login or write - only if we're a moderator or higher + + if (($loginlevel>=QA_USER_LEVEL_MODERATOR) && !qa_user_permit_error()) { + $qa_content['form_profile']['fields']['lastlogin']=array( + 'type' => 'static', + 'label' => qa_lang_html('users/last_login_label'), + 'value' => + strtr(qa_lang_html('users/x_ago_from_y'), array( + '^1' => qa_time_to_string(qa_opt('db_time')-$useraccount['loggedin']), + '^2' => qa_ip_anchor_html($useraccount['loginip']), + )), + 'note' => qa_lang_html('users/only_shown_moderators'), + ); + + if (isset($useraccount['written'])) + $qa_content['form_profile']['fields']['lastwrite']=array( + 'type' => 'static', + 'label' => qa_lang_html('users/last_write_label'), + 'value' => + strtr(qa_lang_html('users/x_ago_from_y'), array( + '^1' => qa_time_to_string(qa_opt('db_time')-$useraccount['written']), + '^2' => qa_ip_anchor_html($useraccount['writeip']), + )), + 'note' => qa_lang_html('users/only_shown_moderators'), + ); + else + unset($qa_content['form_profile']['fields']['lastwrite']); + + } + + + // Show other profile fields + + $fieldsediting=$fieldseditable && $userediting; + + foreach ($userfields as $userfield) { + if (($userfield['flags'] & QA_FIELD_FLAGS_LINK_URL) && !$fieldsediting) + $valuehtml=qa_url_to_html_link(@$userprofile[$userfield['title']], qa_opt('links_in_new_window')); + + else { + $value=@$inprofile[$userfield['fieldid']]; + if (!isset($value)) + $value=@$userprofile[$userfield['title']]; + + $valuehtml=qa_html($value, (($userfield['flags'] & QA_FIELD_FLAGS_MULTI_LINE) && !$fieldsediting) ? true : false); + } + + $label=trim(qa_user_userfield_label($userfield), ':'); + if (strlen($label)) + $label.=':'; + + $qa_content['form_profile']['fields'][$userfield['title']]=array( + 'type' => $fieldsediting ? 'text' : 'static', + 'label' => qa_html($label), + 'tags' => 'NAME="field_'.$userfield['fieldid'].'"', + 'value' => $valuehtml, + 'error' => qa_html(@$errors[$userfield['fieldid']]), + 'rows' => ($userfield['flags'] & QA_FIELD_FLAGS_MULTI_LINE) ? 8 : null, + ); + } + + + // Edit form or button, if appropriate + + if ($usereditbutton) { + + if ($userediting) { + + if ( + (qa_opt('avatar_allow_gravatar') && ($useraccount['flags'] & QA_USER_FLAGS_SHOW_GRAVATAR)) || + (qa_opt('avatar_allow_upload') && (($useraccount['flags'] & QA_USER_FLAGS_SHOW_AVATAR)) && isset($useraccount['avatarblobid'])) + ) { + $qa_content['form_profile']['fields']['removeavatar']=array( + 'type' => 'checkbox', + 'label' => qa_lang_html('users/remove_avatar'), + 'tags' => 'NAME="removeavatar"', + ); + } + + if (isset($maxlevelassign)) { + $qa_content['form_profile']['fields']['level']['type']='select'; + + $leveloptions=array(QA_USER_LEVEL_BASIC, QA_USER_LEVEL_EXPERT, QA_USER_LEVEL_EDITOR, QA_USER_LEVEL_MODERATOR, QA_USER_LEVEL_ADMIN, QA_USER_LEVEL_SUPER); + + foreach ($leveloptions as $leveloption) + if ($leveloption<=$maxlevelassign) + $qa_content['form_profile']['fields']['level']['options'][$leveloption]=qa_html(qa_user_level_string($leveloption)); + } + + $qa_content['form_profile']['buttons']=array( + 'save' => array( + 'label' => qa_lang_html('users/save_user'), + ), + + 'cancel' => array( + 'tags' => 'NAME="docancel"', + 'label' => qa_lang_html('main/cancel_button'), + ), + ); + + $qa_content['form_profile']['hidden']=array( + 'dosave' => '1', + ); + + } else { + $qa_content['form_profile']['buttons']=array( + 'edit' => array( + 'tags' => 'NAME="doedit"', + 'label' => qa_lang_html('users/edit_user_button'), + ), + ); + + if (isset($maxlevelassign) && ($useraccount['level'] 'NAME="dounblock"', + 'label' => qa_lang_html('users/unblock_user_button'), + ); + + if (count($questions) && !qa_user_permit_error('permit_hide_show')) + $qa_content['form_profile']['buttons']['hideall']=array( + 'tags' => 'NAME="dohideall"', + 'label' => qa_lang_html('users/hide_all_user_button'), + ); + + if ($loginlevel>=QA_USER_LEVEL_ADMIN) + $qa_content['form_profile']['buttons']['delete']=array( + 'tags' => 'NAME="dodelete"', + 'label' => qa_lang_html('users/delete_user_button'), + ); + + } else + $qa_content['form_profile']['buttons']['block']=array( + 'tags' => 'NAME="doblock"', + 'label' => qa_lang_html('users/block_user_button'), + ); + } + } + } + + if (!is_array($qa_content['form_profile']['fields']['removeavatar'])) + unset($qa_content['form_profile']['fields']['removeavatar']); + + $qa_content['raw']['account']=$useraccount; // for plugin layers to access + $qa_content['raw']['profile']=$userprofile; + } + + +// Information about user activity, available also with single sign-on integration + + $qa_content['form_activity']=array( + 'title' => ''.qa_lang_html_sub('profile/activity_by_x', $userhtml).'', + + 'style' => 'wide', + + 'fields' => array( + 'bonus' => array( + 'label' => qa_lang_html('profile/bonus_points'), + 'tags' => 'NAME="bonus"', + 'value' => qa_html($userpoints['bonus']), + 'type' => 'number', + 'note' => qa_lang_html('users/only_shown_admins'), + ), + + 'points' => array( + 'type' => 'static', + 'label' => qa_lang_html('profile/score'), + 'value' => (@$userpoints['points']==1) + ? qa_lang_html_sub('main/1_point', '1', '1') + : qa_lang_html_sub('main/x_points', ''.qa_html(number_format(@$userpoints['points'])).'') + ), + + 'title' => array( + 'type' => 'static', + 'label' => qa_lang_html('profile/title'), + 'value' => qa_get_points_title_html(@$userpoints['points'], qa_get_points_to_titles()), + ), + + 'questions' => array( + 'type' => 'static', + 'label' => qa_lang_html('profile/questions'), + 'value' => ''.qa_html(number_format(@$userpoints['qposts'])).'', + ), + + 'answers' => array( + 'type' => 'static', + 'label' => qa_lang_html('profile/answers'), + 'value' => ''.qa_html(number_format(@$userpoints['aposts'])).'', + ), + ), + ); + + if ($loginlevel>=QA_USER_LEVEL_ADMIN) { + $qa_content['form_activity']['tags']='METHOD="POST" ACTION="'.qa_self_html().'"'; + + $qa_content['form_activity']['buttons']=array( + 'setbonus' => array( + 'tags' => 'NAME="dosetbonus"', + 'label' => qa_lang_html('profile/set_bonus_button'), + ), + ); + + } else + unset($qa_content['form_activity']['fields']['bonus']); + + if (!isset($qa_content['form_activity']['fields']['title']['value'])) + unset($qa_content['form_activity']['fields']['title']); + + if (qa_opt('comment_on_qs') || qa_opt('comment_on_as')) { // only show comment count if comments are enabled + $qa_content['form_activity']['fields']['comments']=array( + 'type' => 'static', + 'label' => qa_lang_html('profile/comments'), + 'value' => ''.qa_html(number_format(@$userpoints['cposts'])).'', + ); + } + + if (qa_opt('voting_on_qs') || qa_opt('voting_on_as')) { // only show vote record if voting is enabled + $votedonvalue=''; + + if (qa_opt('voting_on_qs')) { + $qvotes=@$userpoints['qupvotes']+@$userpoints['qdownvotes']; + + $innervalue=''.number_format($qvotes).''; + $votedonvalue.=($qvotes==1) ? qa_lang_html_sub('main/1_question', $innervalue, '1') + : qa_lang_html_sub('main/x_questions', $innervalue); + + if (qa_opt('voting_on_as')) + $votedonvalue.=', '; + } + + if (qa_opt('voting_on_as')) { + $avotes=@$userpoints['aupvotes']+@$userpoints['adownvotes']; + + $innervalue=''.number_format($avotes).''; + $votedonvalue.=($avotes==1) ? qa_lang_html_sub('main/1_answer', $innervalue, '1') + : qa_lang_html_sub('main/x_answers', $innervalue); + } + + $qa_content['form_activity']['fields']['votedon']=array( + 'type' => 'static', + 'label' => qa_lang_html('profile/voted_on'), + 'value' => $votedonvalue, + ); + + $upvotes=@$userpoints['qupvotes']+@$userpoints['aupvotes']; + $innervalue=''.number_format($upvotes).''; + $votegavevalue=(($upvotes==1) ? qa_lang_html_sub('profile/1_up_vote', $innervalue, '1') : qa_lang_html_sub('profile/x_up_votes', $innervalue)).', '; + + $downvotes=@$userpoints['qdownvotes']+@$userpoints['adownvotes']; + $innervalue=''.number_format($downvotes).''; + $votegavevalue.=($downvotes==1) ? qa_lang_html_sub('profile/1_down_vote', $innervalue, '1') : qa_lang_html_sub('profile/x_down_votes', $innervalue); + + $qa_content['form_activity']['fields']['votegave']=array( + 'type' => 'static', + 'label' => qa_lang_html('profile/gave_out'), + 'value' => $votegavevalue, + ); + + $innervalue=''.number_format(@$userpoints['upvoteds']).''; + $votegotvalue=((@$userpoints['upvoteds']==1) ? qa_lang_html_sub('profile/1_up_vote', $innervalue, '1') + : qa_lang_html_sub('profile/x_up_votes', $innervalue)).', '; + + $innervalue=''.number_format(@$userpoints['downvoteds']).''; + $votegotvalue.=(@$userpoints['downvoteds']==1) ? qa_lang_html_sub('profile/1_down_vote', $innervalue, '1') + : qa_lang_html_sub('profile/x_down_votes', $innervalue); + + $qa_content['form_activity']['fields']['votegot']=array( + 'type' => 'static', + 'label' => qa_lang_html('profile/received'), + 'value' => $votegotvalue, + ); + } + + if (@$userpoints['points']) + $qa_content['form_activity']['fields']['points']['value'].= + qa_lang_html_sub('profile/ranked_x', ''.number_format($userrank).''); + + if (@$userpoints['aselects']) + $qa_content['form_activity']['fields']['questions']['value'].=($userpoints['aselects']==1) + ? qa_lang_html_sub('profile/1_with_best_chosen', '1', '1') + : qa_lang_html_sub('profile/x_with_best_chosen', ''.number_format($userpoints['aselects']).''); + + if (@$userpoints['aselecteds']) + $qa_content['form_activity']['fields']['answers']['value'].=($userpoints['aselecteds']==1) + ? qa_lang_html_sub('profile/1_chosen_as_best', '1', '1') + : qa_lang_html_sub('profile/x_chosen_as_best', ''.number_format($userpoints['aselecteds']).''); + + +// For plugin layers to access + + $qa_content['raw']['userid']=$userid; + $qa_content['raw']['points']=$userpoints; + $qa_content['raw']['rank']=$userrank; + + +// Recent posts by this user + + if ($pagesize>0) { + if (count($questions)) + $qa_content['q_list']['title']=qa_lang_html_sub('profile/recent_activity_by_x', $userhtml); + else + $qa_content['q_list']['title']=qa_lang_html_sub('profile/no_posts_by_x', $userhtml); + + $qa_content['q_list']['form_profile']=array( + 'tags' => 'METHOD="POST" ACTION="'.qa_self_html().'"', + ); + + $qa_content['q_list']['qs']=array(); + + $htmloptions=qa_post_html_defaults('Q'); + $htmloptions['whoview']=false; + $htmloptions['avatarsize']=0; + + foreach ($questions as $question) + $qa_content['q_list']['qs'][]=qa_any_to_q_html_fields($question, $loginuserid, qa_cookie_get(), $usershtml, + null, $htmloptions); + } + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-users-blocked.php b/qa-include/qa-page-users-blocked.php new file mode 100644 index 000000000..3dc52f19e --- /dev/null +++ b/qa-include/qa-page-users-blocked.php @@ -0,0 +1,89 @@ + array(), + 'rows' => ceil(qa_opt('page_size_users')/qa_opt('columns_users')), + 'type' => 'users' + ); + + foreach ($users as $user) { + $qa_content['ranking']['items'][]=array( + 'label' => $usershtml[$user['userid']], + 'score' => qa_html(qa_user_level_string($user['level'])), + ); + } + + $qa_content['navigation']['sub']=qa_users_sub_navigation(); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-users-special.php b/qa-include/qa-page-users-special.php new file mode 100644 index 000000000..f66763341 --- /dev/null +++ b/qa-include/qa-page-users-special.php @@ -0,0 +1,89 @@ + array(), + 'rows' => ceil(qa_opt('page_size_users')/qa_opt('columns_users')), + 'type' => 'users' + ); + + foreach ($users as $user) { + $qa_content['ranking']['items'][]=array( + 'label' => $usershtml[$user['userid']], + 'score' => qa_html(qa_user_level_string($user['level'])), + ); + } + + $qa_content['navigation']['sub']=qa_users_sub_navigation(); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page-users.php b/qa-include/qa-page-users.php new file mode 100644 index 000000000..e9576c5c2 --- /dev/null +++ b/qa-include/qa-page-users.php @@ -0,0 +1,81 @@ + array(), + 'rows' => ceil($pagesize/qa_opt('columns_users')), + 'type' => 'users' + ); + + if (count($users)) { + foreach ($users as $userid => $user) + $qa_content['ranking']['items'][]=array( + 'label' => (QA_FINAL_EXTERNAL_USERS ? '' : (qa_get_user_avatar_html($user['flags'], $user['email'], $user['handle'], + $user['avatarblobid'], $user['avatarwidth'], $user['avatarheight'], qa_opt('avatar_users_size'), true).' ')).$usershtml[$user['userid']], + 'score' => qa_html(number_format($user['points'])), + ); + + } else + $qa_content['title']=qa_lang_html('main/no_active_users'); + + $qa_content['page_links']=qa_html_page_links(qa_request(), $start, $pagesize, $usercount, qa_opt('pages_prev_next')); + + $qa_content['navigation']['sub']=qa_users_sub_navigation(); + + + return $qa_content; + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-page.php b/qa-include/qa-page.php new file mode 100644 index 000000000..754eedbdb --- /dev/null +++ b/qa-include/qa-page.php @@ -0,0 +1,772 @@ +check_login(); + if (qa_is_logged_in()) // stop and reload page if it worked + qa_redirect(qa_request(), $_GET); + } + } + } + + + function qa_check_page_clicks() +/* + React to any of the common buttons on a page for voting, favorites and closing a notice + If the user has Javascript on, these should come through Ajax rather than here. +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_vote_error_html; + + if (qa_is_http_post()) + foreach ($_POST as $field => $value) { + if (strpos($field, 'vote_')===0) { // voting... + @list($dummy, $postid, $vote, $anchor)=explode('_', $field); + + if (isset($postid) && isset($vote)) { + require_once QA_INCLUDE_DIR.'qa-app-votes.php'; + require_once QA_INCLUDE_DIR.'qa-db-selects.php'; + + $userid=qa_get_logged_in_userid(); + + $post=qa_db_select_with_pending(qa_db_full_post_selectspec($userid, $postid)); + $qa_vote_error_html=qa_vote_error_html($post, $vote, $userid, qa_request()); + + if (!$qa_vote_error_html) { + qa_vote_set($post, $userid, qa_get_logged_in_handle(), qa_cookie_get(), $vote); + qa_redirect(qa_request(), $_GET, null, null, $anchor); + } + break; + } + + } elseif (strpos($field, 'favorite_')===0) { // favorites... + @list($dummy, $entitytype, $entityid, $favorite)=explode('_', $field); + + if (isset($entitytype) && isset($entityid) && isset($favorite)) { + require_once QA_INCLUDE_DIR.'qa-app-favorites.php'; + + qa_user_favorite_set(qa_get_logged_in_userid(), qa_get_logged_in_handle(), qa_cookie_get(), $entitytype, $entityid, $favorite); + qa_redirect(qa_request(), $_GET); + } + + } elseif (strpos($field, 'notice_')===0) { // notices... + @list($dummy, $noticeid)=explode('_', $field); + + if (isset($noticeid)) { + if ($noticeid=='visitor') + setcookie('qa_noticed', 1, time()+86400*3650, '/', QA_COOKIE_DOMAIN); + + elseif ($noticeid=='welcome') { + require_once QA_INCLUDE_DIR.'qa-db-users.php'; + qa_db_user_set_flag(qa_get_logged_in_userid(), QA_USER_FLAGS_WELCOME_NOTICE, false); + + } else { + require_once QA_INCLUDE_DIR.'qa-db-notices.php'; + qa_db_usernotice_delete(qa_get_logged_in_userid(), $noticeid); + } + + qa_redirect(qa_request(), $_GET); + } + } + } + } + + + function qa_get_request_content() +/* + Run the appropriate qa-page-*.php file for this request and return back the $qa_content it passed +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + $requestlower=strtolower(qa_request()); + $requestparts=qa_request_parts(); + $firstlower=strtolower($requestparts[0]); + $routing=qa_page_routing(); + + if (isset($routing[$requestlower])) { + qa_set_template($firstlower); + $qa_content=require QA_INCLUDE_DIR.$routing[$requestlower]; + + } elseif (isset($routing[$firstlower.'/'])) { + qa_set_template($firstlower); + $qa_content=require QA_INCLUDE_DIR.$routing[$firstlower.'/']; + + } elseif (is_numeric($requestparts[0])) { + qa_set_template('question'); + $qa_content=require QA_INCLUDE_DIR.'qa-page-question.php'; + + } else { + qa_set_template(strlen($firstlower) ? $firstlower : 'qa'); // will be changed later + $qa_content=require QA_INCLUDE_DIR.'qa-page-default.php'; // handles many other pages, including custom pages and page modules + } + + if ($firstlower=='admin') { + $_COOKIE['qa_admin_last']=$requestlower; // for navigation tab now... + setcookie('qa_admin_last', $_COOKIE['qa_admin_last'], 0, '/', QA_COOKIE_DOMAIN); // ...and in future + } + + return $qa_content; + } + + + function qa_output_content($qa_content) +/* + Output the $qa_content via the theme class after doing some pre-processing, mainly relating to Javascript +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_template; + + $requestlower=strtolower(qa_request()); + + // Set appropriate selected flags for navigation (not done in qa_content_prepare() since it also applies to sub-navigation) + + foreach ($qa_content['navigation'] as $navtype => $navigation) + if (is_array($navigation) && ($navtype!='cat')) + foreach ($navigation as $navprefix => $navlink) + if (substr($requestlower.'$', 0, strlen($navprefix)) == $navprefix) + $qa_content['navigation'][$navtype][$navprefix]['selected']=true; + + // Slide down notifications + + if (!empty($qa_content['notices'])) + foreach ($qa_content['notices'] as $notice) { + $qa_content['script_onloads'][]=array( + "qa_reveal(document.getElementById(".qa_js($notice['id'])."), 'notice');", + ); + } + + // Handle maintenance mode + + if (qa_opt('site_maintenance') && ($requestlower!='login')) { + if (qa_get_logged_in_level()>=QA_USER_LEVEL_ADMIN) { + if (!isset($qa_content['error'])) + $qa_content['error']=strtr(qa_lang_html('admin/maintenance_admin_only'), array( + '^1' => '', + '^2' => '', + )); + + } else { + $qa_content=qa_content_prepare(); + $qa_content['error']=qa_lang_html('misc/site_in_maintenance'); + } + } + + // Handle new users who must confirm their email now + + $userid=qa_get_logged_in_userid(); + if (isset($userid) && (qa_get_logged_in_flags() & QA_USER_FLAGS_MUST_CONFIRM)) + if ( ($requestlower!='confirm') && ($requestlower!='account') ) { + $qa_content=qa_content_prepare(); + $qa_content['title']=qa_lang_html('users/confirm_title'); + $qa_content['error']=strtr(qa_lang_html('users/confirm_required'), array( + '^1' => '', + '^2' => '', + )); + } + + // Combine various Javascript elements in $qa_content into single array for theme layer + + $script=array(''; + + if (isset($qa_content['script_rel'])) { + $uniquerel=array_unique($qa_content['script_rel']); // remove any duplicates + foreach ($uniquerel as $script_rel) + $script[]=''; + } + + if (isset($qa_content['script_src'])) + foreach ($qa_content['script_src'] as $script_src) + $script[]=''; + + $qa_content['script']=$script; + + // Load the appropriate theme class and output the page + + $themeclass=qa_load_theme_class(qa_get_site_theme(), (substr($qa_template, 0, 7)=='custom-') ? 'custom' : $qa_template, $qa_content, qa_request()); + + header('Content-type: '.$qa_content['content_type']); + + $themeclass->doctype(); + $themeclass->html(); + $themeclass->finish(); + } + + + function qa_do_content_stats($qa_content) +/* + Update any statistics required by the fields in $qa_content, and return true if something was done +*/ + { + if (isset($qa_content['inc_views_postid'])) { + require_once QA_INCLUDE_DIR.'qa-db-hotness.php'; + qa_db_hotness_update($qa_content['inc_views_postid'], null, true); + return true; + } + + return false; + } + + +// Other functions which might be called from anywhere + + function qa_page_routing() +/* + Return an array of the default Q2A requests and which qa-page-*.php file implements them + If the key of an element ends in /, it should be used for any request with that key as its prefix +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + return array( + 'account' => 'qa-page-account.php', + 'activity/' => 'qa-page-activity.php', + 'admin/' => 'qa-page-admin-default.php', + 'admin/categories' => 'qa-page-admin-categories.php', + 'admin/flagged' => 'qa-page-admin-flagged.php', + 'admin/hidden' => 'qa-page-admin-hidden.php', + 'admin/layoutwidgets' => 'qa-page-admin-widgets.php', + 'admin/moderate' => 'qa-page-admin-moderate.php', + 'admin/pages' => 'qa-page-admin-pages.php', + 'admin/plugins' => 'qa-page-admin-plugins.php', + 'admin/points' => 'qa-page-admin-points.php', + 'admin/recalc' => 'qa-page-admin-recalc.php', + 'admin/stats' => 'qa-page-admin-stats.php', + 'admin/userfields' => 'qa-page-admin-userfields.php', + 'admin/usertitles' => 'qa-page-admin-usertitles.php', + 'answers/' => 'qa-page-answers.php', + 'ask' => 'qa-page-ask.php', + 'categories/' => 'qa-page-categories.php', + 'comments/' => 'qa-page-comments.php', + 'confirm' => 'qa-page-confirm.php', + 'favorites' => 'qa-page-favorites.php', + 'feedback' => 'qa-page-feedback.php', + 'forgot' => 'qa-page-forgot.php', + 'hot/' => 'qa-page-hot.php', + 'ip/' => 'qa-page-ip.php', + 'login' => 'qa-page-login.php', + 'logout' => 'qa-page-logout.php', + 'message/' => 'qa-page-message.php', + 'questions/' => 'qa-page-questions.php', + 'register' => 'qa-page-register.php', + 'reset' => 'qa-page-reset.php', + 'search' => 'qa-page-search.php', + 'tag/' => 'qa-page-tag.php', + 'tags' => 'qa-page-tags.php', + 'unanswered/' => 'qa-page-unanswered.php', + 'unsubscribe' => 'qa-page-unsubscribe.php', + 'updates' => 'qa-page-updates.php', + 'user/' => 'qa-page-user.php', + 'users' => 'qa-page-users.php', + 'users/blocked' => 'qa-page-users-blocked.php', + 'users/special' => 'qa-page-users-special.php', + ); + } + + + function qa_set_template($template) +/* + Sets the template which should be passed to the theme class, telling it which type of page it's displaying +*/ + { + global $qa_template; + $qa_template=$template; + } + + + function qa_content_prepare($voting=false, $categoryids=null) +/* + Start preparing theme content in global $qa_content variable, with or without $voting support, + in the context of the categories in $categoryids (if not null) +*/ + { + if (qa_to_override(__FUNCTION__)) return qa_call_override(__FUNCTION__, $args=func_get_args()); + + global $qa_template, $qa_vote_error_html; + + if (QA_DEBUG_PERFORMANCE) + qa_usage_mark('control'); + + $request=qa_request(); + $requestlower=qa_request(); + $navpages=qa_db_get_pending_result('navpages'); + $widgets=qa_db_get_pending_result('widgets'); + + if (isset($categoryids) && !is_array($categoryids)) // accept old-style parameter + $categoryids=array($categoryids); + + $lastcategoryid=count($categoryids) ? end($categoryids) : null; + + $qa_content=array( + 'content_type' => 'text/html; charset=utf-8', + + 'site_title' => qa_html(qa_opt('site_title')), + + 'head_lines' => array(), + + 'navigation' => array( + 'user' => array(), + + 'main' => array(), + + 'footer' => array( + 'feedback' => array( + 'url' => qa_path_html('feedback'), + 'label' => qa_lang_html('main/nav_feedback'), + ), + ), + + ), + + 'sidebar' => qa_opt('show_custom_sidebar') ? qa_opt('custom_sidebar') : null, + + 'sidepanel' => qa_opt('show_custom_sidepanel') ? qa_opt('custom_sidepanel') : null, + + 'widgets' => array(), + ); + + if (qa_opt('show_custom_in_head')) + $qa_content['head_lines'][]=qa_opt('custom_in_head'); + + if (qa_opt('show_custom_header')) + $qa_content['body_header']=qa_opt('custom_header'); + + if (qa_opt('show_custom_footer')) + $qa_content['body_footer']=qa_opt('custom_footer'); + + if (isset($categoryids)) + $qa_content['categoryids']=$categoryids; + + foreach ($navpages as $page) + if ($page['nav']=='B') + qa_navigation_add_page($qa_content['navigation']['main'], $page); + + if (qa_opt('nav_home') && qa_opt('show_custom_home')) + $qa_content['navigation']['main']['$']=array( + 'url' => qa_path_html(''), + 'label' => qa_lang_html('main/nav_home'), + ); + + if (qa_opt('nav_activity')) + $qa_content['navigation']['main']['activity']=array( + 'url' => qa_path_html('activity'), + 'label' => qa_lang_html('main/nav_activity'), + ); + + $hascustomhome=qa_has_custom_home(); + + if (qa_opt($hascustomhome ? 'nav_qa_not_home' : 'nav_qa_is_home')) + $qa_content['navigation']['main'][$hascustomhome ? 'qa' : '$']=array( + 'url' => qa_path_html($hascustomhome ? 'qa' : ''), + 'label' => qa_lang_html('main/nav_qa'), + ); + + if (qa_opt('nav_questions')) + $qa_content['navigation']['main']['questions']=array( + 'url' => qa_path_html('questions'), + 'label' => qa_lang_html('main/nav_qs'), + ); + + if (qa_opt('nav_hot')) + $qa_content['navigation']['main']['hot']=array( + 'url' => qa_path_html('hot'), + 'label' => qa_lang_html('main/nav_hot'), + ); + + if (qa_opt('nav_unanswered')) + $qa_content['navigation']['main']['unanswered']=array( + 'url' => qa_path_html('unanswered'), + 'label' => qa_lang_html('main/nav_unanswered'), + ); + + if (qa_using_tags() && qa_opt('nav_tags')) + $qa_content['navigation']['main']['tag']=array( + 'url' => qa_path_html('tags'), + 'label' => qa_lang_html('main/nav_tags'), + ); + + if (qa_using_categories() && qa_opt('nav_categories')) + $qa_content['navigation']['main']['categories']=array( + 'url' => qa_path_html('categories'), + 'label' => qa_lang_html('main/nav_categories'), + ); + + if (qa_opt('nav_users')) + $qa_content['navigation']['main']['user']=array( + 'url' => qa_path_html('users'), + 'label' => qa_lang_html('main/nav_users'), + ); + + if (qa_opt('nav_ask') && (qa_user_permit_error('permit_post_q')!='level')) + $qa_content['navigation']['main']['ask']=array( + 'url' => qa_path_html('ask', (qa_using_categories() && strlen($lastcategoryid)) ? array('cat' => $lastcategoryid) : null), + 'label' => qa_lang_html('main/nav_ask'), + ); + + + if ( + (qa_get_logged_in_level()>=QA_USER_LEVEL_ADMIN) || + (!qa_user_permit_error('permit_moderate')) || + (!qa_user_permit_error('permit_hide_show')) || + (!qa_user_permit_error('permit_delete_hidden')) + ) + $qa_content['navigation']['main']['admin']=array( + 'url' => qa_path_html('admin'), + 'label' => qa_lang_html('main/nav_admin'), + ); + + + $qa_content['search']=array( + 'form_tags' => 'METHOD="GET" ACTION="'.qa_path_html('search').'"', + 'form_extra' => qa_path_form_html('search'), + 'title' => qa_lang_html('main/search_title'), + 'field_tags' => 'NAME="q"', + 'button_label' => qa_lang_html('main/search_button'), + ); + + if (!qa_opt('feedback_enabled')) + unset($qa_content['navigation']['footer']['feedback']); + + foreach ($navpages as $page) + if ( ($page['nav']=='M') || ($page['nav']=='O') || ($page['nav']=='F') ) + qa_navigation_add_page($qa_content['navigation'][($page['nav']=='F') ? 'footer' : 'main'], $page); + + $regioncodes=array( + 'F' => 'full', + 'M' => 'main', + 'S' => 'side', + ); + + $placecodes=array( + 'T' => 'top', + 'H' => 'high', + 'L' => 'low', + 'B' => 'bottom', + ); + + foreach ($widgets as $widget) + if (is_numeric(strpos(','.$widget['tags'].',', ','.$qa_template.',')) || is_numeric(strpos(','.$widget['tags'].',', ',all,'))) { // see if it has been selected for display on this template + $region=@$regioncodes[substr($widget['place'], 0, 1)]; + $place=@$placecodes[substr($widget['place'], 1, 2)]; + + if (isset($region) && isset($place)) { // check region/place codes recognized + $module=qa_load_module('widget', $widget['title']); + + if ( + isset($module) && method_exists($module, 'allow_template') && + $module->allow_template((substr($qa_template, 0, 7)=='custom-') ? 'custom' : $qa_template) && + method_exists($module, 'allow_region') && $module->allow_region($region) && method_exists($module, 'output_widget') + ) + $qa_content['widgets'][$region][$place][]=$module; // if module loaded and happy to be displayed here, tell theme about it + } + } + + $logoshow=qa_opt('logo_show'); + $logourl=qa_opt('logo_url'); + $logowidth=qa_opt('logo_width'); + $logoheight=qa_opt('logo_height'); + + if ($logoshow) + $qa_content['logo']=''. + ''; + else + $qa_content['logo']=''.qa_html(qa_opt('site_title')).''; + + $topath=qa_get('to'); // lets user switch between login and register without losing destination page + + $userlinks=qa_get_login_links(qa_path_to_root(), isset($topath) ? $topath : qa_path($request, $_GET, '')); + + $qa_content['navigation']['user']=array(); + + if (qa_is_logged_in()) { + $qa_content['loggedin']=qa_lang_html_sub_split('main/logged_in_x', QA_FINAL_EXTERNAL_USERS + ? qa_get_logged_in_user_html(qa_get_logged_in_user_cache(), qa_path_to_root(), false) + : qa_get_one_user_html(qa_get_logged_in_handle(), false) + ); + + if (!QA_FINAL_EXTERNAL_USERS) + $qa_content['navigation']['user']['account']=array( + 'url' => qa_path_html('account'), + 'label' => qa_lang_html('main/nav_account'), + ); + + $qa_content['navigation']['user']['updates']=array( + 'url' => qa_path_html('updates'), + 'label' => qa_lang_html('main/nav_updates'), + ); + + if (!empty($userlinks['logout'])) + $qa_content['navigation']['user']['logout']=array( + 'url' => qa_html(@$userlinks['logout']), + 'label' => qa_lang_html('main/nav_logout'), + ); + + if (!QA_FINAL_EXTERNAL_USERS) { + $source=qa_get_logged_in_source(); + + if (strlen($source)) { + $loginmodules=qa_load_modules_with('login', 'match_source'); + + foreach ($loginmodules as $module) + if ($module->match_source($source) && method_exists($module, 'logout_html')) { + ob_start(); + $module->logout_html(qa_path('logout', array(), qa_opt('site_url'))); + $qa_content['navigation']['user']['logout']=array('label' => ob_get_clean()); + } + } + } + + $notices=qa_db_get_pending_result('notices'); + foreach ($notices as $notice) + $qa_content['notices'][]=qa_notice_form($notice['noticeid'], qa_viewer_html($notice['content'], $notice['format']), $notice); + + } else { + $loginmodules=qa_load_modules_with('login', 'login_html'); + + foreach ($loginmodules as $tryname => $module) { + ob_start(); + $module->login_html(isset($topath) ? (qa_opt('site_url').$topath) : qa_path($request, $_GET, qa_opt('site_url')), 'menu'); + $qa_content['navigation']['user'][$tryname]=array('label' => ob_get_clean()); + } + + if (!empty($userlinks['login'])) + $qa_content['navigation']['user']['login']=array( + 'url' => qa_html(@$userlinks['login']), + 'label' => qa_lang_html('main/nav_login'), + ); + + if (!empty($userlinks['register'])) + $qa_content['navigation']['user']['register']=array( + 'url' => qa_html(@$userlinks['register']), + 'label' => qa_lang_html('main/nav_register'), + ); + } + + if (QA_FINAL_EXTERNAL_USERS || !qa_is_logged_in()) { + if (qa_opt('show_notice_visitor') && (!isset($topath)) && (!isset($_COOKIE['qa_noticed']))) + $qa_content['notices'][]=qa_notice_form('visitor', qa_opt('notice_visitor')); + + } else { + setcookie('qa_noticed', 1, time()+86400*3650, '/', QA_COOKIE_DOMAIN); // don't show first-time notice if a user has logged in + + if (qa_opt('show_notice_welcome') && (qa_get_logged_in_flags() & QA_USER_FLAGS_WELCOME_NOTICE) ) + if ( ($requestlower!='confirm') && ($requestlower!='account') ) // let people finish registering in peace + $qa_content['notices'][]=qa_notice_form('welcome', qa_opt('notice_welcome')); + } + + $qa_content['script_rel']=array('qa-content/jquery-1.7.1.min.js'); + $qa_content['script_rel'][]='qa-content/qa-page.js?'.QA_VERSION; + + if ($voting) + $qa_content['error']=@$qa_vote_error_html; + + $qa_content['script_var']=array( + 'qa_root' => qa_path_to_root(), + 'qa_request' => $request, + ); + + return $qa_content; + } + + + function qa_get_start() +/* + Get the start parameter which should be used, as constrained by the setting in qa-config.php +*/ + { + return min(max(0, (int)qa_get('start')), QA_MAX_LIMIT_START); + } + + + function qa_get_state() +/* + Get the state parameter which should be used, as set earlier in qa_load_state() +*/ + { + global $qa_state; + return $qa_state; + } + + +// Below are the steps that actually execute for this file - all the above are function definitions + + qa_report_process_stage('init_page'); + qa_db_connect('qa_page_db_fail_handler'); + + qa_page_queue_pending(); + qa_load_state(); + qa_check_login_modules(); + + if (QA_DEBUG_PERFORMANCE) + qa_usage_mark('setup'); + + qa_check_page_clicks(); + + $qa_content=qa_get_request_content(); + + if (is_array($qa_content)) { + if (QA_DEBUG_PERFORMANCE) + qa_usage_mark('view'); + + qa_output_content($qa_content); + + if (QA_DEBUG_PERFORMANCE) + qa_usage_mark('theme'); + + if (qa_do_content_stats($qa_content)) + if (QA_DEBUG_PERFORMANCE) + qa_usage_mark('stats'); + + if (QA_DEBUG_PERFORMANCE) + qa_usage_output(); + } + + qa_db_disconnect(); + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-search-basic.php b/qa-include/qa-search-basic.php new file mode 100644 index 000000000..df7cb1dbe --- /dev/null +++ b/qa-include/qa-search-basic.php @@ -0,0 +1,150 @@ + $count) + if (isset($wordtoid[$word])) + $contentwordidcounts[$wordtoid[$word]]=$count; + + qa_db_contentwords_add_post_wordidcounts($postid, $type, $questionid, $contentwordidcounts); + + // Add to tag words index + + $tagwordids=qa_array_filter_by_keys($wordtoid, $tagwords); + qa_db_tagwords_add_post_wordids($postid, $tagwordids); + + // Add to whole tags index + + $wholetagids=qa_array_filter_by_keys($wordtoid, $wholetags); + qa_db_posttags_add_post_wordids($postid, $wholetagids); + + // Update counts cached in database (will be skipped if qa_suspend_update_counts() was called + + qa_db_word_titlecount_update($titlewordids); + qa_db_word_contentcount_update(array_keys($contentwordidcounts)); + qa_db_word_tagwordcount_update($tagwordids); + qa_db_word_tagcount_update($wholetagids); + qa_db_tagcount_update(); + } + + + function unindex_post($postid) + { + require_once QA_INCLUDE_DIR.'qa-db-post-update.php'; + + $titlewordids=qa_db_titlewords_get_post_wordids($postid); + qa_db_titlewords_delete_post($postid); + qa_db_word_titlecount_update($titlewordids); + + $contentwordids=qa_db_contentwords_get_post_wordids($postid); + qa_db_contentwords_delete_post($postid); + qa_db_word_contentcount_update($contentwordids); + + $tagwordids=qa_db_tagwords_get_post_wordids($postid); + qa_db_tagwords_delete_post($postid); + qa_db_word_tagwordcount_update($tagwordids); + + $wholetagids=qa_db_posttags_get_post_wordids($postid); + qa_db_posttags_delete_post($postid); + qa_db_word_tagcount_update($wholetagids); + } + + + function index_page($pageid, $title, $content, $format, $text) + { + // for now, the built-in search engine ignores custom pages + } + + + function unindex_page($pageid) + { + // for now, the built-in search engine ignores custom pages + } + + + function process_search($query, $start, $count, $userid, $absoluteurls, $fullcontent) + { + require_once QA_INCLUDE_DIR.'qa-db-selects.php'; + require_once QA_INCLUDE_DIR.'qa-util-string.php'; + + $words=qa_string_to_words($query); + + $questions=qa_db_select_with_pending( + qa_db_search_posts_selectspec($userid, $words, $words, $words, $words, trim($query), $start, $fullcontent, $count) + ); + + $results=array(); + + foreach ($questions as $question) { + qa_search_set_max_match($question, $type, $postid); // to link straight to best part + + $results[]=array( + 'question' => $question, + 'match_type' => $type, + 'match_postid' => $postid, + ); + } + + return $results; + } + + } + + +/* + Omit PHP closing tag to help avoid accidental output +*/ \ No newline at end of file diff --git a/qa-include/qa-theme-base.php b/qa-include/qa-theme-base.php new file mode 100644 index 000000000..2a34ed17d --- /dev/null +++ b/qa-include/qa-theme-base.php @@ -0,0 +1,1954 @@ +template=$template; + $this->content=$content; + $this->rooturl=$rooturl; + $this->request=$request; + } + + function output_array($elements) + /* + Output each element in $elements on a separate line, with automatic HTML indenting. + This should be passed markup which uses the form for unpaired tags, to help keep + track of indenting, although its actual output converts these to for W3C validation + */ + { + foreach ($elements as $element) { + $delta=substr_count($element, '<')-substr_count($element, ''); + + if ($delta<0) + $this->indent+=$delta; + + echo str_repeat("\t", max(0, $this->indent)).str_replace('/>', '>', $element)."\n"; + + if ($delta>0) + $this->indent+=$delta; + + $this->lines++; + } + } + + + function output() // other parameters picked up via func_get_args() + /* + Output each passed parameter on a separate line - see output_array() comments + */ + { + $this->output_array(func_get_args()); + } + + + function output_raw($html) + /* + Output $html at the current indent level, but don't change indent level based on the markup within. + Useful for user-entered HTML which is unlikely to follow the rules we need to track indenting + */ + { + if (strlen($html)) + echo str_repeat("\t", max(0, $this->indent)).$html."\n"; + } + + + function output_split($parts, $class, $outertag='SPAN', $innertag='SPAN', $extraclass=null) + /* + Output the three elements ['prefix'], ['data'] and ['suffix'] of $parts (if they're defined), + with appropriate CSS classes based on $class, using $outertag and $innertag in the markup. + */ + { + if (empty($parts) && ($outertag!='TD')) + return; + + $this->output( + '<'.$outertag.' CLASS="'.$class.(isset($extraclass) ? (' '.$extraclass) : '').'">', + (strlen(@$parts['prefix']) ? ('<'.$innertag.' CLASS="'.$class.'-pad">'.$parts['prefix'].'') : ''). + (strlen(@$parts['data']) ? ('<'.$innertag.' CLASS="'.$class.'-data">'.$parts['data'].'') : ''). + (strlen(@$parts['suffix']) ? ('<'.$innertag.' CLASS="'.$class.'-pad">'.$parts['suffix'].'') : ''), + '' + ); + } + + + function set_context($key, $value) + /* + Set some context, which be accessed via $this->context for a function to know where it's being used on the page + */ + { + $this->context[$key]=$value; + } + + + function clear_context($key) + /* + Clear some context (used at the end of the appropriate loop) + */ + { + unset($this->context[$key]); + } + + + function widgets($region, $place) + /* + Output the widgets (as provided in $this->content['widgets']) for $region and $place + */ + { + if (count(@$this->content['widgets'][$region][$place])) { + $this->output('
    '); + + foreach ($this->content['widgets'][$region][$place] as $module) { + $this->output('
    '); + $module->output_widget($region, $place, $this, $this->template, $this->request, $this->content); + $this->output('
    '); + } + + $this->output('
    ', ''); + } + } + + + function finish() + /* + Post-output cleanup. For now, check that the indenting ended right, and if not, output a warning in an HTML comment + */ + { + if ($this->indent) + echo "\n"; + } + + + // From here on, we have a large number of class methods which output particular pieces of HTML markup + // The calling chain is initiated from qa-page.php, or qa-ajax-*.php for refreshing parts of a page, + // For most HTML elements, the name of the function is similar to the element's CSS class, for example: + // search() outputs ');return o;},getHolderElement:function(){var m=this._.holder;if(!m){if(this.forceIFrame||this.css.length){var n=this.document.getById(this.id+'_frame'),o=n.getParent(),p=o.getAttribute('dir'),q=o.getParent().getAttribute('class'),r=o.getParent().getAttribute('lang'),s=n.getFrameDocument();b.iOS&&o.setStyles({overflow:'scroll','-webkit-overflow-scrolling':'touch'});var t=e.addFunction(e.bind(function(w){this.isLoaded=true;if(this.onLoad)this.onLoad();},this)),u=''+''+''+''+''+e.buildStyleHtml(this.css)+'';s.write(u);var v=s.getWindow();v.$.CKEDITOR=a;s.on('key'+(b.opera?'press':'down'),function(w){var z=this;var x=w.data.getKeystroke(),y=z.document.getById(z.id).getAttribute('dir');if(z._.onKeyDown&&z._.onKeyDown(x)===false){w.data.preventDefault();return;}if(x==27||x==(y=='rtl'?39:37))if(z.onEscape&&z.onEscape(x)===false)w.data.preventDefault();},this);m=s.getBody();m.unselectable();b.air&&e.callFunction(t);}else m=this.document.getById(this.id);this._.holder=m;}return m;},addBlock:function(m,n){var o=this;n=o._.blocks[m]=n instanceof k.panel.block?n:new k.panel.block(o.getHolderElement(),n); +if(!o._.currentBlock)o.showBlock(m);return n;},getBlock:function(m){return this._.blocks[m];},showBlock:function(m){var r=this;var n=r._.blocks,o=n[m],p=r._.currentBlock,q=r.forceIFrame?r.document.getById(r.id+'_frame'):r._.holder;q.getParent().getParent().disableContextMenu();if(p){q.removeAttributes(p.attributes);p.hide();}r._.currentBlock=o;q.setAttributes(o.attributes);a.fire('ariaWidget',q);o._.focusIndex=-1;r._.onKeyDown=o.onKeyDown&&e.bind(o.onKeyDown,o);o.show();return o;},destroy:function(){this.element&&this.element.remove();}};k.panel.block=e.createClass({$:function(m,n){var o=this;o.element=m.append(m.getDocument().createElement('div',{attributes:{tabIndex:-1,'class':'cke_panel_block',role:'presentation'},styles:{display:'none'}}));if(n)e.extend(o,n);if(!o.attributes.title)o.attributes.title=o.attributes['aria-label'];o.keys={};o._.focusIndex=-1;o.element.disableContextMenu();},_:{markItem:function(m){var p=this;if(m==-1)return;var n=p.element.getElementsByTag('a'),o=n.getItem(p._.focusIndex=m);if(b.webkit||b.opera)o.getDocument().getWindow().focus();o.focus();p.onMark&&p.onMark(o);}},proto:{show:function(){this.element.setStyle('display','');},hide:function(){var m=this;if(!m.onHide||m.onHide.call(m)!==true)m.element.setStyle('display','none');},onKeyDown:function(m){var r=this;var n=r.keys[m];switch(n){case 'next':var o=r._.focusIndex,p=r.element.getElementsByTag('a'),q;while(q=p.getItem(++o)){if(q.getAttribute('_cke_focus')&&q.$.offsetWidth){r._.focusIndex=o;q.focus();break;}}return false;case 'prev':o=r._.focusIndex;p=r.element.getElementsByTag('a');while(o>0&&(q=p.getItem(--o))){if(q.getAttribute('_cke_focus')&&q.$.offsetWidth){r._.focusIndex=o;q.focus();break;}}return false;case 'click':case 'mouseup':o=r._.focusIndex;q=o>=0&&r.element.getElementsByTag('a').getItem(o);if(q)q.$[n]?q.$[n]():q.$['on'+n]();return false;}return true;}}});j.add('listblock',{requires:['panel'],onLoad:function(){k.panel.prototype.addListBlock=function(m,n){return this.addBlock(m,new k.listBlock(this.getHolderElement(),n));};k.listBlock=e.createClass({base:k.panel.block,$:function(m,n){var q=this;n=n||{};var o=n.attributes||(n.attributes={});(q.multiSelect=!!n.multiSelect)&&(o['aria-multiselectable']=true);!o.role&&(o.role='listbox');q.base.apply(q,arguments);var p=q.keys;p[40]='next';p[9]='next';p[38]='prev';p[2228224+9]='prev';p[32]=c?'mouseup':'click';c&&(p[13]='mouseup');q._.pendingHtml=[];q._.items={};q._.groups={};},_:{close:function(){if(this._.started){this._.pendingHtml.push(''); +delete this._.started;}},getClick:function(){if(!this._.click)this._.click=e.addFunction(function(m){var o=this;var n=true;if(o.multiSelect)n=o.toggle(m);else o.mark(m);if(o.onClick)o.onClick(m,n);},this);return this._.click;}},proto:{add:function(m,n,o){var r=this;var p=r._.pendingHtml,q=e.getNextId();if(!r._.started){p.push('