Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 108 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
PFTree | |
0.00% |
0 / 108 |
|
0.00% |
0 / 11 |
1640 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
addChild | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTreeFromWikiText | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
42 | |||
configArray | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
setParentsId | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
setChildren | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
42 | |||
getFromTopCategory | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
addSubCategories | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
30 | |||
getCurValues | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
30 | |||
populateChildren | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getSubcategories | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | /** |
3 | * A class that defines a tree - and can populate it based on either |
4 | * wikitext or a category structure. |
5 | * |
6 | * @ingroup PFFormInput |
7 | * |
8 | * @author Yaron Koren |
9 | * @author Amr El-Absy |
10 | */ |
11 | class PFTree { |
12 | public $title; |
13 | public $children; |
14 | public $depth; |
15 | public $top_category; |
16 | /** |
17 | * @var array[] |
18 | * @phan-var list<array> |
19 | */ |
20 | public $tree_array; |
21 | public $current_values; |
22 | |
23 | public function __construct( $depth, $cur_values ) { |
24 | $this->depth = $depth; |
25 | $this->current_values = $cur_values; |
26 | $this->title = ""; |
27 | $this->children = []; |
28 | } |
29 | |
30 | public function addChild( $child ) { |
31 | $this->children[] = $child; |
32 | } |
33 | |
34 | /** |
35 | * This Function takes the wikitext-styled bullets as a parameter, and converts it into |
36 | * an array which is used within the class to modify the data passed to JS. |
37 | * @param string $wikitext |
38 | */ |
39 | public function getTreeFromWikiText( $wikitext ) { |
40 | $lines = explode( "\n", $wikitext ); |
41 | $full_tree = []; |
42 | $temporary_values = []; |
43 | foreach ( $lines as $line ) { |
44 | $numBullets = 0; |
45 | for ( $i = 0; $i < strlen( $line ) && $line[$i] == '*'; $i++ ) { |
46 | $numBullets++; |
47 | } |
48 | $lineText = trim( substr( $line, $numBullets ) ); |
49 | $full_tree[] = [ 'level' => $numBullets, "text" => $lineText ]; |
50 | |
51 | if ( in_array( $lineText, $this->current_values ) && !in_array( $lineText, $temporary_values ) ) { |
52 | $temporary_values[] = $lineText; |
53 | } |
54 | } |
55 | $this->tree_array = $full_tree; |
56 | $this->current_values = $temporary_values; |
57 | $this->configArray(); |
58 | $this->setParentsId(); |
59 | $this->setChildren(); |
60 | // Get rid of array keys, to make this a regular array again, as jsTree requires. |
61 | // @phan-suppress-next-line PhanRedundantArrayValuesCall False positive: array_values() renumbers here to 0, 1, 2,… |
62 | $this->tree_array = array_values( $this->tree_array ); |
63 | } |
64 | |
65 | /** |
66 | * This function sets an ID for each element to be used in the function setParentsId() |
67 | * so that every child can know its parent |
68 | * This function also determine whether or not the node will be opened, depending on |
69 | * the attribute $depth. |
70 | * This function also determine whether or not the node is selected. |
71 | * @suppress PhanTypeInvalidDimOffset TODO Document tree_array |
72 | */ |
73 | private function configArray() { |
74 | for ( $i = 0; $i < count( $this->tree_array ); $i++ ) { |
75 | $this->tree_array[$i]['node_id'] = $i; |
76 | if ( $this->tree_array[$i]['level'] <= $this->depth ) { |
77 | $this->tree_array[$i]['state']['opened'] = true; |
78 | } |
79 | if ( in_array( $this->tree_array[$i]['text'], $this->current_values ) ) { |
80 | $this->tree_array[$i]['state']['selected'] = true; |
81 | } |
82 | } |
83 | } |
84 | |
85 | /** |
86 | * For the tree array that was generated from wikitext, the node doesn't know its parent |
87 | * although it's easy to know for the human. |
88 | * This function searches for the nodes and get the closest node of the parent level, and |
89 | * sets it as a parent. |
90 | * The parent ID will be used in the function setChildren() that adds every child to its |
91 | * parent's attribute "children" |
92 | */ |
93 | private function setParentsId() { |
94 | $numNodes = count( $this->tree_array ); |
95 | for ( $i = $numNodes - 1; $i >= 0; $i-- ) { |
96 | for ( $j = $i; $j >= 0; $j-- ) { |
97 | if ( $this->tree_array[$i]['level'] - $this->tree_array[$j]['level'] == 1 ) { |
98 | $this->tree_array[$i]['parent_id'] = $this->tree_array[$j]['node_id']; |
99 | break; |
100 | } |
101 | } |
102 | } |
103 | } |
104 | |
105 | /** |
106 | * This function convert the attribute $tree_array from its so-called flat structure |
107 | * into tree-like structure, as every node has an attribute called "children" that holds |
108 | * the children of this node. |
109 | * The attribute "children" is important because it is used in the library jsTree. |
110 | */ |
111 | private function setChildren() { |
112 | for ( $i = count( $this->tree_array ) - 1; $i >= 0; $i-- ) { |
113 | for ( $j = $i; $j >= 0; $j-- ) { |
114 | if ( isset( $this->tree_array[$i]['parent_id'] ) ) { |
115 | if ( $this->tree_array[$i]['parent_id'] == $this->tree_array[$j]['node_id'] ) { |
116 | if ( isset( $this->tree_array[$j]['children'] ) ) { |
117 | array_unshift( $this->tree_array[$j]['children'], $this->tree_array[$i] ); |
118 | } else { |
119 | $this->tree_array[$j]['children'][] = $this->tree_array[$i]; |
120 | } |
121 | unset( $this->tree_array[$i] ); |
122 | } |
123 | } |
124 | } |
125 | } |
126 | } |
127 | |
128 | /** |
129 | * This Function takes the Top Category name as a parameter, and generate |
130 | * tree_array, which is used within the class to modify the data passed to JS. |
131 | * @param string $top_category |
132 | * @param bool $hideroot |
133 | */ |
134 | public function getFromTopCategory( $top_category, $hideroot ) { |
135 | $this->top_category = $top_category; |
136 | $this->populateChildren(); |
137 | |
138 | $this->tree_array[0]['text'] = $top_category; |
139 | if ( in_array( $top_category, $this->current_values ) ) { |
140 | $this->tree_array[0]['state']['selected'] = true; |
141 | } |
142 | $children = $this->children; |
143 | $this->tree_array[0]['level'] = 1; |
144 | $this->tree_array[0]['state']['opened'] = true; |
145 | |
146 | $children = self::addSubCategories( $children, 2, $this->depth, $this->current_values ); |
147 | |
148 | $this->tree_array[0]['children'] = $children; |
149 | |
150 | $this->current_values = self::getCurValues( $this->tree_array ); |
151 | |
152 | if ( $hideroot ) { |
153 | $this->tree_array = $this->tree_array[0]['children']; |
154 | } |
155 | } |
156 | |
157 | /** |
158 | * This function handles adding the children of the nodes in the Top Category tree. |
159 | * Also, it determines whether or not the node is selected depending on $cur_values |
160 | * @param array $children |
161 | * @param int $level |
162 | * @param int $depth |
163 | * @param array $cur_values |
164 | * @return array |
165 | */ |
166 | public static function addSubCategories( $children, $level, $depth, $cur_values ) { |
167 | $newChildren = []; |
168 | foreach ( $children as $child ) { |
169 | $is_selected = false; |
170 | if ( $cur_values !== null ) { |
171 | if ( in_array( $child->title, $cur_values ) ) { |
172 | $is_selected = true; |
173 | unset( $cur_values[ array_search( $child->title, $cur_values ) ] ); |
174 | } |
175 | } |
176 | |
177 | $newChild = [ |
178 | 'text' => $child->title, |
179 | 'level' => $level, |
180 | 'children' => self::addSubCategories( $child->children, $level + 1, $depth, $cur_values ) |
181 | ]; |
182 | $newChild['state']['opened'] = $level <= $depth; |
183 | if ( $is_selected ) { |
184 | $newChild['state']['selected'] = true; |
185 | } |
186 | $newChildren[] = $newChild; |
187 | } |
188 | return $newChildren; |
189 | } |
190 | |
191 | private static function getCurValues( $tree ) { |
192 | $cur_values = []; |
193 | foreach ( $tree as $node ) { |
194 | if ( isset( $node['state']['selected'] ) && $node['state']['selected'] ) { |
195 | $cur_values[] = $node['text']; |
196 | } |
197 | if ( isset( $node['children'] ) ) { |
198 | $children = self::getCurValues( $node['children'] ); |
199 | $cur_values = array_merge( $cur_values, $children ); |
200 | } |
201 | } |
202 | return $cur_values; |
203 | } |
204 | |
205 | /** |
206 | * Recursive function to populate a tree based on category information. |
207 | */ |
208 | private function populateChildren() { |
209 | $subcats = self::getSubcategories( $this->top_category ); |
210 | foreach ( $subcats as $subcat ) { |
211 | $childTree = new PFTree( $this->depth, $this->current_values ); |
212 | $childTree->top_category = $subcat; |
213 | $childTree->title = $subcat; |
214 | $childTree->populateChildren(); |
215 | $this->addChild( $childTree ); |
216 | } |
217 | } |
218 | |
219 | /** |
220 | * Gets all the subcategories of the passed-in category. |
221 | * |
222 | * @todo This might not belong in this class. |
223 | * |
224 | * @param string $categoryName |
225 | * @return array |
226 | */ |
227 | private static function getSubcategories( $categoryName ) { |
228 | $dbr = PFUtils::getReadDB(); |
229 | |
230 | $tables = [ 'page', 'categorylinks' ]; |
231 | $fields = [ 'page_id', 'page_namespace', 'page_title', |
232 | 'page_is_redirect', 'page_len', 'page_latest', 'cl_to', |
233 | 'cl_from' ]; |
234 | $where = []; |
235 | $joins = []; |
236 | $options = [ 'ORDER BY' => 'cl_type, cl_sortkey' ]; |
237 | |
238 | $joins['categorylinks'] = [ 'JOIN', 'cl_from = page_id' ]; |
239 | $where['cl_to'] = str_replace( ' ', '_', $categoryName ); |
240 | $options['USE INDEX']['categorylinks'] = 'cl_sortkey'; |
241 | |
242 | $tables = array_merge( $tables, [ 'category' ] ); |
243 | $fields = array_merge( $fields, [ 'cat_id', 'cat_title', 'cat_subcats', 'cat_pages', 'cat_files' ] ); |
244 | $joins['category'] = [ 'LEFT JOIN', [ 'cat_title = page_title', 'page_namespace' => NS_CATEGORY ] ]; |
245 | |
246 | $res = $dbr->select( $tables, $fields, $where, __METHOD__, $options, $joins ); |
247 | $subcats = []; |
248 | |
249 | foreach ( $res as $row ) { |
250 | $t = Title::newFromRow( $row ); |
251 | if ( $t->getNamespace() == NS_CATEGORY ) { |
252 | $subcats[] = $t->getText(); |
253 | } |
254 | } |
255 | return $subcats; |
256 | } |
257 | } |