Store the results when running checkwps on all themes.
[rockboxthemes.git] / private / themesite.class.php
blob11b92a1c99864b596992c179c37c083adcc4689b
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_abs;
29 public function __construct($dbfile) {
30 $this->db = new db($dbfile);
31 $this->themedir_abs = sprintf("%s/%s", $_SERVER['DOCUMENT_ROOT'], config::datadir);
33 /* Make sure the theme dir exists */
34 if (!file_exists($this->themedir_abs)) {
35 if (!@mkdir($this->themedir_abs)) {
36 die("The theme dir doesn't exist, and I can't create it. Giving up.");
41 private function targetlist($orderby) {
42 $sql = "SELECT shortname, fullname, pic, mainlcd, depth, remotelcd FROM targets ORDER BY " . $orderby;
43 return $this->db->query($sql);
46 public function listtargets($orderby = 'fullname ASC') {
47 $targets = $this->targetlist($orderby);
48 $ret = array();
49 while ($target = $targets->next()) {
50 $ret[] = $target;
52 return $ret;
56 * Run checkwps on all our themes
58 public function checkallthemes() {
59 $sql = "SELECT RowID, * FROM themes";
60 $themes = $this->db->query($sql);
61 $return = array();
62 while ($theme = $themes->next()) {
63 $zipfile = sprintf("%s/%s/%s/%s",
64 config::datadir,
65 $theme['mainlcd'],
66 $theme['shortname'],
67 $theme['zipfile']
69 $result = $this->checkwps($zipfile, $theme['mainlcd'], $theme['remotelcd']);
71 /*
72 * Store the results and check if at least one check passed (for
73 * the summary)
75 $passany = false;
76 foreach($result as $version_type => $targets) {
77 foreach($targets as $target => $result) {
78 if ($result['pass']) $passany = true; /* For the summary */
80 * Maybe we want to have two tables - one with historic
81 * data, and one with only the latest results for fast
82 * retrieval?
84 $this->db->query(sprintf("DELETE FROM checkwps WHERE themeid=%d", $theme['RowID']));
85 $sql = sprintf("INSERT INTO checkwps (themeid, version_type, version_number, target, pass) VALUES (%d, '%s', '%s', '%s', '%s')",
86 $theme['RowID'],
87 db::quote($version_type),
88 db::quote($result['version']),
89 db::quote($target),
90 db::quote($result['pass'] ? 1 : 0)
92 $this->db->query($sql);
95 $return[] = array(
96 'theme' => $theme,
97 'result' => $result,
98 'summary' => array('theme' => $theme['name'], 'pass' => $passany)
101 return $return;
104 public function adminlogin($user, $pass) {
105 $sql = sprintf("SELECT COUNT(*) as count FROM admins WHERE name='%s' AND pass='%s'",
106 db::quote($user),
107 db::quote(md5($pass))
109 $result = $this->db->query($sql)->next();
110 return $result['count'] == 1 ? true : false;
113 public function listthemes($target, $orderby = 'timestamp DESC', $approved = 'approved', $onlyverified = true) {
114 $ret = array();
115 switch($approved) {
116 case 'any':
117 $approved_clause = "";
118 break;
119 case 'hidden':
120 $approved_clause = " AND th.approved = 0 ";
121 break;
122 case 'approved':
123 default:
124 $approved_clause = " AND th.approved = 1 ";
125 break;
127 if ($onlyverified == true) {
128 $verified = " AND th.emailverification = 1 ";
130 else {
131 $verified = "";
133 $sql = sprintf("SELECT name, 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",
134 $verified,
135 $approved_clause,
136 db::quote($target),
137 $orderby
139 $themes = $this->db->query($sql);
140 while ($theme = $themes->next()) {
141 $ret[] = $theme;
143 return $ret;
146 public function target2lcd($shortname) {
147 $sql = sprintf("SELECT mainlcd, remotelcd, depth FROM targets WHERE shortname='%s'",
148 db::quote($shortname)
150 return $this->db->query($sql)->next();
153 public function themenameexists($name, $mainlcd) {
154 $sql = sprintf("SELECT COUNT(*) as count FROM themes WHERE name='%s' AND mainlcd='%s'",
155 db::quote($name),
156 db::quote($mainlcd)
158 $result = $this->db->query($sql)->next();
159 return $result['count'] > 0 ? true : false;
162 public function changestatus($themeid, $newstatus, $oldstatus, $reason) {
163 if ($newstatus == -1) {
164 $theme = $this->db->query(sprintf("SELECT shortname, mainlcd FROM themes WHERE RowID='%d'", db::quote($themeid)))->next();
165 $sql = sprintf("DELETE FROM themes WHERE RowID='%d'",
166 db::quote($themeid)
169 /* Delete the files */
170 $dir = sprintf("%s/%s/%s",
171 config::datadir,
172 $theme['mainlcd'],
173 $theme['shortname']
175 if (file_exists($dir)) {
176 foreach(glob(sprintf("%s/*", $dir)) as $file) {
177 unlink($file);
179 rmdir($dir);
182 else {
183 $sql = sprintf("UPDATE themes SET approved='%d' WHERE RowID='%d'",
184 db::quote($newstatus),
185 db::quote($themeid)
188 if ($oldstatus == 1 && $newstatus < 1) {
189 // Send a mail to notify the user that his theme has been
190 // hidden/deleted
191 print("Yeah hi we deleted your themz lol");
193 print("SQL: $sql<br />\n");
194 $this->db->query($sql);
197 public function addtarget($shortname, $fullname, $mainlcd, $pic, $depth, $remotelcd = false) {
198 $sql = sprintf("INSERT INTO targets
199 (shortname, fullname, mainlcd, pic, depth, remotelcd)
200 VALUES
201 ('%s', '%s', '%s', '%s', '%s', %s)",
202 db::quote($shortname),
203 db::quote($fullname),
204 db::quote($mainlcd),
205 db::quote($pic),
206 db::quote($depth),
207 $remotelcd === false ? 'NULL' : sprintf("'%s'", db::quote($remotelcd))
209 $this->db->query($sql);
210 $themedir = sprintf("%s/%s", $this->themedir_abs, $mainlcd);
211 if (!file_exists($themedir)) {
212 mkdir($themedir);
216 public function validatetheme($zipfile) {
217 $err = array();
218 return $err;
221 public function prepareverification($id, $email, $author) {
222 $token = md5(uniqid());
223 $sql = sprintf("UPDATE themes SET emailverification='%s' WHERE RowID='%s'",
224 db::quote($token),
225 db::quote($id)
227 $this->db->query($sql);
228 $url = sprintf("%s%s/verify.php?t=%s", config::hostname, config::path, $token);
229 /* xxx: Someone rewrite this message to not sound horrible */
230 $msg = <<<END
231 Hello, you just uploaded a Rockbox theme and now we need you to verify your
232 email address. To do this, simply open the link below in your browser. You
233 may have to copy/paste the text into your browser's location bar in some cases.
235 $url
237 Thank for your contributions
239 The Rockbox Theme Site team.
240 END;
241 /* ' (this is here to keep my syntax hilighting happy) */
242 $msg = wordwrap($msg, 78);
243 $subject = "Rockbox Theme Site email verification";
244 $to = sprintf("%s <%s>", $author, $email);
245 $headers = 'From: themes@rockbox.org';
246 mail($to, $subject, $msg, $headers);
249 public function verifyemail($token) {
250 $sql = sprintf("UPDATE themes SET emailverification=1 WHERE emailverification='%s'",
251 db::quote($token)
253 $res = $this->db->query($sql);
254 return $res->rowsaffected();
257 public function addtheme($name, $shortname, $author, $email, $mainlcd, $remotelcd, $description, $zipfile, $sshot_wps, $sshot_menu) {
258 $err = array();
259 /* return array("Skipping upload"); */
261 /* Create the destination dir */
262 $destdir = sprintf("%s/%s/%s",
263 $this->themedir_abs,
264 $mainlcd,
265 $shortname
267 if (!file_exists($destdir) && !mkdir($destdir)) {
268 $err[] = sprintf("Couldn't create themedir %s", $destdir);
269 return $err;
272 /* Prepend wps- and menu- to screenshots */
273 $sshot_wps['name'] = empty($sshot_wps['name']) ? '' : 'wps-'.$sshot_wps['name'];
274 $sshot_menu['name'] = empty($sshot_menu['name']) ? '' : 'menu-'.$sshot_menu['name'];
276 /* Start moving files in place */
277 $uploads = array($zipfile, $sshot_wps, $sshot_menu);
278 $movedfiles = array();
279 foreach($uploads as $file) {
280 if ($file === false || empty($file['tmp_name'])) {
281 continue;
283 $dest = sprintf("%s/%s",
284 $destdir,
285 $file['name']
288 if (!@move_uploaded_file($file['tmp_name'], $dest)) {
289 /* Upload went wrong, clean up */
290 foreach ($movedfiles as $movedfile) {
291 unlink($movedfile);
293 rmdir($destdir);
294 $err[] = sprintf("Couldn't move %s.", $file['name'], $dest);
295 return $err;
297 else {
298 $movedfiles[] = $dest;
301 $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)";
302 $sql = sprintf($sql_f,
303 db::quote($author),
304 db::quote($email),
305 db::quote($name),
306 db::quote($mainlcd),
307 db::quote($zipfile['name']),
308 db::quote($sshot_wps['name']),
309 $sshot_menu === false ? 'NULL' : sprintf("'%s'", db::quote($sshot_menu['name'])),
310 $remotelcd === false ? 'NULL' : sprintf("'%s'", db::quote($remotelcd)),
311 db::quote($description),
312 db::quote($shortname),
313 config::defaultstatus
315 $result = $this->db->query($sql);
316 $id = $result->insertid();
317 $check = $this->checkwps(sprintf("%s/%s/%s", config::datadir, $mainlcd, $zipfile['name']), $mainlcd, $remotelcd, true);
318 return $id;
322 * Use this rather than plain pathinfo for compatibility with PHP<5.2.0
324 private function my_pathinfo($path) {
325 $pathinfo = pathinfo($path);
326 /* Make sure we have the $pathinfo['filename'] element added in PHP 5.2.0 */
327 if (!isset($pathinfo['filename'])) {
328 $pathinfo['filename'] = substr(
329 $pathinfo['basename'],
331 strrpos($pathinfo['basename'],'.') === false ? strlen($pathinfo['basename']) : strrpos($pathinfo['basename'],'.')
334 return $pathinfo;
338 * Convenience function called from several locations
340 private function getzipentrycontents($zip, $ze) {
341 $ret = "";
342 zip_entry_open($zip, $ze);
343 while($read = zip_entry_read($ze)) {
344 $ret .= $read;
346 zip_entry_close($ze);
347 return $ret;
351 * xxx: I don't know what kind of validation is wanted for cfg files
353 public function validatecfg($cfg, $files) {
354 $conf = array();
355 foreach(explode("\n", $cfg) as $line) {
356 if (substr($line, 0, 1) == '#') continue;
357 preg_match("/^(?P<name>[^:]*)\s*:\s*(?P<value>[^#]*)\s*$/", $line, $matches);
358 if (count($matches) > 0) {
359 extract($matches);
360 switch($name) {
361 default:
362 break;
368 public function lcd2targets($lcd) {
369 $ret = array();
370 $sql = sprintf("SELECT shortname FROM targets WHERE mainlcd='%s' OR remotelcd='%s'",
371 db::quote($lcd),
372 db::quote($lcd)
374 $targets = $this->db->query($sql);
375 while ($target = $targets->next()) {
376 $ret[] = $target['shortname'];
378 return $ret;
382 * Check a WPS against two revisions: current and the latest release
384 public function checkwps($zipfile, $mainlcd, $remotelcd) {
385 $return = array();
387 /* First, create a temporary dir */
388 $tmpdir = sprintf("%s/temp-%s", preconfig::privpath, md5(uniqid()));
389 mkdir($tmpdir);
391 /* Then, unzip the theme here */
392 $cmd = sprintf("%s -d %s %s", config::unzip, $tmpdir, escapeshellarg($zipfile));
393 exec($cmd, $dontcare, $ret);
395 /* Now, cd into that dir */
396 $olddir = getcwd();
397 chdir($tmpdir);
400 * For all .wps and .rwps, run checkwps of both release and current for
401 * all applicable targets
403 foreach(glob('.rockbox/wps/*wps') as $file) {
404 $p = $this->my_pathinfo($file);
405 $lcd = ($p['extension'] == 'rwps' ? $remotelcd : $mainlcd);
406 foreach(array('release', 'current') as $version) {
407 foreach($this->lcd2targets($lcd) as $shortname) {
408 $result = array();
409 $checkwps = sprintf("%s/checkwps/%s/checkwps.%s",
410 '..', /* We'll be in a subdir of the private dir */
411 $version,
412 $shortname
414 $result['version'] = trim(file_get_contents(sprintf('%s/checkwps/%s/VERSION',
415 '..',
416 $version,
417 $shortname
418 )));
419 if (file_exists($checkwps)) {
420 exec(sprintf("%s %s", $checkwps, $file), $output, $ret);
421 $result['pass'] = ($ret == 0);
422 $result['output'] = $output;
423 $return[$version][$shortname] = $result;
429 /* chdir back */
430 chdir($olddir);
432 /* Remove the tempdir */
433 $this->rmdir_recursive($tmpdir);
434 return $return;
437 private function rmdir_recursive($dirname) {
438 $dir = dir($dirname);
439 while (false !== ($entry = $dir->read())) {
440 if ($entry == '.' || $entry == '..') continue;
441 $path = sprintf("%s/%s", $dir->path, $entry);
442 if (is_dir($path)) {
443 $this->rmdir_recursive($path);
445 else {
446 unlink($path);
449 $dir->close();
450 rmdir($dirname);
454 * This rather unwieldy function validates the structure of a theme's
455 * zipfile. It checks the following:
456 * - Exactly 1 .wps file
457 * - 0 or 1 .rwps file
458 * - Only .bmp files in /.rockbox/backdrops/ and /.rockbox/wps/<shortname>/
459 * - All files are inside /.rockbox
460 * - All .wps, .rwps and .cfg files use the same shortname, which is also
461 * the one used for the subdir in /.rockbox/wps
463 * It does not uncompress any of the files.
465 * We continue checking for errors, rather than aborting, so the uploader
466 * gets a full list of things we didn't like.
468 public function validatezip($themezipupload) {
469 $err = array();
470 $zip = zip_open($themezipupload['tmp_name']);
471 $totalsize = 0;
472 $files = array();
473 $wpsfound = array();
474 $rwpsfound = array();
475 $shortname = '';
476 $cfg = '';
478 if (is_int($zip)) {
479 $err[] = sprintf("Couldn't open zipfile %s", $themezipupload['name']);
480 return $err;
482 while ($ze = zip_read($zip)) {
483 $filename = zip_entry_name($ze);
484 $pathinfo = $this->my_pathinfo($filename);
485 $totalsize += zip_entry_filesize($ze);
486 $files[] = $filename;
488 /* Count .wps and .rwps files for later checking */
489 if (strtolower($pathinfo['extension']) == 'wps')
490 $wpsfound[] = $filename;
491 if (strtolower($pathinfo['extension']) == 'rwps')
492 $rwpsfound[] = $filename;
494 /* Check that all files are within .rockbox */
495 if (strpos($filename, '.rockbox') !== 0)
496 $err[] = sprintf("File outside /.rockbox/: %s", $filename);
498 /* Check that all .wps, .rwps and .cfg filenames use the same shortname */
499 switch(strtolower($pathinfo['extension'])) {
500 case 'cfg':
501 /* Save the contents for later checking */
502 $cfg = $this->getzipentrycontents($zip, $ze);
503 case 'wps':
504 case 'rwps':
505 if ($shortname === '')
506 $shortname = $pathinfo['filename'];
507 elseif ($shortname !== $pathinfo['filename'])
508 $err[] = sprintf("Filename invalid: %s (should be %s.%s)", $filename, $shortname, $pathinfo['extension']);
509 break;
513 * Check that the dir inside /.rockbox/wps also has the same name.
514 * This automatically ensures that there is only one.
516 if ($pathinfo['dirname'] == '.rockbox/wps' && $pathinfo['extension'] == '') {
517 if ($shortname === '')
518 $shortname = $pathinfo['filename'];
519 elseif ($shortname !== $pathinfo['filename'])
520 $err[] = sprintf("Invalid dirname: %s (should be %s.)", $filename, $shortname);
524 * Check that the only files we have inside /.rockbox/backdrops/
525 * and subdirs of /.rockbox/wps/ are .bmp files
527 if (strtolower($pathinfo['extension']) != 'bmp' &&
528 ($pathinfo['dirname'] == '.rockbox/backdrops' || // Files inside .rockbox/backdrops
529 ($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)
532 $err[] = sprintf("Non-bmp file not allowed here: %s", $filename);
535 /* Check for paths that are too deep */
536 if (count(explode('/', $pathinfo['dirname'])) > 3) {
537 $err[] = sprintf("Path too deep: %s", $filename);
540 /* Check for unwanted junk files */
541 switch(strtolower($pathinfo['basename'])) {
542 case "thumbs.db":
543 case "desktop.ini":
544 case ".ds_store":
545 case ".directory":
546 $err[] = sprintf("Unwanted file: %s", $filename);
550 /* Now we check all the things that could be wrong */
551 $this->validatecfg($cfg, $files);
553 if ($themezipupload['size'] > config::maxzippedsize)
554 $err[] = sprintf("Theme zip too large at %s (max size is %s)", $themezipupload['size'], config::maxzippedsize);
555 if ($totalsize > config::maxthemesize)
556 $err[] = sprintf("Unzipped theme size too large at %s (max size is %s)", $totalsize, config::maxthemesize);
557 if (count($files) > config::maxfiles)
558 $err[] = sprintf("Too many files+dirs in theme (%d). Maximum is %d.", count($files), config::maxfiles);
560 if (count($wpsfound) > 1)
561 $err[] = sprintf("More than one .wps found (%s).", implode(', ', $wpsfound));
562 elseif (count($wpsfound) == 0)
563 $err[] = "No .wps files found.";
565 if (count($rwpsfound) > 1)
566 $err[] = sprintf("More than one .rwps found (%s).", implode(', ', $rwpsfound));
567 return $err;
570 public function validatesshot($upload, $mainlcd) {
571 $err = array();
572 $size = getimagesize($upload['tmp_name']);
573 $dimensions = sprintf("%dx%d", $size[0], $size[1]);
574 if ($size === false) {
575 $err[] = sprintf("Couldn't open screenshot %s", $upload['name']);
577 else {
578 if ($dimensions != $mainlcd) {
579 $err[] = sprintf("Wrong resolution of %s. Should be %s (is %s).", $upload['name'], $mainlcd, $dimensions);
581 if ($size[2] != IMAGETYPE_PNG) {
582 $err[] = "Screenshots must be of type PNG.";
585 return $err;