2 /***************************************************************************
4 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
5 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
6 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
7 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
11 * Copyright (C) 2009 Jonas Häggqvist
13 * This program is free software; you can redistribute it and/or
14 * modify it under the terms of the GNU General Public License
15 * as published by the Free Software Foundation; either version 2
16 * of the License, or (at your option) any later version.
18 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
19 * KIND, either express or implied.
21 ****************************************************************************/
23 require_once('db.class.php');
27 private $themedir_public;
28 private $themedir_private;
30 public function __construct($dbfile) {
31 $this->db
= new db($dbfile);
32 $this->themedir_public
= sprintf("%s/%s/%s", $_SERVER['DOCUMENT_ROOT'], config
::path
, config
::datadir
);
33 $this->themedir_private
= sprintf("%s/%s", preconfig
::privpath
, config
::datadir
);
37 * Log a message to the log table. Time, IP and admin user (if any)
38 * is automaticly added.
40 private function log($message) {
41 $sql_f = "INSERT INTO log (time, ip, admin, msg) VALUES (datetime('now'), '%s', '%s', '%s')";
42 $sql = sprintf($sql_f,
43 $_SERVER['REMOTE_ADDR'],
44 isset($_SESSION['user']) ? db
::quote($_SESSION['user']) : '',
47 $this->db
->query($sql);
50 private function targetlist($orderby) {
51 $sql = "SELECT shortname, fullname, pic, mainlcd, depth, remotelcd FROM targets ORDER BY " . $orderby;
52 return $this->db
->query($sql);
55 public function listtargets($orderby = 'fullname ASC') {
56 $targets = $this->targetlist($orderby);
58 while ($target = $targets->next()) {
65 * Run checkwps on all our themes
67 public function checkallthemes() {
68 $this->log("Running checkwps");
69 $sql = "SELECT RowID, * FROM themes";
70 $themes = $this->db
->query($sql);
72 while ($theme = $themes->next()) {
73 $starttime = microtime(true);
74 $zipfile = sprintf("%s/%s/%s/%s",
80 $result = $this->checkwps($zipfile, $theme['mainlcd'], $theme['remotelcd']);
83 * Store the results and check if at least one check passed (for
87 foreach($result as $version_type => $targets) {
88 foreach($targets as $target => $result) {
89 if ($result['pass']) $passany = true; /* For the summary */
91 * Maybe we want to have two tables - one with historic
92 * data, and one with only the latest results for fast
95 $this->db
->query(sprintf("DELETE FROM checkwps WHERE themeid=%d AND version_type='%s'", $theme['RowID'], db
::quote($version_type)));
96 $sql = sprintf("INSERT INTO checkwps (themeid, version_type, version_number, target, pass) VALUES (%d, '%s', '%s', '%s', '%s')",
98 db
::quote($version_type),
99 db
::quote($result['version']),
101 db
::quote($result['pass'] ?
1 : 0)
103 $this->db
->query($sql);
109 'summary' => array('theme' => $theme['name'], 'pass' => $passany, 'duration' => microtime(true) - $starttime)
115 public function adminlogin($user, $pass) {
116 $sql = sprintf("SELECT COUNT(*) as count FROM admins WHERE name='%s' AND pass='%s'",
118 db
::quote(md5($pass))
120 $result = $this->db
->query($sql)->next();
121 return $result['count'] == 1 ?
true : false;
124 public function listthemes($mainlcd, $orderby = 'timestamp DESC', $approved = 'approved', $onlyverified = true) {
128 $approved_clause = "";
131 $approved_clause = " AND approved = 0 ";
135 $approved_clause = " AND approved = 1 ";
138 if ($onlyverified == true) {
139 $verified = " AND emailverification = 1 ";
144 $sql = sprintf("SELECT name, timestamp, mainlcd, approved, reason, description, RowID as id, shortname, zipfile, sshot_wps, sshot_menu, emailverification = 1 as verified FROM themes WHERE 1 %s %s AND mainlcd='%s' ORDER BY %s",
150 $themes = $this->db
->query($sql);
151 while ($theme = $themes->next()) {
152 $theme['size'] = filesize(sprintf("%s/%s/%s/%s",
153 $theme['approved'] == 1 ?
$this->themedir_public
: $this->themedir_private
,
163 public function target2lcd($shortname) {
164 $sql = sprintf("SELECT mainlcd, remotelcd, depth FROM targets WHERE shortname='%s'",
165 db
::quote($shortname)
167 return $this->db
->query($sql)->next();
170 public function themenameexists($name, $mainlcd) {
171 $sql = sprintf("SELECT COUNT(*) as count FROM themes WHERE name='%s' AND mainlcd='%s'",
175 $result = $this->db
->query($sql)->next();
176 return $result['count'] > 0 ?
true : false;
179 public function changestatus($themeid, $newstatus, $oldstatus, $reason) {
180 $status_text = array('1' => 'Approved', '0' => 'hidden', '-1' => 'deleted');
181 $this->log(sprintf("Changing status of theme %d from %s to %s - Reason: %s",
183 $status_text[$oldstatus],
184 $status_text[$newstatus],
187 $sql = sprintf("SELECT shortname, mainlcd, email, name, author, zipfile FROM themes WHERE RowID='%d'", db
::quote($themeid));
188 $theme = $this->db
->query($sql)->next();
190 if ($newstatus == -1) {
191 $sql = sprintf("DELETE FROM themes WHERE RowID='%d'",
195 /* Delete the files */
196 foreach(array($this->themedir_public
, $this->themedir_private
) as $root) {
197 $dir = sprintf("%s/%s/%s",
202 if (file_exists($dir)) {
203 foreach(glob(sprintf("%s/*", $dir)) as $file) {
211 $sql = sprintf("UPDATE themes SET approved='%d', reason='%s' WHERE RowID='%d'",
212 db
::quote($newstatus),
216 $from = sprintf("%s/%s/%s/%s", $this->themedir_public
, $theme['mainlcd'], $theme['shortname'], $theme['zipfile']);
217 $to = sprintf("%s/%s/%s/%s", $this->themedir_private
, $theme['mainlcd'], $theme['shortname'], $theme['zipfile']);
218 if ($newstatus == 1) {
225 if ($oldstatus == 1 && $newstatus < 1) {
226 // Send a mail to notify the user that his theme has been
227 // hidden/deleted. No reason to distinguish, since the result
228 // for him is the same.
229 $to = sprintf("%s <%s>", $theme['author'], $theme['email']);
230 $subject = sprintf("Your theme '%s' has been removed from %s", $theme['name'], config
::hostname
);
232 Your theme {$theme['name']} was removed from the Rockbox theme site. The
233 following reason should explain why:
239 If you think this was a mistake, or disagree with the decision, contact the
240 theme site admins in the Rockbox Forums or on IRC.
242 $this->send_mail($subject, $to, $msg);
244 $this->db
->query($sql);
247 public function addtarget($shortname, $fullname, $mainlcd, $pic, $depth, $remotelcd = false) {
248 $this->log(sprintf("Add new target %s", $fullname));
250 $sql = sprintf("INSERT INTO targets
251 (shortname, fullname, mainlcd, pic, depth, remotelcd)
253 ('%s', '%s', '%s', '%s', '%s', %s)",
254 db
::quote($shortname),
255 db
::quote($fullname),
259 $remotelcd === false ?
'NULL' : sprintf("'%s'", db
::quote($remotelcd))
261 $this->db
->query($sql);
262 /* Create the target's dir in both the private and public theme dir */
263 foreach(array($this->themedir_public
, $this->themedir_private
) as $root) {
264 $themedir = sprintf("%s/%s", $root, $mainlcd);
265 if (!file_exists($themedir)) {
271 private function send_mail($subject, $to, $msg) {
272 $msg = wordwrap($msg, 78);
273 $headers = 'From: themes@rockbox.org';
274 mail($to, $subject, $msg, $headers);
277 public function validatetheme($zipfile) {
282 public function prepareverification($id, $email, $author) {
283 $token = md5(uniqid());
284 $sql = sprintf("UPDATE themes SET emailverification='%s' WHERE RowID='%s'",
288 $this->db
->query($sql);
289 $url = sprintf("%s%s/verify.php?t=%s", config
::hostname
, config
::path
, $token);
290 /* xxx: Someone rewrite this message to not sound horrible */
292 Hello, you just uploaded a Rockbox theme and now we need you to verify your
293 email address. To do this, simply open the link below in your browser. You
294 may have to copy/paste the text into your browser's location bar in some cases.
298 Thank for your contributions
300 The Rockbox Theme Site team.
302 /* ' (this is here to keep my syntax hilighting happy) */
303 $subject = "Rockbox Theme Site email verification";
304 $to = sprintf("%s <%s>", $author, $email);
305 $this->send_mail($subject, $to, $msg);
308 public function verifyemail($token) {
309 $sql = sprintf("UPDATE themes SET emailverification=1 WHERE emailverification='%s'",
312 $res = $this->db
->query($sql);
313 return $res->rowsaffected();
316 public function addtheme($name, $shortname, $author, $email, $mainlcd, $remotelcd, $description, $zipfile, $sshot_wps, $sshot_menu) {
318 /* return array("Skipping upload"); */
320 /* Create the destination dir in both private and public area */
321 foreach(array($this->themedir_public
, $this->themedir_private
) as $root) {
322 mkdir(sprintf("%s/%s/%s",
329 /* This is the actual destination dir */
330 $destdir = sprintf("%s/%s/%s",
331 config
::defaultstatus
== 1 ?
$this->themedir_public
: $this->themedir_private
,
336 /* Prepend wps- and menu- to screenshots */
337 $sshot_wps['name'] = empty($sshot_wps['name']) ?
'' : 'wps-'.$sshot_wps['name'];
338 $sshot_menu['name'] = empty($sshot_menu['name']) ?
'' : 'menu-'.$sshot_menu['name'];
340 /* Start moving files in place */
341 $uploads = array($zipfile, $sshot_wps, $sshot_menu);
342 $movedfiles = array();
343 foreach($uploads as $file) {
344 if ($file === false ||
empty($file['tmp_name'])) {
347 $dest = sprintf("%s/%s",
352 if (!@move_uploaded_file
($file['tmp_name'], $dest)) {
353 /* Upload went wrong, clean up */
354 foreach ($movedfiles as $movedfile) {
358 $err[] = sprintf("Couldn't move %s.", $file['name'], $dest);
362 $movedfiles[] = $dest;
365 $sql_f = "INSERT INTO themes (author, email, name, mainlcd, zipfile, sshot_wps, sshot_menu, remotelcd, description, shortname, emailverification, timestamp, approved) VALUES ('%s', '%s', '%s', '%s', '%s', '%s', %s, %s, '%s', '%s', 0, datetime('now'), %d)";
366 $sql = sprintf($sql_f,
371 db
::quote($zipfile['name']),
372 db
::quote($sshot_wps['name']),
373 $sshot_menu === false ?
'NULL' : sprintf("'%s'", db
::quote($sshot_menu['name'])),
374 $remotelcd === false ?
'NULL' : sprintf("'%s'", db
::quote($remotelcd)),
375 db
::quote($description),
376 db
::quote($shortname),
377 config
::defaultstatus
379 $result = $this->db
->query($sql);
380 $id = $result->insertid();
381 $check = $this->checkwps(sprintf("%s/%s/%s", config
::datadir
, $mainlcd, $zipfile['name']), $mainlcd, $remotelcd);
382 /* xxx: store these results */
383 $this->log(sprintf("Added theme %d (email: %s)", $id, $email));
388 * Use this rather than plain pathinfo for compatibility with PHP<5.2.0
390 private function my_pathinfo($path) {
391 $pathinfo = pathinfo($path);
392 /* Make sure we have the $pathinfo['filename'] element added in PHP 5.2.0 */
393 if (!isset($pathinfo['filename'])) {
394 $pathinfo['filename'] = substr(
395 $pathinfo['basename'],
397 strrpos($pathinfo['basename'],'.') === false ?
strlen($pathinfo['basename']) : strrpos($pathinfo['basename'],'.')
404 * Convenience function called from several locations
406 private function getzipentrycontents($zip, $ze) {
408 zip_entry_open($zip, $ze);
409 while($read = zip_entry_read($ze)) {
412 zip_entry_close($ze);
417 * xxx: I don't know what kind of validation is wanted for cfg files
419 public function validatecfg($cfg, $files) {
421 foreach(explode("\n", $cfg) as $line) {
422 if (substr($line, 0, 1) == '#') continue;
423 preg_match("/^(?P<name>[^:]*)\s*:\s*(?P<value>[^#]*)\s*$/", $line, $matches);
424 if (count($matches) > 0) {
434 public function lcd2targets($lcd) {
436 $sql = sprintf("SELECT shortname FROM targets WHERE mainlcd='%s' OR remotelcd='%s'",
440 $targets = $this->db
->query($sql);
441 while ($target = $targets->next()) {
442 $ret[] = $target['shortname'];
448 * Check a WPS against two revisions: current and the latest release
450 public function checkwps($zipfile, $mainlcd, $remotelcd) {
453 /* First, create a temporary dir */
454 $tmpdir = sprintf("%s/temp-%s", preconfig
::privpath
, md5(uniqid()));
457 /* Then, unzip the theme here */
458 $cmd = sprintf("%s -d %s %s", config
::unzip
, $tmpdir, escapeshellarg($zipfile));
459 exec($cmd, $dontcare, $ret);
461 /* Now, cd into that dir */
466 * For all .wps and .rwps, run checkwps of both release and current for
467 * all applicable targets
469 foreach(glob('.rockbox/wps/*wps') as $file) {
470 $p = $this->my_pathinfo($file);
471 $lcd = ($p['extension'] == 'rwps' ?
$remotelcd : $mainlcd);
472 foreach(array('release', 'current') as $version) {
473 foreach($this->lcd2targets($lcd) as $shortname) {
475 $checkwps = sprintf("%s/checkwps/%s/checkwps.%s",
476 '..', /* We'll be in a subdir of the private dir */
480 $result['version'] = trim(file_get_contents(sprintf('%s/checkwps/%s/VERSION',
485 if (file_exists($checkwps)) {
486 exec(sprintf("%s %s", $checkwps, $file), $output, $ret);
487 $result['pass'] = ($ret == 0);
488 $result['output'] = $output;
489 $return[$version][$shortname] = $result;
498 /* Remove the tempdir */
499 $this->rmdir_recursive($tmpdir);
503 private function rmdir_recursive($dirname) {
504 $dir = dir($dirname);
505 while (false !== ($entry = $dir->read())) {
506 if ($entry == '.' ||
$entry == '..') continue;
507 $path = sprintf("%s/%s", $dir->path
, $entry);
509 chmod($path, 0700); // To make sure we're allowed to delete files
510 $this->rmdir_recursive($path);
521 * This rather unwieldy function validates the structure of a theme's
522 * zipfile. It checks the following:
523 * - Exactly 1 .wps file
524 * - 0 or 1 .rwps file
525 * - Only .bmp files in /.rockbox/backdrops/ and /.rockbox/wps/<shortname>/
526 * - All files are inside /.rockbox
527 * - All .wps, .rwps and .cfg files use the same shortname, which is also
528 * the one used for the subdir in /.rockbox/wps
530 * It does not uncompress any of the files.
532 * We continue checking for errors, rather than aborting, so the uploader
533 * gets a full list of things we didn't like.
535 public function validatezip($themezipupload) {
537 $zip = zip_open($themezipupload['tmp_name']);
541 $rwpsfound = array();
546 $err[] = sprintf("Couldn't open zipfile %s", $themezipupload['name']);
549 while ($ze = zip_read($zip)) {
550 $filename = zip_entry_name($ze);
551 $pathinfo = $this->my_pathinfo($filename);
552 $totalsize +
= zip_entry_filesize($ze);
553 $files[] = $filename;
555 /* Count .wps and .rwps files for later checking */
556 if (strtolower($pathinfo['extension']) == 'wps')
557 $wpsfound[] = $filename;
558 if (strtolower($pathinfo['extension']) == 'rwps')
559 $rwpsfound[] = $filename;
561 /* Check that all files are within .rockbox */
562 if (strpos($filename, '.rockbox') !== 0)
563 $err[] = sprintf("File outside /.rockbox/: %s", $filename);
565 /* Check that all .wps, .rwps and .cfg filenames use the same shortname */
566 switch(strtolower($pathinfo['extension'])) {
568 /* Save the contents for later checking */
569 $cfg = $this->getzipentrycontents($zip, $ze);
572 if ($shortname === '')
573 $shortname = $pathinfo['filename'];
574 elseif ($shortname !== $pathinfo['filename'])
575 $err[] = sprintf("Filename invalid: %s (should be %s.%s)", $filename, $shortname, $pathinfo['extension']);
580 * Check that the dir inside /.rockbox/wps also has the same name.
581 * This automatically ensures that there is only one.
583 if ($pathinfo['dirname'] == '.rockbox/wps' && $pathinfo['extension'] == '') {
584 if ($shortname === '')
585 $shortname = $pathinfo['filename'];
586 elseif ($shortname !== $pathinfo['filename'])
587 $err[] = sprintf("Invalid dirname: %s (should be %s.)", $filename, $shortname);
591 * Check that the only files we have inside /.rockbox/backdrops/
592 * and subdirs of /.rockbox/wps/ are .bmp files
594 if (strtolower($pathinfo['extension']) != 'bmp' &&
595 ($pathinfo['dirname'] == '.rockbox/backdrops' ||
// Files inside .rockbox/backdrops
596 ($pathinfo['dirname'] != '.rockbox/wps' && strpos($pathinfo['dirname'], '.rockbox/wps') === 0) // Files in a subdir of .rockbox/wps (first part or dirname is .rockbox/wps, but it's not all of it)
599 $err[] = sprintf("Non-bmp file not allowed here: %s", $filename);
602 /* Check for paths that are too deep */
603 if (count(explode('/', $pathinfo['dirname'])) > 3) {
604 $err[] = sprintf("Path too deep: %s", $filename);
607 /* Check for unwanted junk files */
608 switch(strtolower($pathinfo['basename'])) {
613 $err[] = sprintf("Unwanted file: %s", $filename);
617 /* Now we check all the things that could be wrong */
618 $this->validatecfg($cfg, $files);
620 if ($themezipupload['size'] > config
::maxzippedsize
)
621 $err[] = sprintf("Theme zip too large at %s (max size is %s)", $themezipupload['size'], config
::maxzippedsize
);
622 if ($totalsize > config
::maxthemesize
)
623 $err[] = sprintf("Unzipped theme size too large at %s (max size is %s)", $totalsize, config
::maxthemesize
);
624 if (count($files) > config
::maxfiles
)
625 $err[] = sprintf("Too many files+dirs in theme (%d). Maximum is %d.", count($files), config
::maxfiles
);
627 if (count($wpsfound) > 1)
628 $err[] = sprintf("More than one .wps found (%s).", implode(', ', $wpsfound));
629 elseif (count($wpsfound) == 0)
630 $err[] = "No .wps files found.";
632 if (count($rwpsfound) > 1)
633 $err[] = sprintf("More than one .rwps found (%s).", implode(', ', $rwpsfound));
637 public function validatesshot($upload, $mainlcd) {
639 $size = getimagesize($upload['tmp_name']);
640 $dimensions = sprintf("%dx%d", $size[0], $size[1]);
641 if ($size === false) {
642 $err[] = sprintf("Couldn't open screenshot %s", $upload['name']);
645 if ($dimensions != $mainlcd) {
646 $err[] = sprintf("Wrong resolution of %s. Should be %s (is %s).", $upload['name'], $mainlcd, $dimensions);
648 if ($size[2] != IMAGETYPE_PNG
) {
649 $err[] = "Screenshots must be of type PNG.";