Saturday, November 29, 2008

Enterprise Guts slides up, with CSS animations

I just realized that I didn't post the final version of my slides from my talk at ZendCon, but now I have: Digging Through the Guts of Enterprise PHP. However, they will only work in Safari, since I hacked CSS transitions into S5. Different builds of WebKit (and different releases of Safari) will have varying degrees of support for everything used in there (sliding, fading, and flipping). For the talk itself, I ended up having to go back a couple of weeks in the WebKit builds, since they broke a few things... Part of the fun of using experimental technology in a nightly build, though.

As a bonus, this means that the slides (mostly) work in the iPhone/iPod Touch version of Safari:

Due to how S5 triggers moving to the next slide, you just need to bring up the control bar at the bottom so you can tap on the arrows to move forward. In the last bit there, the animation of two simultaneous transforms, flipping the slides around, caused what we software engineers refer to as a "rendering freak out" followed by mobile Safari freezing up. All in due time!

Labels: , , , , , , , ,

Tuesday, September 02, 2008

S5 with CSS Slide Transitions in WebKit

So, in an effort to have the ability to post my slides for my Zend/PHP Conference talk online without having to rely on Flash or typically awkward HTML exports from other formats, I've decided to go with S5, as mentioned previously. I've also decided to add a little of WebKit's recent additions of CSS transforms and animation support for transitions.

Since CSS CSS animation only occurs when attributes change, it just means that the current and next slides need to have the changes applied at different times, or at the same time, depending. To make managing these things easier, I started with a base object:

function Transition() { }
Transition.prototype = {
 "duration" : 1,
 "cslide" : null,
 "nslide" : null,
 "transition" : function(toggle) {
  this.cslide.style.WebkitTransition =
   this.nslide.style.WebkitTransition =
    (toggle
     ? 'all ' + this.duration + 's ease-in-out'
     : 'all 0s none');
 },
 "prepare" : function() { },
 "perform" : function() { },
 "cleanup" : function() { },
 "run" : function(cid, nid) {
  this.cslide = document.getElementById(cid);
  this.nslide = document.getElementById(nid);
  this.cslide.style.visibility = this.nslide.style.visibility = 'visible';
  this.transition(false);
  this.prepare();
  var dis = this;
  setTimeout(function() {
   dis.transition(true);
   setTimeout(function() { dis.perform.apply(dis); }, 0);
  }, 0);
  setTimeout(function() {
   dis.transition(false);
   dis.cslide.style.visibility = 'hidden';
   dis.cleanup.apply(dis);
  }, dis.duration * 1000);
 }
}

The code managing the switch of the slides now calls RunTransition(transitions[snum], cid, nid);, where transitions just holds an array of constructor references corresponding to the slide numbers (snum):

var transitions = [
 null,
 Fade,
 Wipe,
 Rotate,
 Rotate,
 Fade
]

function RunTransition(className, cid, nid) {
 var trans = new className();
 trans.run.call(trans, cid, nid);
}

Each transition just extends Transition and overrides the applicable methods in order to change the slide CSS properties at the correct time, like this Fade transition:

function Fade() { }
Fade.prototype = new Transition;
Fade.prototype.prepare = function() {
 this.cslide.style.opacity = '1';
 this.nslide.style.opacity = '0';
}
Fade.prototype.perform = function() {
 this.cslide.style.opacity = '0';
 this.nslide.style.opacity = '1';
}

I've uploaded a very quick demo of the transitions in action, which will work in whichever builds of WebKit support CSS transforms and animation. It worked a bit smoother in WebKit builds a few weeks ago than those current (bugs galore at the moment), but nightly builds always tend introduce unpredictable behavior.

Labels: , , , , ,

Monday, June 02, 2008

I couldn't resist one named "Tendrils"

While setting up the most recent addition to the house (my wife's new hi-res 17" MacBook Pro), I had a little time during Windows' installation into its VMWare image, so I took a look through some more of the visualizations. I saw this one, and ported it to JavaScript, adding a few lines to animate it:

13 long, winding tendrils starting from a central base

I recommend sticking to a low number of tendrils (especially in Opera, which gets choppy rather quickly), or the non-animated version.

Based on Tendrils, Created by Tom De Smedt (which Tom based on the "Tendrils" algorithm by ART+COM).

Labels: , ,

Saturday, May 31, 2008

Fun with visualizations

Looking through MacResearch, I saw NodeBox, "...a Mac OS X application that lets you create 2D visuals (static, animated or interactive) using Python programming code and export them as a PDF or a QuickTime movie." A bit of a hobbyist math geek, I checked it out a bit and realized that a lot of the Python source code used with NodeBox looks rather similar to JavaScript interacting with the canvas HTML5 element.

So I took a few and wrote a quick library to behave similarly to NodeBox, albeit for JavaScript to run in a browser. I just looked at the parametric equation examples for something easy to start off with, and wrote a few examples. The first, extremely simple example more or less just draws a circle within certain bounds:

function CircleEquation(dt, steps, r) {
 Frozen.Chart.Equation.call(this, steps, dt);
 this.r = r;
}
CircleEquation.prototype = new Frozen.Chart.Equation;
CircleEquation.prototype.next = function() {
 this.x = Math.cos(this.t) * this.r;
 this.y = Math.sin(this.t) * this.r;
 return Frozen.Chart.Equation.prototype.next.apply(this);
};

function circleTest1() {
 chart.clear();
 chart.map.lineWidth = 0.5;
 chart.renderEquation(new CircleEquation(4.5, 75, 95));
}

This results in the following visualization:

A spiral design made up of thin lines making up a 100 pixel circle

You can see it in action, along with a couple of slightly more interesting examples. NodeBox can do a lot more than simple charting, this part of its functionality just inspired a fun exercise in web+math geekery.

Labels: , ,

Sunday, June 17, 2007

Frozen Toolkit

So the toolkit came out of the book, and the toolkit basically exists primarily for education purposes. It provides an object-oriented abstraction of the XMLHttpRequest object, as well as providing a method of easily implementing event-driven application development. Throughout the book, the toolkit has grown, and by the final appendix (currently in writing) it meets OpenAjax compliance.

The toolkit has minimal documentation at this point, and zero examples (outside of the book, of course), but I rather like it, as it allows the use of Ajax connections, with support for various HTTP response codes, event dispatching available for extension, and external JavaScript file loading. However, I do have a bit of a bias, since I wrote it. The API will probably change quite a bit before it settles into itself, but it has at least stabilized to the point that I'll post a link to the current source files, consolidated and compressed source, and JSDoc2-generated docs.

Frozen Toolkit

Labels: , ,

Sunday, May 27, 2007

A PDOIterator class using SPL

While writing about the scalability issues of using function calls like file_get_contents and PDOStatement::fetchAll, I wrote the following PDOIterator class, which I have every intention of using for a while. It makes it very easy to keep queries in the application logic while keeping the retrieval of the results in the output, without using any database driver calls. Because it implements the Iterator interface from SPL, the template can use a foreach loop just like it would if the application had passed the results from PDOStatement::fetchAll.

class PDOIterator implements Iterator {
 /**
  * The PDO connection object
  */
 protected $database;
 protected $statement;
 /**
  * The query to run on the first iteration
  */
 protected $query;
 /**
  * Optional parameters to use for prepared statements
  */
 protected $parameters;
 /**
  * The current record in the results
  */
 protected $current;
 /**
  * The row number of the current record
  */
 protected $key;
 /**
  * A boolean as to whether it has more results
  */
 protected $valid;

 /**
  * Forward-only cursor assumed and enforced
  */
 public function rewind() {
  return false;
 }
 
 public function current() {
  if ($this->key === -1) {
   if (!$this->runQuery()) {
    $this->valid = false;
    return false;
   } else {
    $this->next();
   }
  }
  return $this->current;
 }
 
 public function key() {
  return $this->key;
 }
 
 public function next() {
  $this->current = $this->statement->fetch(PDO::FETCH_ASSOC);
  if ($this->current) {
   $this->key++;
   if (!$this->valid) {
    $this->valid = true;
   }
   return true;
  } else {
   $this->statement = null;
   $this->valid = false;
   return false;
  }
 }
 
 protected function runQuery() {
  $this->statement = $this->database->prepare($this->query);
  $this->statement->execute($this->parameters);
 }
 
 public function valid() {
  return $this->valid;
 }
 
 public function setParameters($params) {
  $this->parameters = $params;
 }
 
 public function __construct($database, $query) {
  $this->database = $database;
  $this->query = $query;
  $this->parameters = null;
  $this->current = null;
  $this->key = -1;
  $this->valid = true;
 }
}

The following two code chunks show how to use this class in the application logic, and then later on when displaying the results of the query, assuming an already open connection in $database, an instance of the MySQL flavor of PDO:

// First, run the query and get the list
$query = 'SELECT `id`, `name` FROM `users` ORDER BY `name`';
$users = new PDOIterator($database, $query);

<!-- Later in the application, output the list -->
<ol>
<?php foreach ($users as $user) { ?>
 <li><a href="?id=<?php echo (int)$user['id']; ?>">
  <?php echo Utilities::escapeXMLEntities($user['name']); ?>
 </a></li>
<?php } ?>
</ol>

The query doesn't actually run until the first iteration, so the database connection does not have any open statement until it starts the iterations. Once complete, it sets the statement to null, freeing up the connection for other queries, without having to clutter any of the application code with PDO-specific calls.

This could easily extend to other database drivers (like the mysqli-specific driver, for example), but since PDO stands to have a large rate of adoption (and since I like it), I used it instead.

Labels: ,

Saturday, May 26, 2007

Intro to Chapter 10: Game Development

I do hope to finish this game since I have it quite close, but I need to finish the book first. I considered using sprites for the ships, star, and missiles, but decided against it to practice rotation and see what canvas could handle. Also, the original version didn't exactly use image sprites, so I stuck with entirely two-dimensional shapes in canvas. Anyway, on with a couple of paragraphs:

Ajax-driven game development brings together the challenges of scalability and performance, but often allows developers to push the boundaries of current web technologies. Just as with console or computer games, users will put up with stricter minimum requirements in order to have the better experience with the more advanced technologies available, properly used.

This chapter will focus on Universe Conflict, an implementation of Space War!, one of the first digital computer games, created in 1961 on the PDP-1 computer, as recreated using the canvas HTML5 element and Ajax (shown below). This allows the two players to battle each other from different machines, as opposed to the same one as in the original and ports since then. The game has very simple rules and a simple setup. Two ships, each controlled by a user, try to shoot each other without getting pulled into a star in the center of the screen.

Two ships poised to attach each other around a star

Sidenote: you can see the rendering in action in this simple demo of Universe Conflict, which just allows you to click a button and fly each ship around the screen using the arrow keys. Not terribly exciting, since I took the networking bits out, haven't created gravity, and only works in Opera and Firefox for now (Safari has some flickering and keyboard issues that I just haven't bothered to fix yet), but it should give a decent idea of how canvas can handle animation. I haven't tested this a whole lot, so if it sucks for you, let me know.

Labels: , ,

Tuesday, May 08, 2007

Practice with the canvas tag: cellular automata

I decided to do a quick study on the canvas tag (which I'll use in the chapter on game development) before diving into the next chapter, so I ported an old PHP script that generated one-dimensional cellular automata to a JavaScript and <canvas> cellular automata display. I recommend numbers 30, 60, 182, and 225 (see Elementary Cellular Automaton on Mathworld for more, including previews of all 256 of them).

This obviously will not work in IE, and I have it defaulting to rendering them line by line so that it doesn't seize up Firefox and Safari for a few seconds (depending on which one you pick) while it draws it out. Opera displays it incredibly quickly and seems the only browser to have threaded rendering rather than locking up the browser until it finishes when drawing it all at once.

Labels: , ,

Saturday, May 05, 2007

Support for different HTTP status codes with Ajax

While writing about adding support for different status codes into the AjaxRequest object (very easy, as I'll show momentarily), I made a little test page to try each of the initial set: HTTP Status Codes. Just click each link (check Firebug) and it optionally fetches all, part, or none of a 400-line file.

Now on to the excerpt:

Most browsers and XML feed readers take full advantage of the HTTP/1.1 specification to greatly reduce the data sent from the server, and the XMLHttpRequest object gives developers access to the same functionality. Two intertwined aspects of the HTTP/1.1 specification in particular can help Ajax-driven applications: status codes and cache-control.

Supporting these in the previously defined AjaxRequest object comes easily. It already supports setting custom headers through its headers object, just like setting GET and POST variables. In order to support the various status codes that the server can return, it just takes adding event types and adding some more flexibility to the stateChanged() method by changing it from:

// Event dispatching
AjaxRequest.prototype.events = {
  abort : [],
  data : [],
  load : [],
  open : [],
  send : [],
 };

// Callback for this.xhr.onreadystatechanged
AjaxRequest.prototype.stateChanged = function() {
 // Only trigger load if finished returning
 switch(this.xhr.readyState) {
  case 3:
   var e = new AjaxEvent(this);
   this.dispatchEvent('data', e);
   break;
  case 4:
   // Only continue if status OK
   if (this.xhr.status == 200) {
    var e = new AjaxEvent(this);
    this.dispatchEvent('load', e);
   }
 }
}

...to a new version that can handle multiple status codes:

// Event dispatching
AjaxRequest.prototype.events = {
  abort : [],
  data : [],
  internalservererror : [],
  load : [],
  notfound : [],
  notmodified : [],
  open : [],
  partialload : [],
  requestedrangenotsatisfiable : [],
  send : [],
  unauthorized : []
 };
// Simple lookup of event types by status code
AjaxRequest.prototype.statusCodeEvents = {
  200 : 'load',
  206 : 'partialload',
  304 : 'notmodified',
  401 : 'unauthorized',
  404 : 'notfound',
  416 : 'requestedrangenotsatisfiable',
  500 : 'internalservererror'
 };
// Callback for this.xhr.onreadystatechanged
AjaxRequest.prototype.stateChanged = function() {
 // Only trigger load if finished returning
 switch(this.xhr.readyState) {
  case 3:
   var e = new AjaxEvent(this);
   this.dispatchEvent('data', e);
   break;
  case 4:
   if (this.statusCodeEvents[this.xhr.status]) {
    var e = new AjaxEvent(this);
    this.dispatchEvent(this.statusCodeEvents[this.xhr.status], e);
   }
 }
}

The new implementation of stateChanged() now simply triggers the event mapped to the returned status code from the request, if the AjaxRequest object implements that event type. While the list of status codes only includes the most commonly used codes for this chapter's usage, it can include any additional codes added to the events and statusCodeEvents objects.

Labels: , , , ,

Sunday, April 22, 2007

Note on JavaScript prototypes and object variables

This makes complete sense, once you think about it, but I didn't at first and it bit me.

>>> function Obj() { }
>>> Obj.prototype.position = { "x" : 0 , "y" : 0 };
Object x=0 y=0
>>> var a = new Obj();
>>> var b = new Obj();
>>> a.position.x = 10;
10
>>> b.position.x
10

This Firebug console output shows writing a simple object, Obj, and setting a prototype variable position to an object literal having two member variables, x and y, both initially set to 0. Since object instances simply reference their prototype variables when set to objects (Array, Object, Function, etc.), updating instance a's x position will update instance b's x position as well.

In order to get around this, while still setting aside a spot for the position variable in the Obj prototype, assign the value in the constructor like this:

>>> function Obj() { this.position = { "x" : 0 , "y" : 0 }; }
>>> Obj.prototype.position = null;
null
>>> var a = new Obj();
>>> var b = new Obj();
>>> a.position.x = 10;
10
>>> b.position.x
0

It doesn't look as nice, and requires that you call the parent constructor when you extend the object (always a fun process with OO JavaScript), but it works much better this way.

Labels: ,

Saturday, December 16, 2006

Updated sample and ajax.lib.js

The quick example now has some basic formatting and a link to run it.

More interestingly, from my perspective at least, I've updated the AjaxRequest and AjaxRequestManager objects.

The AjaxRequest object now has four events (open, send, load, and abort), and now has an actually readable onreadystatechange listener by using a quick trick in order to keep this references intact:

this.xhr.onreadystatechange = new Function("AjaxRequest.prototype.stateChanged.apply(requests["+id+"], arguments);");

The AjaxRequestManager now emulates support for event listening, in that it can accept addEventListener and removeEventListener calls. But instead of dispatching events, it auto-adds the listeners to each of the AjaxRequest objects it creates. This makes it a lot easier to write, for example, a Throbber object, since it can get added as a listener to each request in order to keep an eye on things while keeping things nice and loosely coupled.

Edited so you can read the code...

Labels: , ,

Friday, December 15, 2006

Current state of the Ajax library

A little large to post here, but you can check out the source and an example, where the usage boils down to:

this.request = request_manager.createAjaxRequest();
// For this example, this does nothing, but demonstrates how to set GET parameters
this.request.get = {
  one : 1,
  two : 2
 };
this.request.addEventListener('load', new Array(this, this.ran));
this.request.open('GET', 'xml.php');
return this.request.send();

In order to run the code, a simple new SimpleDemo().run(); will do the trick from Firebug (side note: sample only tested from Firefox - let me know how it fares in other browsers).

Basically, I need to ensure that usage like this makes things easier to read and port to other libraries. I honestly don't care if anyone actually uses this library, I just want the usability and abstraction to get through. The Ajax portion weighs in at just under seven kilobytes, including white-space, comments, and full names so you can actually read it, and I have every intention on keeping it that way. I've included a reference to Firebug Lite, just in case you don't have the full version installed.

Edit: okay, so the source code viewing works in Opera, but not Safari, and calling run(); from Firebug Lite pretty much does squat in either of them. Editing will commence, but I will still greatly appreciate any feedback and criticism.

Labels: , ,

Saturday, December 09, 2006

Contextually dispatching events

I just realized that I could implement EventDispatcher.prototype.dispatchEvent as (difference emphasized):

dispatchEvent : function(type, event) {
 if (this.events[type]) {
  for (var i in this.events[type]) {
   if (typeof this.events[type][i] == 'function') {
    this.events[type][i](event);
   // Accepts an array of the contextual object and the function to call
   } else if (typeof this.events[type][i] == 'object') {
    this.events[type][i][1].call(this.events[type][i][1], event);
   }
  }
 }
}

This makes it much easier to keep this references happy and I can't believe I didn't think of that until just now, since I used almost the same code to call parent object methods from a child that had overridden the method. So now you can create listeners from within object methods like this:

myeventdispatcher.addEventListener('load', new Array(this, this.myLoadEventListener));

or this:

myeventdispatcher.addEventListener('load', new Array(this, MyObject.prototype.myLoadEventListener));

Side note: I also realize I need to find a way of posted syntax-highlighted code consistently and easily. The code I had posted on Simple Event Generation came from copying the source into Kate, exporting the highlighted code as HTML, and pasting the mass of spans into Blogger.

Labels: , ,

Monday, December 04, 2006

Simple event generation

In an effort to keep each chapter and its included code samples as relevent as possible to the context around it, I decided that a toolkit made the most sense. I also decided to simple write the toolkit and include it for the following reasons:

  1. I haven't used any of the ones out there yet for long enough to use them to correctly accomplish exactly what I need without coding the samples specifically for that particular toolkit.
  2. I needed a toolkit anyway for other things I work on and (since I'll release this under the BSD License), I can write it and use it anywhere.
  3. The toolkit will only need to cover a comparatively limited set of features, so I won't re-write all of Dojo, for example.
  4. Incuding code samples like the following reinforce the overall method of development.

The following implements very simple, yet effective, event listening. Just extend the EventDispatcher with whatever object has events (form handlers save, fail, delete, load, clear, etc.) and extend the CustomEvent object to fit your needs. This gets used extensively throughout the book in order to simplify the objects through encapsulation.

function CustomEvent() { }
CustomEvent.prototype = {
 type : 'custom'
}

// Custom EventTarget equivalent
function EventDispatcher() { }
EventDispatcher.prototype = {
 // An object literal to store arrays of listeners by type
 events : {},
 
 // If it supports the type, add the listener (capture ignored)
 addEventListener : function(type, listener, capture) {
  if (this.events[type]) {
   this.events[type].push(listener);
  }
 },
 
 // If it supports the type, remove the listener (capture ignored)
 removeEventListener : function(type, listener, capture) {
  if (this.events[type] == undefined) {
   return;
  }
  var index = this.events[type].indexOf(listener);
  if (this.events[type][index]) {
   this.events[type].splice(index, 1);
  }
 },
 
 // Cycle through all of the event listeners, passing the event to the callbacks
 dispatchEvent : function(type, event) {
  if (this.events[type]) {
   for (var i in this.events[type]) {
    this.events[type][i](event);
   }
  }
 }
}

Edited so you can read the code.

12/8/06 - Edited to fix dispatchEvent to actually work.

Labels: , ,

Saturday, October 21, 2006

Hacking s9y into a multi-user, centrally controlled tool, continued

Now that authors can only manage their own posts and have everything managed centrally, I just have to display filtered results, so each author has their own main page. Since s9y already has a way to filter by author, we just need to trigger that without the full query having to appear in the URL request.

The index.php in whatever directory you want. For this install, I'll have it as /username/index.php.

// Get the journal contents for the current journal
$_SERVER['REQUEST_URI'] .= '?/authors/(authorid)-(Hyphenated-Full-Name)';
require 'includes/journals.php';

Then, journals.php simple follows the standard, embedding practice for s9y:

$pwd = dirname(__FILE__);
chdir($p . 'journals/');
include 'index.php';
chdir($pwd);
Since my own code already logs you into s9y and, in doing so, starts the session, I just need to quiet the notice that will otherwise pepper the error log from serendipity_config.inc.php:

Index: serendipity_config.inc.php
===================================================================
--- serendipity_config.inc.php  (revision 162)
+++ serendipity_config.inc.php  (working copy)
@@ -3,7 +3,7 @@
 # All rights reserved.  See LICENSE file for licensing details
 
 if (!headers_sent()) {
-    session_start();
+    @session_start();
 }

Since s9y didn't get built with this kind of behavior in mind, this definitely falls into the realm of sketchiness. But it works solidly and since any distribution of this will get included with my own web application (source code and license for s9y included, of course), only I have to worry about upgrade paths.

Until, of course, they do this for real.

Labels:

Hacking s9y into a multi-user, centrally controlled tool

Warning: I did these changes under my own source control tree so I could roll back changes and keep track of things. Don't do this unless you either know exactly what this means and does, or if you actively want to break your own installation of s9y. I've included svn diffs of my changes to s9y version 1.0 in order to make this work.

Once I got my own authentication to create the session for s9y to allow authors in for posting and editing, I next had to make s9y do two things different from its normal behavior for base-level authors:

  1. Keep authors from editing their information that gets managed by my own admin tool.
  2. Allow authors to only edit their own entries

First off, authors only have the ability to edit their info using the tool I wrote, since it also updates their central user record, as well as the Phorum user record. Luckily, s9y makes this practically as easy as pushing a button. Just a couple of quick permission changes in s9y/include/tpl/config_personal.inc.php:

Index: config_personal.inc.php
===================================================================
--- config_personal.inc.php     (revision 115)
+++ config_personal.inc.php     (working copy)
@@ -13,28 +13,28 @@
                                           'description' => USERCONF_USERNAME_DESC,
                                           'type'        => 'string',
                                           'default'     => 'johndoe',
-                                          'permission'  => 'personalConfiguration'),
+                                          'permission'  => 'adminUsersMaintainOthers'),
 
                                     array('var'         => 'password',
                                           'title'       => USERCONF_PASSWORD,
                                           'description' => USERCONF_PASSWORD_DESC,
                                           'type'        => 'protected',
                                           'default'     => '',
-                                          'permission'  => 'personalConfiguration'),
+                                          'permission'  => 'adminUsersMaintainOthers'),
 
                                     array('var'         => 'check_password',
                                           'title'       => USERCONF_CHECK_PASSWORD,
                                           'description' => USERCONF_CHECK_PASSWORD_DESC,
                                           'type'        => 'protected',
                                           'default'     => '',
-                                          'permission'  => 'personalConfiguration'),
+                                          'permission'  => 'adminUsersMaintainOthers'),
 
                                     array('var'         => 'realname',
                                           'title'       => USERCONF_REALNAME,
                                           'description' => USERCONF_REALNAME_DESC,
                                           'type'        => 'string',
                                           'default'     => 'John Doe',
-                                          'permission'  => 'personalConfiguration'),
+                                          'permission'  => 'adminUsersMaintainOthers'),
 
                                     array('var'         => 'userlevel',
                                           'title'       => USERCONF_USERLEVEL,
@@ -57,7 +57,7 @@
                                           'description' => USERCONF_EMAIL_DESC,
                                           'type'        => 'string',
                                           'default'     => 'john@example.com',
-                                          'permission'  => 'personalConfiguration'),
+                                          'permission'  => 'adminUsersMaintainOthers'),
 
                                     array('var'         => 'lang',
                                           'title'       => INSTALL_LANG,

This way, admins can still go in and fix things without much fuss if something manages to somehow fall out of sync with the central record for some reason.

Next, authors shouldn't have the ability to edit other authors' entries for this site. That means that s9y/include/admin/entries.inc.php has a few quick changes:

Index: entries.inc.php
===================================================================
--- entries.inc.php (revision 115)
+++ entries.inc.php (working copy)
@@ -59,7 +59,9 @@
 
     $filter = array();
 
-    if (!empty($serendipity['GET']['filter']['author'])) {
+    if (!serendipity_checkPermission('adminEntriesMaintainOthers')) {
+               $filter[] = "e.authorid = '" . serendipity_db_escape_string($serendipity['authorid']) . "'";
+       } else if (!empty($serendipity['GET']['filter']['author'])) {
         $filter[] = "e.authorid = '" . serendipity_db_escape_string($serendipity['GET']['filter']['author']) . "'";
     }
 
@@ -110,7 +112,10 @@
         <tr>
             <td valign="top" width="80"><?php echo AUTHOR ?></td>
             <td valign="top">
-                <select name="serendipity[filter][author]">
+<?php
+                      if (serendipity_checkPermission('adminEntriesMaintainOthers')) {
+?>
+                      <select name="serendipity[filter][author]">
                     <option value="">--</option>
 <?php
                     $users = serendipity_fetchUsers();
@@ -119,7 +124,16 @@
                             echo '<option value="' . $user['authorid'] . '" ' . (isset($serendipity['GET']['filter']['author']) && $serendipity['GET']['filter']['author'] == $user['authorid'] ? 'selected="selected"' : '') . '>' . $user['realname'] . '</option>' . "\n";
                         }
                     }
-?>              </select> <select name="serendipity[filter][isdraft]">
+?>
+                      </select>
+<?php
+                      } else {
+?>
+                       <?php echo htmlentities($serendipity['user']); ?>
+                       <?php
+                      }
+?>
+                      <select name="serendipity[filter][isdraft]">
                     <option value="all"><?php echo COMMENTS_FILTER_ALL; ?></option>
                     <option value="draft"   <?php echo (isset($serendipity['GET']['filter']['isdraft']) &&  $serendipity['GET']['filter']['isdraft'] == 'draft' ? 'selected="selected"' : ''); ?>><?php echo DRAFT; ?></option>
                     <option value="publish" <?php echo (isset($serendipity['GET']['filter']['isdraft']) &&  $serendipity['GET']['filter']['isdraft'] == 'publish' ? 'selected="selected"' : ''); ?>><?php echo PUBLISH; ?></option>

Basically, this just looks at whether or not you have permission to edit others. If you don't, it automatically filters by your authorid and doesn't allow you to view any others. If you try to save an entry owned by someone else, and you do cannot adminEntriesMaintainOthers, you'll get an error anyway, but it looks a lot cleaner if you can't open them up for editing in the first place.

*edited for formatting fixes

Labels: