1<?php
2
3/**
4 * Ensures that files are not executable unless they are either binary or
5 * contain a shebang.
6 */
7final class ArcanistChmodLinter extends ArcanistLinter {
8
9  const LINT_INVALID_EXECUTABLE = 1;
10
11  public function getInfoName() {
12    return 'Chmod';
13  }
14
15  public function getInfoDescription() {
16    return pht(
17      'Checks the permissions on files and ensures that they are not made to '.
18      'be executable unnecessarily. In particular, a file should not be '.
19      'executable unless it is either binary or contain a shebang.');
20  }
21
22  public function getLinterName() {
23    return 'CHMOD';
24  }
25
26  public function getLinterConfigurationName() {
27    return 'chmod';
28  }
29
30  public function getLintNameMap() {
31    return array(
32      self::LINT_INVALID_EXECUTABLE => pht('Invalid Executable'),
33    );
34  }
35
36  protected function getDefaultMessageSeverity($code) {
37    return ArcanistLintSeverity::SEVERITY_WARNING;
38  }
39
40  protected function shouldLintBinaryFiles() {
41    return true;
42  }
43
44  public function lintPath($path) {
45    $engine = $this->getEngine();
46
47    if (is_executable($engine->getFilePathOnDisk($path))) {
48      if ($engine->isBinaryFile($path)) {
49        $mime = Filesystem::getMimeType($engine->getFilePathOnDisk($path));
50
51        switch ($mime) {
52          // Archives
53          case 'application/jar':
54          case 'application/java-archive':
55          case 'application/x-bzip2':
56          case 'application/x-gzip':
57          case 'application/x-rar-compressed':
58          case 'application/x-tar':
59          case 'application/zip':
60
61          // Audio
62          case 'audio/midi':
63          case 'audio/mpeg':
64          case 'audio/mp4':
65          case 'audio/x-wav':
66
67          // Fonts
68          case 'application/vnd.ms-fontobject':
69          case 'application/x-font-ttf':
70          case 'application/x-woff':
71
72          // Images
73          case 'application/x-shockwave-flash':
74          case 'image/gif':
75          case 'image/jpeg':
76          case 'image/png':
77          case 'image/tiff':
78          case 'image/x-icon':
79          case 'image/x-ms-bmp':
80
81          // Miscellaneous
82          case 'application/msword':
83          case 'application/pdf':
84          case 'application/postscript':
85          case 'application/rtf':
86          case 'application/vnd.ms-excel':
87          case 'application/vnd.ms-powerpoint':
88
89          // Video
90          case 'video/mpeg':
91          case 'video/quicktime':
92          case 'video/x-flv':
93          case 'video/x-msvideo':
94          case 'video/x-ms-wmv':
95
96            $this->raiseLintAtPath(
97              self::LINT_INVALID_EXECUTABLE,
98              pht("'%s' files should not be executable.", $mime));
99            return;
100
101          default:
102            // Path is a binary file, which makes it a valid executable.
103            return;
104        }
105      } else if ($this->getShebang($path)) {
106        // Path contains a shebang, which makes it a valid executable.
107        return;
108      } else {
109        $this->raiseLintAtPath(
110          self::LINT_INVALID_EXECUTABLE,
111          pht(
112            'Executable files should either be binary or contain a shebang.'));
113      }
114    }
115  }
116
117  /**
118   * Returns the path's shebang.
119   *
120   * @param  string
121   * @return string|null
122   */
123  private function getShebang($path) {
124    $line = head(phutil_split_lines($this->getEngine()->loadData($path), true));
125
126    $matches = array();
127    if (preg_match('/^#!(.*)$/', $line, $matches)) {
128      return $matches[1];
129    } else {
130      return null;
131    }
132  }
133
134}
135