Importing project into Git

This project lived only on the server without version control. This is now the starting point for the repository.
This commit is contained in:
Emi
2023-05-23 20:03:24 +02:00
commit 07ec659385
1190 changed files with 140706 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
qa-config.php

9
CHANGELOG.html Normal file
View File

@@ -0,0 +1,9 @@
<html>
<head>
<meta http-equiv="refresh" content="2;url=http://www.question2answer.org/versions.php">
</head>
<body>
Redirecting... if nothing happens, <a href="http://www.question2answer.org/versions.php">click here</a>.
</body>
</html>

41
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,41 @@
# How to contribute
As of version 1.6.3, all development of [Question2Answer](http://www.question2answer.org/) will take place through GitHub. Bug reports and pull requests are encouraged, provided they follow these guidelines.
## Bug reports (issues)
If you find a bug (error) with Question2Answer, please [submit an issue here](https://github.com/q2a/question2answer/issues). Be as descriptive as possible: include exactly what you did to make the bug appear, what you expect to happen, and what happened instead. Also include your PHP version and MySQL version. Remember to check for similar issues already reported.
If you think you've found a security issue, you can responsibly disclose it to us using the [contact form here](http://www.question2answer.org/feedback.php).
Note that general troubleshooting issues such as installation or how to use a feature should continue to be asked on the [Question2Answer Q&A](http://www.question2answer.org/qa/).
## Pull requests
If you have found the cause of the bug in the Q2A code, you can submit the patch back to the Q2A repository. Create a fork of the repo, make the changes in your fork, then submit a pull request. Bug fix pull requests must be targeted to the **`bugfix`** branch. PRs for new features or large code changes must be made to the **`dev`** branch.
If you wish to implement a feature, you should start a discussion on the [Question2Answer Q&A][QA] first. We welcome all ideas but they may not be appropriate for the Q2A core. Consider whether your idea could be developed as a plugin.
## Coding style
From 1.7 onwards a new coding style has been implemented that is more in line with other projects. All PHP code should use these guidelines:
- PHP code should start with `<?php` (almost always the very first line). The closing tag `?>` should be omitted to avoid accidental whitespace output.
- PHP files should use UTF-8 encoding without BOM (this is usually default in most text editors).
- Trailing whitespace (tabs or spaces at the end of lines) should not be present. Any advanced text editor should be able to do this automatically when saving. (For Sublime Text you can add the option `"trim_trailing_white_space_on_save": true` to your preferences. In Notepad++ you can press Alt+Shift+S.)
- Use tabs for indenting. Each file should start at level 0 (i.e. no indentation).
- Functions should use a DocBlock-style comment.
- Operators (`=`, `+` etc) should have a space either side.
- Control structure keywords (`if`, `else`, `foreach` etc) should have a space between them and the opening parenthesis.
- Opening braces for classes and functions should be on the next line.
- Opening braces for control structures should be on the same line. All control structures should use braces.
If in doubt, follow the style of the surrounding code. Code examples can be found in the [Q2A docs here](http://docs.question2answer.org/contribute/).
## Documentation
Please see the repository [q2a.github.io](https://github.com/q2a/q2a.github.io/) which automatically produces the documentation website [docs.question2answer.org](http://docs.question2answer.org/).

9
LICENSE.html Normal file
View File

@@ -0,0 +1,9 @@
<html>
<head>
<meta http-equiv="refresh" content="2;url=http://www.question2answer.org/license.php">
</head>
<body>
Redirecting... if nothing happens, <a href="http://www.question2answer.org/license.php">click here</a>.
</body>
</html>

62
Q2A-README.md Normal file
View File

@@ -0,0 +1,62 @@
Question2Answer
-----------------------------
[![Build Status](https://travis-ci.org/q2a/question2answer.png?branch=dev)](https://travis-ci.org/q2a/question2answer/branches)
[Question2Answer][Q2A] (Q2A) is a popular free open source Q&A platform for PHP/MySQL, used by over 20,898 [sites] in 40 languages.
Q2A is highly customisable with many awesome features:
- Asking and answering questions (duh!)
- Voting, comments, best answer selection, follow-on and closed questions.
- Complete user management including points-based reputation management.
- Create experts, editors, moderators and admins.
- Fast integrated search engine, plus checking for similar questions when asking.
- Categories (up to 4 levels deep) and/or tagging.
- Easy styling with CSS themes.
- Supports translation into any language.
- Custom sidebar, widgets, pages and links.
- SEO features such as neat URLs, microformats and XML Sitemaps.
- RSS, email notifications and personal news feeds.
- User avatars (or Gravatar) and custom fields.
- Private messages and public wall posts.
- Log in via Facebook or others (using plugins).
- Out-of-the-box WordPress 3+ integration.
- Out-of-the-box Joomla! 3.0+ integration (in conjunction with a Joomla! extension).
- Custom single sign-on support for other sites.
- PHP/MySQL scalable to millions of users and posts.
- Safe from XSS, CSRF and SQL injection attacks.
- Beat spam with captchas, rate-limiting, moderation and/or flagging.
- Block users, IP addresses, and censor words
Q2A also features an extensive plugin system:
- Modify the HTML output for a page with *layers*.
- Add custom pages to a Q2A site with *page modules*.
- Add extra content in various places with *widget modules*.
- Allow login via an external identity provider such as Facebook with *login modules*.
- Integrate WYSIWYG or other text editors with *editor/viewer modules*.
- Do something when certain actions take place with *event modules*.
- Validate and/or modify many types of user input with *filter modules*.
- Implement a custom search engine with *search modules*.
- Add extra spam protection with *captcha modules*.
- Extend many core Q2A functions using *function overrides*.
----------
As of version 1.6.3, all development is taking place through GitHub. The collaborative development process is being managed by [Scott Vivian][1]. (Note that official releases are still distributed via the [Q2A website][Q2A].)
Please read the [contributing page][2] for more information on how to get involved.
Thanks and enjoy!
Gideon
[Q2A]: http://www.question2answer.org/
[1]: http://www.question2answer.org/qa/user/Scott
[2]: https://github.com/q2a/question2answer/blob/master/CONTRIBUTING.md
[sites]: http://www.question2answer.org/sites.php

9
README.html Normal file
View File

@@ -0,0 +1,9 @@
<html>
<head>
<meta http-equiv="refresh" content="2;url=http://www.question2answer.org/install.php">
</head>
<body>
Redirecting... if nothing happens, <a href="http://www.question2answer.org/install.php">click here</a>.
</body>
</html>

4
README.md Normal file
View File

@@ -0,0 +1,4 @@
# Godot's Questions2Answers page
This is the repo for the page https://ask.godotengine.org/ using the [Questions2Answers](http://www.question2answer.org/) platform to create a Q&A site for the Godot Engine.

1
VERSION.txt Normal file
View File

@@ -0,0 +1 @@
1.8.5

1
highlightjs/highlight.gdscript.min.js vendored Normal file
View File

@@ -0,0 +1 @@
hljs.registerLanguage("gdscript",function(){"use strict";var e=e||{};function r(e){return{aliases:["godot","gdscript"],keywords:{keyword:"and in not or self void as assert breakpoint class class_name extends is func setget signal tool yield const enum export onready static var break continue if elif else for pass return match while remote sync master puppet remotesync mastersync puppetsync",built_in:"Color8 ColorN abs acos asin atan atan2 bytes2var cartesian2polar ceil char clamp convert cos cosh db2linear decimals dectime deg2rad dict2inst ease exp floor fmod fposmod funcref get_stack hash inst2dict instance_from_id inverse_lerp is_equal_approx is_inf is_instance_valid is_nan is_zero_approx len lerp lerp_angle linear2db load log max min move_toward nearest_po2 ord parse_json polar2cartesian posmod pow preload print_stack push_error push_warning rad2deg rand_range rand_seed randf randi randomize range_lerp round seed sign sin sinh smoothstep sqrt step_decimals stepify str str2var tan tanh to_json type_exists typeof validate_json var2bytes var2str weakref wrapf wrapi bool int float String NodePath Vector2 Rect2 Transform2D Vector3 Rect3 Plane Quat Basis Transform Color RID Object NodePath Dictionary Array PoolByteArray PoolIntArray PoolRealArray PoolStringArray PoolVector2Array PoolVector3Array PoolColorArray",literal:"true false null"},contains:[e.NUMBER_MODE,e.HASH_COMMENT_MODE,{className:"comment",begin:/"""/,end:/"""/},e.QUOTE_STRING_MODE,{variants:[{className:"function",beginKeywords:"func"},{className:"class",beginKeywords:"class"}],end:/:/,contains:[e.UNDERSCORE_TITLE_MODE]}]}}return e.exports=function(e){e.registerLanguage("gdscript",r)},e.exports.definer=r,e.exports.definer||e.exports}());

Binary file not shown.

1296
highlightjs/highlight.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -0,0 +1 @@
.hljs{display:block;overflow-x:auto;padding:.5em;background:#282b2e}.hljs-keyword,.hljs-literal,.hljs-selector-id,.hljs-selector-tag{color:#93c763}.hljs-number{color:#ffcd22}.hljs{color:#e0e2e4}.hljs-attribute{color:#668bb0}.hljs-class .hljs-title,.hljs-code,.hljs-section{color:#fff}.hljs-link,.hljs-regexp{color:#d39745}.hljs-meta{color:#557182}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-emphasis,.hljs-name,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-subst,.hljs-tag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable{color:#8cbbad}.hljs-string,.hljs-symbol{color:#ec7600}.hljs-comment,.hljs-deletion,.hljs-quote{color:#818e96}.hljs-selector-class{color:#a082bd}.hljs-doctag,.hljs-keyword,.hljs-literal,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-strong,.hljs-title,.hljs-type{font-weight:700}

Binary file not shown.

27
index.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
File: index.php
Description: A stub that only sets up the Q2A root and includes qa-index.php
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
*/
// Set base path here so this works with symbolic links for multiple installations
define('QA_BASE_DIR', dirname(empty($_SERVER['SCRIPT_FILENAME']) ? __FILE__ : $_SERVER['SCRIPT_FILENAME']) . '/');
require 'qa-include/qa-index.php';

1
qa-cache/.htaccess Normal file
View File

@@ -0,0 +1 @@
Deny from all

202
qa-config-example.php Normal file
View File

@@ -0,0 +1,202 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
File: qa-config-example.php
Description: After renaming, use this to set up database details and other stuff
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
*/
/*
======================================================================
THE 4 DEFINITIONS BELOW ARE REQUIRED AND MUST BE SET BEFORE USING!
======================================================================
For QA_MYSQL_HOSTNAME, try '127.0.0.1' or 'localhost' if MySQL is on the same server.
For persistent connections, set the QA_PERSISTENT_CONN_DB at the bottom of this file; do NOT
prepend the hostname with 'p:'.
To use a non-default port, add the following line to the list of defines, with the appropriate port number:
define('QA_MYSQL_PORT', '3306');
*/
define('QA_MYSQL_HOSTNAME', '127.0.0.1');
define('QA_MYSQL_USERNAME', 'your-mysql-username');
define('QA_MYSQL_PASSWORD', 'your-mysql-password');
define('QA_MYSQL_DATABASE', 'your-mysql-db-name');
/*
Ultra-concise installation instructions:
1. Create a MySQL database.
2. Create a MySQL user with full permissions for that database.
3. Rename this file to qa-config.php.
4. Set the above four definitions and save.
5. Place all the Question2Answer files on your server.
6. Open the appropriate URL, and follow the instructions.
More detailed installation instructions here: http://www.question2answer.org/
*/
/*
======================================================================
OPTIONAL CONSTANT DEFINITIONS, INCLUDING SUPPORT FOR SINGLE SIGN-ON
======================================================================
QA_MYSQL_TABLE_PREFIX will be added to table names, to allow multiple datasets in a single
MySQL database, or to include the Question2Answer tables in an existing MySQL database.
*/
define('QA_MYSQL_TABLE_PREFIX', 'qa_');
/*
If you wish, you can define QA_MYSQL_USERS_PREFIX separately from QA_MYSQL_TABLE_PREFIX.
If so, tables containing information about user accounts (not including users' activity and points)
get the prefix of QA_MYSQL_TABLE_PREFIX. This allows multiple Q2A sites to have shared logins
and users, but separate posts and activity.
If you have installed question2answer with default "qa_" prefix and want to setup a second
installation, you define the QA_MYSQL_USERS_PREFIX as "qa_" so this new installation
can access the same database as the first installation.
define('QA_MYSQL_USERS_PREFIX', 'sharedusers_');
*/
/*
If you wish, you can define QA_BLOBS_DIRECTORY to store BLOBs (binary large objects) such
as avatars and uploaded files on disk, rather than in the database. If so this directory
must be writable by the web server process - on Unix/Linux use chown/chmod as appropriate.
Note than if multiple Q2A sites are using QA_MYSQL_USERS_PREFIX to share users, they must
also have the same value for QA_BLOBS_DIRECTORY.
If there are already some BLOBs stored in the database from previous uploads, click the
'Move BLOBs to disk' button in the 'Stats' section of the admin panel to move them to disk.
define('QA_BLOBS_DIRECTORY', '/path/to/writable_blobs_directory/');
*/
/*
If you wish to use file-based caching, you must define QA_CACHE_DIRECTORY to store the cache
files. The directory must be writable by the web server. For maximum security it's STRONGLY
recommended to place the folder outside of the web root (so they can never be accessed via a
web browser).
define('QA_CACHE_DIRECTORY', '/path/to/writable_cache_directory/');
*/
/*
If you wish, you can define QA_COOKIE_DOMAIN so that any cookies created by Q2A are assigned
to a specific domain name, instead of the full domain name of the request by default. This is
useful if you're running multiple Q2A sites on subdomains with a shared user base.
define('QA_COOKIE_DOMAIN', '.example.com'); // be sure to keep the leading period
*/
/*
If you wish, you can define an array $QA_CONST_PATH_MAP to modify the URLs used in your Q2A site.
The key of each array element should be the standard part of the path, e.g. 'questions',
and the value should be the replacement for that standard part, e.g. 'topics'. If you edit this
file in UTF-8 encoding you can also use non-ASCII characters in these URLs.
$QA_CONST_PATH_MAP = array(
'questions' => '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');
*/
/*
Out-of-the-box Joomla! 3.x integration - to integrate with your Joomla! site, define
QA_JOOMLA_INTEGRATE_PATH. as the full path to the Joomla! directory. If your Q2A
site is a subdirectory of your main Joomla site (recommended), you can specify
dirname(__DIR__) rather than the full path.
With this set, you do not need to set the QA_MYSQL_* constants above since these
will be taken from Joomla automatically. See online documentation for more details.
define('QA_JOOMLA_INTEGRATE_PATH', dirname(__DIR__));
*/
/*
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, which
will increase the performance of your site (if the user's browser indicates this is supported).
This is best done at the server level if possible, but many hosts don't provide server access.
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_DISTANT_DB to false 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.
Otherwise, set it 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.
The option QA_OPTIMIZE_LOCAL_DB is no longer used, since QA_OPTIMIZE_DISTANT_DB covers our uses.
Set QA_PERSISTENT_CONN_DB to true to use persistent database connections. Requires PHP 5.3.
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', false);
define('QA_MAX_LIMIT_START', 19999);
define('QA_IGNORED_WORDS_FREQ', 10000);
define('QA_ALLOW_UNINDEXED_QUERIES', 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!
*/

5
qa-content/jquery-1.11.3.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
qa-content/jquery-3.5.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

161
qa-content/qa-admin.js Normal file
View File

@@ -0,0 +1,161 @@
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
File: qa-content/qa-admin.js
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) {
var recalcCode = elem.form.elements.code_recalc ? elem.form.elements.code_recalc.value : elem.form.elements.code.value;
qa_ajax_post(
'recalc',
{state: state, code: recalcCode},
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[1];
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 = {entityid: p[1], action: p[2]};
params.code = target.form.elements.code.value;
qa_ajax_post('click_admin', params,
function(lines) {
if (lines[0] == '1')
qa_conceal(document.getElementById('p' + p[1]), 'admin');
else if (lines[0] == '0') {
alert(lines[1]);
qa_hide_waiting(target);
} else
qa_ajax_error();
}
);
qa_show_waiting_after(target, false);
return false;
}
function qa_version_check(uri, version, elem, isCore)
{
var params = {uri: uri, version: version, isCore: isCore};
qa_ajax_post('version', params,
function(lines) {
if (lines[0] == '1')
document.getElementById(elem).innerHTML = lines[1];
}
);
}
function qa_get_enabled_plugins_hashes()
{
var hashes = [];
$('[id^=plugin_enabled]:checked').each(
function(idx, elem) {
hashes.push(elem.id.replace("plugin_enabled_", ""));
}
);
$('[name=enabled_plugins_hashes]').val(hashes.join(';'));
}

295
qa-content/qa-ask.js Normal file
View File

@@ -0,0 +1,295 @@
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
File: qa-content/qa-ask.js
Description: THIS FILE HAS BEEN DEPRECATED IN FAVOUR OF qa-global.js
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(true);
}
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();
});
qa_show_waiting_after(document.getElementById('similar'), true);
}
function qa_html_unescape(html)
{
return html.replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>');
}
function qa_html_escape(text)
{
return text.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
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 = qa_html_unescape(link.innerHTML.replace(/<[^>]*>/g, ''));
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 html = '';
var completed = false;
// first try to auto-complete
if (qa_tags_complete && !skipcomplete) {
var parts = qa_tag_typed_parts(elem);
if (parts.typed) {
html = qa_tags_to_html((qa_html_unescape(qa_tags_examples + ',' + qa_tags_complete)).split(','), parts.typed.toLowerCase());
completed = html ? true : false;
}
}
// otherwise show examples
if (qa_tags_examples && !completed)
html = qa_tags_to_html((qa_html_unescape(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 < tags.length; i++) {
var tag = tags[i];
var taglc = tag.toLowerCase();
if (!tagseen[taglc]) {
tagseen[taglc] = true;
if ((!matchlc) || (taglc.indexOf(matchlc) >= 0)) { // match if necessary
if (matchlc) { // if matching, show appropriate part in bold
var matchstart = taglc.indexOf(matchlc);
var matchend = matchstart + matchlc.length;
inner = '<span style="font-weight:normal;">' + qa_html_escape(tag.substring(0, matchstart)) + '<b>' +
qa_html_escape(tag.substring(matchstart, matchend)) + '</b>' + qa_html_escape(tag.substring(matchend)) + '</span>';
} else // otherwise show as-is
inner = qa_html_escape(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 (l < startval.length && startval[l].length) {
var val = startval[l];
for (var j = 0; j < elem.options.length; j++)
if (elem.options[j].value == val)
elem.selectedIndex = j;
} else
var val = elem.options[elem.selectedIndex].value;
} else
val = '';
if (elem.qa_last_sel !== val) {
elem.qa_last_sel = val;
var subelem = document.getElementById(idprefix + '_' + l + '_sub');
if (subelem)
subelem.parentNode.removeChild(subelem);
if (val.length || (l == 0)) {
subelem = elem.parentNode.insertBefore(document.createElement('span'), elem.nextSibling);
subelem.id = idprefix + '_' + l + '_sub';
qa_show_waiting_after(subelem, true);
qa_ajax_post('category', {categoryid: val},
(function(elem, l) {
return function(lines) {
var subelem = document.getElementById(idprefix + '_' + l + '_sub');
if (subelem)
subelem.parentNode.removeChild(subelem);
if (lines[0] == '1') {
elem.qa_cat_desc = lines[1];
var addedoption = false;
if (lines.length > 2) {
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 < lines.length; i++) {
var parts = lines[i].split('/');
if (String(qa_cat_exclude).length && (String(qa_cat_exclude) == parts[0]))
continue;
newelem.options[newelem.options.length] = new Option(parts.slice(1).join('/'), parts[0]);
addedoption = true;
}
if (addedoption) {
subelem.appendChild(newelem);
qa_category_select(idprefix, startpath);
}
if (l == 0)
elem.style.display = 'none';
}
if (!addedoption)
set_category_description(idprefix);
} else if (lines[0] == '0')
alert(lines[1]);
else
qa_ajax_error();
}
})(elem, l)
);
setdescnow = false;
}
break;
}
}
}
if (setdescnow)
set_category_description(idprefix);
}
function set_category_description(idprefix)
{
var n = document.getElementById(idprefix + '_note');
if (n) {
desc = '';
for (var l = 1; l <= qa_cat_maxdepth; l++) {
var elem = document.getElementById(idprefix + '_' + l);
if (elem && elem.options[elem.selectedIndex].value.length)
desc = elem.qa_cat_desc;
}
n.innerHTML = desc;
}
}

834
qa-content/qa-global.js Normal file
View File

@@ -0,0 +1,834 @@
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Common Javascript for Q2A pages including posting and AJAX
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
*/
// General page functions
function qa_reveal(elem, type, callback)
{
if (elem)
$(elem).slideDown(400, callback);
}
function qa_conceal(elem, type, callback)
{
if (elem)
$(elem).slideUp(400);
}
function qa_set_inner_html(elem, type, html)
{
if (elem)
elem.innerHTML = html;
}
function qa_set_outer_html(elem, type, html)
{
if (elem) {
var e = document.createElement('div');
e.innerHTML = html;
elem.parentNode.replaceChild(e.firstChild, elem);
}
}
function qa_show_waiting_after(elem, inside)
{
if (elem && !elem.qa_waiting_shown) {
var w = document.getElementById('qa-waiting-template');
if (w) {
var c = w.cloneNode(true);
c.id = null;
if (inside)
elem.insertBefore(c, null);
else
elem.parentNode.insertBefore(c, elem.nextSibling);
elem.qa_waiting_shown = c;
}
}
}
function qa_hide_waiting(elem)
{
var c = elem.qa_waiting_shown;
if (c) {
c.parentNode.removeChild(c);
elem.qa_waiting_shown = null;
}
}
function qa_vote_click(elem)
{
var ens = elem.name.split('_');
var postid = ens[1];
var vote = parseInt(ens[2]);
var code = elem.form.elements.code.value;
var anchor = ens[3];
qa_ajax_post('vote', {postid: postid, vote: vote, code: code},
function(lines) {
if (lines[0] == '1') {
qa_set_inner_html(document.getElementById('voting_' + postid), 'voting', lines.slice(1).join("\n"));
} else if (lines[0] == '0') {
var mess = document.getElementById('errorbox');
if (!mess) {
mess = document.createElement('div');
mess.id = 'errorbox';
mess.className = 'qa-error';
mess.innerHTML = lines[1];
mess.style.display = 'none';
}
var postelem = document.getElementById(anchor);
var e = postelem.parentNode.insertBefore(mess, postelem);
qa_reveal(e);
} else
qa_ajax_error();
}
);
return false;
}
function qa_notice_click(elem)
{
var ens = elem.name.split('_');
var code = elem.form.elements.code.value;
qa_ajax_post('notice', {noticeid: ens[1], code: code},
function(lines) {
if (lines[0] == '1')
qa_conceal(document.getElementById('notice_' + ens[1]), 'notice');
else if (lines[0] == '0')
alert(lines[1]);
else
qa_ajax_error();
}
);
return false;
}
function qa_favorite_click(elem)
{
var ens = elem.name.split('_');
var code = elem.form.elements.code.value;
qa_ajax_post('favorite', {entitytype: ens[1], entityid: ens[2], favorite: parseInt(ens[3]), code: code},
function(lines) {
if (lines[0] == '1')
qa_set_inner_html(document.getElementById('favoriting'), 'favoriting', lines.slice(1).join("\n"));
else if (lines[0] == '0') {
alert(lines[1]);
qa_hide_waiting(elem);
} else
qa_ajax_error();
}
);
qa_show_waiting_after(elem, false);
return false;
}
function qa_ajax_post(operation, params, callback)
{
$.extend(params, {qa: 'ajax', qa_operation: operation, qa_root: qa_root, qa_request: qa_request});
$.post(qa_root, params, function(response) {
var header = 'QA_AJAX_RESPONSE';
var headerpos = response.indexOf(header);
if (headerpos >= 0)
callback(response.substr(headerpos + header.length).replace(/^\s+/, '').split("\n"));
else
callback([]);
}, 'text').fail(function(jqXHR) {
if (jqXHR.readyState > 0)
callback([])
});
}
function qa_ajax_error()
{
alert('Unexpected response from server - please try again or switch off Javascript.');
}
function qa_display_rule_show(target, show, first)
{
var e = document.getElementById(target);
if (e) {
if (first || e.nodeName == 'SPAN')
e.style.display = (show ? '' : 'none');
else if (show)
$(e).fadeIn();
else
$(e).fadeOut();
}
}
// Question page actions
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) || (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, elem)
{
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';
}
var t = document.getElementById('a_list_title');
qa_set_inner_html(t, 'a_list_title', lines[2]);
qa_reveal(t, 'a_list_title');
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['a_form'].submit();
} else {
qa_ajax_error();
}
}
);
qa_show_waiting_after(elem, false);
return false;
}
function qa_submit_comment(questionid, parentid, elem)
{
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['c_form_' + parentid].submit();
} else {
qa_ajax_error();
}
}
);
qa_show_waiting_after(elem, false);
return false;
}
function qa_answer_click(answerid, questionid, target)
{
var params = {};
params.answerid = answerid;
params.questionid = questionid;
params.code = target.form.elements.code.value;
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 {
target.form.elements.qa_click.value = target.name;
target.form.submit();
}
}
);
qa_show_waiting_after(target, false);
return false;
}
function qa_comment_click(commentid, questionid, parentid, target)
{
var params = {};
params.commentid = commentid;
params.questionid = questionid;
params.parentid = parentid;
params.code = target.form.elements.code.value;
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 {
target.form.elements.qa_click.value = target.name;
target.form.submit();
}
}
);
qa_show_waiting_after(target, false);
return false;
}
function qa_show_comments(questionid, parentid, elem)
{
var params = {};
params.c_questionid = questionid;
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();
}
}
);
qa_show_waiting_after(elem, true);
return false;
}
function qa_form_params(formname)
{
var es = document.forms[formname].elements;
var params = {};
for (var i = 0; i < es.length; i++) {
var e = es[i];
var t = (e.type || '').toLowerCase();
if (((t != 'checkbox') && (t != 'radio')) || e.checked)
params[e.name] = e.value;
}
return params;
}
function qa_scroll_page_to(scroll)
{
$('html,body').animate({scrollTop: scroll}, 400);
}
// Ask form
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(true);
}
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();
});
qa_show_waiting_after(document.getElementById('similar'), true);
}
function qa_html_unescape(html)
{
return html.replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>');
}
function qa_html_escape(text)
{
return text.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
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 = qa_html_unescape(link.innerHTML.replace(/<[^>]*>/g, ''));
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 html = '';
var completed = false;
// first try to auto-complete
if (qa_tags_complete && !skipcomplete) {
var parts = qa_tag_typed_parts(elem);
if (parts.typed) {
html = qa_tags_to_html((qa_html_unescape(qa_tags_examples + ',' + qa_tags_complete)).split(','), parts.typed.toLowerCase());
completed = html ? true : false;
}
}
// otherwise show examples
if (qa_tags_examples && !completed)
html = qa_tags_to_html((qa_html_unescape(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 < tags.length; i++) {
var tag = tags[i];
var taglc = tag.toLowerCase();
if (!tagseen[taglc]) {
tagseen[taglc] = true;
if ((!matchlc) || (taglc.indexOf(matchlc) >= 0)) { // match if necessary
if (matchlc) { // if matching, show appropriate part in bold
var matchstart = taglc.indexOf(matchlc);
var matchend = matchstart + matchlc.length;
inner = '<span style="font-weight:normal;">' + qa_html_escape(tag.substring(0, matchstart)) + '<b>' +
qa_html_escape(tag.substring(matchstart, matchend)) + '</b>' + qa_html_escape(tag.substring(matchend)) + '</span>';
} else // otherwise show as-is
inner = qa_html_escape(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 (l < startval.length && startval[l].length) {
var val = startval[l];
for (var j = 0; j < elem.options.length; j++)
if (elem.options[j].value == val)
elem.selectedIndex = j;
} else
var val = elem.options[elem.selectedIndex].value;
} else
val = '';
if (elem.qa_last_sel !== val) {
elem.qa_last_sel = val;
var subelem = document.getElementById(idprefix + '_' + l + '_sub');
if (subelem)
subelem.parentNode.removeChild(subelem);
if (val.length || (l == 0)) {
subelem = elem.parentNode.insertBefore(document.createElement('span'), elem.nextSibling);
subelem.id = idprefix + '_' + l + '_sub';
qa_show_waiting_after(subelem, true);
qa_ajax_post('category', {categoryid: val},
(function(elem, l) {
return function(lines) {
var subelem = document.getElementById(idprefix + '_' + l + '_sub');
if (subelem)
subelem.parentNode.removeChild(subelem);
if (lines[0] == '1') {
elem.qa_cat_desc = lines[1];
var addedoption = false;
if (lines.length > 2) {
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 < lines.length; i++) {
var parts = lines[i].split('/');
if (String(qa_cat_exclude).length && (String(qa_cat_exclude) == parts[0]))
continue;
newelem.options[newelem.options.length] = new Option(parts.slice(1).join('/'), parts[0]);
addedoption = true;
}
if (addedoption) {
subelem.appendChild(newelem);
qa_category_select(idprefix, startpath);
}
if (l == 0)
elem.style.display = 'none';
}
if (!addedoption)
set_category_description(idprefix);
} else if (lines[0] == '0')
alert(lines[1]);
else
qa_ajax_error();
}
})(elem, l)
);
setdescnow = false;
}
break;
}
}
}
if (setdescnow)
set_category_description(idprefix);
}
function set_category_description(idprefix)
{
var n = document.getElementById(idprefix + '_note');
if (n) {
desc = '';
for (var l = 1; l <= qa_cat_maxdepth; l++) {
var elem = document.getElementById(idprefix + '_' + l);
if (elem && elem.options[elem.selectedIndex].value.length)
desc = elem.qa_cat_desc;
}
n.innerHTML = desc;
}
}
// User functions
function qa_submit_wall_post(elem, morelink)
{
var params = {};
params.message = document.forms.wallpost.message.value;
params.handle = document.forms.wallpost.handle.value;
params.start = document.forms.wallpost.start.value;
params.code = document.forms.wallpost.code.value;
params.morelink = morelink ? 1 : 0;
qa_ajax_post('wallpost', params,
function(lines) {
if (lines[0] == '1') {
var l = document.getElementById('wallmessages');
l.innerHTML = lines.slice(2).join("\n");
var c = document.getElementById(lines[1]); // id of new message
if (c) {
c.style.display = 'none';
qa_reveal(c, 'wallpost');
}
document.forms.wallpost.message.value = '';
qa_hide_waiting(elem);
} else if (lines[0] == '0') {
document.forms.wallpost.qa_click.value = elem.name;
document.forms.wallpost.submit();
} else {
qa_ajax_error();
}
}
);
qa_show_waiting_after(elem, false);
return false;
}
function qa_wall_post_click(messageid, target)
{
var params = {};
params.messageid = messageid;
params.handle = document.forms.wallpost.handle.value;
params.start = document.forms.wallpost.start.value;
params.code = document.forms.wallpost.code.value;
params[target.name] = target.value;
qa_ajax_post('click_wall', params,
function(lines) {
if (lines[0] == '1') {
var l = document.getElementById('m' + messageid);
var h = lines.slice(1).join("\n");
if (h.length)
qa_set_outer_html(l, 'wallpost', h);
else
qa_conceal(l, 'wallpost');
} else {
document.forms.wallpost.qa_click.value = target.name;
document.forms.wallpost.submit();
}
}
);
qa_show_waiting_after(target, false);
return false;
}
function qa_pm_click(messageid, target, box)
{
var params = {};
params.messageid = messageid;
params.box = box;
params.handle = document.forms.pmessage.handle.value;
params.start = document.forms.pmessage.start.value;
params.code = document.forms.pmessage.code.value;
params[target.name] = target.value;
qa_ajax_post('click_pm', params,
function(lines) {
if (lines[0] == '1') {
var l = document.getElementById('m' + messageid);
var h = lines.slice(1).join("\n");
if (h.length)
qa_set_outer_html(l, 'pmessage', h);
else
qa_conceal(l, 'pmessage');
} else {
document.forms.pmessage.qa_click.value = target.name;
document.forms.pmessage.submit();
}
}
);
qa_show_waiting_after(target, false);
return false;
}

190
qa-content/qa-page.js Normal file
View File

@@ -0,0 +1,190 @@
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
File: qa-content/qa-page.js
Description: THIS FILE HAS BEEN DEPRECATED IN FAVOUR OF qa-global.js
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_reveal(elem, type, callback)
{
if (elem)
$(elem).slideDown(400, callback);
}
function qa_conceal(elem, type, callback)
{
if (elem)
$(elem).slideUp(400);
}
function qa_set_inner_html(elem, type, html)
{
if (elem)
elem.innerHTML = html;
}
function qa_set_outer_html(elem, type, html)
{
if (elem) {
var e = document.createElement('div');
e.innerHTML = html;
elem.parentNode.replaceChild(e.firstChild, elem);
}
}
function qa_show_waiting_after(elem, inside)
{
if (elem && !elem.qa_waiting_shown) {
var w = document.getElementById('qa-waiting-template');
if (w) {
var c = w.cloneNode(true);
c.id = null;
if (inside)
elem.insertBefore(c, null);
else
elem.parentNode.insertBefore(c, elem.nextSibling);
elem.qa_waiting_shown = c;
}
}
}
function qa_hide_waiting(elem)
{
var c = elem.qa_waiting_shown;
if (c) {
c.parentNode.removeChild(c);
elem.qa_waiting_shown = null;
}
}
function qa_vote_click(elem)
{
var ens = elem.name.split('_');
var postid = ens[1];
var vote = parseInt(ens[2]);
var code = elem.form.elements.code.value;
var anchor = ens[3];
qa_ajax_post('vote', {postid: postid, vote: vote, code: code},
function(lines) {
if (lines[0] == '1') {
qa_set_inner_html(document.getElementById('voting_' + postid), 'voting', lines.slice(1).join("\n"));
} else if (lines[0] == '0') {
var mess = document.getElementById('errorbox');
if (!mess) {
mess = document.createElement('div');
mess.id = 'errorbox';
mess.className = 'qa-error';
mess.innerHTML = lines[1];
mess.style.display = 'none';
}
var postelem = document.getElementById(anchor);
var e = postelem.parentNode.insertBefore(mess, postelem);
qa_reveal(e);
} else
qa_ajax_error();
}
);
return false;
}
function qa_notice_click(elem)
{
var ens = elem.name.split('_');
var code = elem.form.elements.code.value;
qa_ajax_post('notice', {noticeid: ens[1], code: code},
function(lines) {
if (lines[0] == '1')
qa_conceal(document.getElementById('notice_' + ens[1]), 'notice');
else if (lines[0] == '0')
alert(lines[1]);
else
qa_ajax_error();
}
);
return false;
}
function qa_favorite_click(elem)
{
var ens = elem.name.split('_');
var code = elem.form.elements.code.value;
qa_ajax_post('favorite', {entitytype: ens[1], entityid: ens[2], favorite: parseInt(ens[3]), code: code},
function(lines) {
if (lines[0] == '1')
qa_set_inner_html(document.getElementById('favoriting'), 'favoriting', lines.slice(1).join("\n"));
else if (lines[0] == '0') {
alert(lines[1]);
qa_hide_waiting(elem);
} else
qa_ajax_error();
}
);
qa_show_waiting_after(elem, false);
return false;
}
function qa_ajax_post(operation, params, callback)
{
$.extend(params, {qa: 'ajax', qa_operation: operation, qa_root: qa_root, qa_request: qa_request});
$.post(qa_root, params, function(response) {
var header = 'QA_AJAX_RESPONSE';
var headerpos = response.indexOf(header);
if (headerpos >= 0)
callback(response.substr(headerpos + header.length).replace(/^\s+/, '').split("\n"));
else
callback([]);
}, 'text').fail(function(jqXHR) {
if (jqXHR.readyState > 0)
callback([])
});
}
function qa_ajax_error()
{
alert('Unexpected response from server - please try again or switch off Javascript.');
}
function qa_display_rule_show(target, show, first)
{
var e = document.getElementById(target);
if (e) {
if (first || e.nodeName == 'SPAN')
e.style.display = (show ? '' : 'none');
else if (show)
$(e).fadeIn();
else
$(e).fadeOut();
}
}

271
qa-content/qa-question.js Normal file
View File

@@ -0,0 +1,271 @@
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
File: qa-content/qa-question.js
Description: THIS FILE HAS BEEN DEPRECATED IN FAVOUR OF qa-global.js
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) || (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, elem)
{
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';
}
var t = document.getElementById('a_list_title');
qa_set_inner_html(t, 'a_list_title', lines[2]);
qa_reveal(t, 'a_list_title');
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['a_form'].submit();
} else {
qa_ajax_error();
}
}
);
qa_show_waiting_after(elem, false);
return false;
}
function qa_submit_comment(questionid, parentid, elem)
{
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['c_form_' + parentid].submit();
} else {
qa_ajax_error();
}
}
);
qa_show_waiting_after(elem, false);
return false;
}
function qa_answer_click(answerid, questionid, target)
{
var params = {};
params.answerid = answerid;
params.questionid = questionid;
params.code = target.form.elements.code.value;
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 {
target.form.elements.qa_click.value = target.name;
target.form.submit();
}
}
);
qa_show_waiting_after(target, false);
return false;
}
function qa_comment_click(commentid, questionid, parentid, target)
{
var params = {};
params.commentid = commentid;
params.questionid = questionid;
params.parentid = parentid;
params.code = target.form.elements.code.value;
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 {
target.form.elements.qa_click.value = target.name;
target.form.submit();
}
}
);
qa_show_waiting_after(target, false);
return false;
}
function qa_show_comments(questionid, parentid, elem)
{
var params = {};
params.c_questionid = questionid;
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();
}
}
);
qa_show_waiting_after(elem, true);
return false;
}
function qa_form_params(formname)
{
var es = document.forms[formname].elements;
var params = {};
for (var i = 0; i < es.length; i++) {
var e = es[i];
var t = (e.type || '').toLowerCase();
if (((t != 'checkbox') && (t != 'radio')) || e.checked)
params[e.name] = e.value;
}
return params;
}
function qa_scroll_page_to(scroll)
{
$('html,body').animate({scrollTop: scroll}, 400);
}

129
qa-content/qa-user.js Normal file
View File

@@ -0,0 +1,129 @@
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
File: qa-content/qa-user.js
Description: THIS FILE HAS BEEN DEPRECATED IN FAVOUR OF qa-global.js
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_submit_wall_post(elem, morelink)
{
var params = {};
params.message = document.forms.wallpost.message.value;
params.handle = document.forms.wallpost.handle.value;
params.start = document.forms.wallpost.start.value;
params.code = document.forms.wallpost.code.value;
params.morelink = morelink ? 1 : 0;
qa_ajax_post('wallpost', params,
function(lines) {
if (lines[0] == '1') {
var l = document.getElementById('wallmessages');
l.innerHTML = lines.slice(2).join("\n");
var c = document.getElementById(lines[1]); // id of new message
if (c) {
c.style.display = 'none';
qa_reveal(c, 'wallpost');
}
document.forms.wallpost.message.value = '';
qa_hide_waiting(elem);
} else if (lines[0] == '0') {
document.forms.wallpost.qa_click.value = elem.name;
document.forms.wallpost.submit();
} else {
qa_ajax_error();
}
}
);
qa_show_waiting_after(elem, false);
return false;
}
function qa_wall_post_click(messageid, target)
{
var params = {};
params.messageid = messageid;
params.handle = document.forms.wallpost.handle.value;
params.start = document.forms.wallpost.start.value;
params.code = document.forms.wallpost.code.value;
params[target.name] = target.value;
qa_ajax_post('click_wall', params,
function(lines) {
if (lines[0] == '1') {
var l = document.getElementById('m' + messageid);
var h = lines.slice(1).join("\n");
if (h.length)
qa_set_outer_html(l, 'wallpost', h);
else
qa_conceal(l, 'wallpost');
} else {
document.forms.wallpost.qa_click.value = target.name;
document.forms.wallpost.submit();
}
}
);
qa_show_waiting_after(target, false);
return false;
}
function qa_pm_click(messageid, target, box)
{
var params = {};
params.messageid = messageid;
params.box = box;
params.handle = document.forms.pmessage.handle.value;
params.start = document.forms.pmessage.start.value;
params.code = document.forms.pmessage.code.value;
params[target.name] = target.value;
qa_ajax_post('click_pm', params,
function(lines) {
if (lines[0] == '1') {
var l = document.getElementById('m' + messageid);
var h = lines.slice(1).join("\n");
if (h.length)
qa_set_outer_html(l, 'pmessage', h);
else
qa_conceal(l, 'pmessage');
} else {
document.forms.pmessage.qa_click.value = target.name;
document.forms.pmessage.submit();
}
}
);
qa_show_waiting_after(target, false);
return false;
}

View File

@@ -0,0 +1,636 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
File: qa-external-example/qa-external-users.php
Description: Example of how to integrate with your own user database
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
*/
/*
=========================================================================
THIS FILE ALLOWS YOU TO INTEGRATE WITH AN EXISTING USER MANAGEMENT SYSTEM
=========================================================================
It is used if QA_EXTERNAL_USERS is set to true in qa-config.php.
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../');
exit;
}
/**
* ==========================================================================
* YOU MUST MODIFY THIS FUNCTION *BEFORE* Q2A CREATES ITS DATABASE
* ==========================================================================
*
* You should return the appropriate MySQL column type to use for the userid,
* for smooth integration with your existing users. Allowed options are:
*
* SMALLINT, SMALLINT UNSIGNED, MEDIUMINT, MEDIUMINT UNSIGNED, INT, INT UNSIGNED,
* BIGINT, BIGINT UNSIGNED or VARCHAR(x) where x is the maximum length.
*/
function qa_get_mysql_user_column_type()
{
// Set this before anything else
return null;
/*
Example 1 - suitable if:
* You use textual user identifiers with a maximum length of 32
return 'VARCHAR(32)';
*/
/*
Example 2 - suitable if:
* You use unsigned numerical user identifiers in an INT UNSIGNED column
return 'INT UNSIGNED';
*/
}
/**
* ===========================================================================
* YOU MUST MODIFY THIS FUNCTION, BUT CAN DO SO AFTER Q2A CREATES ITS DATABASE
* ===========================================================================
*
* You should return an array containing URLs for the login, register and logout pages on
* your site. These URLs will be used as appropriate within the Q2A site.
*
* You may return absolute or relative URLs for each page. If you do not want one of the links
* to show, omit it from the array, or use null or an empty string.
*
* If you use absolute URLs, then return an array with the URLs in full (see example 1 below).
*
* If you use relative URLs, the URLs should start with $relative_url_prefix, followed by the
* relative path from the root of the Q2A site to your login page. Like in example 2 below, if
* the Q2A site is in a subdirectory, $relative_url_prefix.'../' refers to your site root.
*
* Now, about $redirect_back_to_url. Let's say a user is viewing a page on the Q2A site, and
* clicks a link to the login URL that you returned from this function. After they log in using
* the form on your main site, they want to automatically go back to the page on the Q2A site
* where they came from. This can be done with an HTTP redirect, but how does your login page
* know where to redirect the user to? The solution is $redirect_back_to_url, which is the URL
* of the page on the Q2A site where you should send the user once they've successfully logged
* in. To implement this, you can add $redirect_back_to_url as a parameter to the login URL
* that you return from this function. Your login page can then read it in from this parameter,
* and redirect the user back to the page after they've logged in. The same applies for your
* register and logout pages. Note that the URL you are given in $redirect_back_to_url is
* relative to the root of the Q2A site, so you may need to add something.
*/
function qa_get_login_links($relative_url_prefix, $redirect_back_to_url)
{
// Until you edit this function, don't show login, register or logout links
return array(
'login' => 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),
);
*/
}
/**
* ===========================================================================
* 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
* - passsalt: (optional) password salt specific to this user, used for form security codes
* - 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
*
* To indicate that the user is blocked you can also add an element 'blocked' with the value true.
* Blocked users are not allowed to perform any write actions such as voting or posting.
*
* 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 also use the various qa_db_* functions 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.
*/
function qa_get_logged_in_user()
{
// 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 (isset($_SESSION['is_logged_in'])) {
$userid = $_SESSION['logged_in_userid'];
$result = qa_db_read_one_assoc(qa_db_query_sub(
'SELECT email FROM users WHERE id=$',
$userid
));
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 (isset($_COOKIE['sessionid'])) {
$result = qa_db_read_one_assoc(qa_db_query_sub(
'SELECT userid, username, email, admin_flag FROM users WHERE userid=(SELECT userid FROM sessions WHERE sessionid=#)',
$_COOKIE['sessionid']
));
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;
*/
}
/**
* ===========================================================================
* 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 also use the various qa_db_* functions to run queries.
*/
function qa_get_user_email($userid)
{
// 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
$result = qa_db_read_one_assoc(qa_db_query_sub(
'SELECT email FROM users WHERE userid=#',
$userid
));
if (is_array($result))
return $result['email'];
return null;
*/
}
/**
* ===========================================================================
* 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 valid usernames to internal user ids. For each element of this array, the username should be
* in the key, with the corresponding user id in the value. If your usernames are case- or accent-
* insensitive, keys should contain the usernames as stored, not necessarily as in $publicusernames.
*
* Call qa_db_connection() to get the connection to the Q2A database. If your database is shared with
* Q2A, you can also use the various qa_db_* functions to run queries. If you access this database or
* any other, try to use a single query instead of one per user.
*/
function qa_get_userids_from_public($publicusernames)
{
// 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)) {
$escapedusernames = array();
foreach ($publicusernames as $publicusername)
$escapedusernames[] = "'" . qa_db_escape_string($publicusername) . "'";
$results = qa_db_read_all_assoc(qa_db_query_raw(
'SELECT username, userid FROM users WHERE username IN (' . implode(',', $escapedusernames) . ')'
));
foreach ($results as $result)
$publictouserid[$result['username']] = $result['userid'];
}
return $publictouserid;
*/
}
/**
* ===========================================================================
* 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 valid
* userids 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 also use the various qa_db_* functions to run queries. If you access this database or
* any other, try to use a single query instead of one per user.
*/
function qa_get_public_from_userids($userids)
{
// 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)) {
$escapeduserids = array();
foreach ($userids as $userid)
$escapeduserids[] = "'" . qa_db_escape_string($userid) . "'";
$results = qa_db_read_all_assoc(qa_db_query_raw(
'SELECT username, userid FROM users WHERE userid IN (' . implode(',', $escapeduserids) . ')'
));
foreach ($results as $result)
$useridtopublic[$result['userid']] = $result['username'];
}
return $useridtopublic;
*/
}
/**
* ==========================================================================
* 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.
* Note: the username MUST be escaped with htmlspecialchars() for general output, or urlencode()
* for link URLs.
*
* $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.
*/
function qa_get_logged_in_user_html($logged_in_user, $relative_url_prefix)
{
// By default, show the public username linked to the Q2A profile page for the user
$publicusername = $logged_in_user['publicusername'];
return '<a href="' . qa_path_html('user/' . $publicusername) . '" class="qa-user-link">' . htmlspecialchars($publicusername) . '</a>';
/*
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 '<a href="' . htmlspecialchars($relative_url_prefix . '../user/' . urlencode($publicusername)) .
'" class="qa-user-link">' . htmlspecialchars($publicusername) . '</a>';
*/
/*
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.jpg
$publicusername = $logged_in_user['publicusername'];
return '<a href="http://www.mysite.com/' . htmlspecialchars(urlencode($publicusername)) . '/" class="qa-user-link">' .
'<img src="http://www.mysite.com/' . htmlspecialchars(urlencode($publicusername)) . '/photo-small.jpg" ' .
'style="width:16px; height:16px; border:none; margin-right:4px;">' . htmlspecialchars($publicusername) . '</a>';
*/
}
/**
* ==========================================================================
* 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.
* Note: the username MUST be escaped with htmlspecialchars() for general output, or urlencode()
* for link URLs.
*
* Call qa_db_connection() to get the connection to the Q2A database. If your database is shared with
* Q2A, you can also use the various qa_db_* functions 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.
*/
function qa_get_users_html($userids, $should_include_link, $relative_url_prefix)
{
// 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] = '<a href="' . qa_path_html('user/' . $publicusername) . '" class="qa-user-link">' . $usershtml[$userid] . '</a>';
}
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] = '<a href="' . htmlspecialchars($relative_url_prefix . '../user/' . urlencode($publicusername)) .
'" class="qa-user-link">' . $usershtml[$userid] . '</a>';
}
}
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.jpg
$useridtopublic = qa_get_public_from_userids($userids);
foreach ($userids as $userid) {
$publicusername = $useridtopublic[$userid];
$usershtml[$userid] = '<img src="http://www.mysite.com/' . htmlspecialchars(urlencode($publicusername)) . '/photo-small.jpg" ' .
'style="width:16px; height:16px; border:0; margin-right:4px;">' . htmlspecialchars($publicusername);
if ($should_include_link) {
$usershtml[$userid] = '<a href="http://www.mysite.com/' . htmlspecialchars(urlencode($publicusername)) .
'/" class="qa-user-link">' . $usershtml[$userid] . '</a>';
}
}
return $usershtml;
*/
}
/**
* ==========================================================================
* YOU MAY MODIFY THIS FUNCTION, BUT THE DEFAULT BELOW WILL WORK OK
* ==========================================================================
*
* qa_avatar_html_from_userid($userid, $size, $padding)
*
* You should return some HTML for displaying the avatar of $userid on the page.
* If you do not wish to show an avatar for this user, return null.
*
* $size contains the maximum width and height of the avatar to be displayed, in pixels.
*
* If $padding is true, the HTML you return should render to a square of $size x $size pixels,
* even if the avatar is not square. This can be achieved using CSS padding - see function
* qa_get_avatar_blob_html(...) in qa-app-format.php for an example. If $padding is false,
* the HTML can render to anything which would fit inside a square of $size x $size pixels.
*
* Note that this function may be called many times to render an individual page, so it is not
* a good idea to perform a database query each time it is called. Instead, you can use the fact
* that before qa_avatar_html_from_userid(...) is called, qa_get_users_html(...) will have been
* called with all the relevant users in the array $userids. So you can pull out the information
* you need in qa_get_users_html(...) and cache it in a global variable, for use in this function.
*/
function qa_avatar_html_from_userid($userid, $size, $padding)
{
// Show no avatars by default
return null;
/*
Example 1 - suitable if:
* All your avatars are square
* Your Q2A site: http://www.mysite.com/qa/
* Your avatar images: http://www.mysite.com/avatar/[userid]-[size]x[size].jpg
$htmlsize = (int)$size;
return '<img src="http://www.mysite.com/avatar/' . htmlspecialchars($userid) . '-' . $htmlsize . 'x' . $htmlsize . '.jpg" ' .
'width="' . $htmlsize . '" height="' . $htmlsize . '" class="qa-avatar-image" alt=""/>';
*/
}
/**
* ==========================================================================
* 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 also use the various qa_db_* functions 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().
*/
function qa_user_report_action($userid, $action)
{
// Do nothing by default
}

View File

@@ -0,0 +1,183 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
File: qa-include/Q2A/Plugin/PluginManager.php
Description: Keeps track of the installed plugins
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
*/
class Q2A_Plugin_PluginManager
{
const PLUGIN_DELIMITER = ';';
const OPT_ENABLED_PLUGINS = 'enabled_plugins';
private $loadBeforeDbInit = array();
private $loadAfterDbInit = array();
public function readAllPluginMetadatas()
{
$pluginDirectories = $this->getFilesystemPlugins(true);
foreach ($pluginDirectories as $pluginDirectory) {
$pluginFile = $pluginDirectory . 'qa-plugin.php';
if (!file_exists($pluginFile)) {
continue;
}
$metadataUtil = new Q2A_Util_Metadata();
$metadata = $metadataUtil->fetchFromAddonPath($pluginDirectory);
if (empty($metadata)) {
// limit plugin parsing to first 8kB
$contents = file_get_contents($pluginFile, false, null, 0, 8192);
$metadata = qa_addon_metadata($contents, 'Plugin', true);
}
// skip plugin which requires a later version of Q2A
if (isset($metadata['min_q2a']) && qa_qa_version_below($metadata['min_q2a'])) {
continue;
}
// skip plugin which requires a later version of PHP
if (isset($metadata['min_php']) && qa_php_version_below($metadata['min_php'])) {
continue;
}
$pluginInfoKey = basename($pluginDirectory);
$pluginInfo = array(
'pluginfile' => $pluginFile,
'directory' => $pluginDirectory,
'urltoroot' => substr($pluginDirectory, strlen(QA_BASE_DIR)),
);
if (isset($metadata['load_order'])) {
switch ($metadata['load_order']) {
case 'after_db_init':
$this->loadAfterDbInit[$pluginInfoKey] = $pluginInfo;
break;
case 'before_db_init':
$this->loadBeforeDbInit[$pluginInfoKey] = $pluginInfo;
break;
default:
}
} else {
$this->loadBeforeDbInit[$pluginInfoKey] = $pluginInfo;
}
}
}
private function loadPlugins($pluginInfos)
{
global $qa_plugin_directory, $qa_plugin_urltoroot;
foreach ($pluginInfos as $pluginInfo) {
$qa_plugin_directory = $pluginInfo['directory'];
$qa_plugin_urltoroot = $pluginInfo['urltoroot'];
require_once $pluginInfo['pluginfile'];
}
$qa_plugin_directory = null;
$qa_plugin_urltoroot = null;
}
public function loadPluginsBeforeDbInit()
{
$this->loadPlugins($this->loadBeforeDbInit);
}
public function loadPluginsAfterDbInit()
{
$enabledPlugins = $this->getEnabledPlugins(false);
$enabledForAfterDbInit = array();
foreach ($enabledPlugins as $enabledPluginDirectory) {
if (isset($this->loadAfterDbInit[$enabledPluginDirectory])) {
$enabledForAfterDbInit[$enabledPluginDirectory] = $this->loadAfterDbInit[$enabledPluginDirectory];
}
}
$this->loadPlugins($enabledForAfterDbInit);
}
public function getEnabledPlugins($fullPath = false)
{
$pluginDirectories = $this->getEnabledPluginsOption();
if ($fullPath) {
foreach ($pluginDirectories as $key => &$pluginDirectory) {
$pluginDirectory = QA_PLUGIN_DIR . $pluginDirectory . '/';
}
}
return $pluginDirectories;
}
public function setEnabledPlugins($array)
{
$this->setEnabledPluginsOption($array);
}
public function getFilesystemPlugins($fullPath = false)
{
$result = array();
$fileSystemPluginFiles = glob(QA_PLUGIN_DIR . '*/qa-plugin.php');
foreach ($fileSystemPluginFiles as $pluginFile) {
$directory = dirname($pluginFile) . '/';
if (!$fullPath) {
$directory = basename($directory);
}
$result[] = $directory;
}
return $result;
}
public function getHashesForPlugins($pluginDirectories)
{
$result = array();
foreach ($pluginDirectories as $pluginDirectory) {
$result[$pluginDirectory] = md5($pluginDirectory);
}
return $result;
}
private function getEnabledPluginsOption()
{
return explode(self::PLUGIN_DELIMITER, qa_opt(self::OPT_ENABLED_PLUGINS));
}
private function setEnabledPluginsOption($array)
{
qa_opt(self::OPT_ENABLED_PLUGINS, implode(self::PLUGIN_DELIMITER, $array));
}
public function cleanRemovedPlugins()
{
$finalEnabledPlugins = array_intersect(
$this->getFilesystemPlugins(),
$this->getEnabledPlugins()
);
$this->setEnabledPluginsOption($finalEnabledPlugins);
}
}

View File

@@ -0,0 +1,91 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
File: qa-include/Q2A/Storage/FileCache.php
Description: Interface for drivers of caching system.
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
*/
/**
* Interface for caching drivers.
*/
interface Q2A_Storage_CacheDriver
{
/**
* Get the cached data for the supplied key. Data can be any format but is usually an array.
* @param string $key The unique cache identifier.
*
* @return mixed The cached data, or null otherwise.
*/
public function get($key);
/**
* Store something in the cache along with the key and expiry time. Data gets 'serialized' to a string before storing.
* @param string $key The unique cache identifier.
* @param mixed $data The data to cache (in core Q2A this is usually an array).
* @param int $ttl Number of minutes for which to cache the data.
*
* @return bool Whether the file was successfully cached.
*/
public function set($key, $data, $ttl);
/**
* Delete an item from the cache.
* @param string $key The unique cache identifier.
*
* @return bool Whether the operation succeeded.
*/
public function delete($key);
/**
* Delete multiple items from the cache.
* @param int $limit Maximum number of items to process. 0 = unlimited
* @param int $start Offset from which to start (used for 'batching' deletes).
* @param bool $expiredOnly Delete cache only if it has expired.
*
* @return int Number of files deleted.
*/
public function clear($limit = 0, $start = 0, $expiredOnly = false);
/**
* Whether caching is available.
*
* @return bool
*/
public function isEnabled();
/**
* Get the last error.
*
* @return string
*/
public function getError();
/**
* Get the prefix used for all cache keys.
*
* @return string
*/
public function getKeyPrefix();
/**
* Get current statistics for the cache.
*
* @return array Array of stats: 'files' => number of files, 'size' => total file size in bytes.
*/
public function getStats();
}

View File

@@ -0,0 +1,61 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
File: qa-include/Q2A/Storage/CacheManager.php
Description: Handler for caching system.
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
*/
/**
* Caches data (typically from database queries) to the filesystem.
*/
class Q2A_Storage_CacheFactory
{
private static $cacheDriver = null;
/**
* Get the appropriate cache handler.
* @return Q2A_Storage_CacheDriver The cache handler.
*/
public static function getCacheDriver()
{
if (self::$cacheDriver === null) {
$config = array(
'enabled' => (int) qa_opt('caching_enabled') === 1,
'keyprefix' => QA_FINAL_MYSQL_DATABASE . '.' . QA_MYSQL_TABLE_PREFIX . '.',
'dir' => defined('QA_CACHE_DIRECTORY') ? QA_CACHE_DIRECTORY : null,
);
$driver = qa_opt('caching_driver');
switch($driver)
{
case 'memcached':
self::$cacheDriver = new Q2A_Storage_MemcachedDriver($config);
break;
case 'filesystem':
default:
self::$cacheDriver = new Q2A_Storage_FileCacheDriver($config);
break;
}
}
return self::$cacheDriver;
}
}

View File

@@ -0,0 +1,284 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
File: qa-include/Q2A/Storage/FileCacheDriver.php
Description: File-based driver for caching system.
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
*/
/**
* Caches data (typically from database queries) to the filesystem.
*/
class Q2A_Storage_FileCacheDriver implements Q2A_Storage_CacheDriver
{
private $enabled = false;
private $keyPrefix = '';
private $error;
private $cacheDir;
private $phpProtect = '<?php header($_SERVER[\'SERVER_PROTOCOL\'].\' 404 Not Found\'); die; ?>';
/**
* Creates a new FileCache instance and checks we can write to the cache directory.
* @param array $config Configuration data, including cache storage directory.
*/
public function __construct($config)
{
if (!$config['enabled']) {
return;
}
if (isset($config['keyprefix'])) {
$this->keyPrefix = $config['keyprefix'];
}
if (isset($config['dir'])) {
$this->cacheDir = realpath($config['dir']);
if (!is_writable($this->cacheDir)) {
$this->error = qa_lang_html_sub('admin/caching_dir_error', $config['dir']);
}
} else {
$this->error = qa_lang_html('admin/caching_dir_missing');
}
$this->enabled = empty($this->error);
}
/**
* Get the cached data for the supplied key.
* @param string $key The unique cache identifier.
*
* @return string The cached data, or null otherwise.
*/
public function get($key)
{
if (!$this->enabled) {
return null;
}
$fullKey = $this->keyPrefix . $key;
$file = $this->getFilename($fullKey);
if (is_readable($file)) {
$lines = file($file, FILE_IGNORE_NEW_LINES);
$skipLine = array_shift($lines);
$actualKey = array_shift($lines);
// double check this is the correct data
if ($fullKey === $actualKey) {
$expiry = array_shift($lines);
if (is_numeric($expiry) && time() < $expiry) {
$encData = implode("\n", $lines);
// decode data, ignoring any notices
$data = @unserialize($encData);
if ($data !== false) {
return $data;
}
}
}
}
return null;
}
/**
* Store something in the cache along with the key and expiry time. Data gets 'serialized' to a string before storing.
* @param string $key The unique cache identifier.
* @param mixed $data The data to cache (in core Q2A this is usually an array).
* @param int $ttl Number of minutes for which to cache the data.
*
* @return bool Whether the file was successfully cached.
*/
public function set($key, $data, $ttl)
{
$success = false;
$ttl = (int) $ttl;
$fullKey = $this->keyPrefix . $key;
if ($this->enabled && $ttl > 0) {
$encData = serialize($data);
$expiry = time() + ($ttl * 60);
$cache = $this->phpProtect . "\n" . $fullKey . "\n" . $expiry . "\n" . $encData;
$file = $this->getFilename($fullKey);
$dir = dirname($file);
if (is_dir($dir) || mkdir($dir, 0777, true)) {
$success = @file_put_contents($file, $cache) !== false;
}
}
return $success;
}
/**
* Delete an item from the cache.
* @param string $key The unique cache identifier.
*
* @return bool Whether the operation succeeded.
*/
public function delete($key)
{
$fullKey = $this->keyPrefix . $key;
if ($this->enabled) {
$file = $this->getFilename($fullKey);
return $this->deleteFile($file);
}
return false;
}
/**
* Delete multiple items from the cache.
* @param int $limit Maximum number of items to process. 0 = unlimited
* @param int $start Offset from which to start (used for 'batching' deletes).
* @param bool $expiredOnly Delete cache only if it has expired.
*
* @return int Number of files deleted.
*/
public function clear($limit = 0, $start = 0, $expiredOnly = false)
{
$seek = $processed = $deleted = 0;
// fetch directories first to lower memory usage
$cacheDirs = glob($this->cacheDir . '/*/*', GLOB_ONLYDIR);
foreach ($cacheDirs as $dir) {
$cacheFiles = glob($dir . '/*');
foreach ($cacheFiles as $file) {
if ($seek < $start) {
$seek++;
continue;
}
$wasDeleted = false;
if ($expiredOnly) {
if (is_readable($file)) {
$fp = fopen($file, 'r');
$skipLine = fgets($fp);
$key = fgets($fp);
$expiry = (int) trim(fgets($fp));
if (time() > $expiry) {
$wasDeleted = $this->deleteFile($file);
}
}
} else {
$wasDeleted = $this->deleteFile($file);
}
if ($wasDeleted) {
$deleted++;
}
$processed++;
if ($processed >= $limit) {
break 2;
}
}
}
// return how many files were deleted - caller can figure out how many to skip next time
return $deleted;
}
/**
* Whether caching is available.
*
* @return bool
*/
public function isEnabled()
{
return $this->enabled;
}
/**
* Get the last error.
*
* @return string
*/
public function getError()
{
return $this->error;
}
/**
* Get the prefix used for all cache keys.
*
* @return string
*/
public function getKeyPrefix()
{
return $this->keyPrefix;
}
/**
* Get current statistics for the cache.
*
* @return array Array of stats: 'files' => number of files, 'size' => total file size in bytes.
*/
public function getStats()
{
if (!$this->enabled) {
return array('files' => 0, 'size' => 0);
}
$totalFiles = 0;
$totalBytes = 0;
$dirIter = new RecursiveDirectoryIterator($this->cacheDir);
foreach (new RecursiveIteratorIterator($dirIter) as $file) {
if (strpos($file->getFilename(), '.') === 0) {
// TODO: use FilesystemIterator::SKIP_DOTS once we're on minimum PHP 5.3
continue;
}
$totalFiles++;
$totalBytes += $file->getSize();
}
return array(
'files' => $totalFiles,
'size' => $totalBytes,
);
}
/**
* Delete a specific file
* @param string $file Filename to delete.
*
* @return bool Whether the file was deleted successfully.
*/
private function deleteFile($file)
{
if (is_writable($file)) {
return @unlink($file) === true;
}
return false;
}
/**
* Generates filename for cache key, of the form `1/23/123abc`
* @param string $key The unique cache key (including prefix).
*
* @return string
*/
private function getFilename($fullKey)
{
$filename = sha1($fullKey);
return $this->cacheDir . '/' . substr($filename, 0, 1) . '/' . substr($filename, 1, 2) . '/' . $filename . '.php';
}
}

View File

@@ -0,0 +1,220 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
File: qa-include/Q2A/Storage/MemcachedDriver.php
Description: Memcached-based driver for caching system.
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
*/
/**
* Caches data (typically from database queries) in memory using Memcached.
*/
class Q2A_Storage_MemcachedDriver implements Q2A_Storage_CacheDriver
{
private $memcached;
private $enabled = false;
private $keyPrefix = '';
private $error;
private $flushed = false;
const HOST = '127.0.0.1';
const PORT = 11211;
/**
* Creates a new Memcached instance and checks we can cache items.
* @param array $config Configuration data, including cache storage directory.
*
* @return void
*/
public function __construct($config)
{
if (!$config['enabled']) {
return;
}
if (isset($config['keyprefix'])) {
$this->keyPrefix = $config['keyprefix'];
}
if (extension_loaded('memcached')) {
$this->memcached = new Memcached;
$this->memcached->addServer(self::HOST, self::PORT);
if ($this->memcached->set($this->keyPrefix . 'test', 'TEST')) {
$this->enabled = true;
} else {
$this->setMemcachedError();
}
} else {
$this->error = qa_lang_html('admin/no_memcached');
}
}
/**
* Get the cached data for the supplied key. Data can be any format but is usually an array.
* @param string $key The unique cache identifier.
*
* @return mixed The cached data, or null otherwise.
*/
public function get($key)
{
if (!$this->enabled) {
return null;
}
$result = $this->memcached->get($this->keyPrefix . $key);
if ($result === false) {
$this->setMemcachedError();
return null;
}
return $result;
}
/**
* Store something in the cache along with the key and expiry time. Data gets 'serialized' to a string before storing.
* @param string $key The unique cache identifier.
* @param mixed $data The data to cache (in core Q2A this is usually an array).
* @param int $ttl Number of minutes for which to cache the data.
*
* @return bool Whether the file was successfully cached.
*/
public function set($key, $data, $ttl)
{
if (!$this->enabled) {
return false;
}
$ttl = (int) $ttl;
$expiry = time() + ($ttl * 60);
$success = $this->memcached->set($this->keyPrefix . $key, $data, $expiry);
if (!$success) {
$this->setMemcachedError();
}
return $success;
}
/**
* Delete an item from the cache.
* @param string $key The unique cache identifier.
*
* @return bool Whether the operation succeeded.
*/
public function delete($key)
{
if (!$this->enabled) {
return false;
}
$success = $this->memcached->delete($this->keyPrefix . $key);
if (!$success) {
$this->setMemcachedError();
}
return $success;
}
/**
* Delete multiple items from the cache.
* @param int $limit Maximum number of items to process. 0 = unlimited
* @param int $start Offset from which to start (used for 'batching' deletes).
* @param bool $expiredOnly This parameter is ignored because Memcached automatically clears expired items.
*
* @return int Number of files deleted. For Memcached we return 0
*/
public function clear($limit = 0, $start = 0, $expiredOnly = false)
{
if ($this->enabled && !$expiredOnly && !$this->flushed) {
$success = $this->memcached->flush();
// avoid multiple calls to flush()
$this->flushed = true;
if (!$success) {
$this->setMemcachedError();
}
}
return 0;
}
/**
* Whether caching is available.
*
* @return bool
*/
public function isEnabled()
{
return $this->enabled;
}
/**
* Get the last error.
*
* @return string
*/
public function getError()
{
return $this->error;
}
/**
* Get the prefix used for all cache keys.
*
* @return string
*/
public function getKeyPrefix()
{
return $this->keyPrefix;
}
/**
* Get current statistics for the cache.
*
* @return array Array of stats: 'files' => number of files, 'size' => total file size in bytes.
*/
public function getStats()
{
$totalFiles = 0;
$totalBytes = 0;
if ($this->enabled) {
$stats = $this->memcached->getStats();
$key = self::HOST . ':' . self::PORT;
$totalFiles = isset($stats[$key]['curr_items']) ? $stats[$key]['curr_items'] : 0;
$totalBytes = isset($stats[$key]['bytes']) ? $stats[$key]['bytes'] : 0;
}
return array(
'files' => $totalFiles,
'size' => $totalBytes,
);
}
/**
* Set current error to Memcached result message
*
* @return void
*/
private function setMemcachedError()
{
$this->error = qa_lang_html_sub('admin/memcached_error', $this->memcached->getResultMessage());
}
}

View File

@@ -0,0 +1,72 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
File: qa-include/Q2A/Util/Metadata.php
Description: Some useful metadata handling stuff
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
*/
class Q2A_Util_Metadata
{
const METADATA_FILE_JSON = 'metadata.json';
/**
* Fetch metadata information from an addon path
* @param string $path Directory the addon is in (without trailing slash)
* @return array The metadata fetched from the JSON file, or an empty array otherwise
*/
public function fetchFromAddonPath($path)
{
$metadataFile = $path . '/' . self::METADATA_FILE_JSON;
if (!is_file($metadataFile)) {
return array();
}
$content = file_get_contents($metadataFile);
return $this->getArrayFromJson($content);
}
/**
* Fetch metadata information from an URL
* @param string $url URL linking to a metadata.json file
* @return array The metadata fetched from the file
*/
public function fetchFromUrl($url, $type = 'Plugin')
{
$contents = qa_retrieve_url($url);
$metadata = $this->getArrayFromJson($contents);
// fall back to old metadata format
if (empty($metadata)) {
$metadata = qa_addon_metadata($contents, $type);
}
return $metadata;
}
/**
* Return an array from a JSON string
* @param mixed $json The JSON string to turn into an array
* @return array Always return an array containing the decoded JSON or an empty array in case the
* $json parameter is not a valid JSON string
*/
private function getArrayFromJson($json)
{
$result = json_decode($json, true);
return is_array($result) ? $result : array();
}
}

View File

@@ -0,0 +1,212 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
File: qa-include/Q2A/Util/Usage.php
Description: Debugging stuff, currently used for tracking resource usage
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
*/
class Q2A_Util_Usage
{
private $stages;
private $startUsage;
private $prevUsage;
private $databaseUsage;
private $databaseQueryLog;
/**
* Initialize the counts of resource usage.
*/
public function __construct()
{
$this->stages = array();
$this->databaseUsage = array('queries'=>0, 'clock'=>0);
$this->databaseQueryLog = '';
$this->prevUsage = $this->startUsage = $this->getCurrent();
}
/**
* Return an array representing the resource usage as of now.
*/
public function getCurrent()
{
$usage = array(
'files' => count(get_included_files()),
'queries' => $this->databaseUsage['queries'],
'ram' => function_exists('memory_get_usage') ? memory_get_usage() : 0,
'clock' => array_sum(explode(' ', microtime())),
'mysql' => $this->databaseUsage['clock'],
);
if (function_exists('getrusage')) {
$rusage = getrusage();
$usage['cpu'] = $rusage["ru_utime.tv_sec"] + $rusage["ru_stime.tv_sec"]
+ ($rusage["ru_utime.tv_usec"] + $rusage["ru_stime.tv_usec"]) / 1000000;
}
else
$usage['cpu'] = 0;
$usage['other'] = $usage['clock'] - $usage['cpu'] - $usage['mysql'];
return $usage;
}
/**
* Mark the beginning of a new stage of script execution and store usages accordingly.
* @param $stage
*/
public function mark($stage)
{
$usage = $this->getCurrent();
$this->stages[$stage] = $this->delta($this->prevUsage, $usage);
$this->prevUsage = $usage;
}
/**
* Logs query and updates database usage stats.
* @param $query
* @param $usedtime
* @param $gotrows
* @param $gotcolumns
*/
public function logDatabaseQuery($query, $usedtime, $gotrows, $gotcolumns)
{
$this->databaseUsage['clock'] += $usedtime;
if (strlen($this->databaseQueryLog) < 1048576) { // don't keep track of big tests
$rowcolstring = '';
if (is_numeric($gotrows))
$rowcolstring .= ' - ' . $gotrows . ($gotrows == 1 ? ' row' : ' rows');
if (is_numeric($gotcolumns))
$rowcolstring .= ' - ' . $gotcolumns . ($gotcolumns == 1 ? ' column' : ' columns');
$this->databaseQueryLog .= $query . "\n\n" . sprintf('%.2f ms', $usedtime*1000) . $rowcolstring . "\n\n";
}
$this->databaseUsage['queries']++;
}
/**
* Output an (ugly) block of HTML detailing all resource usage and database queries.
*/
public function output()
{
$totaldelta = $this->delta($this->startUsage, $this->getCurrent());
$stages = $this->stages;
$stages['total'] = $totaldelta;
?>
<style>
.debug-table { border-collapse: collapse; width: auto; margin: 20px auto; }
.debug-table th, .debug-table td { border: 1px solid #aaa; background-color: #ddd; padding: 5px 10px; }
.debug-table td { text-align: right; }
.debug-table th:empty { border: none; background-color: initial; }
.debug-table .row-heading { font-weight: bold; }
.debug-table tr:last-child td { background-color: #ccc; border-top-width: 3px; }
textarea.debug-output { box-sizing: border-box; width: 100%; font: 12px monospace; color: #000; }
.extra-info { border-collapse: collapse; box-sizing: border-box; width: 100%; }
.extra-info td.debug-cell-files { width: 30%; padding: 10px 5px 10px 10px; }
.extra-info td.debug-cell-queries { width: 70%; padding: 10px 10px 10px 5px; }
.extra-info tr { background-color: #ccc; }
.extra-info textarea { margin: 0; }
</style>
<table class="debug-table">
<thead>
<tr>
<th></th>
<th colspan="2">Total</th>
<th colspan="3">PHP</th>
<th colspan="3">MySQL</th>
<th colspan="2">Other</th>
<th colspan="2">RAM</th>
</tr>
<tr>
<th></th>
<th>Time (ms)</th>
<th>%</th>
<th>Time (ms)</th>
<th>%</th>
<th>File count</th>
<th>Time (ms)</th>
<th>%</th>
<th>Query count</th>
<th>Time (ms)</th>
<th>%</th>
<th>Amount</th>
<th>%</th>
</tr>
</thead>
<tbody>
<?php foreach ($stages as $stage => $stagedelta) : ?>
<tr>
<td class="row-heading"><?php echo ucfirst($stage); ?></td>
<td><?php echo sprintf('%.1f', $stagedelta['clock'] * 1000); ?></td>
<td><?php echo sprintf('%d%%', $stagedelta['clock'] * 100 / $totaldelta['clock']); ?></td>
<td><?php echo sprintf('%.1f', $stagedelta['cpu'] * 1000); ?></td>
<td><?php echo sprintf('%d%%', $stagedelta['cpu'] * 100 / $totaldelta['clock']); ?></td>
<td><?php echo $stagedelta['files']; ?></td>
<td><?php echo sprintf('%.1f', $stagedelta['mysql'] * 1000); ?></td>
<td><?php echo sprintf('%d%%', $stagedelta['mysql'] * 100 / $totaldelta['clock']); ?></td>
<td><?php echo $stagedelta['queries']; ?></td>
<td><?php echo sprintf('%.1f', $stagedelta['other'] * 1000); ?></td>
<td><?php echo sprintf('%d%%', $stagedelta['other'] * 100 / $totaldelta['clock']); ?></td>
<td><?php echo sprintf('%dk', $stagedelta['ram'] / 1024); ?></td>
<td><?php echo sprintf('%d%%', $stagedelta['ram'] ? ($stagedelta['ram'] * 100 / $totaldelta['ram']) : 0); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<table class="extra-info">
<tbody>
<tr>
<td class="debug-cell-files">
<textarea class="debug-output" cols="40" rows="20"><?php
foreach (get_included_files() as $file)
echo qa_html(implode('/', array_slice(explode('/', $file), -3)))."\n";
?></textarea>
</td>
<td class="debug-cell-queries">
<textarea class="debug-output" cols="40" rows="20"><?php echo qa_html($this->databaseQueryLog)?></textarea>
</td>
</tr>
</tbody>
</table>
<?php
}
/**
* Return the difference between two resource usage arrays, as an array.
* @param $oldusage
* @param $newusage
* @return array
*/
private function delta($oldusage, $newusage)
{
$delta = array();
foreach ($newusage as $key => $value)
$delta[$key] = max(0, $value-@$oldusage[$key]);
return $delta;
}
}

View File

@@ -0,0 +1,99 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Server-side response to Ajax create answer requests
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
*/
require_once QA_INCLUDE_DIR . 'app/posts.php';
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'app/limits.php';
require_once QA_INCLUDE_DIR . 'db/selects.php';
// Load relevant information about this question
$questionid = qa_post_text('a_questionid');
$userid = qa_get_logged_in_userid();
list($question, $childposts) = qa_db_select_with_pending(
qa_db_full_post_selectspec($userid, $questionid),
qa_db_full_child_posts_selectspec($userid, $questionid)
);
// Check if the question exists, is not closed, and whether the user has permission to do this
if (@$question['basetype'] == 'Q' && !qa_post_is_closed($question) && !qa_user_post_permit_error('permit_post_a', $question, QA_LIMIT_ANSWERS)) {
require_once QA_INCLUDE_DIR . 'app/captcha.php';
require_once QA_INCLUDE_DIR . 'app/format.php';
require_once QA_INCLUDE_DIR . 'app/post-create.php';
require_once QA_INCLUDE_DIR . 'app/cookies.php';
require_once QA_INCLUDE_DIR . 'pages/question-view.php';
require_once QA_INCLUDE_DIR . 'pages/question-submit.php';
// Try to create the new answer
$usecaptcha = qa_user_use_captcha(qa_user_level_for_post($question));
$answers = qa_page_q_load_as($question, $childposts);
$answerid = qa_page_q_add_a_submit($question, $answers, $usecaptcha, $in, $errors);
// If successful, page content will be updated via Ajax
if (isset($answerid)) {
$answer = qa_db_select_with_pending(qa_db_full_post_selectspec($userid, $answerid));
$question = $question + qa_page_q_post_rules($question, null, null, $childposts); // array union
$answer = $answer + qa_page_q_post_rules($answer, $question, $answers, null);
$usershtml = qa_userids_handles_html(array($answer), true);
$a_view = qa_page_q_answer_view($question, $answer, false, $usershtml, false);
$themeclass = qa_load_theme_class(qa_get_site_theme(), 'ajax-answer', null, null);
$themeclass->initialize();
echo "QA_AJAX_RESPONSE\n1\n";
// Send back whether the 'answer' button should still be visible
echo (int)qa_opt('allow_multi_answers') . "\n";
// Send back the count of answers
$countanswers = $question['acount'] + 1;
if ($countanswers == 1) {
echo qa_lang_html('question/1_answer_title') . "\n";
} else {
echo qa_lang_html_sub('question/x_answers_title', $countanswers) . "\n";
}
// Send back the HTML
$themeclass->a_list_item($a_view);
return;
}
}
echo "QA_AJAX_RESPONSE\n0\n"; // fall back to non-Ajax submission if there were any problems

View File

@@ -0,0 +1,100 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Server-side response to Ajax request based on ask a question title
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
*/
require_once QA_INCLUDE_DIR . 'db/selects.php';
require_once QA_INCLUDE_DIR . 'util/string.php';
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'app/format.php';
// Collect the information we need from the database
$intitle = qa_post_text('title');
$doaskcheck = qa_opt('do_ask_check_qs');
$doexampletags = qa_using_tags() && qa_opt('do_example_tags');
if ($doaskcheck || $doexampletags) {
$countqs = max($doexampletags ? QA_DB_RETRIEVE_ASK_TAG_QS : 0, $doaskcheck ? qa_opt('page_size_ask_check_qs') : 0);
$relatedquestions = qa_db_select_with_pending(
qa_db_search_posts_selectspec(null, qa_string_to_words($intitle), null, null, null, null, 0, false, $countqs)
);
}
// Collect example tags if appropriate
if ($doexampletags) {
$tagweight = array();
foreach ($relatedquestions as $question) {
$tags = qa_tagstring_to_tags($question['tags']);
foreach ($tags as $tag) {
@$tagweight[$tag] += exp($question['score']);
}
}
arsort($tagweight, SORT_NUMERIC);
$exampletags = array();
$minweight = exp(qa_match_to_min_score(qa_opt('match_example_tags')));
$maxcount = qa_opt('page_size_ask_tags');
foreach ($tagweight as $tag => $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(qa_html(implode(',', $exampletags)), "\r\n", ' ') . "\n";
// Collect and output the list of related questions
if ($doaskcheck) {
$minscore = qa_match_to_min_score(qa_opt('match_ask_check_qs'));
$maxcount = qa_opt('page_size_ask_check_qs');
$relatedquestions = array_slice($relatedquestions, 0, $maxcount);
$limitedquestions = array();
foreach ($relatedquestions as $question) {
if ($question['score'] < $minscore)
break;
$limitedquestions[] = $question;
}
$themeclass = qa_load_theme_class(qa_get_site_theme(), 'ajax-asktitle', null, null);
$themeclass->initialize();
$themeclass->q_ask_similar($limitedquestions, qa_lang_html('question/ask_same_q'));
}

View File

@@ -0,0 +1,41 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Server-side response to Ajax category information requests
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
*/
require_once QA_INCLUDE_DIR . 'db/selects.php';
$categoryid = qa_post_text('categoryid');
if (!strlen($categoryid))
$categoryid = null;
list($fullcategory, $categories) = qa_db_select_with_pending(
qa_db_full_category_selectspec($categoryid, true),
qa_db_category_sub_selectspec($categoryid)
);
echo "QA_AJAX_RESPONSE\n1\n";
echo qa_html(strtr(@$fullcategory['content'], "\r\n", ' ')); // category description
foreach ($categories as $category) {
// subcategory information
echo "\n" . $category['categoryid'] . '/' . $category['title'];
}

View File

@@ -0,0 +1,35 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Server-side response to Ajax single clicks on posts in admin section
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
*/
require_once QA_INCLUDE_DIR . 'app/admin.php';
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'app/cookies.php';
$entityid = qa_post_text('entityid');
$action = qa_post_text('action');
if (!qa_check_form_security_code('admin/click', qa_post_text('code')))
echo "QA_AJAX_RESPONSE\n0\n" . qa_lang('misc/form_security_reload');
elseif (qa_admin_single_click($entityid, $action)) // permission check happens in here
echo "QA_AJAX_RESPONSE\n1\n";
else
echo "QA_AJAX_RESPONSE\n0\n" . qa_lang('main/general_error');

View File

@@ -0,0 +1,112 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Server-side response to Ajax single clicks on answer
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
*/
require_once QA_INCLUDE_DIR . 'app/cookies.php';
require_once QA_INCLUDE_DIR . 'app/format.php';
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'db/selects.php';
require_once QA_INCLUDE_DIR . 'pages/question-view.php';
require_once QA_INCLUDE_DIR . 'pages/question-submit.php';
require_once QA_INCLUDE_DIR . 'util/sort.php';
// Load relevant information about this answer
$answerid = qa_post_text('answerid');
$questionid = qa_post_text('questionid');
$userid = qa_get_logged_in_userid();
list($answer, $question, $qchildposts, $achildposts) = qa_db_select_with_pending(
qa_db_full_post_selectspec($userid, $answerid),
qa_db_full_post_selectspec($userid, $questionid),
qa_db_full_child_posts_selectspec($userid, $questionid),
qa_db_full_child_posts_selectspec($userid, $answerid)
);
// Check if there was an operation that succeeded
if (@$answer['basetype'] == 'A' && @$question['basetype'] == 'Q') {
$answers = qa_page_q_load_as($question, $qchildposts);
$question = $question + qa_page_q_post_rules($question, null, null, $qchildposts); // array union
$answer = $answer + qa_page_q_post_rules($answer, $question, $qchildposts, $achildposts);
if (qa_page_q_single_click_a($answer, $question, $answers, $achildposts, false, $error)) {
list($answer, $question) = qa_db_select_with_pending(
qa_db_full_post_selectspec($userid, $answerid),
qa_db_full_post_selectspec($userid, $questionid)
);
// If so, page content to be updated via Ajax
echo "QA_AJAX_RESPONSE\n1\n";
// Send back new count of answers
$countanswers = $question['acount'];
if ($countanswers == 1)
echo qa_lang_html('question/1_answer_title');
else
echo qa_lang_html_sub('question/x_answers_title', $countanswers);
// If the answer was not deleted....
if (isset($answer)) {
$question = $question + qa_page_q_post_rules($question, null, null, $qchildposts); // array union
$answer = $answer + qa_page_q_post_rules($answer, $question, $qchildposts, $achildposts);
$commentsfollows = qa_page_q_load_c_follows($question, $qchildposts, $achildposts);
foreach ($commentsfollows as $key => $commentfollow) {
$commentsfollows[$key] = $commentfollow + qa_page_q_post_rules($commentfollow, $answer, $commentsfollows, null);
}
$usershtml = qa_userids_handles_html(array_merge(array($answer), $commentsfollows), true);
qa_sort_by($commentsfollows, 'created');
$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($question, $answer, $commentsfollows, false, $usershtml, false, null);
$themeclass = qa_load_theme_class(qa_get_site_theme(), 'ajax-answer', null, null);
$themeclass->initialize();
// ... 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

View File

@@ -0,0 +1,87 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Server-side response to Ajax single clicks on comments
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
*/
require_once QA_INCLUDE_DIR . 'app/cookies.php';
require_once QA_INCLUDE_DIR . 'app/format.php';
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'db/selects.php';
require_once QA_INCLUDE_DIR . 'pages/question-view.php';
require_once QA_INCLUDE_DIR . 'pages/question-submit.php';
// Load relevant information about this comment
$commentid = qa_post_text('commentid');
$questionid = qa_post_text('questionid');
$parentid = qa_post_text('parentid');
$userid = qa_get_logged_in_userid();
list($comment, $question, $parent, $children) = qa_db_select_with_pending(
qa_db_full_post_selectspec($userid, $commentid),
qa_db_full_post_selectspec($userid, $questionid),
qa_db_full_post_selectspec($userid, $parentid),
qa_db_full_child_posts_selectspec($userid, $parentid)
);
// Check if there was an operation that succeeded
if (@$comment['basetype'] == 'C' && @$question['basetype'] == 'Q' &&
(@$parent['basetype'] == 'Q' || @$parent['basetype'] == 'A')
) {
$comment = $comment + qa_page_q_post_rules($comment, $parent, $children, null); // array union
if (qa_page_q_single_click_c($comment, $question, $parent, $error)) {
$comment = qa_db_select_with_pending(qa_db_full_post_selectspec($userid, $commentid));
// If so, page content to be updated via Ajax
echo "QA_AJAX_RESPONSE\n1";
// If the comment was not deleted...
if (isset($comment)) {
$parent = $parent + qa_page_q_post_rules($parent, ($questionid == $parentid) ? null : $question, null, $children);
// in theory we should retrieve the parent's siblings for the above, but they're not going to be relevant
$comment = $comment + qa_page_q_post_rules($comment, $parent, $children, null);
$usershtml = qa_userids_handles_html(array($comment), true);
$c_view = qa_page_q_comment_view($question, $parent, $comment, $usershtml, false);
$themeclass = qa_load_theme_class(qa_get_site_theme(), 'ajax-comment', null, null);
$themeclass->initialize();
// ... send back the HTML for it
echo "\n";
$themeclass->c_list_item($c_view);
}
return;
}
}
echo "QA_AJAX_RESPONSE\n0\n"; // fall back to non-Ajax submission if something failed

View File

@@ -0,0 +1,56 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Server-side response to Ajax single clicks on private messages
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
*/
require_once QA_INCLUDE_DIR . 'app/messages.php';
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'app/cookies.php';
require_once QA_INCLUDE_DIR . 'db/selects.php';
$loginUserId = qa_get_logged_in_userid();
$loginUserHandle = qa_get_logged_in_handle();
$fromhandle = qa_post_text('handle');
$start = (int)qa_post_text('start');
$box = qa_post_text('box');
$pagesize = qa_opt('page_size_pms');
if (!isset($loginUserId) || $loginUserHandle !== $fromhandle || !in_array($box, array('inbox', 'outbox'))) {
echo "QA_AJAX_RESPONSE\n0\n";
return;
}
$func = 'qa_db_messages_' . $box . '_selectspec';
$pmSpec = $func('private', $loginUserId, true, $start, $pagesize);
$userMessages = qa_db_select_with_pending($pmSpec);
foreach ($userMessages as $message) {
if (qa_clicked('m' . $message['messageid'] . '_dodelete')) {
if (qa_check_form_security_code('pm-' . $fromhandle, qa_post_text('code'))) {
qa_pm_delete($loginUserId, qa_get_logged_in_handle(), qa_cookie_get(), $message, $box);
echo "QA_AJAX_RESPONSE\n1\n";
return;
}
}
}
echo "QA_AJAX_RESPONSE\n0\n";

View File

@@ -0,0 +1,44 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Server-side response to Ajax single clicks on wall posts
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
*/
require_once QA_INCLUDE_DIR . 'app/messages.php';
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'app/cookies.php';
require_once QA_INCLUDE_DIR . 'db/selects.php';
$tohandle = qa_post_text('handle');
$start = (int)qa_post_text('start');
$usermessages = qa_db_select_with_pending(qa_db_recent_messages_selectspec(null, null, $tohandle, false, null, $start));
$usermessages = qa_wall_posts_add_rules($usermessages, $start);
foreach ($usermessages as $message) {
if (qa_clicked('m' . $message['messageid'] . '_dodelete') && $message['deleteable']) {
if (qa_check_form_security_code('wall-' . $tohandle, qa_post_text('code'))) {
qa_wall_delete_post(qa_get_logged_in_userid(), qa_get_logged_in_handle(), qa_cookie_get(), $message);
echo "QA_AJAX_RESPONSE\n1\n";
return;
}
}
}
echo "QA_AJAX_RESPONSE\n0\n";

View File

@@ -0,0 +1,95 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Server-side response to Ajax create comment requests
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
*/
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'app/limits.php';
require_once QA_INCLUDE_DIR . 'db/selects.php';
// Load relevant information about this question and the comment parent
$questionid = qa_post_text('c_questionid');
$parentid = qa_post_text('c_parentid');
$userid = qa_get_logged_in_userid();
list($question, $parent, $children) = qa_db_select_with_pending(
qa_db_full_post_selectspec($userid, $questionid),
qa_db_full_post_selectspec($userid, $parentid),
qa_db_full_child_posts_selectspec($userid, $parentid)
);
// Check if the question and parent exist, and whether the user has permission to do this
if (@$question['basetype'] == 'Q' && (@$parent['basetype'] == 'Q' || @$parent['basetype'] == 'A') &&
!qa_user_post_permit_error('permit_post_c', $parent, QA_LIMIT_COMMENTS)
) {
require_once QA_INCLUDE_DIR . 'app/captcha.php';
require_once QA_INCLUDE_DIR . 'app/format.php';
require_once QA_INCLUDE_DIR . 'app/post-create.php';
require_once QA_INCLUDE_DIR . 'app/cookies.php';
require_once QA_INCLUDE_DIR . 'pages/question-view.php';
require_once QA_INCLUDE_DIR . 'pages/question-submit.php';
require_once QA_INCLUDE_DIR . 'util/sort.php';
// Try to create the new comment
$usecaptcha = qa_user_use_captcha(qa_user_level_for_post($question));
$commentid = qa_page_q_add_c_submit($question, $parent, $children, $usecaptcha, $in, $errors);
// If successful, page content will be updated via Ajax
if (isset($commentid)) {
$children = qa_db_select_with_pending(qa_db_full_child_posts_selectspec($userid, $parentid));
$parent = $parent + qa_page_q_post_rules($parent, ($questionid == $parentid) ? null : $question, null, $children);
// in theory we should retrieve the parent's siblings for the above, but they're not going to be relevant
foreach ($children as $key => $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($question, $parent, $children, true, $usershtml, false, null);
$themeclass = qa_load_theme_class(qa_get_site_theme(), 'ajax-comments', null, null);
$themeclass->initialize();
echo "QA_AJAX_RESPONSE\n1\n";
// send back the ID of the new comment
echo qa_anchor('C', $commentid) . "\n";
// send back the HTML
$themeclass->c_list_items($c_list['cs']);
return;
}
}
echo "QA_AJAX_RESPONSE\n0\n"; // fall back to non-Ajax submission if there were any problems

View File

@@ -0,0 +1,49 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Server-side response to Ajax favorite requests
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
*/
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'app/cookies.php';
require_once QA_INCLUDE_DIR . 'app/favorites.php';
require_once QA_INCLUDE_DIR . 'app/format.php';
$entitytype = qa_post_text('entitytype');
$entityid = qa_post_text('entityid');
$setfavorite = qa_post_text('favorite');
$userid = qa_get_logged_in_userid();
if (!qa_check_form_security_code('favorite-' . $entitytype . '-' . $entityid, qa_post_text('code'))) {
echo "QA_AJAX_RESPONSE\n0\n" . qa_lang('misc/form_security_reload');
} elseif (isset($userid)) {
$cookieid = qa_cookie_get();
qa_user_favorite_set($userid, qa_get_logged_in_handle(), $cookieid, $entitytype, $entityid, $setfavorite);
$favoriteform = qa_favorite_form($entitytype, $entityid, $setfavorite, qa_lang($setfavorite ? 'main/remove_favorites' : 'main/add_favorites'));
$themeclass = qa_load_theme_class(qa_get_site_theme(), 'ajax-favorite', null, null);
$themeclass->initialize();
echo "QA_AJAX_RESPONSE\n1\n";
$themeclass->favorite_inner_html($favoriteform);
}

View File

@@ -0,0 +1,47 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Server-side response to Ajax mailing loop requests
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
*/
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'app/mailing.php';
$continue = false;
if (qa_get_logged_in_level() >= 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);

View File

@@ -0,0 +1,47 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Server-side response to Ajax requests to close a notice
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
*/
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'db/notices.php';
require_once QA_INCLUDE_DIR . 'db/users.php';
$noticeid = qa_post_text('noticeid');
if (!qa_check_form_security_code('notice-' . $noticeid, qa_post_text('code')))
echo "QA_AJAX_RESPONSE\n0\n" . qa_lang('misc/form_security_reload');
else {
if ($noticeid == 'visitor')
setcookie('qa_noticed', 1, time() + 86400 * 3650, '/', QA_COOKIE_DOMAIN, (bool)ini_get('session.cookie_secure'), true);
else {
$userid = qa_get_logged_in_userid();
if ($noticeid == 'welcome')
qa_db_user_set_flag($userid, QA_USER_FLAGS_WELCOME_NOTICE, false);
else
qa_db_usernotice_delete($userid, $noticeid);
}
echo "QA_AJAX_RESPONSE\n1";
}

View File

@@ -0,0 +1,48 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Server-side response to Ajax admin recalculation requests
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
*/
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'app/recalc.php';
if (qa_get_logged_in_level() >= QA_USER_LEVEL_ADMIN) {
if (!qa_check_form_security_code('admin/recalc', qa_post_text('code'))) {
$state = '';
$message = qa_lang('misc/form_security_reload');
} else {
$state = qa_post_text('state');
$stoptime = time() + 3;
while (qa_recalc_perform_step($state) && time() < $stoptime) {
// wait
}
$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);

View File

@@ -0,0 +1,75 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Server-side response to Ajax request to view full comment list
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
*/
require_once QA_INCLUDE_DIR . 'db/selects.php';
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'app/cookies.php';
require_once QA_INCLUDE_DIR . 'app/format.php';
require_once QA_INCLUDE_DIR . 'pages/question-view.php';
require_once QA_INCLUDE_DIR . 'util/sort.php';
// Load relevant information about this question and check it exists
$questionid = qa_post_text('c_questionid');
$parentid = qa_post_text('c_parentid');
$userid = qa_get_logged_in_userid();
list($question, $parent, $children, $duplicateposts) = qa_db_select_with_pending(
qa_db_full_post_selectspec($userid, $questionid),
qa_db_full_post_selectspec($userid, $parentid),
qa_db_full_child_posts_selectspec($userid, $parentid),
qa_db_post_duplicates_selectspec($questionid)
);
if (isset($parent)) {
$parent = $parent + qa_page_q_post_rules($parent, null, null, $children + $duplicateposts);
// in theory we should retrieve the parent's parent and siblings for the above, but they're not going to be relevant
foreach ($children as $key => $child) {
$children[$key] = $child + qa_page_q_post_rules($child, $parent, $children, null);
}
$commentsfollows = $questionid == $parentid
? qa_page_q_load_c_follows($question, $children, array(), $duplicateposts)
: qa_page_q_load_c_follows($question, array(), $children);
$usershtml = qa_userids_handles_html($commentsfollows, true);
qa_sort_by($commentsfollows, 'created');
$c_list = qa_page_q_comment_follow_list($question, $parent, $commentsfollows, true, $usershtml, false, null);
$themeclass = qa_load_theme_class(qa_get_site_theme(), 'ajax-comments', null, null);
$themeclass->initialize();
echo "QA_AJAX_RESPONSE\n1\n";
// Send back the HTML
$themeclass->c_list_items($c_list['cs']);
return;
}
echo "QA_AJAX_RESPONSE\n0\n";

View File

@@ -0,0 +1,80 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Server-side response to Ajax version check requests
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
*/
require_once QA_INCLUDE_DIR . 'app/admin.php';
require_once QA_INCLUDE_DIR . 'app/users.php';
if (qa_get_logged_in_level() < QA_USER_LEVEL_ADMIN) {
echo "QA_AJAX_RESPONSE\n0\n" . qa_lang_html('admin/no_privileges');
return;
}
$uri = qa_post_text('uri');
$currentVersion = qa_post_text('version');
$isCore = qa_post_text('isCore') === "true";
if ($isCore) {
$contents = qa_retrieve_url($uri);
if (strlen($contents) > 0) {
if (qa_qa_version_below($contents)) {
$response =
'<a href="https://github.com/q2a/question2answer/releases" style="color:#d00;">' .
qa_lang_html_sub('admin/version_get_x', qa_html('v' . $contents)) .
'</a>';
} else {
$response = qa_html($contents); // Output the current version number
}
} else {
$response = qa_lang_html('admin/version_latest_unknown');
}
} else {
$metadataUtil = new Q2A_Util_Metadata();
$metadata = $metadataUtil->fetchFromUrl($uri);
if (strlen(@$metadata['version']) > 0) {
if (version_compare($currentVersion, $metadata['version']) < 0) {
if (qa_qa_version_below(@$metadata['min_q2a'])) {
$response = strtr(qa_lang_html('admin/version_requires_q2a'), array(
'^1' => qa_html('v' . $metadata['version']),
'^2' => qa_html($metadata['min_q2a']),
));
} elseif (qa_php_version_below(@$metadata['min_php'])) {
$response = strtr(qa_lang_html('admin/version_requires_php'), array(
'^1' => qa_html('v' . $metadata['version']),
'^2' => qa_html($metadata['min_php']),
));
} else {
$response = qa_lang_html_sub('admin/version_get_x', qa_html('v' . $metadata['version']));
if (strlen(@$metadata['uri'])) {
$response = '<a href="' . qa_html($metadata['uri']) . '" style="color:#d00;">' . $response . '</a>';
}
}
} else {
$response = qa_lang_html('admin/version_latest');
}
} else {
$response = qa_lang_html('admin/version_latest_unknown');
}
}
echo "QA_AJAX_RESPONSE\n1\n" . $response;

63
qa-include/ajax/vote.php Normal file
View File

@@ -0,0 +1,63 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Server-side response to Ajax voting requests
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
*/
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'app/cookies.php';
require_once QA_INCLUDE_DIR . 'app/votes.php';
require_once QA_INCLUDE_DIR . 'app/format.php';
require_once QA_INCLUDE_DIR . 'app/options.php';
require_once QA_INCLUDE_DIR . 'db/selects.php';
$postid = qa_post_text('postid');
$vote = qa_post_text('vote');
$code = qa_post_text('code');
$userid = qa_get_logged_in_userid();
$cookieid = qa_cookie_get();
if (!qa_check_form_security_code('vote', $code)) {
$voteerror = qa_lang_html('misc/form_security_reload');
} else {
$post = qa_db_select_with_pending(qa_db_full_post_selectspec($userid, $postid));
$voteerror = qa_vote_error_html($post, $vote, $userid, qa_request());
}
if ($voteerror === false) {
qa_vote_set($post, $userid, qa_get_logged_in_handle(), $cookieid, $vote);
$post = qa_db_select_with_pending(qa_db_full_post_selectspec($userid, $postid));
$fields = qa_post_html_fields($post, $userid, $cookieid, array(), null, array(
'voteview' => qa_get_vote_view($post, true), // behave as if on question page since the vote succeeded
));
$themeclass = qa_load_theme_class(qa_get_site_theme(), 'voting', null, null);
$themeclass->initialize();
echo "QA_AJAX_RESPONSE\n1\n";
$themeclass->voting_inner_html($fields);
return;
}
echo "QA_AJAX_RESPONSE\n0\n" . $voteerror;

View File

@@ -0,0 +1,60 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Server-side response to Ajax wall post requests
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
*/
require_once QA_INCLUDE_DIR . 'app/messages.php';
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'app/cookies.php';
require_once QA_INCLUDE_DIR . 'db/selects.php';
$message = qa_post_text('message');
$tohandle = qa_post_text('handle');
$morelink = qa_post_text('morelink');
$touseraccount = qa_db_select_with_pending(qa_db_user_account_selectspec($tohandle, false));
$loginuserid = qa_get_logged_in_userid();
$errorhtml = qa_wall_error_html($loginuserid, $touseraccount['userid'], $touseraccount['flags']);
if ($errorhtml || !strlen($message) || !qa_check_form_security_code('wall-' . $tohandle, qa_post_text('code'))) {
echo "QA_AJAX_RESPONSE\n0"; // if there's an error, process in non-Ajax way
} else {
$messageid = qa_wall_add_post($loginuserid, qa_get_logged_in_handle(), qa_cookie_get(),
$touseraccount['userid'], $touseraccount['handle'], $message, '');
$touseraccount['wallposts']++; // won't have been updated
$usermessages = qa_db_select_with_pending(qa_db_recent_messages_selectspec(null, null, $touseraccount['userid'], true, qa_opt('page_size_wall')));
$usermessages = qa_wall_posts_add_rules($usermessages, 0);
$themeclass = qa_load_theme_class(qa_get_site_theme(), 'wall', null, null);
$themeclass->initialize();
echo "QA_AJAX_RESPONSE\n1\n";
echo 'm' . $messageid . "\n"; // element in list to be revealed
foreach ($usermessages as $message) {
$themeclass->message_item(qa_wall_post_view($message));
}
if ($morelink && ($touseraccount['wallposts'] > count($usermessages)))
$themeclass->message_item(qa_wall_view_more_link($tohandle, count($usermessages)));
}

671
qa-include/app/admin.php Normal file
View File

@@ -0,0 +1,671 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Functions used in the admin center pages
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Return true if user is logged in with admin privileges. If not, return false
* and set up $qa_content with the appropriate title and error message
* @param $qa_content
* @return bool
*/
function qa_admin_check_privileges(&$qa_content)
{
if (!qa_is_logged_in()) {
require_once QA_INCLUDE_DIR . 'app/format.php';
$qa_content = qa_content_prepare();
$qa_content['title'] = qa_lang_html('admin/admin_title');
$qa_content['error'] = qa_insert_login_links(qa_lang_html('admin/not_logged_in'), qa_request());
return false;
} elseif (qa_get_logged_in_level() < QA_USER_LEVEL_ADMIN) {
$qa_content = qa_content_prepare();
$qa_content['title'] = qa_lang_html('admin/admin_title');
$qa_content['error'] = qa_lang_html('admin/no_privileges');
return false;
}
return true;
}
/**
* Return a sorted array of available languages, [short code] => [long name]
*/
function qa_admin_language_options()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
/**
* @deprecated The hardcoded language ids will be removed in favor of language metadata files.
* See qa-lang/en-GB directory for a clear example of how to use them.
*/
$codetolanguage = array( // 2-letter language codes as per ISO 639-1
'ar' => 'Arabic - العربية',
'az' => 'Azerbaijani - Azərbaycanca',
'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 - 日本語',
'ka' => 'Georgian - ქართული ენა',
'kh' => 'Khmer - ភាសាខ្មែរ',
'ko' => 'Korean - 한국어',
'ku-CKB' => 'Kurdish Central - کورد',
'lt' => 'Lithuanian - Lietuvių',
'lv' => 'Latvian - Latviešu',
'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',
'sq' => 'Albanian - Shqip',
'sr' => 'Serbian - Српски',
'sv' => 'Swedish - Svenska',
'th' => 'Thai - ไทย',
'tr' => 'Turkish - Türkçe',
'ug' => 'Uyghur - ئۇيغۇرچە',
'uk' => 'Ukrainian - Українська',
'uz' => 'Uzbek - ўзбек',
'vi' => 'Vietnamese - Tiếng Việt',
'zh-TW' => 'Chinese Traditional - 繁體中文',
'zh' => 'Chinese Simplified - 简体中文',
);
$options = array('' => 'English (US)');
// find all language folders
$metadataUtil = new Q2A_Util_Metadata();
foreach (glob(QA_LANG_DIR . '*', GLOB_ONLYDIR) as $directory) {
$code = basename($directory);
$metadata = $metadataUtil->fetchFromAddonPath($directory);
if (isset($metadata['name'])) {
$options[$code] = $metadata['name'];
} elseif (isset($codetolanguage[$code])) {
// otherwise use an entry from above
$options[$code] = $codetolanguage[$code];
}
}
asort($options, SORT_STRING);
return $options;
}
/**
* Return a sorted array of available themes, [theme name] => [theme name]
*/
function qa_admin_theme_options()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
$metadataUtil = new Q2A_Util_Metadata();
foreach (glob(QA_THEME_DIR . '*', GLOB_ONLYDIR) as $directory) {
$theme = basename($directory);
$metadata = $metadataUtil->fetchFromAddonPath($directory);
if (empty($metadata)) {
// limit theme parsing to first 8kB
$contents = @file_get_contents($directory . '/qa-styles.css', false, null, 0, 8192);
$metadata = qa_addon_metadata($contents, 'Theme');
}
$options[$theme] = isset($metadata['name']) ? $metadata['name'] : $theme;
}
asort($options, SORT_STRING);
return $options;
}
/**
* Return an array of widget placement options, with keys matching the database value
*/
function qa_admin_place_options()
{
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_low'),
'SB' => qa_lang_html('options/place_side_last'),
);
}
/**
* Return an array of page size options up to $maximum, [page size] => [page size]
* @param $maximum
* @return array
*/
function qa_admin_page_size_options($maximum)
{
$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;
}
/**
* Return an array of options representing matching precision, [value] => [label]
*/
function qa_admin_match_options()
{
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'),
);
}
/**
* Return an array of options representing permission restrictions, [value] => [label]
* ranging from $widest to $narrowest. Set $doconfirms to whether email confirmations are on
* @param $widest
* @param $narrowest
* @param bool $doconfirms
* @param bool $dopoints
* @return array
*/
function qa_admin_permit_options($widest, $narrowest, $doconfirms = true, $dopoints = true)
{
require_once QA_INCLUDE_DIR . '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_APPROVED => qa_lang_html('options/permit_approved'),
QA_PERMIT_APPROVED_POINTS => qa_lang_html('options/permit_approved_points'),
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]);
unset($options[QA_PERMIT_APPROVED_POINTS]);
}
if (QA_FINAL_EXTERNAL_USERS || !qa_opt('moderate_users')) {
unset($options[QA_PERMIT_APPROVED]);
unset($options[QA_PERMIT_APPROVED_POINTS]);
}
return $options;
}
/**
* Return the sub navigation structure common to admin pages
*/
function qa_admin_sub_navigation()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
$navigation = array();
$level = qa_get_logged_in_level();
if ($level >= QA_USER_LEVEL_ADMIN) {
$navigation['admin/general'] = array(
'label' => qa_lang_html('admin/general_title'),
'url' => qa_path_html('admin/general'),
);
$navigation['admin/emails'] = array(
'label' => qa_lang_html('admin/emails_title'),
'url' => qa_path_html('admin/emails'),
);
$navigation['admin/users'] = array(
'label' => qa_lang_html('admin/users_title'),
'url' => qa_path_html('admin/users'),
'selected_on' => array('admin/users$', 'admin/userfields$', 'admin/usertitles$'),
);
$navigation['admin/layout'] = array(
'label' => qa_lang_html('admin/layout_title'),
'url' => qa_path_html('admin/layout'),
);
$navigation['admin/posting'] = array(
'label' => qa_lang_html('admin/posting_title'),
'url' => qa_path_html('admin/posting'),
);
$navigation['admin/viewing'] = array(
'label' => qa_lang_html('admin/viewing_title'),
'url' => qa_path_html('admin/viewing'),
);
$navigation['admin/lists'] = array(
'label' => qa_lang_html('admin/lists_title'),
'url' => qa_path_html('admin/lists'),
);
if (qa_using_categories())
$navigation['admin/categories'] = array(
'label' => qa_lang_html('admin/categories_title'),
'url' => qa_path_html('admin/categories'),
);
$navigation['admin/permissions'] = array(
'label' => qa_lang_html('admin/permissions_title'),
'url' => qa_path_html('admin/permissions'),
);
$navigation['admin/pages'] = array(
'label' => qa_lang_html('admin/pages_title'),
'url' => qa_path_html('admin/pages'),
);
$navigation['admin/feeds'] = array(
'label' => qa_lang_html('admin/feeds_title'),
'url' => qa_path_html('admin/feeds'),
);
$navigation['admin/points'] = array(
'label' => qa_lang_html('admin/points_title'),
'url' => qa_path_html('admin/points'),
);
$navigation['admin/spam'] = array(
'label' => qa_lang_html('admin/spam_title'),
'url' => qa_path_html('admin/spam'),
);
$navigation['admin/caching'] = array(
'label' => qa_lang_html('admin/caching_title'),
'url' => qa_path_html('admin/caching'),
);
$navigation['admin/stats'] = array(
'label' => qa_lang_html('admin/stats_title'),
'url' => qa_path_html('admin/stats'),
);
if (!QA_FINAL_EXTERNAL_USERS)
$navigation['admin/mailing'] = array(
'label' => qa_lang_html('admin/mailing_title'),
'url' => qa_path_html('admin/mailing'),
);
$navigation['admin/plugins'] = array(
'label' => qa_lang_html('admin/plugins_title'),
'url' => qa_path_html('admin/plugins'),
);
}
if (!qa_user_maximum_permit_error('permit_moderate')) {
$count = qa_user_permit_error('permit_moderate') ? null : qa_opt('cache_queuedcount'); // if only in some categories don't show cached count
$navigation['admin/moderate'] = array(
'label' => qa_lang_html('admin/moderate_title') . ($count ? (' (' . $count . ')') : ''),
'url' => qa_path_html('admin/moderate'),
);
}
if (qa_opt('flagging_of_posts') && !qa_user_maximum_permit_error('permit_hide_show')) {
$count = qa_user_permit_error('permit_hide_show') ? null : qa_opt('cache_flaggedcount'); // if only in some categories don't show cached count
$navigation['admin/flagged'] = array(
'label' => qa_lang_html('admin/flagged_title') . ($count ? (' (' . $count . ')') : ''),
'url' => qa_path_html('admin/flagged'),
);
}
if (!qa_user_maximum_permit_error('permit_hide_show') || !qa_user_maximum_permit_error('permit_delete_hidden')) {
$navigation['admin/hidden'] = array(
'label' => qa_lang_html('admin/hidden_title'),
'url' => qa_path_html('admin/hidden'),
);
}
if (!QA_FINAL_EXTERNAL_USERS && qa_opt('moderate_users') && $level >= QA_USER_LEVEL_MODERATOR) {
$count = qa_opt('cache_uapprovecount');
$navigation['admin/approve'] = array(
'label' => qa_lang_html('admin/approve_users_title') . ($count ? (' (' . $count . ')') : ''),
'url' => qa_path_html('admin/approve'),
);
}
return $navigation;
}
/**
* Return the error that needs to displayed on all admin pages, or null if none
*/
function qa_admin_page_error()
{
if (file_exists(QA_INCLUDE_DIR . 'db/install.php')) // file can be removed for extra security
include_once QA_INCLUDE_DIR . 'db/install.php';
if (defined('QA_DB_VERSION_CURRENT') && qa_opt('db_version') < QA_DB_VERSION_CURRENT && qa_get_logged_in_level() >= QA_USER_LEVEL_ADMIN) {
return strtr(
qa_lang_html('admin/upgrade_db'),
array(
'^1' => '<a href="' . qa_path_html('install') . '">',
'^2' => '</a>',
)
);
} elseif (defined('QA_BLOBS_DIRECTORY') && !is_writable(QA_BLOBS_DIRECTORY)) {
return qa_lang_html_sub('admin/blobs_directory_error', qa_html(QA_BLOBS_DIRECTORY));
}
return null;
}
/**
* Return an HTML fragment to display for a URL test which has passed
*/
function qa_admin_url_test_html()
{
return '; font-size:9px; color:#060; font-weight:bold; font-family:arial,sans-serif; border-color:#060;">OK<';
}
/**
* Returns whether a URL path beginning with $requestpart is reserved by the engine or a plugin page module
* @param $requestpart
* @return bool
*/
function qa_admin_is_slug_reserved($requestpart)
{
$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;
}
/**
* Returns true if admin (hidden/flagged/approve/moderate) page $action performed on $entityid is permitted by the
* logged in user and was processed successfully
* @param $entityid
* @param $action
* @return bool
*/
function qa_admin_single_click($entityid, $action)
{
$userid = qa_get_logged_in_userid();
if (!QA_FINAL_EXTERNAL_USERS && ($action == 'userapprove' || $action == 'userblock')) { // approve/block moderated users
require_once QA_INCLUDE_DIR . 'db/selects.php';
$useraccount = qa_db_select_with_pending(qa_db_user_account_selectspec($entityid, true));
if (isset($useraccount) && qa_get_logged_in_level() >= QA_USER_LEVEL_MODERATOR) {
switch ($action) {
case 'userapprove':
if ($useraccount['level'] <= QA_USER_LEVEL_APPROVED) { // don't demote higher level users
require_once QA_INCLUDE_DIR . 'app/users-edit.php';
qa_set_user_level($useraccount['userid'], $useraccount['handle'], QA_USER_LEVEL_APPROVED, $useraccount['level']);
return true;
}
break;
case 'userblock':
require_once QA_INCLUDE_DIR . 'app/users-edit.php';
qa_set_user_blocked($useraccount['userid'], $useraccount['handle'], true);
return true;
break;
}
}
} else { // something to do with a post
require_once QA_INCLUDE_DIR . 'app/posts.php';
$post = qa_post_get_full($entityid);
if (isset($post)) {
$queued = (substr($post['type'], 1) == '_QUEUED');
switch ($action) {
case 'approve':
if ($queued && !qa_user_post_permit_error('permit_moderate', $post)) {
qa_post_set_status($entityid, QA_POST_STATUS_NORMAL, $userid);
return true;
}
break;
case 'reject':
if ($queued && !qa_user_post_permit_error('permit_moderate', $post)) {
qa_post_set_status($entityid, QA_POST_STATUS_HIDDEN, $userid);
return true;
}
break;
case 'hide':
if (!$queued && !qa_user_post_permit_error('permit_hide_show', $post)) {
qa_post_set_status($entityid, QA_POST_STATUS_HIDDEN, $userid);
return true;
}
break;
case 'reshow':
if ($post['hidden'] && !qa_user_post_permit_error('permit_hide_show', $post)) {
qa_post_set_status($entityid, QA_POST_STATUS_NORMAL, $userid);
return true;
}
break;
case 'delete':
if ($post['hidden'] && !qa_user_post_permit_error('permit_delete_hidden', $post)) {
qa_post_delete($entityid);
return true;
}
break;
case 'clearflags':
require_once QA_INCLUDE_DIR . 'app/votes.php';
if (!qa_user_post_permit_error('permit_hide_show', $post)) {
qa_flags_clear_all($post, $userid, qa_get_logged_in_handle(), null);
return true;
}
break;
}
}
}
return false;
}
/**
* Checks for a POSTed click on an admin (hidden/flagged/approve/moderate) page, and refresh the page if processed successfully (non Ajax)
*/
function qa_admin_check_clicks()
{
if (!qa_is_http_post()) {
return null;
}
foreach ($_POST as $field => $value) {
if (strpos($field, 'admin_') !== 0) {
continue;
}
@list($dummy, $entityid, $action) = explode('_', $field);
if (strlen($entityid) && strlen($action)) {
if (!qa_check_form_security_code('admin/click', qa_post_text('code')))
return qa_lang_html('misc/form_security_again');
elseif (qa_admin_single_click($entityid, $action))
qa_redirect(qa_request());
}
}
return null;
}
/**
* Retrieve metadata information from the $contents of a qa-theme.php or qa-plugin.php file, mapping via $fields.
*
* @deprecated Deprecated from 1.7; use `qa_addon_metadata($contents, $type)` instead.
* @param $contents
* @param $fields
* @return array
*/
function qa_admin_addon_metadata($contents, $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;
}
/**
* Return the hash code for the plugin in $directory (without trailing slash), used for in-page navigation on admin/plugins page
* @param $directory
* @return mixed
*/
function qa_admin_plugin_directory_hash($directory)
{
$pluginManager = new Q2A_Plugin_PluginManager();
$hashes = $pluginManager->getHashesForPlugins(array($directory));
return reset($hashes);
}
/**
* Return the URL (relative to the current page) to navigate to the options panel for the plugin in $directory (without trailing slash)
* @param $directory
* @return mixed|string
*/
function qa_admin_plugin_options_path($directory)
{
$hash = qa_admin_plugin_directory_hash($directory);
return qa_path_html('admin/plugins', array('show' => $hash), null, null, $hash);
}
/**
* Return the URL (relative to the current page) to navigate to the options panel for plugin module $name of $type
* @param $type
* @param $name
* @return mixed|string
*/
function qa_admin_module_options_path($type, $name)
{
$info = qa_get_module_info($type, $name);
$dir = basename($info['directory']);
return qa_admin_plugin_options_path($dir);
}

217
qa-include/app/blobs.php Normal file
View File

@@ -0,0 +1,217 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Application-level blob-management functions
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Return the URL which will output $blobid from the database when requested, $absolute or relative
* @param $blobid
* @param bool $absolute
* @return mixed|string
*/
function qa_get_blob_url($blobid, $absolute = false)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
return qa_path('blob', array('qa_blobid' => $blobid), $absolute ? qa_opt('site_url') : null, QA_URL_FORMAT_PARAMS);
}
/**
* Return the full path to the on-disk directory for blob $blobid (subdirectories are named by the first 3 digits of $blobid)
* @param $blobid
* @return mixed|string
*/
function qa_get_blob_directory($blobid)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
return rtrim(QA_BLOBS_DIRECTORY, '/') . '/' . substr(str_pad($blobid, 20, '0', STR_PAD_LEFT), 0, 3);
}
/**
* Return the full page and filename of blob $blobid which is in $format ($format is used as the file name suffix e.g. .jpg)
* @param $blobid
* @param $format
* @return mixed|string
*/
function qa_get_blob_filename($blobid, $format)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
return qa_get_blob_directory($blobid) . '/' . $blobid . '.' . preg_replace('/[^A-Za-z0-9]/', '', $format);
}
/**
* Create a new blob (storing the content in the database or on disk as appropriate) with $content and $format, returning its blobid.
* Pass the original name of the file uploaded in $sourcefilename and the $userid, $cookieid and $ip of the user creating it
* @param $content
* @param $format
* @param $sourcefilename
* @param $userid
* @param $cookieid
* @param $ip
* @return mixed|null|string
*/
function qa_create_blob($content, $format, $sourcefilename = null, $userid = null, $cookieid = null, $ip = null)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'db/blobs.php';
$blobid = qa_db_blob_create(defined('QA_BLOBS_DIRECTORY') ? null : $content, $format, $sourcefilename, $userid, $cookieid, $ip);
if (isset($blobid) && defined('QA_BLOBS_DIRECTORY')) {
// still write content to the database if writing to disk failed
if (!qa_write_blob_file($blobid, $content, $format))
qa_db_blob_set_content($blobid, $content);
}
return $blobid;
}
/**
* Write the on-disk file for blob $blobid with $content and $format. Returns true if the write succeeded, false otherwise.
* @param $blobid
* @param $content
* @param $format
* @return bool|mixed
*/
function qa_write_blob_file($blobid, $content, $format)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
$written = false;
$directory = qa_get_blob_directory($blobid);
if (is_dir($directory) || mkdir($directory, fileperms(rtrim(QA_BLOBS_DIRECTORY, '/')) & 0777)) {
$filename = qa_get_blob_filename($blobid, $format);
$file = fopen($filename, 'xb');
if (is_resource($file)) {
if (fwrite($file, $content) >= strlen($content))
$written = true;
fclose($file);
if (!$written)
unlink($filename);
}
}
return $written;
}
/**
* Retrieve blob $blobid from the database, reading the content from disk if appropriate
* @param $blobid
* @return array|mixed|null
*/
function qa_read_blob($blobid)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'db/blobs.php';
$blob = qa_db_blob_read($blobid);
if (isset($blob) && defined('QA_BLOBS_DIRECTORY') && !isset($blob['content']))
$blob['content'] = qa_read_blob_file($blobid, $blob['format']);
return $blob;
}
/**
* Read the content of blob $blobid in $format from disk. On failure, it will return false.
* @param $blobid
* @param $format
* @return mixed|null|string
*/
function qa_read_blob_file($blobid, $format)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
$filename = qa_get_blob_filename($blobid, $format);
if (is_readable($filename))
return file_get_contents($filename);
else
return null;
}
/**
* Delete blob $blobid from the database, and remove the on-disk file if appropriate
* @param $blobid
* @return mixed
*/
function qa_delete_blob($blobid)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'db/blobs.php';
if (defined('QA_BLOBS_DIRECTORY')) {
$blob = qa_db_blob_read($blobid);
if (isset($blob) && !isset($blob['content']))
unlink(qa_get_blob_filename($blobid, $blob['format']));
}
qa_db_blob_delete($blobid);
}
/**
* Delete the on-disk file for blob $blobid in $format
* @param $blobid
* @param $format
* @return mixed
*/
function qa_delete_blob_file($blobid, $format)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
unlink(qa_get_blob_filename($blobid, $format));
}
/**
* Check if blob $blobid exists
* @param $blobid
* @return bool|mixed
*/
function qa_blob_exists($blobid)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'db/blobs.php';
return qa_db_blob_exists($blobid);
}

123
qa-include/app/captcha.php Normal file
View File

@@ -0,0 +1,123 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Wrapper functions and utilities for captcha modules
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Return whether a captcha module has been selected and it indicates that it is fully set up to go.
*/
function qa_captcha_available()
{
$module = qa_load_module('captcha', qa_opt('captcha_module'));
return isset($module) && (!method_exists($module, 'allow_captcha') || $module->allow_captcha());
}
/**
* Return an HTML string explaining $captchareason (from qa_user_captcha_reason()) to the user about why they are seeing a captcha
* @param $captchareason
* @return mixed|null|string
*/
function qa_captcha_reason_note($captchareason)
{
$notehtml = null;
switch ($captchareason) {
case 'login':
$notehtml = qa_insert_login_links(qa_lang_html('misc/captcha_login_fix'));
break;
case 'confirm':
$notehtml = qa_insert_login_links(qa_lang_html('misc/captcha_confirm_fix'));
break;
case 'approve':
$notehtml = qa_lang_html('misc/captcha_approve_fix');
break;
}
return $notehtml;
}
/**
* Prepare $qa_content for showing a captcha, adding the element to $fields, given previous $errors, and a $note to display.
* Returns JavaScript required to load CAPTCHA when field is shown by user (e.g. clicking comment button).
* @param $qa_content
* @param $fields
* @param $errors
* @param $note
* @return string
*/
function qa_set_up_captcha_field(&$qa_content, &$fields, $errors, $note = null)
{
if (!qa_captcha_available())
return '';
$captcha = qa_load_module('captcha', qa_opt('captcha_module'));
// workaround for reCAPTCHA, to load multiple instances via JS
$count = @++$qa_content['qa_captcha_count'];
if ($count > 1) {
// use blank captcha in order to load via JS
$html = '';
} else {
// first captcha is always loaded explicitly
$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' => '<div id="qa_captcha_div_' . $count . '">' . $html . '</div>',
'error' => @array_key_exists('captcha', $errors) ? qa_lang_html('misc/captcha_error') : null,
'note' => $note,
);
return "if (!document.getElementById('qa_captcha_div_" . $count . "').hasChildNodes()) { recaptcha_load('qa_captcha_div_" . $count . "'); }";
}
/**
* Check if captcha is submitted correctly, and if not, set $errors['captcha'] to a descriptive string.
* @param $errors
* @return bool
*/
function qa_captcha_validate_post(&$errors)
{
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;
}

View File

@@ -0,0 +1,74 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: User cookie management (application level) for tracking anonymous posts
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Return the user identification cookie sent by the browser for this page request, or null if none
*/
function qa_cookie_get()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
return isset($_COOKIE['qa_id']) ? qa_gpc_to_string($_COOKIE['qa_id']) : null;
}
/**
* Return user identification cookie sent by browser if valid, or create a new one if not.
* Either way, extend for another year (this is used when an anonymous post is created)
*/
function qa_cookie_get_create()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'db/cookies.php';
$cookieid = qa_cookie_get();
if (!isset($cookieid) || !qa_db_cookie_exists($cookieid)) {
// cookie is invalid
$cookieid = qa_db_cookie_create(qa_remote_ip_address());
}
setcookie('qa_id', $cookieid, time() + 86400 * 365, '/', QA_COOKIE_DOMAIN, (bool)ini_get('session.cookie_secure'), true);
$_COOKIE['qa_id'] = $cookieid;
return $cookieid;
}
/**
* Called after a database write $action performed by a user identified by $cookieid,
* relating to $questionid, $answerid and/or $commentid
* @param $cookieid
* @param $action
*/
function qa_cookie_report_action($cookieid, $action)
{
require_once QA_INCLUDE_DIR . 'db/cookies.php';
qa_db_cookie_written($cookieid, qa_remote_ip_address());
}

184
qa-include/app/emails.php Normal file
View File

@@ -0,0 +1,184 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Wrapper functions for sending email notifications to users
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
require_once QA_INCLUDE_DIR . 'app/options.php';
/**
* Suspend the sending of all email notifications via qa_send_notification(...) if $suspend is true, otherwise
* reinstate it. A counter is kept to allow multiple calls.
* @param bool $suspend
*/
function qa_suspend_notifications($suspend = true)
{
global $qa_notifications_suspended;
$qa_notifications_suspended += ($suspend ? 1 : -1);
}
/**
* Send email to person with $userid and/or $email and/or $handle (null/invalid values are ignored or retrieved from
* user database as appropriate). Email uses $subject and $body, after substituting each key in $subs with its
* corresponding value, plus applying some standard substitutions such as ^site_title, ^site_url, ^handle and ^email.
* @param $userid
* @param $email
* @param $handle
* @param $subject
* @param $body
* @param $subs
* @param bool $html
* @return bool
*/
function qa_send_notification($userid, $email, $handle, $subject, $body, $subs, $html = false)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
global $qa_notifications_suspended;
if ($qa_notifications_suspended > 0)
return false;
require_once QA_INCLUDE_DIR . 'db/selects.php';
require_once QA_INCLUDE_DIR . '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(
array(
'columns' => array('email', 'handle'),
'source' => '^users WHERE userid = #',
'arguments' => array($userid),
'single' => 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['^site_url'] = qa_opt('site_url');
$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' => $html,
));
}
return false;
}
/**
* Send the email based on the $params array - the following keys are required (some can be empty): fromemail,
* fromname, toemail, toname, subject, body, html
* @param $params
* @return bool
*/
function qa_send_email($params)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
// @error_log(print_r($params, true));
require_once QA_INCLUDE_DIR . 'vendor/PHPMailer/PHPMailerAutoload.php';
PHPMailer::$validator = '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']);
if (!empty($params['replytoemail'])) {
$mailer->addReplyTo($params['replytoemail'], $params['replytoname']);
}
$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');
} else {
$mailer->SMTPOptions = array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
),
);
}
if (qa_opt('smtp_authenticate')) {
$mailer->SMTPAuth = true;
$mailer->Username = qa_opt('smtp_username');
$mailer->Password = qa_opt('smtp_password');
}
}
$send_status = $mailer->send();
if (!$send_status) {
@error_log('PHP Question2Answer email send error: ' . $mailer->ErrorInfo);
}
return $send_status;
}

100
qa-include/app/events.php Normal file
View File

@@ -0,0 +1,100 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Handles the submission of events to the database (application level)
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
require_once QA_INCLUDE_DIR . 'db/events.php';
require_once QA_INCLUDE_DIR . 'app/updates.php';
/**
* Add appropriate events to the database for an action performed on a question. The event of type $updatetype relates
* to $lastpostid whose antecedent question is $questionid, and was caused by $lastuserid. Pass a unix $timestamp for
* the event time or leave as null to use now. This will add an event to $questionid's and $lastuserid's streams. If
* $otheruserid is set, it will also add an notification-style event for that user, unless they are the one who did it.
* @param $questionid
* @param $lastpostid
* @param $updatetype
* @param $lastuserid
* @param $otheruserid
* @param $timestamp
*/
function qa_create_event_for_q_user($questionid, $lastpostid, $updatetype, $lastuserid, $otheruserid = null, $timestamp = null)
{
qa_db_event_create_for_entity(QA_ENTITY_QUESTION, $questionid, $questionid, $lastpostid, $updatetype, $lastuserid, $timestamp); // anyone who favorited the question
if (isset($lastuserid) && !QA_FINAL_EXTERNAL_USERS)
qa_db_event_create_for_entity(QA_ENTITY_USER, $lastuserid, $questionid, $lastpostid, $updatetype, $lastuserid, $timestamp); // anyone who favorited the user who did it
if (isset($otheruserid) && ($otheruserid != $lastuserid))
qa_db_event_create_not_entity($otheruserid, $questionid, $lastpostid, $updatetype, $lastuserid, $timestamp); // possible other user to be informed
}
/**
* Add appropriate events to the database for an action performed on a set of tags in $tagstring (namely, a question
* being created with those tags or having one of those tags added afterwards). The event of type $updatetype relates
* to the question $questionid, and was caused by $lastuserid. Pass a unix $timestamp for the event time or leave as
* null to use now.
* @param $tagstring
* @param $questionid
* @param $updatetype
* @param $lastuserid
* @param $timestamp
*/
function qa_create_event_for_tags($tagstring, $questionid, $updatetype, $lastuserid, $timestamp = null)
{
require_once QA_INCLUDE_DIR . 'util/string.php';
require_once QA_INCLUDE_DIR . 'db/post-create.php';
$tagwordids = qa_db_word_mapto_ids(array_unique(qa_tagstring_to_tags($tagstring)));
foreach ($tagwordids as $wordid) {
qa_db_event_create_for_entity(QA_ENTITY_TAG, $wordid, $questionid, $questionid, $updatetype, $lastuserid, $timestamp);
}
}
/**
* Add appropriate events to the database for an action performed on $categoryid (namely, a question being created in
* that category or being moved to it later on), along with all of its ancestor categories. The event of type
* $updatetype relates to the question $questionid, and was caused by $lastuserid. Pass a unix $timestamp for the event
* time or leave as null to use now.
* @param $categoryid
* @param $questionid
* @param $updatetype
* @param $lastuserid
* @param $timestamp
*/
function qa_create_event_for_category($categoryid, $questionid, $updatetype, $lastuserid, $timestamp = null)
{
if (isset($categoryid)) {
require_once QA_INCLUDE_DIR . 'db/selects.php';
require_once QA_INCLUDE_DIR . 'app/format.php';
$categories = qa_category_path(qa_db_single_select(qa_db_category_nav_selectspec($categoryid, true)), $categoryid);
foreach ($categories as $category) {
qa_db_event_create_for_entity(QA_ENTITY_CATEGORY, $category['categoryid'], $questionid, $questionid, $updatetype, $lastuserid, $timestamp);
}
}
}

View File

@@ -0,0 +1,211 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Handles favoriting and unfavoriting (application level)
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Set an entity to be favorited or removed from favorites. Handles event reporting.
*
* @param int $userid ID of user assigned to the favorite
* @param string $handle Username of user
* @param string $cookieid Cookie ID of user
* @param string $entitytype Entity type code (one of QA_ENTITY_* constants)
* @param string $entityid ID of the entity being favorited (e.g. postid for questions)
* @param bool $favorite Whether to add favorite (true) or remove favorite (false)
*/
function qa_user_favorite_set($userid, $handle, $cookieid, $entitytype, $entityid, $favorite)
{
require_once QA_INCLUDE_DIR . 'db/favorites.php';
require_once QA_INCLUDE_DIR . 'app/limits.php';
require_once QA_INCLUDE_DIR . 'app/updates.php';
// Make sure the user is not favoriting themselves
if ($entitytype == QA_ENTITY_USER && $userid == $entityid) {
return;
}
if ($favorite)
qa_db_favorite_create($userid, $entitytype, $entityid);
else
qa_db_favorite_delete($userid, $entitytype, $entityid);
switch ($entitytype) {
case QA_ENTITY_QUESTION:
$action = $favorite ? 'q_favorite' : 'q_unfavorite';
$params = array('postid' => $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);
}
/**
* Returns content to set in $qa_content['q_list'] for a user's favorite $questions. Pre-generated
* user HTML in $usershtml.
* @param $questions
* @param $usershtml
* @return array
*/
function qa_favorite_q_list_view($questions, $usershtml)
{
$q_list = array(
'qs' => array(),
);
if (count($questions) === 0)
return $q_list;
$q_list['form'] = array(
'tags' => 'method="post" action="' . qa_self_html() . '"',
'hidden' => array(
'code' => qa_get_form_security_code('vote'),
),
);
$defaults = qa_post_html_defaults('Q');
foreach ($questions as $question) {
$q_list['qs'][] = qa_post_html_fields($question, qa_get_logged_in_userid(), qa_cookie_get(),
$usershtml, null, qa_post_html_options($question, $defaults));
}
return $q_list;
}
/**
* Returns content to set in $qa_content['ranking_users'] for a user's favorite $users. Pre-generated
* user HTML in $usershtml.
* @param $users
* @param $usershtml
* @return array|null
*/
function qa_favorite_users_view($users, $usershtml)
{
if (QA_FINAL_EXTERNAL_USERS)
return null;
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'app/format.php';
$ranking = array(
'items' => array(),
'rows' => ceil(count($users) / qa_opt('columns_users')),
'type' => 'users',
);
foreach ($users as $user) {
$avatarhtml = qa_get_user_avatar_html($user['flags'], $user['email'], $user['handle'],
$user['avatarblobid'], $user['avatarwidth'], $user['avatarheight'], qa_opt('avatar_users_size'), true);
$ranking['items'][] = array(
'avatar' => $avatarhtml,
'label' => $usershtml[$user['userid']],
'score' => qa_html(qa_format_number($user['points'], 0, true)),
'raw' => $user,
);
}
return $ranking;
}
/**
* Returns content to set in $qa_content['ranking_tags'] for a user's favorite $tags.
* @param $tags
* @return array
*/
function qa_favorite_tags_view($tags)
{
require_once QA_INCLUDE_DIR . 'app/format.php';
$ranking = array(
'items' => array(),
'rows' => ceil(count($tags) / qa_opt('columns_tags')),
'type' => 'tags',
);
foreach ($tags as $tag) {
$ranking['items'][] = array(
'label' => qa_tag_html($tag['word'], false, true),
'count' => qa_html(qa_format_number($tag['tagcount'], 0, true)),
);
}
return $ranking;
}
/**
* Returns content to set in $qa_content['nav_list_categories'] for a user's favorite $categories.
* @param $categories
* @return array
*/
function qa_favorite_categories_view($categories)
{
require_once QA_INCLUDE_DIR . 'app/format.php';
$nav_list_categories = array(
'nav' => array(),
'type' => 'browse-cat',
);
foreach ($categories as $category) {
$cat_url = qa_path_html('questions/' . implode('/', array_reverse(explode('/', $category['backpath']))));
$cat_anchor = $category['qcount'] == 1
? qa_lang_html_sub('main/1_question', '1', '1')
: qa_lang_html_sub('main/x_questions', qa_format_number($category['qcount'], 0, true));
$cat_descr = strlen($category['content']) ? qa_html(' - ' . $category['content']) : '';
$nav_list_categories['nav'][$category['categoryid']] = array(
'label' => qa_html($category['title']),
'state' => 'open',
'favorited' => true,
'note' => ' - <a href="' . $cat_url . '">' . $cat_anchor . '</a>' . $cat_descr,
);
}
return $nav_list_categories;
}

2396
qa-include/app/format.php Normal file

File diff suppressed because it is too large Load Diff

313
qa-include/app/limits.php Normal file
View File

@@ -0,0 +1,313 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Monitoring and rate-limiting user actions (application level)
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
define('QA_LIMIT_QUESTIONS', 'Q');
define('QA_LIMIT_ANSWERS', 'A');
define('QA_LIMIT_COMMENTS', 'C');
define('QA_LIMIT_VOTES', 'V');
define('QA_LIMIT_REGISTRATIONS', 'R');
define('QA_LIMIT_LOGINS', 'L');
define('QA_LIMIT_UPLOADS', 'U');
define('QA_LIMIT_FLAGS', 'F');
define('QA_LIMIT_MESSAGES', 'M'); // i.e. private messages
define('QA_LIMIT_WALL_POSTS', 'W');
/**
* How many more times the logged in user (and requesting IP address) can perform an action this hour.
* @param string $action One of the QA_LIMIT_* constants defined above.
* @return int
*/
function qa_user_limits_remaining($action)
{
$userlimits = qa_db_get_pending_result('userlimits', qa_db_user_limits_selectspec(qa_get_logged_in_userid()));
$iplimits = qa_db_get_pending_result('iplimits', qa_db_ip_limits_selectspec(qa_remote_ip_address()));
return qa_limits_calc_remaining($action, @$userlimits[$action], @$iplimits[$action]);
}
/**
* Return how many more times user $userid and/or the requesting IP can perform $action (a QA_LIMIT_* constant) this hour.
* @deprecated Deprecated from 1.6.0; use `qa_user_limits_remaining($action)` instead.
* @param int $userid
* @param string $action
* @return mixed
*/
function qa_limits_remaining($userid, $action)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'db/limits.php';
$dblimits = qa_db_limits_get($userid, qa_remote_ip_address(), $action);
return qa_limits_calc_remaining($action, @$dblimits['user'], @$dblimits['ip']);
}
/**
* Calculate how many more times an action can be performed this hour by the user/IP.
* @param string $action One of the QA_LIMIT_* constants defined above.
* @param array $userlimits Limits for the user.
* @param array $iplimits Limits for the requesting IP.
* @return mixed
*/
function qa_limits_calc_remaining($action, $userlimits, $iplimits)
{
switch ($action) {
case QA_LIMIT_QUESTIONS:
$usermax = qa_opt('max_rate_user_qs');
$ipmax = qa_opt('max_rate_ip_qs');
break;
case QA_LIMIT_ANSWERS:
$usermax = qa_opt('max_rate_user_as');
$ipmax = qa_opt('max_rate_ip_as');
break;
case QA_LIMIT_COMMENTS:
$usermax = qa_opt('max_rate_user_cs');
$ipmax = qa_opt('max_rate_ip_cs');
break;
case QA_LIMIT_VOTES:
$usermax = qa_opt('max_rate_user_votes');
$ipmax = qa_opt('max_rate_ip_votes');
break;
case QA_LIMIT_REGISTRATIONS:
$usermax = 1; // not really relevant
$ipmax = qa_opt('max_rate_ip_registers');
break;
case QA_LIMIT_LOGINS:
$usermax = 1; // not really relevant
$ipmax = qa_opt('max_rate_ip_logins');
break;
case QA_LIMIT_UPLOADS:
$usermax = qa_opt('max_rate_user_uploads');
$ipmax = qa_opt('max_rate_ip_uploads');
break;
case QA_LIMIT_FLAGS:
$usermax = qa_opt('max_rate_user_flags');
$ipmax = qa_opt('max_rate_ip_flags');
break;
case QA_LIMIT_MESSAGES:
case QA_LIMIT_WALL_POSTS:
$usermax = qa_opt('max_rate_user_messages');
$ipmax = qa_opt('max_rate_ip_messages');
break;
default:
qa_fatal_error('Unknown limit code in qa_limits_calc_remaining: ' . $action);
break;
}
$period = (int)(qa_opt('db_time') / 3600);
return max(0, min(
$usermax - (@$userlimits['period'] == $period ? $userlimits['count'] : 0),
$ipmax - (@$iplimits['period'] == $period ? $iplimits['count'] : 0)
));
}
/**
* Determine whether the requesting IP address has been blocked from write operations.
* @return bool
*/
function qa_is_ip_blocked()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
global $qa_curr_ip_blocked;
// return cached value early
if (isset($qa_curr_ip_blocked))
return $qa_curr_ip_blocked;
$qa_curr_ip_blocked = false;
$blockipclauses = qa_block_ips_explode(qa_opt('block_ips_write'));
$ip = qa_remote_ip_address();
foreach ($blockipclauses as $blockipclause) {
if (qa_block_ip_match($ip, $blockipclause)) {
$qa_curr_ip_blocked = true;
break;
}
}
return $qa_curr_ip_blocked;
}
/**
* Return an array of the clauses within $blockipstring, each of which can contain hyphens or asterisks
* @param $blockipstring
* @return array
*/
function qa_block_ips_explode($blockipstring)
{
$blockipstring = preg_replace('/\s*\-\s*/', '-', $blockipstring); // special case for 'x.x.x.x - x.x.x.x'
return preg_split('/[^0-9A-Fa-f\.:\-\*]/', $blockipstring, -1, PREG_SPLIT_NO_EMPTY);
}
/**
* Checks if the IP address is matched by the individual block clause, which can contain a hyphen or asterisk
* @param string $ip The IP address
* @param string $blockipclause The IP/clause to check against, e.g. 127.0.0.*
* @return bool
*/
function qa_block_ip_match($ip, $blockipclause)
{
$ipv4 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
$blockipv4 = filter_var($blockipclause, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
// allow faster return if IP and blocked IP are plain IPv4 strings (IPv6 requires expanding)
if ($ipv4 && $blockipv4) {
return $ip === $blockipclause;
}
if (filter_var($ip, FILTER_VALIDATE_IP)) {
if (preg_match('/^(.*)\-(.*)$/', $blockipclause, $matches)) {
// match IP range
if (filter_var($matches[1], FILTER_VALIDATE_IP) && filter_var($matches[2], FILTER_VALIDATE_IP)) {
return qa_ip_between($ip, $matches[1], $matches[2]);
}
} elseif (strlen($blockipclause)) {
// normalize IPv6 addresses
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$ip = qa_ipv6_expand($ip);
$blockipclause = qa_ipv6_expand($blockipclause);
}
// expand wildcards; preg_quote misses hyphens but that is OK here
return preg_match('/^' . str_replace('\\*', '([0-9A-Fa-f]+)', preg_quote($blockipclause, '/')) . '$/', $ip) > 0;
}
}
return false;
}
/**
* Check if IP falls between two others.
* @param $ip
* @param $startip
* @param $endip
* @return bool
*/
function qa_ip_between($ip, $startip, $endip)
{
$uip = unpack('C*', @inet_pton($ip));
$ustartip = unpack('C*', @inet_pton($startip));
$uendip = unpack('C*', @inet_pton($endip));
if (count($uip) != count($ustartip) || count($uip) != count($uendip))
return false;
foreach ($uip as $i => $byte) {
if ($byte < $ustartip[$i] || $byte > $uendip[$i]) {
return false;
}
}
return true;
}
/**
* Expands an IPv6 address (possibly containing wildcards), e.g. ::ffff:1 to 0000:0000:0000:0000:0000:0000:ffff:0001.
* Based on http://stackoverflow.com/a/12095836/753676
* @param string $ip The IP address to expand.
* @return string
*/
function qa_ipv6_expand($ip)
{
$ipv6_wildcard = false;
$wildcards = '';
$wildcards_matched = array();
if (strpos($ip, "*") !== false) {
$ipv6_wildcard = true;
}
if ($ipv6_wildcard) {
$wildcards = explode(":", $ip);
foreach ($wildcards as $index => $value) {
if ($value == "*") {
$wildcards_matched[] = count($wildcards) - 1 - $index;
$wildcards[$index] = "0";
}
}
$ip = implode($wildcards, ":");
}
$hex = unpack("H*hex", @inet_pton($ip));
$ip = substr(preg_replace("/([0-9A-Fa-f]{4})/", "$1:", $hex['hex']), 0, -1);
if ($ipv6_wildcard) {
$wildcards = explode(":", $ip);
foreach ($wildcards_matched as $value) {
$i = count($wildcards) - 1 - $value;
$wildcards[$i] = "*";
}
$ip = implode($wildcards, ":");
}
return $ip;
}
/**
* Called after a database write $action performed by a user identified by $userid and/or $cookieid.
* @param int $userid
* @param string $cookieid
* @param string $action
* @param int $questionid
* @param int $answerid
* @param int $commentid
*/
function qa_report_write_action($userid, $cookieid, $action, $questionid, $answerid, $commentid)
{
}
/**
* Take note for rate limits that a user and/or the requesting IP just performed an action.
* @param int $userid User performing the action.
* @param string $action One of the QA_LIMIT_* constants defined above.
* @return mixed
*/
function qa_limits_increment($userid, $action)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . '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);
}

168
qa-include/app/mailing.php Normal file
View File

@@ -0,0 +1,168 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Functions for sending a mailing to all users
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Start a mailing to all users, unless one has already been started
*/
function qa_mailing_start()
{
require_once QA_INCLUDE_DIR . 'db/admin.php';
if (strlen(qa_opt('mailing_last_userid')) == 0) {
qa_opt('mailing_last_timestamp', time());
qa_opt('mailing_last_userid', '0');
qa_opt('mailing_total_users', qa_db_count_users());
qa_opt('mailing_done_users', 0);
}
}
/**
* Stop a mailing to all users
*/
function qa_mailing_stop()
{
qa_opt('mailing_last_timestamp', '');
qa_opt('mailing_last_userid', '');
qa_opt('mailing_done_users', '');
qa_opt('mailing_total_users', '');
}
/**
* Allow the mailing to proceed forwards, for the appropriate amount of time and users, based on the options
*/
function qa_mailing_perform_step()
{
require_once QA_INCLUDE_DIR . 'db/users.php';
require_once QA_INCLUDE_DIR . 'app/users.php';
$lastuserid = qa_opt('mailing_last_userid');
if (strlen($lastuserid) == 0) {
return;
}
$thistime = time();
$lasttime = qa_opt('mailing_last_timestamp');
$perminute = qa_opt('mailing_per_minute');
if (($lasttime - $thistime) > 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) {
return;
}
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));
$isModeratingUsers = qa_opt('moderate_users');
foreach ($users as $user) {
if (($user['flags'] & QA_USER_FLAGS_NO_MAILINGS) || // exclude users who don't want to get the mailings
($user['flags'] & QA_USER_FLAGS_USER_BLOCKED) || // exclude blocked users
($isModeratingUsers && ($user['level'] < QA_USER_LEVEL_APPROVED))) { // if moderating users exclude unapproved users
continue;
}
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();
}
}
/**
* 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
* @param $userid
* @param $handle
* @param $email
* @param $emailcode
* @return bool
*/
function qa_mailing_send_one($userid, $handle, $email, $emailcode)
{
require_once QA_INCLUDE_DIR . 'app/emails.php';
require_once QA_INCLUDE_DIR . 'db/users.php';
if (!strlen(trim($emailcode))) {
$emailcode = qa_db_user_rand_emailcode();
qa_db_user_set($userid, 'emailcode', $emailcode);
}
$unsubscribeurl = qa_path_absolute('unsubscribe', array('c' => $emailcode, 'u' => $handle));
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,
));
}
/**
* Return a message describing current progress in the mailing
*/
function qa_mailing_progress_message()
{
require_once QA_INCLUDE_DIR . 'app/format.php';
if (strlen(qa_opt('mailing_last_userid'))) {
return strtr(qa_lang('admin/mailing_progress'), array(
'^1' => qa_format_number(qa_opt('mailing_done_users')),
'^2' => qa_format_number(qa_opt('mailing_total_users')),
));
}
return null;
}

229
qa-include/app/messages.php Normal file
View File

@@ -0,0 +1,229 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Handling private or public messages (wall posts)
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Returns an HTML string describing the reason why user $fromuserid cannot post on the wall of $touserid who has
* user flags $touserflags. If there is no such reason the function returns false.
* @param $fromuserid
* @param $touserid
* @param $touserflags
* @return bool|mixed|string
*/
function qa_wall_error_html($fromuserid, $touserid, $touserflags)
{
require_once QA_INCLUDE_DIR . 'app/limits.php';
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
if (!QA_FINAL_EXTERNAL_USERS && qa_opt('allow_user_walls')) {
if (($touserflags & QA_USER_FLAGS_NO_WALL_POSTS) && !(isset($fromuserid) && $fromuserid == $touserid))
return qa_lang_html('profile/post_wall_blocked');
else {
switch (qa_user_permit_error('permit_post_wall', QA_LIMIT_WALL_POSTS)) {
case 'limit':
return qa_lang_html('profile/post_wall_limit');
break;
case 'login':
return qa_insert_login_links(qa_lang_html('profile/post_wall_must_login'), qa_request());
break;
case 'confirm':
return qa_insert_login_links(qa_lang_html('profile/post_wall_must_confirm'), qa_request());
break;
case 'approve':
return strtr(qa_lang_html('profile/post_wall_must_be_approved'), array(
'^1' => '<a href="' . qa_path_html('account') . '">',
'^2' => '</a>',
));
break;
case false:
return false;
break;
}
}
}
return qa_lang_html('users/no_permission');
}
/**
* Adds a post to the wall of user $touserid with handle $tohandle, containing $content in $format (e.g. '' for text or 'html')
* The post is by user $userid with handle $handle, and $cookieid is the user's current cookie (used for reporting the event).
* @param $userid
* @param $handle
* @param $cookieid
* @param $touserid
* @param $tohandle
* @param $content
* @param $format
* @return mixed
*/
function qa_wall_add_post($userid, $handle, $cookieid, $touserid, $tohandle, $content, $format)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'app/format.php';
require_once QA_INCLUDE_DIR . 'db/messages.php';
$messageid = qa_db_message_create($userid, $touserid, $content, $format, true);
qa_db_user_recount_posts($touserid);
qa_report_event('u_wall_post', $userid, $handle, $cookieid, array(
'userid' => $touserid,
'handle' => $tohandle,
'messageid' => $messageid,
'content' => $content,
'format' => $format,
'text' => qa_viewer_text($content, $format),
));
return $messageid;
}
/**
* Deletes the wall post described in $message (as obtained via qa_db_recent_messages_selectspec()). The deletion was performed
* by user $userid with handle $handle, and $cookieid is the user's current cookie (all used for reporting the event).
* @param $userid
* @param $handle
* @param $cookieid
* @param $message
*/
function qa_wall_delete_post($userid, $handle, $cookieid, $message)
{
require_once QA_INCLUDE_DIR . 'db/messages.php';
qa_db_message_delete($message['messageid']);
qa_db_user_recount_posts($message['touserid']);
qa_report_event('u_wall_delete', $userid, $handle, $cookieid, array(
'messageid' => $message['messageid'],
'oldmessage' => $message,
));
}
/**
* Return the list of messages in $usermessages (as obtained via qa_db_recent_messages_selectspec()) with additional
* fields indicating what actions can be performed on them by the current user. The messages were retrieved beginning
* at offset $start in the database. Currently only 'deleteable' is relevant.
* @param $usermessages
* @param $start
* @return mixed
*/
function qa_wall_posts_add_rules($usermessages, $start)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
$userid = qa_get_logged_in_userid();
// reuse "Hiding or showing any post" and "Deleting hidden posts" permissions
$userdeleteall = !(qa_user_permit_error('permit_hide_show') || qa_user_permit_error('permit_delete_hidden'));
$userrecent = $start == 0 && isset($userid); // User can delete all of the recent messages they wrote on someone's wall...
foreach ($usermessages as $key => $message) {
if ($message['fromuserid'] != $userid)
$userrecent = false; // ... until we come across one that they didn't write (which could be a reply)
$usermessages[$key]['deleteable'] =
$message['touserid'] == $userid || // if it's this user's wall
($userrecent && $message['fromuserid'] == $userid) || // if it's one the user wrote that no one replied to yet
$userdeleteall; // if the user has enough permissions to delete from any wall
}
return $usermessages;
}
/**
* Returns an element to add to $qa_content['message_list']['messages'] for $message (as obtained via
* qa_db_recent_messages_selectspec() and then qa_wall_posts_add_rules()).
* @param $message
* @return array
*/
function qa_wall_post_view($message)
{
require_once QA_INCLUDE_DIR . 'app/format.php';
$options = qa_message_html_defaults();
$htmlfields = qa_message_html_fields($message, $options);
if ($message['deleteable']) {
$htmlfields['form'] = array(
'style' => 'light',
'buttons' => array(
'delete' => array(
'tags' => 'name="m' . qa_html($message['messageid']) . '_dodelete" onclick="return qa_wall_post_click(' . qa_js($message['messageid']) . ', this);"',
'label' => qa_lang_html('question/delete_button'),
'popup' => qa_lang_html('profile/delete_wall_post_popup'),
),
),
);
}
return $htmlfields;
}
/**
* Returns an element to add to $qa_content['message_list']['messages'] with a link to view all wall posts
* @param $handle
* @param $start
* @return array
*/
function qa_wall_view_more_link($handle, $start)
{
$url = qa_path_html('user/' . $handle . '/wall', array('start' => $start));
return array(
'content' => '<a href="' . $url . '">' . qa_lang_html('profile/wall_view_more') . '</a>',
);
}
/**
* Hides the private message described in $message (as obtained via qa_db_messages_inbox_selectspec() or qa_db_messages_outbox_selectspec()).
* If both sender and receiver have hidden the message, it gets deleted from the database.
* Note: currently no event is reported here, so $handle/$cookieid are unused.
* @param $userid
* @param $handle
* @param $cookieid
* @param $message
* @param $box
*/
function qa_pm_delete($userid, $handle, $cookieid, $message, $box)
{
require_once QA_INCLUDE_DIR . 'db/messages.php';
qa_db_message_user_hide($message['messageid'], $box);
qa_db_message_delete($message['messageid'], false);
}

853
qa-include/app/options.php Normal file
View File

@@ -0,0 +1,853 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Getting and setting admin options (application level)
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
require_once QA_INCLUDE_DIR . 'db/options.php';
define('QA_PERMIT_ALL', 150);
define('QA_PERMIT_USERS', 120);
define('QA_PERMIT_CONFIRMED', 110);
define('QA_PERMIT_POINTS', 106);
define('QA_PERMIT_POINTS_CONFIRMED', 104);
define('QA_PERMIT_APPROVED', 103);
define('QA_PERMIT_APPROVED_POINTS', 102);
define('QA_PERMIT_EXPERTS', 100);
define('QA_PERMIT_EDITORS', 70);
define('QA_PERMIT_MODERATORS', 40);
define('QA_PERMIT_ADMINS', 20);
define('QA_PERMIT_SUPERS', 0);
/**
* Return an array [name] => [value] of settings for each option in $names.
* If any options are missing from the database, set them to their defaults
* @param $names
* @return array
*/
function qa_get_options($names)
{
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 . '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;
}
/**
* 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)
* @param $name
* @return
*/
function qa_opt_if_loaded($name)
{
global $qa_options_cache;
return @$qa_options_cache[$name];
}
/**
* Load all of the Q2A options from the database.
* From Q2A 1.8 we always load the options in a separate query regardless of QA_OPTIMIZE_DISTANT_DB.
*/
function qa_preload_options()
{
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',
),
);
// fetch options in a separate query before everything else
qa_load_options_results(qa_db_multi_select($selectspecs));
}
}
/**
* Load the options from the $results of the database selectspecs defined in qa_preload_options()
* @param $results
* @return mixed
*/
function qa_load_options_results($results)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $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;
}
/**
* 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
* @param $name
* @param $value
* @param bool $todatabase
* @return mixed
*/
function qa_set_option($name, $value, $todatabase = true)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
global $qa_options_cache;
if ($todatabase && isset($value))
qa_db_set_option($name, $value);
$qa_options_cache[$name] = $value;
}
/**
* Reset the options in $names to their defaults
* @param $names
* @return mixed
*/
function qa_reset_options($names)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
foreach ($names as $name) {
qa_set_option($name, qa_default_option($name));
}
}
/**
* Return the default value for option $name
* @param $name
* @return bool|mixed|string
*/
function qa_default_option($name)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
$fixed_defaults = array(
'allow_anonymous_naming' => 1,
'allow_change_usernames' => 1,
'allow_close_questions' => 1,
'allow_close_own_questions' => 1,
'allow_multi_answers' => 1,
'allow_private_messages' => 1,
'allow_user_walls' => 1,
'allow_self_answer' => 1,
'allow_view_q_bots' => 1,
'avatar_allow_gravatar' => 1,
'avatar_allow_upload' => 1,
'avatar_message_list_size' => 20,
'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,
'caching_catwidget_time' => 30,
'caching_driver' => 'filesystem',
'caching_enabled' => 0,
'caching_q_start' => 7,
'caching_q_time' => 30,
'caching_qlist_time' => 5,
'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,
'minify_html' => 1,
'moderate_notify_admin' => 1,
'moderate_points_limit' => 150,
'moderate_update_time' => 1,
'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_pms' => 10,
'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_users' => 30,
'page_size_wall' => 10,
'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_edit_silent' => QA_PERMIT_MODERATORS,
'permit_flag' => QA_PERMIT_CONFIRMED,
'permit_hide_show' => QA_PERMIT_EDITORS,
'permit_moderate' => QA_PERMIT_EXPERTS,
'permit_post_wall' => QA_PERMIT_CONFIRMED,
'permit_select_a' => QA_PERMIT_EXPERTS,
'permit_view_q_page' => QA_PERMIT_ALL,
'permit_view_new_users_page' => QA_PERMIT_EDITORS,
'permit_view_special_users_page' => QA_PERMIT_MODERATORS,
'permit_view_voters_flaggers' => QA_PERMIT_ADMINS,
'permit_vote_a' => QA_PERMIT_USERS,
'permit_vote_c' => 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_c_voted_max_gain' => 10,
'points_c_voted_max_loss' => 3,
'points_multiple' => 10,
'points_per_c_voted_down' => 0,
'points_per_c_voted_up' => 0,
'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,
'recalc_hotness_q_view' => 1,
'show_a_c_links' => 1,
'show_a_form_immediate' => 'if_no_as',
'show_c_reply_buttons' => 1,
'show_compact_numbers' => 1,
'show_custom_welcome' => 0,
'show_fewer_cs_count' => 5,
'show_fewer_cs_from' => 10,
'show_full_date_days' => 7,
'show_message_history' => 1,
'show_post_update_meta' => 1,
'show_register_terms' => 0,
'show_selected_first' => 1,
'show_url_links' => 1,
'show_user_points' => 1,
'show_user_titles' => 1,
'show_view_count_q_page' => 0,
'show_view_counts' => 0,
'show_when_created' => 1,
'site_text_direction' => 'ltr',
'site_theme' => 'SnowFlat',
'smtp_port' => 25,
'sort_answers_by' => 'created',
'tags_or_categories' => 'tc',
'use_microdata' => 1,
'voting_on_as' => 1,
'voting_on_cs' => 0,
'voting_on_qs' => 1,
);
if (isset($fixed_defaults[$name])) {
return $fixed_defaults[$name];
}
switch ($name) {
case 'site_url':
$protocol =
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
(!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on') ||
(!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')
? 'https'
: 'http';
$value = $protocol . '://' . @$_SERVER['HTTP_HOST'] . strtr(rtrim(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')) > 0;
break;
case 'show_custom_header':
$value = strlen(qa_opt('custom_header')) > 0;
break;
case 'show_custom_footer':
$value = strlen(qa_opt('custom_footer')) > 0;
break;
case 'show_custom_in_head':
$value = strlen(qa_opt('custom_in_head')) > 0;
break;
case 'register_terms':
$value = qa_lang_html_sub('options/default_terms', qa_html(qa_opt('site_title')));
break;
case 'block_bad_usernames':
$value = qa_lang_html('main/anonymous');
break;
case 'custom_sidebar':
$value = qa_lang_html_sub('options/default_sidebar', qa_html(qa_opt('site_title')));
break;
case 'editor_for_qs':
case 'editor_for_as':
require_once QA_INCLUDE_DIR . '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);
else
$value = '';
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;
case 'form_security_salt':
require_once QA_INCLUDE_DIR . 'util/string.php';
$value = qa_random_alphanum(32);
break;
default: // call option_default method in any registered modules
$modules = qa_load_all_modules_with('option_default'); // Loads all modules with the 'option_default' method
foreach ($modules as $module) {
$value = $module->option_default($name);
if (strlen($value))
return $value;
}
$value = '';
break;
}
return $value;
}
/**
* Return a heuristic guess at the name of the site from the HTTP HOST
*/
function qa_default_site_title()
{
$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');
}
/**
* Return an array of defaults for the $options parameter passed to qa_post_html_fields() and its ilk for posts of $basetype='Q'/'A'/'C'
* Set $full to true if these posts will be viewed in full, i.e. on a question page rather than a question listing
* @param $basetype
* @param bool $full
* @return array|mixed
*/
function qa_post_html_defaults($basetype, $full = false)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . '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,
'favoritedview' => true,
'answersview' => $basetype == 'Q',
'viewsview' => $basetype == 'Q' && qa_opt('do_count_q_views') && ($full ? qa_opt('show_view_count_q_page') : 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' => qa_opt('show_post_update_meta'),
'blockwordspreg' => qa_get_block_words_preg(),
'showurllinks' => qa_opt('show_url_links'),
'linksnewwindow' => qa_opt('links_in_new_window'),
'fulldatedays' => qa_opt('show_full_date_days'),
);
}
/**
* Return an array of options for post $post to pass in the $options parameter to qa_post_html_fields() and its ilk. Preferably,
* call qa_post_html_defaults() previously and pass its output in $defaults, to save excessive recalculation for each item in a
* list. Set $full to true if these posts will be viewed in full, i.e. on a question page rather than a question listing.
* @param $post
* @param $defaults
* @param bool $full
* @return array|mixed|null
*/
function qa_post_html_options($post, $defaults = null, $full = false)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
if (!isset($defaults))
$defaults = qa_post_html_defaults($post['basetype'], $full);
$defaults['voteview'] = qa_get_vote_view($post, $full);
$defaults['ipview'] = !qa_user_post_permit_error('permit_anon_view_ips', $post);
return $defaults;
}
/**
* Return an array of defaults for the $options parameter passed to qa_message_html_fields()
*/
function qa_message_html_defaults()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
return array(
'whenview' => qa_opt('show_when_created'),
'whoview' => true,
'avatarsize' => qa_opt('avatar_message_list_size'),
'blockwordspreg' => qa_get_block_words_preg(),
'showurllinks' => qa_opt('show_url_links'),
'linksnewwindow' => qa_opt('links_in_new_window'),
'fulldatedays' => qa_opt('show_full_date_days'),
);
}
/**
* Return $voteview parameter to pass to qa_post_html_fields() in /qa-include/app/format.php.
* @param array|string $postorbasetype The post, or for compatibility just a basetype, i.e. 'Q', 'A' or 'C'
* @param bool $full Whether full post is shown
* @param bool $enabledif Whether to do checks for voting buttons (i.e. will always disable voting if false)
* @return bool|string Possible values:
* updown, updown-disabled-page, updown-disabled-level, updown-uponly-level, updown-disabled-approve, updown-uponly-approve
* net, net-disabled-page, net-disabled-level, net-uponly-level, net-disabled-approve, net-uponly-approve
*/
function qa_get_vote_view($postorbasetype, $full = false, $enabledif = true)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
// The 'level' and 'approve' permission errors are taken care of by disabling the voting buttons.
// Others are reported to the user after they click, in qa_vote_error_html(...)
// deal with dual-use parameter
if (is_array($postorbasetype)) {
$basetype = $postorbasetype['basetype'];
$post = $postorbasetype;
} else {
$basetype = $postorbasetype;
$post = null;
}
$disabledsuffix = '';
switch($basetype)
{
case 'Q':
$view = qa_opt('voting_on_qs');
$permitOpt = 'permit_vote_q';
break;
case 'A':
$view = qa_opt('voting_on_as');
$permitOpt = 'permit_vote_a';
break;
case 'C':
$view = qa_opt('voting_on_cs');
$permitOpt = 'permit_vote_c';
break;
default:
$view = false;
break;
}
if (!$view) {
return false;
}
if (!$enabledif || ($basetype == 'Q' && !$full && qa_opt('voting_on_q_page_only'))) {
$disabledsuffix = '-disabled-page';
}
else {
$permiterror = isset($post) ? qa_user_post_permit_error($permitOpt, $post) : qa_user_permit_error($permitOpt);
if ($permiterror == 'level')
$disabledsuffix = '-disabled-level';
elseif ($permiterror == 'approve')
$disabledsuffix = '-disabled-approve';
else {
$permiterrordown = isset($post) ? qa_user_post_permit_error('permit_vote_down', $post) : qa_user_permit_error('permit_vote_down');
if ($permiterrordown == 'level')
$disabledsuffix = '-uponly-level';
elseif ($permiterrordown == 'approve')
$disabledsuffix = '-uponly-approve';
}
}
return (qa_opt('votes_separated') ? 'updown' : 'net') . $disabledsuffix;
}
/**
* Returns true if the home page has been customized, either due to admin setting, or $QA_CONST_PATH_MAP
*/
function qa_has_custom_home()
{
return qa_opt('show_custom_home') || (array_search('', qa_get_request_map()) !== false);
}
/**
* Return whether the option is set to classify questions by tags
*/
function qa_using_tags()
{
return strpos(qa_opt('tags_or_categories'), 't') !== false;
}
/**
* Return whether the option is set to classify questions by categories
*/
function qa_using_categories()
{
return strpos(qa_opt('tags_or_categories'), 'c') !== false;
}
/**
* Return the regular expression fragment to match the blocked words options set in the database
*/
function qa_get_block_words_preg()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $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 . 'util/string.php';
$qa_blockwordspreg = qa_block_words_to_preg($blockwordstring);
} else
$qa_blockwordspreg = null;
$qa_blockwordspreg_set = true;
}
return $qa_blockwordspreg;
}
/**
* Return an array of [points] => [user title] from the 'points_to_titles' option, to pass to qa_get_points_title_html()
*/
function qa_get_points_to_titles()
{
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;
}
/**
* Return an array of relevant permissions settings, based on other options
*/
function qa_get_permit_options()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $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_cs'))
$permits[] = 'permit_vote_c';
if (qa_opt('voting_on_qs') || qa_opt('voting_on_as') || qa_opt('voting_on_cs'))
$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';
$permits[] = 'permit_edit_silent';
if (qa_opt('allow_close_questions'))
$permits[] = 'permit_close_q';
array_push($permits, 'permit_select_a', 'permit_anon_view_ips');
if (qa_opt('voting_on_qs') || qa_opt('voting_on_as') || qa_opt('voting_on_cs') || qa_opt('flagging_of_posts'))
$permits[] = 'permit_view_voters_flaggers';
if (qa_opt('flagging_of_posts'))
$permits[] = 'permit_flag';
$permits[] = 'permit_moderate';
array_push($permits, 'permit_hide_show', 'permit_delete_hidden');
if (qa_opt('allow_user_walls'))
$permits[] = 'permit_post_wall';
array_push($permits, 'permit_view_new_users_page', 'permit_view_special_users_page');
return $permits;
}

857
qa-include/app/page.php Normal file
View File

@@ -0,0 +1,857 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Routing and utility functions for page requests
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
require_once QA_INCLUDE_DIR . 'app/cookies.php';
require_once QA_INCLUDE_DIR . 'app/format.php';
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'app/options.php';
require_once QA_INCLUDE_DIR . 'db/selects.php';
/**
* Queue any pending requests which are required independent of which page will be shown
*/
function qa_page_queue_pending()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
qa_preload_options();
$loginuserid = qa_get_logged_in_userid();
if (isset($loginuserid)) {
if (!QA_FINAL_EXTERNAL_USERS)
qa_db_queue_pending_select('loggedinuser', qa_db_user_account_selectspec($loginuserid, true));
qa_db_queue_pending_select('notices', qa_db_user_notices_selectspec($loginuserid));
qa_db_queue_pending_select('favoritenonqs', qa_db_user_favorite_non_qs_selectspec($loginuserid));
qa_db_queue_pending_select('userlimits', qa_db_user_limits_selectspec($loginuserid));
qa_db_queue_pending_select('userlevels', qa_db_user_levels_selectspec($loginuserid, true));
}
qa_db_queue_pending_select('iplimits', qa_db_ip_limits_selectspec(qa_remote_ip_address()));
qa_db_queue_pending_select('navpages', qa_db_pages_selectspec(array('B', 'M', 'O', 'F')));
qa_db_queue_pending_select('widgets', qa_db_widgets_selectspec());
}
/**
* Check the page state parameter and then remove it from the $_GET array
*/
function qa_load_state()
{
global $qa_state;
$qa_state = qa_get('state');
unset($_GET['state']); // to prevent being passed through on forms
}
/**
* If no user is logged in, call through to the login modules to see if they want to log someone in
*/
function qa_check_login_modules()
{
if (!QA_FINAL_EXTERNAL_USERS && !qa_is_logged_in()) {
$loginmodules = qa_load_modules_with('login', 'check_login');
foreach ($loginmodules as $loginmodule) {
$loginmodule->check_login();
if (qa_is_logged_in()) // stop and reload page if it worked
qa_redirect(qa_request(), $_GET);
}
}
}
/**
* 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.
*/
function qa_check_page_clicks()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
global $qa_page_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)) {
if (!qa_check_form_security_code('vote', qa_post_text('code')))
$qa_page_error_html = qa_lang_html('misc/form_security_again');
else {
require_once QA_INCLUDE_DIR . 'app/votes.php';
require_once QA_INCLUDE_DIR . 'db/selects.php';
$userid = qa_get_logged_in_userid();
$post = qa_db_select_with_pending(qa_db_full_post_selectspec($userid, $postid));
$qa_page_error_html = qa_vote_error_html($post, $vote, $userid, qa_request());
if (!$qa_page_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)) {
if (!qa_check_form_security_code('favorite-' . $entitytype . '-' . $entityid, qa_post_text('code')))
$qa_page_error_html = qa_lang_html('misc/form_security_again');
else {
require_once QA_INCLUDE_DIR . '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 (!qa_check_form_security_code('notice-' . $noticeid, qa_post_text('code')))
$qa_page_error_html = qa_lang_html('misc/form_security_again');
else {
if ($noticeid == 'visitor')
setcookie('qa_noticed', 1, time() + 86400 * 3650, '/', QA_COOKIE_DOMAIN, (bool)ini_get('session.cookie_secure'), true);
elseif ($noticeid == 'welcome') {
require_once QA_INCLUDE_DIR . '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 . 'db/notices.php';
qa_db_usernotice_delete(qa_get_logged_in_userid(), $noticeid);
}
qa_redirect(qa_request(), $_GET);
}
}
}
}
}
}
/**
* Run the appropriate /qa-include/pages/*.php file for this request and return back the $qa_content it passed
*/
function qa_get_request_content()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $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 . 'pages/question.php';
} else {
qa_set_template(strlen($firstlower) ? $firstlower : 'qa'); // will be changed later
$qa_content = require QA_INCLUDE_DIR . 'pages/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, (bool)ini_get('session.cookie_secure'), true); // ...and in future
}
if (isset($qa_content))
qa_set_form_security_key();
return $qa_content;
}
/**
* Output the $qa_content via the theme class after doing some pre-processing, mainly relating to Javascript
* @param $qa_content
* @return mixed
*/
function qa_output_content($qa_content)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $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') {
continue;
}
foreach ($navigation as $navprefix => $navlink) {
$selected =& $qa_content['navigation'][$navtype][$navprefix]['selected'];
if (isset($navlink['selected_on'])) {
// match specified paths
foreach ($navlink['selected_on'] as $path) {
if (strpos($requestlower . '$', $path) === 0)
$selected = true;
}
} elseif ($requestlower === $navprefix || $requestlower . '$' === $navprefix) {
// exact match for array key
$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' => '<a href="' . qa_path_html('admin/general') . '">',
'^2' => '</a>',
));
}
} 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, or must be approved before continuing
$userid = qa_get_logged_in_userid();
if (isset($userid) && $requestlower != 'confirm' && $requestlower != 'account') {
$flags = qa_get_logged_in_flags();
if (($flags & QA_USER_FLAGS_MUST_CONFIRM) && !($flags & QA_USER_FLAGS_EMAIL_CONFIRMED) && qa_opt('confirm_user_emails')) {
$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' => '<a href="' . qa_path_html('confirm') . '">',
'^2' => '</a>',
));
}
// we no longer block access here for unapproved users; this is handled by the Permissions settings
}
// Combine various Javascript elements in $qa_content into single array for theme layer
$script = array('<script>');
if (isset($qa_content['script_var'])) {
foreach ($qa_content['script_var'] as $var => $value) {
$script[] = 'var ' . $var . ' = ' . qa_js($value) . ';';
}
}
if (isset($qa_content['script_lines'])) {
foreach ($qa_content['script_lines'] as $scriptlines) {
$script[] = '';
$script = array_merge($script, $scriptlines);
}
}
$script[] = '</script>';
if (isset($qa_content['script_rel'])) {
$uniquerel = array_unique($qa_content['script_rel']); // remove any duplicates
foreach ($uniquerel as $script_rel) {
$script[] = '<script src="' . qa_html(qa_path_to_root() . $script_rel) . '"></script>';
}
}
if (isset($qa_content['script_src'])) {
$uniquesrc = array_unique($qa_content['script_src']); // remove any duplicates
foreach ($uniquesrc as $script_src) {
$script[] = '<script src="' . qa_html($script_src) . '"></script>';
}
}
// JS onloads must come after jQuery is loaded
if (isset($qa_content['focusid'])) {
$qa_content['script_onloads'][] = array(
'$(' . qa_js('#' . $qa_content['focusid']) . ').focus();',
);
}
if (isset($qa_content['script_onloads'])) {
$script[] = '<script>';
$script[] = '$(window).on(\'load\', function() {';
foreach ($qa_content['script_onloads'] as $scriptonload) {
foreach ((array)$scriptonload as $scriptline) {
$script[] = "\t" . $scriptline;
}
}
$script[] = '});';
$script[] = '</script>';
}
if (!isset($qa_content['script'])) {
$qa_content['script'] = array();
}
$qa_content['script'] = array_merge($qa_content['script'], $script);
// Load the appropriate theme class and output the page
$tmpl = substr($qa_template, 0, 7) == 'custom-' ? 'custom' : $qa_template;
$themeclass = qa_load_theme_class(qa_get_site_theme(), $tmpl, $qa_content, qa_request());
$themeclass->initialize();
header('Content-type: ' . $qa_content['content_type']);
$themeclass->doctype();
$themeclass->html();
$themeclass->finish();
}
/**
* Update any statistics required by the fields in $qa_content, and return true if something was done
* @param $qa_content
* @return bool
*/
function qa_do_content_stats($qa_content)
{
if (!isset($qa_content['inc_views_postid'])) {
return false;
}
require_once QA_INCLUDE_DIR . 'db/hotness.php';
$viewsIncremented = qa_db_increment_views($qa_content['inc_views_postid']);
if ($viewsIncremented && qa_opt('recalc_hotness_q_view')) {
qa_db_hotness_update($qa_content['inc_views_postid']);
}
return true;
}
// Other functions which might be called from anywhere
/**
* Return an array of the default Q2A requests and which /qa-include/pages/*.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
*/
function qa_page_routing()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
return array(
'account' => 'pages/account.php',
'activity/' => 'pages/activity.php',
'admin/' => 'pages/admin/admin-default.php',
'admin/approve' => 'pages/admin/admin-approve.php',
'admin/categories' => 'pages/admin/admin-categories.php',
'admin/flagged' => 'pages/admin/admin-flagged.php',
'admin/hidden' => 'pages/admin/admin-hidden.php',
'admin/layoutwidgets' => 'pages/admin/admin-widgets.php',
'admin/moderate' => 'pages/admin/admin-moderate.php',
'admin/pages' => 'pages/admin/admin-pages.php',
'admin/plugins' => 'pages/admin/admin-plugins.php',
'admin/points' => 'pages/admin/admin-points.php',
'admin/recalc' => 'pages/admin/admin-recalc.php',
'admin/stats' => 'pages/admin/admin-stats.php',
'admin/userfields' => 'pages/admin/admin-userfields.php',
'admin/usertitles' => 'pages/admin/admin-usertitles.php',
'answers/' => 'pages/answers.php',
'ask' => 'pages/ask.php',
'categories/' => 'pages/categories.php',
'comments/' => 'pages/comments.php',
'confirm' => 'pages/confirm.php',
'favorites' => 'pages/favorites.php',
'favorites/questions' => 'pages/favorites-list.php',
'favorites/users' => 'pages/favorites-list.php',
'favorites/tags' => 'pages/favorites-list.php',
'feedback' => 'pages/feedback.php',
'forgot' => 'pages/forgot.php',
'hot/' => 'pages/hot.php',
'ip/' => 'pages/ip.php',
'login' => 'pages/login.php',
'logout' => 'pages/logout.php',
'messages/' => 'pages/messages.php',
'message/' => 'pages/message.php',
'questions/' => 'pages/questions.php',
'register' => 'pages/register.php',
'reset' => 'pages/reset.php',
'search' => 'pages/search.php',
'tag/' => 'pages/tag.php',
'tags' => 'pages/tags.php',
'unanswered/' => 'pages/unanswered.php',
'unsubscribe' => 'pages/unsubscribe.php',
'updates' => 'pages/updates.php',
'user/' => 'pages/user.php',
'users' => 'pages/users.php',
'users/blocked' => 'pages/users-blocked.php',
'users/new' => 'pages/users-newest.php',
'users/special' => 'pages/users-special.php',
);
}
/**
* Sets the template which should be passed to the theme class, telling it which type of page it's displaying
* @param $template
*/
function qa_set_template($template)
{
global $qa_template;
$qa_template = $template;
}
/**
* 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)
* @param bool $voting
* @param array $categoryids
* @return array
*/
function qa_content_prepare($voting = false, $categoryids = array())
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
global $qa_template, $qa_page_error_html;
if (QA_DEBUG_PERFORMANCE) {
global $qa_usage;
$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 (!is_array($categoryids)) {
// accept old-style parameter
$categoryids = array($categoryids);
}
$lastcategoryid = count($categoryids) > 0 ? end($categoryids) : null;
$charset = 'utf-8';
$language = qa_opt('site_language');
$language = empty($language) ? 'en' : qa_html($language);
$qa_content = array(
'content_type' => 'text/html; charset=' . $charset,
'charset' => $charset,
'language' => $language,
'direction' => qa_opt('site_text_direction'),
'options' => array(
'minify_html' => qa_opt('minify_html'),
),
'site_title' => qa_html(qa_opt('site_title')),
'html_tags' => 'lang="' . $language . '"',
'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(),
);
// add meta description if we're on the home page
if ($request === '' || $request === array_search('', qa_get_request_map())) {
$qa_content['description'] = qa_html(qa_opt('home_description'));
}
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'),
'selected_on' => array('tags$', 'tag/'),
);
}
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'),
'selected_on' => array('categories$', 'categories/'),
);
}
if (qa_opt('nav_users')) {
$qa_content['navigation']['main']['user'] = array(
'url' => qa_path_html('users'),
'label' => qa_lang_html('main/nav_users'),
'selected_on' => array('users$', 'users/', 'user/'),
);
}
// Only the 'level' permission error prevents the menu option being shown - others reported on /qa-include/pages/ask.php
if (qa_opt('nav_ask') && qa_user_maximum_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_maximum_permit_error('permit_moderate') ||
!qa_user_maximum_permit_error('permit_hide_show') || !qa_user_maximum_permit_error('permit_delete_hidden')
) {
$qa_content['navigation']['main']['admin'] = array(
'url' => qa_path_html('admin'),
'label' => qa_lang_html('main/nav_admin'),
'selected_on' => array('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') {
$loc = ($page['nav'] == 'F') ? 'footer' : 'main';
qa_navigation_add_page($qa_content['navigation'][$loc], $page);
}
}
$regioncodes = array(
'F' => 'full',
'M' => 'main',
'S' => 'side',
);
$placecodes = array(
'T' => 'top',
'H' => 'high',
'L' => 'low',
'B' => 'bottom',
);
foreach ($widgets as $widget) {
$tagstring = ',' . $widget['tags'] . ',';
$showOnTmpl = strpos($tagstring, ",$qa_template,") !== false || strpos($tagstring, ',all,') !== false;
// special case for user pages
$showOnUser = strpos($tagstring, ',user,') !== false && preg_match('/^user(-.+)?$/', $qa_template) === 1;
if ($showOnTmpl || $showOnUser) {
// widget 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)) {
// region/place codes recognized
$module = qa_load_module('widget', $widget['title']);
$allowTmpl = (substr($qa_template, 0, 7) == 'custom-') ? 'custom' : $qa_template;
if (isset($module) &&
method_exists($module, 'allow_template') && $module->allow_template($allowTmpl) &&
method_exists($module, 'allow_region') && $module->allow_region($region) &&
method_exists($module, 'output_widget')
) {
// if module loaded and happy to be displayed here, tell theme about it
$qa_content['widgets'][$region][$place][] = $module;
}
}
}
}
$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'] = '<a href="' . qa_path_html('') . '" class="qa-logo-link" title="' . qa_html(qa_opt('site_title')) . '">' .
'<img src="' . qa_html(is_numeric(strpos($logourl, '://')) ? $logourl : qa_path_to_root() . $logourl) . '"' .
($logowidth ? (' width="' . $logowidth . '"') : '') . ($logoheight ? (' height="' . $logoheight . '"') : '') .
' alt="' . qa_html(qa_opt('site_title')) . '"/></a>';
} else {
$qa_content['logo'] = '<a href="' . qa_path_html('') . '" class="qa-logo-link">' . qa_html(qa_opt('site_title')) . '</a>';
}
$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)
);
$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 {
require_once QA_INCLUDE_DIR . 'util/string.php';
if (!QA_FINAL_EXTERNAL_USERS) {
$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');
$label = ob_get_clean();
if (strlen($label))
$qa_content['navigation']['user'][implode('-', qa_string_to_words($tryname))] = array('label' => $label);
}
}
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, (bool)ini_get('session.cookie_secure'), true); // 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-3.5.1.min.js');
$qa_content['script_rel'][] = 'qa-content/qa-global.js?' . QA_VERSION;
if ($voting)
$qa_content['error'] = @$qa_page_error_html;
$qa_content['script_var'] = array(
'qa_root' => qa_path_to_root(),
'qa_request' => $request,
);
return $qa_content;
}
/**
* Get the start parameter which should be used, as constrained by the setting in qa-config.php
* @return int
*/
function qa_get_start()
{
return min(max(0, (int)qa_get('start')), QA_MAX_LIMIT_START);
}
/**
* Get the state parameter which should be used, as set earlier in qa_load_state()
* @return string
*/
function qa_get_state()
{
global $qa_state;
return $qa_state;
}
/**
* Generate a canonical URL for the current request. Preserves certain GET parameters.
* @return string The full canonical URL.
*/
function qa_get_canonical()
{
$params = array();
// variable assignment intentional here
if (($start = qa_get_start()) > 0) {
$params['start'] = $start;
}
if ($sort = qa_get('sort')) {
$params['sort'] = $sort;
}
if ($by = qa_get('by')) {
$params['by'] = $by;
}
return qa_path_html(qa_request(), $params, qa_opt('site_url'));
}

View File

@@ -0,0 +1,334 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Creating questions, answers and comments (application level)
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
require_once QA_INCLUDE_DIR . 'db/maxima.php';
require_once QA_INCLUDE_DIR . 'db/post-create.php';
require_once QA_INCLUDE_DIR . 'db/points.php';
require_once QA_INCLUDE_DIR . 'db/hotness.php';
require_once QA_INCLUDE_DIR . 'util/string.php';
/**
* Return value to store in database combining $notify and $email values entered by user $userid (or null for anonymous)
* @param $userid
* @param $notify
* @param $email
* @return null|string
*/
function qa_combine_notify_email($userid, $notify, $email)
{
return $notify ? (empty($email) ? (isset($userid) ? '@' : null) : $email) : null;
}
/**
* Add a question (application level) - create record, update appropriate counts, index it, send notifications.
* If question is follow-on from an answer, $followanswer should contain answer database record, otherwise null.
* See /qa-include/app/posts.php for a higher-level function which is easier to use.
* @param $followanswer
* @param $userid
* @param $handle
* @param $cookieid
* @param $title
* @param $content
* @param $format
* @param $text
* @param $tagstring
* @param $notify
* @param $email
* @param $categoryid
* @param $extravalue
* @param bool $queued
* @param $name
* @return mixed
*/
function qa_question_create($followanswer, $userid, $handle, $cookieid, $title, $content, $format, $text, $tagstring, $notify, $email,
$categoryid = null, $extravalue = null, $queued = false, $name = null)
{
require_once QA_INCLUDE_DIR . 'db/selects.php';
$postid = qa_db_post_create($queued ? 'Q_QUEUED' : 'Q', @$followanswer['postid'], $userid, isset($userid) ? null : $cookieid,
qa_remote_ip_address(), $title, $content, $format, $tagstring, qa_combine_notify_email($userid, $notify, $email),
$categoryid, isset($userid) ? null : $name);
if (isset($extravalue)) {
require_once QA_INCLUDE_DIR . 'db/metas.php';
qa_db_postmeta_set($postid, 'qa_q_extra', $extravalue);
}
qa_db_posts_calc_category_path($postid);
qa_db_hotness_update($postid);
if ($queued) {
qa_db_queuedcount_update();
} else {
qa_post_index($postid, 'Q', $postid, @$followanswer['postid'], $title, $content, $format, $text, $tagstring, $categoryid);
qa_update_counts_for_q($postid);
qa_db_points_update_ifuser($userid, 'qposts');
}
qa_report_event($queued ? 'q_queue' : 'q_post', $userid, $handle, $cookieid, array(
'postid' => $postid,
'parentid' => @$followanswer['postid'],
'parent' => $followanswer,
'title' => $title,
'content' => $content,
'format' => $format,
'text' => $text,
'tags' => $tagstring,
'categoryid' => $categoryid,
'extra' => $extravalue,
'name' => $name,
'notify' => $notify,
'email' => $email,
));
return $postid;
}
/**
* Perform various common cached count updating operations to reflect changes in the question whose id is $postid
* @param $postid
*/
function qa_update_counts_for_q($postid)
{
if (isset($postid)) // post might no longer exist
qa_db_category_path_qcount_update(qa_db_post_get_category_path($postid));
qa_db_qcount_update();
qa_db_unaqcount_update();
qa_db_unselqcount_update();
qa_db_unupaqcount_update();
qa_db_tagcount_update();
}
/**
* Return an array containing the elements of $inarray whose key is in $keys
* @param $inarray
* @param $keys
* @return array
*/
function qa_array_filter_by_keys($inarray, $keys)
{
$outarray = array();
foreach ($keys as $key) {
if (isset($inarray[$key]))
$outarray[$key] = $inarray[$key];
}
return $outarray;
}
/**
* 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.
* @param bool $suspend
*/
function qa_suspend_post_indexing($suspend = true)
{
global $qa_post_indexing_suspended;
$qa_post_indexing_suspended += ($suspend ? 1 : -1);
}
/**
* Add post $postid (which comes under $questionid) of $type (Q/A/C) to the database index, with $title, $text,
* $tagstring and $categoryid. Calls through to all installed search modules.
* @param $postid
* @param $type
* @param $questionid
* @param $parentid
* @param $title
* @param $content
* @param $format
* @param $text
* @param $tagstring
* @param $categoryid
*/
function qa_post_index($postid, $type, $questionid, $parentid, $title, $content, $format, $text, $tagstring, $categoryid)
{
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, $categoryid);
}
/**
* 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-include/app/posts.php for a higher-level function which is easier to use.
* @param $userid
* @param $handle
* @param $cookieid
* @param $content
* @param $format
* @param $text
* @param $notify
* @param $email
* @param $question
* @param bool $queued
* @param $name
* @return mixed
*/
function qa_answer_create($userid, $handle, $cookieid, $content, $format, $text, $notify, $email, $question, $queued = false, $name = null)
{
$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'], isset($userid) ? null : $name);
qa_db_posts_calc_category_path($postid);
if ($queued) {
qa_db_queuedcount_update();
} else {
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, $question['categoryid']);
qa_update_q_counts_for_a($question['postid']);
qa_db_points_update_ifuser($userid, 'aposts');
}
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'],
'name' => $name,
'notify' => $notify,
'email' => $email,
));
return $postid;
}
/**
* Perform various common cached count updating operations to reflect changes in an answer of question $questionid
* @param $questionid
*/
function qa_update_q_counts_for_a($questionid)
{
qa_db_post_acount_update($questionid);
qa_db_hotness_update($questionid);
qa_db_acount_update();
qa_db_unaqcount_update();
qa_db_unupaqcount_update();
}
/**
* 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-include/app/posts.php for a higher-level function which is easier to use.
* @param $userid
* @param $handle
* @param $cookieid
* @param $content
* @param $format
* @param $text
* @param $notify
* @param $email
* @param $question
* @param $parent
* @param $commentsfollows
* @param bool $queued
* @param $name
* @return mixed
*/
function qa_comment_create($userid, $handle, $cookieid, $content, $format, $text, $notify, $email, $question, $parent, $commentsfollows, $queued = false, $name = null)
{
require_once QA_INCLUDE_DIR . 'app/emails.php';
require_once QA_INCLUDE_DIR . 'app/options.php';
require_once QA_INCLUDE_DIR . 'app/format.php';
require_once QA_INCLUDE_DIR . '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'], isset($userid) ? null : $name);
qa_db_posts_calc_category_path($postid);
if ($queued) {
qa_db_queuedcount_update();
} else {
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, $question['categoryid']);
}
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'],
'name' => $name,
'notify' => $notify,
'email' => $email,
));
return $postid;
}

File diff suppressed because it is too large Load Diff

522
qa-include/app/posts.php Normal file
View File

@@ -0,0 +1,522 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Higher-level functions to create and manipulate posts
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
require_once QA_INCLUDE_DIR . 'qa-db.php';
require_once QA_INCLUDE_DIR . 'db/selects.php';
require_once QA_INCLUDE_DIR . 'app/format.php';
require_once QA_INCLUDE_DIR . 'app/post-create.php';
require_once QA_INCLUDE_DIR . 'app/post-update.php';
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'util/string.php';
/**
* Create a new post in the database, and return its postid.
*
* Set $type to 'Q' for a new question, 'A' for an answer, or 'C' for a comment. You can also use 'Q_QUEUED',
* 'A_QUEUED' or 'C_QUEUED' to create a post which is queued for moderator approval. For questions, set $parentid to
* the postid of the answer to which the question is related, or null if (as in most cases) the question is not related
* to an answer. For answers, set $parentid to the postid of the question being answered. For comments, set $parentid
* to the postid of the question or answer to which the comment relates. The $content and $format parameters go
* together - if $format is '' then $content should be in plain UTF-8 text, and if $format is 'html' then $content
* should be in UTF-8 HTML. Other values of $format may be allowed if an appropriate viewer module is installed. The
* $title, $categoryid and $tags parameters are only relevant when creating a question - $tags can either be an array
* of tags, or a string of tags separated by commas. The new post will be assigned to $userid if it is not null,
* otherwise it will be by a non-user. If $notify is true then the author will be sent notifications relating to the
* post - either to $email if it is specified and valid, or to the current email address of $userid if $email is '@'.
* If you're creating a question, the $extravalue parameter will be set as the custom extra field, if not null. For all
* post types you can specify the $name of the post's author, which is relevant if the $userid is null.
* @param $type
* @param $parentid
* @param $title
* @param $content
* @param string $format
* @param $categoryid
* @param $tags
* @param $userid
* @param $notify
* @param $email
* @param $extravalue
* @param $name
* @return mixed
*/
function qa_post_create($type, $parentid, $title, $content, $format = '', $categoryid = null, $tags = null, $userid = null,
$notify = null, $email = null, $extravalue = null, $name = null)
{
$handle = qa_userid_to_handle($userid);
$text = qa_post_content_to_text($content, $format);
switch ($type) {
case 'Q':
case 'Q_QUEUED':
$followanswer = isset($parentid) ? qa_post_get_full($parentid, 'A') : null;
$tagstring = qa_post_tags_to_tagstring($tags);
$postid = qa_question_create($followanswer, $userid, $handle, null, $title, $content, $format, $text, $tagstring,
$notify, $email, $categoryid, $extravalue, $type == 'Q_QUEUED', $name);
break;
case 'A':
case 'A_QUEUED':
$question = qa_post_get_full($parentid, 'Q');
$postid = qa_answer_create($userid, $handle, null, $content, $format, $text, $notify, $email, $question, $type == 'A_QUEUED', $name);
break;
case 'C':
case 'C_QUEUED':
$parent = qa_post_get_full($parentid, 'QA');
$commentsfollows = qa_db_single_select(qa_db_full_child_posts_selectspec(null, $parentid));
$question = qa_post_parent_to_question($parent);
$postid = qa_comment_create($userid, $handle, null, $content, $format, $text, $notify, $email, $question, $parent, $commentsfollows, $type == 'C_QUEUED', $name);
break;
default:
qa_fatal_error('Post type not recognized: ' . $type);
break;
}
return $postid;
}
/**
* Change the data stored for post $postid based on any of the $title, $content, $format, $tags, $notify, $email,
* $extravalue and $name parameters passed which are not null. The meaning of these parameters is the same as for
* qa_post_create() above. Pass the identify of the user making this change in $byuserid (or null for silent).
* @param $postid
* @param $title
* @param $content
* @param $format
* @param $tags
* @param $notify
* @param $email
* @param $byuserid
* @param $extravalue
* @param $name
*/
function qa_post_set_content($postid, $title, $content, $format = null, $tags = null, $notify = null, $email = null, $byuserid = null, $extravalue = null, $name = null)
{
$oldpost = qa_post_get_full($postid, 'QAC');
if (!isset($title))
$title = $oldpost['title'];
if (!isset($content))
$content = $oldpost['content'];
if (!isset($format))
$format = $oldpost['format'];
if (!isset($tags))
$tags = qa_tagstring_to_tags($oldpost['tags']);
if (isset($notify) || isset($email))
$setnotify = qa_combine_notify_email($oldpost['userid'], isset($notify) ? $notify : isset($oldpost['notify']),
isset($email) ? $email : $oldpost['notify']);
else
$setnotify = $oldpost['notify'];
$byhandle = qa_userid_to_handle($byuserid);
$text = qa_post_content_to_text($content, $format);
switch ($oldpost['basetype']) {
case 'Q':
$tagstring = qa_post_tags_to_tagstring($tags);
qa_question_set_content($oldpost, $title, $content, $format, $text, $tagstring, $setnotify, $byuserid, $byhandle, null, $extravalue, $name);
break;
case 'A':
$question = qa_post_get_full($oldpost['parentid'], 'Q');
qa_answer_set_content($oldpost, $content, $format, $text, $setnotify, $byuserid, $byhandle, null, $question, $name);
break;
case 'C':
$parent = qa_post_get_full($oldpost['parentid'], 'QA');
$question = qa_post_parent_to_question($parent);
qa_comment_set_content($oldpost, $content, $format, $text, $setnotify, $byuserid, $byhandle, null, $question, $parent, $name);
break;
}
}
/**
* Change the category of $postid to $categoryid. The category of all related posts (shown together on the same
* question page) will also be changed. Pass the identify of the user making this change in $byuserid (or null for an
* anonymous change).
* @param $postid
* @param $categoryid
* @param $byuserid
*/
function qa_post_set_category($postid, $categoryid, $byuserid = null)
{
$oldpost = qa_post_get_full($postid, 'QAC');
if ($oldpost['basetype'] == 'Q') {
$byhandle = qa_userid_to_handle($byuserid);
$answers = qa_post_get_question_answers($postid);
$commentsfollows = qa_post_get_question_commentsfollows($postid);
$closepost = qa_post_get_question_closepost($postid);
qa_question_set_category($oldpost, $categoryid, $byuserid, $byhandle, null, $answers, $commentsfollows, $closepost);
} else
qa_post_set_category($oldpost['parentid'], $categoryid, $byuserid); // keep looking until we find the parent question
}
/**
* Set the selected best answer of $questionid to $answerid (or to none if $answerid is null). Pass the identify of the
* user in $byuserid (or null for an anonymous change).
* @param $questionid
* @param $answerid
* @param $byuserid
*/
function qa_post_set_selchildid($questionid, $answerid, $byuserid = null)
{
$oldquestion = qa_post_get_full($questionid, 'Q');
$byhandle = qa_userid_to_handle($byuserid);
$answers = qa_post_get_question_answers($questionid);
if (isset($answerid) && !isset($answers[$answerid]))
qa_fatal_error('Answer ID could not be found: ' . $answerid);
qa_question_set_selchildid($byuserid, $byhandle, null, $oldquestion, $answerid, $answers);
}
/**
* Close $questionid if $closed is true, otherwise reopen it. If $closed is true, pass either the $originalpostid of
* the question that it is a duplicate of, or a $note to explain why it's closed. Pass the identifier of the user in
* $byuserid (or null for an anonymous change).
* @param $questionid
* @param bool $closed
* @param $originalpostid
* @param $note
* @param $byuserid
*/
function qa_post_set_closed($questionid, $closed = true, $originalpostid = null, $note = null, $byuserid = null)
{
$oldquestion = qa_post_get_full($questionid, 'Q');
$oldclosepost = qa_post_get_question_closepost($questionid);
$byhandle = qa_userid_to_handle($byuserid);
if ($closed) {
if (isset($originalpostid))
qa_question_close_duplicate($oldquestion, $oldclosepost, $originalpostid, $byuserid, $byhandle, null);
elseif (isset($note))
qa_question_close_other($oldquestion, $oldclosepost, $note, $byuserid, $byhandle, null);
else
qa_fatal_error('Question must be closed as a duplicate or with a note');
} else
qa_question_close_clear($oldquestion, $oldclosepost, $byuserid, $byhandle, null);
}
/**
* Return whether the given question is closed. This check takes into account the do_close_on_select option which
* considers questions with a selected answer as closed.
* @since 1.8.2
* @param array $question
* @return bool
*/
function qa_post_is_closed(array $question)
{
return isset($question['closedbyid']) || (isset($question['selchildid']) && qa_opt('do_close_on_select'));
}
/**
* Hide $postid if $hidden is true, otherwise show the post. Pass the identify of the user making this change in
* $byuserid (or null for a silent change).
* @deprecated Replaced by qa_post_set_status.
* @param $postid
* @param bool $hidden
* @param $byuserid
*/
function qa_post_set_hidden($postid, $hidden = true, $byuserid = null)
{
qa_post_set_status($postid, $hidden ? QA_POST_STATUS_HIDDEN : QA_POST_STATUS_NORMAL, $byuserid);
}
/**
* Change the status of $postid to $status, which should be one of the QA_POST_STATUS_* constants defined in
* /qa-include/app/post-update.php. Pass the identify of the user making this change in $byuserid (or null for a silent change).
* @param $postid
* @param $status
* @param $byuserid
*/
function qa_post_set_status($postid, $status, $byuserid = null)
{
$oldpost = qa_post_get_full($postid, 'QAC');
$byhandle = qa_userid_to_handle($byuserid);
switch ($oldpost['basetype']) {
case 'Q':
$answers = qa_post_get_question_answers($postid);
$commentsfollows = qa_post_get_question_commentsfollows($postid);
$closepost = qa_post_get_question_closepost($postid);
qa_question_set_status($oldpost, $status, $byuserid, $byhandle, null, $answers, $commentsfollows, $closepost);
break;
case 'A':
$question = qa_post_get_full($oldpost['parentid'], 'Q');
$commentsfollows = qa_post_get_answer_commentsfollows($postid);
qa_answer_set_status($oldpost, $status, $byuserid, $byhandle, null, $question, $commentsfollows);
break;
case 'C':
$parent = qa_post_get_full($oldpost['parentid'], 'QA');
$question = qa_post_parent_to_question($parent);
qa_comment_set_status($oldpost, $status, $byuserid, $byhandle, null, $question, $parent);
break;
}
}
/**
* Set the created date of $postid to $created, which is a unix timestamp.
* @param $postid
* @param $created
*/
function qa_post_set_created($postid, $created)
{
$oldpost = qa_post_get_full($postid);
qa_db_post_set_created($postid, $created);
switch ($oldpost['basetype']) {
case 'Q':
qa_db_hotness_update($postid);
break;
case 'A':
qa_db_hotness_update($oldpost['parentid']);
break;
}
}
/**
* Delete $postid from the database, hiding it first if appropriate.
* @param $postid
*/
function qa_post_delete($postid)
{
$oldpost = qa_post_get_full($postid, 'QAC');
if (!$oldpost['hidden']) {
qa_post_set_status($postid, QA_POST_STATUS_HIDDEN, null);
$oldpost = qa_post_get_full($postid, 'QAC');
}
switch ($oldpost['basetype']) {
case 'Q':
$answers = qa_post_get_question_answers($postid);
$commentsfollows = qa_post_get_question_commentsfollows($postid);
$closepost = qa_post_get_question_closepost($postid);
if (count($answers) || count($commentsfollows))
qa_fatal_error('Could not delete question ID due to dependents: ' . $postid);
qa_question_delete($oldpost, null, null, null, $closepost);
break;
case 'A':
$question = qa_post_get_full($oldpost['parentid'], 'Q');
$commentsfollows = qa_post_get_answer_commentsfollows($postid);
if (count($commentsfollows))
qa_fatal_error('Could not delete answer ID due to dependents: ' . $postid);
qa_answer_delete($oldpost, $question, null, null, null);
break;
case 'C':
$parent = qa_post_get_full($oldpost['parentid'], 'QA');
$question = qa_post_parent_to_question($parent);
qa_comment_delete($oldpost, $question, $parent, null, null, null);
break;
}
}
/**
* Return the full information from the database for $postid in an array.
* @param $postid
* @param $requiredbasetypes
* @return array|mixed
*/
function qa_post_get_full($postid, $requiredbasetypes = null)
{
$post = qa_db_single_select(qa_db_full_post_selectspec(null, $postid));
if (!is_array($post))
qa_fatal_error('Post ID could not be found: ' . $postid);
if (isset($requiredbasetypes) && !is_numeric(strpos($requiredbasetypes, $post['basetype'])))
qa_fatal_error('Post of wrong type: ' . $post['basetype']);
return $post;
}
/**
* Return the handle corresponding to $userid, unless it is null in which case return null.
*
* @deprecated Deprecated from 1.7; use `qa_userid_to_handle($userid)` instead.
* @param $userid
* @return mixed|null
*/
function qa_post_userid_to_handle($userid)
{
return qa_userid_to_handle($userid);
}
/**
* Return the textual rendition of $content in $format (used for indexing).
* @param $content
* @param $format
* @return string
*/
function qa_post_content_to_text($content, $format)
{
$viewer = qa_load_viewer($content, $format);
if (!isset($viewer))
qa_fatal_error('Content could not be parsed in format: ' . $format);
return $viewer->get_text($content, $format, array());
}
/**
* Return tagstring to store in the database based on $tags as an array or a comma-separated string.
* @param $tags
* @return mixed|string
*/
function qa_post_tags_to_tagstring($tags)
{
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)));
}
/**
* Return the full database records for all answers to question $questionid
* @param $questionid
* @return array
*/
function qa_post_get_question_answers($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;
}
/**
* Return the full database records for all comments or follow-on questions for question $questionid or its answers
* @param $questionid
* @return array
*/
function qa_post_get_question_commentsfollows($questionid)
{
$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;
}
/**
* Return the full database record for the post which closed $questionid, if there is any
* @param $questionid
* @return array|mixed
*/
function qa_post_get_question_closepost($questionid)
{
return qa_db_single_select(qa_db_post_close_post_selectspec($questionid));
}
/**
* Return the full database records for all comments or follow-on questions for answer $answerid
* @param $answerid
* @return array
*/
function qa_post_get_answer_commentsfollows($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;
}
/**
* Return $parent if it's the database record for a question, otherwise return the database record for its parent
* @param $parent
* @return array|mixed
*/
function qa_post_parent_to_question($parent)
{
if ($parent['basetype'] == 'Q')
$question = $parent;
else
$question = qa_post_get_full($parent['parentid'], 'Q');
return $question;
}

255
qa-include/app/q-list.php Normal file
View File

@@ -0,0 +1,255 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Controller for most question listing pages, plus custom pages and plugin pages
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Returns the $qa_content structure for a question list page showing $questions retrieved from the
* database. If $pagesize is not null, it sets the max number of questions to display. If $count is
* not null, pagination is determined by $start and $count. The page title is $sometitle unless
* there are no questions shown, in which case it's $nonetitle. $navcategories should contain the
* categories retrived from the database using qa_db_category_nav_selectspec(...) for $categoryid,
* which is the current category shown. If $categorypathprefix is set, category navigation will be
* shown, with per-category question counts if $categoryqcount is true. The nav links will have the
* prefix $categorypathprefix and possible extra $categoryparams. If $feedpathprefix is set, the
* page has an RSS feed whose URL uses that prefix. If there are no links to other pages, $suggest
* is used to suggest what the user should do. The $pagelinkparams are passed through to
* qa_html_page_links(...) which creates links for page 2, 3, etc..
* @param $questions
* @param $pagesize
* @param $start
* @param $count
* @param $sometitle
* @param $nonetitle
* @param $navcategories
* @param $categoryid
* @param $categoryqcount
* @param $categorypathprefix
* @param $feedpathprefix
* @param $suggest
* @param $pagelinkparams
* @param $categoryparams
* @param $dummy
* @return array
*/
function qa_q_list_page_content($questions, $pagesize, $start, $count, $sometitle, $nonetitle,
$navcategories, $categoryid, $categoryqcount, $categorypathprefix, $feedpathprefix, $suggest,
$pagelinkparams = null, $categoryparams = null, $dummy = null)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'app/format.php';
require_once QA_INCLUDE_DIR . 'app/updates.php';
require_once QA_INCLUDE_DIR . 'app/posts.php';
$userid = qa_get_logged_in_userid();
// Chop down to size, get user information for display
if (isset($pagesize)) {
$questions = array_slice($questions, 0, $pagesize);
}
$usershtml = qa_userids_handles_html(qa_any_get_userids_handles($questions));
// Prepare content for theme
$qa_content = qa_content_prepare(true, array_keys(qa_category_path($navcategories, $categoryid)));
$qa_content['q_list']['form'] = array(
'tags' => 'method="post" action="' . qa_self_html() . '"',
'hidden' => array(
'code' => qa_get_form_security_code('vote'),
),
);
$qa_content['q_list']['qs'] = array();
if (!empty($questions)) {
$qa_content['title'] = $sometitle;
$defaults = qa_post_html_defaults('Q');
if (isset($categorypathprefix)) {
$defaults['categorypathprefix'] = $categorypathprefix;
}
foreach ($questions as $question) {
$fields = qa_any_to_q_html_fields($question, $userid, qa_cookie_get(), $usershtml, null, qa_post_html_options($question, $defaults));
if (qa_post_is_closed($question)) {
$fields['closed'] = array(
'state' => qa_lang_html('main/closed'),
);
}
$qa_content['q_list']['qs'][] = $fields;
}
} else {
$qa_content['title'] = $nonetitle;
}
if (isset($userid) && isset($categoryid)) {
$favoritemap = qa_get_favorite_non_qs_map();
$categoryisfavorite = @$favoritemap['category'][$navcategories[$categoryid]['backpath']];
$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);
}
$qa_content['canonical'] = qa_get_canonical();
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);
}
// set meta description on category pages
if (!empty($navcategories[$categoryid]['content'])) {
$qa_content['description'] = qa_html($navcategories[$categoryid]['content']);
}
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;
}
/**
* Return the sub navigation structure common to question listing pages
* @param $sort
* @param $categoryslugs
* @return array
*/
function qa_qs_sub_navigation($sort, $categoryslugs)
{
$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;
}
/**
* Return the sub navigation structure common to unanswered pages
* @param $by
* @param $categoryslugs
* @return array
*/
function qa_unanswered_sub_navigation($by, $categoryslugs)
{
$request = 'unanswered';
if (isset($categoryslugs)) {
foreach ($categoryslugs as $slug) {
$request .= '/' . $slug;
}
}
$navigation = array(
'by-answers' => array(
'label' => qa_lang('main/nav_no_answer'),
'url' => qa_path_html($request),
),
'by-selected' => array(
'label' => qa_lang('main/nav_no_selected_answer'),
'url' => qa_path_html($request, array('by' => 'selected')),
),
'by-upvotes' => array(
'label' => qa_lang('main/nav_no_upvoted_answer'),
'url' => qa_path_html($request, array('by' => 'upvotes')),
),
);
if (isset($navigation['by-' . $by])) {
$navigation['by-' . $by]['selected'] = true;
} else {
$navigation['by-answers']['selected'] = true;
}
if (!qa_opt('voting_on_as')) {
unset($navigation['by-upvotes']);
}
return $navigation;
}

807
qa-include/app/recalc.php Normal file
View File

@@ -0,0 +1,807 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Managing database recalculations (clean-up operations) and status messages
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
*/
/*
A full list of redundant (non-normal) information in the database that can be recalculated:
Recalculated in doreindexcontent:
================================
^titlewords (all): index of words in titles of posts
^contentwords (all): index of words in content of posts
^tagwords (all): index of words in tags of posts (a tag can contain multiple words)
^posttags (all): index tags of posts
^words (all): list of words used for indexes
^options (title=cache_*): cached values for various things (e.g. counting questions)
Recalculated in dorecountposts:
==============================
^posts (upvotes, downvotes, netvotes, hotness, acount, amaxvotes, flagcount): number of votes, hotness, answers, answer votes, flags
Recalculated in dorecalcpoints:
===============================
^userpoints (all except bonus): points calculation for all users
^options (title=cache_userpointscount):
Recalculated in dorecalccategories:
===================================
^posts (categoryid): assign to answers and comments based on their antecedent question
^posts (catidpath1, catidpath2, catidpath3): hierarchical path to category ids (requires QA_CATEGORY_DEPTH=4)
^categories (qcount): number of (visible) questions in each category
^categories (backpath): full (backwards) path of slugs to that category
Recalculated in dorebuildupdates:
=================================
^sharedevents (all): per-entity event streams (see big comment in /qa-include/db/favorites.php)
^userevents (all): per-subscriber event streams
[but these are not entirely redundant since they can contain historical information no longer in ^posts]
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
require_once QA_INCLUDE_DIR . 'db/recalc.php';
require_once QA_INCLUDE_DIR . 'db/post-create.php';
require_once QA_INCLUDE_DIR . 'db/points.php';
require_once QA_INCLUDE_DIR . 'db/selects.php';
require_once QA_INCLUDE_DIR . 'db/admin.php';
require_once QA_INCLUDE_DIR . 'db/users.php';
require_once QA_INCLUDE_DIR . 'app/options.php';
require_once QA_INCLUDE_DIR . 'app/post-create.php';
require_once QA_INCLUDE_DIR . 'app/post-update.php';
/**
* Advance the recalculation operation represented by $state by a single step.
* $state can also be the name of a recalculation operation on its own.
* @param $state
* @return bool
*/
function qa_recalc_perform_step(&$state)
{
$continue = false;
@list($operation, $length, $next, $done) = explode("\t", $state);
switch ($operation) {
case 'doreindexcontent':
qa_recalc_transition($state, 'doreindexcontent_pagereindex');
break;
case 'doreindexcontent_pagereindex':
$pages = qa_db_pages_get_for_reindexing($next, 10);
if (count($pages)) {
require_once QA_INCLUDE_DIR . 'app/format.php';
$lastpageid = max(array_keys($pages));
foreach ($pages as $pageid => $page) {
if (!($page['flags'] & QA_PAGE_FLAGS_EXTERNAL)) {
$searchmodules = qa_load_modules_with('search', 'unindex_page');
foreach ($searchmodules as $searchmodule) {
$searchmodule->unindex_page($pageid);
}
$searchmodules = qa_load_modules_with('search', 'index_page');
if (count($searchmodules)) {
$indextext = qa_viewer_text($page['content'], 'html');
foreach ($searchmodules as $searchmodule)
$searchmodule->index_page($pageid, $page['tags'], $page['heading'], $page['content'], 'html', $indextext);
}
}
}
$next = 1 + $lastpageid;
$done += count($pages);
$continue = true;
} else {
qa_recalc_transition($state, 'doreindexcontent_postcount');
}
break;
case 'doreindexcontent_postcount':
qa_db_qcount_update();
qa_db_acount_update();
qa_db_ccount_update();
qa_recalc_transition($state, 'doreindexcontent_postreindex');
break;
case 'doreindexcontent_postreindex':
$posts = qa_db_posts_get_for_reindexing($next, 10);
if (count($posts)) {
require_once QA_INCLUDE_DIR . 'app/format.php';
$lastpostid = max(array_keys($posts));
qa_db_prepare_for_reindexing($next, $lastpostid);
qa_suspend_update_counts();
foreach ($posts as $postid => $post) {
qa_post_unindex($postid);
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'], $post['categoryid']);
}
$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_db_uapprovecount_update(); // needs to be somewhere and this is the most appropriate place
qa_recalc_transition($state, 'dorecalcpoints_recalc');
break;
case 'dorecalcpoints_recalc':
$recalccount = 10;
$userids = qa_db_users_get_for_recalc_points($next, $recalccount + 1); // get one extra so we know where to start from next
$gotcount = count($userids);
$recalccount = min($recalccount, $gotcount); // can't recalc more than we got
if ($recalccount > 0) {
$lastuserid = $userids[$recalccount - 1];
qa_db_users_recalc_points($next, $lastuserid);
$done += $recalccount;
} else {
$lastuserid = $next; // for truncation
}
if ($gotcount > $recalccount) { // more left to do
$next = $userids[$recalccount]; // start next round at first one not recalculated
$continue = true;
} else {
qa_db_truncate_userpoints($lastuserid);
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 . 'app/events.php';
require_once QA_INCLUDE_DIR . 'app/updates.php';
require_once QA_INCLUDE_DIR . '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);
if ($followonq) {
$updatetype = QA_UPDATE_FOLLOWS;
} elseif ($post['basetype'] == 'C' && @$posts[$post['parentid']]['basetype'] == 'Q') {
$updatetype = QA_UPDATE_C_FOR_Q;
} elseif ($post['basetype'] == 'C' && @$posts[$post['parentid']]['basetype'] == 'A') {
$updatetype = QA_UPDATE_C_FOR_A;
} else {
$updatetype = null;
}
qa_create_event_for_q_user($questionid, $postid, $updatetype, $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'], QA_UPDATE_FOLLOWS, $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 . '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 . '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 . 'app/posts.php';
$postid = $posts[0];
qa_post_delete($postid);
$next = 1 + $postid;
$done++;
$continue = true;
} else {
qa_recalc_transition($state, 'dodeletehidden_complete');
}
break;
case 'doblobstodisk':
qa_recalc_transition($state, 'doblobstodisk_move');
break;
case 'doblobstodisk_move':
$blob = qa_db_get_next_blob_in_db($next);
if (isset($blob)) {
require_once QA_INCLUDE_DIR . 'app/blobs.php';
require_once QA_INCLUDE_DIR . 'db/blobs.php';
if (qa_write_blob_file($blob['blobid'], $blob['content'], $blob['format'])) {
qa_db_blob_set_content($blob['blobid'], null);
}
$next = 1 + $blob['blobid'];
$done++;
$continue = true;
} else {
qa_recalc_transition($state, 'doblobstodisk_complete');
}
break;
case 'doblobstodb':
qa_recalc_transition($state, 'doblobstodb_move');
break;
case 'doblobstodb_move':
$blob = qa_db_get_next_blob_on_disk($next);
if (isset($blob)) {
require_once QA_INCLUDE_DIR . 'app/blobs.php';
require_once QA_INCLUDE_DIR . 'db/blobs.php';
$content = qa_read_blob_file($blob['blobid'], $blob['format']);
qa_db_blob_set_content($blob['blobid'], $content);
qa_delete_blob_file($blob['blobid'], $blob['format']);
$next = 1 + $blob['blobid'];
$done++;
$continue = true;
} else {
qa_recalc_transition($state, 'doblobstodb_complete');
}
break;
case 'docachetrim':
qa_recalc_transition($state, 'docachetrim_process');
break;
case 'docacheclear':
qa_recalc_transition($state, 'docacheclear_process');
break;
case 'docachetrim_process':
case 'docacheclear_process':
$cacheDriver = Q2A_Storage_CacheFactory::getCacheDriver();
$cacheStats = $cacheDriver->getStats();
$limit = min($cacheStats['files'], 500);
if ($cacheStats['files'] > 0 && $next <= $length) {
$deleted = $cacheDriver->clear($limit, $next, ($operation === 'docachetrim_process'));
$done += $deleted;
$next += $limit - $deleted; // skip files that weren't deleted on next iteration
$continue = true;
} else {
qa_recalc_transition($state, 'docacheclear_complete');
}
break;
default:
$state = '';
break;
}
if ($continue) {
$state = $operation . "\t" . $length . "\t" . $next . "\t" . $done;
}
return $continue && $done < $length;
}
/**
* Change the $state to represent the beginning of a new $operation
* @param $state
* @param $operation
*/
function qa_recalc_transition(&$state, $operation)
{
$length = qa_recalc_stage_length($operation);
$next = (QA_FINAL_EXTERNAL_USERS && ($operation == 'dorecalcpoints_recalc')) ? '' : 0;
$done = 0;
$state = $operation . "\t" . $length . "\t" . $next . "\t" . $done;
}
/**
* Return how many steps there will be in recalculation $operation
* @param $operation
* @return int
*/
function qa_recalc_stage_length($operation)
{
switch ($operation) {
case 'doreindexcontent_pagereindex':
$length = qa_db_count_pages();
break;
case 'doreindexcontent_postreindex':
$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;
case 'doblobstodisk_move':
$length = qa_db_count_blobs_in_db();
break;
case 'doblobstodb_move':
$length = qa_db_count_blobs_on_disk();
break;
case 'docachetrim_process':
case 'docacheclear_process':
$cacheDriver = Q2A_Storage_CacheFactory::getCacheDriver();
$cacheStats = $cacheDriver->getStats();
$length = $cacheStats['files'];
break;
default:
$length = 0;
break;
}
return $length;
}
/**
* Return the translated language ID string replacing the progress and total in it.
* @access private
* @param string $langId Language string ID that contains 2 placeholders (^1 and ^2)
* @param int $progress Amount of processed elements
* @param int $total Total amount of elements
*
* @return string Returns the language string ID with their placeholders replaced with
* the formatted progress and total numbers
*/
function qa_recalc_progress_lang($langId, $progress, $total)
{
return strtr(qa_lang($langId), array(
'^1' => qa_format_number($progress),
'^2' => qa_format_number($total),
));
}
/**
* Return a string which gives a user-viewable version of $state
* @param $state
* @return string
*/
function qa_recalc_get_message($state)
{
require_once QA_INCLUDE_DIR . 'app/format.php';
@list($operation, $length, $next, $done) = explode("\t", $state);
$done = (int) $done;
$length = (int) $length;
switch ($operation) {
case 'doreindexcontent_postcount':
case 'dorecountposts_postcount':
case 'dorecalccategories_postcount':
case 'dorefillevents_qcount':
$message = qa_lang('admin/recalc_posts_count');
break;
case 'doreindexcontent_pagereindex':
$message = qa_recalc_progress_lang('admin/reindex_pages_reindexed', $done, $length);
break;
case 'doreindexcontent_postreindex':
$message = qa_recalc_progress_lang('admin/reindex_posts_reindexed', $done, $length);
break;
case 'doreindexposts_complete':
$message = qa_lang('admin/reindex_posts_complete');
break;
case 'doreindexposts_wordcount':
$message = qa_recalc_progress_lang('admin/reindex_posts_wordcounted', $done, $length);
break;
case 'dorecountposts_votecount':
$message = qa_recalc_progress_lang('admin/recount_posts_votes_recounted', $done, $length);
break;
case 'dorecountposts_acount':
$message = qa_recalc_progress_lang('admin/recount_posts_as_recounted', $done, $length);
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 = qa_recalc_progress_lang('admin/recalc_points_recalced', $done, $length);
break;
case 'dorecalcpoints_complete':
$message = qa_lang('admin/recalc_points_complete');
break;
case 'dorefillevents_refill':
$message = qa_recalc_progress_lang('admin/refill_events_refilled', $done, $length);
break;
case 'dorefillevents_complete':
$message = qa_lang('admin/refill_events_complete');
break;
case 'dorecalccategories_postupdate':
$message = qa_recalc_progress_lang('admin/recalc_categories_updated', $done, $length);
break;
case 'dorecalccategories_recount':
$message = qa_recalc_progress_lang('admin/recalc_categories_recounting', $done, $length);
break;
case 'dorecalccategories_backpaths':
$message = qa_recalc_progress_lang('admin/recalc_categories_backpaths', $done, $length);
break;
case 'dorecalccategories_complete':
$message = qa_lang('admin/recalc_categories_complete');
break;
case 'dodeletehidden_comments':
$message = qa_recalc_progress_lang('admin/hidden_comments_deleted', $done, $length);
break;
case 'dodeletehidden_answers':
$message = qa_recalc_progress_lang('admin/hidden_answers_deleted', $done, $length);
break;
case 'dodeletehidden_questions':
$message = qa_recalc_progress_lang('admin/hidden_questions_deleted', $done, $length);
break;
case 'dodeletehidden_complete':
$message = qa_lang('admin/delete_hidden_complete');
break;
case 'doblobstodisk_move':
case 'doblobstodb_move':
$message = qa_recalc_progress_lang('admin/blobs_move_moved', $done, $length);
break;
case 'doblobstodisk_complete':
case 'doblobstodb_complete':
$message = qa_lang('admin/blobs_move_complete');
break;
case 'docachetrim_process':
case 'docacheclear_process':
$message = qa_recalc_progress_lang('admin/caching_delete_progress', $done, $length);
break;
case 'docacheclear_complete':
$message = qa_lang('admin/caching_delete_complete');
break;
default:
$message = '';
break;
}
return $message;
}

140
qa-include/app/search.php Normal file
View File

@@ -0,0 +1,140 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Wrapper functions and utilities for search modules
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Returns $count search results for $query performed by $userid, starting at offset $start. Set $absoluteurls to true
* to get absolute URLs for the results and $fullcontent if the results should include full post content. This calls
* through to the chosen search module, and performs all the necessary post-processing to supplement the results for
* display online or in an RSS feed.
* @param $query
* @param $start
* @param $count
* @param $userid
* @param $absoluteurls
* @param $fullcontent
* @return
*/
function qa_get_search_results($query, $start, $count, $userid, $absoluteurls, $fullcontent)
{
// Identify which search module should be used
$searchmodules = qa_load_modules_with('search', 'process_search');
if (!count($searchmodules))
qa_fatal_error('No search engine is available');
$module = reset($searchmodules); // use first one by default
if (count($searchmodules) > 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;
}

View File

@@ -0,0 +1,53 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Definitions relating to favorites and updates in the database tables
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
// Character codes for the different types of entity that can be followed (entitytype columns)
define('QA_ENTITY_QUESTION', 'Q');
define('QA_ENTITY_USER', 'U');
define('QA_ENTITY_TAG', 'T');
define('QA_ENTITY_CATEGORY', 'C');
define('QA_ENTITY_NONE', '-');
// Character codes for the different types of updates on a post (updatetype columns)
define('QA_UPDATE_CATEGORY', 'A'); // questions only, category changed
define('QA_UPDATE_CLOSED', 'C'); // questions only, closed or reopened
define('QA_UPDATE_CONTENT', 'E'); // title or content edited
define('QA_UPDATE_PARENT', 'M'); // e.g. comment moved when converting its parent answer to a comment
define('QA_UPDATE_SELECTED', 'S'); // answers only, removed if unselected
define('QA_UPDATE_TAGS', 'T'); // questions only
define('QA_UPDATE_TYPE', 'Y'); // e.g. answer to comment
define('QA_UPDATE_VISIBLE', 'H'); // hidden or reshown
// Character codes for types of update that only appear in the streams tables, not on the posts themselves
define('QA_UPDATE_FOLLOWS', 'F'); // if a new question was asked related to one of its answers, or for a comment that follows another
define('QA_UPDATE_C_FOR_Q', 'U'); // if comment created was on a question of the user whose stream this appears in
define('QA_UPDATE_C_FOR_A', 'N'); // if comment created was on an answer of the user whose stream this appears in

211
qa-include/app/upload.php Normal file
View File

@@ -0,0 +1,211 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Application-level file upload functionality
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Return the maximum size of file that can be uploaded, based on database and PHP limits
*/
function qa_get_max_upload_size()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
$mindb = 16777215; // from MEDIUMBLOB column type
$minphp = trim(ini_get('upload_max_filesize'));
$minphp = convert_to_bytes(substr($minphp, -1), $minphp);
return min($mindb, $minphp);
}
/**
* Move an uploaded image or other file into blob storage. Pass the $localfilename where the file is currently stored
* (temporarily) and the $sourcefilename of the file on the user's computer (if using PHP's usual file upload
* mechanism, these are obtained from $_FILES[..]['tmp_name'] and $_FILES[..]['name'] fields respectively). To apply a
* maximum file size (in bytes) beyond the general one, use $maxfilesize, otherwise set it to null. Set $onlyimage to
* true if only image uploads (PNG, GIF, JPEG) are allowed. To apply a maximum width or height (in pixels) to uploaded
* images, set $imagemaxwidth and $imagemaxheight. The function returns an array which may contain the following elements:
*
* 'error' => a string containing an error, if one occurred
* 'format' => the format (file extension) of the blob created (all scaled images end up as 'jpg')
* 'width' => if an image, the width in pixels of the blob created (after possible scaling)
* 'height' => if an image, the height in pixels of the blob created (after possible scaling)
* 'blobid' => the blobid that was created (if there was no error)
* 'bloburl' => the url that can be used to view/download the created blob (if there was no error)
* @param $localfilename
* @param $sourcefilename
* @param $maxfilesize
* @param bool $onlyimage
* @param $imagemaxwidth
* @param $imagemaxheight
* @return array
*/
function qa_upload_file($localfilename, $sourcefilename, $maxfilesize = null, $onlyimage = false, $imagemaxwidth = null, $imagemaxheight = null)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
$result = array();
// Check per-user upload limits
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'app/limits.php';
switch (qa_user_permit_error(null, QA_LIMIT_UPLOADS)) {
case 'limit':
$result['error'] = qa_lang('main/upload_limit');
return $result;
case false:
qa_limits_increment(qa_get_logged_in_userid(), QA_LIMIT_UPLOADS);
break;
default:
$result['error'] = qa_lang('users/no_permission');
return $result;
}
// Check the uploaded file is not too large
$filesize = filesize($localfilename);
if (isset($maxfilesize))
$maxfilesize = min($maxfilesize, qa_get_max_upload_size());
else
$maxfilesize = qa_get_max_upload_size();
if ($filesize <= 0 || $filesize > $maxfilesize) { // if file was too big for PHP, $filesize will be zero
$result['error'] = qa_lang_sub('main/max_upload_size_x', qa_format_number($maxfilesize / pow(1024, 2), 1) . 'MB');
return $result;
}
// Find out what type of source file was uploaded and if appropriate, check it's an image and get preliminary size measure
$pathinfo = pathinfo($sourcefilename);
$format = strtolower(@$pathinfo['extension']);
$isimage = in_array($format, array('png', 'gif', 'jpeg', 'jpg')); // allowed image extensions
if ($isimage) {
$imagesize = @getimagesize($localfilename);
if (is_array($imagesize)) {
$result['width'] = $imagesize[0];
$result['height'] = $imagesize[1];
switch ($imagesize['2']) { // reassign format based on actual content, if we can
case IMAGETYPE_GIF:
$format = 'gif';
break;
case IMAGETYPE_JPEG:
$format = 'jpg';
break;
case IMAGETYPE_PNG:
$format = 'png';
break;
}
}
}
$result['format'] = $format;
if ($onlyimage) {
if (!$isimage || !is_array($imagesize)) {
$result['error'] = qa_lang_sub('main/image_not_read', 'GIF, JPG, PNG');
return $result;
}
}
// Read in the raw file contents
$content = file_get_contents($localfilename);
// If appropriate, get more accurate image size and apply constraints to it
require_once QA_INCLUDE_DIR . 'util/image.php';
if ($isimage && qa_has_gd_image()) {
$image = @imagecreatefromstring($content);
if (is_resource($image)) {
$result['width'] = $width = imagesx($image);
$result['height'] = $height = imagesy($image);
if (isset($imagemaxwidth) || isset($imagemaxheight)) {
if (qa_image_constrain(
$width, $height,
isset($imagemaxwidth) ? $imagemaxwidth : $width,
isset($imagemaxheight) ? $imagemaxheight : $height
)) {
qa_gd_image_resize($image, $width, $height);
if (is_resource($image)) {
$content = qa_gd_image_jpeg($image);
$result['format'] = $format = 'jpg';
$result['width'] = $width;
$result['height'] = $height;
}
}
}
if (is_resource($image)) // might have been lost
imagedestroy($image);
}
}
// Create the blob and return
require_once QA_INCLUDE_DIR . 'app/blobs.php';
$userid = qa_get_logged_in_userid();
$cookieid = isset($userid) ? qa_cookie_get() : qa_cookie_get_create();
$result['blobid'] = qa_create_blob($content, $format, $sourcefilename, $userid, $cookieid, qa_remote_ip_address());
if (!isset($result['blobid'])) {
$result['error'] = qa_lang('main/general_error');
return $result;
}
$result['bloburl'] = qa_get_blob_url($result['blobid'], true);
return $result;
}
/**
* In response to a file upload, move the first uploaded file into blob storage. Other parameters are as for qa_upload_file(...)
* @param $maxfilesize
* @param bool $onlyimage
* @param $imagemaxwidth
* @param $imagemaxheight
* @return array
*/
function qa_upload_file_one($maxfilesize = null, $onlyimage = false, $imagemaxwidth = null, $imagemaxheight = null)
{
$file = reset($_FILES);
return qa_upload_file($file['tmp_name'], $file['name'], $maxfilesize, $onlyimage, $imagemaxwidth, $imagemaxheight);
}

View File

@@ -0,0 +1,541 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: User management (application level) for creating/modifying users
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
if (!defined('QA_MIN_PASSWORD_LEN')) {
define('QA_MIN_PASSWORD_LEN', 8);
}
if (!defined('QA_NEW_PASSWORD_LEN')){
/**
* @deprecated This was the length of the reset password generated by Q2A. No longer used.
*/
define('QA_NEW_PASSWORD_LEN', 8);
}
/**
* Return $errors fields for any invalid aspect of user-entered $handle (username) and $email. Works by calling through
* to all filter modules and also rejects existing values in database unless they belongs to $olduser (if set).
* @param $handle
* @param $email
* @param $olduser
* @return array
*/
function qa_handle_email_filter(&$handle, &$email, $olduser = null)
{
require_once QA_INCLUDE_DIR . 'db/users.php';
require_once QA_INCLUDE_DIR . 'util/string.php';
$errors = array();
// sanitize 4-byte Unicode and invisible characters
$handle = qa_remove_utf8mb4($handle);
$handle = preg_replace('/\p{C}+/u', '', $handle);
$filtermodules = qa_load_modules_with('filter', 'filter_handle');
foreach ($filtermodules as $filtermodule) {
$error = $filtermodule->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;
}
/**
* Make $handle valid and unique in the database - if $allowuserid is set, allow it to match that user only
* @param $handle
* @return string
*/
function qa_handle_make_valid($handle)
{
require_once QA_INCLUDE_DIR . 'util/string.php';
require_once QA_INCLUDE_DIR . 'db/maxima.php';
require_once QA_INCLUDE_DIR . 'db/users.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) {
// filter first without worrying about errors, since our goal is to get a valid one
$filtermodule->filter_handle($tryhandle, null);
}
$haderror = false;
foreach ($filtermodules as $filtermodule) {
$error = $filtermodule->filter_handle($tryhandle, 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 from: ' . $handle);
}
/**
* 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.
* @param $password
* @param $olduser
* @return array
*/
function qa_password_validate($password, $olduser = null)
{
$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();
}
/**
* 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.
* @param $email
* @param $password
* @param $handle
* @param int $level
* @param bool $confirmed
* @return mixed
*/
function qa_create_new_user($email, $password, $handle, $level = QA_USER_LEVEL_BASIC, $confirmed = false)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'db/users.php';
require_once QA_INCLUDE_DIR . 'db/points.php';
require_once QA_INCLUDE_DIR . 'app/options.php';
require_once QA_INCLUDE_DIR . 'app/emails.php';
require_once QA_INCLUDE_DIR . 'app/cookies.php';
$userid = qa_db_user_create($email, $password, $handle, $level, qa_remote_ip_address());
qa_db_points_update_ifuser($userid, null);
qa_db_uapprovecount_update();
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_USER_LEVEL_EXPERT && !$confirmed) {
$confirm = strtr(qa_lang('emails/welcome_confirm'), array(
'^url' => 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 = '';
// we no longer use the 'approve_user_required' option to set QA_USER_FLAGS_MUST_APPROVE; this can be handled by the Permissions settings
qa_send_notification($userid, $email, $handle, qa_lang('emails/welcome_subject'), qa_lang('emails/welcome_body'), array(
'^password' => isset($password) ? qa_lang('main/hidden') : qa_lang('users/password_to_set'), // v 1.6.3: no longer email out passwords
'^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;
}
/**
* 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.
* @param $userid
* @return mixed
*/
function qa_delete_user($userid)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'db/votes.php';
require_once QA_INCLUDE_DIR . 'db/users.php';
require_once QA_INCLUDE_DIR . 'db/post-update.php';
require_once QA_INCLUDE_DIR . '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);
qa_db_uapprovecount_update();
qa_db_userpointscount_update();
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'));
}
}
/**
* Set a new email confirmation code for the user and send it out
* @param $userid
* @return mixed
*/
function qa_send_new_confirm($userid)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'db/users.php';
require_once QA_INCLUDE_DIR . 'db/selects.php';
require_once QA_INCLUDE_DIR . 'app/emails.php';
$userinfo = qa_db_select_with_pending(qa_db_user_account_selectspec($userid, true));
$emailcode = qa_db_user_rand_emailcode();
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'], $emailcode),
'^code' => $emailcode,
))) {
qa_fatal_error('Could not send email confirmation');
}
}
/**
* Set a new email confirmation code for the user and return the corresponding link. If the email code is also sent then that value
* is used. Otherwise, a new email code is generated
* @param $userid
* @param $handle
* @param $emailcode
* @return mixed|string
*/
function qa_get_new_confirm_url($userid, $handle, $emailcode = null)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'db/users.php';
if (!isset($emailcode)) {
$emailcode = qa_db_user_rand_emailcode();
}
qa_db_user_set($userid, 'emailcode', $emailcode);
return qa_path_absolute('confirm', array('c' => $emailcode, 'u' => $handle));
}
/**
* Complete the email confirmation process for the user
* @param $userid
* @param $email
* @param $handle
* @return mixed
*/
function qa_complete_confirm($userid, $email, $handle)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'db/users.php';
require_once QA_INCLUDE_DIR . '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,
));
}
/**
* Set the user level of user $userid with $handle to $level (one of the QA_USER_LEVEL_* constraints in /qa-include/app/users.php)
* Pass the previous user level in $oldlevel. Reports the appropriate event, assumes change performed by the logged in user.
* @param $userid
* @param $handle
* @param $level
* @param $oldlevel
*/
function qa_set_user_level($userid, $handle, $level, $oldlevel)
{
require_once QA_INCLUDE_DIR . 'db/users.php';
qa_db_user_set($userid, 'level', $level);
qa_db_uapprovecount_update();
if ($level >= QA_USER_LEVEL_APPROVED) {
// no longer necessary as QA_USER_FLAGS_MUST_APPROVE is deprecated, but kept for posterity
qa_db_user_set_flag($userid, QA_USER_FLAGS_MUST_APPROVE, false);
}
qa_report_event('u_level', qa_get_logged_in_userid(), qa_get_logged_in_handle(), qa_cookie_get(), array(
'userid' => $userid,
'handle' => $handle,
'level' => $level,
'oldlevel' => $oldlevel,
));
}
/**
* Set the status of user $userid with $handle to blocked if $blocked is true, otherwise to unblocked. Reports the appropriate
* event, assumes change performed by the logged in user.
* @param $userid
* @param $handle
* @param $blocked
*/
function qa_set_user_blocked($userid, $handle, $blocked)
{
require_once QA_INCLUDE_DIR . 'db/users.php';
qa_db_user_set_flag($userid, QA_USER_FLAGS_USER_BLOCKED, $blocked);
qa_db_uapprovecount_update();
qa_report_event($blocked ? 'u_block' : 'u_unblock', qa_get_logged_in_userid(), qa_get_logged_in_handle(), qa_cookie_get(), array(
'userid' => $userid,
'handle' => $handle,
));
}
/**
* Start the 'I forgot my password' process for $userid, sending reset code
* @param $userid
* @return mixed
*/
function qa_start_reset_user($userid)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'db/users.php';
require_once QA_INCLUDE_DIR . 'app/options.php';
require_once QA_INCLUDE_DIR . 'app/emails.php';
require_once QA_INCLUDE_DIR . '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_absolute('reset', array('c' => $userinfo['emailcode'], 'e' => $userinfo['email'])),
))) {
qa_fatal_error('Could not send reset password email');
}
}
/**
* Successfully finish the 'I forgot my password' process for $userid, sending new password
*
* @deprecated This function has been replaced by qa_finish_reset_user since Q2A 1.8
* @param $userid
* @return mixed
*/
function qa_complete_reset_user($userid)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'util/string.php';
require_once QA_INCLUDE_DIR . 'app/options.php';
require_once QA_INCLUDE_DIR . 'app/emails.php';
require_once QA_INCLUDE_DIR . 'app/cookies.php';
require_once QA_INCLUDE_DIR . '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'],
));
}
/**
* Successfully finish the 'I forgot my password' process for $userid, cleaning the emailcode field and logging in the user
* @param mixed $userId The userid identifiying the user who will have the password reset
* @param string $newPassword The new password for the user
* @return void
*/
function qa_finish_reset_user($userId, $newPassword)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
// For qa_db_user_set_password(), qa_db_user_set()
require_once QA_INCLUDE_DIR . 'db/users.php';
// For qa_set_logged_in_user()
require_once QA_INCLUDE_DIR . 'app/options.php';
// For qa_cookie_get()
require_once QA_INCLUDE_DIR . 'app/cookies.php';
// For qa_db_select_with_pending(), qa_db_user_account_selectspec()
require_once QA_INCLUDE_DIR . 'db/selects.php';
// For qa_set_logged_in_user()
require_once QA_INCLUDE_DIR . 'app/users.php';
qa_db_user_set_password($userId, $newPassword);
qa_db_user_set($userId, 'emailcode', ''); // to prevent re-use of the code
$userInfo = qa_db_select_with_pending(qa_db_user_account_selectspec($userId, true));
qa_set_logged_in_user($userId, $userInfo['handle'], false, $userInfo['sessionsource']); // reinstate this specific session
qa_report_event('u_reset', $userId, $userInfo['handle'], qa_cookie_get(), array(
'email' => $userInfo['email'],
));
}
/**
* Flush any information about the currently logged in user, so it is retrieved from database again
*/
function qa_logged_in_user_flush()
{
global $qa_cached_logged_in_user;
$qa_cached_logged_in_user = null;
}
/**
* Set the avatar of $userid to the image in $imagedata, and remove $oldblobid from the database if not null
* @param $userid
* @param $imagedata
* @param $oldblobid
* @return bool
*/
function qa_set_user_avatar($userid, $imagedata, $oldblobid = null)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'util/image.php';
$imagedata = qa_image_constrain_data($imagedata, $width, $height, qa_opt('avatar_store_size'));
if (isset($imagedata)) {
require_once QA_INCLUDE_DIR . 'app/blobs.php';
$newblobid = qa_create_blob($imagedata, 'jpeg', null, $userid, null, qa_remote_ip_address());
if (isset($newblobid)) {
qa_db_user_set($userid, array(
'avatarblobid' => $newblobid,
'avatarwidth' => $width,
'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_delete_blob($oldblobid);
return true;
}
}
return false;
}

1428
qa-include/app/users.php Normal file

File diff suppressed because it is too large Load Diff

363
qa-include/app/votes.php Normal file
View File

@@ -0,0 +1,363 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Handling incoming votes (application level)
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Check if $userid can vote on $post, on the page $topage.
* Return an HTML error to display if there was a problem, or false if it's OK.
* @param $post
* @param $vote
* @param $userid
* @param $topage
* @return bool|mixed|string
*/
function qa_vote_error_html($post, $vote, $userid, $topage)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
// The 'login', 'confirm', 'limit', 'userblock' and 'ipblock' permission errors are reported to the user here.
// Others ('approve', 'level') prevent the buttons being clickable in the first place, in qa_get_vote_view(...)
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'app/limits.php';
if ($post['hidden']) {
return qa_lang_html('main/vote_disabled_hidden');
}
if ($post['queued']) {
return qa_lang_html('main/vote_disabled_queued');
}
switch($post['basetype'])
{
case 'Q':
$allowVoting = qa_opt('voting_on_qs');
break;
case 'A':
$allowVoting = qa_opt('voting_on_as');
break;
case 'C':
$allowVoting = qa_opt('voting_on_cs');
break;
default:
$allowVoting = false;
break;
}
if (!$allowVoting || (isset($post['userid']) && isset($userid) && $post['userid'] == $userid)) {
// voting option should not have been presented (but could happen due to options change)
return qa_lang_html('main/vote_not_allowed');
}
$permiterror = qa_user_post_permit_error(($post['basetype'] == 'Q') ? 'permit_vote_q' : 'permit_vote_a', $post, QA_LIMIT_VOTES);
$errordownonly = !$permiterror && $vote < 0;
if ($errordownonly) {
$permiterror = qa_user_post_permit_error('permit_vote_down', $post);
}
switch ($permiterror) {
case false:
return false;
break;
case 'login':
return qa_insert_login_links(qa_lang_html('main/vote_must_login'), $topage);
break;
case 'confirm':
return qa_insert_login_links(qa_lang_html($errordownonly ? 'main/vote_down_must_confirm' : 'main/vote_must_confirm'), $topage);
break;
case 'limit':
return qa_lang_html('main/vote_limit');
break;
default:
return qa_lang_html('users/no_permission');
break;
}
}
/**
* Actually set (application level) the $vote (-1/0/1) by $userid (with $handle and $cookieid) on $postid.
* Handles user points, recounting and event reports as appropriate.
* @param $post
* @param $userid
* @param $handle
* @param $cookieid
* @param $vote
* @return void
*/
function qa_vote_set($post, $userid, $handle, $cookieid, $vote)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'db/points.php';
require_once QA_INCLUDE_DIR . 'db/hotness.php';
require_once QA_INCLUDE_DIR . 'db/votes.php';
require_once QA_INCLUDE_DIR . 'db/post-create.php';
require_once QA_INCLUDE_DIR . 'app/limits.php';
$vote = (int)min(1, max(-1, $vote));
$oldvote = (int)qa_db_uservote_get($post['postid'], $userid);
if ($oldvote === $vote) {
return;
}
qa_db_uservote_set($post['postid'], $userid, $vote);
qa_db_post_recount_votes($post['postid']);
if (!in_array($post['basetype'], array('Q', 'A', 'C'))) {
return;
}
$prefix = strtolower($post['basetype']);
if ($prefix === 'a') {
qa_db_post_acount_update($post['parentid']);
qa_db_unupaqcount_update();
}
$columns = array();
if ($vote > 0 || $oldvote > 0) {
$columns[] = $prefix . 'upvotes';
}
if ($vote < 0 || $oldvote < 0) {
$columns[] = $prefix . 'downvotes';
}
qa_db_points_update_ifuser($userid, $columns);
qa_db_points_update_ifuser($post['userid'], array($prefix . 'voteds', 'upvoteds', 'downvoteds'));
if ($prefix === 'q') {
qa_db_hotness_update($post['postid']);
}
if ($vote < 0) {
$event = $prefix . '_vote_down';
} elseif ($vote > 0) {
$event = $prefix . '_vote_up';
} else {
$event = $prefix . '_vote_nil';
}
qa_report_event($event, $userid, $handle, $cookieid, array(
'postid' => $post['postid'],
'userid' => $post['userid'],
'vote' => $vote,
'oldvote' => $oldvote,
));
}
/**
* 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.
* @param $post
* @param $userid
* @param $topage
* @return bool|mixed|string
*/
function qa_flag_error_html($post, $userid, $topage)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
// The 'login', 'confirm', 'limit', 'userblock' and 'ipblock' permission errors are reported to the user here.
// Others ('approve', 'level') prevent the flag button being shown, in qa_page_q_post_rules(...)
require_once QA_INCLUDE_DIR . 'db/selects.php';
require_once QA_INCLUDE_DIR . 'app/options.php';
require_once QA_INCLUDE_DIR . 'app/users.php';
require_once QA_INCLUDE_DIR . 'app/limits.php';
if (is_array($post) && qa_opt('flagging_of_posts') &&
(!isset($post['userid']) || !isset($userid) || $post['userid'] != $userid)
) {
switch (qa_user_post_permit_error('permit_flag', $post, 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
}
}
/**
* 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.
* @param $oldpost
* @param $userid
* @param $handle
* @param $cookieid
* @param $question
* @return bool
*/
function qa_flag_set_tohide($oldpost, $userid, $handle, $cookieid, $question)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'db/votes.php';
require_once QA_INCLUDE_DIR . 'app/limits.php';
require_once QA_INCLUDE_DIR . 'db/post-update.php';
qa_db_userflag_set($oldpost['postid'], $userid, true);
qa_db_post_recount_flags($oldpost['postid']);
qa_db_flaggedcount_update();
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'];
}
/**
* Clear (application level) a flag on $oldpost by $userid (with $handle and $cookieid).
* Handles recounting and event reports as appropriate.
* @param $oldpost
* @param $userid
* @param $handle
* @param $cookieid
* @return mixed
*/
function qa_flag_clear($oldpost, $userid, $handle, $cookieid)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'db/votes.php';
require_once QA_INCLUDE_DIR . 'app/limits.php';
require_once QA_INCLUDE_DIR . 'db/post-update.php';
qa_db_userflag_set($oldpost['postid'], $userid, false);
qa_db_post_recount_flags($oldpost['postid']);
qa_db_flaggedcount_update();
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,
));
}
/**
* Clear (application level) all flags on $oldpost by $userid (with $handle and $cookieid).
* Handles recounting and event reports as appropriate.
* @param $oldpost
* @param $userid
* @param $handle
* @param $cookieid
* @return mixed
*/
function qa_flags_clear_all($oldpost, $userid, $handle, $cookieid)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'db/votes.php';
require_once QA_INCLUDE_DIR . 'app/limits.php';
require_once QA_INCLUDE_DIR . 'db/post-update.php';
qa_db_userflags_clear_all($oldpost['postid']);
qa_db_post_recount_flags($oldpost['postid']);
qa_db_flaggedcount_update();
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,
));
}

686
qa-include/db/admin.php Normal file
View File

@@ -0,0 +1,686 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Database access functions which are specific to the admin center
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Return the current version of MySQL
*/
function qa_db_mysql_version()
{
return qa_db_read_one_value(qa_db_query_raw('SELECT VERSION()'));
}
/**
* Return the total size in bytes of all relevant tables in the Q2A database
*/
function qa_db_table_size()
{
if (defined('QA_MYSQL_USERS_PREFIX')) { // check if one of the prefixes is a prefix itself of the other
if (stripos(QA_MYSQL_USERS_PREFIX, QA_MYSQL_TABLE_PREFIX) === 0)
$prefixes = array(QA_MYSQL_TABLE_PREFIX);
elseif (stripos(QA_MYSQL_TABLE_PREFIX, QA_MYSQL_USERS_PREFIX) === 0)
$prefixes = array(QA_MYSQL_USERS_PREFIX);
else
$prefixes = array(QA_MYSQL_TABLE_PREFIX, QA_MYSQL_USERS_PREFIX);
} else
$prefixes = array(QA_MYSQL_TABLE_PREFIX);
$size = 0;
foreach ($prefixes as $prefix) {
$statuses = qa_db_read_all_assoc(qa_db_query_raw(
"SHOW TABLE STATUS LIKE '" . $prefix . "%'"
));
foreach ($statuses as $status)
$size += $status['Data_length'] + $status['Index_length'];
}
return $size;
}
/**
* Return a count of the number of posts of $type in database.
* Set $fromuser to true to only count non-anonymous posts, false to only count anonymous posts
* @param $type
* @param $fromuser
* @return mixed|null
*/
function qa_db_count_posts($type = null, $fromuser = null)
{
$wheresql = '';
if (isset($type))
$wheresql .= ' WHERE type=' . qa_db_argument_to_mysql($type, true);
if (isset($fromuser))
$wheresql .= (strlen($wheresql) ? ' AND' : ' WHERE') . ' userid ' . ($fromuser ? 'IS NOT' : 'IS') . ' NULL';
return qa_db_read_one_value(qa_db_query_sub(
'SELECT COUNT(*) FROM ^posts' . $wheresql
));
}
/**
* Return number of registered users in database.
*/
function qa_db_count_users()
{
return qa_db_read_one_value(qa_db_query_sub(
'SELECT COUNT(*) FROM ^users'
));
}
/**
* Return number of active users in database $table
* @param $table
* @return mixed|null
*/
function qa_db_count_active_users($table)
{
switch ($table) {
case 'posts':
case 'uservotes':
case 'userpoints':
break;
default:
qa_fatal_error('qa_db_count_active_users() called for unknown table');
break;
}
return qa_db_read_one_value(qa_db_query_sub(
'SELECT COUNT(DISTINCT(userid)) FROM ^' . $table
));
}
/**
* Return number of categories in the database
*/
function qa_db_count_categories()
{
return qa_db_read_one_value(qa_db_query_sub(
'SELECT COUNT(*) FROM ^categories'
));
}
/**
* Return number of questions in the database in $categoryid exactly, and not one of its subcategories
* @param $categoryid
* @return mixed|null
*/
function qa_db_count_categoryid_qs($categoryid)
{
return qa_db_read_one_value(qa_db_query_sub(
"SELECT COUNT(*) FROM ^posts WHERE categoryid<=># AND type='Q'",
$categoryid
));
}
/**
* Return list of postids of visible or queued posts by $userid
* @param $userid
* @return array
*/
function qa_db_get_user_visible_postids($userid)
{
return qa_db_read_all_values(qa_db_query_sub(
"SELECT postid FROM ^posts WHERE userid=# AND type IN ('Q', 'A', 'C', 'Q_QUEUED', 'A_QUEUED', 'C_QUEUED')",
$userid
));
}
/**
* Return list of postids of visible or queued posts from $ip address
* @param $ip
* @return array
*/
function qa_db_get_ip_visible_postids($ip)
{
return qa_db_read_all_values(qa_db_query_sub(
"SELECT postid FROM ^posts WHERE createip=UNHEX($) AND type IN ('Q', 'A', 'C', 'Q_QUEUED', 'A_QUEUED', 'C_QUEUED')",
bin2hex(@inet_pton($ip))
));
}
/**
* Return an array whose keys contain the $postids which exist, and whose elements contain the number of other posts depending on each one
* @param $postids
* @return array
*/
function qa_db_postids_count_dependents($postids)
{
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();
}
/**
* Return an array of the (up to) $count most recently created users who are awaiting approval and have not been blocked.
* The array element for each user includes a 'profile' key whose value is an array of non-empty profile fields of the user.
* @param $count
* @return array
*/
function qa_db_get_unapproved_users($count)
{
$results = qa_db_read_all_assoc(qa_db_query_sub(
"SELECT ^users.userid, UNIX_TIMESTAMP(created) AS created, createip, email, handle, flags, title, content FROM ^users LEFT JOIN ^userprofile ON ^users.userid=^userprofile.userid AND LENGTH(content)>0 WHERE level<# AND NOT (flags&#) ORDER BY created DESC LIMIT #",
QA_USER_LEVEL_APPROVED, QA_USER_FLAGS_USER_BLOCKED, $count
));
$users = array();
foreach ($results as $result) {
$userid = $result['userid'];
if (!isset($users[$userid])) {
$users[$result['userid']] = $result;
$users[$result['userid']]['profile'] = array();
unset($users[$userid]['title']);
unset($users[$userid]['content']);
}
if (isset($result['title']) && isset($result['content']))
$users[$userid]['profile'][$result['title']] = $result['content'];
}
return $users;
}
/**
* Return whether there are any blobs whose content has been stored as a file on disk
*/
function qa_db_has_blobs_on_disk()
{
return qa_db_read_one_value(qa_db_query_sub('SELECT blobid FROM ^blobs WHERE content IS NULL LIMIT 1'), true) != null;
}
/**
* Return whether there are any blobs whose content has been stored in the database
*/
function qa_db_has_blobs_in_db()
{
return qa_db_read_one_value(qa_db_query_sub('SELECT blobid FROM ^blobs WHERE content IS NOT NULL LIMIT 1'), true) != null;
}
/**
* Return the maximum position of the categories with $parentid
* @param $parentid
* @return mixed|null
*/
function qa_db_category_last_pos($parentid)
{
return qa_db_read_one_value(qa_db_query_sub(
'SELECT COALESCE(MAX(position), 0) FROM ^categories WHERE parentid<=>#',
$parentid
));
}
/**
* Return how many levels of subcategory there are below $categoryid
* @param $categoryid
* @return int
*/
function qa_db_category_child_depth($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;
}
/**
* Create a new category with $parentid, $title (=name) and $tags (=slug) in the database
* @param $parentid
* @param $title
* @param $tags
* @return mixed
*/
function qa_db_category_create($parentid, $title, $tags)
{
$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;
}
/**
* Recalculate the backpath columns for all categories from $firstcategoryid to $lastcategoryid (if specified)
* @param $firstcategoryid
* @param $lastcategoryid
*/
function qa_db_categories_recalc_backpaths($firstcategoryid, $lastcategoryid = null)
{
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
);
}
/**
* Set the name of $categoryid to $title and its slug to $tags in the database
* @param $categoryid
* @param $title
* @param $tags
*/
function qa_db_category_rename($categoryid, $title, $tags)
{
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
}
/**
* Set the content (=description) of $categoryid to $content
* @param $categoryid
* @param $content
*/
function qa_db_category_set_content($categoryid, $content)
{
qa_db_query_sub(
'UPDATE ^categories SET content=$ WHERE categoryid=#',
$content, $categoryid
);
}
/**
* Return the parentid of $categoryid
* @param $categoryid
* @return mixed|null
*/
function qa_db_category_get_parent($categoryid)
{
return qa_db_read_one_value(qa_db_query_sub(
'SELECT parentid FROM ^categories WHERE categoryid=#',
$categoryid
));
}
/**
* Move the category $categoryid into position $newposition under its parent
* @param $categoryid
* @param $newposition
*/
function qa_db_category_set_position($categoryid, $newposition)
{
qa_db_ordered_move('categories', 'categoryid', $categoryid, $newposition,
qa_db_apply_sub('parentid<=>#', array(qa_db_category_get_parent($categoryid))));
}
/**
* Set the parent of $categoryid to $newparentid, placing it in last position (doesn't do necessary recalculations)
* @param $categoryid
* @param $newparentid
*/
function qa_db_category_set_parent($categoryid, $newparentid)
{
$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
);
}
}
/**
* Change the categoryid of any posts with (exact) $categoryid to $reassignid
* @param $categoryid
* @param $reassignid
*/
function qa_db_category_reassign($categoryid, $reassignid)
{
qa_db_query_sub('UPDATE ^posts SET categoryid=# WHERE categoryid<=>#', $reassignid, $categoryid);
}
/**
* Delete the category $categoryid in the database
* @param $categoryid
*/
function qa_db_category_delete($categoryid)
{
qa_db_ordered_delete('categories', 'categoryid', $categoryid,
qa_db_apply_sub('parentid<=>#', array(qa_db_category_get_parent($categoryid))));
}
/**
* Return the categoryid for the category with parent $parentid and $slug
* @param $parentid
* @param $slug
* @return mixed|null
*/
function qa_db_category_slug_to_id($parentid, $slug)
{
return qa_db_read_one_value(qa_db_query_sub(
'SELECT categoryid FROM ^categories WHERE parentid<=># AND tags=$',
$parentid, $slug
), true);
}
/**
* Create a new custom page (or link) in the database
* @param $title
* @param $flags
* @param $tags
* @param $heading
* @param $content
* @param $permit
* @return mixed
*/
function qa_db_page_create($title, $flags, $tags, $heading, $content, $permit = null)
{
$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();
}
/**
* Set the fields of $pageid to the values provided in the database
* @param $pageid
* @param $title
* @param $flags
* @param $tags
* @param $heading
* @param $content
* @param $permit
*/
function qa_db_page_set_fields($pageid, $title, $flags, $tags, $heading, $content, $permit = null)
{
qa_db_query_sub(
'UPDATE ^pages SET title=$, flags=#, permit=#, tags=$, heading=$, content=$ WHERE pageid=#',
$title, $flags, $permit, $tags, $heading, $content, $pageid
);
}
/**
* Move the page $pageid into navigation menu $nav and position $newposition in the database
* @param $pageid
* @param $nav
* @param $newposition
*/
function qa_db_page_move($pageid, $nav, $newposition)
{
qa_db_query_sub(
'UPDATE ^pages SET nav=$ WHERE pageid=#',
$nav, $pageid
);
qa_db_ordered_move('pages', 'pageid', $pageid, $newposition);
}
/**
* Delete the page $pageid in the database
* @param $pageid
*/
function qa_db_page_delete($pageid)
{
qa_db_ordered_delete('pages', 'pageid', $pageid);
}
/**
* Move the entity identified by $idcolumn=$id into position $newposition (within optional $conditionsql) in $table in the database
* @param $table
* @param $idcolumn
* @param $id
* @param $newposition
* @param $conditionsql
*/
function qa_db_ordered_move($table, $idcolumn, $id, $newposition, $conditionsql = null)
{
$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
// move it temporarily off the top because we have a unique key on the position column
qa_db_query_sub('UPDATE ^' . $table . ' SET position=# WHERE ' . $idcolumn . '=#' . $andsql, 1 + $lastposition, $id);
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');
}
/**
* Delete the entity identified by $idcolumn=$id (and optional $conditionsql) in $table in the database
* @param $table
* @param $idcolumn
* @param $id
* @param $conditionsql
*/
function qa_db_ordered_delete($table, $idcolumn, $id, $conditionsql = null)
{
$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');
}
/**
* Create a new user field with (internal) tag $title, label $content, $flags and $permit in the database.
* @param $title
* @param $content
* @param $flags
* @param $permit
* @return mixed
*/
function qa_db_userfield_create($title, $content, $flags, $permit = null)
{
$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, permit) VALUES ($, $, #, #, #)',
$title, $content, $position, $flags, $permit
);
return qa_db_last_insert_id();
}
/**
* Change the user field $fieldid to have label $content, $flags and $permit in the database (the title column cannot be changed once set)
* @param $fieldid
* @param $content
* @param $flags
* @param $permit
*/
function qa_db_userfield_set_fields($fieldid, $content, $flags, $permit = null)
{
qa_db_query_sub(
'UPDATE ^userfields SET content=$, flags=#, permit=# WHERE fieldid=#',
$content, $flags, $permit, $fieldid
);
}
/**
* Move the user field $fieldid into position $newposition in the database
* @param $fieldid
* @param $newposition
*/
function qa_db_userfield_move($fieldid, $newposition)
{
qa_db_ordered_move('userfields', 'fieldid', $fieldid, $newposition);
}
/**
* Delete the user field $fieldid in the database
* @param $fieldid
*/
function qa_db_userfield_delete($fieldid)
{
qa_db_ordered_delete('userfields', 'fieldid', $fieldid);
}
/**
* Return the ID of a new widget, to be displayed by the widget module named $title on templates within $tags (comma-separated list)
* @param $title
* @param $tags
* @return mixed
*/
function qa_db_widget_create($title, $tags)
{
$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();
}
/**
* Set the comma-separated list of templates for $widgetid to $tags
* @param $widgetid
* @param $tags
*/
function qa_db_widget_set_fields($widgetid, $tags)
{
qa_db_query_sub(
'UPDATE ^widgets SET tags=$ WHERE widgetid=#',
$tags, $widgetid
);
}
/**
* Move the widget $widgetit into position $position in the database's order, and show it in $place on the page
* @param $widgetid
* @param $place
* @param $newposition
*/
function qa_db_widget_move($widgetid, $place, $newposition)
{
qa_db_query_sub(
'UPDATE ^widgets SET place=$ WHERE widgetid=#',
$place, $widgetid
);
qa_db_ordered_move('widgets', 'widgetid', $widgetid, $newposition);
}
/**
* Delete the widget $widgetid in the database
* @param $widgetid
*/
function qa_db_widget_delete($widgetid)
{
qa_db_ordered_delete('widgets', 'widgetid', $widgetid);
}

121
qa-include/db/blobs.php Normal file
View File

@@ -0,0 +1,121 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Database-level access to blobs table for large chunks of data (e.g. images)
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Create a new blob in the database with $content and $format, other fields as provided
* @param $content
* @param $format
* @param $sourcefilename
* @param $userid
* @param $cookieid
* @param $ip
* @return mixed|null|string
*/
function qa_db_blob_create($content, $format, $sourcefilename = null, $userid = null, $cookieid = null, $ip = null)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
for ($attempt = 0; $attempt < 10; $attempt++) {
$blobid = qa_db_random_bigint();
if (qa_db_blob_exists($blobid))
continue;
qa_db_query_sub(
'INSERT INTO ^blobs (blobid, format, content, filename, userid, cookieid, createip, created) VALUES (#, $, $, $, $, #, UNHEX($), NOW())',
$blobid, $format, $content, $sourcefilename, $userid, $cookieid, bin2hex(@inet_pton($ip))
);
return $blobid;
}
return null;
}
/**
* Get the information about blob $blobid from the database
* @param $blobid
* @return array|mixed|null
*/
function qa_db_blob_read($blobid)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
return qa_db_read_one_assoc(qa_db_query_sub(
'SELECT content, format, filename FROM ^blobs WHERE blobid=#',
$blobid
), true);
}
/**
* Change the content of blob $blobid in the database to $content (can also be null)
* @param $blobid
* @param $content
*/
function qa_db_blob_set_content($blobid, $content)
{
qa_db_query_sub(
'UPDATE ^blobs SET content=$ WHERE blobid=#',
$content, $blobid
);
}
/**
* Delete blob $blobid in the database
* @param $blobid
* @return mixed
*/
function qa_db_blob_delete($blobid)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
qa_db_query_sub(
'DELETE FROM ^blobs WHERE blobid=#',
$blobid
);
}
/**
* Check if blob $blobid exists in the database
* @param $blobid
* @return bool|mixed
*/
function qa_db_blob_exists($blobid)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
$blob = qa_db_read_one_value(qa_db_query_sub(
'SELECT COUNT(*) FROM ^blobs WHERE blobid=#',
$blobid
));
return $blob > 0;
}

76
qa-include/db/cache.php Normal file
View File

@@ -0,0 +1,76 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Database-level access to cache table
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
require_once QA_INCLUDE_DIR . 'db/maxima.php';
/**
* Create (or replace) the item ($type, $cacheid) in the database cache table with $content
* @param $type
* @param $cacheid
* @param $content
* @return mixed
*/
function qa_db_cache_set($type, $cacheid, $content)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
qa_db_query_sub(
'DELETE FROM ^cache WHERE lastread<NOW()-INTERVAL # SECOND',
QA_DB_MAX_CACHE_AGE
);
qa_db_query_sub(
'INSERT INTO ^cache (type, cacheid, content, created, lastread) VALUES ($, $, $, NOW(), NOW()) ' .
'ON DUPLICATE KEY UPDATE content = VALUES(content), created = VALUES(created), lastread = VALUES(lastread)',
$type, $cacheid, $content
);
}
/**
* Retrieve the item ($type, $cacheid) from the database cache table
* @param $type
* @param $cacheid
* @return mixed|null
*/
function qa_db_cache_get($type, $cacheid)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
$content = qa_db_read_one_value(qa_db_query_sub(
'SELECT content FROM ^cache WHERE type=$ AND cacheid=#',
$type, $cacheid
), true);
if (isset($content))
qa_db_query_sub(
'UPDATE ^cache SET lastread=NOW() WHERE type=$ AND cacheid=#',
$type, $cacheid
);
return $content;
}

81
qa-include/db/cookies.php Normal file
View File

@@ -0,0 +1,81 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Database access functions for user cookies
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Create a new random cookie for $ipaddress and insert into database, returning it
* @param $ipaddress
* @return null|string
*/
function qa_db_cookie_create($ipaddress)
{
for ($attempt = 0; $attempt < 10; $attempt++) {
$cookieid = qa_db_random_bigint();
if (qa_db_cookie_exists($cookieid))
continue;
qa_db_query_sub(
'INSERT INTO ^cookies (cookieid, created, createip) ' .
'VALUES (#, NOW(), UNHEX($))',
$cookieid, bin2hex(@inet_pton($ipaddress))
);
return $cookieid;
}
return null;
}
/**
* Note in database that a write operation has been done by user identified by $cookieid and from $ipaddress
* @param $cookieid
* @param $ipaddress
*/
function qa_db_cookie_written($cookieid, $ipaddress)
{
qa_db_query_sub(
'UPDATE ^cookies SET written=NOW(), writeip=UNHEX($) WHERE cookieid=#',
bin2hex(@inet_pton($ipaddress)), $cookieid
);
}
/**
* Return whether $cookieid exists in database
* @param $cookieid
* @return bool
*/
function qa_db_cookie_exists($cookieid)
{
$cookie = qa_db_read_one_value(qa_db_query_sub(
'SELECT COUNT(*) FROM ^cookies WHERE cookieid=#',
$cookieid
));
return $cookie > 0;
}

184
qa-include/db/events.php Normal file
View File

@@ -0,0 +1,184 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Database-level access to userevents and sharedevents tables
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Add an event to the event streams for entity $entitytype with $entityid. The event of type $updatetype relates to
* $lastpostid whose antecedent question is $questionid, and was caused by $lastuserid. Pass a unix $timestamp for the
* event time or leave as null to use now. This will add the event both to the entity's shared stream, and the
* individual user streams for any users following the entity not via its shared stream (See long comment in
* /qa-include/db/favorites.php). Also handles truncation.
* @param $entitytype
* @param $entityid
* @param $questionid
* @param $lastpostid
* @param $updatetype
* @param $lastuserid
* @param $timestamp
*/
function qa_db_event_create_for_entity($entitytype, $entityid, $questionid, $lastpostid, $updatetype, $lastuserid, $timestamp = null)
{
require_once QA_INCLUDE_DIR . 'db/maxima.php';
require_once QA_INCLUDE_DIR . 'app/updates.php';
$updatedsql = isset($timestamp) ? ('FROM_UNIXTIME(' . qa_db_argument_to_mysql($timestamp, false) . ')') : 'NOW()';
// Enter it into the appropriate shared event stream for that entity
qa_db_query_sub(
'INSERT INTO ^sharedevents (entitytype, entityid, questionid, lastpostid, updatetype, lastuserid, updated) ' .
'VALUES ($, #, #, #, $, $, ' . $updatedsql . ')',
$entitytype, $entityid, $questionid, $lastpostid, $updatetype, $lastuserid
);
// If this is for a question entity, check the shared event stream doesn't have too many entries for that question
$questiontruncated = false;
if ($entitytype == QA_ENTITY_QUESTION) {
$truncate = qa_db_read_one_value(qa_db_query_sub(
'SELECT updated FROM ^sharedevents WHERE entitytype=$ AND entityid=# AND questionid=# ORDER BY updated DESC LIMIT #,1',
$entitytype, $entityid, $questionid, QA_DB_MAX_EVENTS_PER_Q
), true);
if (isset($truncate)) {
qa_db_query_sub(
'DELETE FROM ^sharedevents WHERE entitytype=$ AND entityid=# AND questionid=# AND updated<=$',
$entitytype, $entityid, $questionid, $truncate
);
$questiontruncated = true;
}
}
// If we didn't truncate due to a specific question, truncate the shared event stream for its overall length
if (!$questiontruncated) {
$truncate = qa_db_read_one_value(qa_db_query_sub(
'SELECT updated FROM ^sharedevents WHERE entitytype=$ AND entityid=$ ORDER BY updated DESC LIMIT #,1',
$entitytype, $entityid, (int)qa_opt('max_store_user_updates')
), true);
if (isset($truncate))
qa_db_query_sub(
'DELETE FROM ^sharedevents WHERE entitytype=$ AND entityid=$ AND updated<=$',
$entitytype, $entityid, $truncate
);
}
// See if we can identify a user who has favorited this entity, but is not using its shared event stream
$randomuserid = qa_db_read_one_value(qa_db_query_sub(
'SELECT userid FROM ^userfavorites WHERE entitytype=$ AND entityid=# AND nouserevents=0 ORDER BY RAND() LIMIT 1',
$entitytype, $entityid
), true);
if (isset($randomuserid)) {
// If one was found, this means we have one or more individual event streams, so update them all
qa_db_query_sub(
'INSERT INTO ^userevents (userid, entitytype, entityid, questionid, lastpostid, updatetype, lastuserid, updated) ' .
'SELECT userid, $, #, #, #, $, $, ' . $updatedsql . ' FROM ^userfavorites WHERE entitytype=$ AND entityid=# AND nouserevents=0',
$entitytype, $entityid, $questionid, $lastpostid, $updatetype, $lastuserid, $entitytype, $entityid
);
// Now truncate the random individual event stream that was found earlier
// (in theory we should truncate them all, but truncation is just a 'housekeeping' activity, so it's not necessary)
qa_db_user_events_truncate($randomuserid, $questionid);
}
}
/**
* Add an event to the event stream for $userid which is not related to an entity they are following (but rather a
* notification which is relevant for them, e.g. if someone answers their question). The event of type $updatetype
* relates to $lastpostid whose antecedent question is $questionid, and was caused by $lastuserid. Pass a unix
* $timestamp for the event time or leave as null to use now. Also handles truncation of event streams.
* @param $userid
* @param $questionid
* @param $lastpostid
* @param $updatetype
* @param $lastuserid
* @param $timestamp
*/
function qa_db_event_create_not_entity($userid, $questionid, $lastpostid, $updatetype, $lastuserid, $timestamp = null)
{
require_once QA_INCLUDE_DIR . 'app/updates.php';
$updatedsql = isset($timestamp) ? ('FROM_UNIXTIME(' . qa_db_argument_to_mysql($timestamp, false) . ')') : 'NOW()';
qa_db_query_sub(
"INSERT INTO ^userevents (userid, entitytype, entityid, questionid, lastpostid, updatetype, lastuserid, updated) " .
"VALUES ($, $, 0, #, #, $, $, " . $updatedsql . ")",
$userid, QA_ENTITY_NONE, $questionid, $lastpostid, $updatetype, $lastuserid
);
qa_db_user_events_truncate($userid, $questionid);
}
/**
* Trim the number of events in the event stream for $userid. If an event was just added for a particular question,
* pass the question's id in $questionid (to help focus the truncation).
* @param $userid
* @param $questionid
*/
function qa_db_user_events_truncate($userid, $questionid = null)
{
// First try truncating based on there being too many events for this question
$questiontruncated = false;
if (isset($questionid)) {
$truncate = qa_db_read_one_value(qa_db_query_sub(
'SELECT updated FROM ^userevents WHERE userid=$ AND questionid=# ORDER BY updated DESC LIMIT #,1',
$userid, $questionid, QA_DB_MAX_EVENTS_PER_Q
), true);
if (isset($truncate)) {
qa_db_query_sub(
'DELETE FROM ^userevents WHERE userid=$ AND questionid=# AND updated<=$',
$userid, $questionid, $truncate
);
$questiontruncated = true;
}
}
// If that didn't happen, try truncating the stream in general based on its total length
if (!$questiontruncated) {
$truncate = qa_db_read_one_value(qa_db_query_sub(
'SELECT updated FROM ^userevents WHERE userid=$ ORDER BY updated DESC LIMIT #,1',
$userid, (int)qa_opt('max_store_user_updates')
), true);
if (isset($truncate))
qa_db_query_sub(
'DELETE FROM ^userevents WHERE userid=$ AND updated<=$',
$userid, $truncate
);
}
}

204
qa-include/db/favorites.php Normal file
View File

@@ -0,0 +1,204 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Database-level access to userfavorites table
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/*
Why do we have two types of event streams, shared (in qa_sharedevents) and user-specific (in qa_userevents)?
An event stream is defined as the set of events which are thrown off ("published") by a particular entity. For
example, it could include the activity on a particular question, or the activity by a particular user.
We have an arbitrary many-to-many mapping between event streams and users subscribed to those streams. Over time, a
particularly popular event stream could accumulate thousands of subscribers. Similarly, over time, a particularly
hyperactive user could end up subscribing to thousands of event streams.
If we stored a single copy of each event stream in the database, publishing an event would be very fast. However
retrieving a hyperactive user's update page would be extremely slow, because it would require retrieving all the
streams they are subscribed to, and finding the globally most recent (e.g.) 50 events across all those streams.
So instead we could store a list of news updates for each user. In this case, retrieving a user's update page would
be very fast. However, recording an event for a popular stream could become extremely slow, since it would have to
be copied for every user subscribed to the stream.
The standard solution to these "publish and subscribe" situations is a message-passing architecture. That's what
Twitter et al use. However that's not a viable option here, because it requires a process to be running in the
background to manage the queuing and transport of these messages from publishers (event streams) to subscribers
(users' lists of news updates). While we could have a cron-style process to manage this, I'm avoiding it for as long
as possible since it complicates setup. It also means there can be delays in updating users' news feeds.
So instead we adopt a hybrid approach. For each event created in an entity's stream, we record a single copy of that
event in the entity's stream in the qa_sharedevents table. In addition, by default, we place a copy of that event into
the list of news updates for each user subscribed to the stream, via the qa_userevents table.
However, if there are more than a certain number of subscribers to the stream, we skip this second step, i.e. we
only record one copy in the qa_sharedevents table. This limits the cost of publishing an event.
When we generate a user's list of recent updates, we of course retrieve the list of news updates for that user from
qa_userevents. However we also check to see whether that user is subscribed to any event streams for which updates
are no longer posted into the user's own list, because the stream has too many subscribers. For each of these
popular streams, we also retrieve the stream's events from qa_sharedevents. Since users are only likely to be
subscribed to a small number of popular streams, this limits the cost of retrieving the news updates.
(Having a shared event stream helps us another way. When a user subscribes to a stream, they can immediately have
recent events from that stream copied into their list of news updates.)
Note that this approach isn't aimed at reducing the total cost of keeping all users up-to-date on all events, but
rather ensuring that no individual operation (posting an event or retrieving a user's list of updates) takes too
long, since that would turn into a very slow response time for the corresponding HTTP request.
What should we use for the threshold T, so that if a stream has more than T subscribers, its events are only
recorded in the shared stream? One approach is as follows:
[this ignores stream length and truncation, which are constant factors]
T = our threshold
M = the maximum number of streams subscribed to by any user
P(x) = the probability that a particular stream has more than x subscribers
C1 = maximum cost of adding an event = maximum number of streams to which event must be added = O(T)
C2 = maximum cost of retrieving news updates = maximum number of shared streams to be combined = O(M * P(T))
[we assume that the chance a particular user is subscribed to a particular stream is independent of the user]
Now if we assume the power law, aka 80/20 rule, we can estimate that P(T) is proportional to 1/T, so that:
C2 = O(M / T)
To minimize the maximum of these two complexity maxima, we want to equate them, so that:
T = M/T => 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-include/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.
*/
/**
* 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.
* @param $userid
* @param $entitytype
* @param $entityid
*/
function qa_db_favorite_create($userid, $entitytype, $entityid)
{
$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
);
} else {
// Otherwise if we're going to record this in user-specific streams ...
require_once QA_INCLUDE_DIR . '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);
}
}
/**
* Delete the entity $entitytype with $entityid from the favorites list of $userid, removing any corresponding events
* from the user's stream.
* @param $userid
* @param $entitytype
* @param $entityid
*/
function qa_db_favorite_delete($userid, $entitytype, $entityid)
{
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
);
}

85
qa-include/db/hotness.php Normal file
View File

@@ -0,0 +1,85 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Functions for dealing with question hotness in the database
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Increment the views counter for the post (if different IP from last view).
* @param int $postid The ID of the post
* @return bool Whether views were actually incremented.
*/
function qa_db_increment_views($postid)
{
$query = 'UPDATE ^posts SET views=views+1, lastviewip=UNHEX($) WHERE postid=# AND (lastviewip IS NULL OR lastviewip!=UNHEX($))';
$ipHex = bin2hex(@inet_pton(qa_remote_ip_address()));
qa_db_query_sub($query, $ipHex, $postid, $ipHex);
return qa_db_affected_rows() > 0;
}
/**
* Recalculate the hotness in the database for one or more posts.
*
* @param int $firstpostid First post to recalculate (or only post if $lastpostid is null).
* @param int $lastpostid Last post in the range to recalculate.
* @param bool $viewincrement Deprecated - view counter is now incremented separately. Previously, would increment the post's
* views and include that in the hotness calculation.
* @return void
*/
function qa_db_hotness_update($firstpostid, $lastpostid = null, $viewincrement = false)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
if (!qa_should_update_counts()) {
return;
}
if (!isset($lastpostid))
$lastpostid = $firstpostid;
$query = "UPDATE ^posts AS x, (SELECT parents.postid, MAX(parents.created) AS qcreated, COALESCE(MAX(children.created), MAX(parents.created)) as acreated, COUNT(children.postid) AS acount, MAX(parents.netvotes) AS netvotes, MAX(parents.views) AS views FROM ^posts AS parents LEFT JOIN ^posts AS children ON parents.postid=children.parentid AND children.type='A' WHERE parents.postid BETWEEN # AND # 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)*#' .
') 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,
qa_opt('hot_weight_views') * 4000,
);
qa_db_query_raw(qa_db_apply_sub($query, $arguments));
}

1667
qa-include/db/install.php Normal file

File diff suppressed because it is too large Load Diff

93
qa-include/db/limits.php Normal file
View File

@@ -0,0 +1,93 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Database-level access to tables which monitor rate limits
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Get rate limit information for $action from the database for user $userid and/or IP address $ip, if they're set.
* Return as an array with the limit type in the key, and a labelled array of the period and count.
* @param $userid
* @param $ip
* @param $action
* @return array
*/
function qa_db_limits_get($userid, $ip, $action)
{
$selects = array();
$arguments = array();
if (isset($userid)) {
$selects[] = "(SELECT 'user' AS limitkey, period, count FROM ^userlimits WHERE userid=$ AND action=$)";
$arguments[] = $userid;
$arguments[] = $action;
}
if (isset($ip)) {
$selects[] = "(SELECT 'ip' AS limitkey, period, count FROM ^iplimits WHERE ip=UNHEX($) AND action=$)";
$arguments[] = bin2hex(@inet_pton($ip));
$arguments[] = $action;
}
if (count($selects)) {
$query = qa_db_apply_sub(implode(' UNION ALL ', $selects), $arguments);
return qa_db_read_all_assoc(qa_db_query_raw($query), 'limitkey');
} else
return array();
}
/**
* Increment the database rate limit count for user $userid and $action by $count within $period
* @param $userid
* @param $action
* @param $period
* @param $count
*/
function qa_db_limits_user_add($userid, $action, $period, $count)
{
qa_db_query_sub(
'INSERT INTO ^userlimits (userid, action, period, count) VALUES ($, $, #, #) ' .
'ON DUPLICATE KEY UPDATE count=IF(period=#, count+#, #), period=#',
$userid, $action, $period, $count, $period, $count, $count, $period
);
}
/**
* Increment the database rate limit count for IP address $ip and $action by $count within $period
* @param $ip
* @param $action
* @param $period
* @param $count
*/
function qa_db_limits_ip_add($ip, $action, $period, $count)
{
qa_db_query_sub(
'INSERT INTO ^iplimits (ip, action, period, count) VALUES (UNHEX($), $, #, #) ' .
'ON DUPLICATE KEY UPDATE count=IF(period=#, count+#, #), period=#',
bin2hex(@inet_pton($ip)), $action, $period, $count, $period, $count, $count, $period
);
}

71
qa-include/db/maxima.php Normal file
View File

@@ -0,0 +1,71 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Definitions that determine database column size and rows retrieved
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
$maximaDefaults = array(
// Maximum column sizes - any of these can be defined in qa-config.php to override the defaults below,
// but you need to do so before creating the database, otherwise it's too late.
'QA_DB_MAX_EMAIL_LENGTH' => 80,
'QA_DB_MAX_HANDLE_LENGTH' => 20,
'QA_DB_MAX_TITLE_LENGTH' => 800,
'QA_DB_MAX_CONTENT_LENGTH' => 12000,
'QA_DB_MAX_FORMAT_LENGTH' => 20,
'QA_DB_MAX_TAGS_LENGTH' => 800,
'QA_DB_MAX_NAME_LENGTH' => 40,
'QA_DB_MAX_WORD_LENGTH' => 80,
'QA_DB_MAX_CAT_PAGE_TITLE_LENGTH' => 80,
'QA_DB_MAX_CAT_PAGE_TAGS_LENGTH' => 200,
'QA_DB_MAX_CAT_CONTENT_LENGTH' => 800,
'QA_DB_MAX_WIDGET_TAGS_LENGTH' => 800,
'QA_DB_MAX_WIDGET_TITLE_LENGTH' => 80,
'QA_DB_MAX_OPTION_TITLE_LENGTH' => 40,
'QA_DB_MAX_PROFILE_TITLE_LENGTH' => 40,
'QA_DB_MAX_PROFILE_CONTENT_LENGTH' => 8000,
'QA_DB_MAX_CACHE_AGE' => 86400,
'QA_DB_MAX_BLOB_FILE_NAME_LENGTH' => 255,
'QA_DB_MAX_META_TITLE_LENGTH' => 40,
'QA_DB_MAX_META_CONTENT_LENGTH' => 8000,
'QA_DB_MAX_WORD_COUNT' => 255, // The field is currently a TINYINT so it shouldn't exceed this value
// How many records to retrieve for different circumstances. In many cases we retrieve more records than we
// end up needing to display once we know the value of an option. Wasteful, but allows one query per page.
'QA_DB_RETRIEVE_QS_AS' => 50,
'QA_DB_RETRIEVE_TAGS' => 200,
'QA_DB_RETRIEVE_USERS' => 200,
'QA_DB_RETRIEVE_ASK_TAG_QS' => 500,
'QA_DB_RETRIEVE_COMPLETE_TAGS' => 1000,
'QA_DB_RETRIEVE_MESSAGES' => 20,
// Keep event streams trimmed - not worth storing too many events per question because we only display the
// most recent event for each question, that has not been invalidated due to hiding/unselection/etc...
'QA_DB_MAX_EVENTS_PER_Q' => 5,
);
foreach ($maximaDefaults as $key => $def) {
if (!defined($key)) {
define($key, $def);
}
}

View File

@@ -0,0 +1,94 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Database-level access to messages table for private message history
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Record a message sent from $fromuserid to $touserid with $content in $format in the database. $public sets whether
* public (on wall) or private. Return the messageid of the row created.
* @param $fromuserid
* @param $touserid
* @param $content
* @param $format
* @param bool $public
* @return mixed
*/
function qa_db_message_create($fromuserid, $touserid, $content, $format, $public = false)
{
qa_db_query_sub(
'INSERT INTO ^messages (type, fromuserid, touserid, content, format, created) VALUES ($, #, #, $, $, NOW())',
$public ? 'PUBLIC' : 'PRIVATE', $fromuserid, $touserid, $content, $format
);
return qa_db_last_insert_id();
}
/**
* Hide the message with $messageid, in $box (inbox|outbox) from the user.
* @param $messageid
* @param $box
*/
function qa_db_message_user_hide($messageid, $box)
{
$field = ($box === 'inbox' ? 'tohidden' : 'fromhidden');
qa_db_query_sub(
"UPDATE ^messages SET $field=1 WHERE messageid=#",
$messageid
);
}
/**
* Delete the message with $messageid from the database.
* @param $messageid
* @param bool $public
*/
function qa_db_message_delete($messageid, $public = true)
{
// delete PM only if both sender and receiver have hidden it
$clause = $public ? '' : ' AND fromhidden=1 AND tohidden=1';
qa_db_query_sub(
'DELETE FROM ^messages WHERE messageid=#' . $clause,
$messageid
);
}
/**
* Recalculate the cached count of wall posts for user $userid in the database
* @param $userid
*/
function qa_db_user_recount_posts($userid)
{
if (qa_should_update_counts()) {
qa_db_query_sub(
"UPDATE ^users AS x, (SELECT COUNT(*) AS wallposts FROM ^messages WHERE touserid=# AND type='PUBLIC') AS a SET x.wallposts=a.wallposts WHERE x.userid=#",
$userid, $userid
);
}
}

241
qa-include/db/metas.php Normal file
View File

@@ -0,0 +1,241 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Database-level access to metas tables
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Set the metadata for user $userid with $key to $value. Keys beginning qa_ are reserved for the Q2A core.
* @param $userid
* @param $key
* @param $value
*/
function qa_db_usermeta_set($userid, $key, $value)
{
qa_db_meta_set('usermetas', 'userid', $userid, $key, $value);
}
/**
* Clear the metadata for user $userid with $key ($key can also be an array of keys)
* @param $userid
* @param $key
*/
function qa_db_usermeta_clear($userid, $key)
{
qa_db_meta_clear('usermetas', 'userid', $userid, $key);
}
/**
* Return the metadata value for user $userid with $key ($key can also be an array of keys in which case this
* returns an array of metadata key => value).
* @param $userid
* @param $key
* @return array|mixed|null
*/
function qa_db_usermeta_get($userid, $key)
{
return qa_db_meta_get('usermetas', 'userid', $userid, $key);
}
/**
* Set the metadata for post $postid with $key to $value. Keys beginning qa_ are reserved for the Q2A core.
* @param $postid
* @param $key
* @param $value
*/
function qa_db_postmeta_set($postid, $key, $value)
{
qa_db_meta_set('postmetas', 'postid', $postid, $key, $value);
}
/**
* Clear the metadata for post $postid with $key ($key can also be an array of keys)
* @param $postid
* @param $key
*/
function qa_db_postmeta_clear($postid, $key)
{
qa_db_meta_clear('postmetas', 'postid', $postid, $key);
}
/**
* Return the metadata value for post $postid with $key ($key can also be an array of keys in which case this
* returns an array of metadata key => value).
* @param $postid
* @param $key
* @return array|mixed|null
*/
function qa_db_postmeta_get($postid, $key)
{
return qa_db_meta_get('postmetas', 'postid', $postid, $key);
}
/**
* Set the metadata for category $categoryid with $key to $value. Keys beginning qa_ are reserved for the Q2A core.
* @param $categoryid
* @param $key
* @param $value
*/
function qa_db_categorymeta_set($categoryid, $key, $value)
{
qa_db_meta_set('categorymetas', 'categoryid', $categoryid, $key, $value);
}
/**
* Clear the metadata for category $categoryid with $key ($key can also be an array of keys)
* @param $categoryid
* @param $key
*/
function qa_db_categorymeta_clear($categoryid, $key)
{
qa_db_meta_clear('categorymetas', 'categoryid', $categoryid, $key);
}
/**
* Return the metadata value for category $categoryid with $key ($key can also be an array of keys in which
* case this returns an array of metadata key => value).
* @param $categoryid
* @param $key
* @return array|mixed|null
*/
function qa_db_categorymeta_get($categoryid, $key)
{
return qa_db_meta_get('categorymetas', 'categoryid', $categoryid, $key);
}
/**
* Set the metadata for tag $tag with $key to $value. Keys beginning qa_ are reserved for the Q2A core.
* @param $tag
* @param $key
* @param $value
*/
function qa_db_tagmeta_set($tag, $key, $value)
{
qa_db_meta_set('tagmetas', 'tag', $tag, $key, $value);
}
/**
* Clear the metadata for tag $tag with $key ($key can also be an array of keys)
* @param $tag
* @param $key
*/
function qa_db_tagmeta_clear($tag, $key)
{
qa_db_meta_clear('tagmetas', 'tag', $tag, $key);
}
/**
* Return the metadata value for tag $tag with $key ($key can also be an array of keys in which case this
* returns an array of metadata key => value).
* @param $tag
* @param $key
* @return array|mixed|null
*/
function qa_db_tagmeta_get($tag, $key)
{
return qa_db_meta_get('tagmetas', 'tag', $tag, $key);
}
/**
* Internal general function to set metadata
* @param $metatable
* @param $idcolumn
* @param $idvalue
* @param $title
* @param $content
*/
function qa_db_meta_set($metatable, $idcolumn, $idvalue, $title, $content)
{
qa_db_query_sub(
'INSERT INTO ^' . $metatable . ' (' . $idcolumn . ', title, content) VALUES ($, $, $) ' .
'ON DUPLICATE KEY UPDATE content = VALUES(content)',
$idvalue, $title, $content
);
}
/**
* Internal general function to clear metadata
* @param $metatable
* @param $idcolumn
* @param $idvalue
* @param $title
*/
function qa_db_meta_clear($metatable, $idcolumn, $idvalue, $title)
{
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
);
}
}
/**
* Internal general function to return metadata
* @param $metatable
* @param $idcolumn
* @param $idvalue
* @param $title
* @return array|mixed|null
*/
function qa_db_meta_get($metatable, $idcolumn, $idvalue, $title)
{
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);
}
}

72
qa-include/db/notices.php Normal file
View File

@@ -0,0 +1,72 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Database-level access to usernotices table
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Create a notice for $userid with $content in $format and optional $tags (not displayed) and return its noticeid
* @param $userid
* @param $content
* @param string $format
* @param $tags
* @return mixed
*/
function qa_db_usernotice_create($userid, $content, $format = '', $tags = null)
{
qa_db_query_sub(
'INSERT INTO ^usernotices (userid, content, format, tags, created) VALUES ($, $, $, $, NOW())',
$userid, $content, $format, $tags
);
return qa_db_last_insert_id();
}
/**
* Delete the notice $notice which belongs to $userid
* @param $userid
* @param $noticeid
*/
function qa_db_usernotice_delete($userid, $noticeid)
{
qa_db_query_sub(
'DELETE FROM ^usernotices WHERE userid=$ AND noticeid=#',
$userid, $noticeid
);
}
/**
* Return an array summarizing the notices to be displayed for $userid, including the tags (not displayed)
* @param $userid
* @return array
*/
function qa_db_usernotices_list($userid)
{
return qa_db_read_all_assoc(qa_db_query_sub(
'SELECT noticeid, tags, UNIX_TIMESTAMP(created) AS created FROM ^usernotices WHERE userid=$ ORDER BY created',
$userid
));
}

40
qa-include/db/options.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Database-level access to table containing admin options
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Set option $name to $value in the database
* @param $name
* @param $value
*/
function qa_db_set_option($name, $value)
{
qa_db_query_sub(
'INSERT INTO ^options (title, content) VALUES ($, $) ' .
'ON DUPLICATE KEY UPDATE content = VALUES(content)',
$name, $value
);
}

244
qa-include/db/points.php Normal file
View File

@@ -0,0 +1,244 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Database-level access to user points and statistics
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Returns an array of option names required to perform calculations in userpoints table
*/
function qa_db_points_option_names()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
return array(
'points_post_q', 'points_select_a', 'points_per_q_voted_up', 'points_per_q_voted_down', 'points_q_voted_max_gain', 'points_q_voted_max_loss',
'points_post_a', 'points_a_selected', 'points_per_a_voted_up', 'points_per_a_voted_down', 'points_a_voted_max_gain', 'points_a_voted_max_loss',
'points_per_c_voted_up', 'points_per_c_voted_down', 'points_c_voted_max_gain', 'points_c_voted_max_loss',
'points_vote_up_q', 'points_vote_down_q', 'points_vote_up_a', 'points_vote_down_a',
'points_multiple', 'points_base',
);
}
/**
* Returns an array containing all the calculation formulae for the userpoints table. Each element of this
* array is for one column - the key contains the column name, and the value is a further array of two elements.
* The element 'formula' contains the SQL fragment that calculates the columns value for one or more users,
* where the ~ symbol within the fragment is substituted for a constraint on which users we are interested in.
* The element 'multiple' specifies what to multiply each column by to create the final sum in the points column.
*/
function qa_db_points_calculations()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'app/options.php';
$options = qa_get_options(qa_db_points_option_names());
return array(
'qposts' => 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",
),
'cupvotes' => array(
'multiple' => 0,
'formula' => "COUNT(*) AS cupvotes FROM ^uservotes AS userid_src JOIN ^posts ON userid_src.postid=^posts.postid WHERE userid_src.userid~ AND LEFT(^posts.type, 1)='C' AND userid_src.vote>0",
),
'cdownvotes' => array(
'multiple' => 0,
'formula' => "COUNT(*) AS cdownvotes FROM ^uservotes AS userid_src JOIN ^posts ON userid_src.postid=^posts.postid WHERE userid_src.userid~ AND LEFT(^posts.type, 1)='C' 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~",
),
'cvoteds' => array(
'multiple' => $options['points_multiple'],
'formula' => "COALESCE(SUM(" .
"LEAST(" . ((int)$options['points_per_c_voted_up']) . "*upvotes," . ((int)$options['points_c_voted_max_gain']) . ")" .
"-" .
"LEAST(" . ((int)$options['points_per_c_voted_down']) . "*downvotes," . ((int)$options['points_c_voted_max_loss']) . ")" .
"), 0) AS cvoteds FROM ^posts AS userid_src WHERE LEFT(type, 1)='C' 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~",
),
);
}
/**
* 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.
* @param $userid
* @param $columns
* @return mixed
*/
function qa_db_points_update_ifuser($userid, $columns)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
if (qa_should_update_counts() && isset($userid)) {
require_once QA_INCLUDE_DIR . 'app/options.php';
require_once QA_INCLUDE_DIR . 'app/cookies.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';
// build like this so that a #, $ or ^ character in the $userid (if external integration) isn't substituted
qa_db_query_raw(str_replace('~', "='" . qa_db_escape_string($userid) . "'", qa_db_apply_sub($query, array($userid))));
if (qa_db_insert_on_duplicate_inserted()) {
qa_db_userpointscount_update();
}
}
}
/**
* Set the number of explicit bonus points for $userid to $bonus
* @param $userid
* @param $bonus
*/
function qa_db_points_set_bonus($userid, $bonus)
{
qa_db_query_sub(
"INSERT INTO ^userpoints (userid, bonus) VALUES ($, #) ON DUPLICATE KEY UPDATE bonus=#",
$userid, $bonus, $bonus
);
}
/**
* Update the cached count in the database of the number of rows in the userpoints table
*/
function qa_db_userpointscount_update()
{
if (qa_should_update_counts()) {
qa_db_query_sub(
"INSERT INTO ^options (title, content) " .
"SELECT 'cache_userpointscount', COUNT(*) FROM ^userpoints " .
"ON DUPLICATE KEY UPDATE content = VALUES(content)"
);
}
}

View File

@@ -0,0 +1,459 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Database functions for creating a question, answer or comment
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Create a new post in the database and return its ID (based on auto-incrementing)
* @param $type
* @param $parentid
* @param $userid
* @param $cookieid
* @param $ip
* @param $title
* @param $content
* @param $format
* @param $tagstring
* @param $notify
* @param $categoryid
* @param $name
* @return mixed
*/
function qa_db_post_create($type, $parentid, $userid, $cookieid, $ip, $title, $content, $format, $tagstring, $notify, $categoryid = null, $name = null)
{
qa_db_query_sub(
'INSERT INTO ^posts (categoryid, type, parentid, userid, cookieid, createip, title, content, format, tags, notify, name, created) ' .
'VALUES (#, $, #, $, #, UNHEX($), $, $, $, $, $, $, NOW())',
$categoryid, $type, $parentid, $userid, $cookieid, bin2hex(@inet_pton($ip)), $title, $content, $format, $tagstring, $notify, $name
);
return qa_db_last_insert_id();
}
/**
* Recalculate the full category path (i.e. columns catidpath1/2/3) for posts from $firstpostid to $lastpostid (if specified)
* @param $firstpostid
* @param $lastpostid
*/
function qa_db_posts_calc_category_path($firstpostid, $lastpostid = null)
{
if (!isset($lastpostid))
$lastpostid = $firstpostid;
qa_db_query_sub(
"UPDATE ^posts AS x, (SELECT ^posts.postid, " .
"COALESCE(parent2.parentid, parent1.parentid, parent0.parentid, parent0.categoryid) AS catidpath1, " .
"IF (parent2.parentid IS NOT NULL, parent1.parentid, IF (parent1.parentid IS NOT NULL, parent0.parentid, IF (parent0.parentid IS NOT NULL, parent0.categoryid, NULL))) AS catidpath2, " .
"IF (parent2.parentid IS NOT NULL, parent0.parentid, IF (parent1.parentid IS NOT NULL, parent0.categoryid, NULL)) AS catidpath3 " .
"FROM ^posts LEFT JOIN ^categories AS parent0 ON ^posts.categoryid=parent0.categoryid LEFT JOIN ^categories AS parent1 ON parent0.parentid=parent1.categoryid LEFT JOIN ^categories AS parent2 ON parent1.parentid=parent2.categoryid WHERE ^posts.postid BETWEEN # AND #) AS a SET x.catidpath1=a.catidpath1, x.catidpath2=a.catidpath2, x.catidpath3=a.catidpath3 WHERE x.postid=a.postid",
$firstpostid, $lastpostid
); // requires QA_CATEGORY_DEPTH=4
}
/**
* Get the full category path (including categoryid) for $postid
* @param $postid
* @return array|null
*/
function qa_db_post_get_category_path($postid)
{
return qa_db_read_one_assoc(qa_db_query_sub(
'SELECT categoryid, catidpath1, catidpath2, catidpath3 FROM ^posts WHERE postid=#',
$postid
)); // requires QA_CATEGORY_DEPTH=4
}
/**
* Update the cached number of answers for $questionid in the database, along with the highest netvotes of any of its answers
* @param $questionid
*/
function qa_db_post_acount_update($questionid)
{
if (qa_should_update_counts()) {
qa_db_query_sub(
"UPDATE ^posts AS x, (SELECT COUNT(*) AS acount, COALESCE(GREATEST(MAX(netvotes), 0), 0) AS amaxvote FROM ^posts WHERE parentid=# AND type='A') AS a SET x.acount=a.acount, x.amaxvote=a.amaxvote WHERE x.postid=#",
$questionid, $questionid
);
}
}
/**
* Recalculate the number of questions for each category in $path retrieved via qa_db_post_get_category_path()
* @param $path
*/
function qa_db_category_path_qcount_update($path)
{
qa_db_ifcategory_qcount_update($path['categoryid']); // requires QA_CATEGORY_DEPTH=4
qa_db_ifcategory_qcount_update($path['catidpath1']);
qa_db_ifcategory_qcount_update($path['catidpath2']);
qa_db_ifcategory_qcount_update($path['catidpath3']);
}
/**
* Update the cached number of questions for category $categoryid in the database, including its subcategories
* @param $categoryid
*/
function qa_db_ifcategory_qcount_update($categoryid)
{
if (qa_should_update_counts() && isset($categoryid)) {
// This seemed like the most sensible approach which avoids explicitly calculating the category's depth in the hierarchy
qa_db_query_sub(
"UPDATE ^categories SET qcount=GREATEST( (SELECT COUNT(*) FROM ^posts WHERE categoryid=# AND type='Q'), (SELECT COUNT(*) FROM ^posts WHERE catidpath1=# AND type='Q'), (SELECT COUNT(*) FROM ^posts WHERE catidpath2=# AND type='Q'), (SELECT COUNT(*) FROM ^posts WHERE catidpath3=# AND type='Q') ) WHERE categoryid=#",
$categoryid, $categoryid, $categoryid, $categoryid, $categoryid
); // requires QA_CATEGORY_DEPTH=4
}
}
/**
* Add rows into the database title index, where $postid contains the words $wordids - this does the same sort
* of thing as qa_db_posttags_add_post_wordids() in a different way, for no particularly good reason.
* @param $postid
* @param $wordids
*/
function qa_db_titlewords_add_post_wordids($postid, $wordids)
{
if (count($wordids)) {
$rowstoadd = array();
foreach ($wordids as $wordid)
$rowstoadd[] = array($postid, $wordid);
qa_db_query_sub(
'INSERT INTO ^titlewords (postid, wordid) VALUES #',
$rowstoadd
);
}
}
/**
* Add rows into the database content index, where $postid (of $type, with the antecedent $questionid)
* has words as per the keys of $wordidcounts, and the corresponding number of those words in the values.
* @param $postid
* @param $type
* @param $questionid
* @param $wordidcounts
*/
function qa_db_contentwords_add_post_wordidcounts($postid, $type, $questionid, $wordidcounts)
{
if (count($wordidcounts)) {
$rowstoadd = array();
foreach ($wordidcounts as $wordid => $count) {
if ($count > QA_DB_MAX_WORD_COUNT) {
$count = QA_DB_MAX_WORD_COUNT;
}
$rowstoadd[] = array($postid, $wordid, $count, $type, $questionid);
}
qa_db_query_sub(
'INSERT INTO ^contentwords (postid, wordid, count, type, questionid) VALUES #',
$rowstoadd
);
}
}
/**
* Add rows into the database index of individual tag words, where $postid contains the words $wordids
* @param $postid
* @param $wordids
*/
function qa_db_tagwords_add_post_wordids($postid, $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
);
}
}
/**
* Add rows into the database index of whole tags, where $postid contains the tags $wordids
* @param $postid
* @param $wordids
*/
function qa_db_posttags_add_post_wordids($postid, $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
);
}
}
/**
* Return an array mapping each word in $words to its corresponding wordid in the database
* @param $words
* @return array
*/
function qa_db_word_mapto_ids($words)
{
if (count($words)) {
return qa_db_read_all_assoc(qa_db_query_sub(
'SELECT wordid, word FROM ^words WHERE word IN ($)', $words
), 'word', 'wordid');
}
return array();
}
/**
* Return an array mapping each word in $words to its corresponding wordid in the database, adding any that are missing
* @param $words
* @return array
*/
function qa_db_word_mapto_ids_add($words)
{
$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 IGNORE 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;
}
/**
* Update the titlecount column in the database for the words in $wordids, based on how many posts they appear in the title of
* @param $wordids
*/
function qa_db_word_titlecount_update($wordids)
{
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
);
}
}
/**
* Update the contentcount column in the database for the words in $wordids, based on how many posts they appear in the content of
* @param $wordids
*/
function qa_db_word_contentcount_update($wordids)
{
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
);
}
}
/**
* 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
* @param $wordids
*/
function qa_db_word_tagwordcount_update($wordids)
{
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
);
}
}
/**
* Update the tagcount column in the database for the whole tags in $wordids, based on how many posts they appear as tags of
* @param $wordids
*/
function qa_db_word_tagcount_update($wordids)
{
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
);
}
}
/**
* Update the cached count in the database of the number of questions (excluding hidden/queued)
*/
function qa_db_qcount_update()
{
if (qa_should_update_counts()) {
qa_db_query_sub(
"INSERT INTO ^options (title, content) " .
"SELECT 'cache_qcount', COUNT(*) FROM ^posts " .
"WHERE type = 'Q' " .
"ON DUPLICATE KEY UPDATE content = VALUES(content)"
);
}
}
/**
* Update the cached count in the database of the number of answers (excluding hidden/queued)
*/
function qa_db_acount_update()
{
if (qa_should_update_counts()) {
qa_db_query_sub(
"INSERT INTO ^options (title, content) " .
"SELECT 'cache_acount', COUNT(*) FROM ^posts " .
"WHERE type = 'A' " .
"ON DUPLICATE KEY UPDATE content = VALUES(content)"
);
}
}
/**
* Update the cached count in the database of the number of comments (excluding hidden/queued)
*/
function qa_db_ccount_update()
{
if (qa_should_update_counts()) {
qa_db_query_sub(
"INSERT INTO ^options (title, content) " .
"SELECT 'cache_ccount', COUNT(*) FROM ^posts " .
"WHERE type = 'C' " .
"ON DUPLICATE KEY UPDATE content = VALUES(content)"
);
}
}
/**
* Update the cached count in the database of the number of different tags used
*/
function qa_db_tagcount_update()
{
if (qa_should_update_counts()) {
qa_db_query_sub(
"INSERT INTO ^options (title, content) " .
"SELECT 'cache_tagcount', COUNT(*) FROM ^words " .
"WHERE tagcount > 0 " .
"ON DUPLICATE KEY UPDATE content = VALUES(content)"
);
}
}
/**
* Update the cached count in the database of the number of unanswered questions (excluding hidden/queued)
*/
function qa_db_unaqcount_update()
{
if (qa_should_update_counts()) {
qa_db_query_sub(
"INSERT INTO ^options (title, content) " .
"SELECT 'cache_unaqcount', COUNT(*) FROM ^posts " .
"WHERE type = 'Q' AND acount = 0 AND closedbyid IS NULL " .
"ON DUPLICATE KEY UPDATE content = VALUES(content)"
);
}
}
/**
* Update the cached count in the database of the number of questions with no answer selected (excluding hidden/queued)
*/
function qa_db_unselqcount_update()
{
if (qa_should_update_counts()) {
qa_db_query_sub(
"INSERT INTO ^options (title, content) " .
"SELECT 'cache_unselqcount', COUNT(*) FROM ^posts " .
"WHERE type = 'Q' AND selchildid IS NULL AND closedbyid IS NULL " .
"ON DUPLICATE KEY UPDATE content = VALUES(content)"
);
}
}
/**
* Update the cached count in the database of the number of questions with no upvoted answers (excluding hidden/queued)
*/
function qa_db_unupaqcount_update()
{
if (qa_should_update_counts()) {
qa_db_query_sub(
"INSERT INTO ^options (title, content) " .
"SELECT 'cache_unupaqcount', COUNT(*) FROM ^posts " .
"WHERE type = 'Q' AND amaxvote = 0 AND closedbyid IS NULL " .
"ON DUPLICATE KEY UPDATE content = VALUES(content)"
);
}
}
/**
* Update the cached count in the database of the number of posts which are queued for moderation
*/
function qa_db_queuedcount_update()
{
if (qa_should_update_counts()) {
qa_db_query_sub(
"INSERT INTO ^options (title, content) " .
"SELECT 'cache_queuedcount', COUNT(*) FROM ^posts " .
"WHERE type IN ('Q_QUEUED', 'A_QUEUED', 'C_QUEUED') " .
"ON DUPLICATE KEY UPDATE content = VALUES(content)"
);
}
}

View File

@@ -0,0 +1,432 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Database functions for changing a question, answer or comment
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
require_once QA_INCLUDE_DIR . 'app/updates.php';
/**
* Update the selected answer in the database for $questionid to $selchildid, and optionally record that $lastuserid did it from $lastip
* @param $questionid
* @param $selchildid
* @param $lastuserid
* @param $lastip
*/
function qa_db_post_set_selchildid($questionid, $selchildid, $lastuserid = null, $lastip = null)
{
qa_db_query_sub(
"UPDATE ^posts AS x, (SELECT selchildid FROM ^posts WHERE postid=#) AS a " .
"SET x.updated=NULL, x.updatetype=NULL, x.lastuserid=NULL, x.lastip=NULL WHERE " . // if previous answer's last edit was to be selected, remove that
"x.postid=a.selchildid AND x.updatetype=$",
$questionid, QA_UPDATE_SELECTED
);
qa_db_query_sub(
'UPDATE ^posts SET selchildid=# WHERE postid=#',
$selchildid, $questionid
);
if (isset($selchildid) && isset($lastuserid) && isset($lastip)) {
qa_db_query_sub(
"UPDATE ^posts SET updated=NOW(), updatetype=$, lastuserid=$, lastip=UNHEX($) WHERE postid=#",
QA_UPDATE_SELECTED, $lastuserid, bin2hex(@inet_pton($lastip)), $selchildid
);
}
}
/**
* Set $questionid to be closed by post $closedbyid (null if not closed) in the database, and optionally record that
* $lastuserid did it from $lastip
* @param $questionid
* @param $closedbyid
* @param $lastuserid
* @param $lastip
*/
function qa_db_post_set_closed($questionid, $closedbyid, $lastuserid = null, $lastip = null)
{
if (isset($lastuserid) || isset($lastip)) {
qa_db_query_sub(
"UPDATE ^posts SET closedbyid=#, updated=NOW(), updatetype=$, lastuserid=$, lastip=UNHEX($) WHERE postid=#",
$closedbyid, QA_UPDATE_CLOSED, $lastuserid, bin2hex(@inet_pton($lastip)), $questionid
);
} else {
qa_db_query_sub(
'UPDATE ^posts SET closedbyid=# WHERE postid=#',
$closedbyid, $questionid
);
}
}
/**
* Set the type in the database of $postid to $type, and optionally record that $lastuserid did it from $lastip
* @param $postid
* @param $type
* @param $lastuserid
* @param $lastip
* @param string $updatetype
*/
function qa_db_post_set_type($postid, $type, $lastuserid = null, $lastip = null, $updatetype = QA_UPDATE_TYPE)
{
if (isset($lastuserid) || isset($lastip)) {
qa_db_query_sub(
'UPDATE ^posts SET type=$, updated=NOW(), updatetype=$, lastuserid=$, lastip=UNHEX($) WHERE postid=#',
$type, $updatetype, $lastuserid, bin2hex(@inet_pton($lastip)), $postid
);
} else {
qa_db_query_sub(
'UPDATE ^posts SET type=$ WHERE postid=#',
$type, $postid
);
}
}
/**
* Set the parent in the database of $postid to $parentid, and optionally record that $lastuserid did it from $lastip
* (if at least one is specified)
* @param $postid
* @param $parentid
* @param $lastuserid
* @param $lastip
*/
function qa_db_post_set_parent($postid, $parentid, $lastuserid = null, $lastip = null)
{
if (isset($lastuserid) || isset($lastip)) {
qa_db_query_sub(
"UPDATE ^posts SET parentid=#, updated=NOW(), updatetype=$, lastuserid=$, lastip=UNHEX($) WHERE postid=#",
$parentid, QA_UPDATE_PARENT, $lastuserid, bin2hex(@inet_pton($lastip)), $postid
);
} else {
qa_db_query_sub(
'UPDATE ^posts SET parentid=# WHERE postid=#',
$parentid, $postid
);
}
}
/**
* Set the text fields in the database of $postid to $title, $content, $tagstring, $notify and $name, and record that
* $lastuserid did it from $lastip (if at least one is specified) with $updatetype. For backwards compatibility if $name
* is null then the name will not be changed.
* @param $postid
* @param $title
* @param $content
* @param $format
* @param $tagstring
* @param $notify
* @param $lastuserid
* @param $lastip
* @param string $updatetype
* @param $name
*/
function qa_db_post_set_content($postid, $title, $content, $format, $tagstring, $notify, $lastuserid = null, $lastip = null, $updatetype = QA_UPDATE_CONTENT, $name = null)
{
if (isset($lastuserid) || isset($lastip)) {
// use COALESCE() for name since $name=null means it should not be modified (for backwards compatibility)
qa_db_query_sub(
'UPDATE ^posts SET title=$, content=$, format=$, tags=$, name=COALESCE($, name), notify=$, updated=NOW(), updatetype=$, lastuserid=$, lastip=UNHEX($) WHERE postid=#',
$title, $content, $format, $tagstring, $name, $notify, $updatetype, $lastuserid, bin2hex(@inet_pton($lastip)), $postid
);
} else {
qa_db_query_sub(
'UPDATE ^posts SET title=$, content=$, format=$, tags=$, name=COALESCE($, name), notify=$ WHERE postid=#',
$title, $content, $format, $tagstring, $name, $notify, $postid
);
}
}
/**
* Set the author in the database of $postid to $userid, and set the lastuserid to $userid as well if appropriate
* @param $postid
* @param $userid
*/
function qa_db_post_set_userid($postid, $userid)
{
qa_db_query_sub(
'UPDATE ^posts SET userid=$, lastuserid=IF(updated IS NULL, lastuserid, COALESCE(lastuserid,$)) WHERE postid=#',
$userid, $userid, $postid
);
}
/**
* Set the (exact) category in the database of $postid to $categoryid, and optionally record that $lastuserid did it from
* $lastip (if at least one is specified)
* @param $postid
* @param $categoryid
* @param $lastuserid
* @param $lastip
*/
function qa_db_post_set_category($postid, $categoryid, $lastuserid = null, $lastip = null)
{
if (isset($lastuserid) || isset($lastip)) {
qa_db_query_sub(
"UPDATE ^posts SET categoryid=#, updated=NOW(), updatetype=$, lastuserid=$, lastip=UNHEX($) WHERE postid=#",
$categoryid, QA_UPDATE_CATEGORY, $lastuserid, bin2hex(@inet_pton($lastip)), $postid
);
} else {
qa_db_query_sub(
'UPDATE ^posts SET categoryid=# WHERE postid=#',
$categoryid, $postid
);
}
}
/**
* Set the category path in the database of each of $postids to $path retrieved via qa_db_post_get_category_path()
* @param $postids
* @param $path
*/
function qa_db_posts_set_category_path($postids, $path)
{
if (count($postids)) {
// requires QA_CATEGORY_DEPTH=4
qa_db_query_sub(
'UPDATE ^posts SET categoryid=#, catidpath1=#, catidpath2=#, catidpath3=# WHERE postid IN (#)',
$path['categoryid'], $path['catidpath1'], $path['catidpath2'], $path['catidpath3'], $postids
);
}
}
/**
* Set the created date of $postid to $created, which is a unix timestamp. If created is null, set to now.
* @param $postid
* @param $created
*/
function qa_db_post_set_created($postid, $created)
{
if (isset($created)) {
qa_db_query_sub(
'UPDATE ^posts SET created=FROM_UNIXTIME(#) WHERE postid=#',
$created, $postid
);
} else {
qa_db_query_sub(
'UPDATE ^posts SET created=NOW() WHERE postid=#',
$postid
);
}
}
/**
* Set the last updated date of $postid to $updated, which is a unix timestamp. If updated is null, set to now.
* @param $postid
* @param $updated
*/
function qa_db_post_set_updated($postid, $updated)
{
if (isset($updated)) {
qa_db_query_sub(
'UPDATE ^posts SET updated=FROM_UNIXTIME(#) WHERE postid=#',
$updated, $postid
);
} else {
qa_db_query_sub(
'UPDATE ^posts SET updated=NOW() WHERE postid=#',
$postid
);
}
}
/**
* Deletes post $postid from the database (will also delete any votes on the post due to foreign key cascading)
* @param $postid
*/
function qa_db_post_delete($postid)
{
qa_db_query_sub(
'DELETE FROM ^posts WHERE postid=#',
$postid
);
}
/**
* Return an array of wordids that were indexed in the database for the title of $postid
* @param $postid
* @return array
*/
function qa_db_titlewords_get_post_wordids($postid)
{
return qa_db_read_all_values(qa_db_query_sub(
'SELECT wordid FROM ^titlewords WHERE postid=#',
$postid
));
}
/**
* Remove all entries in the database index of title words for $postid
* @param $postid
*/
function qa_db_titlewords_delete_post($postid)
{
qa_db_query_sub(
'DELETE FROM ^titlewords WHERE postid=#',
$postid
);
}
/**
* Return an array of wordids that were indexed in the database for the content of $postid
* @param $postid
* @return array
*/
function qa_db_contentwords_get_post_wordids($postid)
{
return qa_db_read_all_values(qa_db_query_sub(
'SELECT wordid FROM ^contentwords WHERE postid=#',
$postid
));
}
/**
* Remove all entries in the database index of content words for $postid
* @param $postid
*/
function qa_db_contentwords_delete_post($postid)
{
qa_db_query_sub(
'DELETE FROM ^contentwords WHERE postid=#',
$postid
);
}
/**
* Return an array of wordids that were indexed in the database for the individual words in tags of $postid
* @param $postid
* @return array
*/
function qa_db_tagwords_get_post_wordids($postid)
{
return qa_db_read_all_values(qa_db_query_sub(
'SELECT wordid FROM ^tagwords WHERE postid=#',
$postid
));
}
/**
* Remove all entries in the database index of individual words in tags of $postid
* @param $postid
*/
function qa_db_tagwords_delete_post($postid)
{
qa_db_query_sub(
'DELETE FROM ^tagwords WHERE postid=#',
$postid
);
}
/**
* Return an array of wordids that were indexed in the database for the whole tags of $postid
* @param $postid
* @return array
*/
function qa_db_posttags_get_post_wordids($postid)
{
return qa_db_read_all_values(qa_db_query_sub(
'SELECT wordid FROM ^posttags WHERE postid=#',
$postid
));
}
/**
* Remove all entries in the database index of whole tags for $postid
* @param $postid
*/
function qa_db_posttags_delete_post($postid)
{
qa_db_query_sub(
'DELETE FROM ^posttags WHERE postid=#',
$postid
);
}
/**
* Return the array $postids containing only those elements which are the postid of a question in the database
* @param $postids
* @return array
*/
function qa_db_posts_filter_q_postids($postids)
{
if (count($postids)) {
return qa_db_read_all_values(qa_db_query_sub(
"SELECT postid FROM ^posts WHERE type='Q' AND postid IN (#)",
$postids
));
}
return array();
}
/**
* Return an array of all the userids of authors of posts in the array $postids
* @param $postids
* @return array
*/
function qa_db_posts_get_userids($postids)
{
if (count($postids)) {
return qa_db_read_all_values(qa_db_query_sub(
"SELECT DISTINCT userid FROM ^posts WHERE postid IN (#) AND userid IS NOT NULL",
$postids
));
}
return array();
}
/**
* Update the cached count of the number of flagged posts in the database
*/
function qa_db_flaggedcount_update()
{
if (qa_should_update_counts()) {
qa_db_query_sub(
"INSERT INTO ^options (title, content) " .
"SELECT 'cache_flaggedcount', COUNT(*) FROM ^posts " .
"WHERE flagcount > 0 AND type IN ('Q', 'A', 'C') " .
"ON DUPLICATE KEY UPDATE content = VALUES(content)"
);
}
}

467
qa-include/db/recalc.php Normal file
View File

@@ -0,0 +1,467 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Database functions for recalculations (clean-up 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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
require_once QA_INCLUDE_DIR . 'db/post-create.php';
// For reindexing pages...
/**
* Return the number of custom pages currently in the database
*/
function qa_db_count_pages()
{
return qa_db_read_one_value(qa_db_query_sub(
'SELECT COUNT(*) FROM ^pages'
));
}
/**
* Return the information to reindex up to $count pages starting from $startpageid in the database
* @param $startpageid
* @param $count
* @return array
*/
function qa_db_pages_get_for_reindexing($startpageid, $count)
{
return qa_db_read_all_assoc(qa_db_query_sub(
'SELECT pageid, flags, tags, heading, content FROM ^pages WHERE pageid>=# ORDER BY pageid LIMIT #',
$startpageid, $count
), 'pageid');
}
// For reindexing posts...
/**
* Return the information required to reindex up to $count posts starting from $startpostid in the database
* @param $startpostid
* @param $count
* @return array
*/
function qa_db_posts_get_for_reindexing($startpostid, $count)
{
return qa_db_read_all_assoc(qa_db_query_sub(
"SELECT ^posts.postid, ^posts.title, ^posts.content, ^posts.format, ^posts.tags, ^posts.categoryid, ^posts.type, IF (^posts.type='Q', ^posts.postid, IF(parent.type='Q', parent.postid, grandparent.postid)) AS questionid, ^posts.parentid 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>=# 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');
}
/**
* Prepare posts $firstpostid to $lastpostid for reindexing in the database by removing their prior index entries
* @param $firstpostid
* @param $lastpostid
*/
function qa_db_prepare_for_reindexing($firstpostid, $lastpostid)
{
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
);
}
/**
* Remove any rows in the database word indexes with postid from $firstpostid upwards
* @param $firstpostid
*/
function qa_db_truncate_indexes($firstpostid)
{
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
);
}
/**
* Return the number of words currently referenced in the database
*/
function qa_db_count_words()
{
return qa_db_read_one_value(qa_db_query_sub(
'SELECT COUNT(*) FROM ^words'
));
}
/**
* Return the ids of up to $count words in the database starting from $startwordid
* @param $startwordid
* @param $count
* @return array
*/
function qa_db_words_prepare_for_recounting($startwordid, $count)
{
return qa_db_read_all_values(qa_db_query_sub(
'SELECT wordid FROM ^words WHERE wordid>=# ORDER BY wordid LIMIT #',
$startwordid, $count
));
}
/**
* Recalculate the cached counts for words $firstwordid to $lastwordid in the database
* @param $firstwordid
* @param $lastwordid
*/
function qa_db_words_recount($firstwordid, $lastwordid)
{
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...
/**
* Return the ids of up to $count posts in the database starting from $startpostid
* @param $startpostid
* @param $count
* @return array
*/
function qa_db_posts_get_for_recounting($startpostid, $count)
{
return qa_db_read_all_values(qa_db_query_sub(
'SELECT postid FROM ^posts WHERE postid>=# ORDER BY postid LIMIT #',
$startpostid, $count
));
}
/**
* Recalculate the cached vote counts for posts $firstpostid to $lastpostid in the database
* @param $firstpostid
* @param $lastpostid
*/
function qa_db_posts_votes_recount($firstpostid, $lastpostid)
{
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);
}
/**
* Recalculate the cached answer counts for posts $firstpostid to $lastpostid in the database, along with the highest netvotes of any of their answers
* @param $firstpostid
* @param $lastpostid
*/
function qa_db_posts_answers_recount($firstpostid, $lastpostid)
{
require_once QA_INCLUDE_DIR . '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...
/**
* 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
* @param $startuserid
* @param $count
* @return array
*/
function qa_db_users_get_for_recalc_points($startuserid, $count)
{
if (QA_FINAL_EXTERNAL_USERS) {
return qa_db_read_all_values(qa_db_query_sub(
'SELECT userid FROM ((SELECT DISTINCT userid FROM ^posts WHERE userid>=# ORDER BY userid LIMIT #) UNION (SELECT DISTINCT userid FROM ^uservotes WHERE userid>=# ORDER BY userid LIMIT #)) x ORDER BY userid LIMIT #',
$startuserid, $count, $startuserid, $count, $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
));
}
}
/**
* Recalculate all userpoints columns for users $firstuserid to $lastuserid in the database
* @param $firstuserid
* @param $lastuserid
*/
function qa_db_users_recalc_points($firstuserid, $lastuserid)
{
require_once QA_INCLUDE_DIR . '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
);
}
/**
* Remove any rows in the userpoints table where userid is greater than $lastuserid
* @param $lastuserid
*/
function qa_db_truncate_userpoints($lastuserid)
{
qa_db_query_sub(
'DELETE FROM ^userpoints WHERE userid>#',
$lastuserid
);
}
// For refilling event streams...
/**
* Return the ids of up to $count questions in the database starting from $startpostid
* @param $startpostid
* @param $count
* @return array
*/
function qa_db_qs_get_for_event_refilling($startpostid, $count)
{
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...
/**
* Return the ids of up to $count posts (including queued/hidden) in the database starting from $startpostid
* @param $startpostid
* @param $count
* @return array
*/
function qa_db_posts_get_for_recategorizing($startpostid, $count)
{
return qa_db_read_all_values(qa_db_query_sub(
"SELECT postid FROM ^posts WHERE postid>=# ORDER BY postid LIMIT #",
$startpostid, $count
));
}
/**
* 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
* @param $firstpostid
* @param $lastpostid
*/
function qa_db_posts_recalc_categoryid($firstpostid, $lastpostid)
{
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
);
}
/**
* Return the ids of up to $count categories in the database starting from $startcategoryid
* @param $startcategoryid
* @param $count
* @return array
*/
function qa_db_categories_get_for_recalcs($startcategoryid, $count)
{
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...
/**
* Return the ids of up to $limit posts of $type that can be deleted from the database (i.e. have no dependents)
* @param $type
* @param int $startpostid
* @param $limit
* @return array
*/
function qa_db_posts_get_for_deleting($type, $startpostid = 0, $limit = null)
{
$limitsql = isset($limit) ? (' ORDER BY ^posts.postid 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 LEFT JOIN ^posts AS dupe ON dupe.closedbyid=^posts.postid WHERE ^posts.type=$ AND ^posts.postid>=# AND child.postid IS NULL AND dupe.postid IS NULL" . $limitsql,
$type . '_HIDDEN', $startpostid
));
}
// For moving blobs between database and disk...
/**
* Return the number of blobs whose content is stored in the database, rather than on disk
*/
function qa_db_count_blobs_in_db()
{
return qa_db_read_one_value(qa_db_query_sub('SELECT COUNT(*) FROM ^blobs WHERE content IS NOT NULL'));
}
/**
* Return the id, content and format of the first blob whose content is stored in the database starting from $startblobid
* @param $startblobid
* @return array|null
*/
function qa_db_get_next_blob_in_db($startblobid)
{
return qa_db_read_one_assoc(qa_db_query_sub(
'SELECT blobid, content, format FROM ^blobs WHERE blobid>=# AND content IS NOT NULL LIMIT 1',
$startblobid
), true);
}
/**
* Return the number of blobs whose content is stored on disk, rather than in the database
*/
function qa_db_count_blobs_on_disk()
{
return qa_db_read_one_value(qa_db_query_sub('SELECT COUNT(*) FROM ^blobs WHERE content IS NULL'));
}
/**
* Return the id and format of the first blob whose content is stored on disk starting from $startblobid
* @param $startblobid
* @return array|null
*/
function qa_db_get_next_blob_on_disk($startblobid)
{
return qa_db_read_one_assoc(qa_db_query_sub(
'SELECT blobid, format FROM ^blobs WHERE blobid>=# AND content IS NULL LIMIT 1',
$startblobid
), true);
}

2028
qa-include/db/selects.php Normal file

File diff suppressed because it is too large Load Diff

412
qa-include/db/users.php Normal file
View File

@@ -0,0 +1,412 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Database-level access to user management tables (if not using single sign-on)
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Return the expected value for the passcheck column given the $password and password $salt
* @param $password
* @param $salt
* @return mixed|string
*/
function qa_db_calc_passcheck($password, $salt)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
return sha1(substr($salt, 0, 8) . $password . substr($salt, 8));
}
/**
* Create a new user in the database with $email, $password, $handle, privilege $level, and $ip address
* @param $email
* @param $password
* @param $handle
* @param $level
* @param $ip
* @return mixed
*/
function qa_db_user_create($email, $password, $handle, $level, $ip)
{
require_once QA_INCLUDE_DIR . 'util/string.php';
$ipHex = bin2hex(@inet_pton($ip));
if (QA_PASSWORD_HASH) {
qa_db_query_sub(
'INSERT INTO ^users (created, createip, email, passhash, level, handle, loggedin, loginip) ' .
'VALUES (NOW(), UNHEX($), $, $, #, $, NOW(), UNHEX($))',
$ipHex, $email, isset($password) ? password_hash($password, PASSWORD_BCRYPT) : null, (int)$level, $handle, $ipHex
);
} else {
$salt = isset($password) ? qa_random_alphanum(16) : null;
qa_db_query_sub(
'INSERT INTO ^users (created, createip, email, passsalt, passcheck, level, handle, loggedin, loginip) ' .
'VALUES (NOW(), UNHEX($), $, $, UNHEX($), #, $, NOW(), UNHEX($))',
$ipHex, $email, $salt, isset($password) ? qa_db_calc_passcheck($password, $salt) : null, (int)$level, $handle, $ipHex
);
}
return qa_db_last_insert_id();
}
/**
* Delete user $userid from the database, along with everything they have ever done (to the extent that it's possible)
* @param $userid
*/
function qa_db_user_delete($userid)
{
qa_db_query_sub('UPDATE ^posts SET lastuserid=NULL WHERE lastuserid=$', $userid);
qa_db_query_sub('DELETE FROM ^userpoints WHERE userid=$', $userid);
qa_db_query_sub('DELETE FROM ^blobs WHERE blobid=(SELECT avatarblobid FROM ^users WHERE userid=$)', $userid);
qa_db_query_sub('DELETE FROM ^users WHERE userid=$', $userid);
// All the queries below should be superfluous due to foreign key constraints, but just in case the user switched to MyISAM.
// Note also that private messages to/from that user are kept since we don't have all the keys we need to delete efficiently.
qa_db_query_sub('UPDATE ^posts SET userid=NULL WHERE userid=$', $userid);
qa_db_query_sub('DELETE FROM ^userlogins WHERE userid=$', $userid);
qa_db_query_sub('DELETE FROM ^userprofile WHERE userid=$', $userid);
qa_db_query_sub('DELETE FROM ^userfavorites WHERE userid=$ OR entitytype=$ AND entityid=$', $userid, QA_ENTITY_USER, $userid);
qa_db_query_sub('DELETE FROM ^userevents WHERE userid=$', $userid);
qa_db_query_sub('DELETE FROM ^uservotes WHERE userid=$', $userid);
qa_db_query_sub('DELETE FROM ^userlimits WHERE userid=$', $userid);
}
/**
* Return the ids of all users in the database which match $email (should be one or none)
* @param $email
* @return array
*/
function qa_db_user_find_by_email($email)
{
return qa_db_read_all_values(qa_db_query_sub(
'SELECT userid FROM ^users WHERE email=$',
$email
));
}
/**
* Return the ids of all users in the database which match $handle (=username), should be one or none
* @param $handle
* @return array
*/
function qa_db_user_find_by_handle($handle)
{
return qa_db_read_all_values(qa_db_query_sub(
'SELECT userid FROM ^users WHERE handle=$',
$handle
));
}
/**
* Return an array mapping each userid in $userids that can be found to that user's handle
* @param $userids
* @return array
*/
function qa_db_user_get_userid_handles($userids)
{
if (count($userids)) {
return qa_db_read_all_assoc(qa_db_query_sub(
'SELECT userid, handle FROM ^users WHERE userid IN (#)',
$userids
), 'userid', 'handle');
}
return array();
}
/**
* Return an array mapping mapping each handle in $handle that can be found to that user's userid
* @param $handles
* @return array
*/
function qa_db_user_get_handle_userids($handles)
{
if (count($handles)) {
return qa_db_read_all_assoc(qa_db_query_sub(
'SELECT handle, userid FROM ^users WHERE handle IN ($)',
$handles
), 'handle', 'userid');
}
return array();
}
/**
* Set $field of $userid to $value in the database users table. If the $fields parameter is an array, the $value
* parameter is ignored and each element of the array is treated as a key-value pair of user fields and values.
* @param mixed $userid
* @param string|array $fields
* @param string|null $value
*/
function qa_db_user_set($userid, $fields, $value = null)
{
if (!is_array($fields)) {
$fields = array(
$fields => $value,
);
}
$sql = 'UPDATE ^users SET ';
foreach ($fields as $field => $fieldValue) {
$sql .= qa_db_escape_string($field) . ' = $, ';
}
$sql = substr($sql, 0, -2) . ' WHERE userid = $';
$params = array_values($fields);
$params[] = $userid;
qa_db_query_sub_params($sql, $params);
}
/**
* Set the password of $userid to $password, and reset their salt at the same time
* @param $userid
* @param $password
* @return mixed
*/
function qa_db_user_set_password($userid, $password)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'util/string.php';
if (QA_PASSWORD_HASH) {
qa_db_query_sub(
'UPDATE ^users SET passhash=$, passsalt=NULL, passcheck=NULL WHERE userid=$',
password_hash($password, PASSWORD_BCRYPT), $userid
);
} else {
$salt = qa_random_alphanum(16);
qa_db_query_sub(
'UPDATE ^users SET passsalt=$, passcheck=UNHEX($) WHERE userid=$',
$salt, qa_db_calc_passcheck($password, $salt), $userid
);
}
}
/**
* Switch on the $flag bit of the flags column for $userid if $set is true, or switch off otherwise
* @param $userid
* @param $flag
* @param $set
*/
function qa_db_user_set_flag($userid, $flag, $set)
{
qa_db_query_sub(
'UPDATE ^users SET flags=flags' . ($set ? '|' : '&~') . '# WHERE userid=$',
$flag, $userid
);
}
/**
* Return a random string to be used for a user's emailcode column
*/
function qa_db_user_rand_emailcode()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'util/string.php';
return qa_random_alphanum(8);
}
/**
* Return a random string to be used for a user's sessioncode column (for browser session cookies)
*/
function qa_db_user_rand_sessioncode()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
require_once QA_INCLUDE_DIR . 'util/string.php';
return qa_random_alphanum(8);
}
/**
* Set a row in the database user profile table to store $value for $field for $userid
* @param $userid
* @param $field
* @param $value
*/
function qa_db_user_profile_set($userid, $field, $value)
{
qa_db_query_sub(
'INSERT INTO ^userprofile (userid, title, content) VALUES ($, $, $) ' .
'ON DUPLICATE KEY UPDATE content = VALUES(content)',
$userid, $field, $value
);
}
/**
* Note in the database that $userid just logged in from $ip address
* @param $userid
* @param $ip
*/
function qa_db_user_logged_in($userid, $ip)
{
qa_db_query_sub(
'UPDATE ^users SET loggedin=NOW(), loginip=UNHEX($) WHERE userid=$',
bin2hex(@inet_pton($ip)), $userid
);
}
/**
* Note in the database that $userid just performed a write operation from $ip address
* @param $userid
* @param $ip
*/
function qa_db_user_written($userid, $ip)
{
qa_db_query_sub(
'UPDATE ^users SET written=NOW(), writeip=UNHEX($) WHERE userid=$',
bin2hex(@inet_pton($ip)), $userid
);
}
/**
* Add an external login in the database for $source and $identifier for user $userid
* @param $userid
* @param $source
* @param $identifier
*/
function qa_db_user_login_add($userid, $source, $identifier)
{
qa_db_query_sub(
'INSERT INTO ^userlogins (userid, source, identifier, identifiermd5) ' .
'VALUES ($, $, $, UNHEX($))',
$userid, $source, $identifier, md5($identifier)
);
}
/**
* Return some information about the user with external login $source and $identifier in the database, if a match is found
* @param $source
* @param $identifier
* @return array
*/
function qa_db_user_login_find($source, $identifier)
{
return qa_db_read_all_assoc(qa_db_query_sub(
'SELECT ^userlogins.userid, handle, email FROM ^userlogins LEFT JOIN ^users ON ^userlogins.userid=^users.userid ' .
'WHERE source=$ AND identifiermd5=UNHEX($) AND identifier=$',
$source, md5($identifier), $identifier
));
}
/**
* Lock all tables if $sync is true, otherwise unlock them. Used to synchronize creation of external login mappings.
* @param $sync
*/
function qa_db_user_login_sync($sync)
{
if ($sync) { // need to lock all tables since any could be used by a plugin's event module
$tables = qa_db_list_tables();
$locks = array();
foreach ($tables as $table)
$locks[] = $table . ' WRITE';
qa_db_query_sub('LOCK TABLES ' . implode(', ', $locks));
} else {
qa_db_query_sub('UNLOCK TABLES');
}
}
/**
* Reset the full set of context-specific (currently, per category) user levels for user $userid to $userlevels, where
* $userlevels is an array of arrays, the inner arrays containing items 'entitytype', 'entityid' and 'level'.
* @param $userid
* @param $userlevels
*/
function qa_db_user_levels_set($userid, $userlevels)
{
qa_db_query_sub(
'DELETE FROM ^userlevels WHERE userid=$',
$userid
);
foreach ($userlevels as $userlevel) {
qa_db_query_sub(
'INSERT INTO ^userlevels (userid, entitytype, entityid, level) VALUES ($, $, #, #) ' .
'ON DUPLICATE KEY UPDATE level = VALUES(level)',
$userid, $userlevel['entitytype'], $userlevel['entityid'], $userlevel['level']
);
}
}
/**
* Get the information required for sending a mailing to the next $count users with userids greater than $lastuserid
* @param $lastuserid
* @param $count
* @return array
*/
function qa_db_users_get_mailing_next($lastuserid, $count)
{
return qa_db_read_all_assoc(qa_db_query_sub(
'SELECT userid, email, handle, emailcode, flags, level FROM ^users WHERE userid># ORDER BY userid LIMIT #',
$lastuserid, $count
));
}
/**
* Update the cached count of the number of users who are awaiting approval after registration
*/
function qa_db_uapprovecount_update()
{
if (qa_should_update_counts() && !QA_FINAL_EXTERNAL_USERS) {
qa_db_query_sub(
"INSERT INTO ^options (title, content) " .
"SELECT 'cache_uapprovecount', COUNT(*) FROM ^users " .
"WHERE level < # AND NOT (flags & #) " .
"ON DUPLICATE KEY UPDATE content = VALUES(content)",
QA_USER_LEVEL_APPROVED, QA_USER_FLAGS_USER_BLOCKED
);
}
}

179
qa-include/db/votes.php Normal file
View File

@@ -0,0 +1,179 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Database-level access to votes tables
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
*/
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../../');
exit;
}
/**
* Set the vote for $userid on $postid to $vote in the database
* @param $postid
* @param $userid
* @param $vote
*/
function qa_db_uservote_set($postid, $userid, $vote)
{
$vote = max(min(($vote), 1), -1);
qa_db_query_sub(
'INSERT INTO ^uservotes (postid, userid, vote, flag, votecreated) VALUES (#, #, #, 0, NOW()) ON DUPLICATE KEY UPDATE vote=#, voteupdated=NOW()',
$postid, $userid, $vote, $vote
);
}
/**
* Get the vote for $userid on $postid from the database (or NULL if none)
* @param $postid
* @param $userid
* @return mixed|null
*/
function qa_db_uservote_get($postid, $userid)
{
return qa_db_read_one_value(qa_db_query_sub(
'SELECT vote FROM ^uservotes WHERE postid=# AND userid=#',
$postid, $userid
), true);
}
/**
* Set the flag for $userid on $postid to $flag (true or false) in the database
* @param $postid
* @param $userid
* @param $flag
*/
function qa_db_userflag_set($postid, $userid, $flag)
{
$flag = $flag ? 1 : 0;
qa_db_query_sub(
'INSERT INTO ^uservotes (postid, userid, vote, flag) VALUES (#, #, 0, #) ON DUPLICATE KEY UPDATE flag=#',
$postid, $userid, $flag, $flag
);
}
/**
* Clear all flags for $postid in the database
* @param $postid
*/
function qa_db_userflags_clear_all($postid)
{
qa_db_query_sub(
'UPDATE ^uservotes SET flag=0 WHERE postid=#',
$postid
);
}
/**
* Recalculate the cached count of upvotes, downvotes and netvotes for $postid in the database
* @param $postid
*/
function qa_db_post_recount_votes($postid)
{
if (qa_should_update_counts()) {
qa_db_query_sub(
'UPDATE ^posts AS x, (SELECT COALESCE(SUM(GREATEST(0,vote)),0) AS upvotes, -COALESCE(SUM(LEAST(0,vote)),0) AS downvotes FROM ^uservotes WHERE postid=#) AS a SET x.upvotes=a.upvotes, x.downvotes=a.downvotes, x.netvotes=a.upvotes-a.downvotes WHERE x.postid=#',
$postid, $postid
);
}
}
/**
* Recalculate the cached count of flags for $postid in the database
* @param $postid
*/
function qa_db_post_recount_flags($postid)
{
if (qa_should_update_counts()) {
qa_db_query_sub(
'UPDATE ^posts AS x, (SELECT COALESCE(SUM(IF(flag, 1, 0)),0) AS flagcount FROM ^uservotes WHERE postid=#) AS a SET x.flagcount=a.flagcount WHERE x.postid=#',
$postid, $postid
);
}
}
/**
* Returns all non-zero votes on post $postid from the database as an array of [userid] => [vote]
* @param $postid
* @return array
*/
function qa_db_uservote_post_get($postid)
{
return qa_db_read_all_assoc(qa_db_query_sub(
'SELECT userid, vote FROM ^uservotes WHERE postid=# AND vote!=0',
$postid
), 'userid', 'vote');
}
/**
* Returns all the postids from the database for posts that $userid has voted on or flagged
* @param $userid
* @return array
*/
function qa_db_uservoteflag_user_get($userid)
{
return qa_db_read_all_values(qa_db_query_sub(
'SELECT postid FROM ^uservotes WHERE userid=# AND (vote!=0 OR flag!=0)',
$userid
));
}
/**
* Return information about all the non-zero votes and/or flags on the posts in postids, including user handles for internal user management
* @param $postids
* @return array
*/
function qa_db_uservoteflag_posts_get($postids)
{
if (QA_FINAL_EXTERNAL_USERS) {
return qa_db_read_all_assoc(qa_db_query_sub(
'SELECT postid, userid, vote, flag, votecreated, voteupdated FROM ^uservotes WHERE postid IN (#) AND (vote!=0 OR flag!=0)',
$postids
));
} else {
return qa_db_read_all_assoc(qa_db_query_sub(
'SELECT postid, handle, vote, flag, votecreated, voteupdated FROM ^uservotes LEFT JOIN ^users ON ^uservotes.userid=^users.userid WHERE postid IN (#) AND (vote!=0 OR flag!=0)',
$postids
));
}
}
/**
* Remove all votes assigned to a post that had been cast by the owner of the post.
*
* @param int $postid The post ID from which the owner's votes will be removed.
*/
function qa_db_uservote_remove_own($postid)
{
qa_db_query_sub(
'DELETE uv FROM ^uservotes uv JOIN ^posts p ON uv.postid=p.postid AND uv.userid=p.userid WHERE uv.postid=#', $postid
);
}

View File

@@ -0,0 +1,289 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Language phrases for admin center
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
*/
return array(
'active_widgets_explanation' => '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' => 'After "^"',
'after_x_tab' => 'After "^" tab',
'approve_user_popup' => 'Approve user',
'approve_users_title' => 'Approve users',
'basic_editor' => 'Basic Editor',
'before_main_menu' => 'Before tabs at top',
'blobs_directory_error' => 'The directory ^ defined as QA_BLOBS_DIRECTORY is not writable by the web server.',
'blobs_move_complete' => 'Migration of uploaded images and documents completed.',
'blobs_move_moved' => 'Migration ^1 of ^2 uploaded images and documents',
'blobs_stop' => 'Stop migrating',
'blobs_to_db' => 'Blobs to database',
'blobs_to_db_note' => '- migrate all uploaded images and documents from disk files to the database',
'blobs_to_disk' => 'Blobs to disk',
'blobs_to_disk_note' => '- migrate all uploaded images and documents from the database to disk files',
'block_button' => 'block',
'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_user_popup' => 'Block user',
'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).',
'caching_cleanup' => 'Caching clean-up operations',
'caching_delete_all' => 'Delete entire cache',
'caching_delete_complete' => 'Cache successfully deleted',
'caching_delete_expired' => 'Delete expired cache',
'caching_delete_progress' => 'Deleted ^1 of ^2 cache files...',
'caching_dir_error' => 'The directory ^ defined as QA_CACHE_DIRECTORY is not writable by the web server.',
'caching_dir_missing' => 'Cache directory has not been defined.',
'caching_dir_public' => 'The directory ^ defined as QA_CACHE_DIRECTORY must be outside the public root.',
'caching_num_items' => 'Number of items in cache',
'caching_space_used' => 'Total size of cache',
'caching_title' => 'Caching',
'cancel_mailing_button' => 'Cancel Mailing',
'categories' => 'Categories',
'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',
'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' => 'Category name:',
'category_name_first' => 'Name of first category:',
'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' => 'Delete this category',
'delete_category_reassign' => 'Delete this category and reassign its questions to:',
'delete_field' => 'Delete this field',
'delete_hidden' => 'Delete hidden posts',
'delete_hidden_complete' => 'All hidden posts without dependents have been deleted',
'delete_hidden_note' => ' - all hidden questions, answer and comments without dependents',
'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',
'enabled' => 'Enabled',
'feed_link' => 'Feed',
'feed_link_example' => 'Example 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',
'form_security_expired' => 'Form security code expired - please try again',
'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...',
'hidden_title' => 'Hidden',
'hotness_factors' => 'Relative importance for question hotness:',
'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 ^)',
'memcached_error' => 'Memcached error: ^',
'moderate_title' => 'Moderate',
'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_memcached' => 'The Memcached PHP extension is not installed.',
'no_multibyte' => 'The installed version of PHP was compiled without multibyte string support. Searching will be less effective for non-Roman characters.',
'no_privileges' => 'Only administrators may access this page.',
'no_unapproved_found' => 'No users waiting for approval',
'not_logged_in' => 'Please ^1log in^2 as the administrator to access this page.',
'opposite_main_menu' => 'Far end of tabs at top',
'options' => 'options',
'options_reset' => 'Options reset',
'options_saved' => 'Options saved',
'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' => 'points',
'points_defaults_shown' => 'Defaults shown below but NOT YET APPLIED:',
'points_required' => 'Points required to receive title:',
'points_title' => 'Points',
'position' => 'Position:',
'posting_title' => 'Posting',
'profile_fields' => 'Extra fields on user pages or registration form:',
'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' => 'Recalculate categories',
'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_hotness_q_view_note' => 'May slightly improve page speed if disabled, but hotness values will become out of date if views are included in hotness settings',
'recalc_points' => 'Recalculate user points',
'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_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' => 'Recount posts',
'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...',
'refill_events' => 'Refill event streams',
'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...',
'registration_fields' => 'add registration fields',
'reindex_content' => 'Reindex content',
'reindex_content_note' => ' - for searching and related question suggestions',
'reindex_content_stop' => 'Stop reindexing',
'reindex_pages_reindexed' => 'Reindexed ^1 of ^2 pages...',
'reindex_posts_complete' => 'All posts were successfully reindexed.',
'reindex_posts_reindexed' => 'Reindexed ^1 of ^2 posts...',
'reindex_posts_wordcounted' => 'Recounted ^1 of ^2 words...',
'requires_php_version' => 'Disabled - requires PHP ^ or later',
'requires_q2a_version' => 'Disabled - requires Question2Answer ^ or later',
'reset_options_button' => 'Reset to Defaults',
'reset_options_confirm' => 'Are you sure you want to reset all options on this page to their defaults?',
'resume_mailing_button' => 'Resume Mailing',
'save_options_button' => 'Save Options',
'save_recalc_button' => 'Save and Recalculate',
'save_view_button' => 'Save and View',
'send_test_button' => 'Send Test to Me',
'show_defaults_button' => 'Show Defaults',
'show_on_register_form' => 'Show field on user registration form',
'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' => 'Tags',
'tags_and_categories' => 'Tags and Categories',
'tags_not_shown' => 'Some questions have tags which will not be displayed.',
'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:',
'total_qs_unans' => 'Total questions unanswered:',
'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' => 'latest',
'version_latest_unknown' => 'latest unknown',
'version_requires_php' => '^1 requires PHP ^2',
'version_requires_q2a' => '^1 requires Q2A ^2',
'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:',
);

View File

@@ -0,0 +1,85 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Language phrases for email notifications
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
*/
return array(
'a_commented_body' => "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\nConfirmation code: ^code\n\n Thank 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\n\nClick below to review all flagged posts:\n\n^a_url\n\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\n\nClick below to review all queued posts:\n\n^a_url\n\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',
'remoderate_body' => "An edited post by ^p_handle requires your reapproval:\n\n^open^p_context^close\n\nClick below to approve or hide the edited post:\n\n^url\n\n\nClick below to review all queued posts:\n\n^a_url\n\n\nThank you,\n\n^site_title",
'remoderate_subject' => '^site_title moderation',
'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",
'u_registered_body' => "A new user has registered as ^u_handle.\n\nClick below to view the user profile:\n\n^url\n\nThank you,\n\n^site_title",
'u_registered_subject' => '^site_title has a new registered user',
'u_to_approve_body' => "A new user has registered as ^u_handle.\n\nClick below to approve the user:\n\n^url\n\nClick below to review all users waiting for approval:\n\n^a_url\n\nThank you,\n\n^site_title",
'u_approved_body' => "You can see your new user profile here:\n\n^url\n\nThank you,\n\n^site_title",
'u_approved_subject' => 'Your ^site_title user has been approved',
'wall_post_body' => "^f_handle has posted on your user wall at ^site_title:\n\n^open^post^close\n\nYou may respond to the post here:\n\n^url\n\nThank you,\n\n^site_title",
'wall_post_subject' => 'Post on your ^site_title wall',
'welcome_body' => "Thank you for registering for ^site_title.\n\n^custom^confirmYour login details are as follows:\n\nUsername: ^handle\nEmail: ^email\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!',
);

View File

@@ -0,0 +1,232 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Language phrases commonly used throughout Q2A
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
*/
return array(
'_decimal_point' => '.',
'_thousands_separator' => ',',
'_thousands_suffix' => 'k',
'_millions_suffix' => 'm',
'1_answer' => '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' => 'answered',
'answered_qs_in_x' => 'Most answered questions in ^',
'answered_qs_title' => 'Most answered questions',
'asked' => 'asked',
'asked_related_q' => 'asked related question',
'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',
'email_error' => 'An error occurred trying to send the email.',
'field_required' => 'Please enter something in this field',
'file_upload_limit_exceeded' => 'The size of the file exceeds the server\'s limits',
'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',
'max_upload_size_x' => 'Maximum upload size is ^',
'me' => 'me',
'meta_order' => '^what^when^where^who', // you can reorder but DO NOT translate! e.g. <answered> <15 hours ago> <in Problems> <by me (500 points)>
'min_length_x' => 'Please provide more information - at least ^ characters',
'moved' => 'moved',
'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',
'newest_users' => 'Newest 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 commented questions in ^',
'recent_cs_title' => 'Recently commented questions',
'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',
'since_x' => 'since ^',
'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' => 'To see more, click for the ^1full list of questions^2.',
'suggest_qs_tags' => 'To see more, click for the ^1full list of questions^2 or ^3popular tags^4.',
'to_x' => 'to ^',
'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_be_approved' => 'Your account must be approved to view question pages. Please wait or ^1add more information^2.',
'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_approve' => 'Your account must be approved before you can vote',
'vote_disabled_down' => 'Voting down is only available to some users',
'vote_disabled_down_approve' => 'Your account must be approved before you can vote down',
'vote_disabled_hidden_post' => 'You cannot vote on hidden posts',
'vote_disabled_hidden_a' => 'You cannot vote on hidden answers', // @deprecated
'vote_disabled_hidden_q' => 'You cannot vote on hidden questions', // @deprecated
'vote_disabled_level' => 'Voting is only available to some users',
'vote_disabled_my_post' => 'You cannot vote on your own posts',
'vote_disabled_my_a' => 'You cannot vote on your own answers', // @deprecated
'vote_disabled_my_q' => 'You cannot vote on your own questions', // @deprecated
'vote_disabled_q_page_only' => 'Please view this question to vote',
'vote_disabled_queued' => 'You can only vote on approved posts',
'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',
'written' => '', // blank in English - placeholder for other languages
'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',
);

View File

@@ -0,0 +1,119 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Miscellaneous language phrases
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
*/
return array(
'block_ip_button' => 'Block IP address',
'browse_categories' => 'Browse categories',
'captcha_approve_fix' => 'This verification will stop appearing once your account is approved.',
'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',
'form_security_again' => 'Please click again to confirm',
'form_security_reload' => 'Please reload the page then try again',
'hide_all_ip_button' => 'Hide all posts from this IP',
'host_name' => 'Host name:',
'inbox' => 'Inbox',
'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' => 'You cannot send more private messages this hour',
'message_must_login' => 'Please ^1log in^2 or ^3register^4 to send private messages.',
'message_recent_history' => 'Recent correspondence with ^',
'message_sent' => 'Your private message below was sent',
'more_favorite_qs' => 'More favorite questions...',
'more_favorite_tags' => 'More favorite tags...',
'more_favorite_users' => 'More favorite users...',
'my_favorites_title' => 'My favorites',
'nav_all_my_updates' => 'All my updates',
'nav_my_content' => 'My content',
'nav_my_details' => 'My account',
'nav_my_favorites' => 'My favorites',
'nav_user_activity' => 'Recent activity',
'nav_user_as' => 'All answers',
'nav_user_pms' => 'Private messages',
'nav_user_qs' => 'All questions',
'nav_user_wall' => 'Wall',
'next_step' => 'Next step',
'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',
'outbox' => 'Sent items',
'pm_inbox_title' => 'Private messages received',
'pm_outbox_title' => 'Private messages sent',
'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',
'your_a_commented' => 'your answer commented',
'your_a_edited' => 'your answer edited',
'your_a_hidden' => 'your answer hidden',
'your_a_questioned' => 'asked on your answer',
'your_a_reshown' => 'your answer reshown',
'your_a_selected' => 'your answer selected',
'your_c_edited' => 'your comment edited',
'your_c_followed' => 'your comment followed',
'your_c_hidden' => 'your comment hidden',
'your_c_moved' => 'your comment moved',
'your_c_reshown' => 'your comment reshown',
'your_q_answered' => 'your question answered',
'your_q_closed' => 'your question closed',
'your_q_commented' => 'your question commented',
'your_q_edited' => 'your question edited',
'your_q_hidden' => 'your question hidden',
'your_q_recategorized' => 'your question recategorized',
'your_q_reopened' => 'your question reopened',
'your_q_reshown' => 'your question reshown',
'your_q_retagged' => 'your question retagged',
);

View File

@@ -0,0 +1,306 @@
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
Description: Language phrases for all options, as shown in admin center
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
*/
return array(
'allow_anonymous_naming' => 'Allow anonymous posters to specify their name:',
'allow_change_usernames' => 'Allow users with posts to change their username:',
'allow_close_questions' => 'Allow questions to be manually closed:',
'allow_close_own_questions' => 'Allow users to close their own questions:',
'allow_login_email_only' => 'Only log in by email address (not username):',
'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_user_walls' => 'Enable wall posts on user profiles:',
'allow_view_q_bots' => 'Allow search engines to view question pages',
'approve_user_required' => 'All new users must be approved:', // @deprecated
'avatar_allow_gravatar' => 'Allow ^1Gravatar^2 avatars:',
'avatar_allow_upload' => 'Allow users to upload avatars:',
'avatar_default_show' => 'Default avatar:',
'avatar_message_list_size' => 'Avatar size on message lists:',
'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_usernames' => 'Disallowed usernames - separate by spaces or commas:',
'block_bad_words' => 'Censored words - separate by spaces or commas:',
'block_ips_write' => 'Blocked IP addresses - separate by spaces or commas:',
'caching_catwidget_time' => 'Cache category widget for:',
'caching_enabled' => 'Enable caching:',
'caching_driver' => 'Caching driver',
'caching_filesystem' => 'Filesystem',
'caching_memcached' => 'Memcached',
'caching_q_start' => 'Start caching questions after:',
'caching_q_time' => 'Cache question pages for:',
'caching_qlist_time' => 'Cache question lists for:',
'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_unapproved' => 'Use captcha if user not yet approved:',
'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' => 'All new users must confirm their email:',
'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',
'default_terms' => 'I agree to the ^ Terms & Conditions and Privacy Policy',
'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' => 'Show the extra information on question pages',
'extra_field_display_label' => 'Show the extra information on question pages with label:',
'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 private and wall 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:',
'minify_html' => 'Minify HTML:',
'moderate_anon_post' => 'Use moderation for anonymous posts:',
'moderate_by_points' => 'Use moderation for users with few points:',
'moderate_edited_again' => 'Re-moderate posts after editing:',
'moderate_notify_admin' => 'Email me when a post needs moderation:',
'moderate_points_limit' => 'Use moderation for users with less than:',
'moderate_unapproved' => 'Use moderation if user not yet approved:',
'moderate_unconfirmed' => 'Use moderation if email not confirmed:',
'moderate_update_time' => 'Time to show on moderated posts:',
'moderate_users' => 'Enable moderation (approval) of users:',
'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_pms' => 'Private messages per 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_users' => 'Length of Users page:',
'page_size_wall' => 'Wall posts per page:',
'pages_prev_next' => 'Links to previous/next pages:',
'permit_admins' => 'Administrators',
'permit_all' => 'Anybody',
'permit_approve_users' => 'Approving registered users:',
'permit_approved' => 'Approved users only',
'permit_approved_points' => 'Approved users with enough points',
'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' => 'Registered users with enough points',
'permit_points_confirmed' => 'Registered & email confirmed & 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', // @deprecated
'place_side_below_sidebar' => 'Side panel - Below sidebar box',
'place_side_low' => 'Side panel - Low',
'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_c_voted_max_gain' => 'Limit from up votes on each comment:',
'points_c_voted_max_loss' => 'Limit from down votes on each comment:',
'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_c_voted_down' => 'Per down vote on your comment:',
'points_per_c_voted_up' => 'Per up vote on your comment:',
'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:',
'recalc_hotness_q_view' => 'Recalculate hotness on every question page view:',
'register_notify_admin' => 'Email me when a new user registers:',
'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_compact_numbers' => 'Show compact numbers (e.g. 1.3k):',
'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 <head> section of every page:',
'show_custom_register' => 'Custom message on registration 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 <meta> 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_post_update_meta' => 'Show edits/updates in post meta:',
'show_register_terms' => 'Terms & Conditions checkbox on registration form - 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_count_q_page' => 'Show view count on question pages:',
'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_text_direction' => 'Site text direction:',
'site_theme' => 'Site theme:',
'site_theme_mobile' => 'Theme for mobiles:',
'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' => 'SMTP secure connection:',
'smtp_secure_none' => 'None',
'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:',
'time_approved' => 'Time approved',
'time_written' => 'Time written',
'use_microdata' => 'Use schema.org microdata:',
'votes_separated' => 'Show separate up and down votes:',
'voting_on_as' => 'Allow voting on answers:',
'voting_on_cs' => 'Allow voting on comments:',
'voting_on_q_page_only' => 'Allow voting on question page only:',
'voting_on_qs' => 'Allow voting on questions:',
);

Some files were not shown because too many files have changed in this diff Show More