install.php 48.9 KB
Newer Older
Scott committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
<?php
/*
	Question2Answer by Gideon Greenspan and contributors
	http://www.question2answer.org/

	Description: Database-level functions for installation and upgrading


	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
*/

Scott committed
22
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
23
	header('Location: ../../');
Scott committed
24 25 26
	exit;
}

27
define('QA_DB_VERSION_CURRENT', 68);
Scott committed
28 29 30 31


/**
 * Return the column type for user ids after verifying it is one of the legal options
32
 * @return string
Scott committed
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
 */
function qa_db_user_column_type_verify()
{
	$coltype = strtoupper(qa_get_mysql_user_column_type());

	switch ($coltype) {
		case 'SMALLINT':
		case 'MEDIUMINT':
		case 'INT':
		case 'BIGINT':
		case 'SMALLINT UNSIGNED':
		case 'MEDIUMINT UNSIGNED':
		case 'INT UNSIGNED':
		case 'BIGINT UNSIGNED':
			// these are all OK
			break;

		default:
			if (!preg_match('/VARCHAR\([0-9]+\)/', $coltype))
				qa_fatal_error('Specified user column type is not one of allowed values - please read documentation');
			break;
Scott committed
54 55
	}

Scott committed
56 57
	return $coltype;
}
Scott committed
58 59


Scott committed
60 61 62
/**
 * Return an array of table definitions. For each element of the array, the key is the table name (without prefix)
 * and the value is an array of column definitions, [column name] => [definition]. The column name is omitted for indexes.
63
 * @return array
Scott committed
64 65 66 67
 */
function qa_db_table_definitions()
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott committed
68

Scott committed
69 70
	require_once QA_INCLUDE_DIR . 'db/maxima.php';
	require_once QA_INCLUDE_DIR . 'app/users.php';
Scott committed
71 72 73 74 75 76 77 78 79 80 81 82

	/*
		Important note on character encoding in database and PHP connection to MySQL

		[this note is no longer relevant since we *do* explicitly set the connection character set since Q2A 1.5 - see qa-db.php
	*/

	/*
		Other notes on the definitions below

		* In MySQL versions prior to 5.0.3, VARCHAR(x) columns will be silently converted to TEXT where x>255

83
		* See box at top of /qa-include/Q2A/Recalc/RecalcMain.php for a list of redundant (non-normal) information in the database
Scott committed
84 85 86 87 88 89 90 91 92 93 94 95 96 97

		* Starting in version 1.2, we explicitly name keys and foreign key constraints, instead of allowing MySQL
		  to name these by default. Our chosen names match the default names that MySQL would have assigned, and
		  indeed *did* assign for people who installed an earlier version of Q2A. By naming them explicitly, we're
		  on more solid ground for possible future changes to indexes and foreign keys in the schema.

		* There are other foreign key constraints that it would be valid to add, but that would not serve much
		  purpose in terms of preventing inconsistent data being retrieved, and would just slow down some queries.

		* We name some columns here in a not entirely intuitive way. The reason is to match the names of columns in
		  other tables which are of a similar nature. This will save time and space when combining several SELECT
		  queries together via a UNION in qa_db_multi_select() - see comments in qa-db.php for more information.
	*/

Scott committed
98 99 100 101 102 103
	$useridcoltype = qa_db_user_column_type_verify();

	$tables = array(
		'users' => array(
			'userid' => $useridcoltype . ' NOT NULL AUTO_INCREMENT',
			'created' => 'DATETIME NOT NULL',
104
			'createip' => 'VARBINARY(16) NOT NULL', // INET6_ATON of IP address when created
Scott committed
105 106 107 108 109 110 111 112 113 114
			'email' => 'VARCHAR(' . QA_DB_MAX_EMAIL_LENGTH . ') NOT NULL',
			'handle' => 'VARCHAR(' . QA_DB_MAX_HANDLE_LENGTH . ') NOT NULL', // username
			'avatarblobid' => 'BIGINT UNSIGNED', // blobid of stored avatar
			'avatarwidth' => 'SMALLINT UNSIGNED', // pixel width of stored avatar
			'avatarheight' => 'SMALLINT UNSIGNED', // pixel height of stored avatar
			'passsalt' => 'BINARY(16)', // salt used to calculate passcheck - null if no password set for direct login
			'passcheck' => 'BINARY(20)', // checksum from password and passsalt - null if no password set for direct login
			'passhash' => 'VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL', // password_hash
			'level' => 'TINYINT UNSIGNED NOT NULL', // basic, editor, admin, etc...
			'loggedin' => 'DATETIME NOT NULL', // time of last login
115
			'loginip' => 'VARBINARY(16) NOT NULL', // INET6_ATON of IP address of last login
Scott committed
116
			'written' => 'DATETIME', // time of last write action done by user
117
			'writeip' => 'VARBINARY(16)', // INET6_ATON of IP address of last write action done by user
Scott committed
118 119 120
			'emailcode' => 'CHAR(8) CHARACTER SET ascii NOT NULL DEFAULT \'\'', // for email confirmation or password reset
			'sessioncode' => 'CHAR(8) CHARACTER SET ascii NOT NULL DEFAULT \'\'', // for comparing against session cookie in browser
			'sessionsource' => 'VARCHAR (16) CHARACTER SET ascii DEFAULT \'\'', // e.g. facebook, openid, etc...
121
			'flags' => 'SMALLINT UNSIGNED NOT NULL DEFAULT 0', // see constants at top of /qa-include/app/users.php
Scott committed
122 123 124 125 126
			'wallposts' => 'MEDIUMINT NOT NULL DEFAULT 0', // cached count of wall posts
			'PRIMARY KEY (userid)',
			'KEY email (email)',
			'KEY handle (handle)',
			'KEY level (level)',
Scott committed
127
			'KEY created (created, level, flags)',
Scott committed
128 129 130 131 132 133 134 135 136 137 138 139 140
		),

		'userlogins' => array(
			'userid' => $useridcoltype . ' NOT NULL',
			'source' => 'VARCHAR (16) CHARACTER SET ascii NOT NULL', // e.g. facebook, openid, etc...
			'identifier' => 'VARBINARY (1024) NOT NULL', // depends on source, e.g. Facebook uid or OpenID url
			'identifiermd5' => 'BINARY (16) NOT NULL', // used to reduce size of index on identifier
			'KEY source (source, identifiermd5)',
			'KEY userid (userid)',
		),

		'userlevels' => array(
			'userid' => $useridcoltype . ' NOT NULL', // the user who has this level
141
			'entitytype' => 'CHAR(1) CHARACTER SET ascii NOT NULL', // see /qa-include/app/updates.php
Scott committed
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
			'entityid' => 'INT UNSIGNED NOT NULL', // relevant postid / userid / tag wordid / categoryid
			'level' => 'TINYINT UNSIGNED', // if not NULL, special permission level for that user and that entity
			'UNIQUE userid (userid, entitytype, entityid)',
			'KEY entitytype (entitytype, entityid)',
		),

		'userprofile' => array(
			'userid' => $useridcoltype . ' NOT NULL',
			'title' => 'VARCHAR(' . QA_DB_MAX_PROFILE_TITLE_LENGTH . ') NOT NULL', // profile field name
			'content' => 'VARCHAR(' . QA_DB_MAX_PROFILE_CONTENT_LENGTH . ') NOT NULL', // profile field value
			'UNIQUE userid (userid,title)',
		),

		'userfields' => array(
			'fieldid' => 'SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT',
			'title' => 'VARCHAR(' . QA_DB_MAX_PROFILE_TITLE_LENGTH . ') NOT NULL', // to match title column in userprofile table
			'content' => 'VARCHAR(' . QA_DB_MAX_PROFILE_TITLE_LENGTH . ')', // label for display on user profile pages - NULL means use default
			'position' => 'SMALLINT UNSIGNED NOT NULL',
160
			'flags' => 'TINYINT UNSIGNED NOT NULL', // QA_FIELD_FLAGS_* at top of /qa-include/app/users.php
Scott committed
161 162 163 164 165 166 167
			'permit' => 'TINYINT UNSIGNED', // minimum user level required to view (uses QA_PERMIT_* constants), null means no restriction
			'PRIMARY KEY (fieldid)',
		),

		'messages' => array(
			'messageid' => 'INT UNSIGNED NOT NULL AUTO_INCREMENT',
			'type' => "ENUM('PUBLIC', 'PRIVATE') NOT NULL DEFAULT 'PRIVATE'",
168 169
			'fromuserid' => $useridcoltype,
			'touserid' => $useridcoltype,
Scott committed
170 171 172 173 174 175 176 177 178 179 180 181 182 183
			'fromhidden' => 'TINYINT(1) UNSIGNED NOT NULL DEFAULT 0',
			'tohidden' => 'TINYINT(1) UNSIGNED NOT NULL DEFAULT 0',
			'content' => 'VARCHAR(' . QA_DB_MAX_CONTENT_LENGTH . ') NOT NULL',
			'format' => 'VARCHAR(' . QA_DB_MAX_FORMAT_LENGTH . ') CHARACTER SET ascii NOT NULL',
			'created' => 'DATETIME NOT NULL',
			'PRIMARY KEY (messageid)',
			'KEY type (type, fromuserid, touserid, created)',
			'KEY touserid (touserid, type, created)',
			'KEY fromhidden (fromhidden)',
			'KEY tohidden (tohidden)',
		),

		'userfavorites' => array(
			'userid' => $useridcoltype . ' NOT NULL', // the user who favorited the entity
184
			'entitytype' => 'CHAR(1) CHARACTER SET ascii NOT NULL', // see /qa-include/app/updates.php
Scott committed
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
			'entityid' => 'INT UNSIGNED NOT NULL', // favorited postid / userid / tag wordid / categoryid
			'nouserevents' => 'TINYINT UNSIGNED NOT NULL', // do we skip writing events to the user stream?
			'PRIMARY KEY (userid, entitytype, entityid)',
			'KEY userid (userid, nouserevents)',
			'KEY entitytype (entitytype, entityid, nouserevents)',
		),

		'usernotices' => array(
			'noticeid' => 'INT UNSIGNED NOT NULL AUTO_INCREMENT',
			'userid' => $useridcoltype . ' NOT NULL', // the user to whom the notice is directed
			'content' => 'VARCHAR(' . QA_DB_MAX_CONTENT_LENGTH . ') NOT NULL',
			'format' => 'VARCHAR(' . QA_DB_MAX_FORMAT_LENGTH . ') CHARACTER SET ascii NOT NULL',
			'tags' => 'VARCHAR(' . QA_DB_MAX_CAT_PAGE_TAGS_LENGTH . ')', // any additional information for a plugin to access
			'created' => 'DATETIME NOT NULL',
			'PRIMARY KEY (noticeid)',
			'KEY userid (userid, created)',
		),

		'userevents' => array(
			'userid' => $useridcoltype . ' NOT NULL', // the user to be informed about this event in their updates
205
			'entitytype' => 'CHAR(1) CHARACTER SET ascii NOT NULL', // see /qa-include/app/updates.php
Scott committed
206 207 208
			'entityid' => 'INT UNSIGNED NOT NULL', // favorited source of event - see userfavorites table - 0 means not from a favorite
			'questionid' => 'INT UNSIGNED NOT NULL', // the affected question
			'lastpostid' => 'INT UNSIGNED NOT NULL', // what part of question was affected
209
			'updatetype' => 'CHAR(1) CHARACTER SET ascii', // what was done to this part - see /qa-include/app/updates.php
Scott committed
210 211 212 213 214 215 216
			'lastuserid' => $useridcoltype, // which user (if any) did this action
			'updated' => 'DATETIME NOT NULL', // when the event happened
			'KEY userid (userid, updated)', // for truncation
			'KEY questionid (questionid, userid)', // to limit number of events per question per stream
		),

		'sharedevents' => array(
217
			'entitytype' => 'CHAR(1) CHARACTER SET ascii NOT NULL', // see /qa-include/app/updates.php
Scott committed
218 219 220 221 222 223 224 225 226 227 228 229 230
			'entityid' => 'INT UNSIGNED NOT NULL', // see userfavorites table
			'questionid' => 'INT UNSIGNED NOT NULL', // see userevents table
			'lastpostid' => 'INT UNSIGNED NOT NULL', // see userevents table
			'updatetype' => 'CHAR(1) CHARACTER SET ascii', // see userevents table
			'lastuserid' => $useridcoltype, // see userevents table
			'updated' => 'DATETIME NOT NULL', // see userevents table
			'KEY entitytype (entitytype, entityid, updated)', // for truncation
			'KEY questionid (questionid, entitytype, entityid)', // to limit number of events per question per stream
		),

		'cookies' => array(
			'cookieid' => 'BIGINT UNSIGNED NOT NULL',
			'created' => 'DATETIME NOT NULL',
231
			'createip' => 'VARBINARY(16) NOT NULL', // INET6_ATON of IP address when cookie created
Scott committed
232
			'written' => 'DATETIME', // time of last write action done by anon user with cookie
233
			'writeip' => 'VARBINARY(16)', // INET6_ATON of IP address of last write action done by anon user with cookie
Scott committed
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
			'PRIMARY KEY (cookieid)',
		),

		'categories' => array(
			'categoryid' => 'INT UNSIGNED NOT NULL AUTO_INCREMENT',
			'parentid' => 'INT UNSIGNED',
			'title' => 'VARCHAR(' . QA_DB_MAX_CAT_PAGE_TITLE_LENGTH . ') NOT NULL', // category name
			'tags' => 'VARCHAR(' . QA_DB_MAX_CAT_PAGE_TAGS_LENGTH . ') NOT NULL', // slug (url fragment) used to identify category
			'content' => 'VARCHAR(' . QA_DB_MAX_CAT_CONTENT_LENGTH . ') NOT NULL DEFAULT \'\'', // description of category
			'qcount' => 'INT UNSIGNED NOT NULL DEFAULT 0',
			'position' => 'SMALLINT UNSIGNED NOT NULL',
			// full slug path for category, with forward slash separators, in reverse order to make index from effective
			'backpath' => 'VARCHAR(' . (QA_CATEGORY_DEPTH * (QA_DB_MAX_CAT_PAGE_TAGS_LENGTH + 1)) . ') NOT NULL DEFAULT \'\'',
			'PRIMARY KEY (categoryid)',
			'UNIQUE parentid (parentid, tags)',
			'UNIQUE parentid_2 (parentid, position)',
			'KEY backpath (backpath(' . QA_DB_MAX_CAT_PAGE_TAGS_LENGTH . '))',
		),

		'pages' => array(
			'pageid' => 'SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT',
			'title' => 'VARCHAR(' . QA_DB_MAX_CAT_PAGE_TITLE_LENGTH . ') NOT NULL', // title for navigation
			'nav' => 'CHAR(1) CHARACTER SET ascii NOT NULL', // which navigation does it go in (M=main, F=footer, B=before main, O=opposite main, other=none)
			'position' => 'SMALLINT UNSIGNED NOT NULL', // global ordering, which allows links to be ordered within each nav area
			'flags' => 'TINYINT UNSIGNED NOT NULL', // local or external, open in new window?
			'permit' => 'TINYINT UNSIGNED', // is there a minimum user level required for it (uses QA_PERMIT_* constants), null means no restriction
			'tags' => 'VARCHAR(' . QA_DB_MAX_CAT_PAGE_TAGS_LENGTH . ') NOT NULL', // slug (url fragment) for page, or url for external pages
			'heading' => 'VARCHAR(' . QA_DB_MAX_TITLE_LENGTH . ')', // for display within <h1> tags
			'content' => 'MEDIUMTEXT', // remainder of page HTML
			'PRIMARY KEY (pageid)',
			'KEY tags (tags)',
q2apro committed
265
			'UNIQUE `position` (position)',
Scott committed
266 267 268 269 270 271 272 273 274 275 276 277
		),

		'widgets' => array(
			'widgetid' => 'SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT',
			// full region: FT=very top of page, FH=below nav area, FL=above footer, FB = very bottom of page
			// side region: ST=top of side, SH=below sidebar, SL=below categories, SB=very bottom of side
			// main region: MT=top of main, MH=below page title, ML=above links, MB=very bottom of main region
			'place' => 'CHAR(2) CHARACTER SET ascii NOT NULL',
			'position' => 'SMALLINT UNSIGNED NOT NULL', // global ordering, which allows widgets to be ordered within each place
			'tags' => 'VARCHAR(' . QA_DB_MAX_WIDGET_TAGS_LENGTH . ') CHARACTER SET ascii NOT NULL', // comma-separated list of templates to display on
			'title' => 'VARCHAR(' . QA_DB_MAX_WIDGET_TITLE_LENGTH . ') NOT NULL', // name of widget module that should be displayed
			'PRIMARY KEY (widgetid)',
q2apro committed
278
			'UNIQUE `position` (position)',
Scott committed
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
		),

		'posts' => array(
			'postid' => 'INT UNSIGNED NOT NULL AUTO_INCREMENT',
			'type' => "ENUM('Q', 'A', 'C', 'Q_HIDDEN', 'A_HIDDEN', 'C_HIDDEN', 'Q_QUEUED', 'A_QUEUED', 'C_QUEUED', 'NOTE') NOT NULL",
			'parentid' => 'INT UNSIGNED', // for follow on questions, all answers and comments
			'categoryid' => 'INT UNSIGNED', // this is the canonical final category id
			'catidpath1' => 'INT UNSIGNED', // the catidpath* columns are calculated from categoryid, for the full hierarchy of that category
			'catidpath2' => 'INT UNSIGNED', // note that QA_CATEGORY_DEPTH=4
			'catidpath3' => 'INT UNSIGNED',
			'acount' => 'SMALLINT UNSIGNED NOT NULL DEFAULT 0', // number of answers (for questions)
			'amaxvote' => 'SMALLINT UNSIGNED NOT NULL DEFAULT 0', // highest netvotes of child answers (for questions)
			'selchildid' => 'INT UNSIGNED', // selected answer (for questions)
			// if closed due to being a duplicate, this is the postid of that other question
			// if closed for another reason, that reason should be added as a comment on the question, and this field is the comment's id
			'closedbyid' => 'INT UNSIGNED', // not null means question is closed
			'userid' => $useridcoltype, // which user wrote it
			'cookieid' => 'BIGINT UNSIGNED', // which cookie wrote it, if an anonymous post
297
			'createip' => 'VARBINARY(16)', // INET6_ATON of IP address used to create the post
Scott committed
298
			'lastuserid' => $useridcoltype, // which user last modified it
299
			'lastip' => 'VARBINARY(16)', // INET6_ATON of IP address which last modified the post
Scott committed
300 301 302
			'upvotes' => 'SMALLINT UNSIGNED NOT NULL DEFAULT 0',
			'downvotes' => 'SMALLINT UNSIGNED NOT NULL DEFAULT 0',
			'netvotes' => 'SMALLINT NOT NULL DEFAULT 0',
303
			'lastviewip' => 'VARBINARY(16)', // INET6_ATON of IP address which last viewed the post
Scott committed
304 305 306 307 308 309
			'views' => 'INT UNSIGNED NOT NULL DEFAULT 0',
			'hotness' => 'FLOAT',
			'flagcount' => 'TINYINT UNSIGNED NOT NULL DEFAULT 0',
			'format' => 'VARCHAR(' . QA_DB_MAX_FORMAT_LENGTH . ') CHARACTER SET ascii NOT NULL DEFAULT \'\'', // format of content, e.g. 'html'
			'created' => 'DATETIME NOT NULL',
			'updated' => 'DATETIME', // time of last update
310
			'updatetype' => 'CHAR(1) CHARACTER SET ascii', // see /qa-include/app/updates.php
Scott committed
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351
			'title' => 'VARCHAR(' . QA_DB_MAX_TITLE_LENGTH . ')',
			'content' => 'VARCHAR(' . QA_DB_MAX_CONTENT_LENGTH . ')',
			'tags' => 'VARCHAR(' . QA_DB_MAX_TAGS_LENGTH . ')', // string of tags separated by commas
			'name' => 'VARCHAR(' . QA_DB_MAX_NAME_LENGTH . ')', // name of author if post anonymonus
			'notify' => 'VARCHAR(' . QA_DB_MAX_EMAIL_LENGTH . ')', // email address, or @ to get from user, or NULL for none
			'PRIMARY KEY (postid)',
			'KEY type (type, created)', // for getting recent questions, answers, comments
			'KEY type_2 (type, acount, created)', // for getting unanswered questions
			'KEY type_4 (type, netvotes, created)', // for getting posts with the most votes
			'KEY type_5 (type, views, created)', // for getting questions with the most views
			'KEY type_6 (type, hotness)', // for getting 'hot' questions
			'KEY type_7 (type, amaxvote, created)', // for getting questions with no upvoted answers
			'KEY parentid (parentid, type)', // for getting a question's answers, any post's comments and follow-on questions
			'KEY userid (userid, type, created)', // for recent questions, answers or comments by a user
			'KEY selchildid (selchildid, type, created)', // for counting how many of a user's answers have been selected, unselected qs
			'KEY closedbyid (closedbyid)', // for the foreign key constraint
			'KEY catidpath1 (catidpath1, type, created)', // for getting question, answers or comments in a specific level category
			'KEY catidpath2 (catidpath2, type, created)', // note that QA_CATEGORY_DEPTH=4
			'KEY catidpath3 (catidpath3, type, created)',
			'KEY categoryid (categoryid, type, created)', // this can also be used for searching the equivalent of catidpath4
			'KEY createip (createip, created)', // for getting posts created by a specific IP address
			'KEY updated (updated, type)', // for getting recent edits across all categories
			'KEY flagcount (flagcount, created, type)', // for getting posts with the most flags
			'KEY catidpath1_2 (catidpath1, updated, type)', // for getting recent edits in a specific level category
			'KEY catidpath2_2 (catidpath2, updated, type)', // note that QA_CATEGORY_DEPTH=4
			'KEY catidpath3_2 (catidpath3, updated, type)',
			'KEY categoryid_2 (categoryid, updated, type)',
			'KEY lastuserid (lastuserid, updated, type)', // for getting posts edited by a specific user
			'KEY lastip (lastip, updated, type)', // for getting posts edited by a specific IP address
			'CONSTRAINT ^posts_ibfk_2 FOREIGN KEY (parentid) REFERENCES ^posts(postid)', // ^posts_ibfk_1 is set later on userid
			'CONSTRAINT ^posts_ibfk_3 FOREIGN KEY (categoryid) REFERENCES ^categories(categoryid) ON DELETE SET NULL',
			'CONSTRAINT ^posts_ibfk_4 FOREIGN KEY (closedbyid) REFERENCES ^posts(postid)',
		),

		'blobs' => array(
			'blobid' => 'BIGINT UNSIGNED NOT NULL',
			'format' => 'VARCHAR(' . QA_DB_MAX_FORMAT_LENGTH . ') CHARACTER SET ascii NOT NULL', // format e.g. 'jpeg', 'gif', 'png'
			'content' => 'MEDIUMBLOB', // null means it's stored on disk in QA_BLOBS_DIRECTORY
			'filename' => 'VARCHAR(' . QA_DB_MAX_BLOB_FILE_NAME_LENGTH . ')', // name of source file (if appropriate)
			'userid' => $useridcoltype, // which user created it
			'cookieid' => 'BIGINT UNSIGNED', // which cookie created it
352
			'createip' => 'VARBINARY(16)', // INET6_ATON of IP address that created it
Scott committed
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
			'created' => 'DATETIME', // when it was created
			'PRIMARY KEY (blobid)',
		),

		'words' => array(
			'wordid' => 'INT UNSIGNED NOT NULL AUTO_INCREMENT',
			'word' => 'VARCHAR(' . QA_DB_MAX_WORD_LENGTH . ') NOT NULL',
			'titlecount' => 'INT UNSIGNED NOT NULL DEFAULT 0', // only counts one per post
			'contentcount' => 'INT UNSIGNED NOT NULL DEFAULT 0', // only counts one per post
			'tagwordcount' => 'INT UNSIGNED NOT NULL DEFAULT 0', // for words in tags - only counts one per post
			'tagcount' => 'INT UNSIGNED NOT NULL DEFAULT 0', // for tags as a whole - only counts one per post (though no duplicate tags anyway)
			'PRIMARY KEY (wordid)',
			'KEY word (word)',
			'KEY tagcount (tagcount)', // for sorting by most popular tags
		),

		'titlewords' => array(
			'postid' => 'INT UNSIGNED NOT NULL',
			'wordid' => 'INT UNSIGNED NOT NULL',
			'KEY postid (postid)',
			'KEY wordid (wordid)',
			'CONSTRAINT ^titlewords_ibfk_1 FOREIGN KEY (postid) REFERENCES ^posts(postid) ON DELETE CASCADE',
			'CONSTRAINT ^titlewords_ibfk_2 FOREIGN KEY (wordid) REFERENCES ^words(wordid)',
		),

		'contentwords' => array(
			'postid' => 'INT UNSIGNED NOT NULL',
			'wordid' => 'INT UNSIGNED NOT NULL',
			'count' => 'TINYINT UNSIGNED NOT NULL', // how many times word appears in the post - anything over 255 can be ignored
			'type' => "ENUM('Q', 'A', 'C', 'NOTE') NOT NULL", // the post's type (copied here for quick searching)
			'questionid' => 'INT UNSIGNED NOT NULL', // the id of the post's antecedent parent (here for quick searching)
			'KEY postid (postid)',
			'KEY wordid (wordid)',
			'CONSTRAINT ^contentwords_ibfk_1 FOREIGN KEY (postid) REFERENCES ^posts(postid) ON DELETE CASCADE',
			'CONSTRAINT ^contentwords_ibfk_2 FOREIGN KEY (wordid) REFERENCES ^words(wordid)',
		),

		'tagwords' => array(
			'postid' => 'INT UNSIGNED NOT NULL',
			'wordid' => 'INT UNSIGNED NOT NULL',
			'KEY postid (postid)',
			'KEY wordid (wordid)',
			'CONSTRAINT ^tagwords_ibfk_1 FOREIGN KEY (postid) REFERENCES ^posts(postid) ON DELETE CASCADE',
			'CONSTRAINT ^tagwords_ibfk_2 FOREIGN KEY (wordid) REFERENCES ^words(wordid)',
		),

		'posttags' => array(
			'postid' => 'INT UNSIGNED NOT NULL',
			'wordid' => 'INT UNSIGNED NOT NULL',
			'postcreated' => 'DATETIME NOT NULL', // created time of post (copied here for tag page's list of recent questions)
			'KEY postid (postid)',
Scott committed
404
			'KEY wordid (wordid, postcreated)',
Scott committed
405 406 407 408 409 410 411 412 413
			'CONSTRAINT ^posttags_ibfk_1 FOREIGN KEY (postid) REFERENCES ^posts(postid) ON DELETE CASCADE',
			'CONSTRAINT ^posttags_ibfk_2 FOREIGN KEY (wordid) REFERENCES ^words(wordid)',
		),

		'uservotes' => array(
			'postid' => 'INT UNSIGNED NOT NULL',
			'userid' => $useridcoltype . ' NOT NULL',
			'vote' => 'TINYINT NOT NULL', // -1, 0 or 1
			'flag' => 'TINYINT NOT NULL', // 0 or 1
Scott committed
414 415
			'votecreated' => 'DATETIME', // time of first vote
			'voteupdated' => 'DATETIME', // time of last vote change
Scott committed
416 417
			'UNIQUE userid (userid, postid)',
			'KEY postid (postid)',
Scott committed
418
			'KEY voted (votecreated, voteupdated)',
Scott committed
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
			'CONSTRAINT ^uservotes_ibfk_1 FOREIGN KEY (postid) REFERENCES ^posts(postid) ON DELETE CASCADE',
		),

		// many userpoints columns could be unsigned but MySQL appears to mess up points calculations that go negative as a result

		'userpoints' => array(
			'userid' => $useridcoltype . ' NOT NULL',
			'points' => 'INT NOT NULL DEFAULT 0', // user's points as displayed, after final multiple
			'qposts' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of questions by user (excluding hidden/queued)
			'aposts' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of answers by user (excluding hidden/queued)
			'cposts' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of comments by user (excluding hidden/queued)
			'aselects' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of questions by user where they've selected an answer
			'aselecteds' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of answers by user that have been selected as the best
			'qupvotes' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of questions the user has voted up
			'qdownvotes' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of questions the user has voted down
			'aupvotes' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of answers the user has voted up
			'adownvotes' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of answers the user has voted down
436 437
			'cupvotes' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of comments the user has voted up
			'cdownvotes' => 'MEDIUMINT NOT NULL DEFAULT 0', // number of comments the user has voted down
Scott committed
438 439
			'qvoteds' => 'INT NOT NULL DEFAULT 0', // points from votes on this user's questions (applying per-question limits), before final multiple
			'avoteds' => 'INT NOT NULL DEFAULT 0', // points from votes on this user's answers (applying per-answer limits), before final multiple
440
			'cvoteds' => 'INT NOT NULL DEFAULT 0', // points from votes on this user's comments (applying per-comment limits), before final multiple
Scott committed
441 442 443 444 445 446 447 448 449
			'upvoteds' => 'INT NOT NULL DEFAULT 0', // number of up votes received on this user's questions or answers
			'downvoteds' => 'INT NOT NULL DEFAULT 0', // number of down votes received on this user's questions or answers
			'bonus' => 'INT NOT NULL DEFAULT 0', // bonus assigned by administrator to a user
			'PRIMARY KEY (userid)',
			'KEY points (points)',
		),

		'userlimits' => array(
			'userid' => $useridcoltype . ' NOT NULL',
450
			'action' => 'CHAR(1) CHARACTER SET ascii NOT NULL', // see constants at top of /qa-include/app/limits.php
Scott committed
451 452 453 454 455 456 457 458
			'period' => 'INT UNSIGNED NOT NULL', // integer representing hour of last action
			'count' => 'SMALLINT UNSIGNED NOT NULL', // how many of this action has been performed within that hour
			'UNIQUE userid (userid, action)',
		),

		// most columns in iplimits have the same meaning as those in userlimits

		'iplimits' => array(
459
			'ip' => 'VARBINARY(16) NOT NULL', // INET6_ATON of IP address
Scott committed
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
			'action' => 'CHAR(1) CHARACTER SET ascii NOT NULL',
			'period' => 'INT UNSIGNED NOT NULL',
			'count' => 'SMALLINT UNSIGNED NOT NULL',
			'UNIQUE ip (ip, action)',
		),

		'options' => array(
			'title' => 'VARCHAR(' . QA_DB_MAX_OPTION_TITLE_LENGTH . ') NOT NULL', // name of option
			'content' => 'VARCHAR(' . QA_DB_MAX_CONTENT_LENGTH . ') NOT NULL', // value of option
			'PRIMARY KEY (title)',
		),

		'cache' => array(
			'type' => 'CHAR(8) CHARACTER SET ascii NOT NULL', // e.g. 'avXXX' for avatar sized to XXX pixels square
			'cacheid' => 'BIGINT UNSIGNED DEFAULT 0', // optional further identifier, e.g. blobid on which cache entry is based
			'content' => 'MEDIUMBLOB NOT NULL',
			'created' => 'DATETIME NOT NULL',
			'lastread' => 'DATETIME NOT NULL',
Scott committed
478
			'PRIMARY KEY (type, cacheid)',
Scott committed
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533
			'KEY (lastread)',
		),

		'usermetas' => array(
			'userid' => $useridcoltype . ' NOT NULL',
			'title' => 'VARCHAR(' . QA_DB_MAX_META_TITLE_LENGTH . ') NOT NULL',
			'content' => 'VARCHAR(' . QA_DB_MAX_META_CONTENT_LENGTH . ') NOT NULL',
			'PRIMARY KEY (userid, title)',
		),

		'postmetas' => array(
			'postid' => 'INT UNSIGNED NOT NULL',
			'title' => 'VARCHAR(' . QA_DB_MAX_META_TITLE_LENGTH . ') NOT NULL',
			'content' => 'VARCHAR(' . QA_DB_MAX_META_CONTENT_LENGTH . ') NOT NULL',
			'PRIMARY KEY (postid, title)',
			'CONSTRAINT ^postmetas_ibfk_1 FOREIGN KEY (postid) REFERENCES ^posts(postid) ON DELETE CASCADE',
		),

		'categorymetas' => array(
			'categoryid' => 'INT UNSIGNED NOT NULL',
			'title' => 'VARCHAR(' . QA_DB_MAX_META_TITLE_LENGTH . ') NOT NULL',
			'content' => 'VARCHAR(' . QA_DB_MAX_META_CONTENT_LENGTH . ') NOT NULL',
			'PRIMARY KEY (categoryid, title)',
			'CONSTRAINT ^categorymetas_ibfk_1 FOREIGN KEY (categoryid) REFERENCES ^categories(categoryid) ON DELETE CASCADE',
		),

		'tagmetas' => array(
			'tag' => 'VARCHAR(' . QA_DB_MAX_WORD_LENGTH . ') NOT NULL',
			'title' => 'VARCHAR(' . QA_DB_MAX_META_TITLE_LENGTH . ') NOT NULL',
			'content' => 'VARCHAR(' . QA_DB_MAX_META_CONTENT_LENGTH . ') NOT NULL',
			'PRIMARY KEY (tag, title)',
		),

	);

	if (QA_FINAL_EXTERNAL_USERS) {
		unset($tables['users']);
		unset($tables['userlogins']);
		unset($tables['userprofile']);
		unset($tables['userfields']);
		unset($tables['messages']);

	} else {
		$userforeignkey = 'FOREIGN KEY (userid) REFERENCES ^users(userid)';

		$tables['userlogins'][] = 'CONSTRAINT ^userlogins_ibfk_1 ' . $userforeignkey . ' ON DELETE CASCADE';
		$tables['userprofile'][] = 'CONSTRAINT ^userprofile_ibfk_1 ' . $userforeignkey . ' ON DELETE CASCADE';
		$tables['posts'][] = 'CONSTRAINT ^posts_ibfk_1 ' . $userforeignkey . ' ON DELETE SET NULL';
		$tables['uservotes'][] = 'CONSTRAINT ^uservotes_ibfk_2 ' . $userforeignkey . ' ON DELETE CASCADE';
		$tables['userlimits'][] = 'CONSTRAINT ^userlimits_ibfk_1 ' . $userforeignkey . ' ON DELETE CASCADE';
		$tables['userfavorites'][] = 'CONSTRAINT ^userfavorites_ibfk_1 ' . $userforeignkey . ' ON DELETE CASCADE';
		$tables['usernotices'][] = 'CONSTRAINT ^usernotices_ibfk_1 ' . $userforeignkey . ' ON DELETE CASCADE';
		$tables['userevents'][] = 'CONSTRAINT ^userevents_ibfk_1 ' . $userforeignkey . ' ON DELETE CASCADE';
		$tables['userlevels'][] = 'CONSTRAINT ^userlevels_ibfk_1 ' . $userforeignkey . ' ON DELETE CASCADE';
		$tables['usermetas'][] = 'CONSTRAINT ^usermetas_ibfk_1 ' . $userforeignkey . ' ON DELETE CASCADE';
534 535
		$tables['messages'][] = 'CONSTRAINT ^messages_ibfk_1 FOREIGN KEY (fromuserid) REFERENCES ^users(userid) ON DELETE SET NULL';
		$tables['messages'][] = 'CONSTRAINT ^messages_ibfk_2 FOREIGN KEY (touserid) REFERENCES ^users(userid) ON DELETE SET NULL';
Scott committed
536 537
	}

Scott committed
538 539
	return $tables;
}
Scott committed
540 541


Scott committed
542 543
/**
 * Return array with all values from $array as keys
544
 * @param array $array
Scott committed
545 546 547 548 549 550
 * @return array
 */
function qa_array_to_keys($array)
{
	return empty($array) ? array() : array_combine($array, array_fill(0, count($array), true));
}
Scott committed
551 552


Scott committed
553 554
/**
 * Return a list of tables missing from the database, [table name] => [column/index definitions]
555
 * @param array $definitions
Scott committed
556 557 558 559
 * @return array
 */
function qa_db_missing_tables($definitions)
{
560
	$keydbtables = qa_array_to_keys(qa_db_list_tables(true));
Scott committed
561

Scott committed
562
	$missing = array();
Scott committed
563

Scott committed
564 565 566
	foreach ($definitions as $rawname => $definition)
		if (!isset($keydbtables[qa_db_add_table_prefix($rawname)]))
			$missing[$rawname] = $definition;
Scott committed
567

Scott committed
568 569
	return $missing;
}
Scott committed
570 571


Scott committed
572 573
/**
 * Return a list of columns missing from $table in the database, given the full definition set in $definition
574 575
 * @param string $table
 * @param array $definition
Scott committed
576 577 578 579 580
 * @return array
 */
function qa_db_missing_columns($table, $definition)
{
	$keycolumns = qa_array_to_keys(qa_db_read_all_values(qa_db_query_sub('SHOW COLUMNS FROM ^' . $table)));
Scott committed
581

Scott committed
582
	$missing = array();
Scott committed
583

Scott committed
584
	foreach ($definition as $colname => $coldefn)
Scott committed
585
		if (!is_int($colname) && !isset($keycolumns[$colname]))
Scott committed
586
			$missing[$colname] = $coldefn;
Scott committed
587

Scott committed
588 589
	return $missing;
}
Scott committed
590 591


Scott committed
592 593
/**
 * Return the current version of the Q2A database, to determine need for DB upgrades
594
 * @return int|null
Scott committed
595 596 597 598
 */
function qa_db_get_db_version()
{
	$definitions = qa_db_table_definitions();
Scott committed
599

Scott committed
600 601
	if (count(qa_db_missing_columns('options', $definitions['options'])) == 0) {
		$version = (int)qa_db_read_one_value(qa_db_query_sub("SELECT content FROM ^options WHERE title='db_version'"), true);
Scott committed
602

Scott committed
603 604
		if ($version > 0)
			return $version;
Scott committed
605 606
	}

Scott committed
607 608
	return null;
}
Scott committed
609

610

Scott committed
611 612
/**
 * Set the current version in the database
613
 * @param int $version
Scott committed
614 615 616 617
 */
function qa_db_set_db_version($version)
{
	require_once QA_INCLUDE_DIR . 'db/options.php';
Scott committed
618

Scott committed
619 620
	qa_db_set_option('db_version', $version);
}
Scott committed
621 622


Scott committed
623 624
/**
 * Return a string describing what is wrong with the database, or false if everything is just fine
625
 * @return false|string
Scott committed
626 627 628 629
 */
function qa_db_check_tables()
{
	qa_db_query_raw('UNLOCK TABLES'); // we could be inside a lock tables block
Scott committed
630

Scott committed
631
	$version = qa_db_read_one_value(qa_db_query_raw('SELECT VERSION()'));
Scott committed
632

Scott committed
633 634
	if (((float)$version) < 4.1)
		qa_fatal_error('MySQL version 4.1 or later is required - you appear to be running MySQL ' . $version);
Scott committed
635

Scott committed
636 637
	$definitions = qa_db_table_definitions();
	$missing = qa_db_missing_tables($definitions);
Scott committed
638

Scott committed
639 640
	if (count($missing) == count($definitions))
		return 'none';
Scott committed
641

Scott committed
642 643 644
	else {
		if (!isset($missing['options'])) {
			$version = qa_db_get_db_version();
Scott committed
645

Scott committed
646 647 648
			if (isset($version) && ($version < QA_DB_VERSION_CURRENT))
				return 'old-version';
		}
Scott committed
649

Scott committed
650 651 652 653
		if (count($missing)) {
			if (defined('QA_MYSQL_USERS_PREFIX')) { // special case if two installations sharing users
				$datacount = 0;
				$datamissing = 0;
Scott committed
654

655
				foreach ($definitions as $rawname => $definition) {
Scott committed
656 657
					if (qa_db_add_table_prefix($rawname) == (QA_MYSQL_TABLE_PREFIX . $rawname)) {
						$datacount++;
Scott committed
658

Scott committed
659 660 661
						if (isset($missing[$rawname]))
							$datamissing++;
					}
662
				}
Scott committed
663

Scott committed
664
				if ($datacount == $datamissing && $datamissing == count($missing))
Scott committed
665 666
					return 'non-users-missing';
			}
Scott committed
667

Scott committed
668
			return 'table-missing';
Scott committed
669

Scott committed
670 671 672 673
		} else
			foreach ($definitions as $table => $definition)
				if (count(qa_db_missing_columns($table, $definition)))
					return 'column-missing';
Scott committed
674 675
	}

Scott committed
676 677
	return false;
}
Scott committed
678 679


Scott committed
680 681 682 683 684 685 686
/**
 * Install any missing database tables and/or columns and automatically set version as latest.
 * This is not suitable for use if the database needs upgrading.
 */
function qa_db_install_tables()
{
	$definitions = qa_db_table_definitions();
Scott committed
687

Scott committed
688
	$missingtables = qa_db_missing_tables($definitions);
Scott committed
689

Scott committed
690 691
	foreach ($missingtables as $rawname => $definition) {
		qa_db_query_sub(qa_db_create_table_sql($rawname, $definition));
Scott committed
692

Scott committed
693 694 695
		if ($rawname == 'userfields')
			qa_db_query_sub(qa_db_default_userfields_sql());
	}
Scott committed
696

Scott committed
697 698
	foreach ($definitions as $table => $definition) {
		$missingcolumns = qa_db_missing_columns($table, $definition);
Scott committed
699

Scott committed
700 701
		foreach ($missingcolumns as $colname => $coldefn)
			qa_db_query_sub('ALTER TABLE ^' . $table . ' ADD COLUMN ' . $colname . ' ' . $coldefn);
Scott committed
702 703
	}

Scott committed
704 705
	qa_db_set_db_version(QA_DB_VERSION_CURRENT);
}
Scott committed
706 707


Scott committed
708 709
/**
 * Return the SQL command to create a table with $rawname and $definition obtained from qa_db_table_definitions()
710 711
 * @param string $rawname
 * @param array $definition
Scott committed
712 713 714 715 716 717 718 719
 * @return string
 */
function qa_db_create_table_sql($rawname, $definition)
{
	$querycols = '';
	foreach ($definition as $colname => $coldef)
		if (isset($coldef))
			$querycols .= (strlen($querycols) ? ', ' : '') . (is_int($colname) ? $coldef : ($colname . ' ' . $coldef));
Scott committed
720

Scott committed
721 722
	return 'CREATE TABLE ^' . $rawname . ' (' . $querycols . ') ENGINE=InnoDB CHARSET=utf8';
}
Scott committed
723 724


Scott committed
725 726
/**
 * Return the SQL to create the default entries in the userfields table (before 1.3 these were hard-coded in PHP)
727
 * @return string
Scott committed
728 729 730
 */
function qa_db_default_userfields_sql()
{
731 732 733 734 735
	require_once QA_INCLUDE_DIR . 'app/options.php';

	$profileFields = array(
		array(
			'title' => 'name',
736
			'position' => 1,
737 738 739 740 741
			'flags' => 0,
			'permit' => QA_PERMIT_ALL,
		),
		array(
			'title' => 'location',
742
			'position' => 2,
743 744 745 746 747
			'flags' => 0,
			'permit' => QA_PERMIT_ALL,
		),
		array(
			'title' => 'website',
748
			'position' => 3,
749 750 751 752 753
			'flags' => QA_FIELD_FLAGS_LINK_URL,
			'permit' => QA_PERMIT_ALL,
		),
		array(
			'title' => 'about',
754
			'position' => 4,
755 756 757
			'flags' => QA_FIELD_FLAGS_MULTI_LINE,
			'permit' => QA_PERMIT_ALL,
		),
Scott committed
758
	);
Scott committed
759

760 761 762
	$sql = 'INSERT INTO ^userfields (title, position, flags, permit) VALUES'; // content column will be NULL, meaning use default from lang files

	foreach ($profileFields as $field) {
763
		$sql .= sprintf('("%s", %d, %d, %d), ', $field['title'], $field['position'], $field['flags'], $field['permit']);
764
	}
Scott committed
765

766
	$sql = substr($sql, 0, -2);
Scott committed
767

Scott committed
768 769
	return $sql;
}
Scott committed
770 771


Scott committed
772 773 774 775 776 777 778
/**
 * Upgrade the database schema to the latest version, outputting progress to the browser
 */
function qa_db_upgrade_tables()
{
	$definitions = qa_db_table_definitions();
	$keyrecalc = array();
Scott committed
779

Scott committed
780
	// Write-lock all Q2A tables before we start so no one can read or write anything
Scott committed
781

782
	$keydbtables = qa_array_to_keys(qa_db_list_tables(true));
Scott committed
783

Scott committed
784 785 786
	foreach ($definitions as $rawname => $definition)
		if (isset($keydbtables[qa_db_add_table_prefix($rawname)]))
			$locks[] = '^' . $rawname . ' WRITE';
Scott committed
787

Scott committed
788
	$locktablesquery = 'LOCK TABLES ' . implode(', ', $locks);
Scott committed
789

Scott committed
790
	qa_db_upgrade_query($locktablesquery);
Scott committed
791

Scott committed
792
	// Upgrade it step-by-step until it's up to date (do LOCK TABLES after ALTER TABLE because the lock can sometimes be lost)
793

Scott committed
794 795
	// message (used in sprintf) for skipping shared user tables
	$skipMessage = 'Skipping upgrading %s table since it was already upgraded by another Q2A site sharing it.';
Scott committed
796

Scott committed
797 798
	while (1) {
		$version = qa_db_get_db_version();
Scott committed
799

Scott committed
800 801
		if ($version >= QA_DB_VERSION_CURRENT)
			break;
Scott committed
802

Scott committed
803
		$newversion = $version + 1;
Scott committed
804

Scott committed
805
		qa_db_upgrade_progress(QA_DB_VERSION_CURRENT - $version . ' upgrade step/s remaining...');
Scott committed
806

Scott committed
807 808
		switch ($newversion) {
			// Up to here: Version 1.5.x
Scott committed
809

Scott committed
810 811 812 813
			case 48:
				if (!QA_FINAL_EXTERNAL_USERS) {
					$keycolumns = qa_array_to_keys(qa_db_read_all_values(qa_db_query_sub('SHOW COLUMNS FROM ^messages')));
					// might be using messages table shared with another installation, so check if we need to upgrade
Scott committed
814

Scott committed
815 816 817 818 819
					if (isset($keycolumns['type']))
						qa_db_upgrade_progress('Skipping upgrading messages table since it was already upgraded by another Q2A site sharing it.');

					else {
						qa_db_upgrade_query('ALTER TABLE ^messages ADD COLUMN type ' . $definitions['messages']['type'] . ' AFTER messageid, DROP KEY fromuserid, ADD key type (type, fromuserid, touserid, created), ADD KEY touserid (touserid, type, created)');
Scott committed
820 821
						qa_db_upgrade_query($locktablesquery);
					}
Scott committed
822 823
				}
				break;
Scott committed
824

Scott committed
825 826 827
			case 49:
				if (!QA_FINAL_EXTERNAL_USERS) {
					qa_db_upgrade_query('ALTER TABLE ^users CHANGE COLUMN flags flags ' . $definitions['users']['flags']);
Scott committed
828
					qa_db_upgrade_query($locktablesquery);
Scott committed
829 830 831 832 833 834 835
				}
				break;

			case 50:
				qa_db_upgrade_query('ALTER TABLE ^posts ADD COLUMN name ' . $definitions['posts']['name'] . ' AFTER tags');
				qa_db_upgrade_query($locktablesquery);
				break;
Scott committed
836

Scott committed
837 838 839 840
			case 51:
				if (!QA_FINAL_EXTERNAL_USERS) {
					// might be using userfields table shared with another installation, so check if we need to upgrade
					$keycolumns = qa_array_to_keys(qa_db_read_all_values(qa_db_query_sub('SHOW COLUMNS FROM ^userfields')));
Scott committed
841

Scott committed
842 843
					if (isset($keycolumns['permit']))
						qa_db_upgrade_progress('Skipping upgrading userfields table since it was already upgraded by another Q2A site sharing it.');
Scott committed
844

Scott committed
845 846 847
					else {
						qa_db_upgrade_query('ALTER TABLE ^userfields ADD COLUMN permit ' . $definitions['userfields']['permit'] . ' AFTER flags');
						qa_db_upgrade_query($locktablesquery);
Scott committed
848
					}
Scott committed
849 850
				}
				break;
Scott committed
851

Scott committed
852 853 854
			case 52:
				if (!QA_FINAL_EXTERNAL_USERS) {
					$keyindexes = qa_array_to_keys(qa_db_read_all_assoc(qa_db_query_sub('SHOW INDEX FROM ^users'), null, 'Key_name'));
Scott committed
855

Scott committed
856 857
					if (isset($keyindexes['created']))
						qa_db_upgrade_progress('Skipping upgrading users table since it was already upgraded by another Q2A site sharing it.');
Scott committed
858

Scott committed
859 860 861
					else {
						qa_db_upgrade_query('ALTER TABLE ^users ADD KEY created (created, level, flags)');
						qa_db_upgrade_query($locktablesquery);
Scott committed
862
					}
Scott committed
863 864
				}
				break;
Scott committed
865

Scott committed
866 867 868 869
			case 53:
				qa_db_upgrade_query('ALTER TABLE ^blobs CHANGE COLUMN content content ' . $definitions['blobs']['content']);
				qa_db_upgrade_query($locktablesquery);
				break;
Scott committed
870

Scott committed
871 872
			case 54:
				qa_db_upgrade_query('UNLOCK TABLES');
Scott committed
873

Scott committed
874
				qa_db_upgrade_query('SET FOREIGN_KEY_CHECKS=0'); // in case InnoDB not available
Scott committed
875

Scott committed
876 877 878 879 880 881 882 883 884
				qa_db_upgrade_query(qa_db_create_table_sql('userlevels', array(
					'userid' => $definitions['userlevels']['userid'],
					'entitytype' => $definitions['userlevels']['entitytype'],
					'entityid' => $definitions['userlevels']['entityid'],
					'level' => $definitions['userlevels']['level'],
					'UNIQUE userid (userid, entitytype, entityid)',
					'KEY entitytype (entitytype, entityid)',
					QA_FINAL_EXTERNAL_USERS ? null : 'CONSTRAINT ^userlevels_ibfk_1 FOREIGN KEY (userid) REFERENCES ^users(userid) ON DELETE CASCADE',
				)));
Scott committed
885

Scott committed
886 887 888
				$locktablesquery .= ', ^userlevels WRITE';
				qa_db_upgrade_query($locktablesquery);
				break;
Scott committed
889

Scott committed
890
			// Up to here: Version 1.6 beta 1
Scott committed
891

Scott committed
892 893 894 895
			case 55:
				if (!QA_FINAL_EXTERNAL_USERS) {
					// might be using users table shared with another installation, so check if we need to upgrade
					$keycolumns = qa_array_to_keys(qa_db_read_all_values(qa_db_query_sub('SHOW COLUMNS FROM ^users')));
Scott committed
896

Scott committed
897 898
					if (isset($keycolumns['wallposts']))
						qa_db_upgrade_progress('Skipping upgrading users table since it was already upgraded by another Q2A site sharing it.');
Scott committed
899

Scott committed
900 901 902
					else {
						qa_db_upgrade_query('ALTER TABLE ^users ADD COLUMN wallposts ' . $definitions['users']['wallposts'] . ' AFTER flags');
						qa_db_upgrade_query($locktablesquery);
Scott committed
903
					}
Scott committed
904 905
				}
				break;
Scott committed
906

Scott committed
907
			// Up to here: Version 1.6 beta 2
Scott committed
908

Scott committed
909 910 911 912
			case 56:
				qa_db_upgrade_query('ALTER TABLE ^pages DROP INDEX tags, ADD KEY tags (tags)');
				qa_db_upgrade_query($locktablesquery);
				break;
Scott committed
913

Scott committed
914
			// Up to here: Version 1.6 (release)
Scott committed
915

Scott committed
916 917 918 919
			case 57:
				if (!QA_FINAL_EXTERNAL_USERS) {
					// might be using messages table shared with another installation, so check if we need to upgrade
					$keycolumns = qa_array_to_keys(qa_db_read_all_values(qa_db_query_sub('SHOW COLUMNS FROM ^messages')));
Scott committed
920

Scott committed
921 922 923 924 925 926
					if (isset($keycolumns['fromhidden']))
						qa_db_upgrade_progress('Skipping upgrading messages table since it was already upgraded by another Q2A site sharing it.');
					else {
						qa_db_upgrade_query('ALTER TABLE ^messages ADD COLUMN fromhidden ' . $definitions['messages']['fromhidden'] . ' AFTER touserid');
						qa_db_upgrade_query('ALTER TABLE ^messages ADD COLUMN tohidden ' . $definitions['messages']['tohidden'] . ' AFTER fromhidden');
						qa_db_upgrade_query('ALTER TABLE ^messages ADD KEY fromhidden (fromhidden), ADD KEY tohidden (tohidden)');
Scott committed
927

Scott committed
928
						qa_db_upgrade_query($locktablesquery);
Scott committed
929
					}
Scott committed
930 931
				}
				break;
Scott committed
932

Scott committed
933 934 935 936 937
			case 58:
				// note: need to use full table names here as aliases trigger error "Table 'x' was not locked with LOCK TABLES"
				qa_db_upgrade_query('DELETE FROM ^userfavorites WHERE entitytype="U" AND userid=entityid');
				qa_db_upgrade_query('DELETE ^uservotes FROM ^uservotes JOIN ^posts ON ^uservotes.postid=^posts.postid AND ^uservotes.userid=^posts.userid');
				qa_db_upgrade_query($locktablesquery);
Scott committed
938

Scott committed
939 940 941
				$keyrecalc['dorecountposts'] = true;
				$keyrecalc['dorecalcpoints'] = true;
				break;
Scott committed
942

Scott committed
943
			// Up to here: Version 1.7
944

Scott committed
945 946 947
			case 59:
				// upgrade from alpha version removed
				break;
948

Scott committed
949
			// Up to here: Version 1.7.1
Scott committed
950

Scott committed
951 952 953 954 955 956 957
			case 60:
				// add new category widget - note title must match that from qa_register_core_modules()
				if (qa_using_categories()) {
					$widgetid = qa_db_widget_create('Categories', 'all');
					qa_db_widget_move($widgetid, 'SL', 1);
				}
				break;
958

Scott committed
959 960 961 962 963 964
			case 61:
				// upgrade length of qa_posts.content field to 12000
				$newlength = QA_DB_MAX_CONTENT_LENGTH;
				$query = 'SELECT CHARACTER_MAXIMUM_LENGTH FROM information_schema.COLUMNS WHERE table_schema=$ AND table_name=$ AND column_name="content"';
				$tablename = qa_db_add_table_prefix('posts');
				$oldlength = qa_db_read_one_value(qa_db_query_sub($query, QA_FINAL_MYSQL_DATABASE, $tablename));
Scott committed
965

Scott committed
966 967 968
				if ($oldlength < $newlength) {
					qa_db_upgrade_query('ALTER TABLE ^posts MODIFY content ' . $definitions['posts']['content']);
				}
Scott committed
969

Scott committed
970
				break;
971

Scott committed
972 973 974 975
			case 62:
				if (!QA_FINAL_EXTERNAL_USERS) {
					// might be using users table shared with another installation, so check if we need to upgrade
					$keycolumns = qa_array_to_keys(qa_db_read_all_values(qa_db_query_sub('SHOW COLUMNS FROM ^users')));
976

Scott committed
977 978
					if (isset($keycolumns['passhash']))
						qa_db_upgrade_progress(sprintf($skipMessage, 'users'));
979
					else {
Scott committed
980 981 982
						// add column to qa_users to handle new bcrypt passwords
						qa_db_upgrade_query('ALTER TABLE ^users ADD COLUMN passhash ' . $definitions['users']['passhash'] . ' AFTER passcheck');
						qa_db_upgrade_query($locktablesquery);
983
					}
Scott committed
984 985
				}
				break;
986

Scott committed
987 988 989 990 991 992 993 994 995
			case 63:
				// check for shared cookies table
				$fieldDef = qa_db_read_one_assoc(qa_db_query_sub('SHOW COLUMNS FROM ^cookies WHERE Field="createip"'));
				if (strtolower($fieldDef['Type']) === 'varbinary(16)')
					qa_db_upgrade_progress(sprintf($skipMessage, 'cookies'));
				else {
					qa_db_upgrade_query('ALTER TABLE ^cookies MODIFY writeip ' . $definitions['cookies']['writeip'] . ', MODIFY createip ' . $definitions['cookies']['createip']);
					qa_db_upgrade_query('UPDATE ^cookies SET writeip = UNHEX(HEX(CAST(writeip AS UNSIGNED))), createip = UNHEX(HEX(CAST(createip AS UNSIGNED)))');
				}
996

Scott committed
997 998
				qa_db_upgrade_query('ALTER TABLE ^iplimits MODIFY ip ' . $definitions['iplimits']['ip']);
				qa_db_upgrade_query('UPDATE ^iplimits SET ip = UNHEX(HEX(CAST(ip AS UNSIGNED)))');
999

Scott committed
1000 1001 1002 1003 1004 1005 1006 1007
				// check for shared blobs table
				$fieldDef = qa_db_read_one_assoc(qa_db_query_sub('SHOW COLUMNS FROM ^blobs WHERE Field="createip"'));
				if (strtolower($fieldDef['Type']) === 'varbinary(16)')
					qa_db_upgrade_progress(sprintf($skipMessage, 'blobs'));
				else {
					qa_db_upgrade_query('ALTER TABLE ^blobs MODIFY createip ' . $definitions['blobs']['createip']);
					qa_db_upgrade_query('UPDATE ^blobs SET createip = UNHEX(HEX(CAST(createip AS UNSIGNED)))');
				}
Scott committed
1008

Scott committed
1009 1010
				qa_db_upgrade_query('ALTER TABLE ^posts MODIFY lastviewip ' . $definitions['posts']['lastviewip'] . ', MODIFY lastip ' . $definitions['posts']['lastip'] . ', MODIFY createip ' . $definitions['posts']['createip']);
				qa_db_upgrade_query('UPDATE ^posts SET lastviewip = UNHEX(HEX(CAST(lastviewip AS UNSIGNED))), lastip = UNHEX(HEX(CAST(lastip AS UNSIGNED))), createip = UNHEX(HEX(CAST(createip AS UNSIGNED)))');
1011

Scott committed
1012 1013 1014 1015 1016 1017 1018 1019 1020 1021
				if (!QA_FINAL_EXTERNAL_USERS) {
					// check for shared users table
					$fieldDef = qa_db_read_one_assoc(qa_db_query_sub('SHOW COLUMNS FROM ^users WHERE Field="createip"'));
					if (strtolower($fieldDef['Type']) === 'varbinary(16)')
						qa_db_upgrade_progress(sprintf($skipMessage, 'users'));
					else {
						qa_db_upgrade_query('ALTER TABLE ^users MODIFY createip ' . $definitions['users']['createip'] . ', MODIFY loginip ' . $definitions['users']['loginip'] . ', MODIFY writeip ' . $definitions['users']['writeip']);
						qa_db_upgrade_query('UPDATE ^users SET createip = UNHEX(HEX(CAST(createip AS UNSIGNED))), loginip = UNHEX(HEX(CAST(loginip AS UNSIGNED))), writeip = UNHEX(HEX(CAST(writeip AS UNSIGNED)))');
					}
				}
1022

Scott committed
1023 1024
				qa_db_upgrade_query($locktablesquery);
				break;
1025

Scott committed
1026
			case 64:
Scott committed
1027
				$pluginManager = new \Q2A\Plugin\PluginManager();
Scott committed
1028 1029 1030
				$allPlugins = $pluginManager->getFilesystemPlugins();
				$pluginManager->setEnabledPlugins($allPlugins);
				break;
Scott committed
1031

Scott committed
1032 1033 1034 1035 1036 1037 1038 1039 1040
			case 65:
				qa_db_upgrade_query('ALTER TABLE ^uservotes ADD COLUMN votecreated ' . $definitions['uservotes']['votecreated'] . ' AFTER flag');
				qa_db_upgrade_query('ALTER TABLE ^uservotes ADD COLUMN voteupdated ' . $definitions['uservotes']['voteupdated'] . ' AFTER votecreated');
				qa_db_upgrade_query('ALTER TABLE ^uservotes ADD KEY voted (votecreated, voteupdated)');
				qa_db_upgrade_query($locktablesquery);

				// for old votes, set a default date of when that post was made
				qa_db_upgrade_query('UPDATE ^uservotes, ^posts SET ^uservotes.votecreated=^posts.created WHERE ^uservotes.postid=^posts.postid AND (^uservotes.vote != 0 OR ^uservotes.flag=0)');
				break;
Scott committed
1041

1042 1043 1044 1045 1046 1047 1048
			case 66:
				$newColumns = array(
					'ADD COLUMN cupvotes ' . $definitions['userpoints']['cupvotes'] . ' AFTER adownvotes',
					'ADD COLUMN cdownvotes ' . $definitions['userpoints']['cdownvotes'] . ' AFTER cupvotes',
					'ADD COLUMN cvoteds ' . $definitions['userpoints']['cvoteds'] . ' AFTER avoteds',
				);
				qa_db_upgrade_query('ALTER TABLE ^userpoints ' . implode(', ', $newColumns));
1049 1050 1051 1052
				qa_db_upgrade_query($locktablesquery);
				break;

			case 67:
1053
				if (!QA_FINAL_EXTERNAL_USERS) {
1054
					// ensure we don't have old userids lying around
1055 1056 1057 1058 1059 1060 1061 1062
					qa_db_upgrade_query('ALTER TABLE ^messages MODIFY fromuserid ' . $definitions['messages']['fromuserid']);
					qa_db_upgrade_query('ALTER TABLE ^messages MODIFY touserid ' . $definitions['messages']['touserid']);
					qa_db_upgrade_query('UPDATE ^messages SET fromuserid=NULL WHERE fromuserid NOT IN (SELECT userid FROM ^users)');
					qa_db_upgrade_query('UPDATE ^messages SET touserid=NULL WHERE touserid NOT IN (SELECT userid FROM ^users)');
					// set up foreign key on messages table
					qa_db_upgrade_query('ALTER TABLE ^messages ADD CONSTRAINT ^messages_ibfk_1 FOREIGN KEY (fromuserid) REFERENCES ^users(userid) ON DELETE SET NULL');
					qa_db_upgrade_query('ALTER TABLE ^messages ADD CONSTRAINT ^messages_ibfk_2 FOREIGN KEY (touserid) REFERENCES ^users(userid) ON DELETE SET NULL');
				}
1063 1064

				qa_db_upgrade_query($locktablesquery);
1065 1066
				break;

Scott committed
1067
			// Up to here: Version 1.8
1068 1069 1070 1071 1072 1073 1074

			case 68:
				// remove favorites of deleted users
				qa_db_upgrade_query('DELETE FROM ^userfavorites WHERE entitytype="U" AND entityid NOT IN (SELECT userid FROM ^users)');
				break;

			// Up to here: Version 1.9
Scott committed
1075 1076
		}

Scott committed
1077
		qa_db_set_db_version($newversion);
Scott committed
1078

Scott committed
1079 1080
		if (qa_db_get_db_version() != $newversion)
			qa_fatal_error('Could not increment database version');
Scott committed
1081 1082
	}

Scott committed
1083
	qa_db_upgrade_query('UNLOCK TABLES');
Scott committed
1084

Scott committed
1085
	// Perform any necessary recalculations, as determined by upgrade steps
Scott committed
1086

1087
	foreach (array_keys($keyrecalc) as $state) {
1088
		$recalc = new \Q2A\Recalc\RecalcMain($state);
1089
		while ($recalc->getState()) {
Scott committed
1090
			set_time_limit(60);
Scott committed
1091

Scott committed
1092
			$stoptime = time() + 2;
Scott committed
1093

1094
			while ($recalc->performStep() && (time() < $stoptime))
Scott committed
1095
				;
Scott committed
1096

1097
			qa_db_upgrade_progress($recalc->getMessage());
Scott committed
1098
		}
1099
	}
Scott committed
1100 1101 1102 1103 1104
}


/**
 * Reset the definitions of $columns in $table according to the $definitions array
1105 1106 1107
 * @param array $definitions
 * @param string $table
 * @param array $columns
Scott committed
1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121
 */
function qa_db_upgrade_table_columns($definitions, $table, $columns)
{
	$sqlchanges = array();

	foreach ($columns as $column)
		$sqlchanges[] = 'CHANGE COLUMN ' . $column . ' ' . $column . ' ' . $definitions[$table][$column];

	qa_db_upgrade_query('ALTER TABLE ^' . $table . ' ' . implode(', ', $sqlchanges));
}


/**
 * Perform upgrade $query and output progress to the browser
1122
 * @param string $query
Scott committed
1123 1124 1125 1126 1127 1128 1129 1130 1131 1132
 */
function qa_db_upgrade_query($query)
{
	qa_db_upgrade_progress('Running query: ' . qa_db_apply_sub($query, array()) . ' ...');
	qa_db_query_sub($query);
}


/**
 * Output $text to the browser (after converting to HTML) and do all we can to get it displayed
1133
 * @param string $text
Scott committed
1134 1135 1136 1137 1138 1139
 */
function qa_db_upgrade_progress($text)
{
	echo qa_html($text) . str_repeat('    ', 1024) . "<br><br>\n";
	flush();
}