Change themedir_abs into two variables holding paths to the private and public version.
[rockboxthemes.git] / private / themesite.class.php
blobca6bd34b86e7c2c886b07b2241602d2801e4098f
1 <?php
2 /***************************************************************************
3 * __________ __ ___.
4 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
5 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
6 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
7 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
8 * \/ \/ \/ \/ \/
9 * $Id$
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');
25 class themesite {
26 private $db;
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);
34 printf("%s<br />\n%s<br />\n", $this->themedir_public, $this->themedir_private);
38 * Log a message to the log table. Time, IP and admin user (if any)
39 * is automaticly added.
41 private function log($message) {
42 $sql_f = "INSERT INTO log (time, ip, admin, msg) VALUES (datetime('now'), '%s', '%s', '%s')";
43 $sql = sprintf($sql_f,
44 $_SERVER['REMOTE_ADDR'],
45 isset($_SESSION['user']) ? db::quote($_SESSION['user']) : '',
46 db::quote($message)
48 $this->db->query($sql);
51 private function targetlist($orderby) {
52 $sql = "SELECT shortname, fullname, pic, mainlcd, depth, remotelcd FROM targets ORDER BY " . $orderby;
53 return $this->db->query($sql);
56 public function listtargets($orderby = 'fullname ASC') {
57 $targets = $this->targetlist($orderby);
58 $ret = array();
59 while ($target = $targets->next()) {
60 $ret[] = $target;
62 return $ret;
66 * Run checkwps on all our themes
68 public function checkallthemes() {
69 $this->log("Running checkwps");
70 $sql = "SELECT RowID, * FROM themes";
71 $themes = $this->db->query($sql);
72 $return = array();
73 while ($theme = $themes->next()) {
74 $starttime = microtime(true);
75 $zipfile = sprintf("%s/%s/%s/%s",
76 config::datadir,
77 $theme['mainlcd'],
78 $theme['shortname'],
79 $theme['zipfile']
81 $result = $this->checkwps($zipfile, $theme['mainlcd'], $theme['remotelcd']);
83 /*
84 * Store the results and check if at least one check passed (for
85 * the summary)
87 $passany = false;
88 foreach($result as $version_type => $targets) {
89 foreach($targets as $target => $result) {
90 if ($result['pass']) $passany = true; /* For the summary */
92 * Maybe we want to have two tables - one with historic
93 * data, and one with only the latest results for fast
94 * retrieval?
96 $this->db->query(sprintf("DELETE FROM checkwps WHERE themeid=%d AND version_type='%s'", $theme['RowID'], db::quote($version_type)));
97 $sql = sprintf("INSERT INTO checkwps (themeid, version_type, version_number, target, pass) VALUES (%d, '%s', '%s', '%s', '%s')",
98 $theme['RowID'],
99 db::quote($version_type),
100 db::quote($result['version']),
101 db::quote($target),
102 db::quote($result['pass'] ? 1 : 0)
104 $this->db->query($sql);
107 $return[] = array(
108 'theme' => $theme,
109 'result' => $result,
110 'summary' => array('theme' => $theme['name'], 'pass' => $passany, 'duration' => microtime(true) - $starttime)
113 return $return;
116 public function adminlogin($user, $pass) {
117 $sql = sprintf("SELECT COUNT(*) as count FROM admins WHERE name='%s' AND pass='%s'",
118 db::quote($user),
119 db::quote(md5($pass))
121 $result = $this->db->query($sql)->next();
122 return $result['count'] == 1 ? true : false;
125 public function listthemes($target, $orderby = 'timestamp DESC', $approved = 'approved', $onlyverified = true) {
126 $ret = array();
127 switch($approved) {
128 case 'any':
129 $approved_clause = "";
130 break;
131 case 'hidden':
132 $approved_clause = " AND th.approved = 0 ";
133 break;
134 case 'approved':
135 default:
136 $approved_clause = " AND th.approved = 1 ";
137 break;
139 if ($onlyverified == true) {
140 $verified = " AND th.emailverification = 1 ";
142 else {
143 $verified = "";
145 $sql = sprintf("SELECT name, timestamp, th.mainlcd as mainlcd, approved, reason, description, th.RowID as id, th.shortname AS shortname, zipfile, sshot_wps, sshot_menu, emailverification = 1 as verified FROM themes th, targets ta WHERE 1 %s %s AND th.mainlcd=ta.mainlcd and ta.shortname='%s' AND (ta.remotelcd IS NULL OR ta.remotelcd=th.remotelcd) ORDER BY %s",
146 $verified,
147 $approved_clause,
148 db::quote($target),
149 $orderby
151 $themes = $this->db->query($sql);
152 while ($theme = $themes->next()) {
153 $ret[] = $theme;
155 return $ret;
158 public function target2lcd($shortname) {
159 $sql = sprintf("SELECT mainlcd, remotelcd, depth FROM targets WHERE shortname='%s'",
160 db::quote($shortname)
162 return $this->db->query($sql)->next();
165 public function themenameexists($name, $mainlcd) {
166 $sql = sprintf("SELECT COUNT(*) as count FROM themes WHERE name='%s' AND mainlcd='%s'",
167 db::quote($name),
168 db::quote($mainlcd)
170 $result = $this->db->query($sql)->next();
171 return $result['count'] > 0 ? true : false;
174 public function changestatus($themeid, $newstatus, $oldstatus, $reason) {
175 $status_text = array('1' => 'Approved', '0' => 'hidden', '-1' => 'deleted');
176 $this->log(sprintf("Changing status of theme %d from %s to %s - Reason: %s",
177 $themeid,
178 $status_text[$oldstatus],
179 $status_text[$newstatus],
180 $reason
182 $sql = sprintf("SELECT shortname, mainlcd, email, name, author FROM themes WHERE RowID='%d'", db::quote($themeid));
183 $theme = $this->db->query($sql)->next();
185 if ($newstatus == -1) {
186 $sql = sprintf("DELETE FROM themes WHERE RowID='%d'",
187 db::quote($themeid)
190 /* Delete the files */
191 $dir = sprintf("%s/%s/%s",
192 config::datadir,
193 $theme['mainlcd'],
194 $theme['shortname']
196 if (file_exists($dir)) {
197 foreach(glob(sprintf("%s/*", $dir)) as $file) {
198 unlink($file);
200 rmdir($dir);
203 else {
204 $sql = sprintf("UPDATE themes SET approved='%d', reason='%s' WHERE RowID='%d'",
205 db::quote($newstatus),
206 db::quote($reason),
207 db::quote($themeid)
210 if ($oldstatus == 1 && $newstatus < 1) {
211 // Send a mail to notify the user that his theme has been
212 // hidden/deleted. No reason to distinguish, since the result
213 // for him is the same.
214 $to = sprintf("%s <%s>", $theme['author'], $theme['email']);
215 $subject = sprintf("Your theme '%s' has been removed from %s", $theme['name'], config::hostname);
216 $msg = <<<END
217 Your theme {$theme['name']} was removed from the Rockbox theme site. The
218 following reason should explain why:
220 ----------
221 {$reason}
222 ----------
224 If you think this was a mistake, or disagree with the decision, contact the
225 theme site admins in the Rockbox Forums or on IRC.
226 END;
227 $this->send_mail($subject, $to, $msg);
229 $this->db->query($sql);
232 public function addtarget($shortname, $fullname, $mainlcd, $pic, $depth, $remotelcd = false) {
233 $this->log(sprintf("Add new target %s", $fullname));
235 $sql = sprintf("INSERT INTO targets
236 (shortname, fullname, mainlcd, pic, depth, remotelcd)
237 VALUES
238 ('%s', '%s', '%s', '%s', '%s', %s)",
239 db::quote($shortname),
240 db::quote($fullname),
241 db::quote($mainlcd),
242 db::quote($pic),
243 db::quote($depth),
244 $remotelcd === false ? 'NULL' : sprintf("'%s'", db::quote($remotelcd))
246 $this->db->query($sql);
247 $themedir = sprintf("%s/%s", $this->themedir_public, $mainlcd);
248 if (!file_exists($themedir)) {
249 mkdir($themedir);
253 private function send_mail($subject, $to, $msg) {
254 $msg = wordwrap($msg, 78);
255 $headers = 'From: themes@rockbox.org';
256 mail($to, $subject, $msg, $headers);
259 public function validatetheme($zipfile) {
260 $err = array();
261 return $err;
264 public function prepareverification($id, $email, $author) {
265 $token = md5(uniqid());
266 $sql = sprintf("UPDATE themes SET emailverification='%s' WHERE RowID='%s'",
267 db::quote($token),
268 db::quote($id)
270 $this->db->query($sql);
271 $url = sprintf("%s%s/verify.php?t=%s", config::hostname, config::path, $token);
272 /* xxx: Someone rewrite this message to not sound horrible */
273 $msg = <<<END
274 Hello, you just uploaded a Rockbox theme and now we need you to verify your
275 email address. To do this, simply open the link below in your browser. You
276 may have to copy/paste the text into your browser's location bar in some cases.
278 $url
280 Thank for your contributions
282 The Rockbox Theme Site team.
283 END;
284 /* ' (this is here to keep my syntax hilighting happy) */
285 $subject = "Rockbox Theme Site email verification";
286 $to = sprintf("%s <%s>", $author, $email);
287 $this->send_mail($subject, $to, $msg);
290 public function verifyemail($token) {
291 $sql = sprintf("UPDATE themes SET emailverification=1 WHERE emailverification='%s'",
292 db::quote($token)
294 $res = $this->db->query($sql);
295 return $res->rowsaffected();
298 public function addtheme($name, $shortname, $author, $email, $mainlcd, $remotelcd, $description, $zipfile, $sshot_wps, $sshot_menu) {
299 $err = array();
300 /* return array("Skipping upload"); */
302 /* Create the destination dir */
303 $destdir = sprintf("%s/%s/%s",
304 $this->themedir_public,
305 $mainlcd,
306 $shortname
308 if (!file_exists($destdir) && !mkdir($destdir)) {
309 $err[] = sprintf("Couldn't create themedir %s", $destdir);
310 return $err;
313 /* Prepend wps- and menu- to screenshots */
314 $sshot_wps['name'] = empty($sshot_wps['name']) ? '' : 'wps-'.$sshot_wps['name'];
315 $sshot_menu['name'] = empty($sshot_menu['name']) ? '' : 'menu-'.$sshot_menu['name'];
317 /* Start moving files in place */
318 $uploads = array($zipfile, $sshot_wps, $sshot_menu);
319 $movedfiles = array();
320 foreach($uploads as $file) {
321 if ($file === false || empty($file['tmp_name'])) {
322 continue;
324 $dest = sprintf("%s/%s",
325 $destdir,
326 $file['name']
329 if (!@move_uploaded_file($file['tmp_name'], $dest)) {
330 /* Upload went wrong, clean up */
331 foreach ($movedfiles as $movedfile) {
332 unlink($movedfile);
334 rmdir($destdir);
335 $err[] = sprintf("Couldn't move %s.", $file['name'], $dest);
336 return $err;
338 else {
339 $movedfiles[] = $dest;
342 $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)";
343 $sql = sprintf($sql_f,
344 db::quote($author),
345 db::quote($email),
346 db::quote($name),
347 db::quote($mainlcd),
348 db::quote($zipfile['name']),
349 db::quote($sshot_wps['name']),
350 $sshot_menu === false ? 'NULL' : sprintf("'%s'", db::quote($sshot_menu['name'])),
351 $remotelcd === false ? 'NULL' : sprintf("'%s'", db::quote($remotelcd)),
352 db::quote($description),
353 db::quote($shortname),
354 config::defaultstatus
356 $result = $this->db->query($sql);
357 $id = $result->insertid();
358 $check = $this->checkwps(sprintf("%s/%s/%s", config::datadir, $mainlcd, $zipfile['name']), $mainlcd, $remotelcd);
359 /* xxx: store these results */
360 $this->log(sprintf("Added theme %d (email: %s)", $id, $email));
361 return $id;
365 * Use this rather than plain pathinfo for compatibility with PHP<5.2.0
367 private function my_pathinfo($path) {
368 $pathinfo = pathinfo($path);
369 /* Make sure we have the $pathinfo['filename'] element added in PHP 5.2.0 */
370 if (!isset($pathinfo['filename'])) {
371 $pathinfo['filename'] = substr(
372 $pathinfo['basename'],
374 strrpos($pathinfo['basename'],'.') === false ? strlen($pathinfo['basename']) : strrpos($pathinfo['basename'],'.')
377 return $pathinfo;
381 * Convenience function called from several locations
383 private function getzipentrycontents($zip, $ze) {
384 $ret = "";
385 zip_entry_open($zip, $ze);
386 while($read = zip_entry_read($ze)) {
387 $ret .= $read;
389 zip_entry_close($ze);
390 return $ret;
394 * xxx: I don't know what kind of validation is wanted for cfg files
396 public function validatecfg($cfg, $files) {
397 $conf = array();
398 foreach(explode("\n", $cfg) as $line) {
399 if (substr($line, 0, 1) == '#') continue;
400 preg_match("/^(?P<name>[^:]*)\s*:\s*(?P<value>[^#]*)\s*$/", $line, $matches);
401 if (count($matches) > 0) {
402 extract($matches);
403 switch($name) {
404 default:
405 break;
411 public function lcd2targets($lcd) {
412 $ret = array();
413 $sql = sprintf("SELECT shortname FROM targets WHERE mainlcd='%s' OR remotelcd='%s'",
414 db::quote($lcd),
415 db::quote($lcd)
417 $targets = $this->db->query($sql);
418 while ($target = $targets->next()) {
419 $ret[] = $target['shortname'];
421 return $ret;
425 * Check a WPS against two revisions: current and the latest release
427 public function checkwps($zipfile, $mainlcd, $remotelcd) {
428 $return = array();
430 /* First, create a temporary dir */
431 $tmpdir = sprintf("%s/temp-%s", preconfig::privpath, md5(uniqid()));
432 mkdir($tmpdir);
434 /* Then, unzip the theme here */
435 $cmd = sprintf("%s -d %s %s", config::unzip, $tmpdir, escapeshellarg($zipfile));
436 exec($cmd, $dontcare, $ret);
438 /* Now, cd into that dir */
439 $olddir = getcwd();
440 chdir($tmpdir);
443 * For all .wps and .rwps, run checkwps of both release and current for
444 * all applicable targets
446 foreach(glob('.rockbox/wps/*wps') as $file) {
447 $p = $this->my_pathinfo($file);
448 $lcd = ($p['extension'] == 'rwps' ? $remotelcd : $mainlcd);
449 foreach(array('release', 'current') as $version) {
450 foreach($this->lcd2targets($lcd) as $shortname) {
451 $result = array();
452 $checkwps = sprintf("%s/checkwps/%s/checkwps.%s",
453 '..', /* We'll be in a subdir of the private dir */
454 $version,
455 $shortname
457 $result['version'] = trim(file_get_contents(sprintf('%s/checkwps/%s/VERSION',
458 '..',
459 $version,
460 $shortname
461 )));
462 if (file_exists($checkwps)) {
463 exec(sprintf("%s %s", $checkwps, $file), $output, $ret);
464 $result['pass'] = ($ret == 0);
465 $result['output'] = $output;
466 $return[$version][$shortname] = $result;
472 /* chdir back */
473 chdir($olddir);
475 /* Remove the tempdir */
476 $this->rmdir_recursive($tmpdir);
477 return $return;
480 private function rmdir_recursive($dirname) {
481 $dir = dir($dirname);
482 while (false !== ($entry = $dir->read())) {
483 if ($entry == '.' || $entry == '..') continue;
484 $path = sprintf("%s/%s", $dir->path, $entry);
485 if (is_dir($path)) {
486 $this->rmdir_recursive($path);
488 else {
489 unlink($path);
492 $dir->close();
493 rmdir($dirname);
497 * This rather unwieldy function validates the structure of a theme's
498 * zipfile. It checks the following:
499 * - Exactly 1 .wps file
500 * - 0 or 1 .rwps file
501 * - Only .bmp files in /.rockbox/backdrops/ and /.rockbox/wps/<shortname>/
502 * - All files are inside /.rockbox
503 * - All .wps, .rwps and .cfg files use the same shortname, which is also
504 * the one used for the subdir in /.rockbox/wps
506 * It does not uncompress any of the files.
508 * We continue checking for errors, rather than aborting, so the uploader
509 * gets a full list of things we didn't like.
511 public function validatezip($themezipupload) {
512 $err = array();
513 $zip = zip_open($themezipupload['tmp_name']);
514 $totalsize = 0;
515 $files = array();
516 $wpsfound = array();
517 $rwpsfound = array();
518 $shortname = '';
519 $cfg = '';
521 if (is_int($zip)) {
522 $err[] = sprintf("Couldn't open zipfile %s", $themezipupload['name']);
523 return $err;
525 while ($ze = zip_read($zip)) {
526 $filename = zip_entry_name($ze);
527 $pathinfo = $this->my_pathinfo($filename);
528 $totalsize += zip_entry_filesize($ze);
529 $files[] = $filename;
531 /* Count .wps and .rwps files for later checking */
532 if (strtolower($pathinfo['extension']) == 'wps')
533 $wpsfound[] = $filename;
534 if (strtolower($pathinfo['extension']) == 'rwps')
535 $rwpsfound[] = $filename;
537 /* Check that all files are within .rockbox */
538 if (strpos($filename, '.rockbox') !== 0)
539 $err[] = sprintf("File outside /.rockbox/: %s", $filename);
541 /* Check that all .wps, .rwps and .cfg filenames use the same shortname */
542 switch(strtolower($pathinfo['extension'])) {
543 case 'cfg':
544 /* Save the contents for later checking */
545 $cfg = $this->getzipentrycontents($zip, $ze);
546 case 'wps':
547 case 'rwps':
548 if ($shortname === '')
549 $shortname = $pathinfo['filename'];
550 elseif ($shortname !== $pathinfo['filename'])
551 $err[] = sprintf("Filename invalid: %s (should be %s.%s)", $filename, $shortname, $pathinfo['extension']);
552 break;
556 * Check that the dir inside /.rockbox/wps also has the same name.
557 * This automatically ensures that there is only one.
559 if ($pathinfo['dirname'] == '.rockbox/wps' && $pathinfo['extension'] == '') {
560 if ($shortname === '')
561 $shortname = $pathinfo['filename'];
562 elseif ($shortname !== $pathinfo['filename'])
563 $err[] = sprintf("Invalid dirname: %s (should be %s.)", $filename, $shortname);
567 * Check that the only files we have inside /.rockbox/backdrops/
568 * and subdirs of /.rockbox/wps/ are .bmp files
570 if (strtolower($pathinfo['extension']) != 'bmp' &&
571 ($pathinfo['dirname'] == '.rockbox/backdrops' || // Files inside .rockbox/backdrops
572 ($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)
575 $err[] = sprintf("Non-bmp file not allowed here: %s", $filename);
578 /* Check for paths that are too deep */
579 if (count(explode('/', $pathinfo['dirname'])) > 3) {
580 $err[] = sprintf("Path too deep: %s", $filename);
583 /* Check for unwanted junk files */
584 switch(strtolower($pathinfo['basename'])) {
585 case "thumbs.db":
586 case "desktop.ini":
587 case ".ds_store":
588 case ".directory":
589 $err[] = sprintf("Unwanted file: %s", $filename);
593 /* Now we check all the things that could be wrong */
594 $this->validatecfg($cfg, $files);
596 if ($themezipupload['size'] > config::maxzippedsize)
597 $err[] = sprintf("Theme zip too large at %s (max size is %s)", $themezipupload['size'], config::maxzippedsize);
598 if ($totalsize > config::maxthemesize)
599 $err[] = sprintf("Unzipped theme size too large at %s (max size is %s)", $totalsize, config::maxthemesize);
600 if (count($files) > config::maxfiles)
601 $err[] = sprintf("Too many files+dirs in theme (%d). Maximum is %d.", count($files), config::maxfiles);
603 if (count($wpsfound) > 1)
604 $err[] = sprintf("More than one .wps found (%s).", implode(', ', $wpsfound));
605 elseif (count($wpsfound) == 0)
606 $err[] = "No .wps files found.";
608 if (count($rwpsfound) > 1)
609 $err[] = sprintf("More than one .rwps found (%s).", implode(', ', $rwpsfound));
610 return $err;
613 public function validatesshot($upload, $mainlcd) {
614 $err = array();
615 $size = getimagesize($upload['tmp_name']);
616 $dimensions = sprintf("%dx%d", $size[0], $size[1]);
617 if ($size === false) {
618 $err[] = sprintf("Couldn't open screenshot %s", $upload['name']);
620 else {
621 if ($dimensions != $mainlcd) {
622 $err[] = sprintf("Wrong resolution of %s. Should be %s (is %s).", $upload['name'], $mainlcd, $dimensions);
624 if ($size[2] != IMAGETYPE_PNG) {
625 $err[] = "Screenshots must be of type PNG.";
628 return $err;