PageRenderTime 45ms CodeModel.GetById 11ms RepoModel.GetById 1ms app.codeStats 0ms

/plugins/Annotations/AnnotationList.php

https://github.com/CodeYellowBV/piwik
PHP | 456 lines | 183 code | 41 blank | 232 comment | 25 complexity | 2ef3a264db26fa42e29abcd0ed32dc9e MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik\Plugins\Annotations;
  10. use Exception;
  11. use Piwik\Date;
  12. use Piwik\Option;
  13. use Piwik\Piwik;
  14. use Piwik\Site;
  15. /**
  16. * This class can be used to query & modify annotations for multiple sites
  17. * at once.
  18. *
  19. * Example use:
  20. * $annotations = new AnnotationList($idSites = "1,2,5");
  21. * $annotation = $annotations->get($idSite = 1, $idNote = 4);
  22. * // do stuff w/ annotation
  23. * $annotations->update($idSite = 2, $idNote = 4, $note = "This is the new text.");
  24. * $annotations->save($idSite);
  25. *
  26. * Note: There is a concurrency issue w/ this code. If two users try to save
  27. * an annotation for the same site, it's possible one of their changes will
  28. * never get made (as it will be overwritten by the other's).
  29. *
  30. */
  31. class AnnotationList
  32. {
  33. const ANNOTATION_COLLECTION_OPTION_SUFFIX = '_annotations';
  34. /**
  35. * List of site IDs this instance holds annotations for.
  36. *
  37. * @var array
  38. */
  39. private $idSites;
  40. /**
  41. * Array that associates lists of annotations with site IDs.
  42. *
  43. * @var array
  44. */
  45. private $annotations;
  46. /**
  47. * Constructor. Loads annotations from the database.
  48. *
  49. * @param string|int $idSites The list of site IDs to load annotations for.
  50. */
  51. public function __construct($idSites)
  52. {
  53. $this->idSites = Site::getIdSitesFromIdSitesString($idSites);
  54. $this->annotations = $this->getAnnotationsForSite();
  55. }
  56. /**
  57. * Returns the list of site IDs this list contains annotations for.
  58. *
  59. * @return array
  60. */
  61. public function getIdSites()
  62. {
  63. return $this->idSites;
  64. }
  65. /**
  66. * Creates a new annotation for a site. This method does not perist the result.
  67. * To save the new annotation in the database, call $this->save.
  68. *
  69. * @param int $idSite The ID of the site to add an annotation to.
  70. * @param string $date The date the annotation is in reference to.
  71. * @param string $note The text of the new annotation.
  72. * @param int $starred Either 1 or 0. If 1, the new annotation has been starred,
  73. * otherwise it will start out unstarred.
  74. * @return array The added annotation.
  75. * @throws Exception if $idSite is not an ID that was supplied upon construction.
  76. */
  77. public function add($idSite, $date, $note, $starred = 0)
  78. {
  79. $this->checkIdSiteIsLoaded($idSite);
  80. $date = Date::factory($date)->toString('Y-m-d');
  81. $this->annotations[$idSite][] = self::makeAnnotation($date, $note, $starred);
  82. // get the id of the new annotation
  83. end($this->annotations[$idSite]);
  84. $newNoteId = key($this->annotations[$idSite]);
  85. return $this->get($idSite, $newNoteId);
  86. }
  87. /**
  88. * Persists the annotations list for a site, overwriting whatever exists.
  89. *
  90. * @param int $idSite The ID of the site to save annotations for.
  91. * @throws Exception if $idSite is not an ID that was supplied upon construction.
  92. */
  93. public function save($idSite)
  94. {
  95. $this->checkIdSiteIsLoaded($idSite);
  96. $optionName = self::getAnnotationCollectionOptionName($idSite);
  97. Option::set($optionName, serialize($this->annotations[$idSite]));
  98. }
  99. /**
  100. * Modifies an annotation in this instance's collection of annotations.
  101. *
  102. * Note: This method does not perist the change in the DB. The save method must
  103. * be called for that.
  104. *
  105. * @param int $idSite The ID of the site whose annotation will be updated.
  106. * @param int $idNote The ID of the note.
  107. * @param string|null $date The new date of the annotation, eg '2012-01-01'. If
  108. * null, no change is made.
  109. * @param string|null $note The new text of the annotation. If null, no change
  110. * is made.
  111. * @param int|null $starred Either 1 or 0, whether the annotation should be
  112. * starred or not. If null, no change is made.
  113. * @throws Exception if $idSite is not an ID that was supplied upon construction.
  114. * @throws Exception if $idNote does not refer to valid note for the site.
  115. */
  116. public function update($idSite, $idNote, $date = null, $note = null, $starred = null)
  117. {
  118. $this->checkIdSiteIsLoaded($idSite);
  119. $this->checkNoteExists($idSite, $idNote);
  120. $annotation =& $this->annotations[$idSite][$idNote];
  121. if ($date !== null) {
  122. $annotation['date'] = Date::factory($date)->toString('Y-m-d');
  123. }
  124. if ($note !== null) {
  125. $annotation['note'] = $note;
  126. }
  127. if ($starred !== null) {
  128. $annotation['starred'] = $starred;
  129. }
  130. }
  131. /**
  132. * Removes a note from a site's collection of annotations.
  133. *
  134. * Note: This method does not perist the change in the DB. The save method must
  135. * be called for that.
  136. *
  137. * @param int $idSite The ID of the site whose annotation will be updated.
  138. * @param int $idNote The ID of the note.
  139. * @throws Exception if $idSite is not an ID that was supplied upon construction.
  140. * @throws Exception if $idNote does not refer to valid note for the site.
  141. */
  142. public function remove($idSite, $idNote)
  143. {
  144. $this->checkIdSiteIsLoaded($idSite);
  145. $this->checkNoteExists($idSite, $idNote);
  146. unset($this->annotations[$idSite][$idNote]);
  147. }
  148. /**
  149. * Removes all notes for a single site.
  150. *
  151. * Note: This method does not perist the change in the DB. The save method must
  152. * be called for that.
  153. *
  154. * @param int $idSite The ID of the site to get an annotation for.
  155. * @throws Exception if $idSite is not an ID that was supplied upon construction.
  156. */
  157. public function removeAll($idSite)
  158. {
  159. $this->checkIdSiteIsLoaded($idSite);
  160. $this->annotations[$idSite] = array();
  161. }
  162. /**
  163. * Retrieves an annotation by ID.
  164. *
  165. * This function returns an array with the following elements:
  166. * - idNote: The ID of the annotation.
  167. * - date: The date of the annotation.
  168. * - note: The text of the annotation.
  169. * - starred: 1 or 0, whether the annotation is stared;
  170. * - user: (unless current user is anonymous) The user that created the annotation.
  171. * - canEditOrDelete: True if the user can edit/delete the annotation.
  172. *
  173. * @param int $idSite The ID of the site to get an annotation for.
  174. * @param int $idNote The ID of the note to get.
  175. * @throws Exception if $idSite is not an ID that was supplied upon construction.
  176. * @throws Exception if $idNote does not refer to valid note for the site.
  177. */
  178. public function get($idSite, $idNote)
  179. {
  180. $this->checkIdSiteIsLoaded($idSite);
  181. $this->checkNoteExists($idSite, $idNote);
  182. $annotation = $this->annotations[$idSite][$idNote];
  183. $this->augmentAnnotationData($idSite, $idNote, $annotation);
  184. return $annotation;
  185. }
  186. /**
  187. * Returns all annotations within a specific date range. The result is
  188. * an array that maps site IDs with arrays of annotations within the range.
  189. *
  190. * Note: The date range is inclusive.
  191. *
  192. * @see self::get for info on what attributes stored within annotations.
  193. *
  194. * @param Date|bool $startDate The start of the date range.
  195. * @param Date|bool $endDate The end of the date range.
  196. * @param array|bool|int|string $idSite IDs of the sites whose annotations to
  197. * search through.
  198. * @return array Array mapping site IDs with arrays of annotations, eg:
  199. * array(
  200. * '5' => array(
  201. * array(...), // annotation
  202. * array(...), // annotation
  203. * ...
  204. * ),
  205. * '6' => array(
  206. * array(...), // annotation
  207. * array(...), // annotation
  208. * ...
  209. * ),
  210. * )
  211. */
  212. public function search($startDate, $endDate, $idSite = false)
  213. {
  214. if ($idSite) {
  215. $idSites = Site::getIdSitesFromIdSitesString($idSite);
  216. } else {
  217. $idSites = array_keys($this->annotations);
  218. }
  219. // collect annotations that are within the right date range & belong to the right
  220. // site
  221. $result = array();
  222. foreach ($idSites as $idSite) {
  223. if (!isset($this->annotations[$idSite])) {
  224. continue;
  225. }
  226. foreach ($this->annotations[$idSite] as $idNote => $annotation) {
  227. if ($startDate !== false) {
  228. $annotationDate = Date::factory($annotation['date']);
  229. if ($annotationDate->getTimestamp() < $startDate->getTimestamp()
  230. || $annotationDate->getTimestamp() > $endDate->getTimestamp()
  231. ) {
  232. continue;
  233. }
  234. }
  235. $this->augmentAnnotationData($idSite, $idNote, $annotation);
  236. $result[$idSite][] = $annotation;
  237. }
  238. // sort by annotation date
  239. if (!empty($result[$idSite])) {
  240. uasort($result[$idSite], array($this, 'compareAnnotationDate'));
  241. }
  242. }
  243. return $result;
  244. }
  245. /**
  246. * Counts annotations & starred annotations within a date range and returns
  247. * the counts. The date range includes the start date, but not the end date.
  248. *
  249. * @param int $idSite The ID of the site to count annotations for.
  250. * @param string|false $startDate The start date of the range or false if no
  251. * range check is desired.
  252. * @param string|false $endDate The end date of the range or false if no
  253. * range check is desired.
  254. * @return array eg, array('count' => 5, 'starred' => 2)
  255. */
  256. public function count($idSite, $startDate, $endDate)
  257. {
  258. $this->checkIdSiteIsLoaded($idSite);
  259. // search includes end date, and count should not, so subtract one from the timestamp
  260. $annotations = $this->search($startDate, Date::factory($endDate->getTimestamp() - 1));
  261. // count the annotations
  262. $count = $starred = 0;
  263. if (!empty($annotations[$idSite])) {
  264. $count = count($annotations[$idSite]);
  265. foreach ($annotations[$idSite] as $annotation) {
  266. if ($annotation['starred']) {
  267. ++$starred;
  268. }
  269. }
  270. }
  271. return array('count' => $count, 'starred' => $starred);
  272. }
  273. /**
  274. * Utility function. Creates a new annotation.
  275. *
  276. * @param string $date
  277. * @param string $note
  278. * @param int $starred
  279. * @return array
  280. */
  281. private function makeAnnotation($date, $note, $starred = 0)
  282. {
  283. return array('date' => $date,
  284. 'note' => $note,
  285. 'starred' => (int)$starred,
  286. 'user' => Piwik::getCurrentUserLogin());
  287. }
  288. /**
  289. * Retrieves annotations from the database for the sites supplied to the
  290. * constructor.
  291. *
  292. * @return array Lists of annotations mapped by site ID.
  293. */
  294. private function getAnnotationsForSite()
  295. {
  296. $result = array();
  297. foreach ($this->idSites as $id) {
  298. $optionName = self::getAnnotationCollectionOptionName($id);
  299. $serialized = Option::get($optionName);
  300. if ($serialized !== false) {
  301. $result[$id] = @unserialize($serialized);
  302. if(empty($result[$id])) {
  303. // in case unserialize failed
  304. $result[$id] = array();
  305. }
  306. } else {
  307. $result[$id] = array();
  308. }
  309. }
  310. return $result;
  311. }
  312. /**
  313. * Utility function that checks if a site ID was supplied and if not,
  314. * throws an exception.
  315. *
  316. * We can only modify/read annotations for sites that we've actually
  317. * loaded the annotations for.
  318. *
  319. * @param int $idSite
  320. * @throws Exception
  321. */
  322. private function checkIdSiteIsLoaded($idSite)
  323. {
  324. if (!in_array($idSite, $this->idSites)) {
  325. throw new Exception("This AnnotationList was not initialized with idSite '$idSite'.");
  326. }
  327. }
  328. /**
  329. * Utility function that checks if a note exists for a site, and if not,
  330. * throws an exception.
  331. *
  332. * @param int $idSite
  333. * @param int $idNote
  334. * @throws Exception
  335. */
  336. private function checkNoteExists($idSite, $idNote)
  337. {
  338. if (empty($this->annotations[$idSite][$idNote])) {
  339. throw new Exception("There is no note with id '$idNote' for site with id '$idSite'.");
  340. }
  341. }
  342. /**
  343. * Returns true if the current user can modify or delete a specific annotation.
  344. *
  345. * A user can modify/delete a note if the user has admin access for the site OR
  346. * the user has view access, is not the anonymous user and is the user that
  347. * created the note in question.
  348. *
  349. * @param int $idSite The site ID the annotation belongs to.
  350. * @param array $annotation The annotation.
  351. * @return bool
  352. */
  353. public static function canUserModifyOrDelete($idSite, $annotation)
  354. {
  355. // user can save if user is admin or if has view access, is not anonymous & is user who wrote note
  356. $canEdit = Piwik::isUserHasAdminAccess($idSite)
  357. || (!Piwik::isUserIsAnonymous()
  358. && Piwik::getCurrentUserLogin() == $annotation['user']);
  359. return $canEdit;
  360. }
  361. /**
  362. * Adds extra data to an annotation, including the annotation's ID and whether
  363. * the current user can edit or delete it.
  364. *
  365. * Also, if the current user is anonymous, the user attribute is removed.
  366. *
  367. * @param int $idSite
  368. * @param int $idNote
  369. * @param array $annotation
  370. */
  371. private function augmentAnnotationData($idSite, $idNote, &$annotation)
  372. {
  373. $annotation['idNote'] = $idNote;
  374. $annotation['canEditOrDelete'] = self::canUserModifyOrDelete($idSite, $annotation);
  375. // we don't supply user info if the current user is anonymous
  376. if (Piwik::isUserIsAnonymous()) {
  377. unset($annotation['user']);
  378. }
  379. }
  380. /**
  381. * Utility function that compares two annotations.
  382. *
  383. * @param array $lhs An annotation.
  384. * @param array $rhs An annotation.
  385. * @return int -1, 0 or 1
  386. */
  387. public function compareAnnotationDate($lhs, $rhs)
  388. {
  389. if ($lhs['date'] == $rhs['date']) {
  390. return $lhs['idNote'] <= $rhs['idNote'] ? -1 : 1;
  391. }
  392. return $lhs['date'] < $rhs['date'] ? -1 : 1; // string comparison works because date format should be YYYY-MM-DD
  393. }
  394. /**
  395. * Returns true if the current user can add notes for a specific site.
  396. *
  397. * @param int $idSite The site to add notes to.
  398. * @return bool
  399. */
  400. public static function canUserAddNotesFor($idSite)
  401. {
  402. return Piwik::isUserHasViewAccess($idSite)
  403. && !Piwik::isUserIsAnonymous($idSite);
  404. }
  405. /**
  406. * Returns the option name used to store annotations for a site.
  407. *
  408. * @param int $idSite The site ID.
  409. * @return string
  410. */
  411. public static function getAnnotationCollectionOptionName($idSite)
  412. {
  413. return $idSite . self::ANNOTATION_COLLECTION_OPTION_SUFFIX;
  414. }
  415. }