Ticket #8317 (closed defect (bug): fixed)

Opened 2 months ago

Last modified 1 month ago

New installs: admin has user_level 0

Reported by: Mr Pete Assigned to: jacobsantos
Priority: normal Milestone: 2.7.1
Component: Upgrade Version: 2.5
Severity: major Keywords:
Cc:

Description

On a fresh install of WordPress, the admin user begins with user_level == 0 (even thought level_10 works fine.)

This seems rather serious to me. It breaks any plugin, widget or theme feature that makes use of $user_level to identify administrators.

It's easy to check:

global $current_user;
get_currentuserinfo();
echo "level:".$user_level."<br />";
exit;

This was not a problem in 2.3.3; it is a problem from 2.5 onward at least through the current trunk.

All of my test systems have user level 10, so it somehow gets "fixed" after running a while. However, in the first week of my new plugin's release, I have had several user reports that my plugin is broken, and I traced it down to this issue. It wasn't my plugin, it was the fact that they had new installs of WordPress.

Attachments

rolefix.patch (3.6 kB) - added by Mr Pete on 11/26/08 03:01:16.
Proposed patch to fix
rolefix.diff (3.6 kB) - added by Mr Pete on 11/27/08 03:00:57.
Same thing with *.diff ext'n

Change History

11/23/08 00:26:05 changed by DD32

  • version set to 2.5.

for a start (while it doesnt rule out this 'bug') you should probably switch to using capabilities instead for those who use finer grained control methods such as the Role manager plugin (ie. 'manage_options' or 'administrator' or 'publish_posts', etc) - thats assuming the versions of WP you're wanting to support support it (I have no idea when it was introduced)

11/23/08 00:32:38 changed by DD32

  • keywords set to reporter-feedback.

I cant reproduce it.

After globalising $user_level instad:

global $user_level;
get_currentuserinfo();
echo "level:".$user_level."<br />";

and running that code 2 page loads after a clean install, it appears to be working for me.

11/23/08 01:41:49 changed by jacobsantos

Seems invalid to me.

It shouldn't affect the core of WordPress, since the core uses capabilities instead. The user_level has been deprecated since 2.0 or 2.1 I think. Ever since the new WP_Role class was added.

It will affect older plugins or plugins which use user_level instead of capabilities.

11/23/08 04:21:44 changed by Mr Pete

My bad, yes it's globalized $user_level, not $current_user.

I easily replicated by downloading a fresh copy of any rev from 2.5 to 2.7 and installing from scratch.

It does not affect the core... it affects any plugin that uses $user_level.

The code AND documentation say that $user_level will someday be deprecated. But not now.

Perhaps the "fix" is to explicitly deprecate the old globals now :)

11/23/08 08:41:05 changed by westi

  • owner changed from anonymous to westi.
  • status changed from new to assigned.

Please can you supply a simple example plugin which demostrates this issue as we can't reproduce it and the code looks fine to me too.

11/23/08 14:37:11 changed by Mr Pete

Sure! How about the following. I saved as a php file in wp-content/plugins in yesterday's trunk download, in 2.5, and in 2.3.3. Works fine in 2.3.3, not after. Remember to use a clean new database... your existing tables probably already have some value for admin user_level :) Just activate the plugin and then look at the bottom of the page.

<?php
/*
Plugin Name: UserLevel Test
Plugin URI: http://blogs.icta.net/plugins/
Description: Generic simple test plugin
Version: 0.1
Author: Mr Pete
Author URI: http://blogs.icta.net/plugins/
*/


add_action('wp_footer', 'test_foot', 9999999999);
add_action('admin_footer', 'test_foot', 999999999);
function test_foot()
{
	global $user_level;
	get_currentuserinfo();
	
	echo "<div>User level is: $user_level<br/>User can level_10: ".(current_user_can('level_10') ? "yes" : "no")."</div>";
}
?>

11/23/08 14:38:29 changed by Mr Pete

Idea: right now I'm testing on PHP 4.4.4 ... I'll try some other PHP's in a bit...

(follow-up: ↓ 9 ) 11/24/08 06:21:32 changed by ryan

  • status changed from assigned to closed.
  • resolution set to worksforme.
  • milestone deleted.

Performed new install in clean DB with both PHP 4 and 5 and it works for me.

(in reply to: ↑ 8 ; follow-up: ↓ 10 ) 11/24/08 18:13:25 changed by Mr Pete

  • status changed from closed to reopened.
  • resolution deleted.

Replying to ryan:

Performed new install in clean DB with both PHP 4 and 5 and it works for me.

Please recheck carefully. I've now rerun the test several times, using the current trunk.

Linux, php 5.2.6 -- works fine Linux, php 4.4.9 -- FAILS as described above Windows XP, php 5.2.1 -- works fine Windows XP, php 4.4.4 -- FAILS as described above

This appears to be an issue with PHP4 vs PHP5 support. Perhaps some kind of object code?

(in reply to: ↑ 9 ) 11/24/08 18:23:10 changed by Mr Pete

Linux, php 5.2.6 -- works fine Linux, php 4.4.9 -- FAILS as described above Windows XP, php 5.2.1 -- works fine Windows XP, php 4.4.4 -- FAILS as described above

Sorry about the formatting. Wonder if this will work...

11/24/08 18:29:46 changed by Mr Pete

 Linux, php 5.2.6 -- works fine
 Linux, php 4.4.9 -- FAILS as described above
 Windows XP, php 5.2.1 -- works fine
 Windows XP, php 4.4.4 -- FAILS as described above

Argh. Need to use preview!

11/24/08 19:46:15 changed by ryan

It looks like WP_User::get_role_caps() isn't expanding the role into the full caps list (allcaps) during install. Thus WP_User::update_user_level_from_caps() doesn't see the levels. If someone wants to track this down further, go for it.

11/24/08 20:22:08 changed by jacobsantos

  • status changed from reopened to new.
  • severity changed from normal to major.
  • component changed from General to Upgrade.
  • priority changed from normal to low.
  • owner changed from westi to jacobsantos.
  • milestone set to 2.8.
  • keywords changed from reporter-feedback to early.

I'll check this out. Thanks for finding the cause, should be easy now.

If it was important more people would have had this problem or reported it sooner.

11/24/08 20:22:31 changed by jacobsantos

  • keywords deleted.

Probably should let developers decide that one.

11/24/08 23:43:26 changed by Mr Pete

Re: lack of recognition...

I would have never guessed this was the issue. In my plugin, what was reported was certain users reported that crucial functions were not working correctly. That's all I knew. And, like you, I was unable to replicate the problem.

I suspect a lot of devs would just ignore the issue. After all, what can you do about a problem that not everyone sees, and you can't duplicate it.

I happen to have a prof'l software-breaking background, so I continued to go after it. And finally I found a repeatable test case.

I just searched the dozen plugins I am thinking of using, and googled for a couple of minutes (if anyone can grep the plugin svn this would be easy :)

Here are a few plugins most likely affected in some way by this: openwebanalytics, wp_shopping_cart, social_networking, dashboardopts, subscribe2, theme modern_blue.

11/24/08 23:45:01 changed by Mr Pete

btw, ryan, thanks for debugging it to the next level!

11/25/08 05:00:43 changed by DD32

I just tried to trace the bug, Utterly impossible, Seems like WP_User::$caps isnt being populated at all, and i couldnt work out why

If i was to replace:

http://trac.wordpress.org/browser/trunk/wp-includes/capabilities.php#L107

 $this->roles = get_option( $this->role_key );

with a var_export() a working role key, Everything would work as expected, So my thinking is that the WP_Roles class might not be picking up the role/capabilities list correctly.

11/25/08 13:14:48 changed by Mr Pete

I'm on the hunt... a nice little puzzle!

Tracked down to this, so far. Not done yet...

During install, in wp-admin\include\upgrade.php line 68, set_role('administrator') ultimately calls get_role_caps() in capabilities.php.

PHP5: the result is an array with 'administrator' and all associated caps, including 'level_10'.

PHP4: the result is an array with ONLY 'administrator' and nothing else.

11/25/08 13:46:01 changed by Mr Pete

[How do you enable Wiki Formatting? I can't get bullets to work...]

End of current debug pass:

* add_cap() appears to work ok in php4 and php5

* However, by the time get_role_caps() is called, $role->capabilities is an empty array in php4 but ok in php5

Feel free to pick this up, anyone!

Debug hints:

* Create a temp database, have phpMyAdmin open to quickly select all tables and drop... to reset before each debug run

* Have httpd.conf open to swap between php4/5 as needed

* A debug pass is simple: http://localhost/wp/wp-admin/install.php asks for blog name, email. Click install, you've got your debug result (depending on your trace tools)

(I'm using my WP Tuner tools, w/ a couple of minor mods to function during install)

11/25/08 19:50:34 changed by Mr Pete

Solved it. Interesting bug! I learned a bit of new stuff about php. And... I believe this same pattern is a problem for some other code.

The issue: PHP5 automagically passes and returns objects by reference. PHP4 passes and returns object copies. Documentation of the effect: http://www.php.net/manual/en/language.references.return.php

Note: Unlike parameter passing, here you have to use & in both places - to indicate that you return by-reference, not a copy as usual, and to indicate that reference binding, rather than usual assignment, should be done for $myValue.

Correct use of get_role() everywhere:

   $role =& get_role( 'administrator' );

Incorrect, and current, use of get_role everywhere:

   $role = get_role( 'administrator' );

I changed all use of get_role to =& in schema.php and capabilities.php and the problem was solved.

This is a VERY easy and simple fix. Any chance it can get bumped?

NOTE: I also searched the code base for other return-by-reference functions. There are MANY of these. I checked a few for correct calling syntax, and 90% were wrong. (get_categor* calls). Obviously (?) it is not a big deal if the caller does not intend to modify the object.

However, any situation where the object needs to be changed means a PHP4 incompatibility.

What a pain!

11/25/08 19:55:13 changed by Mr Pete

  • cc set to jacobsantos.
  • priority changed from low to normal.

11/25/08 19:55:49 changed by Mr Pete

  • cc deleted.

oops.

11/25/08 22:54:40 changed by ryan

Let's just change "function &get_role( $role )" to "function get_role( $role )". Does doing that fix the problem for you?

11/25/08 23:40:48 changed by Mr Pete

Ummm... that can't possibly fix it ;)

Look at schema.php -- It grabs a role object, then uses it to insert a bunch of capabilities.

In PHP5, it automatically grabs a reference to the original, and all is well. Doesn't matter whether you "&" or not.

In PHP4, unless you "&" both the declaration AND use, it grabs a COPY of the object. So, the inserted capabilities never make it into the original object instantiation!

In PHP4, declaring the function with "&" means it is capable of returning a reference. Using with "&" means it actually grabs a reference. Eliminate either one, or both, and you get a copy.

Thus, to code compatibly for PHP4 and PHP5, you have to use "&" in both declaration and use.

11/25/08 23:45:48 changed by Mr Pete

(In PHP5, the harder thing is to get a copy of the original object... requires some tricky code to get a copy in a way that's compatible with both PHP4 and PHP5)

As I suggested above, if this is misunderstood in this case, (and I admit I never looked into this myself until now), I suspect this could easily be an issue elsewhere in the code base.

11/26/08 00:10:22 changed by Mr Pete

Return-by-reference is a critical issue any time the caller needs to modify the referenced object.

Here's a complete list of core function declarations that return references. I marked them based on the code comments, using a quick inspection:

* "AAA" claim to return array (reference)

* "***" claim to be capable of returning an object (reference)

* "xxx" return nothing or something else

AFAIK, only the eleven "***" functions might be a problem.

----------------------------------------
Find 'function\s*\&' in 'E:\tech\cvs\wp\temp\wp-includes\capabilities.php' :
*** E:\tech\cvs\wp\temp\wp-includes\capabilities.php(213): 	function &get_role( $role ) {
Found 'function\s*\&' 1 time(s).
----------------------------------------
Find 'function\s*\&' in 'E:\tech\cvs\wp\temp\wp-includes\category.php' :
AAA E:\tech\cvs\wp\temp\wp-includes\category.php(39): function &get_categories( $args = '' ) {
*** E:\tech\cvs\wp\temp\wp-includes\category.php(76): function &get_category( $category, $output = OBJECT, $filter = 'raw' ) {
AAA E:\tech\cvs\wp\temp\wp-includes\category.php(277): function &get_tags( $args = '' ) {
*** E:\tech\cvs\wp\temp\wp-includes\category.php(309): function &get_tag( $tag, $output = OBJECT, $filter = 'raw' ) {
Found 'function\s*\&' 4 time(s).
----------------------------------------
Find 'function\s*\&' in 'E:\tech\cvs\wp\temp\wp-includes\comment.php' :
*** E:\tech\cvs\wp\temp\wp-includes\comment.php(133): function &get_comment(&$comment, $output = OBJECT) {
AAA E:\tech\cvs\wp\temp\wp-includes\comment.php(485): function &separate_comments(&$comments) {
Found 'function\s*\&' 2 time(s).
----------------------------------------
Find 'function\s*\&' in 'E:\tech\cvs\wp\temp\wp-includes\http.php' :
*** E:\tech\cvs\wp\temp\wp-includes\http.php(90): 	function &_getTransport( $args = array() ) {
*** E:\tech\cvs\wp\temp\wp-includes\http.php(138): 	function &_postTransport( $args = array() ) {
*** E:\tech\cvs\wp\temp\wp-includes\http.php(1063): function &_wp_http_get_object() {
Found 'function\s*\&' 3 time(s).
----------------------------------------
Find 'function\s*\&' in 'E:\tech\cvs\wp\temp\wp-includes\post.php' :
*** E:\tech\cvs\wp\temp\wp-includes\post.php(116): function &get_children($args = '', $output = OBJECT) {
*** E:\tech\cvs\wp\temp\wp-includes\post.php(209): function &get_post(&$post, $output = OBJECT, $filter = 'raw') {
*** E:\tech\cvs\wp\temp\wp-includes\post.php(1891): function &get_page(&$page, $output = OBJECT, $filter = 'raw') {
AAA E:\tech\cvs\wp\temp\wp-includes\post.php(1979): function &get_page_children($page_id, $pages) {
AAA E:\tech\cvs\wp\temp\wp-includes\post.php(2054): function &get_pages($args = '') {
*** E:\tech\cvs\wp\temp\wp-includes\post.php(3446): function &wp_get_post_revision(&$post, $output = OBJECT, $filter = 'raw') {
Found 'function\s*\&' 6 time(s).
----------------------------------------
Find 'function\s*\&' in 'E:\tech\cvs\wp\temp\wp-includes\query.php' :
AAA E:\tech\cvs\wp\temp\wp-includes\query.php(59): function &query_posts($query) {
AAA E:\tech\cvs\wp\temp\wp-includes\query.php(1562): 	function &get_posts() {
AAA E:\tech\cvs\wp\temp\wp-includes\query.php(2499): 	function &query($query) {
Found 'function\s*\&' 3 time(s).
----------------------------------------
Find 'function\s*\&' in 'E:\tech\cvs\wp\temp\wp-includes\taxonomy.php' :
E:\tech\cvs\wp\temp\wp-includes\taxonomy.php(299): function &get_term($term, $taxonomy, $output = OBJECT, $filter = 'raw') {
AAA E:\tech\cvs\wp\temp\wp-includes\taxonomy.php(589): function &get_terms($taxonomies, $args = '') {
AAA E:\tech\cvs\wp\temp\wp-includes\taxonomy.php(1807): function &get_object_term_cache($id, $taxonomy) {
AAA E:\tech\cvs\wp\temp\wp-includes\taxonomy.php(1957): function &_get_term_children($term_id, $terms, $taxonomy) {
Found 'function\s*\&' 4 time(s).
----------------------------------------
Find 'function\s*\&' in 'E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\EnchantSpell.php' :
AAA E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\EnchantSpell.php(20): 	function &checkWords($lang, $words) {
AAA E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\EnchantSpell.php(49): 	function &getSuggestions($lang, $word) {
Found 'function\s*\&' 2 time(s).
----------------------------------------
Find 'function\s*\&' in 'E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\GoogleSpell.php' :
AAA E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\GoogleSpell.php(18): 	function &checkWords($lang, $words) {
AAA E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\GoogleSpell.php(36): 	function &getSuggestions($lang, $word) {
AAA E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\GoogleSpell.php(53): 	function &_getMatches($lang, $str) {
Found 'function\s*\&' 3 time(s).
----------------------------------------
Find 'function\s*\&' in 'E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\PSpell.php' :
AAA E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\PSpell.php(18): 	function &checkWords($lang, $words) {
AAA E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\PSpell.php(37): 	function &getSuggestions($lang, $word) {
AAA E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\PSpell.php(49): 	function &_getPLink($lang) {
Found 'function\s*\&' 3 time(s).
----------------------------------------
Find 'function\s*\&' in 'E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\PSpellShell.php' :
AAA E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\PSpellShell.php(18): 	function &checkWords($lang, $words) {
AAA E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\PSpellShell.php(60): 	function &getSuggestions($lang, $word) {
Found 'function\s*\&' 2 time(s).
----------------------------------------
Find 'function\s*\&' in 'E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\SpellChecker.php' :
AAA E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\SpellChecker.php(26): 	function &loopback(/* args.. */) {
AAA E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\SpellChecker.php(37): 	function &checkWords($lang, $words) {
AAA E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\classes\SpellChecker.php(48): 	function &getSuggestions($lang, $word) {
Found 'function\s*\&' 3 time(s).
----------------------------------------
Find 'function\s*\&' in 'E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\includes\general.php' :
*** E:\tech\cvs\wp\temp\wp-includes\js\tinymce\plugins\spellchecker\includes\general.php(44): function &getLogger() {
Found 'function\s*\&' 1 time(s).
----------------------------------------
Find 'function\s*\&' in 'E:\tech\cvs\wp\temp\wp-includes\Text\Diff.php' :
xxx E:\tech\cvs\wp\temp\wp-includes\Text\Diff.php(308):     function &reverse()
xxx E:\tech\cvs\wp\temp\wp-includes\Text\Diff.php(342):     function &reverse()
xxx E:\tech\cvs\wp\temp\wp-includes\Text\Diff.php(364):     function &reverse()
xxx E:\tech\cvs\wp\temp\wp-includes\Text\Diff.php(386):     function &reverse()
xxx E:\tech\cvs\wp\temp\wp-includes\Text\Diff.php(408):     function &reverse()
Found 'function\s*\&' 5 time(s).
Search complete, found 'function\s*\&' 42 time(s). (14 files.)

11/26/08 00:29:54 changed by Mr Pete

I'm browsing down quickly. Obviously the first one is affected.

Next one I see that will act differently in PHP5 and PHP4:

taxonomy.php line 114, function _cat_row()

	$category = get_category( $category, OBJECT, 'display' );
...
	$category->count = number_format_i18n( $category->count );

And the same at line 274, function link_cat_row()

In PHP4, the original category object is not changed because $category is a copy. In PHP5, the original would be changed if number_format_i18n() is anything but a nop.

I don't know if this is a problem or not. If the original should not be modified, the code is wrong for PHP5.

Every function that accepts OBJECT as a return type acts differently in PHP5 and PHP4. I don't immediately see other issues in the code base than what I've outlined here, but then I only spent a few minutes.

Again, this most likely impacts non-core code (all those "OBJECT" returns are there for a purpose, even though not used in the core code base.)

Overall, I suspect the actual core code bugs related to this are rare.

I suspect this is a potential security hole. A badly coded plugin has direct access to original objects in PHP5, because the objects (and arrays!) are returned by reference in all of the above functions.

Should this be documented? I dunno... I'm just a noob.

11/26/08 00:40:44 changed by ryan

We return by reference in places where we really don't need to. Obviously get_role() is a place where we need to. Can you create a patch that uses "& get_role()" everywhere?

11/26/08 03:01:16 changed by Mr Pete

  • attachment rolefix.patch added.

Proposed patch to fix

11/26/08 03:01:59 changed by Mr Pete

First time with SVN (I've dev'd CVS before) and first time w/ WP. Is this about right?

11/27/08 03:00:57 changed by Mr Pete

  • attachment rolefix.diff added.

Same thing with *.diff ext'n

12/12/08 20:47:19 changed by ryan

  • status changed from new to closed.
  • resolution set to fixed.

(In [10200]) Explicitly return ref for the sake of PHP4. Fixes user_level being empty when installing on PHP4. Props Mr Pete. fixes #8317 for trunk

12/12/08 20:48:09 changed by ryan

(In [10201]) Explicitly return ref for the sake of PHP4. Fixes user_level being empty when installing on PHP4. Props Mr Pete. fixes #8317 for 2.7

12/12/08 21:23:25 changed by ryan

  • milestone changed from 2.8 to 2.7.1.